HTML5小游戏动手做(二):使用PIXI引擎制作坦克大战游戏

1. 简介

1.1 PIXI 简介

PIXI JS是一款轻量级的HTML5的2D引擎,我看现在它的官网上并没有说自己是“游戏”引擎而是说“The HTML5 Creation Engine”。确实,作为游戏引擎的话它提供的功能比底层也比较单一,基本上聚焦于渲染2D图形和动画。
PixiJS V5
它的特点是:

  • 轻量,压缩后比较小
  • 快速,性能好,在移动设备上也能轻松跑到60帧
  • 支持Canvas 2D和webgl两种底层实现方式,这在过去几年webgl支持参差不齐时确实实用
  • 社区活跃,文档齐全,在github上有28k star,目前已经发展到5.0版,应该不必太担心选用以后烂尾的情况

另外它的局限性:

  • 只支持2D
  • 不支持声音
  • 没有专门的IDE、SDK

PIXI JS官网:https://www.pixijs.com/
Github 地址:https://github.com/pixijs/pixi.js

1.2 坦克大战游戏简介

这次我将以一个简单的小游戏“坦克大战”为例来说说怎么用PIXI来做一个小游戏。这个游戏很简单:玩家控制自己的坦克在沙场上驰骋,躲避敌军的射击并开炮击毁敌军坦克。
代码地址: https://github.com/SpaceSample/tank_game

2. PIXI 引擎入门

2.1 基本概念

这里会解释一下PIXI引擎里面的一些基本概念,事实上它们大部分和其他引擎里的概念都一样。

2.1.1 舞台 Stage

顾名思义,舞台就是展示你的游戏的地方,是所有要显示的东西的总的容器。要显示一个什么东西,要么直接加入舞台中,要么加入已经在舞台中的容器里面。就好象HTML DOM 中的document 元素一样。

2.1.2 容器 Container

顾名思义,容器是用来放东西的,比如放一个精灵。而且容器可以嵌套使用,父容器里面放子容器,子容器里面放孙子容器。
容器有很多很常用的属性比如位置x, y,旋转方向 rotation,缩放 scale,不透明度 alpha 等等。

2.1.3 精灵 Sprite

这次可不能望文生义了。在这里说的精灵,其实是指一个游戏中要显示的对象,比如一个人物,一辆车,一个怪物,甚至某些不需要动作或者交互的对象,比如一棵树,一幢房子。
我们可以使用一个或者几个图片资源来构造一个精灵。简单点说其实它就是一个可以显示图片的容器。它也继承了容器的位置方向等属性。
如果我们希望这个显示对象有一系列的动画,比如说一个跑动的小人,我们可以用动画精灵AnimatedSprite 来实现,只要把动画的每一帧都告诉它就行。

2.1.4 图形 Graphics

除了精灵我们还有办法在屏幕上画东西吗?使用图形 Graphics 里面的对象即可,比如说矩形, 圆,折线等

2.1.5 文字 Text

那么,如果要往屏幕上写字怎么办?比如说游戏里面记分的文字,按钮中的文字… PIXI提供了文字 Text来做这件事。当然,这个文字的功能在排版方面还是远远比不上HTML的。如果你想写一个几千字的图文混排的游戏说明,还是老老实实地写个HTML或者MarkDown来得方便。

2.1.6 资源 Resource

游戏中需要用到的声音,图片,字体等等一系列程序之外的文件。一般来说我们需要在使用之前加载它们,PIXI很贴心地提供了一个内建的资源加载器 ResourceLoader。但是你若是需要更强大的或自定义功能的加载器,还得自己写一个,当然要是有第三方现成的就更方便了。

2.2 坐标系及对象变换

2.2.1 绝对坐标系和相对坐标系

简单来说,绝对坐标系(global)就是以舞台为基准的坐标系,精灵相对应舞台的位置和方向。
在这里插入图片描述
相对坐标系(local)就是以舞台中某一个容器为基准的坐标系,一般来说是精灵的父容器。
在这里插入图片描述
PIXI API提供了两个函数用于帮助坐标变换:

  • “toGlobal” 用于相对坐标转绝对坐标
  • “toLocal” 用于绝对坐标转相对坐标

无论是容器还是精灵他们的坐标(x, y)以及方向(rotation)都是相对于父容器的坐标系的。这样我们就很容易做出骨骼动画(后面会解释)。
坐标原点默认是容器左上角,但我们可以改变容器的坐标原点(anchor)。这样的话不仅坐标系位置变化了,而且旋转当前容器的旋转轴也会跟着移动到新的坐标原点。

2.2.2 对象变换

2.2.2.1 移动 (translate)

即对象坐标位置平移,坐标位置变化但方向不变

2.2.2.2 旋转 (rotate)

即对象坐标不变但方向改变,注意这里的参数单位是弧度不是度数,即一周不是360度而是2PI。

2.2.2.3 缩放 (scale)

对象坐标和反向都不变但是大小变化了

2.2.2.4 扭曲 (skew)

沿着x和y轴的倾斜变换

2.3 动画 Animation

2.3.1 逐帧动画

就像最传统的动画片一样,美工画出精灵的动作的每一帧,比如一个奔跑的米老鼠,他每跑一步都有好几帧图片,逐渐变话,脚和手臂逐渐抬起来再一点点放下。播放时顺序播放,速度比较快时比如1秒30帧以上,就可以看到比较连续的动画。

2.3.1.1 精灵表

逐帧动画虽然很自然流畅,但是也会导致大量的图片文件数量。比如一个精灵有5种动作:跑步,招手,摇头,点头,欢呼。每个动作有1到2秒,每秒有60帧动画,这样这个简单的精灵就有几百张图片才能完全展示。这对资源管理和加载是个很大的挑战。所以人们发明了精灵表,也有人翻译成雪碧图,把逐帧动画的每一帧拼合到一张图片上,然后用一个配置文件记录拼合的信息。这样引擎只需加载一个图片文件就能从里面切割出很多图片。无论从图片大小还是数量上都大大减少了。

2.3.2 插值动画

插值动画是相对于逐帧动画的另一种动画,程序员指定动画的开始和结尾状态,由计算机动态算出中间的每一帧,而不是由美工提前画好,比如说一个球从左边飞到右边的坐标变化;一个轮子旋转其实只是方向变化;一个缩放的动画只是大小变化。

2.3.2.1 骨骼动画

相对坐标和插值动画的技术相组合就有了骨骼动画,这使得游戏里面可以表现相对复杂的动作而不用事先画出大量图片。这很像皮影戏,里面人物(精灵)的每个关节都可以相对于主体活动,排列组合起来就可以实现丰富的动作,多到用逐帧动画几乎无法完成的数量。
如下图,小机器人身上的每个红圈处的关节都可以相对于其父容器旋转移动,就可以拼出复杂的舞步。以胳膊为例,大臂的父容器是机器人的躯干,机器人躯干移动可以带动大臂移动,同时大臂可以沿着肩关节旋转。而小臂的父容器是大臂,大臂的移动和旋转也会带动小臂,而小臂本身也可以沿着肘关节旋转。你可以看到这就像真人和动物的骨骼一样,所以叫做骨骼动画。
在这里插入图片描述

3. 实现细节

3.1 技术选型

  • 编程语言:javascript ES6
  • 渲染引擎:PIXI JS v5
  • 声音引擎://TODO

3.2 环境搭建

我偷了一个懒,直接用create-react-app一键生成了项目,里面项目结构,编译用的babel 和webpack,以及调试用的服务器和脚本一应俱全。
我所做的是在App.js里面去掉react相关的东西(好狠心,卸磨杀驴…)另外加了个eslint帮忙检查代码。
完成后运行yarn start启动服务器,自动打开页面,空白一片,成功建立了一个空项目。

3.3 创建舞台

Pixi很贴心地提供了一个application 类(PIXI.Application)来帮我们建立舞台基本的功能:

  • app.view,pixi创建的DOM对象,其实就是用来渲染游戏的canvas
  • app.stage,即舞台,pixi创建的根容器,将来我们游戏里用的无论是精灵还是容器,都要加到这个舞台上才能看得见。
  • app.ticker,自带的ticker,可以理解为每次渲染一帧前执行
  • app.loader,自备加载器,用于加载静态资源比如图片,声音什么的。

3.4 游戏状态机

在动手写代码之前我们还是得稍微设计一下游戏基本的状态逻辑。这是个状态机。对于我们这个小游戏,很简单:

const GAME_STATUS = {
    UNINIT: 0, //初始态
    INIT: 1, //开始初始化
    LOADED: 2, //资源加载完成
    PLAYING: 3, //游戏中
    GAME_OVER:4 //游戏结束
};

3.5 消息总线

监听器模式是一个减少组件间耦合的好办法,不同组件间通过消息总线收发消息,可以有效避免组件间的复杂相互调用。尤其是游戏这种互动多,状态多的情况。比如我们游戏的状态机从LOADED状态转变成PLAYING状态, 广播这个消息告诉所有相关的组件游戏开始了,于是乎我们的坦克开始监听玩家指挥的操作,敌军的坦克开始出现,背景音乐开始播放… 如果哪天我们突然想加入一些新的组件,比如敌人的飞机,不用修改现有代码,只要让敌机监听游戏开始的消息发动进攻就好了。

3.6 加载页面

网页游戏不同于普通网页,一般来说需要下载的静态资源比较多。为了不让用户等的不耐烦,我们一般都会有一个加载页面。这就是INIT状态对应的时段。最简单的就是显示个加载中的文字,好一点的加个简单转菊花之类的动画表示游戏没停止响应,再专业点的就要加个进度条了,但这样游戏就得知道一共有多少,当前加载了多少。专业的加载器还是比较复杂,咱们就做个简单的循环动画:显示一个加载中的文字,后面跟着省略号,省略号的点点一个个增加,到三个就清空,然后从新增加。
这里我们创建一个文本对象(PIXI.Text)里面写上“加载中”,然后用一个定时器定时给它追加点点。最后监听加载完毕的消息隐藏消失。

3.7 开始页面

游戏加载完成后是LOADED状态而不是立刻进入游戏,这时候我们可以显示游戏菜单,比如开始游戏,退出游戏,游戏设置之类的。然后让玩家点击“开始游戏”再进入游戏转到PLAYING状态。
注意这里有个小技巧:随着浏览器安全限制的提升,现代浏览器(尤其是safari)是禁止js自动播放声音的,游戏可以借助这个点击开始的动作来触发声音播放。
我们建立一个新的文本对象,写上“开始游戏”,注意这里要把它的interactive设成true,然后再加上click的监听。

const startText = new PIXI.Text(START_STR, {
    fontFamily : 'Arial',
    fontSize: 64,
    fill : 0xff1010,
    align : 'center'}
);
startText.interactive = true;
startText.on('click', onClickStart);

3.8 坦克和炮弹

讲到这里终于开始游戏最主要的逻辑了。我自己用inkscape画了俩简陋的坦克。一代表我们的坦克,一个代表敌军的坦克(游戏中会重复克隆出多个,以显示玩家以一当百的强大)
我们的坦克 敌军的坦克
我们的坦克会监听键盘、鼠标或者触摸屏的消息,玩家由此来控制坦克的移动和炮火。
敌军坦克会自动出现,自动开炮。
所以还得画炮弹:
我们的炮弹 敌军的炮弹
那么怎么让坦克和炮弹动起来呢?我们给坦克和炮弹都加上onTick函数注册到game的ticker里面, 然后每一帧都让他们根据自己的速度移动一点点,数值需要试一下但炮弹应该比坦克快:

game.getTicker().add(() => this.onTick());
onTick(){
      this.sp.y -= Math.cos(this.sp.rotation) * EnemyTank.speed;
      this.sp.x += Math.sin(this.sp.rotation) * EnemyTank.speed;
    }

3.9 碰撞检测

然后我们写个碰撞检测的程序来检测,如果我们的炮弹击中敌军坦克,敌军坦克就爆炸,敌军坦克击中我们的坦克,坦克减少一滴血(有点无耻哈,主角总得有点光环不是…)如果没血了Game over。还有如果敌军坦克撞上我们,同时报废,也game over…
坦克爆炸的图片也是简单的矢量图,程序员的手艺,凑合看吧:
坦克爆炸了
貌似PIXI本身没提供碰撞检测功能,所以自己写个简单的:

  • 策略1,炮弹对坦克,炮弹比较小,我们就检测炮弹中心的anchor是不是在坦克精灵的矩形只内,因为坦克也可以斜着开,方向不一定是横向或竖向。这里我们就需要坐标变换了。我们把炮弹的绝对坐标转为相对于坦克的相对坐标,因为坦克的anchor是矩形的中心,所以炮弹相对坐标同时小于坦克的长宽的一半就是碰撞了。
  • 策略2,坦克对坦克,大小差不多,我们就检测两个矩形是不是有重合。
  • 策略3,因为坐标转换和矩形重合的运算量大,如果屏幕上坦克和炮弹多的话会很卡,所以优化一下,先计算两个物体间的距离,如果很远肯定碰不上,如果小于他们半径之和就用上面的策略1,2认真判断一下。计算两个物体间的距离不需要坐标变换,绝对坐标x,y坐标差的平方和即可(理论上要再开方,但乘法总比开方快点,所以我们就用平方和与半径和的平方对比)

4. 总结

总得来说PIXI JS提供了2D游戏引擎最基本的逻辑抽象和图像渲染功能。体积小而且性能很不错。缺点是高级功能就得用第三方或者DIY了,比如碰撞检测。所以适用于小而美的游戏,或者自研实力强的团队。

发布了20 篇原创文章 · 获赞 9 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/time1812/article/details/103806040