Unity勉強会用の資料として。
自動ドアの制御の3つのやり方、その2。
—————————————————
3つの衝突判定
・Collision Detection [detecting]:コライダーがぶつかっているかどうかで判定。
・Ray Casting [drawing]:コライダーがぶつかる前に、ベクターで対象を検知。
・Trigger Collision Detection [detecting]:コライダーの領域内にもう1つのコライダーが重なっているかどうかで判定。
—————————————————
Approach 2 : Ray casting

デモはこちら。
コライダーがぶつかる前に、ベクターで対象を検知。
特徴:一定の距離に近づき、ドアの方向を向くと、ドアが開く。
欠点:ドアの方向を向かないと開かない。
—————————————————
スクリプト ‘DoorManager’ をドアに適用。
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 28 29 30 31 32 33 34 35 36 37 38 39 40
|
using UnityEngine; using System.Collections; public class DoorManager : MonoBehaviour { bool doorIsOpen = false; float doorTimer = 0.0f; public float doorOpenTime = 3.0f; public AudioClip doorOpenSound; public AudioClip doorShutSound; // Use this for initialization. void Start () { doorTimer = 0.0f; // タイマーをリセット. } // Update is called once per frame. void Update () { if(doorIsOpen){ // ドアが開いていた場合. doorTimer += Time.deltaTime; // タイマーを更新. if(doorTimer > doorOpenTime){ // 規定値に到達した場合. Door (doorShutSound, false, "doorshut"); // ドアを閉める. doorTimer = 0.0f; // タイマーをリセット. } } } void DoorCheck(){ if(!doorIsOpen){ // ドアが閉まっている場合. Door (doorOpenSound, true, "dooropen"); // ドアを開く. } } void Door(AudioClip aClip, bool openCheck, string animName){ // ここでは閉める場合にしか使用せず. audio.PlayOneShot(aClip); doorIsOpen = openCheck; transform.parent.gameObject.animation.Play(animName); } } |
—————————————————
スクリプト ‘PlayerCollisions’ をプレイヤーに適用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
using UnityEngine; using System.Collections; public class PlayerCollisions : MonoBehaviour { GameObject currentDoor; // Update is called once per frame. void Update () { RaycastHit hit; // ray の情報を格納するための RaycastHit 型の変数 'hit' を用意. if(Physics.Raycast(transform.position, transform.forward, out hit, 3)){ // Raycast(プレイヤーの位置、ray の方向、ray の情報を格納する変数、ray の長さ). // 'out' を用いて 'hit' への参照を引数として渡す. if(hit.collider.gameObject.tag == "playerDoor"){ // 衝突対象のタグ名が 'playerDoor' だった場合. currentDoor = hit.collider.gameObject; // 衝突対象の GameObject を変数に格納. currentDoor.SendMessage("DoorCheck"); // 衝突対象の GameObject の関数 'DoorCheck' を呼び出す. } } } } |
C# の ‘out’ パラメータについて。
http://msdn.microsoft.com/en-us/library/ee332485.aspx
・参照渡し ⇔ 値渡し
Unity勉強会用の資料として。
自動ドアの制御の3つのやり方、その1。
—————————————————
3つの衝突判定
・Collision Detection [detecting]:コライダーがぶつかっているかどうかで判定。
・Ray Casting [drawing]:コライダーがぶつかる前に、ベクターで対象を検知。
・Trigger Collision Detection [detecting]:コライダーの領域内にもう1つのコライダーが重なっているかどうかで判定。
—————————————————
Approach 1 : Collision detection

デモはこちら。
コライダーがぶつかっているかどうかで判定。
特徴:ドアに接触すると、ドアが開く。
欠点:ぶつからないとドアが開かない。
—————————————————
スクリプト ‘PlayerCollisions’ をプレイヤーに適用する。
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
|
using UnityEngine; using System.Collections; public class PlayerCollisions : MonoBehaviour { bool doorIsOpen = false; float doorTimer = 0.0f; GameObject currentDoor; public float doorOpenTime = 3.0f; public AudioClip doorOpenSound; public AudioClip doorShutSound; // Update is called once per frame. void Update () { // ドアが開いているかどうかを監視. if(doorIsOpen){ // ドアが開いている場合. doorTimer += Time.deltaTime; // フレーム間に経過している時間でタイマーをカウント. if(doorTimer > doorOpenTime){ // 一定時間を過ぎた場合. //ShutDoor(currentDoor); Door (doorShutSound, false, "doorshut", currentDoor); // ドアを閉める. doorTimer = 0.0f; // タイマーをリセットする. } } } void OnControllerColliderHit(ControllerColliderHit hit){ // 衝突を監視. if(hit.gameObject.tag == "playerDoor" && doorIsOpen == false){ // 衝突対象のタグ名が'playerDoor'で、かつドアが開いている場合. currentDoor = hit.gameObject; // ドアの GameObject の情報を変数に格納. //OpenDoor(currentDoor); Door (doorOpenSound, true, "dooropen", currentDoor); // Door (サウンドの指定、ドアが開いているか、再生するアニメーションの名前、衝突したドアの情報). } } /*void OpenDoor(GameObject door){ doorIsOpen = true; door.audio.PlayOneShot(doorOpenSound); door.transform.parent.animation.play("dooropen"); } void ShutDoor(GameObject door){ doorIsOpen = false; door.audio.PlayOneShot(doorShutSound); door.transform.parent.animation.play("doorshut"); }*/ void Door(AudioClip aClip, bool openCheck, string animName, GameObject thisDoor){ thisDoor.audio.PlayOneShot(aClip); // audio source を参照し、一度だけ音を鳴らす. doorIsOpen = openCheck; // ドアの状態を更新. thisDoor.transform.parent.animation.Play(animName); // 親の outPost に設定されているアニメーションを再生. } } |
ゲーム GUI
作ったもの
・ボウリング・ゲームへのスコアボード/メニュー画面/ポーズ機能の実装(エスケープキーで、一時停止できます)。
クリックすると、別ウィンドウが開きます。(音が出ます。)

http://shakeweb.sakura.ne.jp/demo/LU4_chap9/
————————————————————————————————
スコアボードの実装
・シーンにスコアボード用の空の GameObject を用意して、スクリプトを適用する。
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
|
#pragma strict var style:GUIStyle; // customize the appearance. function OnGUI () { //GUI.Label(Rect(5,100,200,20), "This is a label"); for(var f:int = 0; f < 10; f++) { var score:String = ""; var roll1:int = FuguBowl.player.scores[f].ball1; var roll2:int = FuguBowl.player.scores[f].ball2; var roll3:int = FuguBowl.player.scores[f].ball3; switch(roll1) { case -1: score += " "; break; case 10: score += "X"; break; default: score += roll1; } score += " | "; if(FuguBowl.player.IsSpare(f)) { score += "/"; } else { switch(roll2) { case -1: score += " "; break; case 10: score += "X"; break; default: score += roll2; } } if(f == 9) { score += " | "; if(10 == roll2 + roll3) { score += "/"; } else { switch(roll3) { case -1: score += " "; break; case 10: score += "X"; break; default: score += roll3; } } } GUI.Label(Rect(f * 30 + 5, 5, 50, 20), score, style); // フレームごとにラベルを用意する. var total:int = FuguBowl.player.GetScore(f); if(total != -1) { GUI.Label(Rect(f * 30 + 5, 20, 50, 20), " " + total, style); // フレームごとにラベルを用意する. } } } |
・GUI クラスのラベルにテキストで表示。
————————————————————————————————
メニュー画面/一時停止機能の実装
・一時停止機能が付いていないと、アップルの審査に落とされることがある。
・メニュー画面/一時停止機能用の空の GameObject を用意して、スクリプトを適用する。
|
enum Page { None, Main, Options, Credits } private var currentPage:Page; |
・FSM と同じように、それぞれのページのステートを用意する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
private var savedTimeScale:float; // Time.timeScale before we pause function PauseGame() { savedTimeScale = Time.timeScale; // save normal time scale Time.timeScale = 0; // suspend time AudioListener.pause = true; // suspend music currentPage = Page.Main; // start with the main menu page } function UnPauseGame() { Time.timeScale = savedTimeScale; AudioListener.pause = false; currentPage = Page.None; } static function IsGamePaused() { return Time.timeScale == 0; } |
・Time.timeScale をゼロにすると、ゲーム中の動きが止まる。反対に、保存しておいた元の timeScale に戻すと、動き出す。同様に、timeScale の値を見れば、停止しているかどうか判定できる。
・ゲーム開始時に、ゲームを一時停止させ、メニュー画面を表示させる。
|
var startPaused:boolean = true; // bring up the menu at game start function Start() { if(startPaused) { PauseGame(); } } |
・escape キーで、’一時停止’/’停止解除’/’サブメニュー画面からのメインメニュー画面への遷移’を制御できるように、Update() 関数でキーボード入力を監視する。
・一時停止中は Time.deltaTime の値がゼロになり、ゼロで割るエラーを引き起こしてしまうので、ボールの動きを制御するスクリプトを修正する。
|
function Update() { forcex = 0; forcey = 0; if(Time.deltaTime > 0) { // if it's not paused CalcForce(); } } function CalcForce() { var deltaTime:float = Time.deltaTime; forcex = mousepowerx * Input.GetAxis("Mouse X") / deltaTime; forcey = mousepowery * Input.GetAxis("Mouse Y") / deltaTime; } |
・メニュー画面の表示を OnGUI() 関数で制御。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
var hudColor:Color = Color.white; var skin:GUISkin; function OnGUI() { if(IsGamePaused()) { if(skin != null) { GUI.skin = skin; } else { GUI.color = hudColor; } switch (currentPage) { case Page.Main: ShowPauseMenu(); break; case Page.Options: ShowOptions(); break; case Page.Credits: ShowCredits(); break; } } } |
・OnGUI() コールバックは、1フレーム中に複数回呼び出されるので、一時停止中かどうか、常に監視される。
・GUILayout クラスの BeginArea() 関数で GUI の表示領域の指定を開始し、EndArea() 関数で終了する。
|
var menutop:int = 25; function BeginPage(width:int, height:int) { GUILayout.BeginArea(Rect((Screen.width - width) / 2, menutop, width, height)); } function EndPage() { if(currentPage != Page.Main && GUILayout.Button("Back")) { currentPage = Page.Main; } GUILayout.EndArea(); } |
・GUILayout.Button(“Back”) は、これ自体でボタンを表示させ、ボタンが押されたら、戻り値が true になる。尚、この関数は、一時停止中は OnGUI() 関数経由で常に監視され続ける。
・メインメニュー表示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
function ShowPauseMenu() { BeginPage(150,300); if(GUILayout.Button("Play")) { UnPauseGame(); } if(GUILayout.Button("Options")) { currentPage = Page.Options; } if(GUILayout.Button("Credits")) { currentPage = Page.Credits; } #if !UNITY_WEBPLAYER && !UNITY_EDITOR if(GUILayout.Button("")) { Application.Quit(); } #endif EndPage(); } |
・一時停止中は、OnGUI() 関数経由で常に監視され続ける。ボタンが押されると、OnGUI() の switch 文で別ページに遷移する。
・クレジット・ページ。
|
var credits:String[] = [ "A Fugu Games Production", "Copyright (c) 2012 Technicat, LLC. All Rights Reserved.", "More information at http://fugugames.com/" ]; function ShowCredits() { BeginPage(300,200); for(var credit in credits) { GUILayout.Label(credit); } EndPage(); } |
・オプション・ページ。
|
private var toolbarIndex:int = 0; // current toolbar selection private var toolbarStrings: String[] = ["Audio", "Graphics", "System"]; // tabs function ShowOptions() { BeginPage(300,300); toolbarIndex = GUILayout.Toolbar (toolbarIndex, toolbarStrings); switch(toolbarIndex) { case 0: ShowAudio(); break; case 1: ShowGraphics(); break; case 2: ShowSystem(); break; } EndPage(); } |
・GUILayout.Toolbar に渡す toolbarStrings で、ボタンに表示する名前を指定。toolbarIndex で選択されているボタンを指定。また、この関数は選択されているボタンのインデックスを int 型で返す。ShowOptions() 関数は、OnGUI() 経由で常に監視されているので、ボタンが選択されると、すぐさま、インデックスの値が変数 toobaIndex に代入され、Toolbar() に渡される値に反映されて、表示が切り替わる。
・オーディオ・パネル。
|
function ShowAudio() { GUILayout.Label("Volume"); AudioListener.volume = GUILayout.HorizontalSlider(AudioListener.volume, 0.0, 1.0); } |
・グラフィクス・パネル
|
function ShowGraphics() { GUILayout.Label(QualitySettings.names[QualitySettings.GetQualityLevel()]); GUILayout.Label("Pixel Light Count: " + QualitySettings.pixelLightCount); GUILayout.Label("Shadow Cascades: " + QualitySettings.shadowCascades); GUILayout.Label("Shadow Distance: " + QualitySettings.shadowDistance); GUILayout.Label("Soft Vegetation: " + QualitySettings.softVegetation); GUILayout.BeginHorizontal(); if(GUILayout.Button("Decrease")) { QualitySettings.DecreaseLevel(); } if(GUILayout.Button("Increase")) { QualitySettings.IncreaseLevel(); } GUILayout.EndHorizontal(); } |
・QualitySettings.GetQualityLevel() が int 型で値を返すので、それをインデックスとして配列 name から表示する名前を引っ張ってくる。
・システム・パネル。
|
function ShowSystem() { GUILayout.Label("Graphics: " + SystemInfo.graphicsDeviceName + " " + SystemInfo.graphicsMemorySize + "MB\n" + SystemInfo.graphicsDeviceVersion + "\n" + SystemInfo.graphicsDeviceVendor); // GUILayout.Label("Shadows: " + Available(SystemInfo.supportsShadows)); // GUILayout.Label("Image Effects: " + Available(SystemInfo.supportsImageEffects)); // GUILayout.Label("Render Textures: " + Available(SystemInfo.supportsRenderTextures)); GUILayout.Label("Shadows: " + SystemInfo.supportsShadows); GUILayout.Label("Image Effects: " + SystemInfo.supportsImageEffects); GUILayout.Label("Render Textures: " + SystemInfo.supportsRenderTextures); } |
・Available() がエラーになるため、使用を断念。Unity Pro なら使えるのだろうか。
————————————————————————————————
アセットストア(GUI 関連)
EZGUI (Above and Beyond Software)
NGUI (Tasharen Entertainment)
レッツ・プレイ! ゲーム・スクリプティング
作ったもの
・ボウリング・ゲームへの、ゲーム・ロジックの実装。
クリックすると、別ウィンドウが開きます。(音が出ます。)

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 クラスのラッパーのようなもの。
・スペア/ストライク時のスコア更新用関数を用意。
|
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投目のスコア確定用関数。
|
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投目のスコア確定用関数。
|
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投目のスコア確定用関数。
|
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; } |
・総合得点取得用関数。
|
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 : 自分自身を呼ぶ関数。
————————————————————————————————
・ピンが倒れているかどうかを判別するスクリプトをそれぞれのピンに適用する。
|
#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 をインスタンス化。
|
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; } |
・倒れたピンを非表示にする。
|
function RemoveDownedPins() { for(var pin:GameObject int pinBodies) { if(pin.GetComponent(FuguPinStatus).IsKnockedOver()) { pin.SetActive(false); } } } |
・SetActive(false) で、GameObject を非アクティブにできる。
・ResetPins 関数に非アクティブにしたピンを再度、アクティブにする処理を追加。
|
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
・現在のフレーム数と何投目かを入れる変数を用意。
|
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 を実装する。
・それぞれのステートに合わせた処理を、コルーチンとして呼び出す。
|
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 の処理。
|
function StateNewGame() { player.ClearScore(); frame = 0; state = "StateBall1"; } |
・1投目のステート、StateBall1 で投球前の初期化処理。
|
function StateBall1() { ResetEverything(); // reset pins, camera and ball roll = Roll.Ball1; state = "StateRolling"; } |
・2投目のステート、StateBall2 で投球前の初期化処理。
|
function StateBall2() { ResetBall(); ResetCamera(); if(GetPinsDown() == 10) { ResetPins(); } else { RemoveDownedPins(); } roll = Roll.Ball2; state = "StateRolling"; } |
・3投目のステート、StateBall3 で投球前の初期化処理。
|
function StateBall3() { ResetBall(); ResetCamera(); if(GetPinsDown() == 10) { ResetPins(); } else { RemoveDownedPins(); } roll = Roll.Ball3; state = "StateRolling"; } |
・投球後のステート、StateRolling の処理。
|
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 の処理。
|
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 にする。
|
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 の処理。
|
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 の処理。
|
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 の処理。
|
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 を検索。
レッツ・ボウル! 物理学応用編
作ったもの
・ドラム缶を倒す、ボウリング・ゲーム。
・マウスカーソルを移動させると、ボールがその方向へ転がります。
クリックすると、別ウィンドウが開きます。(音が出ます。)

http://shakeweb.sakura.ne.jp/demo/LU4_chap7/
————————————————————————————————
床を拡張する
・Transform コンポーネントの Local Scale でサイズを変更するのは望ましくない。サイズを変えたい場合は、Import Settings ですること。どうしても Transform で変える場合は、x、y、z、それぞれ均等に値を変更する。
————————————————————————————————
ゲーム・コントローラー
・ピンの prefab を作成し、スクリプトで配置する。1列目に1本、2列目に2本、3列目に3本、4列目に4本の計10本。
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
|
#pragma strict #pragma downcast var pin:GameObject; // pin prefab to instantiate var pinPos:Vector3 = Vector3(0,1,20); // position to place rack of pins var pinDistance = 1.5; // initial distance between pins var pinRows = 4; // number of pin rows var ball:GameObject; // the bowling ball var sunkHeight:float = -10.0; // global y position which we call a gutterball private var pins:Array; function Awake () { CreatePins(); } 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) { pins.push(Instantiate(pin, pinPos + offset, Quaternion.identity)); offset.x += pinDistance; } } } function Update () { if (ball.transform.position.y < sunkHeight) { ResetEverything(); } } function ResetBall() { ball.SendMessage("ResetPosition"); } function ResetPins() { for (var pin:GameObject in pins) { pin.BroadcastMessage("ResetPosition"); } } function ResetCamera () { Camera.main.SendMessage("ResetPosition"); } function ResetEverything () { ResetBall(); ResetPins(); ResetCamera(); } |
・rotation は、Euler angle(x,y,z)ではなく、Unity の内部処理で使われている Quaternion(四元数、w,x,y,z)を使用。
Transform.rotation、Transform.rocalRotation : Quaternion
Transform.eulerAngles、Transform.localEulerAngles : Euler angles
・Instantiate は、Object クラスの静的関数。Instantiate = this.Instantiate。しかし、Object.Instantiate と呼ぶことはできない(Mono でも Object クラスが定義付けられているため。JavaScript のみ)。代わりに UnityEngine.Object.Instantiate を使用する。
System.Object : Mono(こちらが先に参照される)
UnityEngine.Object.Instantiate : Unity
・Awake は、スクリプトが有効になっているかどうかに拘らず、Start より先に、1度だけ呼ばれる。(初期化に適している。)
・空の GameObject をシーンに配置し、そこにコントローラー・スクリプトを適用する。
・ピンが倒れるように Rigidbody コンポーネントと、PhysicMaterial を適用する。
————————————————————————————————
ボールが床から落下した時の処理
・リセット用のスクリプトを、ボール、メインカメラ、ピンの prefab に適用する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
#pragma strict private var startPos:Vector3; private var startRot:Vector3; function Awake () { // save the initial position and rotation of this GameObject startPos = transform.localPosition; startRot = transform.localEulerAngles; } function ResetPosition () { // set back to initial position transform.localPosition = startPos; transform.localEulerAngles = startRot; // make sure we stop all physics movement if (rigidbody != null) { rigidbody.velocity = Vector3.zero; rigidbody.angularVelocity = Vector3.zero; } } |
————————————————————————————————
ゲーム・コントローラー(続き)
・戻り値を必要としない場合は、SendMessage で関数を実行できる。
・BroadcastMessage は、子の GameObject にも送信できる。
・#pragma strict だけだと警告がでるので、#pragma downcast を追加する。Array が Object で定義されているにも拘らず、要素が GameObject として扱われているため。Object のサブクラス GameObject に downcast してやる。
・インポートした Barrel アセットの子 GameObject に、Capsule Collider と Box Collider コンポーネントを適用し Barrel の形に沿った Collider を用意する。
・GameObject | Apply Changes To Prefab で変更を prefab に反映させる。
————————————————————————————————
ボールが転がる音の制御
・ボールが転がる効果音の AudioSource を Ball に適用し、スクリプトで再生タイミングを制御する。メインカメラに AudioListener。
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 28
|
#pragma strict var minSpeed:float = 1.0; private var sqrMinSpeed:float = 0; function Awake() { sqrMinSpeed = minSpeed * minSpeed; } function OnCollisionStay( collider:Collision) { if(rigidbody.velocity.sqrMagnitude > sqrMinSpeed) { if(!audio.isPlaying) { audio.Play(); } } else { if(audio.isPlaying) { audio.Stop(); } } } function OnCollisionExit(collider:Collision){ if(collider.gameObject.tag == "Floor") { if(audio.isPlaying) { audio.Stop(); } } } |
————————————————————————————————
ピンがぶつかる時の音の制御する
|
#pragma strict var minSpeed = 0.01; // actually the square of the minSpeed function OnCollisionEnter (collider:Collision) { if(collider.relativeVelocity.sqrMagnitude > minSpeed) { if(collider.gameObject.tag != "Pin") { audio.Play(); // hit anything besides another pin, play the sound } else { // otherwise pin with lower ID gets to play if(gameObject.GetInstanceID() < collider.gameObject.GetInstanceID()) { audio.Play(); } } } } |
・ピンとそれにぶつかるもの(ボール、他のピン)は動いているので、velocity の代わりに relativeVelocity を使用する。
————————————————————————————————
使用アセット
Barrel (Universal Image)
Free SFX Package (Bleep Blop Audio)
————————————————————————————————
関連リンク
3Dモデル・マーケットプレイス
http://Turbosquid.com
フリー・オーディオ素材
http://freesound.org