前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >手把手教你写一个经典躲避游戏

手把手教你写一个经典躲避游戏

原创
作者头像
HZFEStudio
修改2021-04-25 17:53:23
1.3K0
修改2021-04-25 17:53:23
举报
文章被收录于专栏:HZFEStudio

审核:nightcat

前言

因为作者只是个普普通通的页面仔,并不是从属于游戏行业的开发者。平时会写一些小游戏也只是兴趣使然,脑子里经常会蹦出一些小点子。所以很多知识也只是自己摸索拼拼凑凑来的。

故本文仅在于抛砖引玉,向大家介绍我是如何从零到一,一步一步完成一款能游玩的页面小游戏。如果你本是游戏行业的开发者或是打算步入游戏行业的开发者,建议阅读专业性更强的书籍和学习专业的游戏框架与游戏知识。

提前致谢。


阅读门槛

  • 多少得会点 Typescript
  • 多少知道点 Canvas
  • 多少看过一点阮一峰老师的 ES6 教程
  • 多少还记得一点高中数学

成品图

qq 录屏有点问题,加速了而且色彩也闪来闪去的,文末有在线体验链接,感兴趣的可以去体验一下。


前戏

🔧 初始化开发环境

环境其实不重要,我们只需要一个 canvas 标签和一个支持 typescript 的环境就好了。我这里选用的是最简单迅速的打包工具 Parcel。不需要任何额外配置,直接开箱即用👍。

然后就可以开始引入我们的游戏主体对象了

这里不直接使用 index.ts 来编写游戏内容是为了后续方便增加 UI 界面。通过传递 canvas 组件和配置宽高来 new 一个游戏对象,后续对游戏进程的管理、对画布的渲染都会在这里面实现。

这里随便加了个浅灰色的背景,测试下能否正常渲染

WOW,出现了!这样我们的第一步,开发环境就布置好了。(毫无技术含量 = =

🖼 画布介绍

画布其实就是 <Canvas> 元素,我们可以用它创造了一个上下文,也就是上上图代码中的 ctx,通过调用 ctx 上的 api,我们就可以在画布上绘制出想要展示的内容了。

解决高清屏下模糊的问题

在创建画布时需要考虑的一个点是 DPR 问题,即设备像素比。例如上上图中的代码,我们将 600x600 的画布渲染在一个 600px x 600px 的元素上,在高清屏(DPR >= 2)的场景下,会出现模糊的现象。具体感兴趣为什么模糊的可以自行搜索。总而言之言而总之,要解决在高清屏模糊的问题,我们得将画布等比例放大。

这样在 DPR = 2 的场景下,Canvas 也不会出现模糊的现象😀。

让画布动起来

游戏游戏,不会动那还算游戏吗。所以我们接下来得让画布动起来,这里主要用到的一个 api window.requestAnimationFrame 来告知浏览器尽可能的流畅地(每秒 60 帧)运行我们的游戏。额外需要注意的点是每次重新绘制前都需要先清空画布。

这样我们的画布就以每秒 60 帧的速度在刷新了(虽然现在只有个灰色背景看不出差别。

性能优化

一、多画布渲染

如果你的背景足够复杂,可以考虑单独起一个画布渲染背景。这样就可以不用每秒都需要重新绘制 60 次背景。因为我们这次做的游戏是纯色的背景,所以就单个画布渲染就完事了。

二、离屏渲染

如果你游戏画面很花里胡哨,游戏画面出现了帧数不足的卡顿情况。可以考虑离屏渲染,离屏渲染的原理是创建一个离屏 Canvas 当缓存区,提前把需要重复绘制的内容缓存起来,从而减少 API 调用的损耗,提高渲染效率。具体感兴趣的可以去搜搜,我也没用过。

🧚‍♀️ 精灵 Sprite

精灵实际上就是一个对象,画布上的每一个独立元素都可以看作是精灵。精灵可以包含位置、形状、行为等各种属性。说再多也没代码来得直观。

这样就实现了一个最基础的精灵抽象类了,它包含了一个元素最基本的位置信息,同时提供了两个方法供画布渲染和更新精灵信息。我们之后的精灵实现都会继承该抽象类开发。


正片

🔫 实现子弹精灵

首先我们要确认一个子弹精灵应该有的属性,除了位置外,还需要子弹的半径和颜色以及移动方向和移动速度。

因为子弹都是随机的,所以子弹的位置半径等都应该是在一个范围内随机生成的。具体的游戏设计上我是这样设定的:

  • 子弹在屏幕外生成,并向目标附近的一定范围移动
  • 子弹半径越大,移动速度则越慢
  • 子弹飞出屏幕外时移除,保持屏幕的子弹数量一定

确定好游戏设定后就可以开始敲代码了,首先得先确定好子弹精灵的功能范围,我们只需要给子弹精灵一个位置,一个大小,还有一个目标。而子弹精灵则需要实现根据目标生成对应的移动方向和移动速度。

子弹的移动方向和移动速度我们先暂时留个 TODO,先把子弹的位置半径等属性搞了。还有,为了后续游戏更容易维护,我们把所有游戏配置相关的数值,统一放在 config 里管理。

接下来就可以按设计一步一步实现就完事了:

首先先生成一个随机的子弹半径

然后再随机生成子弹的位置,这里我们在四个方向的屏幕外的边缘,随机位置生成一个子弹

因为我们还没做玩家精灵,所以先暂时 mock 一个目标。并且搞个数组来添加子弹,后续得控制这个数组的长度来控制屏幕上的弹幕密度,最后方法就是这样了:

至此子弹的位置和半径就有了,接下来实现移动方向和移动速度,回到我们的子弹精灵。首先我们得根据半径算出我们的移动速度,因为是半径越大速度越慢,所以用最大的速度去减半径在半径范围内的比例乘以速度的范围:

速度有了,然后现在得将我们的速度分成水平速度和垂直速度。

首先科普下大伙儿平时都不会用到的方法 Math.atan2 ,这个方法可以获得两个点的角度。贴一下 mdn 的概述:

Math.atan2() 返回从原点(0,0)到(x,y)点的线段与x轴正方向之间的平面角度(弧度值),也就是Math.atan2(y,x)

所以假设我们的目标是原地 (0, 0) 那子弹的坐标就是 (子弹x - 目标x, 子弹y - 目标y)。

这样我们就能获取到角度了(这里顺便把目标也随机偏移了下,不然直勾勾的就往目标去就很僵硬)

有了角度之后,简单运用一下高中的三角函数知识,就能很轻松的把我们的速度分成水平速度和垂直速度了。

最后再把绘制子弹和更新子弹的方法随便写一下

记得加上游戏每次渲染后还得更新一下,然后把子弹渲染和子弹更新给加上。

最后我们再修改一下更新逻辑,得控制屏幕中的弹幕密度在一个固定的值。都加上后子弹精就大功告成了!

芜湖!一次成功,弹幕出来了!

😃 实现玩家精灵

玩家精灵相对来说属性上会简单很多,老规矩直接上游戏设定:

  • 玩家形状为三角形▲,方向总是朝着移动方向
  • 可以通过键盘 wsad 和 ↑↓←→ 操控

首先第一步,在开始游戏时,初始化玩家精灵

然后第二步开始画三角形,x 和 y 是三角形的重心,再设定一个重心到三个角的距离 d ,然后我们就可以算出三个点的坐标了

A: (x, y - d)

B: (x - Math.cos(30deg) * d, y + Math.sin(30deg) * d)

C: (x + Math.cos(30deg) * d, y + Math.sin(30deg) * d)

之后照着公式加上代码后,保存看看~

有了,一个小三角形就出来了。

因为需要三角形面向移动方向,所以我们还得加上旋转角度,因为 rotate 默认是基于 (0, 0) 点旋转的,而我们需要基于三角形重心进行旋转,所以我们先使用 translate 进行偏移,偏移到重心旋转完再移动回去,最后别忘了将旋转复位。

接下来可以处理玩家控制移动了。首先我们得支持斜着移动,例如左上右上等等,总共八个方向,所以我们加个字段表示玩家目前向哪个方向移动。

这里我们采用二进制的方法,用 0001 代表上,0010 代表下,0100 代表左,1000 代表右。而且方向和方向可以组合(例如 0101 代表左上),这样我们就能用一个数字表示八个方向了。

我们只需要在按下按键的时候或( | )一下对应的位数,再松开按键的时候再与( & )一下对应的位数取反(~)。就能轻松记录当前前进的方向了。

之后再更新的时候,再按方向去更新位置和旋转角度就大功告成了。

别忘了还有边缘检测,避免玩家跑到区域外。

保存代码,让我们测试一下!

有了!瞧这灵活的小箭头,但是现在碰到子弹没发生什么事,离完成就差最后一步了!

💥 碰撞检测

判断三角形是否与圆形碰撞,我们需要判断两种情况,一种是圆心在三角形中,则发生碰撞。另一种则需要判断圆心到三条边的距离是否小于半径,如果是则发生碰撞。

第一种比较好判断:圆心是否在三角形的路径内。所以我们得把之前绘制三角形路径的代码单独提取一下,并且之后还会用到几个角,所以把几个角的获取也单独提取成一个方法:

然后我们需要用 isPointInPath判断一下圆心是否在这个路径内就可以了:

接下来第二种判断就比较复杂了:判断圆心到三条边的距离,这里需要用到向量的知识:

设三角形的边 AB 向量为 v1,角到圆心的 AO 向量为 v2,我们需要求得 AO 在 AB 向量上的投影 AC 长度为 u。

根据向量的点乘公式:

然后我们再将 v1 进行单位化(归一化),既

然后根据三角函数知识,已知 |v2|cosθ 就是我们需要的投影 u,赶紧用代码实现一下:

这里投影 u 也有三种情况(对应下图123):

  • 第一种是在A点左边时 u 是负数,最近的点为 A 点
  • 第二种是在B点右边时投影超出边的长度,最近的点为 B 点
  • 第三种就是圆正好在边的正上方,最近的点为 C 点

得到圆心距离边最近的点后,用过两点距离公式算出距离,再判断距离是否小于圆心来检测是否碰撞:

然后在更新子弹时,去判断是否射中玩家了(记得游戏结束后再渲染一次,否则会导致画面停留在碰撞前的一刻,看起来像是 BUG)

测试之后,发现不对劲,因为之前玩家精灵旋转用的是 canvas 自带的 API rotate 旋转的,而之后碰撞检测用的确是未旋转的三角形去判断,所以会出现明明没接触也触发碰撞的情况。

解决办法就是将 rotate 旋转改成实打实的三角形三个角旋转,这里需要用到转轴公式:

搞定,赶紧跑起来试试

耶!已经可以正常游玩了,但是这样干巴巴玩也没意思,接下来我们最后完善一下,加上分数计数。

🏁 计数

因为这就是一个坚持时间长短的游戏,所以我们用秒数来当做成绩。这块没什么难度,就不细说了,需要注意的一点是记录时间不能简单的就取时间戳,因为切换浏览器 tab 时游戏是 rAF 会自动暂停的,然后分数还会一直算。

🕹️兼容移动端

这段是本文写完后加的,考虑到现在很多人都是用手机刷文章,所以决定加上移动端支持。这里有两种实现方案

  1. 移动到玩家触碰的位置
  2. 增加虚拟摇杆

因为如果使用方案一,玩家的手指会很遮挡到视野,导致游戏体验很差,所以决定采用方案二,加个虚拟摇杆。

摇杆的相关配置项:

实现上其实也很简单,就是在玩家精灵多加个参数,可以选择控制方式,如果是使用触摸控制,则加入摇杆,我们这里默认是将摇杆中心设定在左下角

然后判断如果是触摸控制,则监听触摸事件

然后加个字段记录下手指按住的地方即可

值得注意的是,当我们触摸位置在摇杆中心的时候,玩家是不移动的,这样游戏可操作性就高很多。所以我们加个 getter 方便后续判断:

然后在更新玩家位置时,再根据控制方式不同区分处理,计算手指触碰位置与摇杆中心的角度就是玩家移动的角度:

最后我们再把摇杆绘制到屏幕上就完成了,具体实现也很简单,就是画两个圆,一个是大的背景圆,一个是玩家目前移动方向的摇杆圆。

大功告成!花了不到半个小时完成了兼容移动端,所以一个完善的代码结构和清晰的代码逻辑是非常重要的,能使后续的维护和功能迭代也变得很轻松。


片尾总结

总的来说实现还是很简单的,不算写文的时间做一个这个小游戏差不多一天就能完成。目前来说代码质量还有很大的优化空间,为了方便阅读理解,有多重复的逻辑计算没有提取出来。


思维拓展

目前只是实现了最基本的功能,如果想要拓展,有很多方向可以做。

例如可以增加关卡设计,因为子弹速度子弹密度都是可以动态配置的。或者增加增益道具,例如玩家加速,缩小玩家大小来降低被撞的几率。

还有能和朋友一起玩比自己一个人玩更有趣,可以再加个玩家精灵分别用wsad和方向键控制,就能实现本地对战了(印象中四五年前我就做过,两个箭头碰撞还会硬直旋转一秒,增加互动性)。

也可以加个虚拟摇杆🕹️兼容移动端,而且游戏本体就依赖一个canvas,把 ui 界面整漂亮点再移植到小程序,也许分分钟就火了呢:-D


原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 阅读门槛
  • 成品图
  • 前戏
    • 🔧 初始化开发环境
      • 🖼 画布介绍
        • 解决高清屏下模糊的问题
        • 让画布动起来
        • 性能优化
      • 🧚‍♀️ 精灵 Sprite
      • 正片
        • 🔫 实现子弹精灵
          • 😃 实现玩家精灵
            • 💥 碰撞检测
              • 🏁 计数
                • 🕹️兼容移动端
                • 片尾总结
                • 思维拓展
                相关产品与服务
                云开发 CloudBase
                云开发(Tencent CloudBase,TCB)是腾讯云提供的云原生一体化开发环境和工具平台,为200万+企业和开发者提供高可用、自动弹性扩缩的后端云服务,可用于云端一体化开发多种端应用(小程序、公众号、Web 应用等),避免了应用开发过程中繁琐的服务器搭建及运维,开发者可以专注于业务逻辑的实现,开发门槛更低,效率更高。
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档