物理引擎Matter.js与在RN内的使用

avatar
@https://www.tuya.com/

作者: 涂鸦-若叶

来涂鸦工作: 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 实现、兼容移动端(触摸、响应)

687474703a2f2f62726d2e696f2f6d61747465722d6a732f696d672f6d61747465722d6a732e737667.svg Jul-28-2020 15-27-57.gif

开始

引入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); // 运行渲染器

MatterJS_EngineAndWorld.jpg

添加一个矩形刚体

// 添加一个矩形刚体
// 前四个参数分别代表矩形刚体的 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)

约束关系

MatterJS_Constraint.jpg

约束可理解为通过一条线,将刚体 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小游戏

项目创建和初始化不再赘述,直接开始

logo.png

创建世界和刚体鸟

首先,我们在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>
    </>
  );
}

企业微信20200729114452.png

至此,我们就在react native中使用Matter.js实现了一个简单的Flappy Bird游戏


来涂鸦工作: job.tuya.com/