TypeScript实战 – 贪吃蛇游戏(2)–实现蛇类和游戏控制器类,完成游戏
在上篇文章(TypeScript实战 – 贪吃蛇游戏(1))中我们实现了贪吃蛇游戏中的食物类:实现了获取食物位置,随机改变食物位置的功能,计分板类:实现了获取积分和等级,增加积分,增加等级规则的功能。下面让我们实现一下最后的两个类:蛇类和游戏控制器类。
分析
我们的游戏,采用面向对象的方法开发,将游戏中的各角色分成一个个的对象,目前来说,它们各自都是独立的部分,各自都有各自独立的特性的方法,这是正确的。但是我们还需要一个将各个对象调动起来的类,所以我们需要创建一个游戏控制器类,用来调动其他各对象以及它们的方法实现游戏的完整运行。
各类的具体分析与实现
Snake类和GameController类
为什么将Snake类和GameController类放一起说,这里我在GameController类里实现了蛇移动的方法以及吃到食物的检测还有检测键盘事件的方法。
如果想要将移动的方法和吃到食物检测的方法写在蛇类里,可以自己下去尝试一下。
思路
蛇类包含了获取与设置蛇身体部分的位置,增加长度,处理撞墙,检测是否吃到自己,禁止掉头
游戏控制器类包含了控制游戏开始和结束,吃到食物检测,蛇移动,调用其他各对象的方法完成游戏的逻辑。
获取与设置蛇身体部分的位置:
想象一下,我们蛇移动的时候,是头部在控制着移动的方向,而整个身体是做一个类似从后往前蠕动的过程。所以我们只需要拿到蛇头的坐标,后面身体做相应的蠕动就行了。所以获取位置只需要获取蛇头的位置即可
蛇移动:
蛇移动身体,我们可以这么想,开始游戏的时候,只有蛇头,我们先考虑蛇头的移动方式:应该先有个变量,存放它移动的方向,然后只要这个变量不变,我们就一只往这个变量存放的方向移动,只要改变了变量存放的位置信息,我们就往改变后的方向移动。所以引出改变变量存放的位置信息的方法就是我们需要检测键盘上下左右键按下去的事件,只要按了这四个其中之一,我们就改变这个变量。
至于蛇头后部分身体的移动方式,我们可以从后往前遍历,将前一节点的位置赋值给后一节点即可。
吃到食物检测以及增加身体:
只要蛇头的偏移量等于食物的偏移量即可
蛇增加的方式是吃一个食物,增加一节身体,这个身体就是给蛇容器里增加子节点,放在头节点后面就好了。我们可以将刚开始的一格定义为一个head,我们蛇的身体包括头存放在HTMLCollection
类型的变量里:HTMLCollection
对象是 HTML
元素的集合,类似一个包含 HTML
元素的数组列表。
处理撞墙:
处理撞墙只需要判断蛇头位置是否超出了规定的屏幕边界即可。
检测是否吃到自己:
遍历蛇身节点偏移量,和蛇头偏移量相等即为吃到自己
禁止掉头:
只要蛇头偏移量等于蛇身第一个子节的偏移量即为掉头,那我们偏让他往掉头的反方向移动,这样就算掉头也没用,它还是在往前走(这样做的前提是有蛇身)
实现步骤
- 首先定义存放蛇各部分的变量,蛇头
head
,蛇身bodies
,蛇容器snakeBox
- 完成获取与设置蛇头的坐标的方法
- 完成蛇身体增加的方法
addBodies
,只需要获取到蛇容器,再给现有子节点的后部加入<div></div>
即可
这里用到了insertAdjacentElement(position, element)
:方法将一个给定的元素节点插入到相对于被调用的元素的给定的一个位置。
参数:
position
表示相对于该元素的位置;必须是以下字符串之一:
-
'beforebegin'
: 在该元素本身的前面. -
'afterbegin'
:只在该元素当中, 在该元素第一个子孩子前面. -
'beforeend'
:只在该元素当中, 在该元素最后一个子孩子后面. -
'afterend'
: 在该元素本身的后面.
element
要插入到树中的元素.
- 我们需要有个
init()
事件控制着游戏的开始,游戏一旦开始检测键盘事件,蛇头开始移动,并且定义一个开关isLive
,控制着蛇是否存活,如果不存活,游戏不再进行下去。 - 完成检测键盘事件,使用
document.addEventListener
,监听keydown
事件,我们还需要做处理,按下去的是哪个按键,合法才行。定义一个变量keyDirection
存放方向,我们应该给它一个初始值,因为游戏开始的时候,蛇头是要开始移动的。 - 完成蛇头移动的方法
move()
,我们可以判断keyDirection
现在是什么方向的,我们就往哪个方向增加蛇头的偏移量。因为蛇是一直移动的,所以我们还需要实现能够一直调用移动的方法,不然只能走一下就停止了 - 完成吃到食物的方法
eatFood()
:food的X,Y等于现在蛇头的X,Y,调用Food
的change()
事件,调用Snake
的addBodies
的事件再调用levelScore
的addScore()
增加一分 - 完成蛇身的蠕动事件
moveBodies()
- 完成检测是否吃到自己的事件
checkIsEatSelf()
- 完成禁止掉头的判断
具体代码
Snake类:
class Snake{
// 蛇容器
private snakeBox: HTMLElement;
// 蛇头
private head: HTMLElement;
// 蛇身
private bodies: HTMLCollection;
constructor() {
this.snakeBox = document.querySelector(".snake")!;
this.head = document.querySelector(".snake > div") as HTMLElement;
this.bodies = this.snakeBox.getElementsByTagName("div");
}
// 获取蛇头坐标
get X() {
return this.head.offsetLeft;
}
get Y() {
return this.head.offsetTop;
}
// 设置蛇头位置
set X(value: number) {
// 去掉多余的赋值
if (value === this.X) {
return;
}
// 处理撞墙
if (value < 0 || value > 290) {
throw new Error('蛇撞墙了');
}
// 禁止掉头
if(this.bodies[1] && (this.bodies[1] as HTMLElement).offsetLeft === value) {
if (value > this.X) {
value = this.X - 10;
} else if (value < this.X) {
value = this.X + 10;
}
}
// 移动身体
this.moveBodies();
this.head.style.left = value + 'px';
// 检查有没有吃到自己
this.checkIsEatSelf();
}
set Y(value: number) {
// 去掉多余的赋值
if (value === this.Y) {
return;
}
// 处理撞墙
if (value < 0 || value > 290) {
// 超出边界抛出异常
throw new Error('蛇撞墙了');
}
// 禁止掉头
if(this.bodies[1] && (this.bodies[1] as HTMLElement).offsetTop === value) {
if (value > this.Y) {
value = this.Y - 10;
} else if (value < this.Y) {
value = this.Y + 10;
}
}
// 移动身体
this.moveBodies();
this.head.style.top = value + 'px';
// 检查有没有吃到自己
this.checkIsEatSelf();
}
// 蛇增加身体的方法
addBodies() {
const divElement = document.createElement("div");
this.snakeBox.insertAdjacentElement("beforeend", divElement);
}
// 蛇移动的方法
moveBodies() {
// 遍历蛇的所有子节,从后往前,将前一个块的位置赋值给前一个
for(let i = this.bodies.length - 1; i > 0; i--) {
// 取前一个块的位置
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';
}
}
// 检查是否撞到自己
checkIsEatSelf() {
for(let i = 1; i < this.bodies.length; i++) {
if (this.X === (this.bodies[i] as HTMLElement).offsetLeft && this.Y === (this.bodies[i] as HTMLElement).offsetTop){
// 抛出异常
throw new Error('撞到自己了~');
}
}
}
}
export default Snake;
GameController类:
import Food from "./Food";
import LevelScore from "./LevelScore";
import Snake from "./Snake";
class GameController {
private food: Food;
private levelScore: LevelScore;
private snake: Snake;
// 获取按键
private keyDirection: string = 'ArrowRight';
// 游戏继续进行的开关
private isLive:Boolean = true;
constructor(){
this.food = new Food();
this.levelScore = new LevelScore(1, 10);
this.snake = new Snake();
this.init();
}
init() {
// 游戏开始,监听键盘事件
document.addEventListener('keydown', this.handleKeyDown.bind(this));
this.move();
}
handleKeyDown(event: KeyboardEvent) {
// 定义指定按下的按键,IE中上下左右是Up,Down,Left,Right
let allowKey = {
"ArrowUp": "ArrowUp",
"Up":"Up",
"ArrowDown":"ArrowDown",
"Down":"Down",
"ArrowLeft" : "ArrowLeft",
"Left":"Left",
"ArrowRight":"ArrowRight",
"Right":"Right"
}
if (event.key in allowKey) {
this.keyDirection = event.key;
}
}
// 蛇移动
move() {
let X = this.snake.X;
let Y = this.snake.Y;
// 根据键盘来改变方向
switch(this.keyDirection) {
case "ArrowUp" || "Up":
Y -= 10;
break;
case "ArrowDown" || "Down":
Y += 10;
break;
case "ArrowLeft" || "Left":
X -= 10;
break;
case "ArrowRight" || "Right":
X += 10;
break;
}
// 判断蛇是否吃到了食物
this.eatFood(X, Y);
// 捕获异常
try {
this.snake.X = X;
this.snake.Y = Y;
} catch (error) {
alert(error.message);
this.isLive = false;
window.location.reload();
}
// 只要蛇还活着就隔段事件掉一次移动事件
this.isLive && setTimeout(this.move.bind(this), 200 - (this.levelScore.getlevel() - 1)*20);
}
// 蛇是否吃到了食物
eatFood(X: number, Y: number) {
if (X === this.food.X && Y === this.food.Y) {
// 食物改变位置
this.food.change();
// 增加一分
this.levelScore.addScore();
// 蛇增加一节
this.snake.addBodies();
}
}
}
export default GameController;
这样整个游戏就完成了,快点也实现一个玩玩吧~