【typescript 实践】贪吃蛇小游戏

本文介绍了一个使用TypeScript实现的贪吃蛇游戏项目。游戏包括蛇、食物、得分板等核心组件,通过键盘控制蛇的移动,吃到食物后蛇会增长并增加分数。文章提供了完整的代码实现及项目结构。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

项目结构

在这里插入图片描述

代码

Snake.ts
class Snake {
    snake: HTMLElement;
    head: HTMLElement;
    bodies: HTMLCollectionOf<HTMLElement>;  //数组内 Element 类型转换成 HTMLElement

    stageWidth: number;
    stageHeight: number;

    lengthMT1: boolean = false; // length more than 1

    constructor(stageWidth: number, stageHeight: number) {
        this.snake = document.getElementById('snake')!;
        // 断言可以代替 ! 功能
        this.head = document.querySelector('#snake > div') as HTMLElement;
        // querySelectorAll 获取的是 NodeListOf<Element> 静态集合 故不用
        // this.bodies = document.querySelectorAll('#snake > div');
        // HTMLCollection 动态集合
        this.bodies = this.snake.getElementsByTagName('div');
        this.stageHeight = stageHeight;
        this.stageWidth = stageWidth;
    }

    get X() {
        return this.head.offsetLeft;
    }

    get Y() {
        return this.head.offsetTop;
    }

    set X(value: number) {    //不能设置 offsetLeft, 只读
        if (this.X === value) return;
        if (value < 0 || value > (this.stageWidth - 1) * 10) {
            throw new Error('🐍撞墙了!')
        }
        this.moveBody()
        this.head.style.left = value + 'px';
        this.checkCrash()
    }

    set Y(value: number) {
        if (this.Y === value) return;
        if (value < 0 || value > (this.stageHeight - 1) * 10) {
            throw new Error('🐍撞墙了!')
        }
        this.moveBody()
        this.head.style.top = value + 'px';
        this.checkCrash()
    }

    growUp() {
        this.lengthMT1 = true;
        this.snake.insertAdjacentHTML('beforeend', '<div></div>');
    }

    moveBody() {
        // 此处使用 后一节 身体替代 前一节 身体位置,而不是将蛇尾放到蛇头要更新的位置,
        // 是因为改变内联样式并不会改变 bodies 的顺序,这样通过 bodies 拿到的蛇尾和蛇头dom 并不是视觉上的蛇尾蛇头,
        // 而仍然是文档流中的 第一个子元素 和 最后一个子元素,并不会更新为新的蛇尾蛇头,最终只是不断内耗,两者不断相互替换,整个蛇停滞不前
        for (let i = this.bodies.length - 1; i > 0; i--) {
            let X = this.bodies[i - 1].offsetLeft;
            let Y = this.bodies[i - 1].offsetTop;

            this.bodies[i].style.left = X + 'px';
            this.bodies[i].style.top = Y + 'px';
        }
    }
    checkCrash(){
        for(let i=1;i<this.bodies.length;i++){
            let b = this.bodies[i];
            if(this.X===b.offsetLeft&&this.Y===b.offsetTop) {
                throw new Error('撞到自己了!');
            }
        }
    }
}

export default Snake;

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

    constructor(stageWidth: number, stageHeight: number) {
        // ! 即不存在 null 的情况 or null 也可以赋值
        this.element = document.getElementById('food')!;
        this.stageWidth = stageWidth;
        this.stageHeight = stageHeight;
    }

    // 获取食物坐标轴
    get X() {
        return this.element.offsetLeft;
    }

    get Y() {
        return this.element.offsetTop;
    }

    change() {
        // 长300,宽300 则,X (0,290)    Y(0,290)    X,Y为10倍数 🐍移动一次就是一格,10px
        // Math.round() 四舍五入
        let top = Math.round(Math.random() * (this.stageWidth-1)) * 10;
        let left = Math.round(Math.random() * (this.stageHeight-1)) * 10;
        this.element.style.left = left + 'px';
        this.element.style.top = top + 'px';
    }
}

export default Food;

ScorePanel.ts
// 记分牌
class ScorePanel {
    score = 0;
    level = 1;
    scoreEl: HTMLElement;
    levelEl: HTMLElement;

    maxLevel: number;
    upScore: number;    //多少分升一级

    constructor(maxLevel:number= 10, upScore:number= 10) {
        this.scoreEl = document.getElementById('score')!;
        this.levelEl = document.getElementById('level')!;
        this.maxLevel = maxLevel;
        this.upScore = upScore;
    }

    addScore() {
        this.scoreEl.innerHTML = ++this.score + '';
        if (this.score % this.upScore === 0) this.levelUp();
    }

    levelUp() {
        if (this.level < this.maxLevel)
            this.levelEl.innerHTML = ++this.level + '';
    }
}
export default ScorePanel;

GameControl.ts
import Food from "./Food";
import Snake from "./Snake";
import ScorePanel from "./ScorePanel";

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

    stageWidth: number;
    stageHeight: number;
    isAlive: boolean = true;
    // 蛇的移动方向
    direction: string = '';
    directionOptions = [
        'ArrowUp',  //chrome
        'Up',       //IE
        'ArrowDown',
        'Down',
        'ArrowLeft',
        'Left',
        'ArrowRight',
        'Right',
    ]

    constructor(stageWidth: number = 30, stageHeight: number = 30) {
        this.scorePanel = new ScorePanel()
        this.snake = new Snake(stageWidth, stageHeight)
        this.food = new Food(stageWidth, stageHeight);
        this.stageHeight = stageHeight;
        this.stageWidth = stageWidth;

        this.food.change()
        this.init()
    }

    init() {
        // 绑定键盘按下事件
        // 严格模式下,this默认指向调用函数的对象实例 下行 handleKeydown 函数内 this 为 document
        // document.addEventListener('keydown', this.handleKeydown)
        // ES5 引入了 Function.prototype.bind。 handleKeydown 的 this 将永久地被绑定到了 bind 的第一个参数
        document.addEventListener('keydown', this.handleKeydown.bind(this))
        this.run()
    }

    handleKeydown(e: KeyboardEvent) {
        if (this.directionOptions.indexOf(e.key) > -1) {
            if(this.snake.lengthMT1){//阻止掉头
                if ((this.direction === 'ArrowUp' || this.direction === 'Up') && (e.key === 'ArrowDown' || e.key === 'Down')) {
                    return;
                }
                if ((this.direction === 'ArrowDown' || this.direction === 'Down') && (e.key === 'ArrowUp' || e.key === 'Up')) {
                    return;
                }
                if ((this.direction === 'ArrowLeft' || this.direction === 'Left') && (e.key === 'ArrowRight' || e.key === 'ArrowRight')) {
                    return;
                }
                if ((this.direction === 'ArrowRight' || this.direction === 'ArrowRight') && (e.key === 'ArrowLeft' || e.key === 'Left')) {
                    return;
                }
            }
            this.direction = e.key;
        }
    }

    run() {
        let X = this.snake.X;
        let Y = this.snake.Y;
        switch (this.direction) {
            case 'ArrowUp':
            case 'Up':
                Y -= 10;
                break;
            case 'ArrowDown':
            case 'Down':
                Y += 10;
                break;
            case 'ArrowLeft':
            case 'Left':
                X -= 10;
                break;
            case 'ArrowRight':
            case 'Right':
                X += 10;
                break;
        }

        this.checkEat(X, Y);

        try {
            this.snake.X = X;
            this.snake.Y = Y;
        } catch (e: any) {
            alert(e.message + 'GAME OVER!');
            this.isAlive = false;
        }
        // 开启一个定时调用
        this.isAlive && setTimeout(this.run.bind(this), 200 - (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.growUp();
        }
    }
}

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 #000;
  border-radius: 40px;

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

  #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;
        color: #fff;
      }
    }

    & > #food {
      height: 10px;
      width: 10px;
      position: absolute;

      display: flex;
      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;
  }
}

index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>贪吃蛇</title>
</head>
<body>
<div id="main">
  <div id="stage">
    <div id="snake">
      <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>
index.ts
import './style/index.less'
import GameControl from "./modules/GameControl";

const gameControl = new GameControl()

package.json
{
  "name": "SnakeGame",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack",
    "start": "webpack serve --open --mode development"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.16.0",
    "@babel/preset-env": "^7.16.4",
    "babel-loader": "^8.2.3",
    "clean-webpack-plugin": "^4.0.0",
    "core-js": "^3.19.1",
    "css-loader": "^6.5.1",
    "html-webpack-plugin": "^5.5.0",
    "less": "^4.1.2",
    "less-loader": "^10.2.0",
    "postcss": "^8.3.11",
    "postcss-loader": "^6.2.0",
    "postcss-preset-env": "^7.0.0",
    "style-loader": "^3.3.1",
    "ts-loader": "^9.2.6",
    "typescript": "^4.5.2",
    "webpack": "^5.64.1",
    "webpack-cli": "^4.9.1",
    "webpack-dev-server": "^4.5.0"
  }
}

tsconfig.json
{
  "compilerOptions": {
    "module": "es2015",
    "target": "es2015",
    "strict": true,
    "noEmitOnError": true,
    "removeComments": true,
  },
  "exclude": [
    "node_modules"
  ]
}

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 从后往前 先用ts-loader将ts转换成js 再将js转换成旧版本js
                use: [
                    //配置babel
                    {
                        //指定加载器
                        loader: 'babel-loader',
                        //设置babel
                        options: {
                            //设置预定义的环境
                            presets: [
                                [
                                    //指定环境插件
                                    "@babel/preset-env",
                                    //配置信息
                                    {
                                        targets: {//要兼容的目标浏览器
                                            "chrome": '58',
                                            'ie': "11"
                                        },
                                        "corejs": '3',   //版本3.x.x
                                        "useBuiltIns": "usage"   //使用core-js的方式 按需加载
                                    }
                                ]
                            ]
                        }
                    },
                    'ts-loader'
                ],
                //要排除的文件
                exclude: /node_modules/
            },
            {
                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(),   //删除dist
        new HTMLWebpackPlugin({
            template: "./src/index.html"
        }),
    ],
    // 用来设置引用模块
    resolve: {
        extensions: ['.ts', '.js','.less']
    },
    mode:'production'
}

实现效果

在这里插入图片描述
本来背景是绿色的,不知为何截图就变白了。
实际课程参考尚硅谷的超哥TS课程,但最终实现有出入。
课程地址

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值