【TypeScript教程】# 16:ts + webpack + less实现贪吃蛇小游戏

简介: 【TypeScript教程】# 16:ts + webpack + less实现贪吃蛇小游戏

说明

尚硅谷TypeScript教程(李立超老师TS新课)学习笔记。

贪吃蛇源码笔记:https://github.com/kaimo313/typescript-demo/tree/main/greedy-snake



项目搭建

我们以demo3的项目为基础,可以复制一份过来

b9359b63c89042bd864c0fa2c63d2e5c.png


在这个基础上添加less相关的处理

npm i -D less less-loader css-loader style-loader


然后添加postcss处理兼容性问题

npm i -D postcss postcss-loader postcss-preset-env


最后配置webpack

// 设置less文件的处理
{
     test: /\.less$/,
     use: [
         "style-loader",
         "css-loader",
         {
             loader: "postcss-loader",
             options: {
                 postcssOptions: {
                     plugins: [
                         [
                             "postcss-preset-env",
                             {
                                 browsers: "last 2 versions"
                             }
                         ]
                     ]
                 }
             }
         },
         "less-loader"
     ]
 }


能将我们写的less文件编译成css即可:

body {
    background-color: #eee;
    display: flex;
}


c001430494ff4427b5ac4a3fcdb82a40.png


项目界面

界面效果如下:

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="app">
        <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>


样式代码

@bg-color: #ebebeb;
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}
body {
    font: 20px bold "Courier";
}
#app {
    display: flex;
    flex-flow: column;
    align-items: center;
    justify-content: space-around;
    width: 360px;
    height: 420px;
    background-color: @bg-color;
    margin: 100px auto;
    border-radius: 6px;
    box-shadow: 0 2px 10px 0 rgba(0, 0, 0, 0.4);
    #stage {
        width: 304px;
        height: 304px;
        border: 2px solid #c9c9c9;
        position: relative;
        #snake {
            &>div {
                width: 10px;
                height: 10px;
                background-color: blueviolet;
                border: 1px solid @bg-color;
                position: absolute;
            }
        }
        #food {
            width: 10px;
            height: 10px;
            position: absolute;
            top: 100px;
            left: 40px;
            display: flex;
            flex-flow: row wrap;
            justify-content: space-between;
            align-content: space-between;
            &>div {
                width: 4px;
                height: 4px;
                background-color: green;
                transform: rotate(45deg);
            }
        }
    }
    #score-panel {
        display: flex;
        justify-content: space-between;
        width: 304px;
    }
}


完成Food类

class Food{
    // 定义一个属性表示食物所对应的元素
    element: HTMLElement;
    constructor() {
        // 变量后使用 !:表示类型推断排除null、undefined
        this.element = document.getElementById("food")!;
    }
    // 定义一个获取食物的X轴坐标的方法
    get X() {
        return this.element.offsetLeft;
    }
    // 定义一个获取食物的Y轴坐标的方法
    get Y() {
        return this.element.offsetTop;
    }
    // 修改食物位置
    change() {
        // 生成一个随机的位置
        // 食物的位置最小是0,最大是290,一格是10
        let left = Math.round(Math.random() * 29) * 10;
        let top = Math.round(Math.random() * 29) * 10;
        this.element.style.left = `${left}px`;
        this.element.style.top = `${top}px`;
    }
}
export default Food;


每次调用change都可以修改到食物的位置,让其在stage里随机显示

24ec0b047ba54b5ab538ac018333c4d2.png



完成ScorePanel类

// 定义表示记分牌的类
class ScorePanel{
    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;

完成Snake类

// 定义蛇类
class Snake{
    // 表示蛇头所对应的元素
    head: HTMLElement;
    // 蛇的身体(包含蛇头)
    bodies: HTMLCollection;
    // 获取蛇的容器
    element: HTMLElement;
    constructor() {
        // 变量后使用 !:表示类型推断排除null、undefined
        this.element = document.querySelector("#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;
    }
    // 设置蛇头的X轴坐标
    set X(value: number) {
        if(this.X === value) {
            return;
        }
        // 撞墙
        if(value < 0 || value > 290) {
            throw new Error("蛇撞墙了");
        }
        // 修改x时,是在修改水平坐标, 蛇在左右移动,蛇在向左移动时,不能向右掉头, 反之亦然
        if(this.bodies[1] && (this.bodies[1] as HTMLElement).offsetLeft === value) {
            console.log("水平方向发生了调头");
            value = value > this.X ? this.X - 10 : this.X + 10;
        }
        // 移动身体
        this.moveBody();
        this.head.style.left = `${value}px`;
        // 检查蛇头是否撞到身体
        this.checkHeadBody();
    }
    // 设置蛇头的Y轴坐标
    set Y(value: number) {
        if(this.Y === value) {
            return;
        }
        // 撞墙
        if(value < 0 || value > 290) {
            throw new Error("蛇撞墙了");
        }
        // 修改y时,是在修改垂直坐标, 蛇在上下移动,蛇在向上移动时,不能向下掉头, 反之亦然
        if(this.bodies[1] && (this.bodies[1] as HTMLElement).offsetTop === value) {
            console.log("垂直方向发生了调头");
            value = value > this.Y ? this.Y - 10 : this.Y + 10;
        }
        // 移动身体
        this.moveBody();
        this.head.style.top = `${value}px`;
        // 检查蛇头是否撞到身体
        this.checkHeadBody();
    }
    // 蛇增加身体的方法
    addBody() {
        // 向element添加一个div
        // insertAdjacentElement 将一个给定的元素节点插入到相对于被调用的元素的给定的一个位置。
        // beforeend 只在该元素当中,在该元素最后一个子孩子后面。
        this.element.insertAdjacentElement("beforeend", document.createElement("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 = 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键盘事件以及使蛇移动

import Food from "./Food";
import Snake from "./Snake";
import ScorePanel from "./ScorePanel";
// 游戏控制器,控制其他的所有类
class GameControl {
    food: Food;
    snake: Snake;
    scorePanel: ScorePanel;
    // 方向
    direction: String = "";
    // 是否结束
    isLive = true;
    constructor() {
        this.food = new Food();
        this.snake = new Snake();
        this.scorePanel = new ScorePanel(10, 1);
        this.init();
    }
    // 游戏初始化
    init() {
        document.addEventListener("keydown", this.keydownHandler.bind(this));
        this.run();
    }
    /**
     * 按下事件
     * ArrowUp Up
     * ArrowRight Right
     * ArrowDown Down
     * ArrowLeft Left
    */
    keydownHandler(event: KeyboardEvent) {
        console.log(event);
        // 检查是否合法
        this.direction = event.key;
    }
    // 控制蛇移动,根据方向移动
    run() {
        /**
         * ArrowUp Up:top -
         * ArrowRight Right:left +
         * ArrowDown Down:top +
         * ArrowLeft Left:left -
        */
        // 蛇现在的坐标
        let X = this.snake.X; 
        let Y = this.snake.Y;
        switch(this.direction){
            case "ArrowUp":
            case "Up":
                Y -= 10;
                break;
            case "ArrowRight":
            case "Right":
                X += 10;
                break;
            case "ArrowDown":
            case "Down":
                Y += 10;
                break;
            case "ArrowLeft":
            case "Left":
                X -= 10;
                break;
        }
        // 检查是否吃到食物
        this.checkEat(X, Y);
        // 修改XY
        try {
            this.snake.X = X;
            this.snake.Y = Y;
        } catch (e) {
            alert(e);
            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;



实例化GameControl

最后引入控制器实例化即可。

import "./style/index.less";
import GameControl from "./modules/GameControl";
new GameControl();


蛇撞墙和吃食检测效果

c89371be9e15435cb286c0a3a9986b14.png


7504de794342402589eadacbcee548d8.png




目录结构

整体的目录结构如下:

5769c5c55e6f4f4ba6f057653b80a6e7.png


目录
相关文章
|
2月前
|
JavaScript
typeScript基础(3)_ts函数默认值和可选参数
本文介绍了在TypeScript中如何使用函数的默认值和可选参数。展示了如何为函数参数指定默认值,使得在调用函数时可以省略某些参数,以及如何定义可选参数。
160 2
|
23天前
|
前端开发 JavaScript
手敲Webpack 5:React + TypeScript项目脚手架搭建实践
手敲Webpack 5:React + TypeScript项目脚手架搭建实践
|
2月前
|
JavaScript
webpack打包TS
webpack打包TS
132 60
|
1月前
|
JavaScript 前端开发
TypeScript【类型别名、泛型】超简洁教程!再也不用看臭又长的TypeScript文档了!
【10月更文挑战第11天】TypeScript【类型别名、泛型】超简洁教程!再也不用看臭又长的TypeScript文档了!
|
1月前
|
JavaScript 前端开发 Java
TypeScript【接口】超简洁教程!再也不用看臭又长的TypeScript文档了!
【10月更文挑战第10天】TypeScript【接口】超简洁教程!再也不用看臭又长的TypeScript文档了!
|
1月前
|
JavaScript 前端开发 安全
TypeScript【基础类型】超简洁教程!再也不用看臭又长的TypeScript文档了!
【10月更文挑战第9天】TypeScript【基础类型】超简洁教程!再也不用看臭又长的TypeScript文档了!
|
1月前
|
JavaScript 索引
TypeScript(TS)安装指南与基础教程学习全攻略(二)
TypeScript(TS)安装指南与基础教程学习全攻略(二)
52 0
|
1月前
|
JavaScript 前端开发 安全
TypeScript(TS)安装指南与基础教程学习全攻略(一)
TypeScript(TS)安装指南与基础教程学习全攻略(一)
29 0
|
2月前
|
JavaScript 前端开发
typeScript基础(8)_ts类型断言
本文介绍了TypeScript中的类型断言,它用于在编译时告诉TypeScript某个对象具有特定的类型,即使它看起来不具备。类型断言可以用来访问一个类型上存在而另一个类型上不存在的属性或方法。需要注意的是,类型断言并不会在运行时改变JavaScript的行为,因此如果断言不当,运行时仍然可能出错。文章还提醒避免将类型断言为`any`类型或进行多重断言。
36 1
|
3月前
|
JavaScript 前端开发 编译器
TypeScript教程(一)在vscode中的配置TypeScript环境
本文是一篇TypeScript入门教程,介绍了在VS Code中配置TypeScript环境的步骤,包括安装Node.js、使用npm安装TypeScript、配置npm镜像源、安装VS Code的TypeScript扩展,以及创建和运行一个简单的TypeScript "Hello World"程序。
TypeScript教程(一)在vscode中的配置TypeScript环境