天天看点

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

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

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

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

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

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

需要实现的功能有以下:

页面布局

随机生成食物

分数统计(吃食物数量)

等级提升(加速)

蛇成长

事件监测

撞身检测

撞壁检测

结束判断

做一个简单的布局,这里主要采用的是 <code>less</code> 和 <code>flex</code> 布局结合

比较有意思的几点

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

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

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

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

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

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

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

每个食物要有一个位置,我们通过 <code>x</code> 和 <code>y</code> 属性定位

同时我们需要一个能够随机生成食物位置的方法

在这里我们创建了一个 <code>food</code> 类,用来定义食物的位置

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

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

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

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

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

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

在写好 <code>food</code> 类之后,我们再来写个简单的 <code>scorepanel</code> 类,用来设置底部的计分和等级

我们需要有一个分数记录,一个等级记录,以及修改它们的方法

为了提高可扩展性,我们需要两个变量来控制限制的最大等级,以及达到多少分升级

我们创建了一个 <code>scorepanel</code> 类

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

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

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

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

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

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

在 <code>ts</code> 中,我们尽量设置好,以确保我们的变量不会被我们误用导致错误

我们再来定义 <code>getter</code> 和 <code>setter</code> 方法,用来获取蛇头的位置,以及设置蛇头的位置

为什么要是蛇头呢?

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

(<code>set</code> 中有很多判断,太长了,影响篇幅)

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

小科普

<code>insertadjacenthtml()</code> 方法将指定的文本解析为 <code>element</code> 元素,并将结果节点插入到dom树中的指定位置。它不会重新解析它正在使用的元素,因此它不会破坏元素内的现有元素。这避免了额外的序列化步骤,使其比直接使用 <code>innerhtml</code> 操作更快。 指定位置有以下几个 <code>'beforebegin'</code>:元素自身的前面。 <code>'afterbegin'</code>:插入元素内部的第一个子节点之前。 <code>'beforeend'</code>:插入元素内部的最后一个子节点之后。 <code>'afterend'</code>:元素自身的后面。

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

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

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

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

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

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

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

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

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

根据 <code>direction</code> 来判断 蛇移动的方向

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

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

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

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

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

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

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

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

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

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

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

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

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

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

同时结束蛇的生命

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

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

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

什么意思呢?

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

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

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

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

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

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

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

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

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

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

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

继续阅读