【HTML5小游戏】二. 让角色动起来吧 - 添加一个可以操作的角色

前情提要

在上一节中,我们已经实现了一个永不停歇的循环,接下来就要在这个循环里添加一个角色了,最好还是可以操作的。

先添加一个不会动的角色吧

在上一节中,通过 fillText 方法已经在画面中增加了几行文字。不过游戏角色一般都是一些图片的集合,所以这次就要用到 drawImage 函数了。
如果你对 Canvas 的这些函数不甚了解,推荐你收藏一下Canvas 教程。 善用这个网站上方的搜索功能,随时查看那些不太熟悉的方法,把这个东西当作字典放到一边吧,等遇到不会的再查就好。

我们只用最基础的用法 ctx.drawImage(image, x, y);
参数从左到右分别是:

  • JS 的 Image(HTMLImageElement) 对象(严格来说,任何的 canvas 图像源都是可用的,这里只是举个例子)
  • 绘制出的图片在画布上的 X 坐标
  • 绘制出的图片在画布上的 Y 坐标

图片已经准备好了,点击这里下载吧

推荐你在上一课中完成的 html 文件旁边新建一个叫 images 的文件夹,把这个图片放到里面,接下来课程里的文件路径,都是按照这个放置路径写的。

现在,把图片加载到 JS 里吧。

// 这段代码放到 drawImg 和 update 外面,我们不需要每帧更新的时候都创建一个 Image 对象。
const image = new Image(); // 新建一个 HTMLImageElement 对象,用于 drawImage 的第一个参数
image.src = './images/mario.png'; // 把 images/mario.png 这个图片加载到 image 对象里

这样就完成了,很简单对吧。接下来就是把这个图片绘制到画布里了,我们就直接把它放到画布正中央吧。
为了更明显的看出来角色处于画布的什么位置,这里建议先把画布换个背景色,顺便做两个辅助线

// 这段代码请放到 drawImg 里面(这部分代码不需要在这节课掌握,只是在画辅助线,方便接下来的步骤)
ctx.fillStyle = '#E6E6FA'; // 把画笔换成一个柔和的紫色,当然你也可以换成其他你觉得顺眼的颜色
ctx.fillRect(0, 0, cvWidth, cvHeight); // 画一个填满整个画布的紫色矩形作为背景色
ctx.fillStyle = '#000'; // 把画笔切换成黑色
ctx.moveTo(cvWidth / 2, 0);
ctx.lineTo(cvWidth / 2, cvHeight); 
ctx.stroke(); // 这三步是在画一条从上到下的黑色线
ctx.moveTo(0, cvHeight / 2);
ctx.lineTo(cvWidth, cvHeight / 2);
ctx.stroke(); // 这三步就是在画一条从左到右的黑色线

终于可以开始画角色了!

ctx.drawImage(image, cvWidth / 2, , cvHeight / 2); // 把角色绘制到 canvas 上

image1

额,好像和预期不一样,这个图片明显只是左上角在画布正中央。
没错,图片的坐标位置指的其实是它左上角那个像素所在的位置。
如果想要让它的中心坐标和画布的正中央重合呢?
其实也很简单,我们稍微计算一下左上角的像素需要偏移多少就可以了,结果如下:

// 这段代码请放到 drawImg 里面
ctx.drawImage(image, (cvWidth - image.width) / 2, , (cvHeight - image.height) / 2); // 修正一下坐标把 image 绘制到 canvas 上

image2

终于到正中央了!

有 Canvas 基础或者认真看 Canvas 教程 的朋友可能会产生一个疑问:

文档里提到,若调用 drawImage 时,图片没装载完,其实是什么也不会发生的。因此我们应该用 load 事件来保证不会在加载完毕之前使用这个图片。
为什么我这里直接调用了,而没有先确保图片已经被装载完成呢?

其实道理很简单,我们有一个无限循环一直在调用 drawImage 来尝试绘制图片呢,也许在当前这个循环里它还没装载完成,导致绘制失败,但是在几秒后它装载完成后的循环里,它自然就会被绘制出来。

让这个角色动起来

稍等,好像走得太快了,先梳理一下这个角色在绘制时需要的数据结构吧,不然变量太松散,迟早你会忘记他们都是什么意思的。

一个简单的角色数据结构

先不使用 Class,就暂用一个简单的 Object 就可以了。

// 这段代码放到 drawImg 和 update 外面,需要注意,player 引用了 cvWidth 和 cvHeight,请确保 player 的定义在这两个变量的定义之后
const player = { // 使用 player 这个 Object 来存储绘制角色需要的信息。
  image: new Image(), // 角色的图像
  x: cvWidth / 2, // 角色的 X 坐标
  y: cvHeight / 2, // 角色的 Y 坐标
};
player.image.src = './images/mario.png';

好像暂时就已经够了,现在也改一下 drawImage 吧。

// 这段代码请放到 drawImg 里面
ctx.drawImage(player.image, player.x - player.image.width / 2, player.y - player.image.height / 2); // 只是把之前的变量替换成了 player 里的变量,没有任何区别

有了 player,代码可读性也就稍微高了一些了。现在只需要修改 player 内的 x 或者 y,就可以让角色的位置发生变化了。

监听并且记录键盘的输入

为了能让角色相应用户的操作,我们也需要让游戏知道用户都按下了什么键
下面的代码就是通过监听 keydown 和 keyup 两个事件,来将用户按下的按钮记录到 keysDown 变量里。

// 这段代码放到 drawImg 和 update 外面
const keysDown = {}; // 存储用户当前按下的按键
addEventListener("keydown", function (e) {
  keysDown[e.code] = true; // 当用户按下按键,就把这个按键的 code 放到 keysDown 里面
});
addEventListener("keyup", function (e) {
  delete keysDown[e.code]; // 当用户松开按键,就把这个按键的 code 从 kyesDown 里移除
});

根据记录的输入内容,改变角色的坐标

// 这段代码属于游戏逻辑,放到 update 里面吧
// 我们会使用 W S A D 来作为控制角色上下左右的操作按键
if ('KeyA' in keysDown) { // 当按键 A 被按下,就把角色的坐标向左移动
  player.x += -1; // 暂时先每个循环移动 1 像素,这里是有个坑在的。上一期中提到,循环的间隔时间其实是不固定的,这里写每个循环移动 1个像素,意味着角色移动的速度也是不固定的,我们会在下一节课修复这个问题。
}
if ('KeyD' in keysDown) { // 当按键 A 被按下,就把角色的坐标向左移动
  player.x += 1;
}
if ('KeyW' in keysDown) { // 当按键 A 被按下,就把角色的坐标向左移动
  player.y = 1;
}
if ('KeyS' in keysDown) { // 当按键 A 被按下,就把角色的坐标向左移动
  player.y = -1;
}

至此,角色终于动起来了,尝试一下吧。

完整的代码点击这里下载