W1: Snake Game
获取资源
回到昨天的仓库,执行以下代码来切换我们需要的内容:
git reset --hard
git fetch
git checkout w1-starter
你应该会看到一个 game.js
,一个 index.html
,一个 style.css
。
这节课中不需要修改 CSS。
启动
我们将使用 setInterval()
来制造循环。
setInterval(func, delay)
可以在延时之后调用函数,并不断重复这个过程。
const SNAKE_SPEED = 5;
setInterval(main, 1000 / SNAKE_SPEED);
注意 delay
以毫秒为单位。
为了将 game.js
连接到 index.html
,我们需要使用 <script>
标签:
<script src="game.js" defer></script>
生成🐍
切换到 w1-step1
分支,发现我们多了一个 snake.js
,用于编写蛇的逻辑。
连接文件
在 <head>
中添加:
<script src="snake.js" defer></script>
生成身体
通过预制的网格,我们可以用二维坐标表示网页中的一块区域。
那么蛇的身体,就可以用一个坐标队列表示。
初始的蛇:
const snakeBody = [
{x: 11, y: 11},
{x: 11, y: 10},
{x: 11, y: 9},
];
移动
我们可以使用 JS 的 pop()
和 unshift()
来完成队尾弹出和队首插入。
值得注意的是,pop()
的返回值是弹出的元素,unshift()
的返回值是队列长度。
const updateSnake = () => {
snakeBody.pop();
const npnt = {...snakeBody[0]};
npnt.y++;
snakeBody.unshift(npnt);
};
在 game.js
的 Update
中调用即可。
响应操作
切换到 w1-step2
分支。多了一个 input.js
,用于我们写键盘输入。和前面一样连接到 index.html
中。
创建键盘监听
先来学习一下事件监听:EventTarget: addEventListener() method - Web APIs | MDN (mozilla.org)
然后再学习一下键盘监听:KeyboardEvent: key property - Web APIs | MDN (mozilla.org)
配合 switch
语句,搓一下方向判断,放在 input.js
中:
const snakeDirection = [
{ x: 0, y: 1 },
{ x: 0, y: -1 },
{ x: -1, y: 0 },
{ x: 1, y: 0 },
];
let directionIndex = 0;
window.addEventListener("keydown", (event) => {
switch (event.key) {
case "ArrowDown":
directionIndex = 0;
break;
case "ArrowUp":
directionIndex = 1;
break;
case "ArrowLeft":
directionIndex = 2;
break;
case "ArrowRight":
directionIndex = 3;
break;
default:
break;
}
});
const getInputDirection = () => {
return snakeDirection[directionIndex];
};
判断折叠
众所周知,🐍的身体不能折叠,所以我们要写个逻辑判断有没有撞到自己,即键盘输入和上一个时刻相反的情况。
修改一下即可:
window.addEventListener("keydown", (event) => {
let newIndex = directionIndex;
switch (event.key) {
case "ArrowDown":
newIndex = 0;
break;
case "ArrowUp":
newIndex = 1;
break;
case "ArrowLeft":
newIndex = 2;
break;
case "ArrowRight":
newIndex = 3;
break;
default:
break;
}
diff = newIndex ^ directionIndex
if (diff != 0 && diff != 1) directionIndex = newIndex;
});
生成🍎
切换到 w1-step3
。多了 food.js
、snakeUtils.js
。建议先阅读 snakeUtils.js
了解可用的函数。
可以发现 onSnake()
可以判定一个位置是否和蛇身重合,getNewFoodPosition()
可以获得一个随机位置。
在 food.js
中添加代码,如果🍎和🐍重合则让🐍变长,同时生成一个新的🍎。
在 update()
中添加 updateFood()
即可。
判定结束
w1-step4
。在 snakeUtils.js
中增加了新的函数,判定蛇有没有出界、有没有和自身相撞。
在 game.js
中,我们需要判定游戏结束、设定游戏结束的提示、让蛇的位置停止更新。
这里需要用到 alert()
函数,用于发送消息弹窗;以及 clearInterval()
函数,用于取消循环。
额外挑战:重置游戏
先把重置按键绑定为 r
。input.js
的其他部分和上文一样。其他文件和 w1-step4
一样。
let isResetGame = true;
window.addEventListener("keydown", (event) => {
if(event.key == "r") isResetGame = true;
});
然后在 game.js
中重新整理函数。原来的 main()
只负责游戏过程,现在我们需要另一个函数掌控它的开关。
所以我把原来的 main()
更名为 gamerun()
,同时用 resetGame()
来完成重启一次的任务。
(这里偷懒,把 snakeBody
定义为变量了,这样直接赋值就能重置🐍)
const SNAKE_SPEED = 5;
const gameBoard = document.getElementById("game-board");
let isGameOver = true;
let gameid = 0;
const main = () => {
if (isResetGame === true) resetGame();
};
const resetGame = () => {
if (!isGameOver) clearInterval(gameid);
isResetGame = false;
isGameOver = false;
snakeBody = [
{ x: 11, y: 11 },
{ x: 11, y: 10 },
{ x: 11, y: 9 },
];
directionIndex = 0;
gameid = setInterval(gamerun, 1000 / SNAKE_SPEED);
};
const gamerun = () => {
update();
draw();
if (isGameOver) {
alert("Game Over!");
clearInterval(gameid);
}
};
const update = () => {
console.log("Updating");
updateSnake();
updateFood();
// TODO 4.2: Update Game State
isGameOver = checkGameOver();
};
const draw = () => {
gameBoard.innerHTML = "";
drawSnake(gameBoard);
drawFood(gameBoard);
};
const checkGameOver = () => {
return snakeOutOfBounds() || snakeIntersectSelf();
};
setInterval(main, 100);
参考答案是 w1-challenge
。