【チュートリアル2】uGUIとPhysics2Dでブロック崩しを作ってみる 続き

【チュートリアル】uGUIとPhysics2Dでブロック崩しを作ってみる
チュートリアルとして試しに作ってみました。 比較的プログラムをあまりできない人向けにできる限り崩して書くようには気を付けます。 また、画像が用意できない人向けに画像リソースは一切使わないようにしてます。 ※この記事はUnity...

この記事の続きです。

最後に課題として投げていたものを順次実装してみます。

ボールが毎回同じ挙動

毎度同じようにはねて面白味もなにもないので、バーの当たる場所で跳ね返る角度を若干変えるようにします。

ボールに角度を変える処理を追加

まず、ボールに角度を変える処理を追加します。

using UnityEngine;

/// <summary>
/// ボールの処理を記述
/// </summary>
public class Ball : MonoBehaviour
{
    /// <summary>
    /// このスクリプトが設定されているGameObjectが初期化されるタイミングで行われる処理を記述する
    /// </summary>
    void Start()
    {
        // このGameObjectに付与されている物理演算コンポネントを取得
        var body = gameObject.GetComponent<Rigidbody2D>();
        // 画面描画用のCanvasを取得
        var canvas = GetComponentInParent<Canvas>();

        // ボールを動かす向き:とりあえず右上
        // normalized(正規化)で大きさを1のベクトルに
        var direction = new Vector2( 1, 1 ).normalized;

        // velocity(速度)を設定する。
        // この速度は秒速
        body.velocity = direction * 640 * canvas.transform.localScale.x;
    }

    /// <summary>
    /// ボールの向きを変える
    /// </summary>
    /// <param name="angle"></param>
    public void Rotate( float angle )
    {
        // このGameObjectに付与されている物理演算コンポネントを取得
        var body = gameObject.GetComponent<Rigidbody2D>();
        // 画面描画用のCanvasを取得
        var canvas = GetComponentInParent<Canvas>();

        // 今の速度
        var velocity = body.velocity;

        // angle度回転させる
        velocity = Quaternion.Euler( 0, 0, angle ) * velocity;

        // 計算誤差で大きさ変わってるかもしれないので計算しなおす
        body.velocity = velocity.normalized * 640 * canvas.transform.localScale.x;
    }
}

ベクトルの回転はQuaternionを使う事で簡単にできます。

衝突位置の検知

次に衝突位置がバーのどのあたりかを知る必要があります。

衝突時に何がどこにぶつかったなどの情報はOnCollisionExit2D関数に引数として渡されます。

    void OnCollisionExit2D( Collision2D collision )
    {
         // 引数にCollision2Dを書いていると衝突時の情報がこれに格納されている
    }

この引数はなくても動きますが、その場合は衝突したタイミングだけがわかり、そのほかの細かい衝突時の情報が取得できません。

試しに衝突の位置をログに表示してみます。

using UnityEngine;
using UnityEngine.EventSystems;

/// <summary>
/// 自機の処理
/// </summary>
public class Bar
    : MonoBehaviour
{
    /// <summary>
    /// 画面をタッチされたときの処理
    /// </summary>
    /// <param name="arg"></param>
    public void OnTouch( BaseEventData arg )
    {
        PointerEventData e = arg as PointerEventData;
        var transform = GetComponent<RectTransform>();
        var position = transform.position;      // 現在のバーの位置取得
        position.x = e.position.x;              // ドラッグされている場所を設定
        transform.position = position;          // Transformに反映
    }

    void OnCollisionExit2D( Collision2D collision )
    {
        var point = collision.contacts[0].point.x - transform.position.x;
        Debug.Log( "衝突座標:" + collision.contacts[0].point );
        Debug.Log( "自機との差:" + point );
    }
}

Collision2Dのcontactsに衝突座標が入っています。配列な理由は複数同時に何かにぶつかることがあり得るからです。

 

バーの座標と衝突座標の差を計算すると、真ん中であれば0、左端であれば-横幅の半分、右端であれば+横幅の半分となります。

なので、差を横幅の半分で割ると、当たった位置で-1~1までの数値が求めることが出来ます。

これに変えたい角度を掛けると真ん中はそのまま跳ね返る、端に行くにつれ大きく角度が変わる。というのが実現できそうです。

    void OnCollisionExit2D( Collision2D collision )
    {
        var point = collision.contacts[0].point.x - transform.position.x;
        var rate = point / (GetComponent<RectTransform>().rect.width * .5f);
        var ball = collision.contacts[0].collider.GetComponent<Ball>();
        // 最大で30度向きを回転させる
        ball.Rotate( rate * 30 );
    }

先ほど作った関数を呼び出してこんな感じです。

バーがはみ出す

はみ出すとちょっと見た目が良くないのではみ出ないようにします。

まず座標が出ないように

座標が一定以上変わらないようにします。

ただし、上記でバーの位置で跳ね返る方向を変えたので半分程度は壁にめり込むようにします。

using UnityEngine;
using UnityEngine.EventSystems;

/// <summary>
/// 自機の処理
/// </summary>
public class Bar
    : MonoBehaviour
{
    /// <summary>
    /// 画面をタッチされたときの処理
    /// </summary>
    /// <param name="arg"></param>
    public void OnTouch( BaseEventData arg )
    {
        PointerEventData e = arg as PointerEventData;
        var transform = GetComponent<RectTransform>();
        var position = transform.position; // 現在のバーの位置取得
        position.x = e.position.x; // ドラッグされている場所を設定
        transform.position = position; // Transformに反映

        // xが-320以下なら-320に、320以上なら320になるようにする
        var localPosition = transform.anchoredPosition;
        localPosition.x = Mathf.Clamp( localPosition.x, -320, 320 );
        transform.anchoredPosition = localPosition;
    }

    void OnCollisionExit2D( Collision2D collision )
    {
        var point = collision.contacts[0].point.x - transform.position.x;
        var rate = point / (GetComponent<RectTransform>().rect.width * .5f);
        var ball = collision.contacts[0].collider.GetComponent<Ball>();
        // 最大で30度向きを回転させる
        ball.Rotate( rate * 30 );
    }
}

これも簡単です。

Clampを使って-320~320の以外の数値にならないようにしているだけです。

はみ出た部分は見えなくする

これでもバーが半分ほどはみ出るので、この部分は見えなくします。

GameAreaにAdd Component → UI → Rect Mask 2Dを追加します。

これだけで、はみ出た部分は表示されなくなります。

ブロックに耐久力

ブロックに耐久力を設定できるようにします。

これも難しくありません。ブロックに耐久力を設定できるようにし、ぶつかる度に減らし、0になったら消す。というだけです。

using UnityEngine;

/// <summary>
/// ブロックに関する処理
/// </summary>
public class Block : MonoBehaviour
{
    /// <summary>
    /// 耐久力
    /// </summary>
    private int hp_;

    /// <summary>
    /// 初期化
    /// </summary>
    void Start()
    {
        hp_ = 2;
    }

    /// <summary>
    /// ボールがぶつかったときの処理
    /// </summary>
    void OnCollisionExit2D()
    {
        // hpを減らして0以下なら消す
        --hp_;
        if ( hp_ <= 0 ) {
            Destroy( gameObject );
        }
    }
}

失敗できる回数を設ける

いわゆる残機を設けます。

Game.csに残機用変数を追加します。

using UnityEngine;

public class Game : MonoBehaviour
{
    /// <summary>
    /// ブロックを追加するGameObject
    /// </summary>
    [SerializeField]
    private RectTransform blocks_;

    /// <summary>
    /// ブロックの元になるPrefab
    /// </summary>
    [SerializeField]
    private GameObject blockPrefab_;

    /// <summary>
    /// 残機
    /// </summary>
    private int life_;

    /// <summary>
    /// 開始時に一度だけ呼ばれる処理
    /// </summary>
    void Start()
    {
        // 横5個なので左に2個分ずらして中央に合わせる
        int centering = 5 / 2;

        // 5x5個ブロックを用意する
        for ( int y = 0; y < 5; ++y ) {
            for ( int x = 0; x < 5; ++x ) {
                // InstantiateでPrefabをコピーしたGameObjectを作る
                var block = Instantiate<GameObject>( blockPrefab_ );
                // 新規GameObjectの親を設定されたGameObjectにする
                block.transform.SetParent( blocks_, false );

                // 座標を調整する
                var transform = block.GetComponent<RectTransform>();
                var rect = transform.rect;      // ブロックの大きさ

                // xは0が真ん中
                //  -1*widthでブロック1つ分左、1*widthでブロック1つ分右に
                // yは0が一番上、0だと壁にめり込むので60下に下げる
                //  そこから-width毎に下にずれる
                transform.anchoredPosition = new Vector2(
                        (x - centering) * rect.width,
                        -y * rect.height - 60 );
            }
        }

        life_ = 3;
    }
}

変数life_を増やしました。とりあえず初期値に3を入れてます。

次に残機を減らして、0になったらゲームオーバーになるような関数を追加します。

using UnityEngine;
using UnityEngine.SceneManagement;

public class Game : MonoBehaviour
{
    /// <summary>
    /// ブロックを追加するGameObject
    /// </summary>
    [SerializeField]
    private RectTransform blocks_;

    /// <summary>
    /// ブロックの元になるPrefab
    /// </summary>
    [SerializeField]
    private GameObject blockPrefab_;

    /// <summary>
    /// 残機
    /// </summary>
    private int life_;

    /// <summary>
    /// 開始時に一度だけ呼ばれる処理
    /// </summary>
    void Start()
    {
        // 横5個なので左に2個分ずらして中央に合わせる
        int centering = 5 / 2;

        // 5x5個ブロックを用意する
        for ( int y = 0; y < 5; ++y ) {
            for ( int x = 0; x < 5; ++x ) {
                // InstantiateでPrefabをコピーしたGameObjectを作る
                var block = Instantiate<GameObject>( blockPrefab_ );
                // 新規GameObjectの親を設定されたGameObjectにする
                block.transform.SetParent( blocks_, false );

                // 座標を調整する
                var transform = block.GetComponent<RectTransform>();
                var rect = transform.rect;      // ブロックの大きさ

                // xは0が真ん中
                //  -1*widthでブロック1つ分左、1*widthでブロック1つ分右に
                // yは0が一番上、0だと壁にめり込むので60下に下げる
                //  そこから-width毎に下にずれる
                transform.anchoredPosition = new Vector2(
                        (x - centering) * rect.width,
                        -y * rect.height - 60 );
            }
        }

        life_ = 3;
    }

    /// <summary>
    /// 失敗
    /// </summary>
    public void Miss()
    {
        --life_;
        if ( life_ <= 0 ) {
            SceneManager.LoadScene( "Game" );
        }
    }
}

このままだとボールが画面外に出たまま戻ってこないので、ボールを復帰させます。

    /// <summary>
    /// 失敗
    /// </summary>
    public void Miss()
    {
        --life_;
        if ( life_ <= 0 ) {
            SceneManager.LoadScene( "Game" );
        } else {
            // 子供の中からボールを探す
            var ball = GetComponentInChildren<Ball>();
            // 0に戻す
            ball.transform.localPosition = new Vector3( 0, 0, 0 );
        }
    }

Field.csでゲームオーバー判定していたので、この関数を呼び出すように変更します。

using UnityEngine;
using UnityEngine.SceneManagement;

public class Field : MonoBehaviour
{
    /// <summary>
    /// ボールがこの領域から出た時の処理
    /// </summary>
    public void OnTriggerExit2D()
    {
        var game = GetComponentInParent<Game>();
        game.Miss();
    }
}

ひとまずこれで失敗してもボールが真ん中に戻ってきてつづきができるようになりました。

復帰早々下向きに飛んで来るのは微妙なので、向きも初期化するようにします。

using UnityEngine;

/// <summary>
/// ボールの処理を記述
/// </summary>
public class Ball : MonoBehaviour
{
    /// <summary>
    /// このスクリプトが設定されているGameObjectが初期化されるタイミングで行われる処理を記述する
    /// </summary>
    void Start()
    {
        Reset();
    }

    /// <summary>
    /// ボールの向きを変える
    /// </summary>
    /// <param name="angle"></param>
    public void Rotate( float angle )
    {
        // このGameObjectに付与されている物理演算コンポネントを取得
        var body = gameObject.GetComponent<Rigidbody2D>();
        // 画面描画用のCanvasを取得
        var canvas = GetComponentInParent<Canvas>();

        // 今の速度
        var velocity = body.velocity;

        // angle度回転させる
        velocity = Quaternion.Euler( 0, 0, angle ) * velocity;

        // 計算誤差で大きさ変わってるかもしれないので計算しなおす
        body.velocity = velocity.normalized * 640 * canvas.transform.localScale.x;
    }

    /// <summary>
    /// ボールの向きと位置を初期化
    /// </summary>
    public void Reset()
    {
        // このGameObjectに付与されている物理演算コンポネントを取得
        var body = gameObject.GetComponent<Rigidbody2D>();
        // 画面描画用のCanvasを取得
        var canvas = GetComponentInParent<Canvas>();

        // ボールを動かす向き:とりあえず右上
        // normalized(正規化)で大きさを1のベクトルに
        var direction = new Vector2( 1, 1 ).normalized;

        // velocity(速度)を設定する。
        // この速度は秒速
        body.velocity = direction * 640 * canvas.transform.localScale.x;

        // 位置を0に
        transform.localPosition = Vector3.zero;
    }
}
    /// <summary>
    /// 失敗
    /// </summary>
    public void Miss()
    {
        --life_;
        if ( life_ <= 0 ) {
            SceneManager.LoadScene( "Game" );
        } else {
            // 子供の中からボールを探す
            var ball = GetComponentInChildren<Ball>();
            // 0に戻す
            ball.Reset();
        }
    }

Ball.csに初期化する関数を追加し、Game.csではそれを使用するように変更しました。

スコアの導入

スコアを導入します。

using UnityEngine;
using UnityEngine.SceneManagement;

public class Game : MonoBehaviour
{
    /// <summary>
    /// ブロックを追加するGameObject
    /// </summary>
    [SerializeField]
    private RectTransform blocks_;

    /// <summary>
    /// ブロックの元になるPrefab
    /// </summary>
    [SerializeField]
    private GameObject blockPrefab_;

    /// <summary>
    /// 残機
    /// </summary>
    private int life_;

    /// <summary>
    /// スコア
    /// </summary>
    private int score_;

    /// <summary>
    /// 開始時に一度だけ呼ばれる処理
    /// </summary>
    void Start()
    {
// 略

        life_ = 3;

        score_ = 0;
    }

// 略

    /// <summary>
    /// スコアの加算
    /// </summary>
    /// <param name="score"></param>
    public void AddScore( int score )
    {
        score_ += score;
    }

    /// <summary>
    /// スコアの取得
    /// </summary>
    public int Score
    {
        get { return score_; }
    }
}

残機と同様にscore_変数を追加しました。

using UnityEngine;

/// <summary>
/// ブロックに関する処理
/// </summary>
public class Block : MonoBehaviour
{
    /// <summary>
    /// 耐久力
    /// </summary>
    private int hp_;

    /// <summary>
    /// 初期化
    /// </summary>
    void Start()
    {
        hp_ = 2;
    }

    /// <summary>
    /// ボールがぶつかったときの処理
    /// </summary>
    void OnCollisionExit2D()
    {
        // hpを減らして0以下なら消す
        --hp_;
        if ( hp_ <= 0 ) {
            var game = GetComponentInParent<Game>();
            game.AddScore( 100 );
            Destroy( gameObject );
        }
    }
}

ブロックが消えるときにスコアを加算するように対応してます。

これでスコアが増えるはずですが、表示されていないとわかりませんので、画面に出すようにします。

まず、Gameの下に新しいGameObjectを作ります。

Gameを右クリックし、Create Emptyを選択します。名前はUIにしました。

addui

他のと同様にストレッチでLeft等はすべて0です。

その下にスコア用のTextを追加します。UIを右クリックし、UI → Textを選択します。

これで文字表示用のオブジェクトが出来ました。

addscore

位置はTop Strechにして、図のようにしました。上から少し下で横幅一杯位がに表示できるようにしています。

右上揃いにしたいので、Pivotは両方1にし、TextのAlignmentも右上揃いに設定します。

Font Sizeは見やすい大きさに適当に設定してください。

これでスコアの表示用オブジェクトの設定が終わったので、Game.csでスコアが加算された際に、このTextにその数値を反映するようにします。

using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;

public class Game : MonoBehaviour
{
    /// <summary>
    /// ブロックを追加するGameObject
    /// </summary>
    [SerializeField]
    private RectTransform blocks_;

    /// <summary>
    /// ブロックの元になるPrefab
    /// </summary>
    [SerializeField]
    private GameObject blockPrefab_;

    /// <summary>
    /// スコアの表示
    /// </summary>
    [SerializeField]
    private Text scoreText_;

    /// <summary>
    /// 残機
    /// </summary>
    private int life_;

    /// <summary>
    /// スコア
    /// </summary>
    private int score_;

    /// <summary>
    /// 開始時に一度だけ呼ばれる処理
    /// </summary>
    void Start()
    {
// 略

        life_ = 3;

        score_ = 0;
        scoreText_.text = score_.ToString();
    }

// 略

    /// <summary>
    /// スコアの加算
    /// </summary>
    /// <param name="score"></param>
    public void AddScore( int score )
    {
        score_ += score;
        scoreText_.text = score_.ToString();
    }

    /// <summary>
    /// スコアの取得
    /// </summary>
    public int Score
    {
        get { return score_; }
    }
}

setscoretext

Editor上で設定するのも忘れないように。

シーン遷移の導入

シーン遷移を入れます。

今はゲーム部分があって、失敗したらまたゲーム部分がはじめからですが、

タイトル → ゲーム部分 → ゲームオーバー画面 → タイトル

という流れに変更しようと思います。

まずはタイトルとゲームオーバーのシーンを作ります。

File → New Sceneで新規シーンを作ります。

作ったらすぐFile → Save Sceneで保存します。

Game → Scenesの下にTitleという名前にしましょう。

Canvasの設定をGameシーンの時と同様にします。

setcanvasscaler

下にタッチ判定用のゲームオブジェクトを追加します。

Canvasを右クリックし、UI → Image

名前はTouchAreaにし、StrechでLeft等はすべて0に。ColorのAlphaも0にして見えなくします。

settoucharea3

タイトルとタッチして開始の文字を出す為のTextを追加します。

TitleHierarchy

TitleHierarchy2

次にタッチされたら、Gameのシーンへ遷移するスクリプトを用意します。

using UnityEngine;
using UnityEngine.SceneManagement;

public class Title : MonoBehaviour
{
    public void OnClick()
    {
        SceneManager.LoadScene( "Game" );
    }
}

これだけです。

settitletoucheven

TouchAreaにAdd ComponentからScripts → TitleとEvent → Event Triggerを追加します。

Pointer Clickイベントを追加して、Titleをドラッグで設定します。

シーンを登録する

これで実行すれば動くかとおもいきや動きません。Gameという名前のシーンが無いと言われます。

複数シーンが存在する場合はBuild Settingsにシーンを登録しなければいけません。

Build SettingsはFileにあります。

addscene

ゲームオーバーを作る

ゲームオーバーを作りますが、ゲームオーバーと表示して、タッチしたらタイトルに戻るだけなので、内容がタイトルとほぼ一緒です。

というわけで面倒なので、Titleシーンをコピペで作ります。

copyscene

Titleシーンを選択して、Edit → Duplicateを選択します。

コピーされてTitle 1が出来上がりますので、GameOverにリネームします。

GameOverシーンをダブルクリックして開いて、細かいところを変更していきます。

※GameOverシーンが空っぽの時はまだ、保存されていないTitleシーンがコピーされていますので、再度コピーしなおしてください。

GameOver

名前を変えたり文言を変えたりしてます。

タイトルに戻るスクリプトも作ります。

using UnityEngine;
using UnityEngine.SceneManagement;

public class GameOver : MonoBehaviour
{
    public void OnClick()
    {
        SceneManager.LoadScene( "Title" );
    }
}

gotoucharea

TouchAreaのスクリプトも差し替えます。

これでゲームオーバー → タイトル → ゲーム という遷移は出来るようになったので、最後にゲームオーバーでこのシーンへ遷移するように変更します。

    /// <summary>
    /// 失敗
    /// </summary>
    public void Miss()
    {
        --life_;
        if ( life_ <= 0 ) {
            SceneManager.LoadScene( "GameOver" );
        } else {
            // 子供の中からボールを探す
            var ball = GetComponentInChildren<Ball>();
            // 0に戻す
            ball.Reset();
        }
    }

Game.csの失敗時の処理を書き換えました。

※Build SettingsにGameOverシーンを登録するのを忘れないようにしてください。

アイテムの導入

ライフを増やすアイテムとボールの速度を速めるアイテムを導入してみます。

進めるにあたってGameシーンを開いておきましょう。(一つ前のトピックでタイトルシーンになってるかもしれないので。)

アイテムはブロックを破壊したときにランダムで出現し、壊したブロックの位置から下にゆっくり落ちてきて、自機で触れると効果が発生する感じにしたいと思います。

まずアイテム取得時の機能を作っておく

アイテム自体を作る前に先にアイテムを取ったときの「ライフを増やす」と「速度を増す」機能を関数として実装しておきます。

ライフはGame.csにあるのでそこに増やす処理を足します。

using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;

public class Game : MonoBehaviour
{
 // 略

    /// <summary>
    /// ライフが増える
    /// </summary>
    public void OneUp()
    {
        ++life_;
    }
}

ライフの変数を1足すだけです。

ボールの速度も変える処理を入れます。これはBall.csで制御されていたのでそれを編集します。

まず、Ballに速度用変数を用意します。

using UnityEngine;

/// <summary>
/// ボールの処理を記述
/// </summary>
public class Ball : MonoBehaviour
{
    /// <summary>
    /// 速度
    /// </summary>
    private float speed_;

 // 略
}

次にこの変数を変更する関数も用意します。

using UnityEngine;

/// <summary>
/// ボールの処理を記述
/// </summary>
public class Ball : MonoBehaviour
{

// 略

    /// <summary>
    /// 速度を加算
    /// </summary>
    /// <param name="addition"></param>
    public void AddSpeed( float addition )
    {
        speed_ += addition;
    }
}

ただ、このままでは変数が変わっているだけで、Rigidbodyの速度が変わっていません。

なので、Rigidbodyの速度を変更する関数も用意します。

    /// <summary>
    /// 速度を更新
    /// </summary>
    private void UpdateVelocity()
    {
        // このGameObjectに付与されている物理演算コンポネントを取得
        var body = gameObject.GetComponent<Rigidbody2D>();
        // 画面描画用のCanvasを取得
        var canvas = GetComponentInParent<Canvas>();

        // velocity(速度)を設定する。
        // この速度は秒速
        body.velocity = body.velocity.normalized * speed_ * canvas.transform.localScale.x;
    }

この関数を速度変更時に呼ぶようにします。また、Resetで速度を元に戻したりを踏まえて最終的に以下のようになります。

using UnityEngine;

/// <summary>
/// ボールの処理を記述
/// </summary>
public class Ball : MonoBehaviour
{
    /// <summary>
    /// 速度
    /// </summary>
    private float speed_;

    /// <summary>
    /// このスクリプトが設定されているGameObjectが初期化されるタイミングで行われる処理を記述する
    /// </summary>
    void Start()
    {
        Reset();
    }

    /// <summary>
    /// ボールの向きを変える
    /// </summary>
    /// <param name="angle"></param>
    public void Rotate( float angle )
    {
        // このGameObjectに付与されている物理演算コンポネントを取得
        var body = gameObject.GetComponent<Rigidbody2D>();
        // 画面描画用のCanvasを取得
        var canvas = GetComponentInParent<Canvas>();

        // 今の速度
        var velocity = body.velocity;

        // angle度回転させる
        velocity = Quaternion.Euler( 0, 0, angle ) * velocity;

        // 計算誤差で大きさ変わってるかもしれないので計算しなおす
        body.velocity = velocity.normalized * speed_ * canvas.transform.localScale.x;
    }

    /// <summary>
    /// ボールの向きと位置を初期化
    /// </summary>
    public void Reset()
    {
        // このGameObjectに付与されている物理演算コンポネントを取得
        var body = gameObject.GetComponent<Rigidbody2D>();
        // 画面描画用のCanvasを取得
        var canvas = GetComponentInParent<Canvas>();

        // 速さを初期値に
        speed_ = 640;

        // ボールを動かす向き:とりあえず右上
        // normalized(正規化)で大きさを1のベクトルに
        var direction = new Vector2( 1, 1 ).normalized;

        // velocity(速度)を設定する。
        // この速度は秒速
        body.velocity = direction * speed_ * canvas.transform.localScale.x;

        // 位置を0に
        transform.localPosition = Vector3.zero;
    }

    /// <summary>
    /// 速度を加算
    /// </summary>
    /// <param name="addition"></param>
    public void AddSpeed( float addition )
    {
        speed_ += addition;
        // Rigidbodyを更新
        UpdateVelocity();
    }

    /// <summary>
    /// 速度を更新
    /// </summary>
    private void UpdateVelocity()
    {
        // このGameObjectに付与されている物理演算コンポネントを取得
        var body = gameObject.GetComponent<Rigidbody2D>();
        // 画面描画用のCanvasを取得
        var canvas = GetComponentInParent<Canvas>();

        // velocity(速度)を設定する。
        // この速度は秒速
        body.velocity = body.velocity.normalized * speed_ * canvas.transform.localScale.x;
    }
}

同じような処理が多く、まとめた方がいいのですがそれはまた別の話なので今回は割愛します。

これでライフを増やす処理と速度の変更処理が出来ました。

アイテムオブジェクトをつくる

次に肝心のアイテムオブジェクトを作ります。

まずはアイテムオブジェクト用の場所をHierarchyに用意しておきしょう。

AddItemGroup

作り方や設定はいままでどおりです。

ライフを増やすアイテム

oneupitem

Itemsの下にTextを作って上記のような設定にしました。

自機に触れると取得するというシステムにするので当たり判定用にBox Colliderもつけています。また、下に落とす処理はRigidbodyを使います。

スクリプトを用意してそこで、下に落ちる処理と自機と触れたときの処理を記述します。

using UnityEngine;

public class OneupItem : MonoBehaviour
{
    /// <summary>
    /// 初期化処理
    /// </summary>
    void Start()
    {
        var canvas = GetComponentInParent<Canvas>();
        var body = GetComponent<Rigidbody2D>();

        // 下に落とす処理
        body.velocity = new Vector2( 0, -1 ) * 200 * canvas.transform.localScale.x;
    }

    /// <summary>
    /// 自機と衝突したら1UP
    /// </summary>
    void OnCollisionEnter2D()
    {
        var game = GetComponentInParent<Game>();
        // 1up
        game.OneUp();

        // アイテムを消す
        Destroy( gameObject );
    }
}

とりあえず出来ました。ボールの下あたりを初期位置にして開始すると1UP出来ます。

が、ボールと同じ場所にあるとボールとぶつかり1UPしてしまいました。

当たる相手を限定する

アイテムは自機だけにぶつかってほしいので、当たる相手を限定します。

スクリプトで判定する事も可能ですが、今回はEditor上の設定で対応したいと思います。

Layerを増やす

Inspectorの左上にLayerがあります。それを選択してAdd Layerを選びます。(Inspectorで見るのはどのGameObjectでもいいです。)

DDAddLayer

選択したらレイヤーの編集画面になるので必要なレイヤーを足します。

AddLayers

それぞれボール、壁とブロック、自機、アイテムに設定するようにします。

  • BallのレイヤーをBallに
  • Wallsの下の4つのレイヤーをBlockに
  • 先ほど作った1UPアイテムのレイヤーをItemに
  • 自機のバーのレイヤーをBarに
  • BlockプレハブのレイヤーをBlockに

各GameObjectにレイヤーの設定が終わったら次にPhysics2Dの設定を開きます。physics2dsetting

ここでは一番下にあるLayer Collision Matrixを設定することで、どのレイヤーのオブジェクトがどのレイヤーのオブジェクトと衝突するかを設定することが出来ます。

setlayercollisionmatrix

  • ItemはBarだけにぶつかる
  • BarはBallとItemにぶつかる
  • BlockはBallだけにぶつかる

ということで上図のようになりました。

※すこし図が見づらいかもしれませんが、例えばBarであれば下図の赤枠の中のチェックがついているもの(ItemとBall)が衝突対象です。

setlayercollisionmatrix2

これで実行してみるとアイテムはボールとぶつからず自機だけに反応するようになりました。

他も問題なさそうです。

速度上昇アイテム作る

同様に速度上昇アイテムを作ります。

1UPのコピペを改変していきます。

Hirerachyの1UPアイテムを右クリックしてDuplicateします。コピーが出来るので適当にいじります。

変更点はGameObjectの名前と、Textの文言と、あとはOneupItem.csの削除くらいです。

OneupItem.csの変わりにSpeedupItem.csを作ってAdd Componentします。

using UnityEngine;

public class SpeedupItem : MonoBehaviour
{
    /// <summary>
    /// 初期化処理
    /// </summary>
    void Start()
    {
        var canvas = GetComponentInParent<Canvas>();
        var body = GetComponent<Rigidbody2D>();

        // 下に落とす処理
        body.velocity = new Vector2( 0, -1 ) * 200 * canvas.transform.localScale.x;
    }

    /// <summary>
    /// 自機と衝突したら1UP
    /// </summary>
    void OnCollisionEnter2D()
    {
        //var ball = GetComponentInParent<Ball>();
        // ボールは親オブジェクトが持っているわけではないので、GetComponentInParentでは取得できない
        var ball = FindObjectOfType<Ball>();

        // 速度上昇
        ball.AddSpeed( 100 );

        // アイテムを消す
        Destroy( gameObject );
    }
}

完成です。

Prefab化する

開始時に出てるのでは意味がないので、Prefabにしてブロック破壊時に出せるように準備します。

createitemprefab

以前にブロックでやった通りHierarchyからドラッグ&ドロップで終了です。

Hierachyにある方はもういらないので削除してください。

アイテムを生み出す処理

まずは、Game.csにアイテム用Prefabを設定します。

using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;

public class Game : MonoBehaviour
{
//略

    /// <summary>
    /// アイテムの元になるPrefab達
    /// </summary>
    [SerializeField]
    private GameObject[] itemPrefabs_;

// 略
}

Editorでここに設定します。setitemprefabs

2個設定できました。ブロックの時と同じようにItemを足す場所も必要です。

using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;

public class Game : MonoBehaviour
{
// 略

    /// <summary>
    /// アイテムを追加するGameObject
    /// </summary>
    [SerializeField]
    private RectTransform items_;

    /// <summary>
    /// アイテムの元になるPrefab達
    /// </summary>
    [SerializeField]
    private GameObject[] itemPrefabs_;

//略
}

これもEditorで設定します。

setitemsrt

次はGame.csでランダムに指定の座標にアイテムを生成する関数も足します。

using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;

public class Game : MonoBehaviour
{
// 略

    /// <summary>
    /// 指定座標にアイテムを適当に生成する
    /// </summary>
    /// <param name="position"></param>
    public void CreateItemAtRandom( Vector2 position )
    {
        // 0~アイテムの個数までの乱数を取得
        int randomItemNo = Random.Range( 0, itemPrefabs_.Length );
        // 乱数でアイテムを選択
        GameObject itemPrefab = itemPrefabs_[randomItemNo];
        // 選択したアイテムをコピー
        GameObject newItem = Instantiate( itemPrefab );

        // itemsの子供に設定
        var transform = newItem.GetComponent<RectTransform>();
        transform.SetParent( items_, false );

        // 座標を設定
        transform.position = position;
    }
}

ブロックが破壊される際にこれを呼びます。

using UnityEngine;

/// <summary>
/// ブロックに関する処理
/// </summary>
public class Block : MonoBehaviour
{
    /// <summary>
    /// 耐久力
    /// </summary>
    private int hp_;

    /// <summary>
    /// 初期化
    /// </summary>
    void Start()
    {
        hp_ = 2;
    }

    /// <summary>
    /// ボールがぶつかったときの処理
    /// </summary>
    void OnCollisionExit2D()
    {
        // hpを減らして0以下なら消す
        --hp_;
        if ( hp_ <= 0 ) {
            var game = GetComponentInParent<Game>();
            game.AddScore( 100 );
            // 適当にアイテム生成
            game.CreateItemAtRandom( transform.position );

            Destroy( gameObject );
        }
    }
}

ひとまず100%生成されるようにして動きを確認します。

実行してみるとブロックを壊すとアイテムがでて、とると効果も出ている感じです。

出現率を設定する

いまは100%なので出現率を変更します。

using UnityEngine;

/// <summary>
/// ブロックに関する処理
/// </summary>
public class Block : MonoBehaviour
{
    /// <summary>
    /// 耐久力
    /// </summary>
    private int hp_;

    /// <summary>
    /// 初期化
    /// </summary>
    void Start()
    {
        hp_ = 2;
    }

    /// <summary>
    /// ボールがぶつかったときの処理
    /// </summary>
    void OnCollisionExit2D()
    {
        // hpを減らして0以下なら消す
        --hp_;
        if ( hp_ <= 0 ) {
            var game = GetComponentInParent<Game>();
            game.AddScore( 100 );

            // 0~99の数値をランダムに取得
            int randomValue = Random.Range( 0, 100 );
            if ( randomValue < 50 ) {
                // 適当にアイテム生成
                game.CreateItemAtRandom( transform.position );
            }

            Destroy( gameObject );
        }
    }
}

ランダムで0~99までの数値を取得し、それが0~49ならアイテム生成としています。

これで50%でアイテム生成されるようになりました。

プロジェクト

ここまでの内容はこれです。

タイトルとURLをコピーしました