Learn Unity 4 for iOS Game Development 8
レッツ・プレイ! ゲーム・スクリプティング
作ったもの
・ボウリング・ゲームへの、ゲーム・ロジックの実装。
クリックすると、別ウィンドウが開きます。(音が出ます。)
http://shakeweb.sakura.ne.jp/demo/LU4_chap8/
————————————————————————————————
・FSM(finite state machine)の概念に則って、汎用的にスコアを計算するためのクラス郡を収めたライブラリ用の .js ファイルを用意する。
————————————————————————————————
フレームごとのスコアを保持するクラス
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
class FuguBowlScore { var ball1:int; // pins down for ball 1 var ball2:int; // pins down for ball 2 var ball3:int; // pins down for ball 3 var total:int; // total score for this frame (may include future rolls) function Clear() { ball1 = -1; ball2 = -1; ball3 = -1; total = -1; } function IsSpare():boolean { // doesn't handle spare on ball3 return !IsStrike() && (ball1 + ball2 == 10); } function IsStrike():boolean { return ball1 == 10; } } |
・スペア/ストライク時、以降のフレームが終了するまでスコアが未確定の場合は、-1 とする。
————————————————————————————————
スコアを計算するクラス
・クラス内に、変数、コンストラクタ、スペア/ストライク判定用関数を用意。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
class FuguBowlPlayer { var scores:FuguBowlScore[]; // all 10 frames of the game // constructor function FuguBowlPlayer() { scores = new FuguBowlScore[10]; for (var i:int = 0; i < scores.length; ++i) { scores[i] = new FuguBowlScore(); } ClearScore(); } function ClearScore() { for (var score:FuguBowlScore in scores) { score.Clear(); } } function IsSpare(frame:int):boolean { return scores[frame].IsSpare(); } function IsStrike(frame:int):boolean { return scores[frame].IsStrike(); } |
・scores:FuguBowlScore[] : built-in array : 処理は早いが、一度作るとリサイズできない。
・コンストラクタで、配列 scores を用意し、10フレーム分のスコアを初期化して未確定にしておく。
・各フレームのスペア、ストライク判定用の関数 IsSpare()、IsStrike() を用意。
・FuguBowlPlayer クラスは、FuguBowlScore クラスのラッパーのようなもの。
・スペア/ストライク時のスコア更新用関数を用意。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
function SetSpareScore(frame:int) { var framescore:FuguBowlScore = scores[frame]; framescore.total = framescore.ball1 + framescore.ball2 + scores[frame + 1].ball1; } function SetStrikeScore(frame:int) { var framescore:FuguBowlScore = scores[frame]; framescore.total = framescore.ball1; framescore.total += scores[frame + 1].ball1; if(frame < 8 && IsStrike(frame + 1)) { // 次のフレームがストライクだった場合. framescore.total += scores[frame + 2].ball1; }else{ // 通常時. framescore.total += scores[frame + 1].ball2; } } |
・スペア時 : 10(1投目 + 2投目)+ 次のフレームの最初の投球。
・ストライク時 : 10(ストライク)+ 次のフレームの最初の投球 + 次の投球。
・1投目のスコア確定用関数。
1 2 3 4 5 6 7 8 9 |
function SetBall1Score(frame:int, pinsDown:int) { scores[frame].ball1 = pinsDown; if(frame > 0 && IsSpare(frame - 1)) { // 1投前がスペアだった場合は、1投前のスコアを確定させる. SetSpareScore(frame - 1); } if(frame > 1 && IsStrike(frame - 1) && IsStrike(frame - 2)) { // 2投前がストライクだった場合は、2投前のスコアを確定させる. SetStrikeScore(frame - 2); } } |
・2投目のスコア確定用関数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function SetBall2Score(frame:int, pinsDown:int) { var framescore:FuguBowlScore = scores[frame]; if(IsStrike(frame)) { // we must be in the final frame framescore.ball2 = pinsDown; } else { framescore.ball2 = pinsDown - framescore.ball1; } if(!IsSpare(frame) && !IsStrike(frame)) { framescore.total = pinsDown; } if(frame > 0 && IsStrike(frame - 1)) { // 1フレーム前(=2投前)がスペアだった場合は、1投前のスコアを確定させる. SetStrikeScore(frame - 1); } } |
・3投目のスコア確定用関数。
1 2 3 4 5 6 7 8 9 |
function SetBall3Score(frame:int, pinsDown:int) { var framescore:FuguBowlScore = scores[frame]; if(IsStrike(frame) && framescore.ball2 < 10) { framescore.ball3 = pinsDown - framescore.ball2; } else { framescore.ball3 = pinsDown; // 前の投球がスペアかストライクの場合. } framescore.total = framescore.ball1 + framescore.ball2 + framescore.ball3; } |
・総合得点取得用関数。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function GetScore(frame:int):int { if(frame == 0 || scores[frame].total == -1) { return scores[frame].total; } else { var prev:int = GetScore(frame - 1); if(prev == -1) { return -1; } else { return scores[frame].total + prev; } } } } |
・recursive function : 自分自身を呼ぶ関数。
————————————————————————————————
・ピンが倒れているかどうかを判別するスクリプトをそれぞれのピンに適用する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#pragma strict var knockedAngle:float = 45.0; private var initialAngles:Vector3; function Awake() { initialAngles = transform.localEulerAngles; } function IsKnockedOver() { return Mathf.Abs(transform.localEulerAngles.x-initialAngles.x) > knockedAngle || Mathf.Abs(transform.localEulerAngles.y-initialAngles.y) > knockedAngle || Mathf.Abs(transform.localEulerAngles.z-initialAngles.z) > knockedAngle; } |
————————————————————————————————
ゲーム・コントローラーに処理を追加
・FuguBowlPlayer をインスタンス化。
1 2 3 4 5 6 |
static var player:FuguBowlPlayer = null; function Awake () { player = new FuguBowlPlayer(); CreatePins(); } |
・倒れているピンの数をカウントする。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
private var pins:Array; private var pinBodies:GameObject[]; // the real physical pins function CreatePins () { pins = new Array(); var offset = Vector3.zero; for (var row=0; row < pinRows; ++row) { offset.z += pinDistance; offset.x = -pinDistance * row/2; // その列の一番左のピンの配置位置を求める. for (var n=0; n <= row; ++n) { // 1列目は1本、2列目は2本・・・. pins.push(Instantiate(pin, pinPos + offset, Quaternion.identity)); offset.x += pinDistance; // 左から順番に配置. } } pinBodies = GameObject.FindGameObjectsWithTag("Pin"); } function GetPinsDown():int { var pinsDown:int = 0; for (var pin:GameObject in pinBodies) { if(pin.GetComponent(FuguPinStatus).IsKnockedOver()) { ++pinsDown; } } return pinsDown; } |
・倒れたピンを非表示にする。
1 2 3 4 5 6 7 |
function RemoveDownedPins() { for(var pin:GameObject int pinBodies) { if(pin.GetComponent(FuguPinStatus).IsKnockedOver()) { pin.SetActive(false); } } } |
・SetActive(false) で、GameObject を非アクティブにできる。
・ResetPins 関数に非アクティブにしたピンを再度、アクティブにする処理を追加。
1 2 3 4 5 6 |
function ResetPins() { for (var pin:GameObject in pinBodies) { pin. SetActive(true); // 追加. pin.SendMessage("ResetPosition"); } } |
————————————————————————————————
FSM (finite state machine)
・ゲーム進行を整理する。
New Game
↓ ※
Ball1 || Ball2 || Ball3
Rolling
RolledPast || GutterBall
Roll Over
Spare || Strike || KnockedSomeDown
NextBall
↓(最終フレームが来るまで ※ に戻る)
GameOver
・現在のフレーム数と何投目かを入れる変数を用意。
1 2 3 4 5 6 7 8 |
private var frame:int; // current frame, ranges for 0-9 (representing 1-10) private var roll:Roll; // the current roll in the current frame enum Roll { Ball1, Ball2, Ball3 } |
・前章で用意した Update() 関数を削除し、代わりに Start() 関数内でコルーチンを使用して FSM を実装する。
・それぞれのステートに合わせた処理を、コルーチンとして呼び出す。
1 2 3 4 5 6 7 8 9 10 11 12 |
private var state:String; function Start() { state = "StateNewGame"; //var tmpCnt:int = 0; while(true) { //++tmpCnt; Debug.Log("State: " + state + ", Frame: " + (frame + 1) + " (" + roll + "), Score: " + player.GetScore(0)); yield StartCoroutine(state); yield; } } |
・yield で処理を明け渡した上で StartCoroutine() でコルーチンを実行し、while 文のループを一時停止させる。
・コルーチンと yield について リファレンス
・ゲーム開始時のステート、StateNewGame の処理。
1 2 3 4 5 |
function StateNewGame() { player.ClearScore(); frame = 0; state = "StateBall1"; } |
・1投目のステート、StateBall1 で投球前の初期化処理。
1 2 3 4 5 |
function StateBall1() { ResetEverything(); // reset pins, camera and ball roll = Roll.Ball1; state = "StateRolling"; } |
・2投目のステート、StateBall2 で投球前の初期化処理。
1 2 3 4 5 6 7 8 9 10 11 |
function StateBall2() { ResetBall(); ResetCamera(); if(GetPinsDown() == 10) { ResetPins(); } else { RemoveDownedPins(); } roll = Roll.Ball2; state = "StateRolling"; } |
・3投目のステート、StateBall3 で投球前の初期化処理。
1 2 3 4 5 6 7 8 9 10 11 |
function StateBall3() { ResetBall(); ResetCamera(); if(GetPinsDown() == 10) { ResetPins(); } else { RemoveDownedPins(); } roll = Roll.Ball3; state = "StateRolling"; } |
・投球後のステート、StateRolling の処理。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function StateRolling() { while(true) { if(ball.transform.position.z > pinPos.z) { // ボールがピンのラインを越えた時. state = "StateRolledPast"; return; } if(ball.transform.position.y < sunkHeight) { // 落下時. state = "StateGutterBall"; return; } yield; } } |
・yield で処理を明け渡した上で、while 文をループさせ続け、条件を満たした時に処理を抜ける。
・投球後、ボールがピンのラインを越えた時のステート、StateRolledPast の処理。
1 2 3 4 5 6 7 8 9 |
function StateRolledPast() { var follow:Behaviour = Camera.main.GetComponent("SmoothFollow"); if(follow != null) { follow.enabled = false; } ball.GetComponent(FuguForce).enabled = false; yield WaitForSeconds(5); state = "StateRollOver"; } |
・SmoothFollow 型や MonoBehaviour 型の変数に代入しても、enabled / disabled を切り替えられるが、汎用性を考慮して、Behaviour 型に代入。
Behaviour クラス → MonoBehaviour クラス → SmoothFollow 等の GameObject に適用されたスクリプトのクラス。
・l.6では GetComponent() にクラス名を渡しているのに対し、l.2ではストリングを渡している。
l.2 GetComponent(“SmoothFollow”) : ランタイムエラー
l.6 GetComponent(FuguForce) : コンパイラエラー
SmoothFollow クラスは、Unity の開発環境によっては、存在しない可能性もあり得る。
存在しないクラスの enabled 変数にアクセスしようとすれば、エラーになる。
ストリングを渡せば、同名のコンポーネントが見つからない場合は、ランタイム時に null が返ってくるため、null チェックができる。
・yield WaitForSecontds() で処理を明け渡した上で、5秒間、一時停止させる。
・disabled にされたコンポーネントを、リセット時に、再度、enabled にする。
1 2 3 4 5 6 7 8 9 10 11 12 |
function ResetBall() { ball.GetComponent(FuguForce).enabled = true; ball.SendMessage("ResetPosition"); } function ResetCamera() { var follow:Behaviour = Camera.main.GetComponent("SmoothFollow"); if(follow != null) { follow.enabled = true; } Camera.main.SendMessage("ResetPosition"); } |
・ガター時のステート、StateGutterBall の処理。
1 2 3 |
function StateGutterBall() { state = "StateRollOver"; } |
・ガター時/ボールがピンのラインを越えた時のステート、StateRollOver の処理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
function StateRollOver() { var pinsDown:int = GetPinsDown(); switch(roll) { case Roll.Ball1: player.SetBall1Score(frame, pinsDown); break; case Roll.Ball2: player.SetBall2Score(frame, pinsDown); break; case Roll.Ball3: player.SetBall3Score(frame, pinsDown); break; } if(roll == Roll.Ball1 && player.IsStrike(frame)) { state = "StateStrike"; return; } if(roll == Roll.Ball2 && player.IsSpare(frame)) { state = "StateSpare"; return; } state = "StateKnockedSomeDown"; } |
・投球後の結果の各ステート、StateSpare、StateStrike、StateKnockedSomeDown の処理。
1 2 3 4 5 6 7 8 9 10 11 |
function StateSpare() { state = "StateNextBall"; } function StateStrike() { state = "StateNextBall"; } function StateKnockedSomeDown() { state = "StateNextBall"; } |
・投球完了時のステート、StateNextBall の処理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
function StateNextBall() { if(frame == 9) { // last frame switch (roll) { case Roll.Ball1: // always has a second roll state = "StateBall2"; break; case Roll.Ball2: // bonus roll if we got a spare or strike if(player.IsSpare(frame) || player.IsStrike(frame)) { state = "StateBall3"; } else { state = "StateGameOver"; } break; case Roll.Ball3: state = "StateGameOver"; break; } // all other frames } else if(roll == Roll.Ball1 && !player.IsStrike(frame)) { state = "StateBall2"; } else { ++frame; state = "StateBall1"; } } |
・ゲームオーバー時のステート、StateGameOver の処理。
1 2 3 4 |
function StateGameOver() { Debug.Log("Final Score: " + player.GetScore(9)); state = "StateNewGame"; } |
————————————————————————————————
参考アセット
FSM のアセット
Playmaker (Huton Games)
————————————————————————————————
FSM に関連した参考サイト
CryEngine 3 Free SDK
Entity States を検索。
Blue Mars wiki
entity script を検索。
Second Life wiki
state を検索。