用TypeScript写贪吃蛇(完结):蛇身体的移动

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>
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值