Typescript实现贪吃蛇(三)

180 阅读4分钟

贪吃蛇:

  1. 随机出现食物
  2. 根据键盘方向控制蛇头,开局只有蛇头,吃到食物,蛇身长度为二的时候, 水平方向移动,比如向左, 按向右方向键则无效
  3. 蛇碰墙壁则游戏结束
  4. 蛇身要跟随蛇头一起移动

目录:

目录.jpg

效果:

snake.png

相关依赖:

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack", // 打包
    "start": "webpack serve --open" // 配置devServe指令
  },
"devDependencies": {
    "@babel/core": "^7.16.7",
    "@babel/preset-env": "^7.16.8",
    "babel-loader": "^8.2.3",
    "clean-webpack-plugin": "^4.0.0",
    "core-js": "^3.20.3",
    "css-loader": "^6.5.1",
    "html-webpack-plugin": "^5.5.0",
    "less": "^4.1.2",
    "less-loader": "^10.2.0",
    "postcss": "^8.4.5",
    "postcss-loader": "^6.2.1",
    "postcss-preset-env": "^7.2.3",
    "style-loader": "^3.3.1",
    "ts-loader": "^9.2.6",
    "typescript": "^4.5.4",
    "webpack": "^5.66.0",
    "webpack-cli": "^4.9.1",
    "webpack-dev-server": "^4.7.3"
  }

teconfig.json:

{
    "compilerOptions": {
      "module": "ES2015",
      "target": "es2015",
      "strict": true,
      "noEmitOnError": true  // ts报错的时候不允许编译成js
    }
  }

webpack.config.js

// 引入一个包
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin')

const {
    CleanWebpackPlugin
} = require('clean-webpack-plugin')

// webpack中的所有的配置信息都应该写在module.exports中
module.exports = {

    // 指定入口文件
    entry: "./src/index.ts",

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

        
        environment: {
            // 告诉webpack不要使用箭头函数去加载js,ie不支持
            arrowFunction: false,
            // 让webpack打包不要用const
            const: false
        }

    },

    // 指定webpack打包时要使用模块
    module: {
        // 指定要加载的规则
        rules: [
            {
                // test指定的是规则生效的文件
                test: /\.ts$/,
                // 要使用的loader
                use: [
                    // 配置babel
                    {
                        // 指定加载器
                        loader: 'babel-loader',
                        options: {
                            // 设置预定义的环境
                            presets: [
                                [
                                    // 制定环境的插件
                                    "@babel/preset-env",
                                    // 配置信息
                                    {
                                        targets: {
                                            "chrome": '88',
                                            "ie": '9'
                                        },
                                        // 指定corejs的版本
                                        /**
                                         * 
                                         * 如果不使用这个,ie没有promise,则处理不了,
                                         * ie没有promise,会自动引入corejs自己的版本的promise给ie用
                                         * 
                                         */
                                        "corejs": "3",
                                        // 使用corejs的方法,usage 表示按需加载
                                        "useBuiltIns": "usage"
                                    }
                                ]
                            ]
                        }
                    },
                    'ts-loader'
                ],
                // 要排除的文件
                exclude: /node-modules/
            }, 
            {
                test: /\.less$/,
                use: [
                    'style-loader',
                    'css-loader',
                    {
                        loader: 'postcss-loader',
                        options: {
                            postcssOptions: {
                                plugins: [
                                    [
                                        "postcss-preset-env",
                                        {
                                            browsers: "last 2 versions"
                                        }
                                    ]
                                ]
                            }
                        }
                    },
                    'less-loader'
                ]
            }
        ]
    },

    devtool:'source-map',
    plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
            template: path.resolve(__dirname, 'src/index.html')
        })
    ],

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

    mode: 'production'
};

index.ts: 入口文件

import './index.less'
import GameControl from './module/GameControl'

new GameControl()

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>Document</title>
</head>
<body>
    <div class="main">
        <div class="stage">
            <div class="snake">
                <div></div>
            </div>
            <div class="food">
                <div></div>
                <div></div>
                <div></div>
                <div></div>
            </div>
        </div>
        <div class="score_box">
            <div class="score">
                SCORE: <span>0</span>
            </div>
            <div class="level">
                LEVEL: <span>1</span>
            </div>
        </div>
    </div>
</body>
</html>

index.less: 样式文件

@bg-color: rgb(185, 218, 170);

.main {
    width: 360px;
    height: 420px;
    margin: 100px auto;
    border: 10px solid #000;
    border-radius: 30px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: space-around;
    background-color: @bg-color;
    box-sizing: border-box;

    .stage {
        width: 304px;
        height: 324px;
        border: 2px solid #000;
        position: relative;
        box-sizing: border-box;
        .snake {
            position: absolute;

            &>div {
                position: absolute;
                width: 10px;
                height: 10px;
                background-color: #000;
                border: 1px solid @bg-color;
                box-sizing: border-box;
            }
        }

        .food {
            width: 10px;
            height: 10px;
            position: absolute;
            top: 100px;
            left: 30px;
            display: flex;
            align-items: center;
            justify-content: space-between;
            flex-wrap: wrap;
            &>div {
                width: 4px;
                height: 4px;
                background-color: #000;
            }
        }
    }

    .score_box {
        box-sizing: border-box;
        width: 100%;
        padding: 0 30px;
        display: flex;
        align-items: center;
        justify-content: space-between;
    }
}

面向对象区分模块

  • 食物 Food.ts
  • 分数盘 ScorePanel.ts
  • 蛇 Snake.ts
  • 游戏控制 GameControl.ts(引入上面三个模块,统一操作,再呗index.ts调用)

Food.ts

// 定义Food类
class Food {
    element: HTMLElement

    constructor() {
        // 这里food编译器会认为获取不到,所以后面加个!表明一定存在
        this.element = document.querySelector('.food')!

    }

    // 获取X
    get X() {
        return this.element.offsetLeft
    }

    // 获取Y
    get Y() {
        return this.element.offsetTop
    }


    // 随机改变食物的位置
    change() {
        // 生成一个随机的位置
        // 食物最小位置是0,最大位置是290(应该不能写死)
        // 蛇每次移动都是一格10,所以要求食物的位置必须是10的倍数
        let stage: HTMLElement
        stage = document.querySelector('.stage')!
        // 获取去除父级border的宽高
        let elementWidht = stage.clientWidth
        let elementHeight = stage.clientHeight

        let maxLeft = (elementWidht - this.element.offsetWidth) / this.element.offsetWidth
        let maxTop = (elementHeight - this.element.offsetHeight) / this.element.offsetHeight


        let top = Math.round(Math.random() * maxTop) * this.element.offsetHeight
        let left = Math.round(Math.random() * maxLeft) * this.element.offsetWidth
        this.element.style.left = left + 'px'
        this.element.style.top = top + 'px'
    }

}


export default Food;

ScorePanel.ts

class ScorePanel {
    score = 0
    level = 1

    scoreEle: HTMLElement
    levelEle: HTMLElement

    // 最多的关卡
    maxLevel: number
    // 最大分数
    upScore: number
    
    constructor(maxLevel: number = 10, upScore: number) {
        this.scoreEle = document.querySelector('.score span')!
        this.levelEle = document.querySelector('.level span')!
        this.maxLevel = maxLevel
        this.upScore = upScore
    }

    // 增加分数
    addScore() {
        this.scoreEle.innerHTML = ++this.score + ''
        if(this.score % this.upScore === 0) {
            this.addLevel()
            
        }
    }

    // 提升关卡
    addLevel() {
        if (this.level < this.maxLevel) {
            this.levelEle.innerHTML = ++this.level + ''
        }
    }
}


export default ScorePanel;

Snake.ts

class Snake {
    // 蛇容器
    element: HTMLElement
    // 蛇头
    head: HTMLElement
    // 蛇身体,包括头, HTMLCollection会实时更新元素
    bodies: HTMLCollection

    constructor() {
        this.element = document.querySelector('.snake')!
        this.head = document.querySelector('.snake > div') as HTMLElement

        // 返回的是一个nodeList,固定获取
        // document.querySelectorAll('.snake > div')
        // 返回的是HTMLCollection,就算后续加入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 (value < 0 || value > (document.querySelector('.stage')!.clientWidth - this.head.offsetWidth)) {
            throw ('蛇撞墙了!')
        }
        
        // 身体跟随
        this.moveBody()
        this.head.style.left = value + 'px'
        // 检查是否撞到自己
        this.checkHeadBody()
    }

    // 设置蛇头Y
    set Y(value: number) {
        if (this.Y === value) return
        if (value < 0 || value > (document.querySelector('.stage')!.clientHeight - this.head.offsetHeight)) {
            throw ('蛇撞墙了!')
        }

        
        this.moveBody()
        this.head.style.top = value + 'px'
        this.checkHeadBody()
    }

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


    // 身体跟随移动
    moveBody() {
        
        for(let i = this.bodies.length-1; i > 0; i--) {
            let bodies =  this.bodies
            let x = (bodies[i - 1] as HTMLElement).offsetLeft;
            let y = (bodies[i - 1] as HTMLElement).offsetTop;
            
            (bodies[i] as HTMLElement).style.left = x + 'px';
            (bodies[i] as HTMLElement).style.top = y + 'px';
        }
       
    }

    // 判断是否碰撞自己身体
    checkHeadBody() {
        [...this.bodies].forEach((item, index) => {
            if(index === 0) return
            let bd = (item as HTMLElement)
            if(bd.offsetLeft === this.X && bd.offsetTop === this.Y) {
                throw('撞到自己了')
            }
        })
    }
}


export default Snake

GameControl.ts

import Food from './Food'
import ScorePanel from './ScorePanel'
import Snake from './Snake'


class GameControl {
    Food: Food
    ScorePanel: ScorePanel
    Snake: Snake

    // 存储箭头移动的方向
    direction: string = 'right'

    timeId: any

    isLive: boolean = true

    constructor() {
        this.Food = new Food()
        this.ScorePanel = new ScorePanel(10, 4)
        this.Snake = new Snake()
        this.init()
    }


    init() {
        // 这回调事件绑定在document上的,所以this指向document,需要bind
        document.addEventListener('keydown', this.keydownHandle.bind(this))
        this.run()
    }


    // 键盘按下的回调函数
    /**
     * 
     * @param event 
     *      event.key:   chrome      ie
        *               ArrowUp      up
                        ArrowRight   right
                        ArrowDown    down
                        ArrowLeft    left
     * 
     */
    keydownHandle(event: KeyboardEvent) {
        // 判断是否允许反方向
        if (this.Snake.bodies[1]) {
            if (this.direction === 'ArrowRight' && (event.key === 'ArrowLeft' || event.key === 'left')) return
            if (this.direction === 'ArrowLeft' && (event.key === 'ArrowRight' || event.key === 'right')) return
            if (this.direction === 'ArrowUp' && (event.key === 'ArrowDown' || event.key === 'down')) return
            if (this.direction === 'ArrowDown' && (event.key === 'ArrowUp' || event.key === 'up')) return
        }

        this.direction = event.key
        this.isLive && this.run()
    }


    // 蛇移动
    run() {
        clearTimeout(this.timeId)
        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 'ArrowRight':
            case 'right':
                X += 10
                break
            case 'ArrowLeft':
            case 'left':
                X -= 10
                break
        }

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

        try {
            this.Snake.X = X
            this.Snake.Y = Y
        } catch (e) {
            alert(e)
            this.isLive = false
        }

        if (this.isLive) {
            this.timeId = setTimeout(this.run.bind(this), 500 - (this.ScorePanel.level - 1) * 50)
        }
    }


    // 检查是否吃到食物, 这里包含了食物,蛇本身,所以得放在gameControl处理
    checkEat(x: number, y: number) {
        if (this.Food.X === x && this.Food.Y === y) {
            // 身体增加
            this.Snake.addBody()
            // 食物重新刷新
            this.Food.change()
            // 记分牌加一
            this.ScorePanel.addScore()
        }
    }
}

export default GameControl