1. 蛇的身体移动逻辑
蛇身体的移动属于蛇的事情,所以写在Snake类中。
后一节移动后的位置刚好为前一节位置,依次移动即可。因此除了蛇头为键盘控制外,蛇身继承前一节的位置即可。
移动身体的moveBody
方法代码如下:
// 添加一个蛇身体移动的方法
moveBody() {
/**
* 将后边的身体设置为前边身体的位置
*/
for (let i = this.bodies.length - 1; i > 0; i--) {
// 获取前边身体的位置(类型断言,就是HTMLElement)
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";
}
}
所以理所应当的,在蛇头移动的时候需要调用moveBody方法。蛇头移动方法是X和Y的setters方法。因此在蛇头的坐标发生改变之后,需要moveBody。setters的代码如下:
// 设置蛇头的X坐标
set X(value: number) {
// 如果新值和旧值相同,则直接返回,不再修改
if (this.X === value) {
return;
}
// X的值的合法范围在0-290px之间
if (value < 0 || value > 290) {
// 进入判断,说明蛇撞墙了
throw new Error("蛇撞墙了");
}
// 移动身体
this.moveBody();
this.head.style.left = value + "px";
}
// 设置蛇头的Y坐标
set Y(value: number) {
// 如果新值和旧值相同,则直接返回,不再修改
if (this.Y === value) {
return;
}
// X的值的合法范围在0-290px之间
if (value < 0 || value > 290) {
// 进入判断,说明蛇撞墙了
throw new Error("蛇撞墙了");
}
// 移动身体
this.moveBody();
this.head.style.top = value + "px";
}
2. 待处理问题
2.1 蛇掉头灵异事件(头转个180跑了)
避免蛇在两节及以上的时候发生掉头的问题,在只有单独一个蛇头的时候可以随意变换方向。
直接判断蛇头和下一节蛇身的坐标是否重叠,重叠了则表明发生了掉头情况,此时将蛇头的位置重新更改即可。
// 设置蛇头的X坐标
set X(value: number) {
// 如果新值和旧值相同,则直接返回,不再修改
if (this.X === value) {
return;
}
// 避免蛇在两节及以上的时候发生掉头的问题
if (
this.bodies[1] &&
(this.bodies[1] as HTMLElement).offsetLeft === value
) {
// 进入判断说明发生了掉头
// 如果发生掉头,让蛇向反方向继续移动
if (value > this.X) {
// 如果新值value大于旧值X,则说明蛇在向右走,如果此时发生掉头,应该使蛇继续向左走
value = this.X - 10;
} else {
value = this.X + 10;
}
}
// X的值的合法范围在0-290px之间
if (value < 0 || value > 290) {
// 进入判断,说明蛇撞墙了
throw new Error("蛇撞墙了");
}
// 移动身体
this.moveBody();
this.head.style.left = value + "px";
}
// 设置蛇头的Y坐标
set Y(value: number) {
// 如果新值和旧值相同,则直接返回,不再修改
if (this.Y === value) {
return;
}
// 避免蛇在两节及以上的时候发生掉头的问题
if (
this.bodies[1] &&
(this.bodies[1] as HTMLElement).offsetTop === value
) {
if (value > this.Y) {
value = this.Y - 10;
} else {
value = this.Y + 10;
}
}
// X的值的合法范围在0-290px之间
if (value < 0 || value > 290) {
// 进入判断,说明蛇撞墙了
throw new Error("蛇撞墙了");
}
// 移动身体
this.moveBody();
this.head.style.top = value + "px";
}
2.2 蛇撞到自己身体的逻辑
这里的逻辑其实挺简单,只要蛇的头和身体的某一节坐标重叠,说明蛇撞到了自己的身体。当然,这部分的代码逻辑需要放在蛇头移动后进行判断。
判断蛇头是否撞击到蛇身的方法checkHeadBody
代码如下:
// 判断蛇头是否撞击到蛇身
checkHeadBody() {
// 获取所有的身体,检查是否和蛇头的坐标发生重叠
for (let i = 1; i < this.bodies.length; i++) {
let node = this.bodies[i] as HTMLElement;
if (this.X === node.offsetLeft && this.Y === node.offsetTop) {
// 进入判断说明蛇头撞到了身体,游戏结束
throw new Error("撞到自己了~");
}
}
}
3. 待处理问题
-
蛇每次开始游戏时,应当在随机位置出生,食物也是如此。
-
食物被吃到后刷新位置不能刷新到蛇的身体上。
这些问题的结局办法较简单,另外,因为没有全方位测试,所以可能还有一些隐藏bug,但是总体的功能是没有问题的。
4. 项目完整代码
4.1 Direction.ts
enum Direction {
Up = "ArrowUp",
Down = "ArrowDown",
Left = "ArrowLeft",
Right = "ArrowRight"
}
export default Direction;
4.2 Food.ts
// 定义食物类Food
class Food {
// 定义一个属性表示食物所对应的元素
private element: HTMLElement;
constructor() {
// 获取页面中的food元素并将其赋值给element成员变量
// 感叹号! : 肯定获取的元素不可能为空,就不用进行空值判断了
this.element = document.getElementById("food")!;
}
// 定义一个获取食物X轴坐标的方法
get X() {
return this.element.offsetLeft;
}
// 定义一个获取食物Y轴坐标的方法
get Y() {
return this.element.offsetTop;
}
// 修改食物的位置
public change() {
// 生成一个随机的位置
// 食物的位置最小是0,最大是290px
let left = Math.floor(Math.random() * 30) * 10;
let top = Math.floor(Math.random() * 30) * 10;
this.element.style.left = top + "px";
this.element.style.top = left + "px";
}
}
export default Food;
4.3 GameControl.ts
// 引入其他的类
import Snake from "./Snake";
import Food from "./Food";
import ScorePanel from "./ScorePanel";
import Direction from "./Direction";
// 游戏控制器,控制其他的所有类
class GameControl {
// 基础运动间隔事件
private baseTimeInterval: number = 300;
private timeInterval: number = this.baseTimeInterval;
// 定义三个属性
// 蛇
private snake: Snake;
// 食物
private food: Food;
// 记分牌
private scorePanel: ScorePanel;
// 创建一个属性来存储蛇的移动方向
private direction: string = "";
// 创建一个属性用来记录游戏是否结束
private isLive: boolean = true;
constructor() {
this.snake = new Snake();
this.food = new Food();
this.scorePanel = new ScorePanel();
this.init();
}
// 游戏的初始化方法,调用后游戏立即开始
private init() {
// 绑定键盘按键按下的事件
document.addEventListener("keydown", this.keyDownHandle.bind(this));
// 调用run方法,使蛇不停移动
this.run();
}
// 创建一个键盘按下的响应函数
private keyDownHandle(event: KeyboardEvent): void {
// 修改direction
this.direction = event.key;
}
// 创建一个控制蛇移动的方法
run() {
/**
* 根据方向 this.direction 来使蛇的位置改变
* 向上:top减少
* 向下:top增加
* 向左:left减少
* 向右:left增加
*/
// 获取蛇现在坐标
let X: number = this.snake.X;
let Y: number = this.snake.Y;
// 判断按键方向后改变蛇头的坐标
switch (this.direction) {
case Direction.Up:
Y -= 10;
break;
case Direction.Down:
Y += 10;
break;
case Direction.Left:
X -= 10;
break;
case Direction.Right:
X += 10;
break;
default:
break;
}
// 检查蛇是否吃到了食物
this.checkEat(X, Y);
// 修改改变后的蛇的坐标
try {
this.snake.X = X;
this.snake.Y = Y;
} catch (error) {
// 进入到catch说明出现了异常,游戏结束。
alert(error.message + "GAME OVER");
// 将isLive设置为false
this.isLive = false;
}
// 蛇还没死的时候,开启一个定时调用,时间间隔和level相关
if (this.isLive) {
this.timeInterval =
this.baseTimeInterval - (this.scorePanel.getLevel() - 1) * 30;
setTimeout(this.run.bind(this), this.timeInterval);
}
}
// 定义一个方法,用来检查蛇是否吃到食物
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;
4.4 ScorePanel.ts
// 定义表示记分牌的类
class ScorePanel {
// score和level用来记录分数和等级
private score: number = 0;
private level: number = 1;
// 分数和等级所在的元素,在构造函数中进行初始化
private scoreEle: HTMLElement;
private levelEle: HTMLElement;
// 设置一个变量限制等级
private maxLevel: number;
// 设置一个变量表示每多少分难度升级一次
private upScore: number;
// 默认的最高等级为10
constructor(maxLevel: number = 10, upScore: number = 10) {
this.scoreEle = document.getElementById("score")!;
this.levelEle = document.getElementById("level")!;
this.maxLevel = maxLevel;
this.upScore = upScore;
}
public getLevel() {
return this.level;
}
// 设置一个分数自增1的方法
public addScore(): void {
this.scoreEle.innerHTML = ++this.score + '';
// 判断分数的阈值,达到阈值提升等级
if (this.score % 10 === 0) {
this.levelUp();
}
}
// 提升等级方法
private levelUp(): void {
// level有最高等级
if (this.level < this.maxLevel) {
this.levelEle.innerHTML = ++this.level + '';
}
}
}
export default ScorePanel;
4.5 Snake.ts
class Snake {
// 表示蛇的元素
head: HTMLElement;
// 蛇的身体(包括蛇头)
bodies: HTMLCollection;
// 获取蛇的容器
element: HTMLElement;
constructor() {
this.element = document.getElementById("snake")!;
// 获取放置蛇的容器,取第一个子元素便为头
this.head = this.element.querySelector("div")!;
this.bodies = this.element.getElementsByTagName("div");
}
// 获取蛇头的X坐标
get X() {
return this.head.offsetLeft;
}
// 获取蛇头的Y坐标
get Y() {
return this.head.offsetTop;
}
// 设置蛇头的X坐标
set X(value: number) {
// 如果新值和旧值相同,则直接返回,不再修改
if (this.X === value) {
return;
}
// 避免蛇在两节及以上的时候发生掉头的问题
if (
this.bodies[1] &&
(this.bodies[1] as HTMLElement).offsetLeft === value
) {
// 进入判断说明发生了掉头
// 如果发生掉头,让蛇向反方向继续移动
if (value > this.X) {
// 如果新值value大于旧值X,则说明蛇在向右走,如果此时发生掉头,应该使蛇继续向左走
value = this.X - 10;
} else {
value = this.X + 10;
}
}
// X的值的合法范围在0-290px之间
if (value < 0 || value > 290) {
// 进入判断,说明蛇撞墙了
throw new Error("蛇撞墙了");
}
// 移动身体
this.moveBody();
// 移动头
this.head.style.left = value + "px";
// 检查有没有撞到自己
this.checkHeadBody();
}
// 设置蛇头的Y坐标
set Y(value: number) {
// 如果新值和旧值相同,则直接返回,不再修改
if (this.Y === value) {
return;
}
// 避免蛇在两节及以上的时候发生掉头的问题
if (
this.bodies[1] &&
(this.bodies[1] as HTMLElement).offsetTop === value
) {
if (value > this.Y) {
value = this.Y - 10;
} else {
value = this.Y + 10;
}
}
// X的值的合法范围在0-290px之间
if (value < 0 || value > 290) {
// 进入判断,说明蛇撞墙了
throw new Error("蛇撞墙了");
}
// 移动身体
this.moveBody();
// 移动头
this.head.style.top = value + "px";
// 判断有没有撞到自己
this.checkHeadBody();
}
// 吃入食物后身体变长(增加div元素)
addBody() {
// 向element中的末尾添加一个div
this.element.insertAdjacentHTML("beforeend", "<div></div>");
}
// 添加一个蛇身体移动的方法
moveBody() {
/**
* 将后边的身体设置为前边身体的位置
*/
for (let i = this.bodies.length - 1; i > 0; i--) {
// 获取前边身体的位置(类型断言,就是HTMLElement)
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";
}
}
// 判断蛇头是否撞击到蛇身
checkHeadBody() {
// 获取所有的身体,检查是否和蛇头的坐标发生重叠
for (let i = 1; i < this.bodies.length; i++) {
let node = this.bodies[i] as HTMLElement;
if (this.X === node.offsetLeft && this.Y === node.offsetTop) {
// 进入判断说明蛇头撞到了身体,游戏结束
throw new Error("撞到自己了~");
}
}
}
}
export default Snake;
4.6 index.ts
// 引入样式
import "./style/index.less";
// 引入类
import GameControl from "./modules/GameControl";
new GameControl();
4.7 index.less
// 设置变量
@bg-color: #b7d4a8;
// 清除默认样式
* {
margin: 0;
padding: 0;
// 改变盒子模型的计算方式
box-sizing: border-box;
}
body {
font: 700 20px "Courier";
}
// 设置主窗口样式
#main {
width: 360px;
height: 420px;
// 设置背景颜色
background-color: @bg-color;
// 设置居中
margin: 100px auto;
border: 10px solid black;
// 设置圆角
border-radius: 20px;
// 开启弹性盒模型
display: flex;
// 设置主轴的方向(纵向)
flex-flow: column;
// 设置侧轴的对齐方式
align-items: center;
// 设置主轴的对齐方式(每个项目两侧的间隔相等)
justify-content: space-around;
// 设计游戏界面样式
#stage {
width: 304px;
height: 304px;
border: 2px solid #000;
// 开启相对定位(子绝父相)
position: relative;
// 设置蛇的样式
#snake {
// & 代表当前选择器的父类
& > div {
width: 10px;
height: 10px;
background-color: #000;
border: 1px solid @bg-color;
// 开启绝对定位
position: absolute;
}
}
// 设置食物样式
#food {
width: 10px;
height: 10px;
position: absolute;
left: 40px;
top: 100px;
// 开启弹性盒模型
display: flex;
// 设置主轴内项目为横向排列,但是会换行
flex-flow: row wrap;
// 设置主轴内项目的对齐方式(两端对齐,项目之间的间隔都相等)
justify-content: space-between;
& > div {
width: 4px;
height: 4px;
background-color: #000;
// 使div旋转45度
transform: rotate(45deg);
}
}
}
// 设置记分牌样式
#score-panel {
width: 300px;
display: flex;
// 设置主轴的对齐方式(两端对齐,项目之间的间隔都相等)
justify-content: space-between;
}
}
4.8 index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>贪吃蛇</title>
</head>
<body>
<!-- 创建游戏的主容器 -->
<div id="main">
<!-- 设置游戏界面 -->
<div id="stage">
<!-- 设置蛇 -->
<div id="snake">
<!-- snake内部的div,表示蛇的各部分 -->
<div></div>
</div>
<!-- 设置食物 -->
<div id="food">
<!-- 添加四个小div,来设置食物的样式(花瓣形) -->
<div></div>
<div></div>
<div></div>
<div></div>
</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>