5
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 ゲーム開発 Whack-A-Mole(もぐらたたき) 第三回

Last updated at Posted at 2020-04-29

Whack-A-Mole(もぐらたたき) 第三回

フロントエンジニアの、YAMATAKUです。
前回、もぐらの表示&アニメーションの実装まで行いました。今回は、ゲームを仕上げていきたいと思います。

まずは、もぐらクラスのすべてのアクションを実装していきます。

もぐらクラス修正

Mole.js

// 省略 //

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

    this.mole = null;           //もぐら本体
    this.actionTimeout = null;  //アニメーション用
    this.isPopping = false;     //飛び出る
    this.isFeisty = false;      //気力
    this.isHealing = false;     //回復もぐら
    this.isWhacked = false;     //叩かれた
    this.isAttacking = false;   //攻撃した
  }

  /**
    * 飛び出る
    * @return void
   */
  pop = () => {
    this.isWhacked = false;
    this.isPopping = true;
    this.isAttacking = false;

    //40%:攻撃あり、57%:表示だけ、3%:回復もぐら
    this.isFeisty = Math.random() < 0.6;
    if (!this.isFeisty) {
      this.isHealing = Math.random() < 0.05;
    }

    if (this.isHealing) {
      this.mole.play({  //playはSpriteSheetのアニメ関数
        type: "heal",   //回復もぐら
        fps: 24,
        onFinish: () => {
          this.actionTimeout = setTimeout(() => {
            this.mole.play({
              type: "hide",
              fps: 24,
              onFinish: () => {
                this.isPopping = false;
                this.props.onFinishPopping(this.props.index);
              }
            })
          }, 1000);
        }
      })
    } else {
      this.mole.play({
        type: "appear",
        fps: 24,
        onFinish: () => {
          if(this.isFeisty) {   //もぐらの攻撃
            this.actionTimeout = setTimeout(() => {
              this.isAttacking = true;
              this.props.onDamage();
              this.mole.play({
                type: "attack",
                fps: 12,
                onFinish: () => {
                  this.mole.play({
                    type: "hide",
                    fps: 24,
                    onFinish: () => {
                      this.isPopping = false;
                      this.props.onFinishPopping(this.props.index);
                    }
                  })
                }
              })
            }, 1000);
          } else {
            this.actionTimeout = setTimeout(() => {
              this.mole.play({
                type: "hide",
                fps: 24,
                onFinish: () => {
                  this.isPopping= false;
                  this.props.onFinishPopping(this.props.index);
                }
              })
            }, 1000);
          }
        }
      })
    }
  }

  /**
   * 叩く
   * @return void
   */
  whack = () => {
        console.log(`wack1 ${this.isPopping} ${this.isWhacked} ${this.isAttacking}`);

    if (!this.isPopping || this.isWhacked || this.isAttacking) {  //非表示 or 叩かれてる or 攻撃してきた 場合はreturn
      return;
    }

    console.log('wack2');

    //やっつけたので、以降のもぐらアニメーションは無効に
    if (this.actionTimeout) {
      clearTimeout(this.actionTimeout);
    }

    this.isWhacked = true;    //叩かれたフラグ、連続押し防止
    this.isFeisty = false;    //念のため

    this.props.onScore();
    if (this.isHealing) {
      this.props.onHeal();
    }

    this.mole.play({
      type: "dizzy",    //めまい
      fps: 24,
      onFinish: () => {
        this.mole.play({
          type: "faint",    //気絶、失神いう意味でフェイント(feint)ではない
          fps: 24,
          onFinish: () => {
            this.isPopping = false;
            this.props.onFinishPopping(this.props.index);
          }
        })
      }
    })
  }

  render() {
    // 省略 //
  }
} 

ゲーム仕様

唐突ですが、今回のゲームは以下の様な仕様になっています。

  • もぐらは、何もしないで穴に戻る/攻撃してくる/回復もぐらの3種類
  • 1ステージ5秒間(ステージが進むごとにレベル表示が1上がる)
  • プレイヤーはHP(Healthメーター)を持ち、もぐらの攻撃によりダメージをうける
  • もぐらが攻撃するまえにタッチすれば攻撃は回避できる
  • 1ステージ5秒以内にHPが0にならなければクリア(HPは次のステージに引き継ぐ)
  • ステージが進むたびに、もぐらの出現スピードが早くなる
  • ステージ上限なし

クリア/ゲームオーバー/一時停止 画面実装

前述仕様において、以下画面を実装します。

  • クリア画面
  • ゲームオーバー画面
  • 一時停止画面

を用意します。

まずは、各クラスのスタイルをまとめたクラスを作成します。

PopupStyles.js
import { StyleSheet } from 'react-native';
import Constants from './Constants';

export default styles = StyleSheet.create({
  clearScreen: {
    position: 'absolute',
    top: 0,
    left: 0,
    width: Constants.MAX_WIDTH,
    height: Constants.MAX_HEIGHT,
    backgroundColor: 'rgba(255, 255, 255, 0.5)',
    justifyContent: 'center',
    alignItems: 'center',
    flexDirection: 'column',
  },
  clearedLevelContainer: {
    width: Constants.YR * 250,
    height: Constants.YR * 250,
    borderRadius: Constants.YR * 125,
    backgroundColor: '#ff1a1a',
    borderWidth: 5,
    borderColor: 'white',
    alignItems: 'center',
    justifyContent: 'center',
    flexDirection: 'column',
  },
  clearedLevelText: {
    fontSize: 45,
    color: 'white',
    fontFamily: Constants.FONT_LITITAONE_REGULAR,
  },
  panel: {
    backgroundColor: '#29aecc',
    borderColor: 'white',
    borderWidth: 5,
    borderRadius: 10,
    alignItems: 'center',
    justifyContent: 'center',
    width: Constants.YR * 350,
    height: Constants.YR * 200,
    marginTop: Constants.YR * -40,
  },
  panelTitle: {
    fontSize: 45,
    color: 'black',
    fontFamily: Constants.FONT_LITITAONE_REGULAR,
    textShadowColor: 'white',
    textShadowOffset: { width: 1, height: 1},
    textShadowRadius: 2,
  },
  panelText: {
    fontSize: 31,
    color: 'white',
    fontFamily: Constants.FONT_LITITAONE_REGULAR,
    textShadowColor: 'black',
    textShadowOffset: { width: 1, height: 1},
    textShadowRadius: 2,
    marginBottom: Constants.YR * 50,
  },
  panelButtonsContainer: {
    position: 'absolute',
    height: Constants.YR * 80,
    bottom: Constants.YR *  -40,
    alignItems: 'center',
    justifyContent: 'center',
    width: Constants.YR * 350,
    flexDirection: 'row'
  },
  panelButton: {
    width: Constants.YR * 80,
    height: Constants.YR * 80,
    borderRadius: Constants.YR * 40,
    backgroundColor: '#ff1a1a',
    borderWidth: 5,
    borderColor: 'white',
    marginLeft: Constants.XR * 15,
    marginRight: Constants.XR * 15,
    alignItems: 'center',
    justifyContent: 'center',
  },
  panelButtonIcon: {
    width: Constants.YR * 35,
    height: Constants.YR * 35,
    alignItems: 'center',
    justifyContent: 'center',
  },
})

各画面を実装していきます。

  • クリア画面
Clear.js

import React, { Component } from 'react';
import { 
  View,
  Text,
  TouchableWithoutFeedback,
  Image
} from 'react-native';
import styles from './PopupStyles';

class Clear extends Component {
  render() {
    return (
      <View style={styles.clearScreen}>
        <View style={styles.clearedLevelContainer}>
          <Text style={styles.clearedLevelText}>Level</Text>
          <Text style={styles.clearedLevelText}>{this.props.level}</Text>
        </View>

        <View style={styles.panel}>
          <Text style={styles.panelTitle}>Cleared</Text>
          <Text style={styles.panelText}>Score:{this.props.score}</Text>

          <View style={styles.panelButtonsContainer}>
            <TouchableWithoutFeedback onPress={this.props.onReset}>
              <View style={styles.panelButton}>
                <Image style={styles.panelButtonIcon} resizeMode="contain" source={Images.restartIcon} />
              </View>
            </TouchableWithoutFeedback>
            <TouchableWithoutFeedback onPress={this.props.onNextLevel}>
              <View style={styles.panelButton}>
                <Image style={styles.panelButtonIcon} resizeMode="contain" source={Images.playIcon} />
              </View>
            </TouchableWithoutFeedback>
          </View>
        </View>
      </View>
    );
  }
}
export default Clear;
  • ゲームオーバー画面
GameOver.js
import React, { Component } from 'react';
import { 
  View,
  Text,
  TouchableWithoutFeedback,
  Image
 } from 'react-native';
 import styles from './PopupStyles';

 export default class GameOver extends Component {
   render() {
      return (
        <View style={styles.clearScreen}>
          <View style={styles.clearedLevelContainer}>
            <Text style={styles.clearedLevelText}>Level</Text>
            <Text style={styles.clearedLevelText}>{this.props.level}</Text>
          </View>

          <View style={styles.panel}>
            <Text style={styles.panelTitle}>Game Over</Text>
            <Text style={styles.panelText}>Score:{this.props.score}</Text>
          
            <View style={styles.panelButtonsContainer}>
              <TouchableWithoutFeedback onPress={this.props.onReset}>
                <View style={styles.panelButton} >
                  <Image style={styles.panelButtonIcon} resizeMode="contain" source={Images.restartIcon} />
                </View>
              </TouchableWithoutFeedback>
            </View>
          </View>
       </View>
     );
   }
 };
  • 一時停止画面
Pause.js
import React, { Component } from 'react';
import { 
  View, 
  Text,
  TouchableWithoutFeedback,
  Image
} from 'react-native';
import styles from './PopupStyles';

class Pause extends Component {
  render() {
    return (
      <View style={styles.clearScreen}>
        <View style={styles.panel}>
          <Text style={styles.panelTitle}>Ready?</Text>

          <View style={styles.panelButtonsContainer}>
            <TouchableWithoutFeedback onPress={this.props.onReset}>
              <View style={styles.panelButton}>
                <Image style={styles.panelButtonIcon} resizeMode="contain" source={Images.restartIcon} />
              </View>
            </TouchableWithoutFeedback>
            <TouchableWithoutFeedback onPress={this.props.onResume}>
              <View style={styles.panelButton}>
                <Image style={styles.panelButtonIcon} resizeMode="contain" source={Images.playIcon} />
              </View>
            </TouchableWithoutFeedback>
          </View>
        </View>
      </View>
    );
  }
}

export default Pause;

GameOver.jsだけ、「export default class GameOver」と定義していますが、他2クラスのように、ソース末尾に「export default クラス名」としても、問題ありません。ここで"export default"したクラスは、他のクラスで、

(例)GameOver.jsを呼び出す場合

import GameOver from './GameOver';

のように呼び出すことができます。

仕上げ

App.jsに、今回作成したクラスを実装していきます。

App.js

import React, { Component } from "react";
import { 
    // 追加
    TouchableWithoutFeedback,
    Button,
} from "react-native";

// 追加
import SpriteSheet from 'rn-sprite-sheet';
import GameOver from './GameOver';
import Clear from './Clear';
import Pause from './Pause';


const DEFAULT_TIME = 5;   //1ステージ5秒に設定

// 省略

export default class MainScreen extends Component {
  constructor(props){
    super(props);
    this.state = DEFAULT_STATE;
    this.moles = [];
    this.molesPopping = 0;
    this.interval = null;
    this.timeInterval = null;
  }


//省略

  reset = () => {
    this.molesPopping = 0;

    this.setState(DEFAULT_STATE, this.setupTicks);
  }

  pause = () => {
    console.log('pause1');
    if (this.interval) clearInterval(this.interval);
    if (this.timeInterval) clearInterval(this.timeInterval);
    
    console.log('pause2');

    this.setState({
      paused: true
    });
  }

  resume = () => {
    this.molesPopping = 0;
    this.setState({
      paused: false
    }, this.setupTicks);
  }

  nextLevel = () => {
    this.molesPopping = 0;

    this.setState({
      level: this.state.level + 1,
      cleared: false,
      gameover: false,
      time: DEFAULT_TIME
    }, this.setupTicks)
  }
  

  onScore = () => {
    this.setState({
      score: this.state.score + 1
    })
  }

  onDamage = () => {
    if(this.state.cleared || this.state.gameOver || this.state.paused) {
      return;
    }

    let targetHealth = this.state.health - 10 < 0 ? 0 : this.state.health - 10;

    this.setState({
      health: targetHealth
    });

    if (targetHealth <= 0) {
      this.gameOver();
    }
  }

  gameOver = () => {
    clearInterval(this.interval);
    clearInterval(this.timeInterval);

    this.setState({
      gameover: true
    });
  }

  onHeal = () => {
    let targetHealth = this.state.health + 10 > 50 ? 50 : this.state.health + 10;
    this.setState({
      health: targetHealth
    });
  }

  render() {
    // 省略
    return (
      <View style={styles.container}>
        
       //省略        

        {this.state.cleared && <Clear onReset={this.reset} onNextLevel={this.nextLevel} level={this.state.level} score={this.state.score} />}
        {this.state.gameover && <GameOver onReset={this.reset} level={this.state.level} score={this.state.score} />}
        {this.state.paused && <Pause onReset={this.reset} onResume={this.resume} />}
      </View> 
    )
  }
 }

const styles = StyleSheet.create({
  // 省略
})

これで実装が完了です。以下のような感じのゲームになります。
タイトルなし.gif

まとめ

今回のチュートリアルで、ReactNativeでゲームを作る上での、お作法を学べた気がします。
以下にソースをアップしました。

次回からは、スマホゲームの火付け役「FlappyBird」のゲームサンプルを学習していきたいと思います。

参考:
https://github.com/lepunk/react-native-videos/tree/master/FlappyBird

5
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
5
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?