おはよう君需要なし

求不得苦な日々

Reactで物理エンジンを使ってみよう!(React+matter+SVG)

はじめに

最近仕事から帰ってきてすぐダラダラしてしまうの本当によくないので、趣味でチマチマ技術ブログ的なことでもやろうかなと思います。

どなたかの役に立てば幸いです。

Reactを始めよう

Reactで何かを試作するときって普通は今まで組んできたやつを適当にコピペしてやってきたんですが、今回は気が向いたので create-react-app も使わずにwebpack-dev-server 一本でいこうと思います。

medium.com

Reactの初歩の初歩の構築はこのサイトが非常にまとまっていてよいです。

仕様

物理エンジンといってもReact用に開発されたものは少なく(ちょっと見当たらない)、結構ジェネラルにJavascript用~~~って感じであるのがほとんどです。

brm.io

個人的にはこの matter.js がデモサイトも凝っていて気になったので試用してみます。

何はともあれはじめる

class App extends Component {
  constructor(props) {
    super(props);
    this.state = { width: 640, height: 480 };
  }
  componentDidMount() {}
  render() {
    return (
      <div>
        <h3>react-physics</h3>
        <svg
          width={this.state.width}
          height={this.state.height}
          style={{ backgroundColor: '#243d52' }}
        />
      </div>
    );
  }
}

はじめのはじめ。こんなかんじでいいかな。最初は。

f:id:yoh_mar28:20181206231630p:plain
ただのはこ

matter.js 導入

おもむろに npm install 。

npm install --save matter-js

初期化部分は matter-js のサンプルコードを見ればよいでしょう。

const engine = Engine.create();
const runner = Runner.create();

class App extends Component {
  constructor(props) {
    super(props);
    this.state = { width: 640, height: 480, bodies: [] };
    this.handleUpdate = this.handleUpdate.bind(this);
  }

  componentDidMount() {
    Events.on(engine, 'afterUpdate', this.handleUpdate);
    Runner.run(runner, engine);
  }

  componentWillUnmount() {
    Events.off(engine, 'afterUpdate', this.handleUpdate);
    Runner.stop(runner);
  }

  handleUpdate() {
    const bodies = Matter.Composite.allBodies(engine.world);
    this.setState({ bodies });
  }
  handleButton() {
    const circle = Bodies.circle(0, -1.0, 10);
    Matter.Body.applyForce(circle, { x: 0, y: 0 }, { x: 0, y: -0.01 });
    World.add(engine.world, circle);
  }
  render() {
    const bodyItems = this.state.bodies.map(body => {
      if (!body.parts) return null;
      return body.parts.map(part => {
        return (
          <circle
            cx={part.position.x}
            cy={part.position.y}
            r={part.circleRadius}
            fill="lightgreen"
          />
        );
      });
    });
    return (
      <div>
        <h3>react-physics</h3>
        <svg
          width={this.state.width}
          height={this.state.height}
          style={{ backgroundColor: '#243d52' }}
        >
          <g
            transform={`scale(1,1) translate(${this.state.width / 2},${this
              .state.height / 2})`}
          >
            {bodyItems}
          </g>
        </svg>
        <button onClick={() => this.handleButton()}>PUSH</button>
      </div>
    );
  }
}

ちょこっと解説を。

matter-js はengineをスタートさせると afterUpdate イベントをトリガーするようになります。ですので、そこで Events.on (matterのヘルパです)で更新内容を取得し、stateに設定すれば render()が自動的に走り、更新内容を描画できるってわけ。

なんで matter-js にはレンダラが用意されてるのにわざわざ SVG 使ってるのかって??それは・・・

  • Reactが良しなに仮想DOMのレンダリングをコントロールしてくれる
  • クリックイベントをこちら(React)でハンドリングしたかったから

です!

楽しい!

f:id:yoh_mar28:20181206235332g:plain
楽しい!

終わりに

今後も物理エンジン使って何か技術ブログ的なことをやりたいと考えてはいるんですが、どうも時間がかかりすぎる・・・

「コード書く→動画撮る・変換する→記事書く」

このフローだけで1時間半はかかってる気がする・・・もうちょっと効率化したいです。

以上、コメント何かあればお願いいたします!

追記20201109:DIV版

Atsushiさんより、DIVでやってみる方法のご質問をいただきましたので、
僭越ながら修正してみました。

このようにすればDIVでも物理エンジンを使って動かすことができますね。
コメントいただいたAtsushiさんに感謝!

// MatterTest.js
import React from 'react';
import Matter, { Engine, Events, Runner, World, Bodies } from 'matter-js';

const engine = Engine.create();
const runner = Runner.create();

class MatterTest extends React.Component {
  constructor(props) {
    super(props);
    this.state = { width: 640, height: 480, bodies: [] };
    this.handleUpdate = this.handleUpdate.bind(this);
  }

  componentDidMount() {
    //Wall
    World.add(engine.world, [
      Bodies.rectangle(0, 240, 800, 6, {
        isStatic: true,
        render: {
          fillStyle: '#2E2E2E',
          //fillStyle: "#000",
        },
      }),
    ]);

    Events.on(engine, 'afterUpdate', this.handleUpdate);
    Runner.run(runner, engine);
  }

  componentWillUnmount() {
    Events.off(engine, 'afterUpdate', this.handleUpdate);
    Runner.stop(runner);
  }

  handleUpdate() {
    const bodies = Matter.Composite.allBodies(engine.world);
    this.setState({ bodies });
  }
  handleButton() {
    const circle = Bodies.circle(0, -1.0, 40);
    Matter.Body.applyForce(circle, { x: 0, y: 0 }, { x: 0, y: -0.2 });
    World.add(engine.world, circle);
  }
  render() {
    const bodyItems = this.state.bodies.map(body => {
      if (!body.parts) return null;
      return body.parts.map((part, idx) => {
        const radius = part.circleRadius;
        const left = part.position.x - radius + this.state.width / 2;
        const top = part.position.y - radius + this.state.height / 2;

        const divStyle = {
          position: 'absolute',
          left,
          top,
          width: radius * 2,
          height: radius * 2,
          backgroundColor: 'lightgreen',
        };
        return <div className="object" style={divStyle} key={`obj_${idx}`} />;
      });
    });

    return (
      <React.Fragment>
        <div
          className="object_container"
          style={{
            width: this.state.width,
            height: this.state.height,
            backgroundColor: '#243d52',
          }}
        >
          {bodyItems}
        </div>
        <button onClick={() => this.handleButton()}>PUSH</button>
      </React.Fragment>
    );
  }
}
export default MatterTest;