前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >刚学会 TypeScript, 顺手做个贪吃蛇小游戏

刚学会 TypeScript, 顺手做个贪吃蛇小游戏

作者头像
小丞同学
发布2022-11-15 19:04:28
3430
发布2022-11-15 19:04:28
举报
文章被收录于专栏:小丞前端库小丞前端库
在这里插入图片描述
在这里插入图片描述

📢 大家好,我是小丞同学,这篇文章将带你制作一个贪吃蛇小游戏 📢 非常感谢你的阅读,不对的地方欢迎指正 🙏 📢 愿你生活明朗,万物可爱

前言

最近在学习中,再次遇到了贪吃蛇的案例,之前刚学 JavaScript 的时候就有遇到过,趁着这段时间有一点点时间,就跟着做了一下,这篇文章将手把手带你实现一个贪吃蛇的小游戏,难度不会很大,嘻嘻

可以从这个案例中学到以下几点:

面向对象编程、this 指向问题、webpack 简单的配置、

一、实现效果预览

贪吃蛇
贪吃蛇

需要实现的功能有以下:

  1. 页面布局
  2. 随机生成食物
  3. 分数统计(吃食物数量)
  4. 等级提升(加速)
  5. 蛇成长
  6. 事件监测
  7. 撞身检测
  8. 撞壁检测
  9. 结束判断

二、代码实现

1. 页面布局

做一个简单的布局,这里主要采用的是 lessflex 布局结合

比较有意思的几点

在布局时,采用了全局变量 bg-color 来定义全局颜色,为代码增加了更多的可扩展性

代码语言:javascript
复制
@bg-color: #b7d4a8;

全局采用了 CSS3 中的盒模型 border-box ,避免了由于边框以及边距对盒原大小造成的影响

代码语言:javascript
复制
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

在绘制蛇身时,需要通过在容器内添加 div 标签的方式来设置,蛇的长度,因此在布局时,需要对容器内的 div 标签单独设置样式

代码语言:javascript
复制
// index.html
<!-- 蛇 -->
<div id="snake">
    <!-- 蛇身 -->
    <div></div>
</div>

// index.less
#snake {
  & > div {
    width: 10px;
    height: 10px;
    background-color: black;
    // 设置间距
    border: 1px solid @bg-color;
    // 开启定位
    position: absolute;
  }
}

对于食物的样式,采用的是 flex 加一个小小的旋转

代码语言:javascript
复制
#food {
  position: absolute;
  width: 10px;
  height: 10px;
  left: 40px;
  top: 100px;
  display: flex;
  flex-flow: row wrap;
  justify-content: space-between;
  align-content: space-between;
  & > div {
    width: 4px;
    height: 4px;
    background-color: black;
    transform: rotate(45deg); 
  }
}

对每个 div 设置旋转一定的角度,好看一点点

这里需要注意的是:由于我们的蛇身以及食物都是需要移动的,我们需要将它们设置为绝定定位方式,并注意父盒子开启相对定位

2. 随机生成食物

我们先梳理一下,食物需要先什么属性或者方法吧

  1. 每个食物要有一个位置,我们通过 XY 属性定位
  2. 同时我们需要一个能够随机生成食物位置的方法
代码语言:javascript
复制
// 定义食物类 Food
class Food {
    // 定义食物元素
    element: HTMLElement;
    constructor() {
        // 获取页面中的 food 元素给 element
        this.element = document.getElementById("food")!
    }
    // 获取食物 x 轴坐标的方法
    get X() {
        return this.element.offsetLeft
    }
    get Y() {
        return this.element.offsetTop
    }
    // 修改食物位置的方法
    change() {
        // 一格大小就是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'
    }
}

在这里我们创建了一个 Food 类,用来定义食物的位置

首先声明了一个 element 属性,指定为 HTMLElement,在constructor 中需要获取到我们的 food 元素赋值给 element 属性

这里由于 ts 的语法检查机制比较严格,我们需要在获取节点的最后加上一个 ! ,表示信任此处的元素获取

这里 TS 其实是做了预判,它担心我们获取不到这个节点而出错,习惯就好,加个 !

在获取食物坐标的方法中,我们采用了 getter 取值函数来取值,我们就可以像使用普通变量一样来获取 XY

由于每次食物被吃了之后,我们都需要生成一个新的食物,其实我们也只是让食物换一个位置而已,始终都是同一个 food 节点,这里我们采用的是 random 来生成一个 0-29 的随机数,然后取10倍,这样就能将位置选择为随机的 10 的倍数,同时在地图范围之内

在这里我们还有很多可以改进的地方,例如我门采用了 29 纯数字,这不利于我们对地图的更改,当地图发生改变时,我们需要修改源码才能改善代码,这不大好,我们可以用一个变量来保存噢

3. 分数统计

在写好 Food 类之后,我们再来写个简单的 ScorePanel 类,用来设置底部的计分和等级

  1. 我们需要有一个分数记录,一个等级记录,以及修改它们的方法
  2. 为了提高可扩展性,我们需要两个变量来控制限制的最大等级,以及达到多少分升级
代码语言:javascript
复制
class ScorePanel {
    // 记录分数和等级
    score = 0;
    level = 1;
    // 分数和等级的元素
    scoreEle: HTMLElement
    levelEle: HTMLElement
    // 设置一个变量的限制等级
    maxLevel: number
    // 设置一个变量 表示多少分时升级
    upScore: number
    constructor(maxLevel: number = 10, upScore: 10) {
        this.scoreEle = document.getElementById("score")!
        this.levelEle = document.getElementById("level")!
        this.maxLevel = maxLevel
        this.upScore = upScore
    }
    // 设置一个加分方法
    addScore() {
        this.scoreEle.innerHTML = ++this.score + '';
        (this.score % this.upScore === 0) && this.levelUp()
    }
    // 提升等级的方法
    levelUp() {
        this.level < this.maxLevel && (this.levelEle.innerHTML = ++this.level + '')
    }
}

我们创建了一个 ScorePanel

在这个类中,我们预先设定了很多的变量,在 TS 中我们需要设置它们的使用类型

在这里我们设置了加分的方法

代码语言:javascript
复制
addScore() {
        this.scoreEle.innerHTML = ++this.score + '';
        (this.score % this.upScore === 0) && this.levelUp()
}

当我们调用这个函数时,就可以实现分数的增加,然后我们需要对当前的分数进行判断,当分数达到我们设置的升级分数时,我们调用类中的 levelUp 方法,让当前的等级提升

4. 蛇的成长

在定义完了基本的周边功能后,我们需要正式的对蛇开始进攻了

我们先创建一个 snake 类,用来设置蛇自身的特性,比如,位置、长度

首先我们需要设置一些变量,用来存储我们的节点

代码语言:javascript
复制
// 蛇头
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")
}

TS 中,我们尽量设置好,以确保我们的变量不会被我们误用导致错误

我们再来定义 gettersetter 方法,用来获取蛇头的位置,以及设置蛇头的位置

为什么要是蛇头呢?

我们需要通过蛇头的移动方向来驱动这个蛇身的移动,因为每个蛇身块都是跟随着上一块蛇身的

代码语言:javascript
复制
// 获取蛇的坐标
get X() {
    return this.head.offsetLeft
}
get Y() {
    return this.head.offsetTop
}

set 中有很多判断,太长了,影响篇幅)

设置好 setget 方法后,我们需要写一个能够使蛇成长的方法,所谓的成长不过就是让 snake 节点中添加多一个 div 元素

代码语言:javascript
复制
// 蛇加身体的方法
addBody() {
    // 向 element 中添加一个 div
    this.element.insertAdjacentHTML("beforeend", "<div></div>")
}

小科普

insertAdjacentHTML() 方法将指定的文本解析为 Element 元素,并将结果节点插入到DOM树中的指定位置。它不会重新解析它正在使用的元素,因此它不会破坏元素内的现有元素。这避免了额外的序列化步骤,使其比直接使用 innerHTML 操作更快。 指定位置有以下几个

  • 'beforebegin':元素自身的前面。
  • 'afterbegin':插入元素内部的第一个子节点之前。
  • 'beforeend':插入元素内部的最后一个子节点之后。
  • 'afterend':元素自身的后面。

5. 控制蛇的移动

现在我们的蛇已经能够添加身体了,但是我们没有添加控制蛇移动的方法,没有办法来展示这个效果

我们继续来看看如何使得蛇能够移动?

我们采用键盘的方向键来控制蛇的移动方向,前面也有提到整个蛇的移动是通过蛇头的驱动的,因此我们先实现控制蛇头的移动

首先我们需要创建一个 GameControl 类,作为这个游戏的控制器,用来控制蛇的移动

首先我们需要有一个键盘响应事件,用来获取用户的键盘事件,同时我们需要对按键进行判断,是否是能够控制蛇移动的四个键

因此我们可以编写两个函数 keydownHandle 键盘事件响应函数run 函数主控制器,判断用户按下的是什么键执行对应变化

我们可以将这两个函数封装到 init 函数中,作为初始化函数一并启动

代码语言:javascript
复制
init() {
    // 绑定键盘事件
    document.addEventListener("keydown", this.keydownHandle.bind(this))
    this.run()
}

在这个函数里,由于我们需要采用 TS 的检查机制,我们可以将事件回调分离成一个函数,但是由于这里的回调调用对象是 document ,我们需要手动更改 this 的指向

我们在 keydownHandle 中处理键盘事件,通过一个 direaction 变量来记录当前的按键

代码语言:javascript
复制
// 存储蛇的移动方向
direction: string = ''

// 键盘响应函数
keydownHandle(event: KeyboardEvent) {
    // 检查是否合法
    this.direction = event.key                    
}

根据 direction 来判断 蛇移动的方向

代码语言:javascript
复制
// 创建蛇移动的方法
run() {
    let X = this.snake.X
    let Y = this.snake.Y
    // 根据按键方向修改值
    switch (this.direction) {
        // 向上 top减少
        case "ArrowUp":
            Y -= 10
            break
        // 向下 top 增加
        case "ArrowDown":
            Y += 10
            break
        // 向左 left 减少
        case "ArrowLeft":
            X -= 10
            break
        // 向右 left 增加
        case "ArrowRight":
            X += 10
            break
    }
}

我们更改了 XY 值后,我们需要将它重新赋值给 snake 中的对应值,由于我们设置了 setter 函数,我们可以直接赋值

代码语言:javascript
复制
this.snake.X = X;
this.snake.Y = Y;

我们通过对四个方向键的 switch 判断,我们使得我们能够控制蛇的移动,但是现在这样还不足以达到不断移动的效果,我们需要实现按下一个方向键后,就不停的向一个方向移动,因此我们可以在 run 中开启一个定时器,使得它能够递归的调用 run

代码语言:javascript
复制
// 递归调用
this.isLive && setTimeout(this.run.bind(this), 300 - (this.scorePanel.level - 1) * 30)

由于我们的蛇有死亡机制,我们需要预先判断以下,这里也存在着 this 指向的问题,我们需要手动调整指向当前的类

在处理到这一步时,我们的蛇头已经能够移动了

snake-2
snake-2

6. 检查吃到食物

现在我们的蛇头已经能够移动了,我们可以去触碰食物以及任何地方了,我们现在需要检查是否吃到食物,吃到食物会怎么样,执行什么函数

代码语言:javascript
复制
// 检查是否吃到食物
checkEat(X: number, Y: number) {
    if (X === this.food.X && Y === this.food.Y) {
        // 食物位置改变
        this.food.change()
        // 加分
        this.scorePanel.addScore()
        // 蛇加一
        this.snake.addBody()
    }
}

在检查是否吃到食物的函数中,我们需要两个参数,也就是蛇头的位置,用来判断是否和食物重叠,如果重叠则改变食物的位置,得分,并且身体加一

7. 控制蛇身移动

现在我们的蛇已经能够吃食物了,但是我们会发现吃完食物后,它的身体不会和它一起走,而是定位到了左上角,因此我们需要处理蛇身移动的问题

由于涉及到 snake 本身的特性,因此我们回到 snake 类中编写

代码语言:javascript
复制
// 添加一个蛇身体移动的方法
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';
    }
}

我们通过循环,从蛇的最后一个蛇块开始遍历,让它的位置变成前一个蛇块的位置

这样就能一个接着一个移动了,不理解的可以想一想噢~

在这段代码中,遇到了很多类型断言的问题,由于 TS 检查机制中不确定数组元素中有没有 offset 类方法,因此会给我们报错提示

8. 撞墙检测

当我们的蛇头撞到墙时,我们需要结束游戏,因此我们需要添加一点判断,同时由于蛇只能往一个方向走,因此我们需要优化以下代码,不需要每次都调用 set Xset Y ,当新值和旧值相同时,我们可以直接返回

代码语言:javascript
复制
set Y(value) {
    // 如果新值和旧值相同,则直接返回不再修改
    if(this.Y === value){
        return;
    }
    if (value < 0 || value > 290) {
        throw new Error('蛇撞墙了')
    }
    // 移动身体
    this.moveBody();
    this.head.style.top = value + 'px';
}

当撞墙时,我们抛出一个错误,然后可以在 GameControl 中采用 try...catch 来捕获这个错误,做出指示

代码语言:javascript
复制
try {
	this.snake.X = X;
	this.snake.Y = Y;
} catch (e: any) {
    alert(e.message + 'GAME OVER')
    // isLive 设置为 false
    this.isLive = false
}

同时结束蛇的生命

9. 掉头检测

由于我们的蛇不能掉头,因此我们需要判断以下用户想反向走时,对这个事件进行处理

我们继续在设置值的函数中添加代码

首先只有一个身体的时候,我们是不需要考虑的,因此我们先要判断是否有第二个蛇身的存在,同时最关键的一点是,这个蛇身的位置是不是和我们即将要行走的 value 值相等

什么意思呢?

在蛇移动的时候,第二节蛇身的位置应该是第一节的位置,蛇头的位置是value 的位置,当蛇头反向时,它的值就会变成第二节身体的位置

image-20210920113407364
image-20210920113407364

画个图好理解一点,圆圈表示蛇头即将到达的位置,右边的方块是蛇头

因此我们添加这段代码,当满足掉头条件时,我们继续让它前进

代码语言:javascript
复制
set Y(value) {
    // 有没有第二个身体
    if (this.bodies[1] && (this.bodies[1] as HTMLElement).offsetTop === value) {
        // 如果掉头,应该继续前进
        if (value > this.Y) {
            value = this.Y - 10
        } else {
            value = this.Y + 10
        }
    }
}

10. 撞身检测

当蛇吃到自己时,需要结束游戏,因此我们需要检测是否吃到自己的身体

我们需要遍历以下蛇身的所有位置,与蛇头的位置进行比较,如果有和蛇头相同的位置,则说明蛇头吃到蛇身了

代码语言:javascript
复制
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('撞到自己了')
        }
    }
}

由于这里我们需要多次类型断言,就提取出来单独断言了

三、总结

整个贪吃蛇游戏的框架就这么多了,在写这篇文章的时候,可以有一些代码篇幅过长,对代码有一点的缩减,可能会影响到阅读或者理解,请见谅

从这个案例中,简单的对 TypeScript 有了一定的认知,但仍然有很多的知识没有被涉及到,感觉这个案例不大行,还需要再练习一下。总的来说,Typescript 相对于 javascipt 来说有很多的限制,这些限制让潜在的未知 bug 都显示了出来,有助于代码的维护同时能够让开发者减少后期找 bug 的苦恼

自己对于 typescript 还有很多未探索的地方,继续努力吧,也欢迎大家提出自己的意见,或者提一点点的建议,让我们一起成长吧!

非常感谢您的阅读,欢迎提出你的意见,有什么问题欢迎指出,谢谢!🎈

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2021-10-07 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 一、实现效果预览
  • 二、代码实现
    • 1. 页面布局
      • 2. 随机生成食物
        • 3. 分数统计
          • 4. 蛇的成长
            • 5. 控制蛇的移动
              • 6. 检查吃到食物
                • 7. 控制蛇身移动
                  • 8. 撞墙检测
                    • 9. 掉头检测
                      • 10. 撞身检测
                      • 三、总结
                      相关产品与服务
                      容器服务
                      腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                      领券
                      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档