Whack-A-Mole(もぐらたたき) 第三回
フロントエンジニアの、YAMATAKUです。
前回、もぐらの表示&アニメーションの実装まで行いました。今回は、ゲームを仕上げていきたいと思います。
まずは、もぐらクラスのすべてのアクションを実装していきます。
もぐらクラス修正
// 省略 //
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は次のステージに引き継ぐ)
- ステージが進むたびに、もぐらの出現スピードが早くなる
- ステージ上限なし
クリア/ゲームオーバー/一時停止 画面実装
前述仕様において、以下画面を実装します。
- クリア画面
- ゲームオーバー画面
- 一時停止画面
を用意します。
まずは、各クラスのスタイルをまとめたクラスを作成します。
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',
},
})
各画面を実装していきます。
- クリア画面
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;
- ゲームオーバー画面
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>
);
}
};
- 一時停止画面
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に、今回作成したクラスを実装していきます。
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({
// 省略
})
まとめ
今回のチュートリアルで、ReactNativeでゲームを作る上での、お作法を学べた気がします。
以下にソースをアップしました。
次回からは、スマホゲームの火付け役「FlappyBird」のゲームサンプルを学習していきたいと思います。
参考:
https://github.com/lepunk/react-native-videos/tree/master/FlappyBird