Canvas怎么实现二娃翠花回家之路小游戏

发布时间:2023-05-08 15:50:00 作者:iii
来源:亿速云 阅读:92

这篇文章主要介绍了Canvas怎么实现二娃翠花回家之路小游戏的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇Canvas怎么实现二娃翠花回家之路小游戏文章都会有所收获,下面我们一起来看看吧。

一、玩法介绍

Canvas是HTML5中的一个非常有用的技术,它可以用于实现各种图形化效果。本文将介绍使用Canvas实现的小游戏——“二娃翠花回家之路”。这个小游戏非常有趣,玩家需要通过绘制角色的行走路线来控制他们的行动,并避免他们相撞。

二、预览效果

Canvas怎么实现二娃翠花回家之路小游戏

三、开发难点

在实现“二娃翠花回家之路”小游戏的过程中,我遇到了如下几个技术难点:如何绘制路径不被页面刷新影响、如何计算两条路线交叉点最相近坐标距离、如何判断碰撞、如何计算人物的移动速度和步长。

Canvas怎么实现二娃翠花回家之路小游戏

???? 划重点: 针对人物移动速度和步长计算。我的实现方案虽然可以得到两个人物的各自独立的移动速度,但是仍然无法保证人物在路径交叉点位置碰撞,这里我暂时没有好的解决方案,希望各位掘友读者们,能把代码fork过去,帮忙解决这个问题,后在评论区,附上你的解决方案。

四、核心实现步骤

1、创建画布和按键元素

使用HTML和JavaScript来创建了一个画布和两个按键元素。首先创建了一个HTML文件,然后在其中添加了一个画布和两个按键。然后使用JavaScript来获取画布和按键元素,并设置了它们的属性和事件监听器。最后为画布创建了一个绘图环境,并在画布上绘制了两个人物和他们的家。代码实现在本文第四部分。

2、创建人物类

创建一个人物类character,并在其中实现绘制角色draw()、绘制家drawHouse()、计算路径总长度calculatePathLength(def_path)这里def_path是为了后面找到两条路线交叉点最相近坐标到各自路线起点的路线备用的。计算两个坐标的距离distance(x1,y1,x2,y2)、人物移动move()等方法。通过这些方法,我们可以实现人物的移动和路径的划分。在实现路径划分时,我们可以通过计算路径总长度,将路径划分成若干个点,使角色在这些点之间移动。代码实现在本文第四部分。

3、创建两个人物实例

创建两个人物实例,并设置它们的属性和方法。我们将为每个人物实例设置起点、终点和当前位置,姓名和颜色属性。在移动时判断碰撞。

const A_Axis = [10, canvas.height - 20, canvas.width - 55, 10];
const B_Axis = [canvas.width - 10, canvas.height - 20, 5, 10];
const A = new Character('(红)二娃', ...A_Axis, 'red');
const B = new Character('(蓝)翠花', ...B_Axis, 'blue');

4、监听鼠标事件

在 Canvas 元素上监听鼠标事件,并根据鼠标的移动轨迹来绘制路径。我们将使用鼠标事件监听器来获取鼠标的坐标,并使用 Canvas API 来绘制路径。在绘制路线时需要保存路径的坐标,以便于后续的操作。

//在鼠标事件中会调用该方法绘制路径
function drawPath(path, color, width) {
  ctx.beginPath();
  ctx.moveTo(path[0].x, path[0].y);
  for (let i = 1; i < path.length; i++) {
    ctx.lineTo(path[i].x, path[i].y);
  }
  ctx.strokeStyle = color;
  ctx.lineWidth = width;
  ctx.lineCap = 'round';
  ctx.stroke();
  ctx.closePath()
}
//事件监听
canvas.addEventListener('mousedown', handleMouseDown);
canvas.addEventListener('mousemove', handleMouseMove);
canvas.addEventListener('mouseup', handleMouseUp);
resetBtn.addEventListener('click', resetGame);
startBtn.addEventListener('click', startGame);

5、绘制路径

根据绘制好的路径,将路径按照一定的长度划分成若干个点。这些点可以作为人物移动的目标位置。然后,我们可以在每个点上计算出人物应该移动的目标位置,从而实现人物的移动。

在 Canvas 中,可以使用 moveTolineTo 方法来绘制路径。使用 stroke 方法来绘制路径。可以设定 lineWidthstrokeStyle 属性来设置路径的颜色和宽度。

算法部分,可以使用距离阈值来判断两个点之间的距离是否超过了阈值。如果超过了阈值,我们就将路径划分成两部分,分别计算出每个部分的长度。然后,选择较短的路径作为人物移动的路径。这样可以避免人物走过太多的弯路,从而增加游戏的流畅度。

//核心部分
//......
//......
if (var_distance &gt; DISTANCE_THRESHOLD) {
   this.x += dx / var_distance * speed;
   this.y += dy / var_distance * speed;
} else {
   this.x = target.x;
   this.y = target.y;
   this.path.shift();
   //......
   //......
 }

6、判断碰撞

在角色行走时,判断角色与另一个角色的距离是否小于一定的阈值。如果小于阈值,则需要根据一定的几率避免碰撞,或者直接暂停游戏并提示失败。需要考虑多个角色之间的相互作用。

//核心部分
//......
if (this.path.length > 0 && this.distance(target.x, target.y, this === A ? B.x : A.x, this === A ? B.y : A.y) < COLLISION_THRESHOLD) {
   const avoidCollision = Math.random() < COLLISION_AVOIDANCE_RATE; // 以一定的几率避免碰撞
   if (avoidCollision) {
       this.path.splice(0, 1); // 直接移动到下个点位
    } else {
       gameStatus = GAME_PAUSE_STATUS;
       alert(`${this === A ? A.uname : B.uname} 碰到了对方,游戏失败`);
    }
 }

7、开始和重置游戏

实现开始和重置游戏的功能,包括重置路径、重置人物位置等。当游戏开始时,需要计算两个人物最短的路径,并将其保存到对应的路径数组中。当游戏结束时,将人物位置重置,并清空路径数组和绘制的路径。

function resetGame() {
  A.x = A_Axis[0];
  A.y = A_Axis[1];
  A.path = [];
  A.moving = false;
  A.total_distance = 0;
  B.x = B_Axis[0];
  B.y = B_Axis[1];
  B.path = [];
  B.moving = false;
  B.total_distance = 0;
  drawing = false;
  path = [];
  gameStatus = GAME_LOOPING_STATUS;
  init();
}
function startGame() {
  update();
  if (!A.path.length || !B.path.length) {
    alert('请先绘制人物回家路线');
    return;
  }
  let close_coord = getClosestCoords(A.path, B.path);
  console.log('得出的路径:', ...close_coord)
  let A_def_path = getRoute(close_coord[0], A.path);
  let B_def_path = getRoute(close_coord[1], B.path);
  let A_def_path_len = A.calculatePathLength(A_def_path);
  let B_def_path_len = B.calculatePathLength(B_def_path);
  let A_B_def_path_max_len = Math.max(A_def_path_len, B_def_path_len);
  console.log('最长的是', A_def_path_len, B_def_path_len, '---&gt;', A_B_def_path_max_len)
  A.total_distance = A_def_path_len;
  B.total_distance = B_def_path_len;
  drawing = false;
  path = [];
  A.moving = true;
  B.moving = true;
}

8、实现动画效果

动画效果的实现主要是通过 requestAnimationFrame 方法来实现的。requestAnimationFrame 是一个用来优化动画效果的方法,可以让动画更流畅自然。具体地,requestAnimationFrame 方法会在下一帧动画之前调用一个回调函数,以便于更新动画效果。在这个回调函数中,可以实现人物的移动、路径的绘制等等,从而达到动画效果。

动画效果的实现主要在人物类(Character)中。每个人物都有自己的动画状态和动画参数,包括位置、速度、目标位置等等。在每一帧的动画中,都会根据当前位置和目标位置之间的距离来计算移动的距离和速度,并且不断更新人物的位置和状态。这个过程中,使用了一些基本的数学计算,比如计算两点之间的距离、计算两点之间的角度等等。

function update() {
  if (!A.path.length && !B.path.length) {
    gameStatus = GAME_PAUSE_STATUS;
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    A.drawHouse();
    B.drawHouse();
    A.draw(true);
    B.draw(true);
  }
  if (gameStatus === GAME_LOOPING_STATUS) {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    A.drawHouse();
    B.drawHouse();
    A.draw();
    B.draw();
    A.path.length && drawPath(A.path, DRAW_LINE_COLOR, DRAW_LINE_WIDTH);
    B.path.length && drawPath(B.path, DRAW_LINE_COLOR, DRAW_LINE_WIDTH);
    A.moving && A.move();
    B.moving && B.move();
  }
  requestId = requestAnimationFrame(update);
}

五、完整的代码实现

下面是实现该游戏的完整代码,这里着重强调几个关键函数,在代码中找到对应的**注释**这跟我在上面文章第二部分划重点的提问有关哦!

主要的涉及的逻辑是,每个人物类有一个属性total_distance用于存储距离,当用户点击开始回家按钮后,在startGame方法里会计算两个人物回家路径最近的两个坐标点,并获得这两个坐标所在路径到所在路径起点的路径坐标数组,然后计算该新路径的长度保存到total_distance中,在人物移动方法move()中会计算人物移动速度和人物移动步长。

Canvas怎么实现二娃翠花回家之路小游戏

1. 页面布局

页面的布局相对简单,基本就是一个画布+按钮。当然想要游戏体验更好,可以更改页面布局,增加更多互动元素。

<!DOCTYPE html>
<html>
  <head>
    <title>二娃、翠花的回家之路</title>
  </head>
  <body>
    <h2>二娃和翠花的回家之路</h2>
    <div class="tool_bar">
      <span>红方:二娃</span>
      <span>蓝方:翠花</span>
      <div>
        攻略:鼠标绘制二娃、翠花的回家之路,不要让他们相撞哦!<br />他们回家的速度看心情哦
      </div>
    </div>
    <canvas id="canvas" width="600" height="500"></canvas>
    <div>
      <button id="startBtn">开始回家</button>
      <button id="resetBtn">重新开始</button>
    </div>
    <script src="game.js"></script>
  </body>
</html>

2. 页面css样式

样式的话,相信不用讲太多,大家一看就知道了。

body {
        text-align: center;
      }
      canvas {
        border: 1px solid gray;
        border-radius: 5px;
        box-shadow: 0 0 20px 0px #ccc;
        margin: 10px 0 5px 0px;
      }
      #startBtn,
      #resetBtn {
        background-color: #4caf50;
        border: none;
        color: white;
        padding: 15px 32px;
        text-align: center;
        text-decoration: none;
        display: inline-block;
        font-size: 16px;
        margin: 4px 2px;
        cursor: pointer;
      }
      #resetBtn {
        background-color: red;
      }
      .tool_bar span {
        background-color: blue;
        color: white;
        padding: 2px 3px;
        margin: 0 5px;
        border-radius: 5px;
      }
      .tool_bar span:first-child {
        background-color: red;
      }

3. js代码

这部分代码着实有点长,需要点耐心来阅读,利用代码中的注释或者变量和方法名称来辅助理解。本来不想贴完整代码的,但是,为了保证大家能够更好的理解这个游戏,还是贴上来吧,相信可以第一时间阅读完整代码的感觉还是挺好的????。

// 游戏状态
const GAME_LOOPING_STATUS = 'looping';
const GAME_PAUSE_STATUS = 'pause';
// 绘制路线的颜色和宽度
const DRAW_LINE_COLOR = 'deepskyblue';
const DRAW_LINE_WIDTH = 8;
// 碰撞的阈值
const COLLISION_THRESHOLD = 30;
// 碰撞避免的概率
const COLLISION_AVOIDANCE_RATE = 0.001;
// 路径点之间的距离阈值
const DISTANCE_THRESHOLD = 1.5;
let requestId = null,//动画控制句柄
//画布、按元素节点
const canvas = document.querySelector('#canvas');
const ctx = canvas.getContext('2d');
const startBtn = document.querySelector('#startBtn');
const resetBtn = document.querySelector('#resetBtn');
//人物类
class Character {
  path = [];
  oving = false;
  total_distance = 0;
  moveTime = 500;
  constructor(uname, x, y, houseX, houseY, color) {
    this.uname = uname;
    this.x = x;
    this.y = y;
    this.houseX = houseX;
    this.houseY = houseY;
    this.color = color;
  }
  //绘制角色
  draw(isInit = false) {
    ctx.beginPath();
    let h = isInit ? 20 : Math.random() * 30;
    let w = isInit ? 10 : Math.random() * 15;
    ctx.moveTo(this.x, this.y);
    ctx.lineTo(this.x - w, this.y + h);
    ctx.lineTo(this.x + w, this.y + h);
    ctx.closePath();
    ctx.fillStyle = this.color;
    ctx.fill();
    ctx.closePath()
  }
	//绘制角色的家
  drawHouse() {
    ctx.beginPath();
    ctx.arc(this.houseX + 25, this.houseY + 25, 25, 0, 2 * Math.PI);
    ctx.fillStyle = 'white';
    ctx.fill();
    ctx.strokeStyle = this.color;
    ctx.lineWidth = 5;
    ctx.stroke();
    ctx.closePath()
  }
  // 计算路径总长度
  calculatePathLength(def_path) {
    let path = this.path
    if (Array.isArray(def_path) && def_path.length > 0) {
      path = def_path
    }
    let length = 0;
    for (let i = 1; i < path.length; i++) {
      length += this.distance(path[i].x, path[i].y, path[i - 1].x, path[i - 1].y);
    }
    return length;
  }
  //计算两个坐标的距离
  distance(x1, y1, x2, y2) {
    const dx = x1 - x2;
    const dy = y1 - y2;
    return Math.sqrt(dx * dx + dy * dy);
  }
  //人物移动
  move() {
    if (this.path.length === 0) {
      this.moving = false;
      return;
    }
    const target = this.path[0];
    const dx = target.x - this.x;
    const dy = target.y - this.y;
    const var_distance = this.distance(target.x, target.y, this.x, this.y);
    const speed = this.total_distance / this.moveTime;
    if (var_distance > DISTANCE_THRESHOLD) {
      this.x += dx / var_distance * speed;
      this.y += dy / var_distance * speed;
    } else {
      this.x = target.x;
      this.y = target.y;
      this.path.shift();
      if (this.path.length > 0 && this.distance(target.x, target.y, this === A ? B.x : A.x, this === A ? B.y : A.y) < COLLISION_THRESHOLD) {
        const avoidCollision = Math.random() < COLLISION_AVOIDANCE_RATE; // 以一定的几率避免碰撞
        if (avoidCollision) {
          this.path.splice(0, 1); // 直接移动到下个点位
        } else {
          gameStatus = GAME_PAUSE_STATUS;
          alert(`${this === A ? A.uname : B.uname} 碰到了对方,游戏失败`);
        }
      }
    }
  }
}
const A_Axis = [10, canvas.height - 20, canvas.width - 55, 10];
const B_Axis = [canvas.width - 10, canvas.height - 20, 5, 10];
const A = new Character('(红)二娃', ...A_Axis, 'red');
const B = new Character('(蓝)翠花', ...B_Axis, 'blue');
let gameStatus = GAME_LOOPING_STATUS;
let drawing = false;
let path = [];
//绘制路径
function drawPath(path, color, width) {
  ctx.beginPath();
  ctx.moveTo(path[0].x, path[0].y);
  for (let i = 1; i < path.length; i++) {
    ctx.lineTo(path[i].x, path[i].y);
  }
  ctx.strokeStyle = color;
  ctx.lineWidth = width;
  ctx.lineCap = 'round';
  ctx.stroke();
  ctx.closePath()
}
//计算两条路径最近的两个坐标点
function getClosestCoords(arr1, arr2) {
  let minDistance = Number.MAX_VALUE;
  let closestCoords = [];
  for (let i = 0; i < arr1.length; i++) {
    for (let j = 0; j < arr2.length; j++) {
      const distance = Math.sqrt(
        Math.pow(arr1[i].x - arr2[j].x, 2) + Math.pow(arr1[i].y - arr2[j].y, 2)
      );
      if (distance < minDistance) {
        minDistance = distance;
        closestCoords = [arr1[i], arr2[j]];
      }
    }
  }
  return closestCoords;
}
//获取坐标点到路径起点的路径数组
function getRoute(coord, targetRoute) {
  let res = [];
  for (let i = 0; i < targetRoute.length; i++) {
    res.push(targetRoute[i])
    if (targetRoute[i].x === coord.x && targetRoute[i].y === coord.y) {
      return res;
    }
  }
}
//鼠标按下事件句柄
function handleMouseDown(event) {
  if (event.target.id === 'canvas') {
    if (A.distance(event.offsetX, event.offsetY, A.x, A.y) < COLLISION_THRESHOLD) {
      drawing = true;
      path.push({ x: A.x, y: A.y });
    } else if (A.distance(event.offsetX, event.offsetY, B.x, B.y) < COLLISION_THRESHOLD) {
      drawing = true;
      path.push({ x: B.x, y: B.y });
    }
  }
}
//鼠标移动事件句柄
function handleMouseMove(event) {
  if (drawing) {
    path.push({ x: event.offsetX, y: event.offsetY });
    drawPath(path, DRAW_LINE_COLOR, DRAW_LINE_WIDTH);
  }
}
//鼠标松开事件句柄
function handleMouseUp(event) {
  if (drawing) {
    if (A.distance(event.offsetX, event.offsetY, A.houseX + 25, A.houseY + 25) < 35) {
      path.push({ x: A.houseX + 25, y: A.houseY + 25 });
      drawPath(path, DRAW_LINE_COLOR, DRAW_LINE_WIDTH);
      A.path = path;
    } else if (A.distance(event.offsetX, event.offsetY, B.houseX + 25, B.houseY + 25) < 35) {
      path.push({ x: B.houseX + 25, y: B.houseY + 25 });
      drawPath(path, DRAW_LINE_COLOR, DRAW_LINE_WIDTH);
      B.path = path;
    }
    path = [];
    drawing = false;
  }
}
//重置游戏
function resetGame() {
  A.x = A_Axis[0];
  A.y = A_Axis[1];
  A.path = [];
  A.moving = false;
  A.total_distance = 0;
  B.x = B_Axis[0];
  B.y = B_Axis[1];
  B.path = [];
  B.moving = false;
  B.total_distance = 0;
  drawing = false;
  path = [];
  gameStatus = GAME_LOOPING_STATUS;
  init();
}
//开始游戏
function startGame() {
  update();
  if (!A.path.length || !B.path.length) {
    alert('请先绘制人物回家路线');
    return;
  }
  let close_coord = getClosestCoords(A.path, B.path);
  console.log('得出的路径:', ...close_coord)
  let A_def_path = getRoute(close_coord[0], A.path);
  let B_def_path = getRoute(close_coord[1], B.path);
  let A_def_path_len = A.calculatePathLength(A_def_path);
  let B_def_path_len = B.calculatePathLength(B_def_path);
  let A_B_def_path_max_len = Math.max(A_def_path_len, B_def_path_len);
  console.log('最长的是', A_def_path_len, B_def_path_len, '--->', A_B_def_path_max_len)
  A.total_distance = A_def_path_len;
  B.total_distance = B_def_path_len;
  drawing = false;
  path = [];
  A.moving = true;
  B.moving = true;
}
//刷新游戏界面
function update() {
  if (!A.path.length && !B.path.length) {
    gameStatus = GAME_PAUSE_STATUS;
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    A.drawHouse();
    B.drawHouse();
    A.draw(true);
    B.draw(true);
  }
  if (gameStatus === GAME_LOOPING_STATUS) {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    A.drawHouse();
    B.drawHouse();
    A.draw();
    B.draw();
    A.path.length && drawPath(A.path, DRAW_LINE_COLOR, DRAW_LINE_WIDTH);
    B.path.length && drawPath(B.path, DRAW_LINE_COLOR, DRAW_LINE_WIDTH);
    A.moving && A.move();
    B.moving && B.move();
  }
  requestId = requestAnimationFrame(update);
}
//初始化
function init() {
  window.cancelAnimationFrame(requestId);
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  A.drawHouse();
  B.drawHouse();
  A.draw(true);
  B.draw(true);
}
//事件监听
canvas.addEventListener('mousedown', handleMouseDown);
canvas.addEventListener('mousemove', handleMouseMove);
canvas.addEventListener('mouseup', handleMouseUp);
resetBtn.addEventListener('click', resetGame);
startBtn.addEventListener('click', startGame);
init();

关于“Canvas怎么实现二娃翠花回家之路小游戏”这篇文章的内容就介绍到这里,感谢各位的阅读!相信大家对“Canvas怎么实现二娃翠花回家之路小游戏”知识都有一定的了解,大家如果还想学习更多知识,欢迎关注亿速云行业资讯频道。

推荐阅读:
  1. HTML5 canvas怎么绘制酷炫能量线条效果
  2. javascript中的canvas方法有哪些

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

canvas

上一篇:springboot指定profiles启动失败问题如何解决

下一篇:Spring单元测试控制Bean注入的方法是什么

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》