Unityで3Dバトルアクションを1本作って学べること ― 移動・敵AI・戦闘・UI・セーブまで

広告

Unityで「キャラクターを操作して、敵を倒して、アイテムを拾う」ゲームを1本作ってみると、ゲーム開発の基本がひととおり身につきます。

ただ、「何から手を付ければいいの?」「どんな技術が必要になるの?」と迷ってしまうこともあると思います。

そこで今回は、約900行・20本ほどのスクリプトで作れる小さな3Dバトルアクションを題材に、押さえておきたい技術と、つまずきやすいポイントを順番に見ていきます。

今回作るゲーム

題材にするのは、次のような内容のゲームです。

  • キャラクターを動かして、剣で攻撃する3Dアクション
  • プレイヤーを追いかけて攻撃してくる敵(スライム)
  • 倒すとアイテム(木・石)をドロップし、拾うと所持数が増える
  • 体力バー、ポーズメニュー、アイテム一覧、タイトル/ゲームオーバー画面
  • BGM・効果音・ヒットエフェクト
  • スマホ向けの仮想スティックとボタン

小さなゲームですが、ゲームに必要な要素はひととおり入っています。それぞれ、順番に見ていきましょう。

ポイント1:新Input Systemでキャラクターを動かす

まずはキャラクターを動かします。入力は PlayerInput からアクションを取得し、ReadValue と WasPressedThisFrame で読み取ります。

移動には CharacterController.Move を使います。重力とジャンプは、速度のベクトルを自分で更新していきます。

var moveValue = _move.ReadValue<Vector2>();
_moveVelocity.x = moveValue.x * moveSpeed;
_moveVelocity.z = moveValue.y * moveSpeed;

if (_characterController.isGrounded)
{
    if (_jump.WasPressedThisFrame())
    {
        _moveVelocity.y = jumpPower;
    }
}
else
{
    _moveVelocity.y += Physics.gravity.y * Time.deltaTime;
}

_characterController.Move(_moveVelocity * Time.deltaTime);

日本語に読み替えると、次のようになります。

地面にいるとき
    ジャンプキーが押されたら、上向きの速度を入れる
空中にいるとき
    重力の分だけ、下向きの速度を足していく
最後に、その速度でキャラクターを動かす

ポイント2:状態(State)で「今できること」を管理する

プレイヤーも敵も、体力があり、ダメージを受け、倒れると死亡します。そこで、体力・ダメージ・死亡の処理は、共通の抽象クラス MobStatus にまとめます。

状態は Normal / Attack / Die の3つだけにします。そして、IsMovable や IsAttackable といったプロパティで、「今動けるか」「今攻撃できるか」を判定します。

こうしておくと、次のようなよいことがあります。

  • if の条件が散らばらず、「死んでいたら何もしない」を一か所で保証できる
  • プレイヤーと敵で処理を使い回せる(継承)
  • [RequireComponent] を付けると、コンポーネント同士の依存関係をはっきり示せる

ポイント3:Animatorとアニメーションイベントで攻撃判定を合わせる

攻撃は、次の流れで進みます。

トリガーを引く
        ↓
アニメーションが再生される
        ↓
剣を振った瞬間に、当たり判定をONにする
        ↓
振り終わったら、当たり判定をOFFにする

この当たり判定のON/OFFは、アニメーションイベントから呼びます。

public void OnAttackStart()
{
    attackCollider.enabled = true;
    // 効果音
}

public void OnAttackFinished()
{
    attackCollider.enabled = false;
    StartCoroutine(Cooldown());
}

効果音は、再生のたびに pitch を Random.Range(0.7f, 1.3f) で少し揺らしてみましょう。同じ音の繰り返しによる単調さを避けられます。

ポイント4:NavMeshで敵を動かし、スポーンさせる

敵(スライム)の動きは、次の2つだけです。

  • 視界に入ったら、プレイヤーを追いかける
  • 攻撃範囲に入ったら、攻撃する(トリガーコライダー)

敵は一定間隔で、プレイヤーの周囲にスポーンさせます。このとき、そのままの座標だと歩けない場所に出てしまうことがあるので、NavMesh.SamplePosition で歩ける場所に補正します。

if (NavMesh.SamplePosition(spawnPosition, out var hit, 10, NavMesh.AllAreas))
{
    Instantiate(enemyPrefab, hit.position, Quaternion.identity);
}

トリガーの通知は、汎用の CollisionDetector に UnityEvent を持たせると、Inspectorから配線できて再利用しやすくなります。

ポイント5:ドロップ・所持アイテム・セーブ

ドロップ率は [SerializeField][Range(0, 1)] で公開しておくと、コードを触らずにInspectorから調整できます。

所持アイテムは、[Serializable] を付けたクラスを JsonUtility で文字列にして、PlayerPrefs に保存します。

所持アイテム(Serializableなクラス)
        ↓ JsonUtility
文字列(JSON)
        ↓
PlayerPrefs に保存

小規模なゲームのセーブなら、これで十分です。

ポイント6:UIと演出

体力バー、ポーズ、アイテムのダイアログ、画面遷移などのUIを作ります。演出にはDOTweenを使い、BGMや効果音は、DontDestroyOnLoad と Resources.LoadAll でまとめて読み込んで管理します。

ヒットエフェクトは、剣に付けるだけでも手応えが大きく変わります。

つまずきやすいポイント

この規模のUnityプロジェクトでは、次のような場面で、誰でも一度はつまずきます。

引数を受け取っているのに使っていない

Add(int number) の中で this.number += 1 と書いてしまい、複数個の追加・消費が反映されないパターンです。

public void Add(int number)
{
    this.number += 1;   // 引数の number を使っていない
}

引数を使うと、次のようになります。

public void Add(int number)
{
    this.number += number;
}

1個のときだけでなく、2個、3個でも試してみると気付けます。

エディタ専用の using が混ざる

入力補完で using UnityEditorInternal; などが自動で入ると、エディタでは動くのに、ビルドでだけエラーになります。使っていない using は消す習慣をつけましょう。

毎フレームの監視で済ませてしまう

「体力が0以下か」を Update で毎フレーム調べ、フラグで二重実行を防ぐ書き方があります。

void Update()
{
    if (hp <= 0 && !isDead)
    {
        isDead = true;
        // 死亡処理
    }
}

これでも動きます。ただ、死亡時に呼ばれるイベント(OnDie)に処理を寄せるほうが、意図が伝わりやすく、無駄もありません。

名前のスペルミス

Damaged を Damged と綴るようなタイプミスは、クラス名やメソッド名で一貫していると動いてしまうぶん、気付きにくいものです。後から直すと、参照の修正も必要になります。

書いた直後に、IDEの補完と検索で確認しておくと安全です。

判定の座標がずれる

モデルの原点とコライダーの中心がずれて、当たらない、あるいは早すぎる当たりになることがあります。Sceneビューでギズモを見て確認するのが近道です。

アニメーションが再生されない

原因は、トリガー名の不一致や、ステート遷移の条件(Has Exit Time など)であることが多いです。プレイ中にAnimatorウィンドウを開いて、どのステートにいるかを見ると、原因を絞り込めます。

デバッグログを残したままにする

開発中の Debug.Log は、コンソールを埋めてしまい、パフォーマンスにも影響します。確認が済んだら消すか、条件付きにしましょう。

一度に作り込みすぎる

たとえば体力バーなら、次のように小さく進めると安定します。

見た目だけ作る
        ↓
データと連動させる
        ↓
敵にも付ける

動かして確認しながら、小さくコミットしていきます。コミットメッセージに「何をしたか」を書いておくと、後から振り返るときにも役立ちます。

まとめ

このゲームで学べることを、表にまとめます。

学べることキーワード
入力と移動新Input System、CharacterController
設計継承、State管理、コンポーネント分割、Singleton
戦闘Animator、アニメーションイベント、トリガー判定
敵AINavMesh、視界判定、スポーン
データJsonUtility+PlayerPrefs
演出AudioSource、エフェクト、DOTween
開発習慣小さく作る、小さくコミットする、ログを整理する

今回のゲームには、次の流れが入っています。

入力
  ↓
移動
  ↓
戦闘
  ↓
報酬
  ↓
UI
  ↓
保存

小さなゲームでも、この流れを一周すると、ゲーム開発の全体像がつかめます。

次のステップとして、敵の種類を増やす、アイテムのクラフトや武器強化を足す、セーブをクラウドに載せる、といった拡張もおすすめです。

訪問数 4 回, 今日の訪問数 4回

広告

C#

Posted by hidepon