TypeScript 实现贪吃蛇游戏

游戏效果如动图所示:蛇吃一个食物就涨一截,不能撞墙和撞自己,每10分升一级,level越高速度越快。

定义食物的类,Food.ts

// 定义食物的类
class Food {
    // 定义一个属性表示食物所对应的元素
    element: HTMLElement;

    constructor() {
        // 获取页面中的food元素并赋值给element属性
        this.element = document.getElementById("food")!;
    }

    // 定义一个获取食物X轴坐标的方法
    get X() {
        return this.element.offsetLeft;
    }

    // 定义一个获取食物Y轴坐标的方法
    get 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";
    }
}

export default Food;

定义计分板的类,ScorePanel.ts

// 定义表示记分牌的类
class ScorePanel {
    // score 用来记录分数
    score = 0;
    // level 用来记录等级
    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() {
        // 使分数自增并更新dom
        this.scoreEle.innerHTML = (++this.score).toString();
        // 当分数达到一定值时,升级。默认是整除10时升级,也可以通过修改upScore的值来改变升级分数
        if (this.score % this.upScore === 0) {
            this.levelUp();
        }
    }

    // 提升等级的方法
    levelUp() {
        if (this.level < this.maxLevel) {
            this.levelEle.innerHTML = (++this.level).toString();
        }
    }
}

export default ScorePanel;

 定义蛇的类,Snake.ts

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

    // 获取蛇的x轴坐标(就是蛇头的坐标)
    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 (this.X > value) {
                // 如果新值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
        ) {
            // console.log("水平方向发生了掉头");
            // 如果发生了掉头,让蛇向反方向继续移动
            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;

 定义游戏控制的类,GameControl.ts

// 引入其他的类
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, 10);

        this.init();
    }

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

    // 创建一个键盘按下的响应函数
    keydownHandler(event: KeyboardEvent) {
        // 需要检查event.key的值是否合法(用户是否按了正确的按键)
        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 (error: any) {
            // 进入到catch,说明出现了异常,游戏结束,弹出一个提示信息
            alert(error.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;

另外,还有界面的样式,index.less

// 设置变量
@bg-color: #b7d4a8;

// 清除默认样式
* {
    margin: 0;
    padding: 0;
    // 改变盒子模型的计算方式
    box-sizing: border-box;
}

body {
    font: bold 20px "Courier";
}

// 设置主窗口的样式
#main {
    width: 360px;
    height: 420px;
    background-color: @bg-color;
    margin: 100px auto;
    border: 10px solid black;
    border-radius: 10px;

    display: flex;
    flex-direction: column;
    justify-content: space-around;
    align-items: center;

    #stage {
        width: 304px;
        height: 304px;
        border: 2px solid black;
        // 开启相对定位
        position: relative;
    }

    // 设置蛇的样式
    #snake {
        & > div {
            width: 10px;
            height: 10px;
            background-color: #000;
            border: 1px solid @bg-color;
            // 开启绝对定位
            position: absolute;
        }
    }

    #food {
        width: 10px;
        height: 10px;
        border: 1px solid @bg-color;

        // 开启绝对定位
        position: absolute;
        left: 40px;
        top: 100px;

        // 开启弹性盒子
        display: flex;
        // flex-wrap: wrap;
        // 设置主轴为横向排列,换行
        flex-flow: row wrap;

        justify-content: space-between;
        align-content: space-between;

        & > div {
            width: 4px;
            height: 4px;
            background-color: #000;
            transform: rotate(45deg);
        }
    }

    // 记分牌
    #score-panel {
        width: 300px;
        display: flex;
        justify-content: space-between;
    }
}

webpack.config.js

// 引入一个包
const path = require("path");
// 引入html插件
const HTMLWebpackPlugin = require("html-webpack-plugin");
// 引入clean插件
const { CleanWebpackPlugin } = require("clean-webpack-plugin");

// webpack中的所有的配置信息都应该写在module.exports中
module.exports = {
    // 指定入口文件
    entry: "./src/index.ts",

    // 指定打包文件所在目录
    output: {
        // 指定打包文件的目录
        path: path.resolve(__dirname, "dist"),
        // 打包后文件的文件
        filename: "bundle.js",

        // 告诉webpack不使用箭头
        environment: {
            arrowFunction: false,
            const: false,
        },
    },

    // 指定webpack打包时要使用模块
    module: {
        // 指定要加载的规则
        rules: [
            {
                // test指定的是规则生效的文件
                test: /\.ts$/,
                // 要使用的loader
                use: [
                    // 配置babel
                    {
                        // 指定加载器
                        loader: "babel-loader",
                        // 设置babel
                        options: {
                            // 设置预定义的环境
                            presets: [
                                [
                                    // 指定环境的插件
                                    "@babel/preset-env",
                                    // 配置信息
                                    {
                                        // 要兼容的目标浏览器
                                        targets: {
                                            chrome: "58",
                                            ie: "11",
                                        },
                                        // 指定corejs的版本
                                        corejs: "3",
                                        // 使用corejs的方式 "usage" 表示按需加载
                                        useBuiltIns: "usage",
                                    },
                                ],
                            ],
                        },
                    },
                    "ts-loader",
                ],
                // 要排除的文件
                exclude: /node-modules/,
            },

            // 设置less文件的处理
            {
                test: /\.less$/,
                use: [
                    // 将js中的css通过创建style标签添加到html文件中
                    "style-loader",
                    // 将css文件变成commonjs模块加载到js中
                    "css-loader",
                    // 引入postcss
                    {
                        loader: "postcss-loader",
                        options: {
                            postcssOptions: {
                                plugins: [
                                    [
                                        "postcss-preset-env",
                                        {
                                            // 配置postcss环境
                                            browsers: "last 2 versions",
                                        },
                                    ],
                                ],
                            },
                        },
                    },
                    // 将less文件编译成css文件
                    "less-loader",
                ],
            },
        ],
    },

    // 配置Webpack插件
    plugins: [
        new CleanWebpackPlugin(),
        new HTMLWebpackPlugin({
            // title: "这是一个自定义的title"
            template: "./src/index.html",
        }),
    ],

    // 用来设置引用模块
    resolve: {
        extensions: [".ts", ".js"],
    },
};

package.json

{
  "name": "snake",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack",
    "start": "webpack serve --open chrome.exe"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.12.9",
    "@babel/preset-env": "^7.12.7",
    "babel-loader": "^8.2.2",
    "clean-webpack-plugin": "^3.0.0",
    "core-js": "^3.8.0",
    "css-loader": "^7.1.2",
    "html-webpack-plugin": "^4.5.0",
    "less": "^4.2.0",
    "less-loader": "^12.2.0",
    "postcss": "^8.4.38",
    "postcss-loader": "^8.1.1",
    "postcss-preset-env": "^9.5.14",
    "style-loader": "^4.0.0",
    "ts-loader": "^8.0.11",
    "typescript": "^4.1.2",
    "webpack": "^5.6.0",
    "webpack-cli": "^4.2.0",
    "webpack-dev-server": "^3.11.0"
  }
}

tsconfig.json

{
  "compilerOptions": {
    "module": "ES2015",
    "target": "ES2015",
    "strict": true,
    "noEmitOnError": true
  }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序猿小野

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值