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、アニメーションイベント、トリガー判定 |
| 敵AI | NavMesh、視界判定、スポーン |
| データ | JsonUtility+PlayerPrefs |
| 演出 | AudioSource、エフェクト、DOTween |
| 開発習慣 | 小さく作る、小さくコミットする、ログを整理する |
今回のゲームには、次の流れが入っています。
入力
↓
移動
↓
戦闘
↓
報酬
↓
UI
↓
保存
小さなゲームでも、この流れを一周すると、ゲーム開発の全体像がつかめます。
次のステップとして、敵の種類を増やす、アイテムのクラフトや武器強化を足す、セーブをクラウドに載せる、といった拡張もおすすめです。









ディスカッション
コメント一覧
まだ、コメントがありません