ブロック崩しゲーム

Unity2Dのシンプルなゲームを作ってみましょう。

プロジェクト設定

プロジェクト作成

まず、新規プロジェクトを作成しましょう。2Dゲームであるため「Universal 2D」を選んでください。プロジェクト名は「breakout」にしましょう。

image.png

素材を追加

ゲームに必要な素材を「Project」に追加しましょう。今回必要なのは

「Sprites」と「Sound」フォルダを作成し、適切にファイルをコピーしてください。

image.png

シーンを構築

スプライトを追加

「SampleScene」の名前はカッコ悪いので、「Game」に書き換えましょう。シーンの名前を変更するには

次、背景とプレーヤーのパドルを追加しましょう。プロジェクトから「Sprites / Background」と「Sprites / Player」をドラッグドロップでシーンに追加。

image.png

ヒエラルキーに2つの新しいゲームオブジェクトが現れたことを確認してください!

スプライトの表示順番

場合により、背景のみ表示され、プレーヤーのパドルが表示されないときもある。なぜなら、背景はパドルの手前に表示されているので、パドルが後ろに隠れているため。

直し方は2つある:

「SpriteRenderer」の「Order In Layer」を調整

「Order In Layer」を調整することにより、スプライトの表示順番を変えることができる。数値が小さければ小さいほど、後ろから先に表示される。つまり、「背景」は小さくし、「前景」を大きい数値にすれば良いでしょう。

Background と Player は両表とも「0」になっているため、ランダムに順番が決め、パドルがたまに手前に現れ、時々は後ろに隠れてしまう。これを防ぐには、Playerの「Order In Layer」は 0 よりも大きい数値にしましょう。例えば、「10」にすると:

image.png

パドルが必ず手前に出る。

「ゲームオブジェクト」の Z軸位置を調整

2Dゲームの場合は「X軸」(左右)と「Y軸」(上下)しかないと思われるが、実は「Z軸」もある。Z軸で、各オブジェクトの表示順番も調整ができる。カメラに近いものを手前に表示されるので、パドルの Z位置は少し小さめすれば良いでしょう

※:Z軸は大きいほど奥になること忘れずに~

image.png

実は、3D空間で確認してみると:

image.png

パドルが背景の手前にあることを確認ができる!

表示範囲を調整

とりあえず、画面サイズがFullHDをターゲットにしましょう。ゲームビューで「FullHD (1920x1080)」のサイズを選んでください

image.png

これで、背景は微妙に入らないことがわかる。なぜなら、FullHDだと、カメラの映る範囲は背景よりも小さいからです。

image.png

これを直すには、カメラの「範囲」を少し広くすれば良いでしょう。ヒエラルキーで Main Camera を選択し、「Size」パラメタを調整してみてください。背景がぴったり入るように適切な値をしてみてください。

最後、プレーヤーのパドルを画面の下の方に適切に配置してください。

image.png

物理:コライダーとリジッドボディ

パドルが物理を使って、動かすし、壁とボールの衝突するべきで、コライダーとリジッドを設置しないければならない。

背景

プレーヤーとボールがプレイエリアからはみ出さないように、各壁(右、左、上)にコライダーを付けましょう。

Backgroundの下に子供オブジェクトとして1つのオブジェクト「Wall Left」を追加し、「Box Collider 2D」のコンポーネントを追加してください。また、コライダーのサイズを調整し、左側の壁に合わせてみてください:

image.png

同じ手順で、右側(Wall Right)と上(Wall Top)のコライダーを作ってください。

image.png

パドル

プレーヤーのパドルの衝突には、もちろん、コライダーが必要で、物理的に動かす予定で、リジッドボディも必要。「Player」に「BoxCollider2D」と「Rigidbody2D」を追加してください。

 

このままゲーム開始すると、プレーヤーのパドルが落下します。重力がかかるし、回転、移動は無制限なので、制約(Constraint)を付けなければならない。

 

まず、重力が影響しないように、「Gravity Scale」は「0」にしましょう。そして「Constraints」は回転できないように「Freeze Rotation」をチェックし、X軸のみで移動するため、Y軸の移動も固定してください(Freeze Position Y)

image.png

スクリプト専用のフォルダを作成

Project の中に「Scripts」の新しいフォルダーを作成してください。

パドル(Player)の移動

パドルが左右に移動できるようにしましょう。このスクリプトを使って:

Scripts フォルダーの中に、新しいスクリプト「PlayerMove」を作成し、上記の機能を実装してみましょう。

スクリプトの変数

using UnityEngine;

// パドルを移動するスクリプト
public class PlayerMove : MonoBehaviour
{
    // 速度をInspectorで設定
    [SerializeField]
    private float speed;

    // 左へ移動するキーをInspectorで設定
    [SerializeField]
    private KeyCode leftKey;

    // 右へ移動するキーをInspectorで設定
    [SerializeField]
    private KeyCode rightKey;

    // 物理的動かすためのリジッドボディ
    private Rigidbody2D _rigidbody;
}

これで新しい PlayerMove のスクリプトを作成し、必要なデータ(変数)を準備ができた。Inspectorで編集するべきの変数に [SerializeField] を頭につけ、アクセス修飾子は private であっても、Unityで見えるようになる。

リジッドボディは Start() で取得するため、[SerializeField] は不要で、プログラムの中で代入する予定。

変数の先頭につけるアンダーバーについて
変数の先頭につけるアンダーバーについて

固いルールではないが、開発者の「いいマナー」としてのルールであり、変数名を見るだけで、アクセス権限がわかるためのものである。

// 速度
[SerializeField]
private float speed;

// 最大体力
[SerializeField]
private int maxHP;

// ステージの名前
[SerializeField]
private string stageName;
// 現在の体力
private float _nowHP;

// リジッドボディコンポーネントの参照
private Rigidbody2D _rigidBody;

// アイテムの位置
private Vector3 _itemPosition;
// コインの数
public int Coins;

// 最大体力(読み込み専用)
public int MaxHP {
  get { return _maxHP; }
}

// 現在の体力
public string NowHP
{
  get { return _nowHP; }
  set { _nowHP = Mathf.Clamp(value, 0, _maxHP); }
}
// 引数の変数は:小文字
private int Sum (int a, int b)
{
  // 内部で使う変数も小文字
  int c = a + b;
  return c;
}
// 攻撃する
public void Attack()
{
}

// ゲームオーバーになったかを確認
private void IsGameOver()
{
}

// フレーム毎の処理 
private void Update()
{
}

初期化 - Start()

パドルの初期化はほとんどなく、あくまで、リジッドボディの参照を取得しておきたい。このスクリプトはパドルにアタッチするべきなので、GetComponent で簡単に取れる。PlayerMove クラスに以下のメソッドを追加してください

private void Start()
{
    _rigidbody = GetComponent<Rigidbody2D>();
}

フレームごとの更新 - Update()

フレームごとに、右か左のキーが押されたかどうかを確認し、押されていたら、その向きに移動する。一方、キーが押されていなかったら、パドルを止める(速度をゼロにする)

PlayerMove クラスに以下のメソッドを追加してください:

private void Update()
{
    // 前提として速度はゼロにする。
    var velocity = Vector2.zero;

    // ただし、左キーが押されたら
    if (Input.GetKey(leftKey))
    {
        // x軸の速度を左の方(マイナス軸)にする
        velocity.x = -speed;
    }

    // 右のキーが押されたら
    if (Input.GetKey(rightKey))
    {
        // x軸の速度を右の方(プラス軸)にする
        velocity.x = speed;
    }

    // 最後に、計算した速度をリジッドボディに与える
    _rigidbody.linearVelocity = velocity;
}

試してみましょう!

スプライトを Player にアタッチし、右キーと左キーを設定の上、速度を設定してください。そして、ゲーム開始して、動くかどうかを確認してください。

image.png


ボールプレハブ

ボールがシーンに直接に追加するものではなく、実行中に生成するものであるため、プレハブとして作成しなければならない。とりあえず、現在のシーンを活用し、プレハブができたら、削除すれば良いでしょう。

プレハブ構築

シーンにボールのスクリプトをドラッグドロップで追加し、丸い「CircleCollider2D」を付けてください。ボールも物理的に動かす予定で、リジッドボディも追加してください。

※ボールが見えなければ、「Order In Layer」または Z軸の位置を適切に変えてください

image.png

今回も重力を使わないので、「Gravity Scale」をゼロにしてね!

ボールの移動を制御する:BallMove

スクリプトフォルダーに新しい「BallMove」スクリプトを作成してください。このスクリプトはボールの動きをコントロールするスクリプトである。また、Unityでボールの速度を変えられるようにしましょう。

スクリプトの変数

using UnityEngine;

// ボールの移動を管理するスクリプト
public class BallMove : MonoBehaviour
{
    // Inspectorで速度を設定する
    [SerializeField]
    private float speed;

    // 移動するためのリジッドボディ
    private Rigidbody2D _rigidbody;
}

プレーヤーと同様に、Unityの物理演算を使い、移動するので、Rigidbody2D を使用する。ただし、入力は関係しないので、Inspector で設定するのは、速度だけになる。

初期化 - Start()

とりあえず、ボールが真上に行くようにし、どうなるのか確認しましょう。

private void Start()
{
    // コンポーネントの参照を取得
    _rigidbody = GetComponent<Rigidbody2D>();
    
    // とりあえず、真上に行くようにしましょう
    _rigidbody.linearVelocity = new Vector2(0, 1) * speed;
}

確認

BallMove のスクリプトをボールにアタッチし、ゲームを実行してみてください。

image.png

問題発見:ボールが止まってしまう!

Unityでは、デフォルトとして衝突が跳ね返しの「弾み」(bounciness) がほとんどない。そのため、ボールと壁が衝突する場合、速度を吸収され、停止してしまう。

これを解決するには、いずれかのコライダーに「弾み強くしろ!」の設定しなければならない。これは「Physics Material 2D」(Physics: 物理、 Material:素材)のデータで調整できるもの。

image.png

スーパーボールのように、ゴムの「素材」を作ろう。この素材の弾みは100%にする(跳ね返す速度は衝突したときと同じ速度にする)まず、プロジェクトのAssetsフォルダーの中に右クリックし、新しい「Create > 2D > Physics Material 2D」を作成し、名前は「Rubber」(ゴム)にする

image.png

Rubberを選択し、Inspectorを見ると、物理的な「素材」のパラメータを調整ができる:

image.png

Friction:(魔雑) - 2つの平面の魔雑のこと

 0:魔雑なし  例:凍った池(すべすべ)

 1:超魔雑   例:サンドペーパー(全く滑らない)

 

Bounciness(弾み)- 衝突するとき跳ね返す力のこと

 0:弾みなし  例:粘土、生地

 1:超跳ね返し 例:スーパーボール

 

Friction Combine / Bounce Combine(合成方法)

 衝突には、2つの物体がかかわるので、どう合成するのか?

 ・Minimum:最低数値を使う

 ・Maximum:最大数値を使う

 ・Mean:2つの数値の平均を使う

image.png

今回は、スーパーボールを再現したいので:

にしましょう。「Rubber」のパラメター設定ができてたら、ボールの CircleCollider2D に付け、再度確認しましょう。

ランダム向きで発射!

ボールが真上だけしか行かないと、あまり面白くないので、ボールがある程度ランダムの向きで始まるようにしましょう。高校で学んできた「三角関数」を少し使えば、解決できる!

※:ゲームでよく使う数学は、また「ゲームエンジンII」で会いましょう!

image.png

アイディアとして、ボールが「最低角度」と「最大角度」の間にランダムに発射すること。これは C# にすると(Startの編集版)

private void Start()
{
    // コンポーネントの参照を取得
    _rigidbody = GetComponent<Rigidbody2D>();
    
    // とりあえず、真上に行くようにしましょう
    // _rigidbody.linearVelocity = new Vector2(0, 1) * speed;

    float minAngle = 45;  // 最低角度 - 度単位
    float maxAngle = 135; // 最大角度 - 度単位
    
    // 最低と最大の間にランダムに角度を求める
    float angle = Random.Range(minAngle, maxAngle);
    
    // 度からラジアンへ変換
    angle = angle * Mathf.Deg2Rad;

    // 三角関数(サインとコサイン)を使って、向きにする
    var direction = new Vector2();
    direction.x = Mathf.Cos(angle);
    direction.y = Mathf.Sin(angle);

    // 速度を与える
    _rigidbody = GetComponent<Rigidbody2D>();
    _rigidbody.linearVelocity = direction * speed;
}

ボールをプレハブ化

作ったボールを実行中に生成できるため、プレハブにしましょう。まず、Project の中にプレハブ専用の「Prefab」フォルダーを作成してください。ボールをヒエラルキーから Prefab フォルダーへドラッグドロップすれば、プレハブの作成ができる。

image.png

これでプレハブは完成であり、シーンからボールを削除しましょう。

ボールをどんどん生成

作ったプレハブを利用し、実行中にたくさんのボールを生成しましょう。

ボール生成する:BallShooter

今度のスクリプトは:

スクリプトの変数

Scripts フォルダーの中に新しい「BallShooter」を追加し、Inspector の変数を準備しましょう:

using UnityEngine;

// ボールを生成するスクリプト
public class BallShooter : MonoBehaviour
{
    // Unityでボールのプレハブを指定
    [SerializeField]
    private GameObject ballPrefab;
}

当然、プレハブ変数の「型」は「GameObject」になる。

マウス処理&ボール生成:Update()

フレーム毎に、マウスボタンのクリックがあったかどうかを確認し、クリックの場合は、プレハブから実物を生成し、実行中にシーンに追加しましょう:

void Update()
{
    // もしも左ボタンをクリックしたら…
    if (Input.GetMouseButtonDown(0))
    {
        // 「Instantiate」を使い、プレハブから実物(インスタンス)を作成
        // newBallは新しく作ったボールの参照である
        var newBall = Instantiate(ballPrefab); 
    }
}

ボールを生成するのは、プレーヤーなので、「player」オブジェクト(パドル)にアタッチして、Ball Prefabの中に、前ステップで作ったボールのプレハブを設定してください。

image.png

これを追加することにより、プレーヤーは移動だけではなく、ボール生成もできるようになった!

1スクリプト⇒1責任

1スクリプト⇒1責任

スクリプトが長ければ、長いほど、バグの修正が辛くなり、ゲームの拡張性が狭まる。基礎ルールとして、「1つのスクリプトは1つの責任」の考えすると、大きな課題を解決しやすい細かい課題に分割し、それぞれが別々のスクリプトで実装する。

これで、機能を増やしたり、減らしたりするのは簡単(スクリプトを追加と削除だけ)。また、スクリプトが短くなるので、バグが発生した場合、すぐ解決できることもメリットである。

考えられる例
  •  武器を打つ:ShootWeapon
    • CPUプレーヤー、人間プレーヤーが関係なく、打つことが可能

  • 体力を表示:HPGauge
    • なんでものHPを表示(プレーヤー、敵、壊せるものなど)

  • プレーヤーを追いかける:FollowPlayer
    • 敵に付けると、プレーヤーを追いかける
    • 仲間に付けると、プレーヤーを追いかける
  • アイテム:Item
    • アイテムを表す

  • アイテムを拾う:ItemCollect
    • アイテム(Item)を拾ったら、インベントリに追加

  • アイテムを管理:Inventory
    • アイテムどこから来たのか関係なく、アイテムを管理

  • アイテムを購入:ItemBuy
    • アイテム(Item)をショップで購入し、インベントリに追加

 

この状態で実行して確認してみてください。

問題発見

いっぱいのボールが現れて楽しいが、いくつかの問題の確認ができた

  1. ボールがたまに遅くなる?
  2. プレーヤーの位置が関係なく、ボールがいつも同じ場所から現れる
  3. ボールが多すぎる。

1. ボールが遅くなる?

超弾みしたのに、なんでボールが遅くなる?これは正しい物理の計算の影響であるため。ビリヤードのように、ある物体は別の物体を移動すると、速度を伝達する(運動量の伝達という)

image.png

image.png

固定物体(壁)とぶつかると、強く跳ね返すが…

…移動可能な物体とぶつけると、速度が移る!

これを直すには、実行中に速度を調整し、指定した「speed」を守るようにする。「BallMove」スクリプトを調整し、フレーム毎は以下の処理にしましょう:

// これは「BallMove」スクリプトの更新処理
void Update()
{
    // 現在の速度ベクトルを取得。向きは保護したいが、
    // 長さ(速度)は必ず「speed」であることを保証
    var velocity = _rigidbody.linearVelocity;
    
    // まず、速度ベクトルの長さは「1」にする(単位ベクトル)
    velocity.Normalize();
    
    // そして、長さは「speed」にしよう
    velocity *= speed;
    
    // 速度を上書きする
    _rigidbody.linearVelocity = velocity;
}

2. ボールがいつも同じ場所から現れる

ブールがプレーヤーのいる場所から発射すれば自然な遊び方になるので、BallShooter のスクリプトの直そう。インスタンスを作った後に、適切な場合に配置:

// BallShooterのUpdateを更新:
void Update()
{
    // もしも左ボタンをクリックしたら…
    if (Input.GetMouseButtonDown(0))
    {
        // 「Instantiate」を使い、プレハブから実物(インスタンス)を作成
        // newBallは新しく作ったボールの参照である
        var newBall = Instantiate(ballPrefab); 
        
        // このスクリプトはパドルにアタッチされているので、
        // そのパドルの位置を取得
        var position = transform.position;
        
        // もう少し上に移動
        position.y += 0.5f;
        
        // ボールの位置を設定
        newBall.transform.position = position;
    }
}

3. ボールが多すぎる

このゲームでは、ボールは1個までの制限があるので、今の作りだとボールが多すぎる。これを修正するには、現在生きているボールの数を追跡すれば良いでしょう。

BallShooter を編集し、実行中に生成したボールの数を数えましょう。

using UnityEngine;

// ボールを生成するスクリプト
public class BallShooter : MonoBehaviour
{
    // Unityでボールのプレハブを指定
    [SerializeField]
    GameObject ballPrefab;

    // 現在生きているボールの数
    private int _ballCounter;
    
    void Update()
    {
        // もしも左ボタンをクリックしたら…
        // 「かつ」
        // ボールの数は1未満だったら
        if (Input.GetMouseButtonDown(0) && _ballCounter < 1)
        {
            // 「Instantiate」を使い、プレハブから実物(インスタンス)を作成
            // newBallは新しく作ったボールの参照である
            var newBall = Instantiate(ballPrefab); 
            
            // このスクリプトはパドルにアタッチされているので、
            // そのパドルの位置を取得
            var position = transform.position;
            
            // もう少し上に移動
            position.y += 0.5f;
            
            // ボールの位置を設定
            newBall.transform.position = position;

            // ボールを数える
            _ballCounter++;
        }
    }
}

4. ボールがなくなっても、次のボールを生成できない?

新しい問題が現れた!確か、ボール1個までの制限ができたが、ボールがなくなっても、次のボールを作れない!実は、ボールがなくなっていない!

実行中にシーンビューを確認しましょう:

image.png

カメラで見えないだけで、ヒエラルキーにも、画面にもボールがあることが確認できる。これを解決するには「トリガー」を使いましょう。画面の真下に透明なトリガーを作成し、ボールが通過したら、ボールを削除し、ボールが失ったことをパドルにお知らせしましょう。

まず、シーンをの設定。backgroundの中に、空のゲームオブジェクトを作成し、BoxCollider2Dを追加してください。サイズが、画面の下のすべてをカバーするようにしてください。

「Is Trigger」のチェックを忘れずに!

image.png

次「ボールが通過したら、ボール削除し、プレーヤーに通知する」処理を追加しましょう。新しい「BallDestroy」スクリプトを作成してください。

using UnityEngine;

// ボールを削除し、お知らせする
public class BallDestroy : MonoBehaviour
{
    // BallShooterに知らせる!
    [SerializeField] 
    private BallShooter ballShooter; 
    
    // トリガーの中に何か入ったら:
    private void OnTriggerEnter2D(Collider2D other)
    {
        // ゲームオブジェクトを廃止(削除)する
        Destroy(other.gameObject);

        // メソッドを呼び出し、知らせる
        ballShooter.BallDestroyed();
    }
}

これで実現できるが、BallShooter 側でお知らせを受け取るメソッド「BallDestroyed」をまだ作っていないので、追加しましょう:

using UnityEngine;

// ボールを生成するスクリプト
public class BallShooter : MonoBehaviour
{
    // 省略(変わりがない)

    // ボール廃止されたときに呼び出される
    // 他のスクリプトから呼び出されるので、「public」アクセスにしましょう
    public void BallDestroyed()
    {
        // 最低はゼロであることを確かめる
        if (_ballCounter > 0)
        {
            // 数を減らす
            _ballCounter--;
        }
    }
    
    void Update()
    {
        // 省略(変わりがない)
    }
}

最後、シーンのトリガーに「BallDestroy」スクリプトを追加し、Inspector での必要な連携を設定してください:

image.png

これで実行して、確認してみましょう!

ブロックのプレハブ

つぎ、ブロックのプレハブを作成し、ボールが当たったら消える処理を作りましょう。

画像の前準備

ブロックの画像(スプライト)は、他のスプライトと比べて、1つではなく、同じ画像に多数が一緒にまとまっている。必要なメモリーの使用量を最適化するためにゲーム開発の中でよくある作戦である。

ただし、まとまったままで使えないので、先に分割しないといけない。このため、Unityは「Sprite Editor」(スプライトエディタ)がある。

まず、ブロック画像を選択し、Inspectorで「Open Sprite Editor」を選択してください:

image.png

ここで、画像を各スプライトに分割ができる。手動で1個ずつ個別に区切りをつける場合もあるが、今回はきれいに揃えているので自動的に分割可能である。

「行列分割」を使い、4x4の行列で分割してもらう。「Slice」メニューで「Automatic」から「Grid by Cell Count」に変えて、4行と4列にして、最後に「Slice」を押してください。

image.png

これで、各ブロックを分割できたので、最後に適用するには「Apply」ボタンを押してください。プロジェクトビューで各スプライトを確認ができる(画像の ▷ を押せば出る)

image.png

プレハブ作成

お好みの色のブロックを選んで、ボールと同じようにドラッグドロップでシーンに追加してください。名前は「Brick」にしましょう。

image.png

当たり判定の処理を行うため、BoxCollider2D を追加してください。その後、「ボールがブロックが衝突したら、ブロックが消える」というスクリプト「BlockDestroy」を作成しましょう。

using UnityEngine;

// ボールが当たったら、ブロックを消す処理
public class BlockDestroy : MonoBehaviour
{
    // 衝突したら…
    private void OnCollisionEnter2D(Collision2D other)
    {
        // 自分のことを削除
        Destroy(gameObject);
    }
}

image.png

プレハブ化にするのは、ボールと同様にヒエラルキーから「Prefab」フォルダーへドラッグドロップするだけ

image.png

確認のために、手動で「Prefab」フォルダーから、シーンにいくつかのブロックを適当に追加してみましょう:

image.png

自動的にブロックを配置しよう~

お試しで手動でいくつかのブロックを配置したが、なかなか面倒な作業なので、自動的に配置しましょう。

スクリプト

まず、手動で配置したブロックのすべてを消しましょう。その後、新しいスクリプト「BlockManager」を作成しましょう。このスクリプトは:

image.png

まずは、変数から始めましょう:

スクリプトの変数

using UnityEngine;

// ブロックの配置を管理する
public class BlockManager : MonoBehaviour
{
    // ブロックのプレハブ
    [SerializeField] 
    private GameObject blockPrefab;

    // 行の数
    [SerializeField] 
    private int rows;

    // 列の数
    [SerializeField] 
    private int cols;
}

初期化 -  Start()

ここで、実際の配置しまほう。始まる位置はシーン上で適切に設定し、1個目のブロックを生成しましょう

void Start()
{
    // 始まる位置は自分の位置にする
    Vector3 start = transform.position;
    
    // 1個目を配置
    GameObject copy = Instantiate(blockPrefab);
    copy.transform.position = start;
}

確認

ヒエラルキーで空のゲームオブジェクトを作成し、「BlockManager」スクリプトをアタッチしてください。なお、このゲームオブジェクトの位置は、ブロックが現れる位置になること確認してください~

image.png

連続に配置:縦と横の「for文」

2個目を配置

つぎ、2個目を配置してみましょう。Y軸(縦軸)の位置は同じが X軸(横軸)のずれはどれぐらいになるの?

image.png

よく考えれば、この距離がちょうど1ブロックの幅であることがわかる。この数値は、スプライトから取得できるので、SpriteRenderer を取得し、そのスプライトのサイズを取っておきましょう。Start() を編集して…

void Start()
{
    // プレハブのSpriteRendererを求める
    SpriteRenderer spriteRenderer = blockPrefab.GetComponent<SpriteRenderer>();
    
    // そこからspriteのサイズを取得
    Vector2 blockSize = spriteRenderer.size;
    
    // 始まる位置は自分の位置にする
    Vector2 start = transform.position;
    
    // 1個目を配置
    GameObject copy = Instantiate(blockPrefab);
    copy.transform.position = start;
    
    // 2個目も配置. Y軸は同じで、X軸を1ブロック幅をずれる
    // オフセット:すらず量のこと
    Vector2 offset = new Vector2(blockSize.x, 0);
    copy = Instantiate(blockPrefab);
    copy.transform.position = start + offset;
}

1行目を配置

1個ずつを配置するのが大変面倒な作業なので、繰り返す命令「for」を使い、連続に1行を配置しましょう。すらず量は

for文の繰り返しに使う変数をうまく使えば、簡単に1行目を配置できる。もう一度 Start() を編集し、forで1行目を作ろう:

void Start()
{
    // プレハブのSpriteRendererを求める
    SpriteRenderer spriteRenderer = blockPrefab.GetComponent<SpriteRenderer>();
    
    // そこからspriteのサイズを取得
    Vector2 blockSize = spriteRenderer.size;
    
    // 始まる位置は自分の位置にする
    Vector2 start = transform.position;
    
    // 1行目目を配置(列数のブロックを配置)
    for (int i = 0; i < cols; i++)
    {
        // すらず量(ブロック幅 × i)
        Vector2 offset = new Vector2(blockSize.x * i, 0);

        // インスタンスを作成し、配置
        GameObject copy = Instantiate(blockPrefab);
        copy.transform.position = start + offset;
    }
}

Inspectorで「cols」(列の数)の設定を忘れずに!

image.png

2行目からを配置

1行が完成したので、これを「行数」を繰り返せば、すべてのブロックを配置ができる。つまり、for文の中にさらにfor文が必要(1行の処理×行数)

void Start()
{
    // プレハブのSpriteRendererを求める
    SpriteRenderer spriteRenderer = blockPrefab.GetComponent<SpriteRenderer>();
    
    // そこからspriteのサイズを取得
    Vector2 blockSize = spriteRenderer.size;
    
    // 始まる位置は自分の位置にする
    Vector2 start = transform.position;
    
    // 各行を表示
    for (int j = 0; j < rows; j++)
    {
        // 各列を表示
        for (int i = 0; i < cols; i++)
        {
            // すらず量(ブロック幅 × i, ブロック高さ × j)
            // y軸の場合は↓の方に進むので、「-」を付ける!
            Vector2 offset = new Vector2(blockSize.x * i, -blockSize.y * j);

            // インスタンスを作成し、配置
            GameObject copy = Instantiate(blockPrefab);
            copy.transform.position = start + offset;
        }
    }
}

Inspectorで「rows」数値を設定し、確認してください:

image.png

 

ボール跳ね返す角度をコントロールする

目的

今の作りだと、ボールがパドルを当たると、入射角を反転し、外向角になるだけで、プレーヤーがボール行かせたい方位を変えることができない。ある程度、ボールを行き先をコントロールできるように、パドルの「当たる部分」によって、跳ね返す向きを調整する処理を入れましょう。

image.png

これで、ボールがパドルの左側に当たると、ボールが左に行く。真ん中を当たると、真上に返す。右側を当たると、右に行くようにする。

解決のアイディア

ボールとパドルの相対位置がポイント。ボールが衝突したら、パドルの位置とボールの位置を使い、その向きに返せば良いでしょう:

image.png

スクリプト

ボールを跳ね返す責任を持つ「BallRebound」を作成しましょう。衝突に時(OnCollisionEnter2D)ボールの速度の向きを変えれば良いでしょう。

// ボールの跳ね返す角度を制御する
public class BallRebound : MonoBehaviour
{
    // 衝突のとき
    private void OnCollisionEnter2D(Collision2D other)
    {
        // "other" がボールでしょう
        // なので、RigidBodyを取得し、速度を変えましょう
        Rigidbody2D rb = other.gameObject.GetComponent<Rigidbody2D>();
        
        // 速度ベクトルの長さを保持するので、求めましょう
        float speed = rb.linearVelocity.magnitude;
        
        // 新しい向きは、ボールの位置とパドルの位置の差分で求める
        Vector2 direction = other.transform.position - transform.position;
        
        // 長さが1(単位ベクトル)にする
        direction.Normalize();
        
        // 新しい速度は、前の速度(量)× 新しい向きにする
        rb.linearVelocity = direction * speed;
    }
}

このスクリプトを「player」にアタッチし、試してみると、ボールをうまくコントロールできるようになったが、パドルが壁とぶつけると:

image.png

image.png

のエラーが発生してしまう。

エラーの解決:タグを使用

今の処理だと「衝突したら」だけなので、何と衝突するのかを確認していない。ボールの場合は、ボールのリジッドボディを取得し、速度を問題なく変えられるが、壁と衝突するとバグってしまう。

なぜなら、壁がリジッドボディがなく、GetComponent<Rigidbody2D>() を呼び出すが失敗してしまうから。

なので、衝突したときに、何と衝突したのかを「タグ」で確認しましょう。まず、スクリプトを修正し、「Ball」タグがなければ、何も処理しない部分を追加しましょう。

// ボールの跳ね返す角度を制御する
public class BallRebound : MonoBehaviour
{
    // 衝突のとき
    private void OnCollisionEnter2D(Collision2D other)
    {
        // "other"が "Ball"タグがないか?
        if (!other.gameObject.CompareTag("Ball"))
        {
            return; // メソッドがここで終わり
        }
        
        // "other" がボールでしょう
        // なので、RigidBodyを取得し、速度を変えましょう
        Rigidbody2D rb = other.gameObject.GetComponent<Rigidbody2D>();
        
        // 速度ベクトルの長さを保持するので、求めましょう
        float speed = rb.linearVelocity.magnitude;
        
        // 新しい向きは、ボールの位置とパドルの位置の差分で求める
        Vector2 direction = other.transform.position - transform.position;
        
        // 長さが1(単位ベクトル)にする
        direction.Normalize();
        
        // 新しい速度は、前の速度(量)× 新しい向きにする
        rb.linearVelocity = direction * speed;
    }
}

そして、ボールのプレハブを編集し、「Ball」タグをつけましょう(小文字・大文字の注意を!):

image.png

これで問題解決!

ゲーム終了処理

次、「ゲームクリア」と「ゲームオーバー」の処理を追加しましょう。

ゲームオーバー

このゲームでは、残機3つがあり、ボールが失ったら残機1つが減る。残機がゼロになった場合は「ゲームオーバー」にする。すでに「ボールが失ったら…」(BallDestroy)という処理があるので、これを活用できる。

ゲーム監督クラス「GameManager」を作成し、残機の数を追跡しましょう。BallDestroyのとき、GameManagerを呼び出し、残機を減らす処理を実装してみましょう:

// ゲーム監督
public class GameManager : MonoBehaviour
{
    // 残機の数
    private int _lives;

    private void Start()
    {
        _lives = 3; // 3個で始まる
    }
    
    // ボール廃止されたときに呼び出される
    public void BallDestroyed()
    {
        // 残機が減る
        _lives--;
        if (_lives == 0)
        {
            // とりあえず、コンソールで表示
            Debug.Log("ゲームオーバー");
        }
    }
}

そして、BallDestroy 側では…

// ボールを削除し、お知らせする
public class BallDestroy : MonoBehaviour
{
    // BallShooterに知らせる!
    [SerializeField] 
    private BallShooter ballShooter; 
    
    // GameManagerにも知らせる!
    [SerializeField] 
    private GameManager gameManager;
    
    // トリガーの中に何か入ったら:
    private void OnTriggerEnter2D(Collider2D other)
    {
        // ゲームオブジェクトを廃止(削除)する
        Destroy(other.gameObject);

        // メソッドを呼び出し、知らせる
        ballShooter.BallDestroyed();
        gameManager.BallDestroyed();
    }
}

シーンにゲーム監督スクリプトを新しいゲームオブジェクトにアタッチし、BallDestroy の参照を付けてください:

image.png

これで3つのボールをなくして、「ゲームオーバー」になることを確認してください。

BallShooter を止める!

ゲームオーバーになっても、次々にボールを投げることができる!これではいけないので、GameManager 側でゲームが終わったことがわかれば、BallShooter を無効化しましょう:

GameManager に Inspector で参照を付けれるようにする:

// ゲーム監督
public class GameManager : MonoBehaviour
{
    // ボールを投げる処理
    [SerializeField] 
    private BallShooter ballShooter;

    // あとは同じ…
}

そして、ゲームオーバーの時:

if (_lives == 0)
{
    // とりあえず、コンソールで表示
    Debug.Log("ゲームオーバー");

    // スクリプトを無効化する
    ballShooter.enabled = false;
}

Unityで参照を設定し、実行してみてください。

ゲームクリア

このゲームでは、すべてのブロックを削除できれば、ゲームクリアとする。ただし、GameManager がブロックが存在することわからず、実行中生成されるものであるため、Inspector上での参照を付けることができない!

BlockManager がブロックを生成するので、新しいブロックを生成した場合は、BlockDestroyGameManager に通知するように、スクリプトの中で連携を作れば良いでしょう!

image.png

GameManager

まず GameManager から行きましょう。生成された数を受け取るメソッドとブロック廃止の時に呼び出されるメソッドを準備しましょう:

// ゲーム監督
public class GameManager : MonoBehaviour
{
    // ボールを投げる処理
    [SerializeField] 
    private BallShooter ballShooter;
    
    // 残機の数
    private int _lives;

    // ブロックの数
    private int _blockCount;

    // Start() と BallDestroyed() はそのまま
  
    // ブロックの数を設定
    public void SetBlockCount(int count)
    {
        _blockCount = count;
    }
    
    // ブロック廃止のとき呼び出される
    public void BlockDestroyed()
    {
        _blockCount--;
        if (_blockCount == 0)
        {
            // コンソールで表示し、ボール投げれなくなる
            Debug.Log("ゲームクリア");
            ballShooter.enabled = false;
        }
    }
}

BlockDestroy

つぎ、ブロック廃止のときに、GameManager に連絡しましょう:

// ボールが当たったら、ブロックを消す処理
public class BlockDestroy : MonoBehaviour
{
    // ゲーム監督の参照
    private GameManager _gameManager;
    
    // ゲーム監督の参照を設定
    public void SetGameManager(GameManager gameManager)
    {
        _gameManager = gameManager;
    }
    
    // 衝突したら…
    private void OnCollisionEnter2D(Collision2D other)
    {
        // 自分のことを削除
        Destroy(gameObject);
        
        // ゲーム監督に通知
        _gameManager.BlockDestroyed();
    }
}

BlockManager

最後、中間のスクリプトですべてを繋げましょう:

// ブロックの配置を管理する
public class BlockManager : MonoBehaviour
{
    // ブロックのプレハブ
    [SerializeField] 
    private GameObject blockPrefab;

    // ゲーム監督クラスの参照をInspectorで設定
    [SerializeField] 
    private GameManager gameManager;
    
    // 行の数
    [SerializeField] 
    private int rows;

    // 列の数
    [SerializeField] 
    private int cols;

    void Start()
    {
        // プレハブのSpriteRendererを求める
        SpriteRenderer spriteRenderer = blockPrefab.GetComponent<SpriteRenderer>();
        
        // そこからspriteのサイズを取得
        Vector2 blockSize = spriteRenderer.size;
        
        // 始まる位置は自分の位置にする
        Vector2 start = transform.position;
        
        // 各行を表示
        for (int j = 0; j < rows; j++)
        {
            // 各列を表示
            for (int i = 0; i < cols; i++)
            {
                // すらず量(ブロック幅 × i, ブロック高さ × j)
                // y軸の場合は↓の方に進むので、「-」を付ける!
                Vector2 offset = new Vector2(blockSize.x * i, -blockSize.y * j);

                // インスタンスを作成し、配置
                GameObject copy = Instantiate(blockPrefab);
                copy.transform.position = start + offset;
                
                // ブロック廃止スクリプトにGameManagerの参照を渡す
                BlockDestroy blockDestroy = copy.GetComponent<BlockDestroy>();
                blockDestroy.SetGameManager(gameManager);
            }
        }
        
        // ゲーム監督に生成したブロック数を教える
        gameManager.SetBlockCount(rows * cols);
    }
}

最後に、Unityで参照を設定し、確認しましょう!

image.png

※:確認しにくい場合は、一旦ゲームオーバーの処理をコメントアウトでなしにして、無制限にボールを使えるようにしてください。

UIを作成

ゲームが動くが、画面になにも情報がなく、Canvas を使い、ゲームの状況を可視化しましょう。

Canvasの準備

まず、Canvas を作成し、残機とスコアの表示場所を作りましょう。

スコア

スコアは画面の右上に表示しよう。テキストをを表示するので「TextMeshPro」で良いでしょう。必ず右上に出したいので、アンカーが画面の右上を追跡するようにしてください。位置を文字のサイズを調整し、とりあえず、「SCORE: 00000」を表示するようにしてください:

image.png

image.png

残機

残機は、スプライトの表示・非表示で作りましょう。とりあえず、最大残機5個までにし、画面の右下に配置しましょう:

image.png

ここでヒエラルキーの順番を注目してください

スクリプト

1つのスクリプト=1つの責任の考えを従って、UIの管理するのは「UIManager」にお任せしましょう。ゲーム監督 GameManagerUIManager を操作し、ゲームの情報を画面上で表示してもらう。

スコア

新しい UIManager スクリプトを作成し、スコアの「TextMeshPro」を Inspector で設定できるようにし、中身を変えられるメソッドを実装しましょう:

using TMPro;
using UnityEngine;

// UIを管理するスクリプト
public class UIManager : MonoBehaviour
{
    // スコア用のテキスト
    [SerializeField] 
    private TextMeshProUGUI scoreText;
    
    // GameManagerからスコアを設定できるようにする
    public void SetScore(int score)
    {
        scoreText.text = $"SCORE: {score}";
    }
}

そして、GameManager を更新し、スコアを数え、表示しましょう。一応、ブロック1個消えたら、100点を与えることにしましょう。すでにブロックが廃止の時に呼ばれるメソッド「BlockDestroyed」があるので、そこで点数を増やせば良いでしょう。

まず、GameManager のメンバー変数で:

// ゲーム監督
public class GameManager : MonoBehaviour
{
    // ボールを投げる処理
    [SerializeField] 
    private BallShooter ballShooter;
    
    // 残機の数
    private int _lives;

    // ブロックの数
    private int _blockCount;

    // スコア
    private int _score;

    // 後は同じ
}

そして、BlockDestroyed を編集し、スコアを更新:

// ブロック廃止のとき呼び出される
public void BlockDestroyed()
{
    // 100点を与える
    _score += 100;
    
    _blockCount--;
    if (_blockCount == 0)
    {
        // コンソールで表示し、ボール投げれなくなる
        Debug.Log("ゲームクリア");
        ballShooter.enabled = false;
    }
}

UIManagerとの連携

スコアを更新しているので、次に UIManager に表示するように命令する。参照が必要であるため、GameManagerUIManager の紐づけできるようにする:

// ゲーム監督
public class GameManager : MonoBehaviour
{
    // ボールを投げる処理
    [SerializeField] 
    private BallShooter ballShooter;
    
    // ゲームの情報を表示できるように
    [SerializeField] 
    private UIManager uiManager;

    // 後は同じ
}

そして、スコアを更新したら、UIの更新を UIManager に頼む

// ブロック廃止のとき呼び出される
public void BlockDestroyed()
{
    // 100点を与える
    _score += 100;
    
    // UIを更新
    uiManager.SetScore(_score);
    
    // 後は同じ
}

Unityでの確認

スクリプトができたので、Unity のシーンを更新しましょう。UIManager は UI を管理するので、Canvas にアタッチするのは適切と思われる。スコアの TextMeshPro を設定してね~

image.png

そして、GameManagerUIManager の参照を設定~

image.png

これで確認ができる!

残機

UIManager で ゲームオブジェクトの表示・非表示することによって、プレーヤーに残機の数を教えましょう。

まず、player 0 ~ player 4 の5個のゲームオブジェクトがどこにあるのかを UIManager に教えましょう:

// UIを管理するスクリプト
public class UIManager : MonoBehaviour
{
    // スコア用のテキスト
    [SerializeField] 
    private TextMeshProUGUI scoreText;
    
    // 残機表示用
    [SerializeField] 
    private GameObject [] playerImages;

    // 後は同じ
}

そして、SetScore と同じように、GameManager からアクセスができる SetLives メソッドを作ろう:

// GameManagerから残機を設定できるようにする
public void SetLives(int lives)
{
    // ゲームオブジェクトを1個ずつに繰り返す
    for (int i = 0; i < playerImages.Length; i++)
    {
        // i番目の残機画像を表示するか?
        bool isAlive = i < lives;
        playerImages[i].SetActive(isAlive);
    }
}

最後に、ゲームが始まるときと、残機が減るときに SetLives で残機の表示を更新しよう:

private void Start()
{
    _lives = 3; // 3個で始まる
    uiManager.SetLives(_lives);
}

// ボール廃止されたときに呼び出される
public void BallDestroyed()
{
    // 残機が減る
    _lives--;
    uiManager.SetLives(_lives);
    if (_lives == 0)
    {
        // とりあえず、コンソールで表示
        Debug.Log("ゲームオーバー");

        // スクリプトを無効化する
        ballShooter.enabled = false;
    }
}

Unityでの確認

UIManager で5個のゲームオブジェクトを設定じ、実行してみてください。

image.png

ゲームオーバー UI

現状、ゲームが終わるとき、Debug.Log でメッセージを表示しているが、その代わりに Canvas で「ゲームオーバー」を表示し、「さいかい」と「おわり」のボタンを実装しましょう!

シーンの準備

スプライト分割

まず、ブロックと同様に、画像を分解しないといけない。ただし、行列ではないので、きれいに縦横で分割できない。この場合は、マウスを使い、各スプライトの領域を定義しましょう:

image.png

3つのスクリプトを分割したら、「Apply」ボタンを押し、シーンを編集しましょう

Canvasの準備

すでに作ったゲーム用の Canvas は別目的なので、ゲームオーバー専用の Canvas を新しく作ろう。そして、Image 1つと Image + Button 2つを追加してください:

image.png

アニメーションを作成

GameOver オブジェクトに Animator コンポーネントを追加し、アニメーションを作成しましょう。ここは好きなように作成しても問題ない

image.png

なお、アニメーションを作成した後、無限再生(ループ)を無効化しましょう。Project フォルダーでアニメーションのファイルを探し、クリックしたら、Inspectorで「Loop Time」のチェックボックスからチェックを外してください:

image.pngスクリプト

GameOverManager を作成し、表示「Show」と非表示「Hide」のメソッドを作成し、「Show」の時は、アニメーションを再生しましょう:

// GameOverを表示・非表示する
public class GameOverManager : MonoBehaviour
{
    // アニメーション再生・停止用
    [SerializeField] 
    private Animator animator;

    // 表示
    public void Show()
    {
        // 定めたアニメーション再生
        // 大文字・小文字を確認!!
        gameObject.SetActive(true);
        animator.Play("ShowGameOver"); 
    }
    
    // 非表示
    public void Hide()
    {
        animator.StopPlayback();
        gameObject.SetActive(false);
    }
}

そして、GameManager 側では…

// ゲーム監督
public class GameManager : MonoBehaviour
{
    // ボールを投げる処理
    [SerializeField] 
    private BallShooter ballShooter;
    
    // ゲームの情報を表示できるように
    [SerializeField] 
    private UIManager uiManager;
    
    // ゲームオーバーの表示・非表示用
    [SerializeField]
    private GameOverManager gameOverManager;

    // 後は同じ
}

で、ゲームが始まったら、ゲームオーバーを非表示し、ゲームオーバー(またはゲームクリア)の時はまた表示すれば良いでしょう:

private void Start()
{
    _lives = 3; // 3個で始まる
    uiManager.SetLives(_lives);
          
    // ゲームオーバー非表示
    gameOverManager.Hide();
}

// ボール廃止されたときに呼び出される
public void BallDestroyed()
{
    // 残機が減る
    _lives--;
    uiManager.SetLives(_lives);
    if (_lives == 0)
    {
        // ゲームオーバーを表示
        gameOverManager.Show();

        // スクリプトを無効化する
        ballShooter.enabled = false;
    }
}

// ブロック廃止のとき呼び出される
public void BlockDestroyed()
{
    // 100点を与える
    _score += 100;
    
    // UIを更新
    uiManager.SetScore(_score);
    
    _blockCount--;
    if (_blockCount == 0)
    {
        // ゲームオーバーを表示
        gameOverManager.Show();
        ballShooter.enabled = false;
    }
}

Unityで確認

GameOverManager のスクリプトを GameOver にアタッチし、GameManager との紐づけできてたら、実行して、確認しましょう!

image.png

ボタンを実装

ボタン(Button)はを押したら、ゲーム監督 GameManager を呼び出し、ゲームを再開するのか、それともゲーム終了するなのかを実装しましょう。

Button から GameManager を呼び出すので、メソッドは「public」にしましょう! それでは、GameManager に新しいメソッドを追加してください:

// ゲーム再開ボタンの処理
public void OnGameRestart()
{
    SceneManager.LoadScene("Game");
}

// ゲーム終了ボタンの処理
public void OnGameEnd()
{
    // 独立に実行ファイルの時
    Application.Quit();
    
    // Unityの中で実行する場合
    EditorApplication.ExitPlaymode();
}

Buttonの設定

Buttonコンポーネントを見ると「OnClick」という項目がある。この項目が「ボタンが押したら何を呼び出すか?」という設定場所である。「+」ボタンを押し、新しい呼び出しを追加しましょう:

image.png

 3つの設定できる項目がある:

 ① ゲームオブジェクト:ボタンを押したら、このゲームオブジェクトを呼び出す。普通通りに、ヒエラルキーからドラッグドロップで設定ができる。
 ② 呼び出したいメソッド:①で選択したゲームオブジェクトのすべてのコンポーネントとスクリプトを表示され、呼び出したいメソッドを選択ができる。
 ③ 引数:メソッドは引数があれば、ここで設定ができる。引数がない場合は何も表示されない。

今回は、「さいかいボタンを押したら、GameManager オブジェクトの中にある GameManager.OnGameRestart() を呼び出したい」にしたいので、OnClick はこうなる:

image.png

「おわり」ボタンは:

image.png

これでもう一度確認しましょう。

サウンドを追加

ボールが何かと衝突、またはボールが失ったらサウンドを再生するようにしましょう。

ボールとパドル

新しい「BallHitSound」のスクリプトを作成し、以下の処理しましょう:

// ボールが衝突したら、サウンドを再生
public class BallHitSound : MonoBehaviour
{
    // 再生したいサウンド
    [SerializeField]
    private AudioClip clip;

    void OnCollisionEnter2D(Collision2D other) 
    {
        // タグは「Ball」?
        // ならば、サウンドを再生
        if (other.gameObject.CompareTag("Ball"))
            AudioSource.PlayClipAtPoint(clip, transform.position);
    }
}

このスクリプトを「パドル」の方にアタッチし、clip は 以下のようにし、実行してください

image.png

ボールと壁

同じスクリプトはそのまま再利用できるので、各壁(Wall Left, Wall Right, Wall Top)にアタッチし、同じ clip にしてください

ボールとブロック

ブロックはシーンにはないので、プレハブを編集しましょう。プレハブフォルダーの中にある Brick プレハブをダブルクリックすると、プレハブの編集ができる!

このプレハブにすでに作った BallHitSound スクリプトをアタッチし、今度は「BallReboundBrick」のサウンドを再生しましょう。

image.png

ボールが失うとき

ボール廃止スクリプトがすでに存在するので、編集しましょう。BallDestroy スクリプトを編集し、上記と同様に、サウンドの設定は Inspector でできるようにし、そして、ボールを廃止したら、そのサウンドを再生:

// ボールを削除し、お知らせする
public class BallDestroy : MonoBehaviour
{
    // BallShooterに知らせる!
    [SerializeField] 
    private BallShooter ballShooter; 
    
    // GameManagerにも知らせる!
    [SerializeField] 
    private GameManager gameManager;
    
    // 失ったとき再生するサウンド
    [SerializeField] 
    private AudioClip clip;
    
    // トリガーの中に何か入ったら:
    private void OnTriggerEnter2D(Collider2D other)
    {
        // ゲームオブジェクトを廃止(削除)する
        Destroy(other.gameObject);

        // サウンド再生
        AudioSource.PlayClipAtPoint(clip, transform.position);
        
        // メソッドを呼び出し、知らせる
        ballShooter.BallDestroyed();
        gameManager.BallDestroyed();
    }
}

Unityで BottomTrigger の BallDestroy スクリプトを編集し「BallLost」のサウンドを設定し、ゲームを実行しましょう。

image.png

BGM

最後、BGMを再生しましょう。今回はBGM曲は1個のみで、設定はとても単純である。新しいゲームオブジェクト「BGM」を作成し、「AudioSource」コンポーネントを追加してください。Audio Generator は「BGM」にしたら完成。「Play On Awake」のチェックが入っていれば、自動的に再生し始まるので、スクリプトは不要。

image.png

2つ以降の曲、またはエフェクト(クロスフェード)などが必要な場合は、専用のスクリプト(BGMManager)を作成してくださいね~

 

 

課題

ブロック崩しゲームに新たな機能を追加し、よりも面白くする!

例えば…

ルール

提出

提出するのは、プロジェクトの以下のフォルダのみ

また、「変更点.txt」を作成し、何を変えたのかを説明してください(速く見つけるため)

この3つのフォルダと「変更点.txt」をZIPファイルに圧縮し、提出フォルダにコピーしてください。