Unity 6.3 LTS|3D的当てゲームを作ろう②

前回作った3D的当てゲームに、新しい遊びを少しずつ追加します。コードを書いたらすぐに動かし、思ったとおりになるか確かめながら進めましょう。

つづきの準備

前回のゲームを確認しよう

Unity 6.3 LTS(6000.3.24f1)で前回のプロジェクトを開き、再生してみましょう。

MISSION
  • マウスで視点を動かせる
  • 画面中央の照準で的を狙える
  • 左クリックで的だけを消せる
  • 右クリックまたはホイール押し込みで視点を戻せる
前回の手順を見直す

前回の記事「3D的当てゲームを作ろう」へ戻り、止まったSTEPを確かめよう。

STEP 1

スコアを画面に出そう

的をいくつ当てたか分かるように、まずは画面に「SCORE: 0」と表示します。このSTEPではまだ数字を増やしません。

Canvasの中にUI → Text – TextMeshProを作ります。初めて使うときにTMP Importerが開いたら、上段の「Import TMP Essentials」を押します。

TextMeshProの基本ファイルを読み込むTMP Importerの画面
教材に必要なのは上段のTMP Essentialsです。Examples & Extrasは今回は使いません。

作った文字の名前を「ScoreText」にし、文字を「SCORE: 0」に変えます。画面の左上へ置き、読みやすい大きさと色にしましょう。

射撃場の左上にSCORE: 0、中央に白い照準が表示されたGameビュー
左上にスコア、中央に照準が表示されました。
POINT|考えてみよう

背景が明るい場所と暗い場所のどちらでも読める色や大きさになっているかな?

ヒントを見る|文字が見えないとき

ScoreTextがCanvasの子になっているか確認しよう。Rect Transformの位置と、文字の色も見直してみよう。

MISSION

Gameビューの左上に「SCORE: 0」を表示しよう。照準と重ならず、読みやすければ成功です。

STEP 2

的を当てたらスコアを増やそう

画面に出したScoreTextをShootCheckから動かします。的を1個消すたびに、スコアを1増やしましょう。

ShootCheck.csの先頭にusing TMPro;を追加し、クラス内に次の変数を用意します。

public TMP_Text scoreText;
int score = 0;

scoreTextは画面の文字、scoreは現在の得点を覚えます。

既存のStart()の中で、最初の表示を0にします。

scoreText.text = "SCORE: " + score;

Targetに当たったときの処理を次のようにします。

if (hit.collider.gameObject.CompareTag("Target"))
{
    score++;
    scoreText.text = "SCORE: " + score;
    Destroy(hit.collider.gameObject);
}

score++で得点を1増やし、新しい数字をScoreTextへ表示します。

Unityへ戻ったら、Main CameraのShoot CheckにあるScore Textへ、HierarchyのScoreTextをドラッグします。

Main CameraのShoot CheckにScoreTextが割り当てられたInspector
Score Text欄に「ScoreText (Text Mesh Pro UGUI)」が入っていれば準備完了です。
注意

Score TextがNoneのままだと、ゲーム中に文字を変更できません。

再生直後にSCORE: 0と4個の的が表示されたGameビュー
再生直後は「SCORE: 0」です。
的を2個消してSCORE: 2になったGameビュー
的を2個消すと「SCORE: 2」になりました。床や台は残っています。
POINT|考えてみよう

床や台を撃ったときにスコアが増えないのは、コードのどの条件が働いているからかな?

ヒントを見る|数字が増えないとき

Main CameraのScore Text欄と、SphereのTarget Tagを確認しよう。Consoleに赤いエラーが出ていないかも見てみよう。

MISSION

的を2個撃って「SCORE: 2」にしよう。次にCubeを撃ち、数字が増えないことも確かめよう。

STEP 3

全部当てたらCLEARにしよう

ゲーム開始時にTargetの数を数え、的を消すたびに残りを1減らします。残りが0になったら「CLEAR!」と表示しましょう。

クラス内に残りの的を覚える変数を追加します。

int remainingTargets;

既存のStart()内で、Target Tagが付いた的を数えます。

remainingTargets =
    GameObject.FindGameObjectsWithTag("Target").Length;

的を増やしたときも、再生開始時の個数を自動で数えます。

Targetに当たったとき、スコアと残りの数を変えます。

if (hit.collider.gameObject.CompareTag("Target"))
{
    score++;
    remainingTargets--;

    if (remainingTargets == 0)
    {
        scoreText.text = "CLEAR!  SCORE: " + score;
    }
    else
    {
        scoreText.text = "SCORE: " + score;
    }

    Destroy(hit.collider.gameObject);
}
問題発見|スコアが8になった

4個の的を消したのにスコアが8になりました。前のscore++を残したまま、新しい処理にも同じ行を追加していたため、1回で2点増えていました。

4個の的をすべて消したあと、CLEARとSCORE: 8が表示されたGameビュー
動いたように見えても、数字を確かめると問題を見つけられます。
ヒントを見る|点数が倍になるとき

ShootCheck.csでscore++;を検索し、Targetを消す処理の中に1回だけあるか確認しよう。

4個の的をすべて消し、CLEARとSCORE: 4が表示されたGameビュー
重複を直すと「CLEAR! SCORE: 4」になりました。床と台も残っています。
MISSION

すべての的を消してCLEARを出そう。表示されたスコアが、置いた的の数と同じか確認しよう。

STEP 4

動く的を作ろう

赤い的は止めたまま、青い的だけを左右に動かします。動く的は、タイミングを考えて撃つ必要があります。

MovingTarget.csを作り、次のコードを書きます。

using UnityEngine;

public class MovingTarget : MonoBehaviour
{
    public float moveDistance = 2f;
    public float moveSpeed = 2f;

    Vector3 startPosition;

    void Start()
    {
        startPosition = transform.position;
    }

    void Update()
    {
        float offset =
            Mathf.Sin(Time.time * moveSpeed) * moveDistance;

        transform.position =
            startPosition + new Vector3(0f, 0f, offset);
    }
}

最初の場所を覚え、時間に合わせてZ方向の位置を行ったり来たりさせます。この射撃場では、Z方向がGameビューの左右に見えます。

Moving Targetを青いSphereだけに付け、Move DistanceとMove Speedをまず2にします。

青いSphereにMoving Targetが付き、Move DistanceとMove Speedが2になったInspector
青いMaterialのSphereだけにMoving Targetを付けています。Target Tagはそのまま残します。
青い的だけが左右へ往復し、赤い的は止まっているGameビュー
青い的だけが左右へ動きます。動く青い的を撃っても、スコアが増えることを確認できました。
POINT|考えてみよう

Move Speedを速くすると難しさはどう変わるかな? Move Distanceを大きくすると、狙う範囲はどうなるかな?

ヒントを見る|違う方向へ動くとき

このコードはZ方向へ動かします。Sceneビューの青い矢印と、カメラから見た左右の向きを確認しよう。

MISSION

青い的だけを左右へ動かし、狙って消そう。スコアが1増えることも確認しよう。

STEP 5

動く的をボーナスにしよう

動いて狙いにくい青い的だけを3点にします。赤い的は、これまでどおり1点です。

MovingTargetに、青い的の点数を追加します。

public int scoreValue = 3;

ShootCheckにあったscore++;を、次の処理へ置き換えます。

MovingTarget movingTarget =
    hit.collider.GetComponent<MovingTarget>();

if (movingTarget != null)
{
    score += movingTarget.scoreValue;
}
else
{
    score++;
}

GetComponentで、当たった的にMovingTargetが付いているか調べます。付いている青い的はScore Valueの点数、付いていない赤い的は1点を加えます。

動く青い的を先に消してSCORE: 3になったGameビュー
青い的を先に撃つと、一度で「SCORE: 3」になりました。
青い的のあとに赤い的を消してSCORE: 4になったGameビュー
続けて赤い的を撃つと、1点増えて「SCORE: 4」になります。
POINT|考えてみよう

動く的の3点はちょうどよいかな? 速さや動く距離を変えたら、何点にすると遊びやすいか試してみよう。

ヒントを見る|青い的が1点のままのとき

青いSphereにMoving Targetが付いているか、Score Valueが3になっているか確認しよう。古いscore++;が条件の前に残っていないかも調べよう。

MISSION

青い的を先に撃って3点、次に赤い的を撃って4点になることを確かめよう。

STEP 6

制限時間を表示しよう

30秒から減るタイマーを作ります。まずは時間を表示し、0になったあとの動きは次に考えます。

Canvas内のScoreTextを複製してTimerTextにします。ShootCheckには次の変数を追加します。

public TMP_Text timerText;
public float timeLimit = 30f;

float timeRemaining;

Start()で、残り時間を30秒にします。

timeRemaining = timeLimit;
timerText.text =
    "TIME: " + Mathf.CeilToInt(timeRemaining);

Update()の最初で、前のフレームから進んだ時間を引きます。

timeRemaining -= Time.deltaTime;

if (timeRemaining < 0f)
{
    timeRemaining = 0f;
}

timerText.text =
    "TIME: " + Mathf.CeilToInt(timeRemaining);
Main CameraのShoot CheckでTimer Textを接続し、Time Limitを30にしたInspector
Timer Text欄へCanvasのTimerTextを接続し、Time Limitを30にしました。
TIME 15、CLEAR、SCORE 6が表示されたGameビュー
数字が30から減り、残り15秒で全4個を倒して「CLEAR! SCORE: 6」になりました。
問題発見|文字が同じ場所に集まった

ScoreTextを複製したため、TimerTextも左上に置かれています。次はTimerTextを右上へ移し、情報を読みやすくします。

SCOREを左上、TIMEを右上に配置したGameビューとTimerTextのRect Transform
TimerTextのアンカーを右上に変更しました。画面の大きさが変わっても、右上を基準に配置されます。
POINT|考えてみよう

CLEARしたあとも時間は減り続けます。0秒になっても撃てるままでよいかな? ゲーム終了時に止めたいものを考えてみよう。

ヒントを見る|時間が表示されないとき

Shoot CheckのTimer Text欄が空になっていないか確認しよう。Consoleに赤いエラーがある場合は、まず一番上のエラーを読んでみよう。

MISSION

30秒から時間を減らしながら、時間内に4個の的を倒そう。

CODE CHECK

ここまでのコードを整理しよう

機能が増えて、コードも長くなりました。ここで3本のスクリプトの仕事を分け、現在の形を確認します。

MouseLook.csマウスで視点を動かし、右クリックまたは中クリックで元の向きに戻す。
ShootCheck.cs時間、射撃、命中判定、得点、CLEARとTIME UPをまとめて管理する。
MovingTarget.cs青い的を往復させ、ボーナス点を覚えておく。

ShootCheckの処理の順番

  1. ゲームが終わっているか調べる
  2. 残り時間を減らす
  3. 左クリックされたか調べる
  4. 画面中央からRayを飛ばす
  5. 当たったものにTarget Tagがあるか調べる
  6. 的の種類に合わせて得点を増やす
  7. 残りの的が0ならCLEARにする
  8. 命中した的を消す
ShootCheck.cs|射撃ゲームの進行
using UnityEngine;
using UnityEngine.InputSystem;
using TMPro;

public class ShootCheck : MonoBehaviour
{
    Camera cameraComponent;

    public TMP_Text scoreText;
    public TMP_Text timerText;
    public float timeLimit = 30f;

    int score = 0;
    int remainingTargets;
    float timeRemaining;
    bool gameEnded;

    void Start()
    {
        cameraComponent = GetComponent<Camera>();
        scoreText.text = "SCORE: " + score;

        remainingTargets =
            GameObject.FindGameObjectsWithTag("Target").Length;

        timeRemaining = timeLimit;
        timerText.text =
            "TIME: " + Mathf.CeilToInt(timeRemaining);
    }

    void Update()
    {
        if (gameEnded)
        {
            return;
        }

        timeRemaining -= Time.deltaTime;

        if (timeRemaining <= 0f)
        {
            timeRemaining = 0f;
            timerText.text = "TIME: 0";
            scoreText.text = "TIME UP!\nSCORE: " + score;
            gameEnded = true;
            return;
        }

        timerText.text =
            "TIME: " + Mathf.CeilToInt(timeRemaining);

        if (Mouse.current == null ||
            !Mouse.current.leftButton.wasPressedThisFrame)
        {
            return;
        }

        Ray ray = cameraComponent.ViewportPointToRay(
            new Vector3(0.5f, 0.5f, 0f));

        if (!Physics.Raycast(ray, out RaycastHit hit))
        {
            return;
        }

        Debug.Log(hit.collider.gameObject.name);

        if (!hit.collider.CompareTag("Target"))
        {
            return;
        }

        MovingTarget movingTarget =
            hit.collider.GetComponent<MovingTarget>();

        if (movingTarget != null)
        {
            score += movingTarget.scoreValue;
        }
        else
        {
            score++;
        }

        remainingTargets--;

        if (remainingTargets == 0)
        {
            gameEnded = true;
            scoreText.text = "CLEAR!\nSCORE: " + score;
        }
        else
        {
            scoreText.text = "SCORE: " + score;
        }

        Destroy(hit.collider.gameObject);
    }
}
MouseLook.cs|視点を動かす
using UnityEngine;
using UnityEngine.InputSystem;

public class MouseLook : MonoBehaviour
{
    public float sensitivity = 0.1f;

    float yaw;
    float pitch;
    float startYaw;
    float startPitch;

    void Start()
    {
        yaw = transform.eulerAngles.y;
        pitch = transform.eulerAngles.x;
        startYaw = yaw;
        startPitch = pitch;
    }

    void Update()
    {
        if (Mouse.current == null) return;

        Vector2 move = Mouse.current.delta.ReadValue();
        yaw += move.x * sensitivity;
        pitch = Mathf.Clamp(
            pitch - move.y * sensitivity, -80f, 80f);

        transform.rotation = Quaternion.Euler(pitch, yaw, 0f);

        if (Mouse.current.rightButton.wasPressedThisFrame ||
            Mouse.current.middleButton.wasPressedThisFrame)
        {
            yaw = startYaw;
            pitch = startPitch;
            transform.rotation = Quaternion.Euler(pitch, yaw, 0f);
            return;
        }
    }
}
MovingTarget.cs|青い的を動かす
using UnityEngine;

public class MovingTarget : MonoBehaviour
{
    public float moveDistance = 2f;
    public float moveSpeed = 2f;
    public int scoreValue = 3;

    Vector3 startPosition;

    void Start()
    {
        startPosition = transform.position;
    }

    void Update()
    {
        float offset =
            Mathf.Sin(Time.time * moveSpeed) * moveDistance;

        transform.position =
            startPosition + new Vector3(0f, 0f, offset);
    }
}
POINT|重複を見つけよう

Debug.LogとDestroyは、それぞれ1回だけです。命中判定を1か所にまとめると、同じ処理を2回行うミスを減らせます。

ヒントを見る|エラーが出たとき

まず波かっこの組み合わせを確認します。次にTMP_Textの間に余分な記号がないか、hitをRaycastより前で使っていないかを見てみよう。

MISSION

3本のスクリプトを見比べ、それぞれが担当している仕事を1つずつ説明してみよう。

コメント

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