d2a3366f9689398203be24f78909ad33.png

Phaser3中提供了两种物理引擎,分别为ArcadeMatter。它们在Phaser中的命名空间分别为Phaser.Physics.Arcade与Phaser.Physics.Matter。Arcade相对简单,物体的形状只支持矩形与圆形两种;Matter则要强大很多,能模拟更多的物理效果。

我们还是先通过Phaser提供的工程模板来创建工程

# 克隆模板工程git clone --depth=1 git@github.com:photonstorm/phaser3-project-template.git study-phaser-physics# 进入文件夹cd study-phaser-physics/# 删除原有git数据rm -rf .git# 安装依赖yarn# 启动工程yarn start

我们分两个场景来分别研究Arcade与Matter。方便起见我们把场景以类的形式来写,在src下新建scenes文件夹,然后在该文件夹下分别创建arcade.js与matter.js。

其中arcade.js的初始代码如下

import { Scene } from 'phaser'export class StudyArcade extends Scene {  constructor() {    super({      key: 'StudyArcade',      // 物理引擎设置      physics: {        default: 'arcade',        arcade: {          // 重力          gravity: { y: 10 },        }      }    })  }  /**   * 加载资源   */  preload() {}  /**   * 创建内容   */  create() {    console.log('I am Arcade')  }  /**   * 更新内容   */  update() {}}export default StudyArcade

matter.js的初始内容与arcade.js除了配置物理引擎的地方外都一样

import { Scene } from 'phaser'export class StudyMatter extends Scene {  constructor() {    super({      key: 'StudyMatter',      physics: {        default: 'matter',        matter: {          gravity: { y: 10 },        }      }    })  }  /**   * 加载资源   */  preload() {}  /**   * 创建内容   */  create() {    console.log('I am Matter')  }  /**   * 更新内容   */  update() {}}export default StudyMatter

然后修改默认的src/index.js文件内容如下

import Phaser from "phaser";import StudyArcade from "./scenes/arcade";import StudyMatter from "./scenes/matter";const config = {  type: Phaser.AUTO,  parent: "phaser-example",  width: 800,  height: 600,  // 配置场景,第一个为默认场景  scene: [StudyArcade, StudyMatter],};// 创建游戏实例const game = new Phaser.Game(config);

刷新页面后,在控制台可以看到StudyArcade场景的日志信息

83b00801a40102151c3c15ecc97ba361.png

说明场景加载成功了。下面先来看看Arcade的用法。

Arcade

先在OpenGameArt上找几个图片用于测试,图片如下

6f7a44963443f1b55b58cbd2ab042378.png

加载资源

资源加载很简单,直接用this.load即可

  // arcade.js  // ...  preload() {    this.load.image('ball', require('./assets/ball.png'))    this.load.image('rock', require('./assets/rock.png'))    this.load.image('ship', require('./assets/ball.png'))  }  // ...  
世界

首先我们需要理解物理引擎中的“世界”,可以把它理解成是一个盒子,大小默认与游戏画布相同。也就是说物理引擎的世界是有范围的,所有的模拟都是在世界范围内,世界以外的地方不会考虑。

场景会自动创建物理引擎实例,使用this.physics即可使用arcade引擎实例。如果要修改世界的相关属性,可以直接使用this.physics.world来操作。如

// 修改世界的边界this.physics.world.setBounds(x, y, width, height)

Body

body是用于描述物理系统中物体的对象,比如速度、质量、重力、弹力等都是body的属性。一个普通的游戏对象是没有body的,但可以通过给普通游戏对象添加body使之拥有物理属性。

body分为静态与动态两种。静态body(Arcade.STATIC_BODY)用于模拟如大地或建筑物这种不会移动的物体;动态body(Arcade.DYNAMIC_BODY)用于模拟会因速度与加速度(受力)而移动的物体。

一个物理对象可以直接通过body来操作相关属性,如

// rock是一个物理对象,修改其body的角速度rock.body.setAngularVelocity(100)

添加物理对象

通常在场景中添加普通的游戏对象是这样的

// 添加一个图片对象const ball = this.add.image(400, 100, 'ball')

此时这个ball并没有物理body对象,即其不会参与到Arcade的物理系统中,如果想让它加入到物理系统中,需要这样

// 为ball创建body对象,即加入到了物理系统中this.physics.world.enable(ball)

此时ball上的body会自动将ball的尺寸与位置等信息进行同步。如果只是使用image生成物理对象的话,这样略显繁琐,实际上可以直接像下面这样添加一个物理对象

// arcade.js// ...create() {  // 通过物理引擎添加一个图片对象到场景中  const ball = this.physics.add.image(400, 100, 'ball')}

sprite对象也是一样的,代码中的“image”改成“sprite”即可。

this.physics.addthis.add一样都是工厂,提供了相应的创建对象并添加到场景的便捷方法。

重力、与世界边界碰撞

用上述代码添加一个球后效果是这样的

b7fa5ad472233a4604d7961028c48a14.gif

可以看到,下落的非常缓慢,而且直接落到下边界以外了。

下落慢应该就是重力设置的问题,在场景的构造函数中调整physics设置的gravity值,改到600后感觉相对舒服些

754b14712f0351c438b07dd67795ec43.gif

掉出边界是因为没有设置与世界边界碰撞,添加如下设置

// 设置球与边界发生碰撞ball.setCollideWorldBounds(true)

效果如下

ff171942f9d67509993440b1f4281732.gif

设置弹跳与速度

上面的球落到下边界后静止不动了。我们希望球落下后可以反弹,可以通过设置弹跳来实现

// 设置弹跳系数,值越大,反弹越强ball.setBounce(0.9)

效果如下

020531d5a7216c261740de8d1c5192cf.gif

可以正常弹跳了。现在我们希望球最初可以做抛物线运动,而不是自由落体,可以为其设置水平初速度来实现

// 设置水平速度ball.setVelocityX(200)

效果如下

b2c0f61bf7f611007eee103e2772422e.gif

但是当球弹跳结束后,出现了在地面平移现象

9e28df0028fbc023c696f9ff37b67770.gif

没有滚动,也没有正常停下,这很可能是因为没有设置摩擦力的原因。球可以设置摩擦力,但物理世界的边界是没有摩擦力的,这个稍后再解决。

碰撞

下面我们添加个大石块看看效果

const rock = this.physics.add.image(100, 100, 'rock')// 石块的反弹设置低一点rock.setBounce(0.1)// 当然也设置为与世界外界发生碰撞rock.setCollideWorldBounds(true)

35460fc897dea3352fd6e2f31ccb8f44.gif

石块稳稳地落在下面,但到后面我们发现,球与石块并没有正常碰撞。这是因为默认情况下,物体之间并不碰撞。这好理解,因为碰撞检测是会消耗性能的,所以需要明确设置哪些物体之间需要发生碰撞,如下

// 添加碰撞检测对象this.physics.add.collider(rock, ball)

这样就可以让石块与球发生碰撞了

9595e5b773666d5691a6f4dea6f91f73.gif

有时当碰撞发生时我们需要做些事情,如增加分数或者game over,可在创建碰撞对象时传入一个回调函数来实现,如

// 当碰撞发生时执行doSomething方法this.physics.add.collider(rock, ball, doSomething)

有时我们希望当两个物体接触或者说重叠时做些事情,但不希望它们有碰撞效果,可以这样

// 只检测碰撞是否发生,但不模拟碰撞效果this.physics.add.overlap(rock, ball, doSomething)

debug模式

我们给石块与球都设置一下摩擦力,看看会怎么样

// 设置摩擦力系数rock.setFriction(0.6)// ...// 设置摩擦系数ball.setFriction(0.9)

很奇怪,与没设置时的效果很接近,球也依然没有滚动。

原来是我忽略了一点,默认情况下物理对象是矩形的,所以这里的球实际上一直是矩形的状态。把debug状态打开看看

// 构造函数的physics配置physics: {  default: 'arcade',  arcade: {    gravity: { y: 600 },    // 开启debug模式    debug: true,  }}

效果如下

566bf4daeca0b4f9f3070b02bed39002.gif

紫色的框表示物体的边界,绿线表示物体速度的方向及大小。可见球是一个矩形的,自然很难滚动起来。

我们把球设置为圆形

// 设置为圆形ball.body.isCircle = true

Arcade太初级了

然后……依然没有滚动!哪怕我们想让石块倾斜也无法做到,如下图

d851daea8e6325322a7f88ad399ee7d7.png

它就这样不动了。

好吧,实际上是因为Arcade太过简单,它对物体受力的模拟是没有力矩的。也就是说,当一个物体受力时,对力的计算只分x轴方向与y轴方向以及大小,对于受力点是不关心的。

如下图,矩形左右两边分别受到拉力F1与F2(灰色),正常应该会旋转,因为受力点不在同一个位置

91b93bb7076678a8462c4d95c2200ad6.png

而Arcade在模拟时会简化,相当于两个力都作用在矩形中心点(红色),所以它不会旋转。水平方向也是同样的道理。

这样的确比较简陋,但优点是速度快,我们应当根据具体情况来选择是否使用它。

组Group

前面有提到,如果希望物体有碰撞需要明确声明。只有两三个物体时还好办,当物体比较多时似乎就比较麻烦了,而且物体经常是在游戏过程中动态生成的,那样岂不是更麻烦。

这时就需要用到组的功能了。我们可以简单的把组理解成标签,一个物体可以在多个组(有多个标签),一个组自然也可以有多个物体。

组有两种,一个是Phaser的游戏对象组Phaser.GameObjects.Group,我们姑且称之为“对象组”;一个是Arcade的组Phaser.Physics.Arcade.Group,我们姑且称之为“物理组”。

对象组不关心其成员是否为物理对象,就是纯粹的分组功能;物理组除了分组外,还能提供一些使游戏对象物理化的便捷功能,即添加到物理组的对象如果没有物理body则会被自动添加上body。

// 创建一个物理组const shipGroup = this.physics.add.group()// 创建一个物理对象并添加到shipGroupshipGroup.create(400, 100, 'ship')

静态物理组

与普通物理组一样,只是由静态物理组创建的物理对象也都静态的,可以用它方便的创建与管理静态对象。

// 创建一个静态物理组const platforms = this.physics.add.staticGroup()// 通过静态物理组直接创建一个静态物理对象platforms.create(600, 200, 'rock')// 让球与静态物理组中的对象可以碰撞this.physics.add.collider(ball, platforms)

994e59749dde0d48b23e34f80a85936b.gif

同时我们也可以看到,在debug模式下静态物理对象的边框默认是蓝色的。批量生成与缩放现在我们想要用10个大石块像砖块一样组成一个平台,如果每个“砖块”都单独生成就比较麻烦,同时我们希望“砖块”要缩小,因为大石块的尺寸的确太大了。使用组来生成的话可以这样
// 创建一个静态物理组,并自动生成10个“砖块”const platforms = this.physics.add.staticGroup({  // 使用石块图片  key: 'rock',  // 一共生成10个(第一次创建后再重复创建9次)  repeat: 9,  // 设置缩放  setScale: {    x: 0.1,    y: 0.1,  },  // 设置位置  setXY: {    // 首次创建对象时的坐标    x: 500,    y: 300,    // 每次重复生成时 x坐标的增量    stepX:25,  }})// 由于静态物体body不会自动同步对象的大小与位置等信息,所以需要手动刷新一下platforms.children.iterate(child => child.refreshBody())// 让球与静态物理组中的对象可以碰撞this.physics.add.collider(ball, platforms)
这里需要注意child.refreshBody()方法。由于静态body不会自动同步游戏对象的尺寸与位置信息,所以需要手动同步;对于动态body则无需手动调用该方法。

02fbdef4f56300f7bd48ebb31d3f96d4.gif


好了,今天先到这,下次继续研究Matter。附

OpenGameArt地址:

https://opengameart.org/

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐