PIXI JS是一款轻量级的HTML5的2D引擎,我看现在它的官网上并没有说自己是“游戏”引擎而是说“The HTML5 Creation Engine”。确实,作为游戏引擎的话它提供的功能比底层也比较单一,基本上聚焦于渲染2D图形和动画。
它的特点是:
另外它的局限性:
PIXI JS官网:https://www.pixijs.com/
Github 地址:https://github.com/pixijs/pixi.js
这次我将以一个简单的小游戏“坦克大战”为例来说说怎么用PIXI来做一个小游戏。这个游戏很简单:玩家控制自己的坦克在沙场上驰骋,躲避敌军的射击并开炮击毁敌军坦克。
代码地址: https://github.com/SpaceSample/tank_game
这里会解释一下PIXI引擎里面的一些基本概念,事实上它们大部分和其他引擎里的概念都一样。
顾名思义,舞台就是展示你的游戏的地方,是所有要显示的东西的总的容器。要显示一个什么东西,要么直接加入舞台中,要么加入已经在舞台中的容器里面。就好象HTML DOM 中的document 元素一样。
顾名思义,容器是用来放东西的,比如放一个精灵。而且容器可以嵌套使用,父容器里面放子容器,子容器里面放孙子容器。
容器有很多很常用的属性比如位置x, y,旋转方向 rotation,缩放 scale,不透明度 alpha 等等。
这次可不能望文生义了。在这里说的精灵,其实是指一个游戏中要显示的对象,比如一个人物,一辆车,一个怪物,甚至某些不需要动作或者交互的对象,比如一棵树,一幢房子。
我们可以使用一个或者几个图片资源来构造一个精灵。简单点说其实它就是一个可以显示图片的容器。它也继承了容器的位置方向等属性。
如果我们希望这个显示对象有一系列的动画,比如说一个跑动的小人,我们可以用动画精灵AnimatedSprite 来实现,只要把动画的每一帧都告诉它就行。
除了精灵我们还有办法在屏幕上画东西吗?使用图形 Graphics 里面的对象即可,比如说矩形, 圆,折线等
那么,如果要往屏幕上写字怎么办?比如说游戏里面记分的文字,按钮中的文字… PIXI提供了文字 Text来做这件事。当然,这个文字的功能在排版方面还是远远比不上HTML的。如果你想写一个几千字的图文混排的游戏说明,还是老老实实地写个HTML或者MarkDown来得方便。
游戏中需要用到的声音,图片,字体等等一系列程序之外的文件。一般来说我们需要在使用之前加载它们,PIXI很贴心地提供了一个内建的资源加载器 ResourceLoader。但是你若是需要更强大的或自定义功能的加载器,还得自己写一个,当然要是有第三方现成的就更方便了。
简单来说,绝对坐标系(global)就是以舞台为基准的坐标系,精灵相对应舞台的位置和方向。
相对坐标系(local)就是以舞台中某一个容器为基准的坐标系,一般来说是精灵的父容器。
PIXI API提供了两个函数用于帮助坐标变换:
无论是容器还是精灵他们的坐标(x, y)以及方向(rotation)都是相对于父容器的坐标系的。这样我们就很容易做出骨骼动画(后面会解释)。
坐标原点默认是容器左上角,但我们可以改变容器的坐标原点(anchor)。这样的话不仅坐标系位置变化了,而且旋转当前容器的旋转轴也会跟着移动到新的坐标原点。
即对象坐标位置平移,坐标位置变化但方向不变
即对象坐标不变但方向改变,注意这里的参数单位是弧度不是度数,即一周不是360度而是2PI。
对象坐标和反向都不变但是大小变化了
沿着x和y轴的倾斜变换
就像最传统的动画片一样,美工画出精灵的动作的每一帧,比如一个奔跑的米老鼠,他每跑一步都有好几帧图片,逐渐变话,脚和手臂逐渐抬起来再一点点放下。播放时顺序播放,速度比较快时比如1秒30帧以上,就可以看到比较连续的动画。
逐帧动画虽然很自然流畅,但是也会导致大量的图片文件数量。比如一个精灵有5种动作:跑步,招手,摇头,点头,欢呼。每个动作有1到2秒,每秒有60帧动画,这样这个简单的精灵就有几百张图片才能完全展示。这对资源管理和加载是个很大的挑战。所以人们发明了精灵表,也有人翻译成雪碧图,把逐帧动画的每一帧拼合到一张图片上,然后用一个配置文件记录拼合的信息。这样引擎只需加载一个图片文件就能从里面切割出很多图片。无论从图片大小还是数量上都大大减少了。
插值动画是相对于逐帧动画的另一种动画,程序员指定动画的开始和结尾状态,由计算机动态算出中间的每一帧,而不是由美工提前画好,比如说一个球从左边飞到右边的坐标变化;一个轮子旋转其实只是方向变化;一个缩放的动画只是大小变化。
相对坐标和插值动画的技术相组合就有了骨骼动画,这使得游戏里面可以表现相对复杂的动作而不用事先画出大量图片。这很像皮影戏,里面人物(精灵)的每个关节都可以相对于主体活动,排列组合起来就可以实现丰富的动作,多到用逐帧动画几乎无法完成的数量。
如下图,小机器人身上的每个红圈处的关节都可以相对于其父容器旋转移动,就可以拼出复杂的舞步。以胳膊为例,大臂的父容器是机器人的躯干,机器人躯干移动可以带动大臂移动,同时大臂可以沿着肩关节旋转。而小臂的父容器是大臂,大臂的移动和旋转也会带动小臂,而小臂本身也可以沿着肘关节旋转。你可以看到这就像真人和动物的骨骼一样,所以叫做骨骼动画。
我偷了一个懒,直接用create-react-app一键生成了项目,里面项目结构,编译用的babel 和webpack,以及调试用的服务器和脚本一应俱全。
我所做的是在App.js里面去掉react相关的东西(好狠心,卸磨杀驴…)另外加了个eslint帮忙检查代码。
完成后运行yarn start启动服务器,自动打开页面,空白一片,成功建立了一个空项目。
Pixi很贴心地提供了一个application 类(PIXI.Application)来帮我们建立舞台基本的功能:
在动手写代码之前我们还是得稍微设计一下游戏基本的状态逻辑。这是个状态机。对于我们这个小游戏,很简单:
const GAME_STATUS = {
UNINIT: 0, //初始态
INIT: 1, //开始初始化
LOADED: 2, //资源加载完成
PLAYING: 3, //游戏中
GAME_OVER:4 //游戏结束
};
监听器模式是一个减少组件间耦合的好办法,不同组件间通过消息总线收发消息,可以有效避免组件间的复杂相互调用。尤其是游戏这种互动多,状态多的情况。比如我们游戏的状态机从LOADED状态转变成PLAYING状态, 广播这个消息告诉所有相关的组件游戏开始了,于是乎我们的坦克开始监听玩家指挥的操作,敌军的坦克开始出现,背景音乐开始播放… 如果哪天我们突然想加入一些新的组件,比如敌人的飞机,不用修改现有代码,只要让敌机监听游戏开始的消息发动进攻就好了。
网页游戏不同于普通网页,一般来说需要下载的静态资源比较多。为了不让用户等的不耐烦,我们一般都会有一个加载页面。这就是INIT状态对应的时段。最简单的就是显示个加载中的文字,好一点的加个简单转菊花之类的动画表示游戏没停止响应,再专业点的就要加个进度条了,但这样游戏就得知道一共有多少,当前加载了多少。专业的加载器还是比较复杂,咱们就做个简单的循环动画:显示一个加载中的文字,后面跟着省略号,省略号的点点一个个增加,到三个就清空,然后从新增加。
这里我们创建一个文本对象(PIXI.Text)里面写上“加载中”,然后用一个定时器定时给它追加点点。最后监听加载完毕的消息隐藏消失。
游戏加载完成后是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);
讲到这里终于开始游戏最主要的逻辑了。我自己用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;
}
然后我们写个碰撞检测的程序来检测,如果我们的炮弹击中敌军坦克,敌军坦克就爆炸,敌军坦克击中我们的坦克,坦克减少一滴血(有点无耻哈,主角总得有点光环不是…)如果没血了Game over。还有如果敌军坦克撞上我们,同时报废,也game over…
坦克爆炸的图片也是简单的矢量图,程序员的手艺,凑合看吧:
貌似PIXI本身没提供碰撞检测功能,所以自己写个简单的:
总得来说PIXI JS提供了2D游戏引擎最基本的逻辑抽象和图像渲染功能。体积小而且性能很不错。缺点是高级功能就得用第三方或者DIY了,比如碰撞检测。所以适用于小而美的游戏,或者自研实力强的团队。