Typescript贪吃蛇项目练习

贪吃蛇项目练习

注意分工明确,遵循单一职责原则,就是一个类只做一件事,把任务分清楚,方便日后维护,分工逐一谁的事谁去做。

1. 项目搭建

该部分可参考前面TypeScript——5. 编译选项、6. 使用webpack打包ts代码部分生成对应的配置文件(可直接使用part3中的配置文件)

  • 在之前的基础上在webpack中引入css插件,使得webpack可以对css代码进行处理并增加css的兼容性

    •   npm i -D less less-loader css-loader style-loader postcss postcss-loader postcss-preset-env
      
  • 修改webpack配置文件,设置less文件的处理

// 引入一个包
const path = require("path");
// 引入插件
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const HtmlWebpackPlugin = require("html-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: /\.ts$/, //匹配以.ts结尾的文件
                // 要使用的loader
                use: [
                    // 配置babel
                    {
                        // 指定加载器
                        loader: "babel-loader",
                        // 设置babel
                        options: {
                            // 设置预定义的环境
                            presets: [
                                [
                                    // 指定环境的插件
                                    "@babel/preset-env",
                                    // 配置信息
                                    {
                                        // 要兼容的目标浏览器
                                        targets: {
                                            chrome: "56",
                                            ie: "8",
                                        },
                                        // 指定corejs版本
                                        corejs: "3",
                                        // 使用corejs的方式
                                        useBuiltIns: "entry", // usage-按需加载
                                    },
                                ],
                            ],
                        },
                    },
                    "ts-loader",
                ],
                // 指定要排除的文件
                exclude: /node_modules/,
            },
            // 设置less文件的处理
            {
                test: /\.less$/,
                // 执行顺序:从下往上
                use: [
                    "style-loader",
                    "css-loader",
                    // 引入postcss
                    {
                        loader: "postcss-loader",
                        options: {
                            postcssOptions: {
                                plugins: [
                                    [
                                        "postcss-preset-env",
                                        // 配置浏览器兼容版本
                                        {
                                            browsers: "last 2 versions",
                                        },
                                    ],
                                ],
                            },
                        },
                    },
                    "less-loader",
                ],
            },
        ],
    },
    // 配置webpack插件
    plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
            // 复制'./src/index.html'文件,并自动引入打包输出的所有资源(JS/CSS)
            template: "./src/index.html",
        }),
    ],
    // 用来设置引用模块
    resolve: {
        extensions: [".ts", "js"],
    },
    mode: 'development',
  // 用于开发环境,不要用于生产环境
  devServer: {
    // 运行代码的目录
    contentBase: path.resolve(__dirname, 'dist'),
    // 监视contentBase目录下的所有文件,一旦文件变化就会reload
    watchContentBase: true,
    watchOptions: {
      // 忽略文件
      ignored: /node_modules/
    },
    // 端口号
    port: 5000,
    // 域名
    host: 'localhost',
    // 自动打开浏览器
    open: true,
    // 开启HMR功能
    hot: true,
  }
};

  • 编译项目测试是否编译成功:npm run build
  • npm run start 运行

2. 项目界面

  • src/style/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: 30px;

    // 开启弹性盒模型
    display: flex;
    // 设置主轴的方向
    flex-flow: column;
    // 设置侧轴的对齐方式
    align-items: center;
    // 设置主轴的对齐方式
    justify-content: space-around;

    // 游戏舞台
    #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;
            // 开启绝对定位
            position: absolute;
            left: 40px;
            top: 100px;
            // 开启弹性盒
            display: flex;
            // 设置横轴为主轴,wrap表示会自动换行
            flex-flow: row wrap;
            // 设置主轴和侧轴的空白控件分配到元素之间
            justify-content: space-between;
            align-content: space-between;
            // 设置div旋转45度
            transform: rotate(45deg);

            & > div {
                width: 4px;
                height: 4px;
                background-color: #000;
            }
        }
    }

    // 游戏记分牌
    #score-panel {
        width: 300px;
        display: flex;
        // 设置主轴对齐方式
        justify-content: space-between;
    }
}

  • src/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 id="score-panel">
                <div>SCORE:<span id="score">0</span></div>
                <div>LEVEL:<span id="level">1</span></div>
            </div>
        </div>
    </body>
</html>

  • npm run start
    在这里插入图片描述

3. 完成Food类

src/module/Food.ts

// 引入样式
import "./style/index.less";

// 定义类
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 left = Math.round(Math.random() * 29) * 10;  // 四舍五入0到29取整再乘以10
        let top = Math.round(Math.random() * 29) * 10; 
        this.element.style.left = left + "px";
        this.element.style.top = top + "px";
    }
}

// 测试代码
const food = new Food();
console.log(food.X, food.Y);
food.change();
console.log(food.X, food.Y);

4. 完成ScorePanel类

src/module/ScorePanel.ts

// 定义表示积分牌的类
class ScorePanel {
    // score和level用来记录分数和等级
    score = 0;
    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() {
        // 使分数自增
        this.scoreEle.innerHTML = ++this.score + "";
        // 判断分数是多少
        if (this.score % this.upScore == 0) {
            this.levelUp();
        }
    }

    // 等级提升
    levelUp() {
        if (this.level < this.maxLevel) {
            this.levelEle.innerHTML = ++this.level + "";
        }
    }
}
export default ScorePanel;

// 测试代码
// const scorePanel = new ScorePanel(10, 50);
// for (let i = 0; i < 200; i++) {
//     scorePanel.addScore();
// }

5. 完成Snake类

src/module/Snake.ts

class Snake {
    // 表示蛇头的元素
    head: HTMLElement;
    // 蛇的身体(包括蛇头)
    bodies: HTMLCollection; // 集合
    // 获取蛇的容器
    element: HTMLElement;


    constructor() {
        this.head = document.querySelector("#snake > div")!;
        this.bodies = document.getElementById("#snake")?.getElementsByTagName("div")!;
        this.element = document.getElementById("#snake")!;
    }

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

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

    // 设置蛇坐标
    set X(value:number) {
        this.head.style.left = value + "px";
    }

    set Y(value:number) {
        this.head.style.top = value + "px";
    }

    // 蛇增加身体
    addBody() {
        // 想element添加一个div
        this.element.insertAdjacentHTML("beforeend", "<div></div>");
    }

}
export default Snake;

6. GameControl键盘事件

src/module/GameControl.ts

// 引入其他类
import Food from "./Food";
import ScorePanel from "./ScorePanel";
import Snake from "./Snake";

// 游戏控制器,控制其他所有类
class GameControl {
    // 定义三个属性
    // 蛇
    snake: Snake;
    // 食物
    food: Food;
    // 记分牌
    scorePanel: ScorePanel;

    // 创建一个属性来存储蛇的移动方向(也就是按键方向)
    direction: string = "";
    /**
     * ArrowUp Up
     * ArrowDown Down
     * ArrowLeft Left
     * ArrowRight Right
     */

    // 记录游戏是否结束
    isLive = true;

    constructor() {
        this.snake = new Snake();
        this.food = new Food();
        this.scorePanel = new ScorePanel();

        this.Init();
    }

    // 游戏的初始化方法
    Init() {
        // 调用后游戏即开始
        // bind()方法主要就是将函数绑定到某个对象,bind()会创建一个函数,函数体内的this对象的值会被绑定到传入
        // bind()第一个参数的值,例如,f.bind(obj),实际上可以理解为obj.f(),这时,f函数体内的this自然指向的是obj
        document.addEventListener("keydown", this.keydownHandler.bind(this));

        // 调用run方法,使蛇动起来
        this.run();
    }

    // 创建一个键盘按下得响应函数
    keydownHandler(event: KeyboardEvent) {
        // 检查event.key的值是否合法(用户是否按了正确的按键)
        // 修改direction属性
        this.direction = event.key;
        console.log(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;
        }

        // 修改蛇的X和Y
        this.snake.X = X;
        this.snake.Y = Y;

        // 开启一个定时调用
        this.isLive && setTimeout(this.run.bind(this), 300 - (this.scorePanel.level - 1) * 30);
    }
}

export default GameControl;

7. 蛇撞墙和吃食检测

src/module/GameControl.ts

// 引入其他类
import Food from "./Food";
import ScorePanel from "./ScorePanel";
import Snake from "./Snake";

// 游戏控制器,控制其他所有类
class GameControl {
    // 定义三个属性
    // 蛇
    snake: Snake;
    // 食物
    food: Food;
    // 记分牌
    scorePanel: ScorePanel;

    // 创建一个属性来存储蛇的移动方向(也就是按键方向)
    direction: string = "";
    /**
     * ArrowUp Up
     * ArrowDown Down
     * ArrowLeft Left
     * ArrowRight Right
     */

    // 记录游戏是否结束
    isLive = true;

    constructor() {
        this.snake = new Snake();
        this.food = new Food();
        this.scorePanel = new ScorePanel();

        this.Init();
    }

    // 游戏的初始化方法
    Init() {
        // 调用后游戏即开始
        // bind()方法主要就是将函数绑定到某个对象,bind()会创建一个函数,函数体内的this对象的值会被绑定到传入
        // bind()第一个参数的值,例如,f.bind(obj),实际上可以理解为obj.f(),这时,f函数体内的this自然指向的是obj
        document.addEventListener("keydown", this.keydownHandler.bind(this));

        // 调用run方法,使蛇动起来
        this.run();
    }

    // 创建一个键盘按下得响应函数
    keydownHandler(event: KeyboardEvent) {
        // 检查event.key的值是否合法(用户是否按了正确的按键)
        // 修改direction属性
        this.direction = event.key;
        console.log(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) {
            // 进入catch说明出现了异常,游戏结束
            alert(error.message);
            // 将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) {
            console.log("吃到食物了!!!");
            // 食物的位置要进行重置
            this.food.change();
            // 分数增加
            this.scorePanel.addScore();
            // 蛇要增加一节
            this.snake.addBody();
        }
    }
}

export default GameControl;

8. 身体的移动

src/module/Snake.ts

class Snake {
    // 表示蛇头的元素
    head: HTMLElement;
    // 蛇的身体(包括蛇头)
    bodies: HTMLCollection; // 集合
    // 获取蛇的容器
    element: HTMLElement;

    constructor() {
        this.head = document.querySelector("#snake > div")!;
        this.bodies = document.getElementById("snake")?.getElementsByTagName("div")!;
        this.element = document.getElementById("snake")!;
    }

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

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

    // 设置蛇坐标
    set X(value: number) {
        // 如果新值和旧值相同,则直接返回不再修改
        if (this.X == value) {
            return;
        }
        // X值的合法范围:0~290
        if (value < 0 || value > 290) {
            // 进入判断说明蛇撞墙了,抛出异常
            throw new Error("蛇撞墙了!GAME OVER");
        }

        // 修改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: number) {
        if (this.Y == value) {
            return;
        }
        // Y值的合法范围:0~290
        if (value < 0 || value > 290) {
            // 进入判断说明蛇撞墙了
            throw new Error("蛇撞墙了!GAME OVER");
        }

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

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

        this.head.style.top = value + "px";

        // 检查有没有撞自己
        this.checkHeadBody();
    }

    // 蛇增加身体
    addBody() {
        // 想element添加一个div
        console.log(this);
        this.element.insertAdjacentHTML("beforeend", "<div></div>");
    }

    // 蛇身体移动的方法
    moveBody() {
        /**
         * 将后边的身体设置为前边身体的位置(从后往前改)
         *      举例子:
         *          第四节 = 第三节的位置
         *          第三节 = 第二节的位置
         *              以此类推
         */
        // 遍历获取所有的身体
        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 = this.bodies.length -1; i > 0; i--) {
            let bd = this.bodies[i] as HTMLElement
            if(this.X === bd.offsetLeft && this.Y === bd.offsetTop) {
                // 进入判断说明蛇头撞到身体,游戏结束
                throw new Error("撞到自己了!GAME OVER!");
            }
        }
    }

}
export default Snake;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值