ブロック崩しゲームの制作、第3回です。今回はスコアの表示、タイトル画面、ゲームオーバー画面、ゲームクリア画面などなどを作っていきます。
- Mac mini (M1, 2020)
- Unity 2022.3.36f1
スコアを表示する
ゲームにスコアはつきものです。世のゲーマーたちはみなハイスコアを目指して日々無駄な時間を費やしているのです。
スコアを表示するためのオブジェクトを作ります。
ヒエラルキータブで右クリック「UI > Text – TextMeshPro」を選択します。オブジェクトの名前を「ScoreText」にします。
UIオブジェクトを作成するとCanvasが作成され、その下にUIオブジェクトが配置されます。で、Sceneタブで見たときにカメラの表示領域とキャンバスの表示領域が一致していないので、なんかモヤモヤします。
このままでもゲーム画面では正常に表示されるのですが、開発中にUIの位置を決めづらいので、カメラの表示領域をキャンバスに合わせます。
Canvasのインスペクターで「Canvas > Render Mode」を「Screen Space – Camera」にします。
「Render Camera」にヒエラルキーの「Main Camera」をドラッグ&ドロップします。
これで、カメラ領域とキャンバス領域が重なりました。いえい。
ScoreTextオブジェクトはTextMesh Proなので、TMPの設定をします。メニューバーの「Window > TextMeshPro > Font Asset Creator」を選択して、フォントアセットを作成しました。
今回は欧文フォントを使うのですが、日本語フォントのときと同じように設定したらうまくいきませんでした。文字に白い膜がかかったようになってしまいます。
いろいろ試した結果、以下の設定でうまくいきました。
ポイントは「Atlas Resolution」の値です。日本語フォントの場合は4096*4096だったのですが、欧文フォントではもっと小さくしたほうがいいっぽいです。扱う文字数に応じて適した値があるのかな。UnityのドキュメントではCharacter SetがASCIIならば512*512が適切とあります。
空のオブジェクトを作成します。ヒエラルキータブで右クリック「Create Empty」を選択します。
名前を「GameManager」にします。
C#スクリプトGameManager.csを作成して、オブジェクトGameManagerにアタッチします。GameManager.csの内容は次のとおりです。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;
public class GameManager : MonoBehaviour
{
public int score = 0;
public int combo = 0;
public TextMeshProUGUI scoreText; // スコアを表示するテキスト
// スコア加算
public void AddScore()
{
score += 10;
combo++;
// コンボボーナス
if (combo >= 2)
score += combo - 1;
string formattedScore = score.ToString("D8"); // スコアを8桁の文字列に変換
scoreText.text = formattedScore;
}
// コンボのリセット
public void ResetCombo()
{
combo = 0;
}
}
Unityで、GameManagerオブジェクトのScore TextにScoreTextオブジェクトを割り当てます。
スクリプトBlockController.csを修正します。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BlockController : MonoBehaviour
{
public GameManager gameManager; // GameManager スクリプトへの参照
void Start()
{
}
void Update()
{
}
void OnCollisionEnter2D(Collision2D collision)
{
if (collision.gameObject.tag == "Ball") // 衝突したオブジェクトのタグが Ball なら
{
Destroy(gameObject); // 自身を削除
gameManager.AddScore(); // スコア加算
}
}
}
Unityで、BlockオブジェクトのGame ManagerにGameManagerオブジェクトを割り当てます。
スクリプトPlayerController.csを修正します。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerController : MonoBehaviour
{
...省略...
public GameManager gameManager; // GameManagerへの参照
void Update()
{
...省略...
}
void OnCollisionEnter2D(Collision2D collision)
{
if (collision.gameObject.tag == "Ball") // ボールと衝突したら
{
gameManager.ResetCombo(); // コンボをリセット
}
}
}
Unityで、PlayerオブジェクトのGame ManagerにGameManagerオブジェクトを割り当てます。
以上で、ブロックを1つ消したら+10点、コンボボーナスは+1点、ボールがプレイヤーに接触したらコンボはリセットの仕組みが完成しました。
ブロックを動かす
ブロックが少しずつ下に下がっていくという機能を実装します。
BlockController.csを修正します。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BlockController : MonoBehaviour
{
...省略...
private float downSpeed = 0.03f; // ブロックが下がる速度
void Update()
{
transform.Translate(Vector2.down * downSpeed * Time.deltaTime); // ブロックをゆっくり下げる
}
...省略...
}
transform.Translate()
でオブジェクトの位置を動かします。
Vactor2.down
は下方向を表すベクトル、downSpeed
はブロックが1秒間にどれだけ下がるか、これらを掛け算して移動する方向と速度を計算しています。
Time.deltaTime
は1フレーム間の経過時間を表す変数で、これを掛けることによって常に一定とは限らないフレームレートの変動に対応します。PCの性能やゲームの負荷によってフレームレートが変わってしまう場合でも、一定の速度でブロックを動かすということね(わかったつもり)。
タイトル画面を作る
タイトル画面を作ります。タイトル用のシーンを作って、シーンの切り替えでゲーム画面に遷移するようにします。
Assets/Scenesフォルダの下で右クリック「Create > Scene」を選択します。新しいシーンが作成されるので名前を「TitleScene」に変更します。
TitleSceneをダブルクリックします。
TitleSceneに切り替わったら、必要なオブジェクトを配置します。とりあえず、タイトルとスタートボタンを作りました。
ヒエラルキータブで右クリック「Create Empty」を選択して、空のオブジェクトを作成します。名前を「TitleManager」にします。
Assets/Scripts下にC#スクリプト TitleManager.cs を作成して TitleManager オブジェクトにアタッチします。 TitleManager.cs の内容は次のとおりです。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class TitleManager : MonoBehaviour
{
public void StartGame()
{
SceneManager.LoadScene("MainScene"); // MainSceneを表示
}
}
using UnityEngine.SceneManagement;
で名前空間の追加。
SceneManager.LoadScene()
で指定したシーンをロードします。
スタートボタンを押したときのアクションを設定します。
スタートボタンのインスペクターの「Button > On Click ()」で「+」をクリックして新しいイベントを作成します。「None(Object)」を「TitleManager」に「No Function」を「TitleManager.StartGame」に設定します。
これでスタートボタンをクリックしたときに TitleManager.cs の StartGame()
関数が実行されるようになりました。
1つのゲームに複数のシーンがある場合、ゲームを起動したときに最初に表示されるのはどのシーンか問題があります。この問題を解決しましょう。
メニューバーの「File > Build Settings…」を選択して Build Settings ウインドウを開きます。「Add Open Scenes」ボタンをクリックするとシーンの一覧画面にすべてのシーンが表示されます。
TitleSceneをドラッグ&ドロップしてリストのいちばん上に持っていきます。このリストのいちばん上にあるのがゲームを起動したときに表示されるシーンです。
ゲームオーバー画面を作る
ゲームオーバー画面のオブジェクトを作ります。
MainSceneに戻って、ヒエラルキーのCanvas上で右クリック「UI > Panel」を選択します。名前を「GameOver」にします。
各種オブジェクトを配置します。「GameOverText」、「RetryButton」、「BackToTitleButton」の3つのオブジェクトを作成しました。
ボールが下の壁(WallBottom)に衝突したらゲームオーバーになるようにします。
BallController.csを修正します。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BallController : MonoBehaviour
{
...省略...
public GameManager gameManager; // GameManagerへの参照
...省略...
void OnCollisionEnter2D(Collision2D collision)
{
...省略...
if (collision.gameObject.name == "WallBottom") // ボールがWallBottomに当たった場合
{
gameManager.GameOver(); // ゲームオーバー
}
}
}
GameManager.csを修正します。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using TMPro;
public class GameManager : MonoBehaviour
{
public GameObject ball; // Ballオブジェクト
public GameObject player; // Playerオブジェクト
public GameObject blocks; // Blocksオブジェクト
public GameObject gameOver; // GameOverオブジェクト
...省略...
// ゲームオーバー
public void GameOver()
{
Time.timeScale = 0; // ゲームを一時停止
gameOver.SetActive(true); // ゲームオーバー画面を表示
ball.SetActive(false); // ボールを非表示
player.SetActive(false); // プレイヤーを非表示
blocks.SetActive(false); // ブロックを非表示
}
// リトライ
public void RetryGame()
{
SceneManager.LoadScene("MainScene");
Time.timeScale = 1; // ゲーム時間を動かす
}
// タイトルに戻る
public void BackToTitle()
{
SceneManager.LoadScene("TitleScene");
}
...省略...
}
GameOver()
関数では、Time.timeScale = 0
でゲーム内の時間を止めます。ゲームオーバー画面を表示し、それ以外のオブジェクトを非表示にします。
RetryGame()
関数では、ゲーム画面を再読込して、一時停止していたゲーム時間を再開します。
BackToTitle()
関数は、タイトル画面をロードします。このとき、タイトル画面からゲームをスタートするとゲーム時間が止まったままになってしまうので、TitleManager.csを修正してゲーム時間を動かしてあげます。
public class TitleManager : MonoBehaviour
{
public void StartGame()
{
SceneManager.LoadScene("MainScene"); // MainSceneを表示
Time.timeScale = 1; // ゲーム時間を動かす
}
}
ブロックが下の壁に衝突したときもゲームオーバーにしたいですね。
その場合、BlockオブジェクトにRigidbody 2Dコンポーネントを追加する必要があります。でもそうすると、ゲームバランスが崩れてしまうんですね。実際にやってみたらブロックが柔らかくなってしまって、ボールが当たったときに跳ね返らなくなってしまいました。
つーわけで、当たり判定を使わずに、ブロックがゲーム画面の外に出たらゲームオーバーとなるようにBlockController.csスクリプトを修正します。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BlockController : MonoBehaviour
{
...省略...
private Camera mainCamera; // メインカメラ
void Start()
{
mainCamera = Camera.main; // Main Cameraを取得
}
void Update()
{
...省略...
float cameraBottom = mainCamera.ViewportToWorldPoint(new Vector3(0, 0, 0)).y; // 画面の下端のy座標を取得
if (transform.position.y < cameraBottom) // ブロックが画面の外に出たら
{
gameManager.GameOver(); // ゲームオーバー
}
}
...省略...
}
リトライボタンとタイトルへ戻るボタンをクリックしたときのアクションを設定します。
RetryButtonオブジェクトのインスペクターの「Button > On Click ()」で新しいイベントを作成して、「None(Object)」を「GameManager」に「No Function」を「GameManager.RetryGame」に設定します。
同じようにして、BackToTitleButtonは「GameManager.BackToTitle」に設定します。
GameOverオブジェクトはゲーム開始時には表示させないので、インスペークターのチェックを外して非表示にしておきます。
ゲームクリア画面を作る
ゲームクリア画面のオブジェクトを作ります。
ヒエラルキーのCanvas上で右クリック「UI > Panel」を選択します。名前を「GameClear」にします。
「GameClear」の下に「GameClearText」、「BackToTitleButton」の2つのオブジェクトを作成しました。
GameManager.csを修正します。
public class GameManager : MonoBehaviour
{
...省略...
public GameObject blocks; // Blocksオブジェクト
public int blockCount; // ブロックの数
void Start()
{
blockCount = blocks.transform.childCount; // ブロックの数を初期化
}
...省略...
// ゲームクリア
public void GameClear()
{
Time.timeScale = 0; // ゲームを一時停止
gameClear.SetActive(true); // ゲームクリア画面を表示
ball.SetActive(false); // ボールを非表示
player.SetActive(false); // プレイヤーを非表示
blocks.SetActive(false); // ブロックを非表示
}
...省略...
}
ゲーム開始時にブロックの数を数えています。Blocksオブジェクトの下にすべてのブロックが入っているので、transform.childCount
で子要素を数えています。
GameClear()
関数では、Time.timeScale = 0
でゲーム内の時間を止め、ゲームクリア画面を表示し、それ以外のオブジェクトを非表示にします。
BlockController.csを修正します。
public class BlockController : MonoBehaviour
{
public GameManager gameManager; // GameManager スクリプトへの参照
...省略...
void OnCollisionEnter2D(Collision2D collision)
{
if (collision.gameObject.tag == "Ball") // 衝突したオブジェクトのタグが Ball なら
{
...省略...
gameManager.blockCount--; // 残りブロック数を1つ減らす
if (gameManager.blockCount <= 0) // 全てのブロックが消えたら
{
gameManager.GameClear(); // ゲームクリア
}
}
}
}
ブロックを消したら残りブロック数を1つ減らし、残りブロック数が0になったらゲームクリア画面を表示します。
BackToTitleButtonのアクションを設定します。
ゲームオーバ画面のときとまったく同じですね。「On Click ()」に「GameManager.BackToTitle」を割り当てます。
GameClearオブジェクトはゲーム開始時には表示させたくないので、インスペークターのチェックを外して非表示にしておきます。
さいごに
記事にはうまくできたことしか書いてないけど、実際には何度も行き詰まったり試行錯誤したりしてます。楽しいー。
でわでわ
コメント