作者: 涂鸦-若叶
来涂鸦工作: job.tuya.com/
物理引擎Matter.js与在RN内的使用
Matter.js 是一个用于 Web 的 JavaScript 2D 物理引擎库
相比其他js物理引擎库,如box2dweb,Ammo.js,JigLibJS 以及 Connon.js,Matter.js 具有api友好,轻量(压缩版仅有 87 KB)等优势,更适用于web端物理效果的开发
其特性如下:
-
刚体、混合体、复合体,凹面和凸面
-
物理特性(质量、面积、密度等),弹性(弹性和非弹性碰撞),碰撞(粗略阶段、中间阶段、精细阶段)
-
事件监听、碰撞查询(射线追踪、区域测试)
-
原生 JS 实现、兼容移动端(触摸、响应)
开始
引入Matter.js
使用npm安装
npm install matter-js
或直接下载release代码在html中引入
<script src="matter.js" type="text/javascript"></script>
基础使用
const Engine = Matter.Engine; // 引擎
const Engine = Matter.Engine; // 引擎
const Render = Matter.Render; // 渲染器
const World = Matter.World; // 世界
const Bodies = Matter.Bodies; // 刚体
const Composites = Matter.Composites; // 复合体
const Constraint = Matter.Constraint; // 约束
const MouseConstraint = Matter.MouseConstraint; //鼠标约束
const engine = Engine.create(); // 创造引擎实例
const world = engine.world; // 创造世界
const render = Render.create({
engine: engine,
element: document.body,
options: {
width: 1000,
height: 700,
wireframes: true, // 关闭线框模式
showAngleIndicator: true
}
});
Engine.run(engine); // 运行引擎
Render.run(render); // 运行渲染器
添加一个矩形刚体
// 添加一个矩形刚体
// 前四个参数分别代表矩形刚体的 x y width height
// 其中x y 为矩形钢体中心点的坐标
const boxA = Bodies.rectangle(500, 200, 100, 200, {
render: {
fillStyle: "#9966ff"
}
});
// 将矩形刚体加入到世界
// 运行后该矩形刚体会受重力影响进行自由落体运动
World.add(world, boxA);
// 1\添加地面
// option中isStatic为true的话代表是静止刚体,即不受外力影响,默认为false
// 添加地面后,之前添加的矩形刚体将自由落体撞到地面后停止运动
const ground = Bodies.rectangle(500, 700, 1000, 100, { isStatic: true });
World.add(world, ground);
同理我们可以创建圆、三角形、多边形等刚体
创建物体堆
此外,我们还可以通过Composites.stack来创建一个物体堆
Composites.stack(xx, yy, columns, rows, columnGap, rowGap, callback)
参数xx,yy为第一个物体的坐标,columns, rows分为创建的物体堆的列数及行数,columnGap,rowGap分别为物体之间的列间隔和行间隔
// 2\添加一堆矩形
const stack = Composites.stack(250, 100, 6, 3, 0, 0, (x, y) => {
return Bodies.rectangle(x, y, 80, 20);
});
World.add(world, stack)
同理我们可以创建一堆球体
// 3\添加一堆球
const stackCircle = Composites.stack(250, 0, 6, 3, 0, 0, (x, y) => {
return Bodies.circle(x, y, 40);
});
World.add(world, stackCircle)
约束关系
约束可理解为通过一条线,将刚体 A 和刚体 B 刚体连接起来,这两个刚体由于被约束连接到了一起,其物理特性就会互相影响。我们可以设定约束的多种属性,如距离、长度、位置、弹性等
// 4\添加约束关系
// 创建两个刚体
const boxB = Bodies.rectangle(500, 500, 40, 200, {
// isStatic: true,
render: {
fillStyle: "#faa"
},
collisionFilter: {
group: -1
}
});
const boxC = Bodies.rectangle(500, 140, 400, 40, {
// isStatic: true,
render:{
fillStyle: "#aaf"
},
collisionFilter:{
group: -1
}
});
// 创建一个约束关系
// 运行结果可以看到两个刚体被一根“棍子”相连接,调整刚性属性可以使这条“棍子”变为“皮筋”
const constraintA = Constraint.create({
bodyA: boxB,
// pointA: { x: 10, y: 0 },
bodyB: boxC,
pointB: { x: 30, y: 0 },
length: 50, // 约束长度
stiffness: 1 // 刚性
});
World.add(world, [boxB, boxC, constraintA])
鼠标约束
由约束关系引申,鼠标约束可以理解为鼠标与钢体创建了一个约束关系,我们可以通过鼠标约束来实现鼠标拖拽引擎内的刚体
const mouseConstraint = MouseConstraint.create(engine, {});
World.add(world, mouseConstraint)
链条
同样从约束关系引申,链条可以理解为将一堆物体通过约束关系连接起来,matter.js提供了Composites.chain快速创建这一约束关系
// 6\生成链条
const chains= Composites.stack(200, 500, 10, 1, 10, 0, (x, y) => {
return Bodies.rectangle(x,y,40,20,{
// chamfer: 10
})
});
// Matter.Composites.chain(composite, xOffsetA, yOffsetA, xOffsetB, yOffsetB, options)
// Composites.chain()可以将已有的物体堆连接在一起
// 参数中chains物体堆,
// xOffsetA为0.4,表示第一个链接点在横向上距离物体中心有向右的宽度的0.4倍的偏移量,
// yOffsetA为0,表示纵向上与物体中心平行。
// xOffsetB为0.4,第二个链接点在横向上距离物体中心有向左的宽度的0.4倍的偏移量,
// yOffsetB为0,表示纵向上与物体中心平行。
Composites.chain(chains, 0.4, 0, -0.4, 0, {});
模拟布料
与链条类似,布料的模拟也是通过约束关系实现的,区别只在从一维的物体堆和约束关系扩展到了二维平面,matter.js提供了Composites.softBody快速实现
Matter.Composites.softBody(xx, yy, columns, rows, columnGap, rowGap, crossBrace, particleRadius, particleOptions, constraintOptions)
const cloth = Composites.softBody(100, 100, 6, 10, 2, 2, false, 10, {
render:{
visible: false
},
collisionFilter:{
group: 1
}
},{});
World.add(world, cloth)
在ReactNative中使用Matter.js实现一个Flappy Bird
这节我们通过借助react-native-game-engine库,在react native中使用matter.js实现一个Flappy Bird小游戏
项目创建和初始化不再赘述,直接开始
创建世界和刚体鸟
首先,我们在app.js中,创建我们的世界🌍和一只鸟🐦
import React from 'react';
import {
SafeAreaView,
StyleSheet,
ScrollView,
View,
Text,
StatusBar,
Dimensions,
TouchableOpacity,
} from 'react-native';
import Matter from 'matter-js';
import { GameEngine } from 'react-native-game-engine';
import Bird from './Bird';
import Physics from './Physics';
import Wall from './Wall';
const {Engine, World, Render, Bodies, MouseConstraint} = Matter;
export const Constants = {
MAX_WIDTH: Dimensions.get('screen').width,
MAX_HEIGHT: Dimensions.get('screen').height,
GAP_SIZE: 200,
PIPE_WIDTH: 100,
};
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
running: true,
};
this.gameEngine = null;
this.entities = this.setupWorld();
}
// 这里Matter.js并不会处理世界及鸟的渲染,它只计算他们的坐标位置,渲染将react-native-game-engine每次执行setupWorld的返回对象时进行
setupWorld = () => {
const engine = Engine.create();
const world = engine.world;
const bird = Matter.Bodies.rectangle(
Constants.MAX_WIDTH / 4,
Constants.MAX_HEIGHT / 2,
50,
50,
);
Matter.World.add(world, bird);
return {
physics: { engine: engine, world: world },
bird: {
body: bird,
size: [50, 50],
color: 'red',
renderer: Bird
},
}
}
render() {
return (
<View style={styles.container}>
<GameEngine
ref={(ref) => { this.gameEngine = ref; }}
style={styles.gameContainer}
systems={[Physics]}
onEvent={this.onEvent}
running={this.state.running}
entities={this.entities}
>
<StatusBar hidden={true} />
</GameEngine>
</View>
);
}
}
Bird.js如下
import React, {Component} from 'react';
import { View } from 'react-native';
export default class Bird extends Component {
render() {
const width = this.props.size[0];
const height = this.props.size[1];
const x = this.props.body.position.x - width / 2;
const y = this.props.body.position.y - height / 2;
return (
<View
// eslint-disable-next-line react-native/no-inline-styles
style={{
position: 'absolute',
left: x,
top: y,
width: width,
height: height,
backgroundColor: this.props.color,
}}
/>
);
}
}
更新渲染
此时我们可以看到屏幕中出现了红色“鸟”,在默认情况下,我们在Matter.js中创建的世界的引力为1.0,但它并不会自由落体,如何让它运动呢?我们需要定期调用引擎的更新方法,以便Matter.js可以重新计算每个物体的位置。对此,react-native-game-engine提供了名为systems的方法
注意上一步中我们在GameEngine类中添加的属性systems={[Physics]}
Physics.js是这样的
import Matter from 'matter-js';
import { Constants } from './App';
const Physics = (entities, {touches, time}) => {
let engine = entities.physics.engine;
let bird = entities.bird.body;
// 由时间推移更新引擎
Matter.Engine.update(engine, time.delta);
return entities;
};
export default Physics;
它将在react-native-game-engine执行的每一帧上被调用,然后我们就可以看到红色的”鸟“自由落体了
添加天花板和地板
在setupWorld添加静态的天花板和地板,这步较为简单。与创建鸟类似,我们需要创建它们的渲染器,名为Wall
Wall.js
import React, { Component } from "react";
import { View } from "react-native";
export default class Wall extends Component {
render() {
const width = this.props.size[0];
const height = this.props.size[1];
const x = this.props.body.position.x - width / 2;
const y = this.props.body.position.y - height / 2;
return (
<View
style={{
position: "absolute",
left: x,
top: y,
width: width,
height: height,
backgroundColor: this.props.color
}}
/>
);
}
}
在setupWorld方法中加入天花板和地板
......
const floor = Matter.Bodies.rectangle(
Constants.MAX_WIDTH / 2,
Constants.MAX_HEIGHT - 25,
Constants.MAX_WIDTH,
50,
{isStatic: true},
);
Matter.World.add(world, floor);
const ceiling = Matter.Bodies.rectangle(
Constants.MAX_WIDTH / 2,
25,
Constants.MAX_WIDTH,
50,
{isStatic: true},
);
Matter.World.add(world, ceiling);
.......
return {
physics: {engine: engine, world: world},
bird: {body: bird, size: [50, 50], color: '#0099ff', renderer: Bird},
floor: {
body: floor,
size: [Constants.MAX_WIDTH, 50],
color: 'green',
renderer: Wall,
},
ceiling: {
body: ceiling,
size: [Constants.MAX_WIDTH, 50],
color: 'green',
renderer: Wall,
},
}
让鸟弹跳
现在我们已经有了鸟、天花板和地板,接下来实现这个游戏最基本的交互——点击屏幕使鸟弹跳
为此我们需要修改Physics
import Matter from 'matter-js';
import { Constants } from './App';
const Physics = (entities, { touches, time }) => {
let engine = entities.physics.engine;
let bird = entities.bird.body;
// 过滤遍历出点击事件
touches
.filter((t) => t.type === 'press')
.forEach((t) => {
// 每次点击,为鸟刚体施加一个竖直方向上-0.1的力,即向上
Matter.Body.applyForce(bird, bird.position, {x: 0.0, y: -0.1});
});
// 由时间推移更新引擎
Matter.Engine.update(engine, time.delta);
return entities;
};
export default Physics;
创建障碍物
在这个游戏中,我们需要操作小鸟避开绿色的管道,因此我们需要创建一些障碍物
在app.js中定义随机生成障碍物的方法
export const randomBetween = (min, max) => {
return Math.floor(Math.random() * (max - min + 1) + min);
};
export const generatePipes = () => {
let topPipeHeight = randomBetween(100, Constants.MAX_HEIGHT / 2 - 100);
let bottomPipeHeight =
Constants.MAX_HEIGHT - topPipeHeight - Constants.GAP_SIZE;
let sizes = [topPipeHeight, bottomPipeHeight];
if (Math.random() < 0.5) {
sizes = sizes.reverse();
}
return sizes;
};
更新setupWorld方法,将障碍物添加至世界
......
let [pipe1Height, pipe2Height] = generatePipes();
const pipe1 = Matter.Bodies.rectangle(
Constants.MAX_WIDTH - Constants.PIPE_WIDTH / 2,
pipe1Height / 2,
Constants.PIPE_WIDTH,
pipe1Height,
{isStatic: true},
);
const pipe2 = Matter.Bodies.rectangle(
Constants.MAX_WIDTH - Constants.PIPE_WIDTH / 2,
Constants.MAX_HEIGHT - pipe2Height / 2,
Constants.PIPE_WIDTH,
pipe2Height,
{isStatic: true},
);
Matter.World.add(world, [pipe1, pipe2]);
const [pipe3Height, pipe4Height] = generatePipes();
const pipe3 = Matter.Bodies.rectangle(
Constants.MAX_WIDTH * 2 - Constants.PIPE_WIDTH / 2,
pipe3Height / 2,
Constants.PIPE_WIDTH,
pipe3Height,
{isStatic: true},
);
const pipe4 = Matter.Bodies.rectangle(
Constants.MAX_WIDTH * 2 - Constants.PIPE_WIDTH / 2,
Constants.MAX_HEIGHT - pipe4Height / 2,
Constants.PIPE_WIDTH,
pipe4Height,
{isStatic: true},
);
Matter.World.add(world, [pipe3, pipe4]);
......
return {
......
pipe1: {
body: pipe1,
size: [Constants.PIPE_WIDTH, pipe1Height],
color: 'green',
renderer: Wall,
},
pipe2: {
body: pipe2,
size: [Constants.PIPE_WIDTH, pipe2Height],
color: 'green',
renderer: Wall,
},
pipe3: {
body: pipe3,
size: [Constants.PIPE_WIDTH, pipe3Height],
color: 'green',
renderer: Wall,
},
pipe4: {
body: pipe4,
size: [Constants.PIPE_WIDTH, pipe4Height],
color: 'green',
renderer: Wall,
},
};
然后更新Physics.js来移动这些障碍物
import Matter from 'matter-js';
import { Constants } from './App';
const Physics = (entities, {touches, time}) => {
let engine = entities.physics.engine;
let bird = entities.bird.body;
touches
.filter((t) => t.type === 'press')
.forEach((t) => {
Matter.Body.applyForce(bird, bird.position, {x: 0.0, y: -0.1});
});
// 遍历并移动障碍物
// 障碍物从右向左移动,障碍物当不可见时则将障碍物重新移动到最右端,因此我们只需要4个障碍即可
for (let i = 1; i <= 4; i++) {
if (
entities['pipe' + i].body.position.x <=
-1 * (Constants.PIPE_WIDTH / 2)
) {
Matter.Body.setPosition(entities['pipe' + i].body, {
x: Constants.MAX_WIDTH * 2 - Constants.PIPE_WIDTH / 2,
y: entities['pipe' + i].body.position.y,
});
} else {
Matter.Body.translate(entities['pipe' + i].body, {x: -1, y: 0});
}
}
// 由时间推移更新引擎
Matter.Engine.update(engine, time.delta);
return entities;
};
export default Physics;
碰撞检测
通过matter.js,我们可以很简单地添加碰撞检测事件,使游戏失败,同样是在setupWorld方法中添加
// 若出现碰撞 执行gameover
Matter.Events.on(engine, 'collisionStart', (event) => {
this.gameEngine.dispatch({type: 'game-over'});
});
之后我们只需要在主界面显示游戏结束提示,如蒙层+文字等,还可以加上再次游戏等交互
......
onEvent = (e) => {
if (e.type === 'game-over') {
//Alert.alert("Game Over");
this.setState({
running: false,
});
}
};
reset = () => {
this.gameEngine.swap(this.setupWorld());
this.setState({
running: true,
});
};
......
render() {
return (
<>
<StatusBar barStyle="dark-content" />
<View style={{height: '100%', flex: 1, backgroundColor: '#eee'}}>
<GameEngine
ref={(ref) => {
this.gameEngine = ref;
}}
style={styles.gameContainer}
systems={[Physics]}
onEvent={this.onEvent}
running={this.state.running}
entities={this.entities}
/>
{/* gameover */}
{!this.state.running && (
<TouchableOpacity
style={styles.fullScreenButton}
onPress={this.reset}>
<View style={styles.fullScreen}>
<Text style={styles.gameOverText}>Game Over</Text>
</View>
</TouchableOpacity>
)}
</View>
</>
);
}
至此,我们就在react native中使用Matter.js实现了一个简单的Flappy Bird游戏
来涂鸦工作: job.tuya.com/