TypeScript:项目实战—贪吃蛇

项目实战—贪吃蛇

配置文件 => 画界面 => 写样式 => 写交互 =>最后打包

先讲一下大概思路再上详细代码:
  • 1.进行项目的搭建,安装项目中需要的包,完成项目前的准备
  • 2.进行项目界面的制作,就是画界面,把大概样子画出来,此时没有任何交互。
  • 3.进行类的定义,定义贪吃蛇里面的各类元素:
    • 食物(Food类): 获取食物的坐标、修改食物的位置(随机生成)
    • 记分牌(ScorePanel类): 记录分数和等级、实现加分功能、实现升级功能
    • 蛇(Snake类): 获取和设置蛇头的坐标、蛇身体变长、蛇不能掉头、蛇身体移动、检查蛇头是否撞到身体
    • 游戏控制器(GameControl类): 键盘事件、使蛇移动、蛇撞墙、吃食检测
  • 4.进行项目的归类,通过导出和引用,优化文件结构和代码,实现index.ts直接使用。

先展示一下最后效果:

1.项目搭建

  • 准备好之前的webpack.config.js、tsconfig.json、package.json、package-lock.json四个文件,然后执行npm i安装依赖
  • 安装其他依赖:npm i -D less less-loader css-loader style-loader(四个包,因为要使用到less)如果有其他web资源的话则还需引入web资源的加载器,引入方法类似
  • 修改webpack配置文件—在rules中添加
//设置less文件的处理
{
    test: /\.less$/,
    use:[
        "style-loader",
        "css-loader",
        "less-loader"
    ]
}

这样就能在项目中使用less了。执行npm run build并打开dist中的index.html即可看到效果。

  • 安装postcss来处理css的浏览器兼容性问题:npm i -D postcss postcss-loader postcss-preset-env,并在webpack中引入
//设置less文件的处理
{
    test: /\.less$/,
    use:[
        "style-loader",
        "css-loader",
        //引入postcss
        {
            loader: "postcss-loader",
            options: {
                postcssOptions: {
                    plugins: [
                        [
                            "postcss-preset-env",
                            {
                                browsers: 'last 2 versions'
                            }
                        ]
                    ]
                }
            }
        },
        "less-loader"
    ]
}

这样就可以看到在打包后的js文件中,有些css属性会加上浏览器前缀。

2.项目界面

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">
                <!-- 添加4个小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>

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: 40px;

    // 开启弹性盒模型
    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;
            // background-color: red;

            // 开启弹性盒子
            display: flex;
            // 设置横轴为主轴, wrap表示会自动换行
            flex-flow: row wrap;

            // 设置主轴和侧轴的空白空间分配到元素之间
            justify-content: space-between;
            align-content: space-between;

            left: 40px;
            top: 100px;

            &>div{
                width: 4px;
                height: 4px;
                background-color: black;
                // 使四个div旋转45度
                transform: rotate(45deg);
            }
        }
    }
}

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

3.定义Food类

Food类为定义食物的类

主要实现

  • 获取食物的坐标
  • 修改食物的位置(随机生成)
// 定义食物类Food
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

        // Math.round(Math.random() * 290);//生成一个[0,290]的整数
       let top = Math.round(Math.random() * 29) * 10;
       let left = Math.round(Math.random() * 29) * 10;
        // Math.floor(Math.random() * 30) * 10;//向下取整

        this.element.style.left = top + 'px';
        this.element.style.top = left + 'px';
    }
}

//导出食物模块
export default Food;

4.定义ScorePanel类

ScorePanel类为定义记分牌的类

主要实现

  • 记录分数和等级
  • 实现加分功能
  • 实现升级功能
// 定义表示记分牌的类
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.score++;
        // this.scoreEle.innerHTML = this.score + '';
        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;

5.定义Snake类

Snake类为定义蛇的类

主要实现

  • 获取和设置蛇头的坐标
  • 蛇身体变长
  • 蛇不能掉头
  • 蛇身体移动
  • 检查蛇头是否撞到身体
class Snake{
    // 表示蛇头的元素
    head: HTMLElement;

    // 蛇的身体(包括蛇头)
    bodies: HTMLCollection;

    // 获取蛇的容器
    element: HTMLElement;

    constructor(){
        this.element = document.getElementById('snake')!;
        this.head = document.querySelector('#snake > div') as HTMLElement;
        // document.querySelectorAll('#snake > div');// nodeList
        this.bodies = this.element.getElementsByTagName('div');
    }

    // 获取蛇的坐标(蛇头坐标)
    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("蛇撞墙了~~");
        }
        // 修改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("蛇撞墙了~~");
        }
        
        // 修改Y时,是在修改水平坐标,蛇在上下移动,蛇在向上移动时,不能向下掉头,反之亦然
        if (this.bodies[1] && (this.bodies[1] as HTMLElement).offsetTop === value) {
            // console.log('垂直方向发生了掉头');
            // 如果发生了掉头,让蛇向方向继续移动
            if (value > this.Y) {
                // 如果新值value大于旧值Y, 则说明蛇在向下走,此时发生掉头,应该使蛇继续向上走
                value = this.Y - 10;
            }else {
                // 向上走
                value = this.Y + 10;
            }
        }
        // 移动身体
        this.moveBody();

        this.head.style.top = value + 'px';
        // 检查有没有撞自己
        this.checkHeadBody();
    }

    // 蛇增加身体的方法
    addBody(){
        // 向element中添加一个div
        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;

6.定义GameControl类

GameControl类为游戏控制器,来控制其他的所有类。

主要实现

  • 键盘事件
  • 使蛇移动
  • 蛇撞墙
  • 吃食检测
// 引入其他的类
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, 2);

        this.init();
    }

    // 游戏的初始化方法,调用后游戏即开始
    init() {
        // 绑定键盘按下的事件
        document.addEventListener('keydown', this.keydownHandler.bind(this));
        // 涉及到this和bind知识

        // 调用run()方法,使蛇移动
        this.run();

    }

    /*  谷歌    ie
        ArrowUp Up
        ArrowDown Down
        ArrowRight Right
        ArrowLeft Left
    */
    // 创建一个键盘按下的响应函数
    keydownHandler(event: KeyboardEvent) {
        // console.log(this);
        // 需要检查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);
        // if (this.checkEat(X, Y)) {
        //     console.log('吃到食物了~~');
        //     // 食物的位置进行重置
        //     this.food.change();
        //     // 分数增加
        //     this.scorePanel.addScore();
        //     // 蛇要增加一节
        //     this.snake.addBody();
        // }

        // 修改蛇的X和Y值
        try {
            this.snake.X = X;
            this.snake.Y = Y;
        } catch (e) {
            // 进入到catch, 说明出现了异常,游戏结束,弹出一个提示信息
            alert(e.message+ 'GAME OVER!');
            // 将isLive设置为false
            this.isLive = false;
        }
    

        // 开启一个定时调用
        clearTimeout();
        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;

7.项目入口文件index.ts

将上述定义的四个类放在文件夹modules中,并将GameControl引入到index.ts中。

// 引入样式
import './style/index.less';
import GameControl from "./modules/GameControl";

const gameControl = new GameControl();

// setInterval(()=>{
//     console.log(gameControl.direction);
// }, 1000);
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,001评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,210评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,874评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,001评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,022评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,005评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,929评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,742评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,193评论 1 309
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,427评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,583评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,305评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,911评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,564评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,731评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,581评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,478评论 2 352

推荐阅读更多精彩内容

  • 使用Javascript做贪吃蛇小游戏, 1.自定义地图宽高,蛇的初始速度 2.食物随机出现 3.蛇的样式属性 4...
    花凝冰阅读 644评论 0 0
  • <!DOCTYPE html> Title * { margin: 0; ...
    雨_8882阅读 365评论 0 0
  • HTML canvas 实现贪吃蛇的效果 思路:1. 画蛇 食物被吃掉之后随机位置出现 食物不能随机在蛇身上...
    Agonl_0929a阅读 500评论 0 1
  • 首先设置canvas和样式 #canvas{ box-shadow: 0 0 10px #000; margin:...
    淡然_x阅读 348评论 0 0
  • 思路: 1.创建画布 2.创建的蛇头、身体、食物 3.蛇头、身体、食物随机产生坐标 4.设置蛇头的初始方向 5.设...
    GY_3ade阅读 1,025评论 0 0