使用Eva+MatterJS实现一个2D版推金币Demo

最近拼多多的推金币比较火,就尝试想用一下2D的游戏引擎试一下效果,做了一个简单的Demo。

引擎选择的是Eva,采用这个引擎是因为它基于ECS的微内核架构,让扩展非常方便,同时也提供了几个官方的插件,足以支持大部分需求,它的渲染引擎插件是基于PixiJS,物理引擎插件是基于MatterJS,它的插件所依赖的版本比较低了,对于我来说还够用,如果有需要完全可以自己实现。

环境准备

  1. 下载官方的demo仓库:https://github.com/eva-engine/start-demo
  2. 网上随便找一个金币的图片和推板的图片

关键代码

资源依赖

首先是需要将金币和推板的图片资源定义好。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import { RESOURCE_TYPE } from '@eva/eva.js';
export default [
{
name: 'coin',
type: RESOURCE_TYPE.IMAGE,
src: {
image: {
type: 'png',
url: './statics/coin.png',
},
},
preload: true,
},
{
name: 'pusher',
type: RESOURCE_TYPE.IMAGE,
src: {
image: {
type: 'png',
url: './statics/pusher.png',
},
},
preload: true,
},
];

然后通过resource加载资源

1
2
import { Game, GameObject, resource } from '@eva/eva.js';
resource.addResource(resources);

创建金币和推板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import { GameObject } from "@eva/eva.js";
// import { Physics, PhysicsType } from "../matter/plugin";
import {Physics, PhysicsType } from '@eva/plugin-matterjs';

import { Img } from "@eva/plugin-renderer-img";
import { Move } from "../../src/components/move";

export function createCoin() {
const coin = new GameObject('coin', {
size: {
width: 60,
height: 60,
},
position: {
x: 75 + Math.random() * 300,
y: 300 + Math.random() * 300,
},
origin: {
x: 0.5,
y: 0.5
}
});
// 4.给游戏对象添加Componet
const physics = coin.addComponent(
new Physics({
type: PhysicsType.CIRCLE,
radius: 20,
bodyOptions: {
isStatic: false, // 物体是否静止,静止状态下任何作用力作用于物体都不会产生效果
restitution: 1,
friction: 1
},
}),
);

coin.addComponent(
new Img({
resource: 'coin',
})
);
//目前支持的碰撞事件 collisionStart collisionActive collisionEnd
//刚体事件 tick,beforeUpdate,afterUpdate,beforeRender,afterRender,afterTick 通常使用beforeUpdate和afterUpdate即可
physics.on('collisionStart', (gameObject1, gameObject2) => {});

return coin;
}

export function createPusher() {
const pusher = new GameObject('pusher', {
size: {
width: 800,
height: 200,
},
position: {
x: 400,
y: 0,
},
origin: {
x: 0.5,
y: 0.5
}
});
// 4.给游戏对象添加Componet
const physics = pusher.addComponent(
new Physics({
type: PhysicsType.RECTANGLE,
bodyOptions: {
isStatic: true, // 物体是否静止,静止状态下任何作用力作用于物体都不会产生效
restitution: 1,
},
}),
);

pusher.addComponent(
new Img({
resource: 'pusher',
})
);

pusher.addComponent(new Move())
//目前支持的碰撞事件 collisionStart collisionActive collisionEnd
//刚体事件 tick,beforeUpdate,afterUpdate,beforeRender,afterRender,afterTick 通常使用beforeUpdate和afterUpdate即可
// physics.on('collisionStart', (gameObject1, gameObject2) => {});

return pusher;
}

为推板创建Component来实现来回移动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import { Component, UpdateParams } from '@eva/eva.js'
// import { Physics } from '../matter/plugin';
import { Physics } from '@eva/plugin-matterjs';
import Matter from 'matter-js';
export class Move extends Component {
static componentName = 'Move'
speed = {
// 设置属性
// 移动速度
x: 100,
y: 200
}
direction = 1;
maxY = 500;
minY = 200;

x: number;

start(): void {
this.x = this.gameObject.transform.position.x;
}

update(e: UpdateParams) {
// 让物体按照一定速度移动 位移 = 速度 * 时间
// this.gameObject.transform.position.x -= this.speed.x * (e.deltaTime / 1000)
const body = this.gameObject.getComponent(Physics).body;
const y = body.position.y + this.direction * this.speed.y * (e.deltaTime / 1000)
if (y > this.maxY) {
Matter.Body.setPosition(body, { x: this.x, y: this.maxY });
} else if (y < this.minY) {
Matter.Body.setPosition(body, { x: this.x, y: this.minY });
} else {
Matter.Body.setPosition(body, { x: this.x, y });
}
// this.gameObject.transform.position.y += this.direction * this.speed.y * (e.deltaTime / 1000)

// console.log(this.gameObject.transform.position.y, this.direction * this.speed.y * (e.deltaTime / 1000))
if(y >= this.maxY) {
this.direction = -1;
} else if(y <= this.minY) {
this.direction = 1;
}
}
}

这里要注意的是,要通过修改推板的MatterBody来移动,直接修改gameObject.transform是无效的,因为在我们的推板还有物理引擎,在物理引擎中,Pysics组件的update函数中有一段代码是将当前的transform变为body的position

1
2
3
4
5
6
7
8
9
if (this.body && this.gameObject) {
this.gameObject.transform.anchor.x = 0;
this.gameObject.transform.anchor.y = 0;
this.gameObject.transform.position.x = this.body.position.x;
this.gameObject.transform.position.y = this.body.position.y;
if (!this.bodyParams.stopRotation) {
this.gameObject.transform.rotation = this.body.angle;
}
}

游戏启动代码

接下来就是定义如何启动

这里会将游戏渲染到 id 为’canvas’的canvas组件上,该组件是在index.html中已经定好的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
function startGame() {

const game = new Game({
systems: [
new RendererSystem({
transparent: true,
canvas: document.querySelector('#canvas'),
backgroundColor: 0x00000000,
width: 750,
height: 1000,
resolution: 1,
}),
new PhysicsSystem({
resolution: 1, // 保持RendererSystem的resolution一致
// isTest: true, // 是否开启调试模式
// canvas: document.getElementById('game-container'), // 调试模式下canvas节点的挂载点
world: {
gravity: {
y: 0, // 重力
x: 0,
},
},
}),
new ImgSystem(),
new TransitionSystem(),
new SpriteAnimationSystem(),
new RenderSystem(),
new EventSystem(),
new GraphicsSystem(),
new TextSystem(),
],
});

game.scene.transform.size.width = 750;
game.scene.transform.size.height = 1484;

// game.scene.addChild(createBackground());

for(let i = 0; i < 50; i++) {
game.scene.addChild(createCoin());
}

game.scene.addChild(createPusher())
}

startGame()

调试小技巧

我在实现了上述代码的时候,发现碰撞效果和渲染效果并不完全一致,猜测是碰撞体和渲染的图片并不是完全重合的,所以要想办法将碰撞体渲染出来并重叠在图片上,比对二者是否完全重合,具体步骤如下:

在index.html文件中,创建一个新的canvas用于渲染matterJS的碰撞体,将其position设置为absolute,定位在左上角,同时z-index为1,这样碰撞体的渲染就正好改在了pixijs的渲染效果上了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<!doctype html>
<html>

<head>
<title></title>
<style>
* {
margin: 0;
border: 0;
font-size: 36px;
}
.nice {
display: flex;
justify-content:center;
align-items: center;
font-size: 36px;
}
input {
margin: 0 10px;
font-size: 36px;
width: 100px;
border: 2px solid rgb(58, 58, 58);
}
button {
margin: 0 10px;
border: 2px solid rgb(58, 58, 58);
}
</style>
</head>

<body>
<canvas width="750" height="1000" id="canvas" style="width: 100%"></canvas>
<canvas width="750" height="1000" id="game-container" style="width: 100%; position: absolute; top: 0; left: 0; z-index: 1; background: 0% 0% / contain rgba(20, 21, 31, 0);"></canvas>
<script src="//cdn.bootcdn.net/ajax/libs/eventemitter3/3.1.2/index.min.js"></script>
<script src="./main.js"></script>
</body>

</html>

然后开启eva matterjs插件的test模式,并将canvas设置为我们在index.html创建的canvas

1
2
3
4
5
6
7
8
9
10
11
new PhysicsSystem({
resolution: 1, // 保持RendererSystem的resolution一致
isTest: true, // 是否开启调试模式
canvas: document.getElementById('game-container'), // 调试模式下canvas节点的挂载点
world: {
gravity: {
y: 0, // 重力
x: 0,
},
},
}),