Android
Unity
アプリ内課金
ゲーム制作

Unity でブロック崩しを作る際に使用する独自コンポーネント

この記事では、Unity でブロック崩しを作る際に使用する独自コンポーネントについて紹介します。

itemstore BLOG で連載している
【いますぐ始めるアプリ内課金】第4回:Unityで開発を進める
の補足になりますので、併せてお読みください。

上記のブログ記事にて、教材となるプロジェクトファイルのダウンロードURLを案内していますので、興味のある方はダウンロードしてみてください。

Unity でブロック崩しを作る際に使用する標準コンポーネントについては、以下の記事にて紹介しています。

Unity でブロック崩しを作る際に使用する標準コンポーネント

ブロック崩しで使用する独自コンポーネントの紹介

ブロック崩しのボールやバーは独自のスクリプト(C#)を作成し、各ゲームオブジェクトにコンポーネントとして付加しています。
それぞれのスクリプトは教材プロジェクトに含まれていますので、詳しくはダウンロード後にソースコードを参考にしてください。

ここでは、ブロック崩しに必要なスクリプトの特徴的な部分を中心に実装コードを紹介します。

Ball.cs

ボールを制御するコンポーネントです。
同じく付加されている Rigidbody2D コンポーネントを操作して移動を調節しています。

プロパティの説明

09-2-Ball.csのinspector

SpeedPerSec

ボールの速度を設定します。
値が大きいほどボールは速く移動します。
これを変更することで難易度に応じて速度を変えたりできます。

スクリプトの説明

一定速度で動かすための調節

ステージ開始直後とプレイ終了後は移動しないように flgPause で制御しています。
ボールは一定速度で常に動くように、 FixedUpdate が呼ばれるたびに Rigidbody2D.velocity の加速度を調節しています。
加速度から取得した単位ベクトルに一定の移動量をかけることで一定速度で動くように調節しています。

Ball.cs
    private void FixedUpdate()
    {
        // ポーズ中は何もしません。
        if (this.flgPause == true)
        {
            return;
        }

        // 移動する方向のベクトルを正規化します。
        Vector2 velocityNormalized = rb.velocity.normalized;

        //...

        // 調整された移動方向に、等速で移動するように加速度を設定します。
        rb.velocity = velocityNormalized.normalized * speedPerSec * Time.fixedDeltaTime;
    }

水平になるとゲームが続行できなくなるので、一定の角度を保つように移動方向を監視・調節しています。

Bar.cs

プレイヤーの入力に応じて横に移動するバーを制御するスクリプトです。
ボールと同じく Rigidbody2D を操作します。

プロパティの説明

10-2-Bar.csのinspector

SeHit

ボールがバーにぶつかったときに再生する効果音です。
画像と同じく Project タブの Assets フォルダ以下に置いた音声ファイルを選択できます。

LimitMovePerSec

バーの移動速度の上限です。
タッチした場所に瞬間移動するのではなく、この移動速度を守りながら横に移動します。
この値が大きくなるほど、バーは高速に移動します。

スクリプトの説明

入力の検知と物理挙動のイベント

入力は Update イベント関数内で取得しています。
マウスのクリックやシングルタップならば Input.GetMouseButton で検知することができます。
入力を受けたらすぐにバーを移動したいかもしれませんが、 Rigidbody2D の操作は FixedUpdate イベント関数の中で行います。
そのため、次の FixedUpdate で参照できるように、入力情報を一度変数に保存しておきます。

Bar.cs
    void Update () {

        // タッチされている / マウス左ボタンが押されている
        if (Input.GetMouseButton(0))
        {
            // タッチ/マウス左ボタンダウンしている位置をワールド座標に変換
            this.wPositionLastTouch = Camera.main.ScreenToWorldPoint(Input.mousePosition);
            // 移動を指示するフラグをセットする。
            this.flgMove = true;
        }
    }

    private void FixedUpdate()
    {
        // ポーズ中は移動しない。
        if (flgPause == true)
        {
            return;
        }

        // 一度加速度を 0 にする。
        rb.velocity = Vector2.zero;

        // 移動の入力があった場合
        if (this.flgMove == true)
        {
            // 移動する方向は、タッチしている方向にします。
            // 移動量の上限で丸めて、上限の移動速度を越えないようにします。
            Vector2 vecMove = new Vector2(
                Mathf.Clamp(this.wPositionLastTouch.x - this.transform.position.x, -this.limitMovePerSec, +this.limitMovePerSec),
                0f);
            // 現在の位置に移動量を足して、移動先を決めます。
            Vector2 vecPosition = new Vector2(vecMove.x + this.transform.position.x, vecMove.y + this.transform.position.y);

            // 当たり判定を行いながら指定した量だけ移動します
            rb.MovePosition(vecPosition);

            // 直前の Update の指示どおり移動したのでフラグを元に戻します
            this.flgMove = false;
        }
    }

衝突位置によるボールの移動方向の変更

ボールが衝突してきたイベントでは、衝突位置によってボールの移動方向を変更しています。
バーの中央に近いほど真上、端っこでは斜めの方向に跳ね返ります。

Collider2D が衝突を検知すると OnCollisionEnter2D などのイベント関数が呼び出されます。
その関数の引数 collision の contacts には衝突位置が保存されているので、その位置をもとにボールの移動方向を変えています。

Bar.cs
    private void OnCollisionEnter2D(Collision2D collision)
    {
        if (collision.gameObject.layer == LayerMask.NameToLayer("Ball"))
        {

            // 接触した x 座標が、バーの中心を基準にどれほど離れているか距離を求めます。
            // その距離をバーの横幅で割り 0 ~ 1.0 の比率にします。
            // その比率に 45f をかけ、 90 度を基準として 90 + 45 ~ 90 - 45 度を設定します。
            // 最初に - しているのは、左側が 90 + 45 というように + にさせるためです。座標は右が + だから反転させます。
            // bounds.size.x / 2 は中心からの距離に対しての割り算なので半分にする必要があります。
            float deg = - (collision.contacts[0].point.x - col.bounds.center.x) / (col.bounds.size.x /2) * 45f + 90f;

            // ボールの移動する方向を変更します。
            Ball compBall = collision.gameObject.GetComponent<Ball>();
            compBall.ChangeDirection(deg);

            // 効果音を再生します。
            Util.PlayAudioClip(this.seHit, Camera.main.transform.position, 1.0f);
        }
    }

Block.cs

ブロックの HP を管理し、ボールとの衝突イベントを処理します。

プロパティの説明

11-2-Block.csのinspector

SeHit

ボールがぶつかったときに再生する効果音を設定します。

SeDestroy

ボールがぶつかったときの効果音ですが、 HP が 0 になって破壊されたときにだけ再生されます。

HpMax

ブロックの HP の初期値です。 HP は inspector 上で設定された HpMax の値で初期化されます。

Hp

ブロックの耐久値です。初期化で HpMax の値が設定され、ボールがぶつかるたびに 1 減ります。 0 になるとブロック自身が消滅します。

スクリプトの説明

ブロックの HP の管理

HP の管理をして HP が 0 になったら消滅します。
Start イベント関数は最初の Update イベント関数の前に呼び出されるので、ここで Hp を初期化しています。
消滅する際は、GameObject.Destroy 関数を使っています。

Block.cs
    void Start () {
        // ブロックの耐久力を初期化します。
        this.hp = hpMax;
    }

    private void OnCollisionEnter2D(Collision2D collision)
    {
        // 衝突したゲームオブジェクトがボールの場合
        if (collision.gameObject.layer == LayerMask.NameToLayer("Ball"))
        {
            // 耐久力を減らします。
            this.hp -= 1;

            // 壊れる場合
            if (this.hp <= 0)
            {
                // 壊されたときの効果音を再生
                Util.PlayAudioClip(this.seDestroy, Camera.main.transform.position, 1.0f);
                // このゲームオブジェクトを破棄します。
                GameObject.Destroy(this.gameObject);
            }
            // まだ壊れない場合
            else
            {
                // 跳ね返す効果音を再生
                Util.PlayAudioClip(this.seHit, Camera.main.transform.position, 1.0f);
            }
        }
    }

CameraStableAspect.cs

指定したピクセル幅の画面になるようにカメラを制御します。

プロパティの説明

12-2-CameraStableAspect.csのinspector

Width

画面のピクセル単位の横幅です。

Height

画面のピクセル単位の縦幅です。

PixelPerUnit

ゲーム内で表示する画像の Pixel Per Unit を設定します。

スクリプトの説明

プロパティの変更の監視

設定サイズが変更された場合にカメラを再調整しています。
調整したときのプロパティをメンバ変数で覚えておき、現在のプロパティと差がないかを確認しています。

CameraStableAspect.cs
    private void Update()
    {
        // 設定サイズの変更または画面サイズの変更があれば調整する。
        if (lastScreenWidth != Screen.width || lastScreenHeight != Screen.height || lastWidth != width || lastHeight != height)
        {
            Adjust();
        }
    }

Adjust 関数では、カメラの調整を行っていますが、Unity2Dで画面のアスペクト比を固定にしたい【Unity】 - Qiitaで公開されているスクリプトを使っていますので、詳しくはそちらの Qiita 記事をご覧ください。

GameoverArea.cs

ボールが接触したら StageCommonScene.cs にゲームオーバーになったことを伝えます。

プロパティの説明

13-2-GameoverArea.csのinspector

StageCommon

ゲームオーバーになったことを伝えるゲームオブジェクトを設定します。
このゲームオブジェクトは StageCommonScene コンポーネントを持っている必要があります。
設定済みなので特に変える必要はありません。

スクリプトの説明

接触イベント

ボールと接触したら、設定されている StageCommonScene のゲームオーバー用の関数を呼び出します。
OnTriggerEnter2D は、Collider2D が接触を検知したときに呼び出されるイベント関数です。
Collider2D の Is Trigger をチェックしておく必要があります。
接触は検知しますが、衝突の物理挙動は発生しません。

GameoverArea.cs
    private void OnTriggerEnter2D(Collider2D collision)
    {
        // Ball と接触したらゲームオーバー処理を呼び出します。
        if (collision.gameObject.layer == LayerMask.NameToLayer("Ball"))
        {
            stageCommon.OnGameover();
        }
    }

StageButton.cs

ステージ選択ボタンを制御するコンポーネントです。
ゲームの状態を保存する Unity の標準機能 PlayerPrefs でステージの状態にアクセスして、ボタンの見た目や機能を変化させます。

プロパティの説明

14-2-StageButton.csのinspector

SeButton

ボタンが押されたときの効果音です。

FlgFirstStage

最初のステージであることを指定するためのフラグです。
この値が true ならば、ステージボタンの状態が Locked(選択不可)の場合も選択可能になります。

NameStage

このステージ選択ボタンが押されたときにロードするステージのシーン名です。
ステージのシーン名でクリア状況は管理されています。

ColorOnClear

クリア済みのボタンの色を指定します。

ImageLocked

ステージが「選択不可」の場合にボタンの上に表示する画像を指定します。
画像はボタンの上に重なるように配置してください。

スクリプトの説明

ステージの読み込み

ステージ選択ボタンが押されると、設定されているステージ固有シーンを読み込みます。
その後、バーやボールの配置されたステージ共通シーンを追加で読み込むことで、ステージ画面を作っています。
ステージを最初からやり直すために「RETRY」ボタンを押した際も同様の処理が行われています。

StageButton.cs
    public void OnButtonStage()
    {
        // 効果音を再生
        Util.PlayAudioClip(this.seButton, Camera.main.transform.position, 1.0f);
        // シーンを読み込みます。
        // 最初に Stage1, Stage2, Stage3 のようにブロックを配置したステージ固有のシーンを読み込みます。
        UnityEngine.SceneManagement.SceneManager.LoadScene(this.nameStage);
        // その後、バー、ボール、壁などの全てのステージで共通のシーンを追加で読み込みます。
        UnityEngine.SceneManagement.SceneManager.LoadScene("StageCommon", UnityEngine.SceneManagement.LoadSceneMode.Additive);
    }

StageCommonConfig.cs

ステージ共通の処理(クリア判定など)で必要になるステージ固有のデータを設定します。
新しいステージシーンを作成するときは、これをもつ StageCommonConfig プレハブをシーンに配置して設定してください。

プロパティの説明

15-2-StageCommonConfig.csのinspector

Blocks

ブロックをまとめているゲームオブジェクトを設定します。
ステージ共通処理では、このゲームオブジェクトの下に配置されているブロックが 0 個になったかどうかを監視しています。
これはステージをクリアするための判定に使います。

NameNextStage

次のステージのシーン名を設定します。
ステージ共通処理では、ステージをクリアしたときに表示される Next ボタンが押されると、そのシーンをロードして次のステージを作ります。
また、次のステージがロックされている場合は、次のステージの状態を「選択不可」から「選択可能」に書き換えてプレイできるようにします。

スクリプトの説明

追加でロードされたシーンに情報を伝える

ステージ画面は、ブロックが配置されているステージ固有シーンとボールやバーがあるステージ共通シーンを組み合わせています。
ステージ固有シーンに配置されている StageCommonConfig コンポーネントは、ステージ共通シーンが追加で読み込まれたタイミングで情報を伝えます。

Awake イベント関数でシーンが読み込まれた際に OnLoadedSceneForContinue を呼び出すように設定します。
あとは、その関数内で読み込まれたステージ共通シーンの特定のゲームオブジェクトを探し、情報を設定します。

StageCommonConfig.cs
    private void Awake()
    {
        UnityEngine.SceneManagement.SceneManager.sceneLoaded += OnLoadedSceneForContinue;
    }

    private void OnDestroy()
    {
        UnityEngine.SceneManagement.SceneManager.sceneLoaded -= OnLoadedSceneForContinue;
    }

    /// <summary>
    /// ステージ共通シーン StageCommon に配置されている StageCommonScene コンポーネントに、このステージ固有の情報を設定します。
    /// このイベントは、ステージ開始直後のほかに、コンティニューで StageCommon のみ再読み込みされたときも呼び出されます。
    /// </summary>
    /// <param name="scene"></param>
    /// <param name="mode"></param>
    public void OnLoadedSceneForContinue(UnityEngine.SceneManagement.Scene scene, UnityEngine.SceneManagement.LoadSceneMode mode)
    {
        // ロードされたシーンが StageCommon の場合
        if (scene.name == "StageCommon")
        {
            // StageCommon シーンに配置されている、特定のゲームオブジェクトを探し、その中のコンポーネントに共通処理で使う設定をコピーします。
            foreach (GameObject rootObj in scene.GetRootGameObjects())
            {
                // StageCommon という名前のゲームオブジェクトが持つ StageCommonScene コンポーネントに設定をコピーします。
                if (rootObj.name == "StageCommon")
                {
                    StageCommonScene coStageCommon = rootObj.GetComponent<StageCommonScene>();
                    if (coStageCommon != null)
                    {
                        coStageCommon.blocks = blocks;
                        coStageCommon.nameNextStage = nameNextStage;
                    }
                }
            }
        }
    }

StageCommonScene.cs

ステージで共通のブロック崩しの進行を行います。

  • タッチしたらプレイ開始
  • ブロックを全て破壊すればクリア
  • クリア時のダイアログの表示
  • クリア時のステージの状態の更新(今のステージをクリア済み、次のステージを選択可能に変化)
  • ボールが落ちたらゲームオーバー
  • ゲームオーバー時のダイアログの表示

プロパティの説明

16-2-StageCommonScene.csのinspector

Blocks

崩す対象のブロックを Hierarchy の下位に配置したゲームオブジェクトです。
このゲームオブジェクトの下位のゲームオブジェクト(ブロック)の個数が 0 個になるとステージクリアです。

ステージ固有シーンに配置された StageCommonConfig コンポーネント経由でこのシーンのロード時に設定されます。

NameNextStage

次のステージ固有のシーン名です。
例えば Stage1 の次が Stage2 の場合は、 Stage1 のこのフィールドに Stage2 を設定します。
最終ステージの場合は空文字列を設定してください。
空文字列の場合は次のステージへのボタン表示や「選択可能」へのステータス変更を行いません。

ステージ固有シーンに配置された StageCommonConfig コンポーネント経由でこのシーンのロード時に設定されます。

PanelOnStageComplete

ステージクリア時のパネルです。
表示・非表示を切り替えるために設定します。

PanelOnGameover

ゲームオーバー時のパネルです。
表示・非表示を切り替えるために設定します。

ButtonContinue

コンティニューボタンです。
課金アイテム(コンティニュー用)があればボタンを有効にします。
公開するアプリにはアプリ内課金が含まれていないので常にボタンは有効です。

UiTextKakinItemCount

課金アイテム(コンティニュー用)の残りの数を表示するテキストです。
公開するアプリにはアプリ内課金が含まれていないので存在しません。

FlgDebugContinueAlways

デバッグ用です。 true の場合は、課金アイテム(コンティニュー用)の個数が 0 でもコンティニューボタンを有効にします。

GoNextStage

ステージクリア時の「Next」ボタンです。
次のステージがない場合は表示しません。

CoBall

シーンに配置しているボールを設定しています。
プレイ開始直後に移動を開始させたり、プレイ終了後に移動を停止します。

CoBar

シーンに配置しているバーを設定しています。
プレイ開始直後に入力に応じて移動させたり、プレイ終了後に移動しないようにします。

PanelOnPause

ステージ開始直後のポーズ時のパネルです。
タッチされてポーズを解除した際に、非表示にします。

SeStageComplete

ステージクリア時の効果音です。

SeGameover

ゲームオーバー時の効果音です。

SeButton

ボタン押下時の効果音です。

スクリプトの説明

クリア処理

繰り返し呼び出される Update イベントで、ブロックの数を確認し、 0 になったらクリア処理を呼び出します。
クリア処理では、効果音を再生したりクリア時のダイアログを表示します。
他にもゲームの進行状況やステージの状態の更新を行います。

ステージの状態は PlayerPrefs という Unity の機能で管理しています。
PlayerPrefs では、任意の名前と値を複数設定できるので、各ステージの状態(選択不可、選択可能、クリア済み)を保存しています。
ステージ選択ボタンでは PlayerPrefs にアクセスして、対応するステージの状態に応じてボタンの見た目や有効性を変化させています。

StageCommonScene.cs
    void Update () {
        // 崩す対象のブロックがなくなったら、ステージクリアの処理を呼び出します。
        if (blocks.transform.childCount == 0)
        {
            OnStageComplete();
        }
    }

    public void OnStageComplete()
    {
        // すでにゲームをクリアしているかゲームオーバーならば何もしません。
        if ((int)this.phase > (int)StagePlayPhase.Playing)
        {
            return;
        }

        // フェーズをプレイ中からステージクリア時に変更します。
        this.phase = StagePlayPhase.StageComplete;

        // ボールを停止させます。
        coBall.ChangePauseFlag(true);
        // バーを停止させます。
        coBar.ChangePauseFlag(true);

        // 効果音を再生
        Util.PlayAudioClip(this.seStageComplete, Camera.main.transform.position, 1.0f);

        // 現在のステージの状態を「クリア済み」にします。
        string keyStageStatus = StageButton.prefixKeyStageStatus + UnityEngine.SceneManagement.SceneManager.GetActiveScene().name;
        PlayerPrefs.SetInt(keyStageStatus, (int)StageButton.StageStatus.Cleared);

        // 次のステージが設定されている場合
        if (string.IsNullOrEmpty(this.nameNextStage) == false)
        {
            // 次のステージのステータスが「選択不可」ならば「選択可能」にします。
            string keyNextStageStatus = StageButton.prefixKeyStageStatus + this.nameNextStage;
            if (PlayerPrefs.GetInt(keyNextStageStatus) == (int)StageButton.StageStatus.Locked)
            {
                PlayerPrefs.SetInt(keyNextStageStatus, (int)StageButton.StageStatus.Unlocked);
            }
        }
        else
        {
            // 次のステージがない場合は、[Next Stage]の UI は無効、非表示にします。
            this.goNextStage.SetActive(false);
        }

        // ステージクリア時のパネルを有効にします。
        panelOnStageComplete.SetActive(true);
    }

コンティニュー処理

コンティニューの場合、ブロックが配置されているステージ固有シーンは残しておき、ボールやバーが配置されているステージ共通シーンだけリセットします。
ステージ共通シーン(StageCommon)のリセットのために、一度アンロードをして、アンロード完了後に再びシーンをロードします。
アンロード完了のイベント SceneManager.sceneUnloaded に呼び出してほしい関数を追加することでアンロード完了時に関数が呼び出されます。

StageCommonScene.cs
    public void OnButtonContinue()
    {
        // 効果音を再生
        Util.PlayAudioClip(this.seButton, Camera.main.transform.position, 1.0f);

        // ステージ共通シーンをリセットします。
        // ここではステージ共通シーンを Unload し、それが完了したイベントで再びロードする処理を行います。 
        UnityEngine.SceneManagement.SceneManager.sceneUnloaded += OnUnloadedSceneForContinue;
        UnityEngine.SceneManagement.SceneManager.UnloadSceneAsync("StageCommon");

        // 課金アイテム(コンティニュー用)の数を1個減らす
        ItemstoreClient.instance.UseKakinItem();
    }

    public void OnUnloadedSceneForContinue(UnityEngine.SceneManagement.Scene scene)
    {
        // Unload されたシーンの名前が StageCommon の場合
        if (scene.name == "StageCommon")
        {
            // さきほど登録した SceneUnload のイベントを登録解除します。
            UnityEngine.SceneManagement.SceneManager.sceneUnloaded -= OnUnloadedSceneForContinue;
            // 追加で StageCommon を再びロードします。
            UnityEngine.SceneManagement.SceneManager.LoadScene("StageCommon", UnityEngine.SceneManagement.LoadSceneMode.Additive);
        }
    }

TitleScene.cs

タイトル画面の制御を行います。
タイトル画面に配置されているステージ選択ボタン以外のボタンのイベント処理はここに記述されています。

プロパティの説明

17-2-TitleScene.csのinspector

SeButton

ボタン押下時の効果音です。

UiTextKakinItem

課金アイテム(コンティニュー用)の数を表示するテキストを設定しています。
公開するアプリにはアプリ内課金が含まれていないので存在しません。

UiPanelPrivacyPolicy

プライバシーポリシーを表示するパネルを設定しています。
公開するアプリにはアプリ内課金が含まれていないので存在しません。

UrlCM

宣伝用の URL です。
この企画の連載記事カテゴリの URL を設定しています。
公開するアプリのタイトル画面にある「 itemstore BLOG いますぐ始めるアプリ内課金」ボタンでのみ使っています。

スクリプトの説明

ボタンが押されたときのイベント用の関数の作成

標準コンポーネントのボタンの説明でも触れましたが、 Button コンポーネントには OnClick というイベントが設定できます。
そこに任意のゲームオブジェクトの任意の関数を設定することができます。
ボタンのクリック時に呼び出される関数は次のように public にして外部からアクセスできるようにしておきます。

TitleScene.cs
    public void OnButtonSiteUrlButton()
    {
        // 効果音を再生
        Util.PlayAudioClip(this.seButton, Camera.main.transform.position, 1.0f);
        // 連載記事のサイトを開きます。
        Util.OpenURL(this.urlCM);
    }

このイベント用の関数は、タイトル画面の「 itemstore BLOG いますぐ始めるアプリ内課金」ボタンが押されたときに実行されます。
TitleScene コンポーネントを持ったゲームオブジェクトをシーンに配置して、OnClick でそのゲームオブジェクトを選択することで TitleScene の public 関数を呼び出せるようになります。

Util.cs

汎用的に用いる関数を集めたスクリプトです。
効果音を再生したり、ウェブサイトをブラウザで開く機能が含まれています。

クラスのメンバ関数に public static をつけることで、インスタンスがなくても外部から関数を呼び出すことができます。

Util.cs
    public static void OpenURL(string url)
    {
        Application.OpenURL(url);
        return;
    }

Wall.cs

ボールが壁にぶつかったときに、設定された効果音を再生するイベント処理が実装されています。

プロパティの説明

18-2-Wall.csのinspector

SeHitWall

ボールが壁にぶつかったときの効果音です。

スクリプトの説明

OnCollisionEnter2D は、Collider2D が衝突を検知したときに呼び出されるイベント関数です。
引数の collision には衝突した際の情報が入っています。
バーではなくボールが衝突したことを確認して、効果音を再生しています。

Wall.cs
    private void OnCollisionEnter2D(Collision2D collision)
    {
        if (collision.gameObject.layer == LayerMask.NameToLayer("Ball"))
        {
            // 効果音を再生
            Util.PlayAudioClip(this.seHitWall, Camera.main.transform.position, 1.0f);
        }
    }

参考リンク