• TypeScript中贪吃蛇项目(系列三)


    ## **贪吃蛇练习**
    
    使用TypeScript + Webpack + Less实现贪吃蛇的例子;
    
    ### **项目依赖**
    
    TypeScript:
    
    -   typescript;
    -   ts-loader;
    
    Webpack:
    
    -   webpack;
    -   webpack-cli;
    -   webpack-dev-server;
    -   html-webpack-plugin;
    -   clean-webpack-plugin;
    
    Babel:
    
    -   core-js;
    -   babel-loader;
    -   @babel/core;
    -   @babel/preset-env;
    
    Less & CSS资源:
    
    -   style-loader;
    -   css-loader;
    -   less;
    -   less-loader;
    -   postcss;
    -   postcss-loader;
    -   postcss-preset-env;
    
    ### **项目使用**
    
    #### **编译运行**
    
    在确保已经正确安装node和npm的前提下:
    
    分别执行下面的命令安装依赖并编译项目:
    
    ```bash
    # 安装依赖
    npm i
    # 编译打包
    npm run build
    ```
    
    编译完成后,使用浏览器打开dist目录下的`index.html`即可游玩;
    
    #### **继续开发**
    
    使用`npm run start`进入开发模式;
    
    默认使用Chrome浏览器打开,可以修改`package.json`中的值:
    
    ```json
    {
      "scripts": {
        "test": "echo "Error: no test specified" && exit 1",
        "build": "webpack",
        "start": "webpack serve --open chrome.exe"
      }
    }
    ```

    webapck.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不使用箭头,打包后的js文件
            environment: {
                arrowFunction: false,
                // 不使用const,此时兼容IE 10
                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: [
                        "style-loader",
                        "css-loader",
    
                        // 引入postcss
                        // 类似于babel,把css语法转换兼容旧版浏览器的语法
                        {
                            loader: "postcss-loader",
                            options: {
                                postcssOptions: {
                                    plugins: [
                                        [
                                            // 浏览器兼容插件
                                            "postcss-preset-env",
                                            {
                                                // 每个浏览器最新两个版本
                                                browsers: 'last 2 versions'
                                            }
                                        ]
                                    ]
                                }
                            }
                        },
                        "less-loader"
                    ]
                }
            ]
        },
    
        // 配置Webpack插件
        plugins: [
            new CleanWebpackPlugin(),
            new HTMLWebpackPlugin({
                // title: "这是一个自定义的title"
                template: "./src/index.html"
            }),
        ],
    
        // 用来设置引用模块
        resolve: {
            extensions: ['.ts', '.js']
        }
    
    };

     src-->index.html

    <!DOCTYPE html>
    <html lang="zh">
    <head>
        <meta charset="UTF-8">
        <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>

    src-->style-->index.less

    // 设置变量
    @bg-color: #b7d4a8;
    
    // 清除默认样式
    * {
      margin: 0;
      padding: 0;
      // 改变盒子模型的计算方式
      box-sizing: border-box;
    }
    
    body {
      font: bold 20px "Courier";
    }
    
    //设置主窗口的样式
    #main {
       360px;
      height: 420px;
      background-color: @bg-color;
      // 设置居中
      margin: 100px auto;
      // 设置边框
      border: 10px solid black;
      // 设置圆角
      border-radius: 40px;
    
      // 开启弹性盒模型
      display: flex;
      // 设置主轴的方向
      flex-flow: column;
      // 设置侧轴的对齐方式
      align-items: center;
      // 设置主轴的对齐方式
      justify-content: space-around;
    
      // 游戏舞台
      #stage {
         304px;
        height: 304px;
        border: 2px solid black;
        // 开启相对定位
        position: relative;
    
        // 设置蛇的样式
        #snake {
          & > div {
             10px;
            height: 10px;
            background-color: #000;
            // 蛇的边框颜色和背景颜色相同
            border: 1px solid @bg-color;
            // 开启绝对定位
            position: absolute;
          }
        }
    
        // 设置食物
        #food {
           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 {
             4px;
            height: 4px;
            background-color: black;
    
            // 使四个div旋转45度
            transform: rotate(45deg);
          }
        }
      }
    
      // 记分牌
      #score-panel {
         300px;
        display: flex;
        // 设置主轴对齐方式
        justify-content: space-between;
      }
    }

    src-index.ts 入口文件

    // 引入样式
    import './style/index.less'
    
    // import Food from './modules/Food';
    // const food =  new Food();
    // console.log(food.X, food.Y);
    // food.change();
    // console.log(food.X, food.Y);
    
    import GameControl from "./modules/GameControl";
    let gameControl= new GameControl();
    
    // setInterval(()=>{
    //     console.log(gameControl.direction);
    // }, 1000);

    src-->modules-->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, 1);
    
            this.init();
        }
    
        // 游戏的初始化方法,调用后游戏即开始
        init() {
            // 绑定键盘按键按下的事件
            document.addEventListener('keydown', this.keydownHandler.bind(this));
            // 调用run方法,使蛇移动
            this.run();
        }
    
        /*
            Chrome       IE
            ArrowUp      Up
            ArrowDown    Down
            ArrowLeft    Left
            ArrowRight   Right
        */
    
        // 创建一个键盘按下的响应函数
        keydownHandler(event: KeyboardEvent) {
            // 需要检查event.key的值是否合法(用户是否按了正确的按键)
            // 修改direction属性
            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 (e) {
                // 进入到catch,说明出现了异常,游戏结束,弹出一个提示信息
                alert(e.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;

    src-->modules-->Food.ts

    // 定义食物类Food
    class Food {
        // 定义一个属性表示食物所对应的元素
        private element: HTMLElement;
    
        constructor() {
            // 获取页面中的food元素并将其赋值给element
            // 末尾加上叹号,表示id为food的元素必定存在(非空)
            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';
        }
    }
    
    // 测试代码
    // const food =  new Food();
    // console.log(food.X, food.Y);
    // food.change();
    // console.log(food.X, food.Y);
    
    export default Food;

    src-->modules-->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 + '';
            }
        }
    }
    
    // 测试代码
    // const scorePanel = new ScorePanel(100, 2);
    // for(let i=0; i<200; i++){
    //     scorePanel.addScore();
    // }
    
    export default ScorePanel;

    src-->modules-->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');
        }
    
        // 获取蛇的坐标(蛇头坐标)
        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 (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) {
            // 如果新值和旧值相同,则直接返回不再修改
            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) {
                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
            // beforeend:结束标签之前的位置
            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;
  • 相关阅读:
    Spring boot核心注解
    Spring-boot配置文件
    JAVA操作Excel
    操作系统之基础
    Batch Normalization
    解决ios微信页面回退不刷新的问题
    textarea高度自适应,随着内容增加高度增加
    旋转卡 可以用做登录注册
    一个页面tab标签切换,都有scroll事件的解决办法
    input type="radio" 赋值问题
  • 原文地址:https://www.cnblogs.com/fsg6/p/14530211.html
Copyright © 2020-2023  润新知