TS项目练习:贪吃蛇小游戏(三)

  • 现在正式开始ts代码的编写,基本上全部的编码流程都是通过来践行面向对象编程思想

1. 食物类(food)

  • 在设计之前,我们首先要了解一下这个到底有哪些属性方法,食物的属性有:
    • 1. 自己本身的元素(它必须要有一个属性是指向自身的div的)
  • 食物的方法(功能)有那些?
    • 食物被吃:判断蛇与食物的位置重合,因此我们需要一个方法来获取食物的xy坐标
    • 食物的被吃后需要随机生成新的位置,这里同时规定蛇每移动一次就是10px,结合下来就是食物必须也是10的整数位置

代码展示:

// 1. 定义食物的类(比较简单)
class Food{
// 定义一个属性用于指向食物对应的元素
element:HTMLElement;

constructor(){
// 获取页面中food元素并将其赋值给类中的element属性
this.element = document.getElementById('food')!//!表示这个元素不可能为空
}

// 设置获取食物坐标的方法
get X(){//获取x坐标
return this.element.offsetLeft;
}

get Y(){//获取y坐标
return this.element.offsetTop;
}

// 设置重新刷新食物的方法
change(){
// 生成一个随机的位置
// 食物的位置最小是0 最大是290
// 蛇移动一次就是一格,一格的大小就是10,所以就要求食物的坐标必须是整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';
}
}

// 测试代码(可删)
const food = new Food
food.change()

结果展示

image

2. 计分板类(score-panel)

  • 属性:
    • 分数: 记录分数,默认值0
    • 等级: 记录等级,默认值1
    • 获取要修改分数的span标签
    • 获取要修改等级的span标签
  • 方法
    • 分数加分方法(每一次吃到食物,分数+1)
      • 升级条件判断
    • 等级加分方法
      • 等级存在上限,例如10级
      • 等级越高,蛇的移速越快

代码展示:

// 2.计分板类(比较简单)
class ScorePanel{
// score和level用来记录分数和等级
score = 0;
level = 1;

// 分数和等级所在的元素,在构造函数中进行初始化
scoreEle: HTMLElement;
levelEle: HTMLElement;

// 设置一个变量限制等级
maxLevel: number;
// 设置一个变量表示多少分时升级
upScore: number;

constructor(maxLevel: number = 10, upScore: number = 10) {//默认值为10
this.scoreEle = document.getElementById('score')!;//元素不为空
this.levelEle = document.getElementById('level')!;//元素不为空
this.maxLevel = maxLevel;
this.upScore = upScore;
}

//设置一个加分的方法
addScore(){
// 使分数自增(span标签里面的是字符串)
this.scoreEle.innerHTML = `${++this.score}`;
// 判断升级间隔的分数是多少
if(this.score % this.upScore === 0){
this.levelUp();
}
}

// 提升等级的方法(span标签里面的是字符串)
levelUp(){
if(this.level < this.maxLevel){
this.levelEle.innerHTML = ++this.level + '';
}
}
}

export default ScorePanel;

3. 蛇类(snake)

  • 属性:

    • 获取蛇元素的属性
    • 蛇头(用于指向移动位置)
    • 蛇身
  • 方法:

    • 获取蛇头的坐标
    • 设置蛇头坐标
    • 增加蛇身
    • 蛇身移动方法(遍历获取身体,让后一节身体等于前一节身体的坐标)
    • 判断蛇头是否撞到蛇身上(遍历身体获取所有坐标后逐一判断即可)

代码展示:

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');
}

// 获取蛇的坐标(蛇头坐标)
get X(){
return this.head.offsetLeft;
}

// 获取蛇的Y轴坐标
get Y(){
return this.head.offsetTop;
}

// 设置蛇头的坐标
set X(value){

// 如果新值和旧值相同,则直接返回不再修改
if(this.X === value){
return;
}

// X的值的合法范围0-290之间
if(value < 0 || value > 290){
// 进入判断说明蛇撞墙了
throw new Error('蛇撞墙了!');
}

// 修改x时,是在修改水平坐标,蛇在左右移动,蛇在向左移动时,不能向右掉头,反之亦然
if(this.bodies[1] && (this.bodies[1] as HTMLElement).offsetLeft === value){
// console.log('水平方向发生了掉头');
// 如果发生了掉头,让蛇向反方向继续移动
if(value > this.X){
// 如果新值value大于旧值X,则说明蛇在向右走,此时发生掉头,应该使蛇继续向左走
value = this.X - 10;
}else{
// 向左走
value = this.X + 10;
}
}

// 移动身体
this.moveBody();

this.head.style.left = value + 'px';
// 检查有没有撞到自己
this.checkHeadBody();
}

set Y(value){
// 如果新值和旧值相同,则直接返回不再修改
if(this.Y === value){
return;
}

// Y的值的合法范围0-290之间
if(value < 0 || value > 290){
// 进入判断说明蛇撞墙了,抛出一个异常
throw new 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();
}

// 蛇增加身体的方法
addBody(){
// 向element中添加一个div
this.element.insertAdjacentHTML("beforeend", "<div></div>");

}

// 添加一个蛇身体移动的方法
moveBody(){
/*
* 将后边的身体设置为前边身体的位置
* 举例子:
* 第4节 = 第3节的位置
* 第3节 = 第2节的位置
* 第2节 = 蛇头的位置
* */
// 遍历获取所有的身体
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';

}

}

// 检查蛇头是否撞到身体的方法
checkHeadBody(){
// 获取所有的身体,检查其是否和蛇头的坐标发生重叠
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('撞到自己了!');
}
}
}
}

export default Snake;

4. 游戏控制类(gamecontrol)

  • 这个类用来控制游戏规则以及联合其他类实现联动

  • 属性:

    • 食物类引入游戏规则中
    • 记分牌类引入游戏规则中
    • 蛇类引入游戏规则中
    • 蛇的移动方向
    • 记录游戏是否结束标志位
  • 方法:

    • 游戏初始化方法:绑定键盘按键(获取当前移动方法)
    • 蛇的移动:根据点击的键盘方向移动
    • 设置switch分支通过判断键盘方向修改坐标值
    • 检查蛇是否吃到食物:吃到食物,食物重置,蛇身+1,分数+1
    • 开启定时器来实现蛇的自动移动并设置随着等级的提升而减少定时器的时间间隔
    • 根据蛇类判断游戏是否结束
// 引入其他的类
import Snake from "./Snake";
import Food from "./Food";
import ScorePanel from "./ScorePanel";

// 游戏控制器,控制其他的所有类
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,2);

this.init();
}

// 游戏的初始化方法,调用后游戏即开始
init() {
// 绑定键盘按键按下的事件
document.addEventListener('keydown', this.keydownHandler.bind(this));
// 调用run方法,使蛇移动
this.run();
}

/* 按键事件keydownHandler返回值
* chrome: ArrowUp ie: Up
chrome: ArrowDown ie: Down
chrome: ArrowLeft ie: Left
chrome: ArrowRight ie: Right
* */

// 创建一个键盘按下的响应函数
keydownHandler(event: KeyboardEvent) {
// 需要检查event.key的值是否合法(用户是否按了正确的按键)
// 修改direction属性
this.direction = event.key;
}

// 创建一个控制蛇移动的方法
run() {
/*
* 根据方向(this.direction)来使蛇的位置改变
* 向上 top 减少
* 向下 top 增加
* 向左 left 减少
* 向右 left 增加
* */
// 获取蛇现在坐标
let X = this.snake.X;
let Y = this.snake.Y;


// 根据按键方向来修改X值和Y值
switch (this.direction) {
case "ArrowUp":
case "Up":
// 向上移动 top 减少
Y -= 10;
break;
case "ArrowDown":
case "Down":
// 向下移动 top 增加
Y += 10;
break;
case "ArrowLeft":
case "Left":
// 向左移动 left 减少
X -= 10;
break;
case "ArrowRight":
case "Right":
// 向右移动 left 增加
X += 10;
break;
}

// 检查蛇是否吃到了食物
this.checkEat(X, Y);

//修改蛇的X和Y值
try{
this.snake.X = X;
this.snake.Y = Y;
}catch (e){
// 进入到catch,说明出现了异常,游戏结束,弹出一个提示信息
// 严格模式下会捕获所有 unknown 类型的非法使用姿势,没有出错的时候,e为unknown,将严格模式设置为false即可
alert(e.message + ' GAME OVER!');
// 将isLive设置为false
this.isLive = false;
}


// 开启一个定时调用
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;

最后执行打包命令即可生成浏览器可运行的文件

npm run build

image

游戏成品展示

image