跳到主要内容
JavaScript 贪吃蛇游戏开发实战:移动逻辑与渲染优化 | 极客日志
JavaScript 大前端 算法
JavaScript 贪吃蛇游戏开发实战:移动逻辑与渲染优化 综述由AI生成 了基于 JavaScript 的贪吃蛇游戏核心功能实现。内容包括蛇的移动逻辑,通过速度和时间增量计算位置;状态管理区分静止、移动和死亡;键盘事件监听控制方向;碰撞检测防止撞墙或自触;以及渲染优化,包括蛇身连接绘制和眼睛跟随方向显示。代码展示了如何完善游戏循环与视觉表现。
云朵棉花糖 发布于 2026/3/23 更新于 2026/4/29 25K 浏览基本的移动
在 Snake.js 中添加代码,实现蛇头的向右移动。
import { AcGameObject } from "./AcGameObject" ;
import { Cell } from "./Cell" ;
export class Snake extends AcGameObject {
constructor (info, gamemap ) {
super ();
this .id = info.id ;
this .color = info.color ;
this .gamemap = gamemap;
this .cells = [new Cell (info.r , info.c )];
this .speed = 5 ;
}
update_move ( ) {
this .cells [0 ].x += this .speed * this .timedelta / 1000 ;
}
update ( ) {
. ();
. ();
}
}
this
update_move
this
render
this.speed:蛇的移动速度,设定为 5。这是在更新运动时用到的一个重要参数。
update_move() 方法负责更新蛇头的位置。根据当前的时间增量 this.timedelta,计算出蛇头的新位置。
在这个例子中,蛇头向右移动,this.cells[0].x 增加,移动的距离是 speed * timedelta / 1000,其中 this.timedelta 是每帧持续的时间(以毫秒为单位),将其转换为秒以计算实际移动距离。
注释掉的部分表示可以向上移动,但在当前实现中并未启用。
update() 方法是游戏的主要更新循环,会调用 update_move() 方法更新蛇的位置,并调用 render() 方法绘制当前状态。
连贯的移动 import { AcGameObject } from "./AcGameObject" ;
import { Cell } from "./Cell" ;
export class Snake extends AcGameObject {
constructor (info, gamemap ) {
super ();
this .id = info.id ;
this .color = info.color ;
this .gamemap = gamemap;
this .cells = [new Cell (info.r , info.c )];
this .speed = 5 ;
this .direction = -1 ;
this .status = "idle" ;
}
}
该属性用于表示蛇的移动方向。
初始值设为 -1,表示没有移动指令。可以使用不同的数值来表示不同的方向:
这个设计允许你在后续的代码中根据用户输入或游戏逻辑来更新蛇的移动方向。
该属性用来表示蛇的当前状态。
初始值设为 'idle',表示蛇处于静止状态。
可能的状态包括:
'idle':静止状态
'move':移动状态
'die':死亡状态
通过这一状态管理,可以在游戏逻辑中更容易地处理不同的行为(例如,只有在状态为 'move' 时才更新蛇的位置)。
check_ready() 方法的主要功能是检查游戏中的两条蛇是否准备好进入下一回合,放在 GameMap.js 中。
check_ready ( ) {
for (const snake of this .snakes ) {
if (snake.status !== "idle" ) return false ;
if (snake.direction === -1 ) return false ;
}
return true ;
}
遍历蛇的数组,检查每条蛇的状态属性 (status) 是否为 'idle'。如果有任意一条蛇的状态不是 'idle',即表示它正在移动或执行其他操作,那么方法就会返回 false,表明不可以进入下一回合。
检查蛇的方向属性 (direction) 是否等于 -1。如果任意一条蛇的方向为 -1,则同样返回 false,表示它们不能开始下一回合。
如果所有蛇都处于 'idle' 状态且没有蛇的方向为 -1,那么方法最后会返回 true,表示所有的蛇都准备好进入下一回合。
this .next_cell = null ;
this .dr = [-1 , 0 , 1 , 0 ];
this .dc = [0 , 1 , 0 , -1 ];
this .step = 0 ;
next_step ( ) {
const d = this .direction ;
this .next_cell = new Cell (this .cells [0 ].r + this .dr [d], this .cells [0 ].c + this .dc [d]);
this .direction = -1 ;
this .status = "move" ;
this .step ++;
}
next_cell 属性用来存储蛇头下一个要移动到的位置,初始值为 null。
dr 和 dc 数组分别定义了蛇在四个方向(上、右、下、左)的行和列变化。
step 属性表示蛇当前的移动步骤计数,初始值为 0,用于跟踪蛇已经移动的步数。
根据当前方向 d,利用 dr 和 dc 数组计算出蛇头下一个位置的行 (r) 和列 (c) 坐标,并创建一个新的 Cell 实例 next_cell。
将方向设置为 -1,通常用来表示蛇在等待或暂停状态。
将状态设置为 'move',表示蛇正在移动中。
增加 step 计数,每当调用 next_step() 方法时,这个计数就会加一。
next_step ( ) {
for (const snake of this .snakes ) {
snake.next_step ();
}
}
update ( ) {
this .update_size ();
if (this .check_ready ()) {
this .next_step ();
}
this .render ();
}
next_step 方法负责处理所有蛇的下一步移动。
使用 for...of 循环遍历 this.snakes 数组,对于每个 snake 对象,调用其 next_step() 方法。
update() 方法:
调用 update_size() 方法,可能用于调整游戏界面的大小或更新蛇的某些属性。
调用 check_ready() 方法来检查是否可以进行下一步移动。
如果返回值为 true,则调用 next_step() 方法,更新所有蛇的位置。
最后,调用 render() 方法负责绘制游戏的当前状态。
读取键盘的操作 在 Snake.js 中加入一个辅助函数,用来获取方向。
set_direction (d ) {
this .direction = d;
}
add_listening_events ( ) {
this .ctx .canvas .focus ();
const [snake0, snake1] = this .snakes ;
this .ctx .canvas .addEventListener ("keydown" , e => {
if (e.key === 'w' ) snake0.set_direction (0 );
else if (e.key === 'd' ) snake0.set_direction (1 );
else if (e.key === 's' ) snake0.set_direction (2 );
else if (e.key === 'a' ) snake0.set_direction (3 );
else if (e.key === 'ArrowUp' ) snake1.set_direction (0 );
else if (e.key === 'ArrowRight' ) snake1.set_direction (1 );
else if (e.key === 'ArrowDown' ) snake1.set_direction (2 );
else if (e.key === 'ArrowLeft' ) snake1.set_direction (3 );
});
}
add_listening_events() 该方法用于添加键盘事件监听器,使得游戏能够响应玩家的输入。
this.ctx.canvas.focus() 使得游戏画布(canvas)获得焦点,以便能够接收键盘事件。
假设 this.snakes 是一个包含两个蛇对象的数组,通常是两个玩家分别控制的蛇。
这部分代码根据用户按下的不同键来设置对应蛇的移动方向:
对于 snake0(第一条蛇),使用 WASD 键来控制:W 向上,D 向右,S 向下,A 向左。
对于 snake1(第二条蛇),使用箭头键来控制:ArrowUp 向上,ArrowRight 向右,ArrowDown 向下,ArrowLeft 向左。
每当按下对应的键,就会调用该蛇的 set_direction 方法,传入相应的方向值。
update ( ) {
if (this .status === 'move' ) {
this .update_move ();
}
this .render ();
}
在每一帧中进行游戏状态的更新和渲染。如果游戏状态允许移动(即状态为 'move'),则调用相应的方法处理移动逻辑。无论状态如何,都会调用 render() 方法来绘制游戏画面。
实现真正的移动 import { AcGameObject } from "./AcGameObject" ;
import { Cell } from "./Cell" ;
export class Snake extends AcGameObject {
constructor (info, gamemap ) {
super ();
this .id = info.id ;
this .color = info.color ;
this .gamemap = gamemap;
this .cells = [new Cell (info.r , info.c )];
this .next_cell = null ;
this .speed = 5 ;
this .direction = -1 ;
this .status = "idle" ;
this .dr = [-1 , 0 , 1 , 0 ];
this .dc = [0 , 1 , 0 , -1 ];
this .step = 0 ;
this .eps = 1e-2 ;
}
start ( ) {}
set_direction (d ) {
this .direction = d;
}
next_step ( ) {
const d = this .direction ;
this .next_cell = new Cell (this .cells [0 ].r + this .dr [d], this .cells [0 ].c + this .dc [d]);
this .direction = -1 ;
this .status = "move" ;
this .step ++;
const k = this .cells .length ;
for (let i = k; i > 0 ; i--) {
this .cells [i] = JSON .parse (JSON .stringify (this .cells [i - 1 ]));
}
}
update_move ( ) {
const dx = this .next_cell .x - this .cells [0 ].x ;
const dy = this .next_cell .y - this .cells [0 ].y ;
const distance = Math .sqrt (dx * dx + dy * dy);
if (distance < this .eps ) {
this .cells [0 ] = this .next_cell ;
this .next_cell = null ;
this .status = 'idle' ;
} else {
const move_distance = this .speed * this .timedelta / 1000 ;
this .cells [0 ].x += move_distance * dx / distance;
this .cells [0 ].y += move_distance * dy / distance;
}
}
update ( ) {
if (this .status === 'move' ) {
this .update_move ();
}
this .render ();
}
render ( ) {
const L = this .gamemap .L ;
const ctx = this .gamemap .ctx ;
ctx.fillStyle = this .color ;
for (const cell of this .cells ) {
ctx.beginPath ();
ctx.arc (cell.x * L, cell.y * L, L / 2 , 0 , Math .PI * 2 );
ctx.fill ();
}
}
}
蛇尾移动 在 Snake.js 中添加代码,判断蛇尾是否增长
check_tail_increasing ( ) {
if (this .step <= 10 ) return true ;
if (this .step % 3 === 1 ) return true ;
return false ;
}
如果当前步骤数小于或等于 10,则直接返回 true,表示蛇的长度会增加。
如果当前步骤数除以 3 的余数为 1,则也返回 true,表示蛇的长度会在该步骤中增加。
check_tail_increasing 方法通过逻辑判断确定蛇的长度是否会在当前回合增加。
修改 Snake.js,判断蛇尾是否在下一步是否增长
this .next_cell = null ;
this .dr = [-1 , 0 , 1 , 0 ];
this .dc = [0 , 1 , 0 , -1 ];
this .step = 0 ;
this .eps = 1e-2 ;
next_step ( ) {
const d = this .direction ;
this .next_cell = new Cell (this .cells [0 ].r + this .dr [d], this .cells [0 ].c + this .dc [d]);
this .direction = -1 ;
this .status = "move" ;
this .step ++;
const k = this .cells .length ;
for (let i = k; i > 0 ; i--) {
this .cells [i] = JSON .parse (JSON .stringify (this .cells [i - 1 ]));
}
}
update_move ( ) {
const dx = this .next_cell .x - this .cells [0 ].x ;
const dy = this .next_cell .y - this .cells [0 ].y ;
const distance = Math .sqrt (dx * dx + dy * dy);
if (distance < this .eps ) {
this .cells [0 ] = this .next_cell ;
this .next_cell = null ;
this .status = "idle" ;
if (!this .check_tail_increasing ()) {
this .cells .pop ();
}
} else {
const move_distance = this .speed * this .timedelta / 1000 ;
this .cells [0 ].x += move_distance * dx / distance;
this .cells [0 ].y += move_distance * dy / distance;
if (!this .check_tail_increasing ()) {
const k = this .cells .length ;
const tail = this .cells [k - 1 ], tail_target = this .cells [k - 2 ];
const tail_dx = tail_target.x - tail.x ;
const tail_dy = tail_target.y - tail.y ;
tail.x += move_distance * tail_dx / distance;
tail.y += move_distance * tail_dy / distance;
}
}
}
美化蛇 render ( ) {
const L = this .gamemap .L ;
const ctx = this .gamemap .ctx ;
ctx.fillStyle = this .color ;
for (const cell of this .cells ) {
ctx.beginPath ();
ctx.arc (cell.x * L, cell.y * L, L / 2 * 0.8 , 0 , Math .PI * 2 );
ctx.fill ();
}
for (let i = 1 ; i < this .cells .length ; i++) {
const a = this .cells [i - 1 ], b = this .cells [i];
if (Math .abs (a.x - b.x ) < this .eps && Math .abs (a.y - b.y ) < this .eps ) continue ;
if (Math .abs (a.x - b.x ) < this .eps ) {
ctx.fillRect ((a.x - 0.4 ) * L, Math .min (a.y , b.y ) * L, L * 0.8 , Math .abs (a.y - b.y ) * L);
} else {
ctx.fillRect (Math .min (a.x , b.x ) * L, (a.y - 0.4 ) * L, Math .abs (a.x - b.x ) * L, L * 0.8 );
}
}
}
绘制蛇的连接部分:
循环从第二个单元格开始(i = 1),用于绘制蛇体各部分之间的连接部分。
获取前一个和当前单元格。
第一个 if 语句检查两个单元格是否非常接近,如果是,则跳过当前迭代。
第二个 if 语句检查两个单元格的 x 坐标是否相等(即它们在垂直方向上对齐)。如果是,则绘制一条垂直矩形;否则绘制水平矩形,连接两部分。
检测非法逻辑 check_valid (cell ) {
for (const wall of this .walls ) {
if (wall.r === cell.r && wall.c === cell.c ) {
return false ;
}
}
for (const snake of this .snakes ) {
let k = snake.cells .length ;
if (!snake.check_tail_increasing ()) {
k--;
}
for (let i = 0 ; i < k; i++) {
if (snake.cells [i].r === cell.r && snake.cells [i].c === cell.c ) return false ;
}
}
return true ;
}
check_valid 的方法,主要用于检查目标位置是否合法。具体来说,它会验证某个单元格是否可以被蛇移动到,确保该位置没有与墙体或其他蛇的身体发生碰撞。
检查墙体碰撞:如果目标位置的行 (cell.r) 和列 (cell.c) 与某个墙体的位置相同,则返回 false。
检查蛇的身体:
使用 for...of 循环遍历所有蛇。
获取当前蛇的身体单元格数量。
调用 snake.check_tail_increasing() 方法来判断蛇尾是否会在当前回合前进。如果蛇尾会前进,则将 k 减少 1,这意味着在检查时不需要考虑蛇尾。
使用内层循环遍历蛇的身体部分,检查每个蛇的单元格位置:如果目标位置的行和列与任何蛇的身体单元格相同,则返回 false。
如果经过上述所有检查后目标位置没有与墙体或其他蛇的身体发生碰撞,则返回 true。
next_step ( ) {
const d = this .direction ;
this .next_cell = new Cell (this .cells [0 ].r + this .dr [d], this .cells [0 ].c + this .dc [d]);
this .direction = -1 ;
this .status = "move" ;
this .step ++;
const k = this .cells .length ;
for (let i = k; i > 0 ; i--) {
this .cells [i] = JSON .parse (JSON .stringify (this .cells [i - 1 ]));
}
if (!this .gamemap .check_valid (this .next_cell )) {
this .status = "die" ;
}
}
render ( ) {
const L = this .gamemap .L ;
const ctx = this .gamemap .ctx ;
ctx.fillStyle = this .color ;
if (this .status === "die" ) {
ctx.fillStyle = "white" ;
}
for (const cell of this .cells ) {
ctx.beginPath ();
ctx.arc (cell.x * L, cell.y * L, L / 2 * 0.8 , 0 , Math .PI * 2 );
ctx.fill ();
}
for (let i = 1 ; i < this .cells .length ; i++) {
const a = this .cells [i - 1 ], b = this .cells [i];
if (Math .abs (a.x - b.x ) < this .eps && Math .abs (a.y - b.y ) < this .eps ) continue ;
if (Math .abs (a.x - b.x ) < this .eps ) {
ctx.fillRect ((a.x - 0.4 ) * L, Math .min (a.y , b.y ) * L, L * 0.8 , Math .abs (a.y - b.y ) * L);
} else {
ctx.fillRect (Math .min (a.x , b.x ) * L, (a.y - 0.4 ) * L, Math .abs (a.x - b.x ) * L, L * 0.8 );
}
}
}
next_step 方法:负责更新蛇的状态,包括计算下一步位置、更新身体位置、检查碰撞,并在碰撞时将状态设为'死亡'。
render 方法:在绘制时根据蛇的状态调整颜色,以便在视觉上区分正常状态和死亡状态。
实现眼睛 this .eye_direction = 0 ;
if (this .id === 1 ) this .eye_direction = 2 ;
this .eye_dx = [
[-1 , 1 ],
[1 , 1 ],
[1 , -1 ],
[-1 , -1 ]
];
this .eye_dy = [
[-1 , -1 ],
[-1 , 1 ],
[1 , 1 ],
[1 , -1 ]
];
render ( ) {
const L = this .gamemap .L ;
const ctx = this .gamemap .ctx ;
ctx.fillStyle = this .color ;
if (this .status === "die" ) {
ctx.fillStyle = "white" ;
}
for (const cell of this .cells ) {
ctx.beginPath ();
ctx.arc (cell.x * L, cell.y * L, L / 2 * 0.8 , 0 , Math .PI * 2 );
ctx.fill ();
}
for (let i = 1 ; i < this .cells .length ; i++) {
const a = this .cells [i - 1 ], b = this .cells [i];
if (Math .abs (a.x - b.x ) < this .eps && Math .abs (a.y - b.y ) < this .eps ) continue ;
if (Math .abs (a.x - b.x ) < this .eps ) {
ctx.fillRect ((a.x - 0.4 ) * L, Math .min (a.y , b.y ) * L, L * 0.8 , Math .abs (a.y - b.y ) * L);
} else {
ctx.fillRect (Math .min (a.x , b.x ) * L, (a.y - 0.4 ) * L, Math .abs (a.x - b.x ) * L, L * 0.8 );
}
}
ctx.fillStyle = "black" ;
for (let i = 0 ; i < 2 ; i++) {
const eye_x = (this .cells [0 ].x + this .eye_dx [this .eye_direction ][i] * 0.15 ) * L;
const eye_y = (this .cells [0 ].y + this .eye_dy [this .eye_direction ][i] * 0.15 ) * L;
ctx.beginPath ();
ctx.arc (eye_x, eye_y, L * 0.05 , 0 , Math .PI * 2 );
ctx.fill ();
}
}
计算眼睛的位置:
eye_x 和 eye_y 是眼睛的坐标。
this.cells[0].x 和 this.cells[0].y 代表蛇头的中心位置。
this.eye_dx[this.eye_direction][i] 和 this.eye_dy[this.eye_direction][i] 分别用于获取当前方向下眼睛相对于蛇头的位置偏移量。
*0.15 是一个缩放因子,用于调整眼睛相对于蛇头的位置。
最后,乘以 L 将这些坐标转换为画布的像素坐标。
ctx.arc(eye_x, eye_y, L * 0.05, 0, Math.PI * 2):使用 arc 方法绘制一个圆形的眼睛。
在蛇头的位置绘制两个黑色的眼睛。通过使用数组 this.eye_dx 和 this.eye_dy,代码能够根据蛇的当前方向动态调整眼睛的位置。
相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
Gemini 图片去水印 基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
Keycode 信息 查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
Escape 与 Native 编解码 JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
JavaScript / HTML 格式化 使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
JavaScript 压缩与混淆 Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online