H5游戏开发:FC小蜜蜂 | Aotu.io「凹凸实验室」

H5游戏开发:FC小蜜蜂

H5游戏开发:FC小蜜蜂

by TH on 2018-01-28

使用 Phaser 游戏引擎开发,主要介绍游戏引擎的功能及游戏逻辑

前言

说起任天堂 FC 那是充满我们童年寒暑假的回忆,那时候没有正版红白机,玩的是几十块一台的山寨小霸王,十块一张的卡带,玩着魂斗罗、马里奥、淘金者、快打旋风、打鸭子等等。

进入正题,今天我们来说说怎么做一个 FC 小蜜蜂游戏,游戏玩法是通过操控飞机,通过发射子弹对蜜蜂造成伤害,蜜蜂全部歼灭则视为胜利。

游戏演示

初始化

本次游戏采用 Phaser 引擎进行开发,Phaser 是一个快速、免费、易于维护的开源 2D 游戏框架,支持 JavaScript 和 TypeScript 两种语言开发,采用 Pixi.js 引擎作为底层渲染,内置了物理引擎、粒子动画、骨骼动画等效果。

功能介绍

在 Phaser 中有一个重要的概念,我们需要通过**状态(State)**来管理游戏中各个不同的场景,这也是 Phaser 官方建议的游戏代码组织方式,场景可以通过 Phaser.Game.state 来添加(add)和启动(start),每个场景有初始化(init)、预加载(preload)、准备就绪(create)、更新周期(update)、渲染完毕(render) 五种状态,按照顺序依次执行,同一时间只能存在一个场景,并且每个场景中至少包含五种状态中的一个。

比如我们的小蜜蜂游戏一共会分为四个场景:开始场景、游戏场景、获胜场景、失败场景

游戏场景

<table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> game = <span class="keyword">new</span> Phaser.Game(<span class="number">750</span>, <span class="number">1206</span>, Phaser.AUTO, <span class="string">'wrapper'</span>)</span><br><span class="line"><span class="keyword">var</span> states = {}</span><br><span class="line"></span><br><span class="line">states.start = { <span class="comment">// 开始场景</span></span><br><span class="line"> preload: <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>{</span><br><span class="line"> ...</span><br><span class="line"> game.load.image(<span class="string">'example-1'</span>, <span class="string">'images/example-1.png'</span>)</span><br><span class="line"> ...</span><br><span class="line"> },</span><br><span class="line"> create: <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>{</span><br><span class="line"> game.state.start(<span class="string">'play'</span>) <span class="comment">// 加载完成后切换到游戏场景</span></span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">states.play = { ... } <span class="comment">// 游戏场景</span></span><br><span class="line">states.victory = { ... } <span class="comment">// 胜利场景</span></span><br><span class="line">states.defeat = { ... } <span class="comment">// 失败场景</span></span><br><span class="line"></span><br><span class="line">game.state.add(<span class="string">'start'</span>, states.load)</span><br><span class="line">game.state.add(<span class="string">'play'</span>, states.play)</span><br><span class="line">game.state.add(<span class="string">'victory'</span>, states.victory)</span><br><span class="line">game.state.add(<span class="string">'defeat'</span> states.defeat)</span><br><span class="line">game.state.start(<span class="string">'start'</span>)</span><br></pre></td></tr></tbody></table>

无限滚屏

在无限滚屏中,游戏背景沿着 x 轴或者 y 轴重复的滚动,从而实现飞机一直在向前飞的错觉,我们通过创建两个背景,分别初始定位到一屏和二屏的位置,在绘制(update)的过程中持续移动两张背景图的 y 轴,当监听到两个背景超出特定位置后重新定位,从而达到无限循环背景的效果。

无限滚屏

<table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> bg1 = game.add.image(<span class="number">0</span>, <span class="number">0</span>, <span class="string">'background'</span>),</span><br><span class="line"> bg2 = game.add.image(<span class="number">0</span>, -bg1.height, <span class="string">'background'</span>)</span><br><span class="line"></span><br><span class="line">update: <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="comment">// 持续的移动</span></span><br><span class="line"> bg1.y += <span class="number">2</span></span><br><span class="line"> bg2.y += <span class="number">2</span></span><br><span class="line"> <span class="comment">// 超出屏幕判断</span></span><br><span class="line"> <span class="keyword">if</span> (bg1.y &gt;= <span class="number">1206</span>) { bg1.y = <span class="number">0</span> }</span><br><span class="line"> <span class="keyword">if</span> (bg2.y &gt;= <span class="number">0</span>) { bg2.y = -bg1.height }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table>

当然,还有更为简便的方式,Phaser 提供了 TileSprite 平铺纹理,非常适合于这类平铺的背景,再结合 autoScroll() 方法,两行代码解决,另外还有一种叫 TileMaps 平铺的瓦片地图,很适合制作 FC 马里欧这类游戏,以后有机会再开一篇文章讲讲。

<table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> bg = game.add.tileSprite(<span class="number">0</span>, <span class="number">0</span>, <span class="number">750</span>, <span class="number">1206</span>, <span class="string">'background'</span>)</span><br><span class="line">bg.autoScroll(<span class="number">0</span>, <span class="number">200</span>) <span class="comment">// 水平滚动速度、垂直滚动速度</span></span><br></pre></td></tr></tbody></table>

创建一架飞机

飞机的移动我们通过键盘方向键进行控制,通过修改 x、y 值来实现位移,为了有更好的灵活性,我们使用 vx 和 vy 来控制 Sprite 的移动,vx 用于设置 Sprite 在 x 轴上的速度和方向,vy 用于设置 Sprite 在 y 轴上的速度和方向,不直接修改 Sprite 的 x 和 y 值,而是先更新速度值,然后再将这些速度值分配给 Sprite。

<table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">...</span><br><span class="line">airplane.vx = <span class="number">0</span></span><br><span class="line">airplane.vy = <span class="number">0</span></span><br><span class="line"></span><br><span class="line">update: <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>{</span><br><span class="line"> airplane.x += airplane.vx</span><br><span class="line"> airplane.y += airplane.vy</span><br><span class="line">}</span><br><span class="line">...</span><br></pre></td></tr></tbody></table>

接着监听键盘事件,需要注意的就是在弹起状态的时候要判断反向的键是否也已经弹起,避免造成互相干扰。

<table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">...</span><br><span class="line">left.onDown.add(<span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>{ airplane.vx = <span class="number">-8</span> })</span><br><span class="line">left.onUp.add(<span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>{ <span class="keyword">if</span> (!right.isDown) { airplane.vx = <span class="number">0</span> } })</span><br><span class="line">...</span><br></pre></td></tr></tbody></table>

最后是限制飞机的移动范围,我们要限制飞机只在屏幕范围内移动,类似空气墙效果,通过持续监听飞机上下左右四个方向是否碰触到边缘,对坐标进行归位,具体实现代码请看 contain 方法。

四种碰边缘情况

生成子弹

在游戏中,我们需要不断的发射子弹,这就存在一个问题,如何管理子弹?

因为子弹越多会越占用我们的内存,游戏会发现越来越卡,我们使用对象池的方式生成子弹,并且在子弹击中蜜蜂或者超出屏幕时进行销毁。

创建子弹示意图

对象池的本质是复用,通过 Group 和 getFirstExists 来实现。在优化前,我们每次创建子弹都会 new Sprite,使用一次后就丢掉,优化后是创建子弹后会放入对象池中,每次使用从对象池中取,如果对象池中有则使用对象池中的子弹。

<table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">this</span>.bullets = game.add.group() <span class="comment">// 创建对象池</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> bullet = <span class="keyword">this</span>.bullets.getFirstExists(<span class="literal">false</span>) <span class="comment">// 从对象池中取非存活状态的子弹</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (bullet) { <span class="comment">// 对象池中存在则复用</span></span><br><span class="line"> bullet.reset(<span class="keyword">this</span>.airplane.x + <span class="number">16</span>, <span class="keyword">this</span>.airplane.y - <span class="number">20</span>)</span><br><span class="line">} <span class="keyword">else</span> { <span class="comment">// 对象池中不存在则创建一个放入对象池中</span></span><br><span class="line"> bullet = game.add.sprite(<span class="keyword">this</span>.airplane.x + <span class="number">27</span>, <span class="keyword">this</span>.airplane.y - <span class="number">15</span>, <span class="string">'bullet'</span>)</span><br><span class="line"> <span class="keyword">this</span>.bullets.addChild(bullet)</span><br><span class="line">}</span><br></pre></td></tr></tbody></table>

创建一群蜜蜂

整体移动

创建 5 x 5 小蜜蜂是采用 Group 将所有的小蜜蜂对象放入其中,持续移动 Group,检测 Group 左右是否碰壁,进行反方向移动,但你会发现小蜜蜂的左右的某一列被歼灭后,Group 的宽度会随着小蜜蜂列数的变化而变化,而 Group 的 X 轴坐标还是以原来的宽度输出 X 坐标,这就导致我们在计算碰撞墙壁的时候出现问题。

整体移动示意图

因此我们改为通过 Group 来控制整体移动,小蜜蜂负责碰撞检测,当检测到小蜜蜂碰撞后,进行反方向移动,并跳出循环。

<table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span> (<span class="keyword">var</span> i = <span class="number">0</span>; i &lt; galaxians.length; i++) {</span><br><span class="line"> <span class="keyword">var</span> cur = galaxians[i]</span><br><span class="line"> <span class="keyword">if</span> (cur.x + cur.parent.x &lt; <span class="number">0</span> || cur.x + cur.parent.x + cur.width &gt; game.world.width) {</span><br><span class="line"> <span class="comment">// 反向移动</span></span><br><span class="line"> <span class="keyword">break</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table>

随机自杀式袭击

在间隔一段时间后随机小蜜蜂发起攻击,间隔不采用 setInterval 的方式,因为 setInterval 即使在页面最小化或非激活状态依然执行,我们采用 Phaser 提供的 Time 进行间隔触发避免此问题。

<table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">game.time.events.loop(Phaser.Timer.SECOND * <span class="number">1.5</span>, <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>{</span><br><span class="line"> <span class="comment">// 每两秒随机一只小蜜蜂</span></span><br><span class="line"> <span class="keyword">var</span> now = galaxians[(<span class="built_in">Math</span>.floor(<span class="built_in">Math</span>.random() * galaxians.length)]</span><br><span class="line">})</span><br></pre></td></tr></tbody></table>

如何计算小蜜蜂向飞机发起攻击的运动轨迹,这里要借助三角函数的力量来解决,通过飞机位置和蜜蜂位置,获得对边(a)和邻边(b)的长度,根据勾股定理求出斜边(c)长度,知道各边长度后就能得到三角比。另外有一点,Group 的 X 轴在持续的移动,小蜜蜂会受 Group 影响,所以在移动小蜜蜂时要注意。

欧股定理示意图

<table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> a = airplane.x + airplane.width / <span class="number">2</span> - now.x + now.width /<span class="number">2</span> <span class="comment">// 获取 a 边长度</span></span><br><span class="line"><span class="keyword">var</span> b = airplane.y + airplane.height / <span class="number">2</span> - now.y + now.height / <span class="number">2</span> <span class="comment">// 获取 b 边长度</span></span><br><span class="line"><span class="keyword">var</span> c = <span class="built_in">Math</span>.sqrt(a * a + b * b) <span class="comment">// 求出斜边 c 长度</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> speedX = a / c * <span class="number">8</span></span><br><span class="line"><span class="keyword">var</span> speedY = b / c * <span class="number">8</span></span><br><span class="line">now.x += speedX</span><br><span class="line">now.y += speedY</span><br></pre></td></tr></tbody></table>

碰撞检测

在游戏中,我们需要检测子弹与蜜蜂的碰撞和检测蜜蜂与飞机的碰撞,在 2D 游戏中,常用的有轴对齐包围盒(简称 AABB)就是一个每条边都平行于 X 轴或者 Y 轴的矩形。

AABB 可以用两个点表示:最大点和最小点,在 2D 中,最小点就是左下角的点,而最大点则是右上角的点。

通过判断 AABB 与 AABB 是否有存在交叉即可得知是否有碰撞。

AABB示意图

<table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">hitTestRectangle</span>(<span class="params">a, b</span>) </span>{</span><br><span class="line"> <span class="keyword">var</span> hit = (a.max.x &lt; b.min.x) || (b.max.x &lt; a.min.x) || (a.max.y &lt; b.min.y) || (b.max.y &lt; a.min.y) {</span><br><span class="line"> <span class="keyword">return</span> !hit</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table>

以上就是 AABB 与 AABB 碰撞检测的原理,当然,你也可以省事采用 Phaser 提供的物理引擎,在 Phaser 中内置了三种物理引擎,分别是:Arcade Physics、P2 Physics 和 Ninja Physics。

Arcade Physics:是三个中最为简单、性能最快的物理引擎,因为它的碰撞都是采用 AABB 与 AABB 的碰撞,所有的碰撞都是基于一个矩形边界(hitbox)来计算的,所有如果你想碰撞一个圆形的 Sprite,碰撞的则是它的矩形边界,而不是圆形本身,并且支持摩擦力、重力、弹跳、加速等物理效果,适合应用于精度要求不高,较为简单的游戏中。

Arcade Physics示意图

P2 Physics:它是一个更为复杂和逼真的物理引擎,使用 P2 你可以创建弹簧、钟摆、马达等东西,它唯一的缺点在于运算量大,对于性能有较高的要求。

Ninja Physics:比 Arcade Physics 要复杂一点,最初是为 Flash 游戏而创造的,而现在由 Phaser 的作者 Richard Davey 移植到 JavaScript,它与其他物理引擎最大的区别在于支持斜坡碰撞。

Ninja Physics示意图

下面简单介绍一下 Arcade Physics 的使用方法,首先要启动物理引擎

<table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">game.physics.startSystem(Phaser.Physics.ARCADE);</span><br></pre></td></tr></tbody></table>

接着是需要为每个对象开启物理效果,显然一个个创建、添加对象并不高效,我更建议的是通过 Group 的形式添加,这样在 Group 上创建的对象都可以开启物理效果。

<table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">game.physics.arcade.enable(airplane) <span class="comment">// 单独开启方式</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> platforms = game.add.group()</span><br><span class="line">platforms.enableBody = <span class="literal">true</span> <span class="comment">// 组开启方式</span></span><br><span class="line">platforms.create(<span class="number">0</span>, <span class="number">0</span>, <span class="string">'airplane'</span>)</span><br></pre></td></tr></tbody></table>

完成这些以后就可以在 update 阶段使用碰撞检测,overlap 方法可传入两个游戏对象,对象可以是 Sprites、Groups 或者 Emitters,可以执行 Sprite 与 Sprite、Sprite 与 Group、Group 与 Group 的碰撞检测,与 collide 方法不同,该方法的物体不会执行任何的物理效果,它只负责碰撞检测。

<table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">update: <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>{</span><br><span class="line"> game.physics.arcade.overlap(object1, object2, overlapCallback, processCallback, callbackContext)</span><br><span class="line">}</span><br></pre></td></tr></tbody></table>

到此碰撞检测介绍就到这,关于物理引擎的更多使用方法可移步至官网查看。

体验地址

【点击这里体验】键盘方向键控制移动,空格发射子弹,暂时只支持 PC 端体验,另外游戏还有很多可增加的功能,比如:关卡设计(蜜蜂血量、速度、分数)、蜜蜂发射子弹、蜜蜂贝塞尔曲线移动、蜜蜂归位、音乐音效、爆炸动画等等。

尾巴

如果你希望入门 H5 游戏开发,不妨拿这个练练手,源码你可以在体验地址中查看到,Phaser 是很适合作为你入门 H5 游戏开发的一款游戏引擎,等你熟练使用也希望你能阅读源码,了解其中的原理,本文较为简单,感谢你的阅读。

我们会定期更新关于「H5游戏开发」的文章,欢迎关注我们的知乎专栏

参考资料

Learning Pixi.js
Phaser
Setting up Ninja Physics in Phaser
《游戏编程算法与技巧》

H5游戏开发 Phaser

H5游戏开发

感谢您的阅读,本文由 凹凸实验室 版权所有。如若转载,请注明出处:凹凸实验室(https://aotu.io/notes/2018/01/28/galaxian/

上次更新:2021-02-02 15:48:05

动画:从 AE 到 Web

H5游戏开发:消灭星星

var gitalkOpts={id:"bm90ZXMvMjAxOC8wMS8yOC9nYWxheGlhbi8=",owner:"o2team",repo:"o2team.github.io",title:"H5游戏开发:FC小蜜蜂",body:"https://aotu.io/notes/2018/01/28/galaxian/index.html\\n\\n使用 Phaser 游戏引擎开发,主要介绍游戏引擎的功能及游戏逻辑",clientID:"3c4d153e6874260f9c7e",clientSecret:"dd44012504c6168bc05b9266e0554bb28c62ce15",admin:["ONE-SUNDAY"]}

本文转自 https://aotu.io/notes/2018/01/28/galaxian/,如有侵权,请联系删除。

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

一篇文章带你了解JavaScript日期

日期对象允许您使用日期(年、月、日、小时、分钟、秒和毫秒)。一、JavaScript的日期格式一个JavaScript日期可以写为一个字符串:ThuFeb02201909:59:51GMT0800(中国标准时间)或者是一个数字:1486000791164写数字的日期,指定的毫秒数自1970年1月1日00:00:00到现在。1\.显示日期使用