这个贪吃蛇好像有点不简单,玩了半天20分都没有获得
写在前面
贪吃蛇这个游戏大家都耳熟能详了,我一次接触这个游戏还是在诺基亚手机上,相信大家都见过这个手机,就是下面这个样子:
这篇文章我们就是用前端技术来实现这个小游戏,这个游戏的代码大部分采用TS+Less编写,正好练习一下TS。
游戏演示如下:
在线体验地址:ywanzhou.github.io/eating-snak…
HTML结构
首先还是先确定我们的HTML结构,代码如下:
<body>
<!-- 创建游戏的主窗口 -->
<div id="main">
<!-- 主要区域 -->
<div id="state">
<!-- 蛇 -->
<div id="snake">
<div></div>
</div>
<!-- 设置食物 -->
<div id="food"></div>
</div>
<!-- 展示 -->
<div id="score-panel">
<div>Score:<span id="score">0</span></div>
<div>Level: <span id="level">1</span></div>
</div>
</div>
</body>
HTML结构比较简单,CSS的样式也不复杂,如果需要可以自行去github查看。
食物类
首先我们先编写一下食物类,首先我们需要获取HTML结构中的元素,然后可以获取食物的x轴y轴的坐标,最后定义一个方法可以改变他的坐标,示例代码如下:
// 定义食物类 food
class Food {
// 定义一个属性表示食物所对应的元素 名称element:类型是 HTMLElement
element: HTMLElement
constructor() {
// ! 表示document.getElementById('food')已经确定可以找到对应的元素不会为空
this.element = document.getElementById("food")!
}
// 获取 x y 轴坐标
get X() {
return this.element.offsetLeft
}
get Y() {
return this.element.offsetTop
}
change() {
// 生成一个随机的位置来表示食物 位置最小为0 最大为290 必须是10的倍数
let top = Math.round(Math.random() * 29) * 10
let left = Math.round(Math.random() * 29) * 10
this.element.style.left = left + "px"
this.element.style.top = top + "px"
}
}
export default Food
蛇类
首先我们定义属性,来表示元素,代码如下:
class Snake {
// 表示蛇儿头
head: HTMLElement
// 表示蛇的身体(包括蛇头)
bodies: HTMLCollection
// 蛇的父级元素
element: HTMLElement
constructor() {
// 元素初始化
this.element = document.getElementById("snake")!
this.head = document.querySelector("#snake > div") as HTMLElement
this.bodies = this.element.getElementsByTagName("div")
}
}
然后定义两个方法,一个用于添加蛇的身体,一个用于移动蛇,示例代码如下:
// 蛇增加身体的方法
addBody() {
// 向element 中添加一个div
this.element.insertAdjacentHTML("beforeend", "<div></div>")
}
// 添加一个蛇身体移动的方法
moveBody() {
/*
将后一节的位置设置为前一节的位置,比如第二节的位置设置为蛇头的位置
因为要先获取前一个节的位置所有需要从后往前遍历 i>0
*/
for (let i = this.bodies.length - 1; i > 0; i--) {
// 获取前边身体的位置 类型“Element”上不存在属性“offsetLeft”需要来一个类型断言
let X = (this.bodies[i - 1] as HTMLElement).offsetLeft
let Y = (this.bodies[i - 1] as HTMLElement).offsetTop
// 将值设置到当前身体上
;(this.bodies[i] as HTMLElement).style.left = X + "px"
;(this.bodies[i] as HTMLElement).style.top = Y + "px"
}
}
获取蛇头的坐标,实现比较简单,代码如下:
// 获取蛇的坐标(蛇头坐标)
get X() {
return this.head.offsetLeft
}
get Y() {
return this.head.offsetTop
}
定义一个方法,用于检测是否撞到了身体,代码如下:
// 检查蛇头是否撞到身体的方法
checkHeadBody() {
// 获取所有的身体,检查其是否和蛇头的坐标发生重叠
/*
let i = 1; i < this.bodies.length; i++
从1 开始不包含蛇头0
*/
for (let i = 1; i < this.bodies.length; i++) {
let bd = this.bodies[i] as HTMLElement
if (this.X === bd.offsetLeft && this.Y === bd.offsetTop) {
// 进入判断说明蛇头撞到了身体,游戏结束
throw new Error("不要自己撞自己哦~")
}
}
}
然后我们去设置蛇头的坐标,在实现过程中需要添加一些逻辑判断,例如不能撞墙,不能向下移动时向上掉头等;
代码如下:
// 设置蛇头的坐标
set X(value: number) {
// 如果新值和旧值相同,则直接返回不再修改
if (this.X === value) {
return
}
// 判断蛇有没有在规定的范围内移动
if (value < 0 || value > 290) {
throw Error("你的小蛇撞墙了~")
}
// 修改x时,是在修改水平坐标,蛇在左右移动,蛇在向左移动时,不能向右掉头,反之亦然
/*
this.bodies[1] 表示先检查有没有第二节身体
(this.bodies[1] as HTMLElement).offsetLeft === value 表示第二节身体的X值与蛇头的X值相同
*/
if (
this.bodies[1] &&
(this.bodies[1] as HTMLElement).offsetLeft === value
) {
if (value > this.X) {
value = this.X - 10
} else {
value = this.X + 10
}
}
// 移动身体
this.moveBody()
this.head.style.left = value + "px"
// 检查有没有撞到自己
this.checkHeadBody()
}
set Y(value: number) {
// 如果新值和旧值相同,则直接返回不再修改
if (this.Y === value) {
return
}
// 判断蛇有没有在规定的范围内移动
if (value < 0 || value > 290) {
throw Error("你的小蛇撞墙了~")
}
// 修改Y时,是在修改垂直坐标,蛇在左右移动,蛇在向上移动时,不能向下掉头,反之亦然
if (this.bodies[1] && (this.bodies[1] as HTMLElement).offsetTop === value) {
if (value > this.Y) {
value = this.Y - 10
} else {
value = this.Y + 10
}
}
// 移动身体
this.moveBody()
this.head.style.top = value + "px"
// 检查有没有撞到自己
this.checkHeadBody()
}
最后导出这个类。
export default Snake
分数等级类
现在我们定义一个方法,实现分数和等级的记录,代码如下:
class ScorePanel {
// 分别记录分数和等级
score = 0;
level = 1;
// 分数和等级所在的元素,在构造函数中进行初始化
ScoreEle: HTMLElement;
LevelEle: HTMLElement;
// 设置一个变量限制等级
maxLevel: number;
upScore: number;
constructor(maxLevel: number = 10, upScore: number = 10) {
this.ScoreEle = document.getElementById("score")!;
this.LevelEle = document.getElementById("level")!;
this.maxLevel = maxLevel;
this.upScore = upScore;
}
// 设置一个加分的方法
addScore() {
// 使分数自增
this.ScoreEle.innerHTML = ++this.score + "";
if (this.score % this.upScore === 0) {
this.UpLevel();
}
}
UpLevel() {
// 使等级自增
if (this.level < this.maxLevel) {
this.LevelEle.innerHTML = ++this.level + "";
}
}
}
export default ScorePanel;
游戏控制器
我们将前面的定义都已经实现,现在我们将前面的3个类进行应用,实现代码如下:
import ScorePanel from "./ScorePanel";
import Snake from "./snake";
import Food from "./Food";
// 游戏控制器,控制其他的所有类
class GameControl {
// 定义三个属性
snake: Snake;
food: Food;
// 记分牌
scorePanel: ScorePanel;
// 创建一个属性用来存储蛇的移动方向(就是键盘的方向键)
direction: string = "";
// 创建一个属性用来记录游戏是否结束
isLive = true;
constructor() {
this.snake = new Snake();
this.food = new Food();
this.scorePanel = new ScorePanel(10, 1);
// 游戏初始化
this.init();
}
// 初始化游戏的方法,调用后游戏开始
init() {
document.addEventListener("keydown", this.keydownHandler.bind(this));
this.run();
}
// 创建一个键盘按下的响应函数
keydownHandler(event: KeyboardEvent) {
// 当用户按下键盘的键之后,需要判断用户按下的键是否符合目标的键
// 当用户按下键盘时存储键
this.direction = event.key;
}
// 定义一个蛇移动起来的方法
run() {
// 获取蛇的坐标
let X = this.snake.X;
let Y = this.snake.Y;
// 根据按键方向来修改蛇的坐标值
switch (this.direction) {
// 上移 top减少
case "ArrowUp":
Y -= 10;
break;
case "ArrowDown":
Y += 10;
break;
case "ArrowLeft":
X -= 10;
break;
case "ArrowRight":
X += 10;
break;
default:
break;
}
// 检查蛇是否吃到了食物
this.checkEat(X, Y);
// 修改蛇的X和Y值
try {
this.snake.X = X;
this.snake.Y = Y;
} catch (e: any) {
// 捕获异常弹出消息提示
alert(e.message);
// isLive改为false 表示游戏结束
this.isLive = false;
}
// 开启一个定时器run()一直执行
this.isLive &&
setTimeout(this.run.bind(this), 300 - (this.scorePanel.level - 1) * 30);
}
// 定义一个方法,用来检查蛇是否吃到食物
checkEat(X: number, Y: number) {
if (X === this.food.X && Y === this.food.Y) {
// 食物位置刷新
this.food.change();
// 加一分
this.scorePanel.addScore();
// 蛇的身体增加一格
this.snake.addBody();
}
}
}
export default GameControl;
到这为止这个游戏的大部分代码已经完成了。
写在最后
全部代码已经放在了GitHub上,代码不复杂,仅适用于学习。
PS:如果您是大佬,或者觉着这个文章垃圾,请忽略这篇文章,谢谢。
转载自:https://juejin.cn/post/7079437726658854920