2D 游戏中,我们经常需要控制主角进行八方向移动;如何利用 WSAD 键来实现呢?核心要点分两块,键盘事件处理
节点8方向移动
。
键盘事件
处理后主要要输出一个角度
,这个角度决定了节点的移动方向
和视觉朝向(角度)
先看看运行效果(搞清楚要做什么,目标是什么)
键盘事件
为了把按键转换为角度,首先我们需要捕获按键,同一时间可能有多个按键,但对于移动方向这个事来讲,我们只关注方向键且最多只关注两个按键。如果有其他的攻击键、技能键等,我们可以在其它键盘处理脚本中处理或者在其他函数中处理。基于这个,我们先定义出各个方向的移动角度
,这里的移动角度
就是单纯指整个节点在地图世界中的移动角度,与节点的具体具体视角朝向角度无关;这里一定要区分清楚。这个角度是基于 2D 坐标系来计算的。 左边就是 0,上面是 90,右边是 180,下面是 270.
如下图角度 α 就是这个角度
基于上面的我们先制定出按键和角度的映射表,这里有个小技巧:对于重复 AW 和 WA 这种组合我们不关心按键先后顺序,角度都是一样的,所以这里我们可以直接给两个按键的 keyCode 做乘法,得到相同的值,就能映射相同的角度了。
typescript
const MoveDirectionConfig = {
[KeyCode.KEY_D]: { speed: 1, angle: 0 },
[KeyCode.KEY_W]: { speed: 1, angle: 90 },
[KeyCode.KEY_A]: { speed: -1, angle: 180 },
[KeyCode.KEY_S]: { speed: -1, angle: 270 },
[KeyCode.KEY_W * KeyCode.KEY_S]: { speed: 1, angle: 90 },
[KeyCode.KEY_A * KeyCode.KEY_D]: { speed: 1, angle: 0 },
[KeyCode.KEY_A * KeyCode.KEY_W]: { speed: 1, angle: 135 },
[KeyCode.KEY_A * KeyCode.KEY_S]: { speed: -1, angle: 225 },
[KeyCode.KEY_D * KeyCode.KEY_S]: { speed: -1, angle: 315 },
[KeyCode.KEY_D * KeyCode.KEY_W]: { speed: 1, angle: 45 },
0: { speed: 0, angle: 0 },
};
接下来是按键处理代码,按下之后如果是方向键则加入 keys 数组;抬起则去除 keys 数组;每次有按键发生变化后,更新移动方向
typescript
isMoveDirectionKey(key: KeyCode) {
return key == KeyCode.KEY_W || key == KeyCode.KEY_S || key == KeyCode.KEY_A || key == KeyCode.KEY_D;
}
onKeyDown(event: EventKeyboard) {
Log('onKeyDown', event.keyCode);
if (this.isMoveDirectionKey(event.keyCode)) {
const isExist = this.keys.findIndex(key => event.keyCode == key) != -1;
!isExist && this.keys.push(event.keyCode);
this.updateMoveDirection();
}
}
onKeyUp(event: EventKeyboard) {
Log('onKeyUp', event.keyCode);
if (this.isMoveDirectionKey(event.keyCode)) {
const index = this.keys.findIndex(key => event.keyCode == key);
index != -1 && this.keys.splice(index, 1);
this.updateMoveDirection();
}
}
// 这里也有一些预判技巧,key2存在的话,key1必定存在
updateMoveDirection() {
const keyCode1 = this.keys[this.keys.length - 1];
const keyCode2 = this.keys[this.keys.length - 2];
let direction = 0;
keyCode1 && (direction = keyCode1);
keyCode2 && (direction = keyCode2 * keyCode1);
this.directionConfig = MoveDirectionConfig[direction];
}
节点移动及旋转
ok,通过上面的按键处理,我们能得到一个移动角度
;接下来我们实现节点朝着指定方向移动
和节点的朝向处理
typescript
protected update(dt: number): void {
this.movePlayer();
}
movePlayer() {
if(this.directionConfig.speed == 0) return;
this.node.angle = this.directionConfig.angle -90;
const angle = this.directionConfig.angle / 180 * Math.PI;
this.moveDirection.set(Math.cos(angle), Math.sin(angle));
this.moveDirection.normalize();
Vec2.multiplyScalar(TempV2, this.moveDirection, this.speed);
let pos = this.node.position;
this.node.setPosition(pos.x + TempV2.x, pos.y + TempV2.y, pos.z);
}
基于上面的代码,「真菜鸟」有如下的两个疑问,我们问问 gpt 看看它怎么说
- 1.this.moveDirection 的默认值是 v2(1,0)是什么意思;为什么不是(1,1);moveDirection 这里为什么要做成类的属性, 可以用局部变量吗代替吗?moveDirection 的 set 操作之后,又进行了 normalize 操作这个又是什么意思
- 2.上面的 TempV2 是一个全局变量,为什么不用局部变量呢,multiplyScalar 又是什么意思,看最后的结果像是一个偏移量,但 multi 不更像是乘的意思吗;这里是谁和谁相乘,是 moveDirection 乘以 speed,算出一个实际 x,y 方向的偏移量吗
对于第一个问题的分析:
-
this.moveDirection
的默认值v2(1,0)
:v2(1,0)
表示一个向右的单位向量。在二维空间中,(1,0) 通常表示完全向右的方向,这是二维坐标系中的标准基向量之一。选择 (1,0) 而不是 (1,1) 或其他值,是因为这代表一个清晰、无歧义的初始方向,通常用于初始化移动方向。- 为什么不是 (1,1):(1,1) 表示向右上方移动的方向,这是一个特定的对角方向。在不清楚初始移动方向需求的情况下,使用 (1,0) 更为中性,代表水平向右,不包含垂直分量。
-
moveDirection
作为类的属性:moveDirection
作为类的属性可以保留每次更新后的方向状态,这样不需要每次调用movePlayer
时都重新计算方向。如果用局部变量代替,每次调用时都需要重新计算,这可能会导致计算上的重复和效率低下。同时,作为类属性也便于在类的其他方法中访问和修改这个方向状态。
-
.set()
和.normalize()
:.set()
方法用于更新Vec2
对象的 x 和 y 值。这里用它来设置新的方向向量。.normalize()
是将向量变成单位向量的操作,即保持方向不变,但长度变为 1。这很重要,因为在移动计算中,你通常希望独立控制方向和速度。方向由向量的方向决定,速度由其长度决定。通过标准化,可以确保向量只表示方向,然后可以通过乘以速度(一个标量)来独立控制移动的快慢。
对于第二个问题的分析:
-
TempV2
作为全局变量:TempV2
可能被定义为全局变量(或这里的类属性)是为了避免在每次调用movePlayer
时重新创建新的Vec2
对象,这样可以减少内存分配和垃圾回收,特别是在每帧都调用时。这是一个优化手段,以减少创建大量临时对象的性能开销。
-
multiplyScalar
的意义:multiplyScalar
是一个向量操作,意味着将向量的每个分量(在这里是 x 和 y)乘以一个标量(在这里是速度)。这个操作用于根据速度调整向量的长度,即实际移动的距离。- 在这里,
multiplyScalar
用于计算实际的移动偏移量。它将单位方向向量(this.moveDirection
)乘以速度(this.speed
),结果是一个表示每一帧移动量的向量。这个向量的 x 和 y 分量表明了沿 x 轴和 y 轴移动的距离。
综上所述,this.moveDirection
作为一个初始化向右的单位向量,通过类属性形式保持其状态,并通过 .normalize()
确保其始终为单位向量,使得方向控制既高效又可靠。而 TempV2
作为一个全局变量和 multiplyScalar
的使用是为了在计算过程中提高性能和明确每步移动的实际距离。
继续追问
- 追问问题 1-1,v2(1,0)是不是也可以写成 v2(1,1)呢,会有影响吗?
- 追问问题 1-2,moveDirection 在 movePlayer 里每次都会重新计算,你说的存储计算结果不符合;是不是说写成局部变量也是没问题的,只不过局部变量会创建大量的临时变量,因为这块最终要在 update 里调用。优化效果类似于 TempV2 作全局变量。
您的追问非常有见地,让我们逐一解答:
追问问题 1-1: v2(1,0)
是否可以替换为 v2(1,1)
?
- 可以替换 :理论上,
this.moveDirection
的初始值可以设为任意向量,包括v2(1,1)
。 - 影响分析 :
v2(1,1)
会将初始移动方向设置为右上方(45 度角)。如果没有立即被updateMoveDirection
更新,它可能会短暂地影响移动方向直到第一次键盘输入。在某些游戏或应用场景中,这种初始方向可能导致非预期的行为,特别是如果这个方向在游戏开始时就被用来移动节点。因此,选择(1,0)
作为默认值通常是出于清晰和有意义的初始状态考虑。 - 适用性:选择哪个初始值取决于游戏的具体要求和设计理念。如果游戏刚开始没有立即需要方向或者你希望有一个特定的初始方向,可以根据需求更改。
追问问题 1-2: moveDirection
是否可以作为局部变量?
- 计算频率 :确实,
moveDirection
在每次movePlayer
被调用时都会重新计算。这意味着它的值不需要在多次调用之间持久化。 - 性能考量 :作为一个经常被更新的变量,将
moveDirection
作为类属性而不是局部变量,可以避免在每次更新时创建新的Vec2
实例。这和TempV2
作为全局变量的逻辑是一样的。虽然 JavaScript 引擎和现代浏览器很擅长处理这类频繁的小对象创建和垃圾回收,但在游戏开发中,特别是对于每帧更新的逻辑,保持对象分配到最低仍然是一个良好的实践,尤其是在性能敏感的环境或低端设备上。 - 可行性 :从技术上讲,你完全可以将
moveDirection
写为局部变量,如果你的游戏不那么注重优化或者更新频率不高,可能看不出明显差别。但作为一个习惯,特别是在每帧都要进行大量计算的游戏循环中,尽可能减少对象创建和垃圾回收是值得的。
总的来说,v2(1,0)
作为默认值是出于通用性和明确初始状态的考虑,而 moveDirection
作为类属性是基于性能优化的考量。如果性能和资源管理对于应用来说不是关键问题,或者更新频率不是非常高,使用局部变量也是可行的。
通用打破砂锅问到底式的深度追问,我们其实发现了一个新的点 this.moveDirection 的初始值 v2(1,0)并不是随意设置的;它是有明确含义的,代表的是向量;这个向量的方向就是 0 度移动方向也就是最上面我图中画 的 X 轴的右侧方向。1,1 的话就是 45 度了,如果有依赖初始移动方向的逻辑,就会被影响到了。
向量与标量科普
在游戏开发中,向量和标量都是非常重要的数学工具,它们被广泛应用于描述和计算空间、运动、力量等方面。
向量(Vector)
向量是有大小(长度)和方向的量,常用于表示物体的位置、速度、加速度、力等。在二维或三维空间中,向量通常有两个(2D)或三个(3D)分量,分别对应于各个坐标轴的值。
在游戏开发中,向量的常用场景包括:
- 位置和位移:用向量表示物体在游戏世界中的位置,或者它的移动位移。
- 速度和加速度:用向量表示物体的速度和加速度,向量的方向表示物体移动的方向,大小表示速度或加速度的快慢。
- 力和冲量:在物理引擎中,力和冲量通常以向量形式表示,指明作用的方向和大小。
- 朝向和旋转:用向量表示物体的朝向,或者在旋转中作为轴和角度的参考。
- 射线和碰撞检测:在进行射线投射或碰撞检测时,通常会使用向量来计算和表示射线路径、法线方向等。
标量(Scalar)
标量是只有大小没有方向的量,常用于表示长度、面积、体积、温度、质量等。在游戏开发中,标量可以用来修改向量的大小,或者作为各种计算的结果或参数。
在游戏开发中,标量的常用场景包括:
- 调整大小或速度:将向量乘以一个标量,可以改变向量的长度而不改变其方向,常用于调整速度或加速度。
- 插值和平滑:使用标量进行数值的插值,如在动画、平滑移动或渐变效果中计算中间值。
- 物理属性:质量、密度、温度等通常以标量表示。
- 测量和比较:使用标量进行长度、面积、体积等的计算和比较。
- 时间计算:游戏中的时间间隔、延迟、持续时间通常以标量秒或毫秒表示。
在游戏中,向量和标量经常一起使用来执行各种操作和计算。例如,将速度向量(向量)乘以时间间隔(标量)来计算位移,或者将力(向量)乘以质量(标量)来计算加速度。理解向量和标量及其在游戏开发中的应用对于创建物理行为、动画、控制系统等是至关重要的。