从Phaser中了解游戏的几个概念

上个周末趁着双休,尝试着学习Phaser框架然后写了两个小游戏。一直想要写些小游戏,与之前学习Cocos框架相比,Phaser要简单得多,加之文档齐全,示例Demo也比较多,所以上手难度要低很多。这里整理了小游戏开发的东西,以及我对于游戏开发的一些理解。

<!--more-->

参考相关教程:

1. 游戏的整体结构:场景

PhaserCocos都提供了场景的概念。把写游戏和拍电影联想起来,电影的不同场景对应着不同的剧情,而游戏的不同场景对应则不同的逻辑,将逻辑以场景的方式包装起来,然后按照顺序将场景组合起来,就构成了我们的游戏世界。

玩了辣么多游戏,基本的套路也了然于胸了:

  • 资源加载场景,各种花式进度条
  • 准备开始界面,一个点击开始的提示,再多两个按钮看个排行榜,设置个音效啥的
  • 游戏场景,包含具体的游戏玩法
  • 游戏结束,进行结算,进入下一关或者重新开始啥的

1.1. StateManager

好比电影有导演,既然有了场景,那么肯定有管理场景的对象,在Phaser中通过StateManager来管理对象,查看stateManager文档

场景管理器用来维护多个游戏场景,包括加载、设置和场景之间的切换等方法,会用到的方法包括:

  • add(key, state)添加一个场景,key是每个场景的标识符,如welcome,over等带有语义性的称谓
  • start(key),启动一个场景,可用于场景之间的切换

我们的游戏整体结构可能就是长成这个样子

// Phaser.Game对象
let game = require('./game');

// 每个具体场景
let boot = require('./boot');
let preload = require('./preload');
let welcome = require('./welcome');
let play = require('./play');
let gameover = require('./gameover');

let states = {
    preload,
    boot,
    welcome,
    play,
    gameover
};
// 通过StateManager来装载对应的场景
for (let key in states){
    if (states.hasOwnProperty(key)){
        game.state.add(key, states[key]);
    }
}
// 启动某个场景,另外场景之间的切换也使用该方法进行
game.state.start("boot");

1.2. state

先不要在意细节,可以发现,我们写游戏实际上就是在设计和实现游戏场景。在一个游戏场景中,存在着他自己的生命周期,场景从构造到显示在屏幕上,再从场景被销毁然后切换到其他场景的过程。一般来将,框架都会提供相应的钩子函数,以便我们对游戏场景进行操作

在Phaser中,一个场景对应一个构造函数,查看state文档

let play = function () {
      this.preload = function(){};
      this.create = function(){};
      // 其他生命周期函数
}

module.exports = play

我们的大部分逻辑都是在场景中实现的,比如加载某张图片,播放一段音乐,移动角色,检测碰撞等。只需要丰富我们的游戏场景,整个游戏就可以跑起来了,整体结构是不是十分清晰呢(还是那句话,先不要管具体实现哈)。

需要注意的是,state不能使用下列ES6对象简写形式

var states = { 
    welcome(){ 
        // todo 
        console.log(
    }
}

我猜测因为场景函数在内部需要更换绑定的this造成的。

此外,在Cocos中还有层的概念,用来管理大场景中的某些节点,相当于将场景进一步划分,在Phaser中貌似使用state就够了。

2. 游戏的资源:加载器

游戏开发中,需要大量的资源文件,如图片、音效等,我们看见的绚丽的技能特效,逼真的背景音乐,都是通过外部资源来实现的。由于资源类型比较多,游戏引擎一般会提供相应的资源加载接口,查看Loader文档

对于资源,一般的处理方式是在游戏启动之前单独使用一个场景来进行加载,并通过反馈加载进度的方式提醒玩家耐心等待

let preload = function () {
  this.preload = function(){
    // 加载游戏资源
    game.load.image('bg', 'assets/images/bg.png');
    game.load.image('hero', 'assets/images/hero.png');
    // todo
  }
  this.create = function(){
    // 资源加载完毕,进行跳转
    game.state.start('welcome');
  }
}

需要注意的是,在Phaser中,资源的加载都是异步进行的,为了使用某个资源,必须确保它已经加载完成,一种常用的做法是在前一个场景中就加载对应的资源,利用场景的生命周期来控制资源的加载

资源加载过程会触发对应的事件,可以用来做进度提示

ame.load.onFileComplete.add(function (progress) {});
game.load.onLoadComplete.add(function(){});

3. 游戏的元素:精灵

精灵是游戏引擎中必不可少的一个元素,一个精灵一般对应着贴图,基本上任何可见的元素都是精灵,查看sprite文档

Sprites are the lifeblood of your game, used for nearly everything visual.

精灵有许多经常用到的属性,比如

  • alpha设置透明度,
  • angle设置角度,
  • anchor设置锚点
  • events注册事件处理函数

上面只是简单列举了几个属性而已,只是为了让大家明白,在游戏逻辑的开发过程中,大部分都是对精灵的操作,比如设置生命属性,添加动画,检查位置及碰撞等,可以类比为在Web中操作DOM节点。

下面的代码是创建一个主角,同样不用深究细节,我们只需要明白,我们创建了一个主角对象,我们可以通过程序来操作他。

player = game.add.sprite(game.world.centerX, height, 'dude');
player.anchor.setTo(0.5, 1);

// 为了使用碰撞检测,这里需要开启精灵的physics
game.physics.enable(player);
player.body.allowGravity = false;
// 监听事件,移动角色
movePlayer();

另外,写前端的同学想必对于精灵图并不陌生,实际上,在游戏开发中也会大量使用精灵图。在去年刚接触编程的时候,手动去拼接精灵图的活我是再也不会做了~

4. 游戏的交互:事件

游戏是通过事件与玩家进行交互的,我们要控制主角移动,要躲避子弹,要跳跃,要使用按键组合技能啥的,无非就是监听事件然后处理的一些逻辑罢了。与浏览器的事件相比,游戏中的事件要简单很多,常见的交互事件无非是鼠标和触摸事件而已。

事件属于Phaser.Signal类所有的事件处理函数通过事件的add`方法添加,查看事件文档

下面的代码 实现了上面移动主角的逻辑

function movePlayer() {
    // 移动角色      
    let touching = false;
    // 为了防止角色瞬移
    game.input.onDown.add(function (pointer) {
        if (Math.abs(pointer.x - player.x) < player.width/2) {
            touching = true;
        }
    });
    game.input.onUp.add(function () {
        touching = false;
    });
    game.input.addMoveCallback(function (pointer, x, y, isTap) {
        if (!isTap && touching) {
            player.x = x
        }
    })
}

代码的逻辑十分简单,监听整个游戏场景的点击事件,让角色跟随触摸点移动。实际上还需要做边界检测等逻辑处理,即在移动时判断角色的位置是否超过了场景边界。

需要注意的是,如果仅仅只希望为某个精灵添加点击事件,需要先设置inputEnabled = true

bird.inputEnabled = true;
bird.events.onInputDown.add(function () {
    console.log("1");
}, this);

使用标识符显式开启点击事件可能是为了提高游戏效率。

5. 游戏的特效:动画

做前端免不了跟动画打交道,Phaser提供了补间动画帧动画,另外还可以通过定时器来产生类似于动画的效果

  • 补间动画是在指定的时间内将精灵从某个状态改变到另一个状态
  • 帧动画是通过按照一定的频率切换显示帧图片来达到动画的效果,比如Flappy bird中的小鸟飞翔动画,就是通过三张图片反复播放来实现的。
  • 通过定时器持续改变精灵的某个属性来达到动画的效果

动画是为了增强游戏的体验而使用的,我的理解是,即使没有动画,游戏的逻辑也能够继续;换句话讲,我们不应该把动画和逻辑的实现混杂在一起。一般来讲,定时器一般是伴随着游戏逻辑的处理(比如水果掉落,敌机移动等),因此,我认为不应该把定时器动画当作具体的动画来处理,尽管他们看起来很像。

5.1. Animation

Phaser中,一个animation实例包含了一个独立的动画,以及操作该动画的方法(播放暂停等),查看animation文档

下面是小鸟的飞行动画,对应的资源文件是bird

// 加载资源
game.load.spritesheet('bird', 'assets/bird.png', 32, 24, 3,0,2)

// 生产精灵
bird = game.add.sprite(width/2, height*0.2, 'bird');
// 添加动画
bird.animations.add('fly');
// 播放动画
bird.animations.play('fly', 12, true);
bird.anchor.setTo(0.5);

可以看见,三张帧就可以组成一个小鸟飞行的动画了,猜测内部的实现原理应该就是通过定时切换对应图片的显示来达到动画的效果的。

5.2. Tween

补间动画是通过在为目标指定某段时间内需要到达的属性来产生动画的,我的理解就是CSS中的transition,查看Tween文档

补间动画在游戏中也十分常见,比如显示爆炸特效然后逐渐消失,将目标逐渐旋转到某个角度等。

下面这段代码是设置标题在数值方向上产生位移的动画。

 game.add.tween(title).to({ y:120 },1000,null,true,0,Number.MAX_VALUE,true);

这里只是了解大概,具体使用查看手册即可。

6. 游戏的世界:物理引擎

在游戏开发中,我们可能需要去模拟展示场景的物理行为,比如碰撞,边界检测等,游戏引擎一般都提供了相关的物理引擎,物理引起可以给予物体赋予物理属性,使其产生运动、碰撞等,通过高度的抽象接口,避免了我们手动去进行底层编码(貌似实现物理引擎还是很有难度的),可以大大提高游戏效率。

在Phaser中内置了3套物理引擎,查看Physics文档。目前我只使用了aracede

 // 首先需要设置物理引擎
 game.physics.startSystem(Phaser.Physics.Arcade);
 game.physics.arcade.gravity.y = gameSpeed;

 // 然后为需要进入物理系统的精灵进行配置
game.physics.enable(fruit);
// 此时精灵进入了物理引擎,就具备了重力等特性,会自由下落啦~

物理引擎是一个比较大且重要的概念,这里还需要进一步的学习和了解

7. 其他

7.1. 定时器

游戏场景本身就是一个巨大的定时器,每一帧都将绘制计算的结果并展示到屏幕上。游戏的性能及取决于每秒钟能绘制的帧数,之前做动画一般是要求每帧的时长是1000/60,而游戏的计算量比较大,一般达到每秒钟二三十帧即可。

7.2. 粒子效果

粒子效果是比较酷炫的动画,之前在学习Canvas的时候有了解到一点相关的东西,我的理解是将图片分解为单个的像素点然后再进行操作。与粒子相关的概念是粒子发射器,这个后面再了解,这里放出我的一个书签:www.effecthub.com,这是之前在学习Cocos的时候找到的~

7.3. 坐标系

渲染游戏对象时,需要知道其具体的位置,而位置这个概念是相对于坐标系的:

  • 屏幕坐标系以屏幕左上角为原点,向右为X轴正方向,向下为Y轴正方向,Phaser采用的就是这种坐标系
  • OpenGL坐标系以屏幕左下角为远点,向右为X轴正方向,向上为Y轴正方向,Cocos采用的是这种坐标系

与坐标系相关的另一个概念就是锚点,因为对象在游戏场景中是有面积的,我们无法仅仅通过坐标系中的一个点确定对象的摆放位置,对象的锚点就是用来解决这个问题的。

锚点是一对百分比系数(范围为0~1),对应该点在对象上的位置,然后将锚点与对象的坐标进行重合,就确定了对象在场景中的位置,锚点相当于一颗钉子(这也是他的名称由来),可以理解为CSS中的transform-origin属性。

8. 小结

这里简单整理了游戏开发中几个概念,也许还算不上是真正的游戏,但我确实很想写一款好玩的游戏,只是被各种各样的事儿耽搁了。现在趁着还没忘记,赶紧先记下来。总有一天会写一款非常好玩的游戏,等着我吧~