2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

React Native ゲーム開発 FlappyBird 番外編

Posted at

React Native ゲーム開発 FlappyBird 番外編

フロントエンジニアの、YAMATAKUです。
今回は、FlappyBirdの開発にあたり、

  • react-native-game-engine
  • Matter.js

上記2プラグインを使用して、物理演算の実装だけに注目した実装をしていきます。
環境構築については、以下の記事をご参考ください。

・React Nativeでゲームを作る(ための、はじめの一歩):
https://qiita.com/team-lot/items/3fe8d3f77535a7cae8b6

出典

Making a Production-Ready Flappy Bird in React Native
https://medium.com/better-programming/flappy-bird-with-react-native-game-engine-and-matter-js-d5673f50eb9

プロジェクト作成/ビルド

例によって、ターミナル起動し、適当なディレクトリに移動して、プロジェクトを作成していきます。

$ react-native init FlappyBirdPhysicsEngine
$ cd FlappyBirdPhysicsEngine
$ react-native run-android 

環境構築ができていた場合、Androidエミュレータが起動し、「Welcome to React」というタイトルページが起動したと思います。

プラグイン

今回の主となるプラグイン(react-native-game-engine、Matter.js)をインストールします。

$ npm install react-native-game-engine matter-js --save

コーディング

まず最初に、共通設定ファイルを作成し、編集します。

./Const.js

import { Dimensions } from 'react-native';

export default Constants = {
    MAX_WIDTH: Dimensions.get("screen").width,
    MAX_HEIGHT: Dimensions.get("screen").height,
    GAP_SIZE: 200, // gap between the two parts of the pipe
    PIPE_WIDTH: 100 // width of the pipe
}

次に天井/床/障害物となる共通のオブジェクトクラス作成します

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
        }}
      />
    );
  }
}

次は、物理演算クラスを作成します

./Physics.js
import Matter from 'matter-js';

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.00,
        y: -0.05
      }
    );
  });

  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;

最後に メインビューとなるApp.jsを以下の様に修正します。

./App.js
//App.jsの初期コードを全消しして、rncでスペニットからコードを生成する

import React, { Component } from 'react';
import { View, StyleSheet, StatusBar, TouchableOpacity, Alert, Text} from 'react-native';
import Matter from 'matter-js';
import { GameEngine } from 'react-native-game-engine';
import Bird from './Bird';
import Constants from './Constants';
import Physics from './Physics';
import Wall from './Wall';

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;
}

export default class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      running: true
    };

    this.gameEngine = null;
    this.entities = this.setupWorld();
  }

  //毎tick描画
  setupWorld = () => {
    let engine = Matter.Engine.create({
      enableSleeping: false
    });
    let world = engine.world;

    //50x50の鳥の体を作成
    let bird = Matter.Bodies.rectangle(
      Constants.MAX_WIDTH / 4,   //画面横位置から
      Constants.MAX_HEIGHT / 2,  //画面縦位置から
      50,                        //横サイズ
      50                         //縦サイズ
    );

    //床
    let floor = Matter.Bodies.rectangle(
      Constants.MAX_WIDTH / 2,
      Constants.MAX_HEIGHT - 25,
      Constants.MAX_WIDTH,
      50,
      { isStatic: true }
    );

    //天井
    let ceiling = Matter.Bodies.rectangle(
      Constants.MAX_WIDTH / 2,
      25,
      Constants.MAX_WIDTH,
      50,
      { isStatic: true }
    );

    //障害物1&2
    let [pipe1Height, pipe2Height] = generatePipes();
    let pipe1 = Matter.Bodies.rectangle(
      Constants.MAX_WIDTH - (Constants.PIPE_WIDTH / 2),
      pipe1Height / 2,
      Constants.PIPE_WIDTH,
      pipe1Height,
      { isStatic: true }
    )
    let pipe2 = Matter.Bodies.rectangle(
      Constants.MAX_WIDTH - (Constants.PIPE_WIDTH / 2),
      Constants.MAX_HEIGHT - (pipe2Height / 2),
      Constants.PIPE_WIDTH,
      pipe2Height,
      { isStatic: true }
    )

    //障害物3&4
    let [pipe3Height, pipe4Height] = generatePipes();
    let pipe3 = Matter.Bodies.rectangle(
      Constants.MAX_WIDTH * 2 - (Constants.PIPE_WIDTH / 2),
      pipe3Height / 2,
      Constants.PIPE_WIDTH,
      pipe3Height,
      { isStatic: true }
    )
    let 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(ゲームシーン, [ゲーム内オブジェクト配列])
    Matter.World.add(world, [bird, floor, ceiling, pipe1, pipe2, pipe3, pipe4]);      

    //衝突判定
    Matter.Events.on(engine, 'collisionStart', (event) => {
      var pairs = event.pairs;
      console.log("Pair no visible: ", pairs)
      console.log("Pair visible: ", pairs[0]);
      console.log("colision between " + pairs[0].bodyA.label + " - " + pairs[0].bodyB.label);


      this.gameEngine.dispatch({ type: "game-over"}); //onEventにgame-overを発疹
    })

    //GameEngineに返す描画プロパティ配列?
    return {
      physics: { engine: engine, world: world },
      bird: { body: bird, size: [50, 50], color: 'red', 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 },
      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 },
    }
  }

  onEvent = (e) => {
    if (e.type === "game-over") {
      this.setState({
        running: false
      });
    }
  }
  
  reset = () => {
    this.gameEngine.swap(this.setupWorld());
    this.setState({
      running: true
    });
  }

  render() {
    return (
      <View style={styles.container}>
        <GameEngine
          ref={(ref) => { this.gameEngine = ref; }}
          style={styles.gameContainer}
          running={this.state.running}
          systems={[Physics]}
          onEvent={this.onEvent}
          entities={this.entities}>
          <StatusBar hidden={true} />
        </GameEngine>
        {!this.state.running &&
          <TouchableOpacity
            style={styles.fullScreenButton}
            onPress={this.reset}>
            <View style={styles.fullScreen}>
              <Text style={styles.gameOverText}>GameOver</Text>
            </View>
          </TouchableOpacity>
        }
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
  },
  gameContainer: {
    position: 'absolute',
    top: 0,
    bottom: 0,
    left: 0,
    right: 0,
  },
  gameOverText: {
    color: 'white',
    fontSize: 48
  },
  fullScreen: {
    position: 'absolute',
    top: 0,
    bottom: 0,
    left: 0,
    right: 0,
    backgroundColor: 'black',
    opacity: 0.8,
    justifyContent: 'center',
    alignItems: 'center'
  },
  fullScreenButton: {
    position: 'absolute',
    top: 0,
    bottom: 0,
    left: 0,
    right: 0,
    flex: 1
  }
})

以上をビルドした結果が以下になります。

タイトルなし.gif

FlappyBirdっぽくなってますね。
ただ筆者の場合、Physics.jsのタッチ処理あたりがあやしく、いきなりバードが急上昇したりする事象を確認済みです。これは、次回以降のFlappyBird本番にて改善が確認出来次第、こちらの記事も修正して参ります。

最後に

今回は、FlappyBirdの物理エンジン部分の実装だけをしました。
次回は、いよいよFlappyBirdに実装に入っていきます。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?