3Dモデルの読み込みとUnityで格闘ゲームを作る
格闘ゲームには、パンチ、キック、ガード、やられ演出などを自然に動かせるキャラクターが必要です。それらは骨格とアニメーションを持つ3Dモデルから作られます。この記事では2つのことを一緒に扱います。コードでゲーム実行中にモデルを読み込むこと、そしてそのモデルを使う戦闘システムを作ることです。戦闘システムには、攻撃、ガード、体力、AI対戦相手が含まれます。
注:コードは元のタイ語版のコメントを残しています。Unityエディタでは実行していません。テストはこの記事のために作ったUnity APIのスタブ(模擬ライブラリ)で行っており、スクリプトのロジックだけを確認しています。モデルとアニメーションはエディタで確認してください。スタブの全文はタイ語版にあります。
必要なもの
- Unity Hub と最新のLTS版Unityエディタ
- Humanoid リグとアニメーションを持つ3Dキャラクターモデル(FBX形式)。無料で入手できるものもありますが、利用前に各配布元のライセンスを確認してください
- FPSの記事の基礎(スクリプト、Inspector、コンパイルとテストの実行)
ステップ1:プロジェクトを作り、モデルを取り込む
1. Unity Hub で **New project** を押し、**3D (URP)** または **3D** のテンプレートを選んで、`MyFighter` と名付けます。
2. キャラクターのFBXファイルを `Assets/Models/` にドラッグします。
3. FBXを選び、Inspector の **Rig** で **Animation Type** を **Humanoid** にして **Apply** を押します。
4. **Animation** で、Idle、Punch、Kick、Block、Hit、Die のクリップがあるか確認します。
Humanoid の設定が重要なのは、他のキャラクターのアニメーションをこのキャラクターにも使えるようになるからです。
ステップ2:Animator Controller を作る
1. `Assets` を右クリックし **Create → Animation → Animator Controller** を選びます。名前を `FighterController` にします。
2. ダブルクリックして Animator ウィンドウを開き、Idle クリップをグラフにドラッグして、初期状態(オレンジ色)にします。
3. **Trigger** パラメータ `Attack`、`Hit`、`Die` と、**Bool** パラメータ `Block` を作ります。
4. Idle から `Attack` トリガーで遷移する Punch 状態を作り、Hit と Die も同様に作ります。
5. `Block` が true のときに入り、false で戻る Block 状態を作ります。
ステップ3:コードでゲーム実行中にモデルを読み込む
コードで読み込むと、あらかじめすべてのキャラクターをシーンに置かなくても、プレイヤーが複数のキャラクターから選べるようになります。最も簡単な方法は、プレハブを `Assets/Resources/` に入れて名前で読み込むことです。
1. Animator Controller を設定したキャラクターのプレハブを `Assets/Resources/Characters/Fighter.prefab` に置きます。
2. `ModelLoader` という名前のスクリプトを作り、次のコードを貼り付けます。
using UnityEngine;
// โหลดโมเดลตัวละครตอนรันเกม จากโฟลเดอร์ Resources แล้วติดตั้งไว้ใต้จุดที่กำหนด
// ก่อนใช้งานให้วางไฟล์ Prefab ไว้ที่ Assets/Resources/Characters/<ชื่อ>.prefab
public class ModelLoader : MonoBehaviour
{
[SerializeField] private string characterPath = "Characters/Fighter"; // ไม่ต้องใส่นามสกุลไฟล์
[SerializeField] private Transform modelParent; // จุดที่จะวางโมเดล
private GameObject model;
private Animator animator;
public GameObject Model => model;
public Animator Animator => animator;
private void Awake()
{
Load();
}
// คืนค่า true ถ้าโหลดสำเร็จและมี Animator ให้ควบคุม
public bool Load()
{
GameObject prefab = Resources.Load<GameObject>(characterPath);
if (prefab == null)
{
Debug.LogError("ไม่พบโมเดลที่ Resources/" + characterPath);
return false;
}
model = Object.Instantiate(prefab, modelParent);
animator = model.GetComponent<Animator>();
if (animator == null)
{
Debug.LogError("โมเดลนี้ไม่มี Animator");
return false;
}
return true;
}
}
ポイント
- `Resources.Load<GameObject>` は拡張子なしで `Resources` フォルダ内のファイルを探します。
- `Object.Instantiate` はプレハブのコピーをシーンに作るので、何度でも生成できます。
- ファイルが見つからない場合と Animator がない場合の両方で `null` を確認するので、後で壊れるのではなく、すぐにコンソールにエラーが表示されます。
注:`Resources` は小規模なプロジェクトや学習向けです。多くのファイルを読み込む大きなプロジェクトでは Addressables を使ってください。
ステップ4:戦う キャラクター
`Fighter` は体力を管理し、ダメージを受け、ガードし、攻撃し、Animator を動かして適切なアニメーションを再生します。
using UnityEngine;
// ตัวละครนักสู้: มีเลือด โจมตี ป้องกัน และตาย ผ่าน Animator เพื่อให้เล่นท่าทางตามเหตุการณ์
public class Fighter : MonoBehaviour
{
[SerializeField] private int maxHealth = 100;
[SerializeField] private int attackDamage = 10;
[SerializeField] private float attackRange = 1.5f; // ระยะที่โจมตีถึง (เมตร)
[SerializeField] private float attackCooldown = 0.6f; // เวลาพักหลังโจมตีแต่ละครั้ง (วินาที)
[SerializeField] private float blockMultiplier = 0.5f; // ป้องกันแล้วรับดาเมจเหลือกี่เปอร์เซ็นต์
private int health;
private bool isBlocking;
private float nextAttackTime;
private Animator animator;
public int Health => health;
public bool IsDead => health <= 0;
public bool IsBlocking => isBlocking;
public void Init(Animator animatorSource)
{
animator = animatorSource;
health = maxHealth;
}
// โจมตีคู่ต่อสู้ คืน true ถ้าการโจมตีเกิดขึ้น (ไม่ได้อยู่ในช่วงพัก ไม่ได้ตาย ไม่ได้กำลังป้องกัน)
public bool Attack(Fighter target)
{
if (IsDead || isBlocking || Time.time < nextAttackTime) return false;
nextAttackTime = Time.time + attackCooldown;
SetTrigger("Attack");
if (DistanceTo(target) <= attackRange)
{
target.TakeHit(attackDamage);
}
return true;
}
public void SetBlocking(bool value)
{
if (IsDead) return;
isBlocking = value;
animator?.SetBool("Block", value);
}
public void TakeHit(int damage)
{
if (IsDead) return;
int finalDamage = isBlocking ? (int)(damage * blockMultiplier) : damage;
health -= finalDamage;
if (health < 0) health = 0;
if (IsDead)
{
SetTrigger("Die");
}
else
{
SetTrigger("Hit");
}
}
public float DistanceTo(Fighter other)
{
return Vector3.Distance(transform.position, other.transform.position);
}
private void SetTrigger(string name)
{
animator?.SetTrigger(name);
}
}
ポイント
- クールダウン(`attackCooldown`)で、連打によって無制限に攻撃できないようにします。
- ガード中は攻撃できず、`blockMultiplier` によりダメージが減ります。
- 状態の変化はすべて Animator のトリガーを通すので、ゲームのロジックはアニメーションの再生方法を知る必要がありません。
- 命中判定は単純な距離で行っています。学習用としては十分ですが、実際のゲームでは `Physics.OverlapBox` のようなヒットボックスをアニメーションに合わせて使うのが一般的です。
ステップ5:AI対戦相手
`FighterAI` はプレイヤーに向かって歩き、範囲内に入ると攻撃します。
using UnityEngine;
// สมองของคู่ต่อสู้: เดินเข้าหาผู้เล่น และโจมตีเมื่ออยู่ในระยะ
[RequireComponent(typeof(Fighter))]
public class FighterAI : MonoBehaviour
{
[SerializeField] private Fighter target; // ผู้เล่น
[SerializeField] private float walkSpeed = 2f; // ความเร็วเดินเข้าหา (เมตรต่อวินาที)
private Fighter self;
private void Awake()
{
self = GetComponent<Fighter>();
}
private void Update()
{
if (self.IsDead || target == null || target.IsDead) return;
if (self.DistanceTo(target) > 1.5f)
{
// ยังไกลเกินไป ให้เดินเข้าหาเป้าหมาย
Vector3 direction = (target.transform.position - transform.position).normalized;
transform.position = transform.position + direction * walkSpeed * Time.deltaTime;
}
else
{
// อยู่ในระยะแล้ว ลองโจมตี (ถ้าพักอยู่จะไม่โจมตี)
self.Attack(target);
}
}
}
ポイント
- この AI の状態は「接近」と「攻撃」の2つだけです。出発点としては十分です。実際のゲームではステートマシンやビヘイビアツリーを使い、ガード、回避、技の選択を加えるのが一般的です。
- 停止距離(1.5)は `Fighter` の `attackRange` と合わせると、到着したらすぐ攻撃できます。
ステップ6:勝敗を判定する
`RoundManager` は、どちらが倒れたかを確認して勝者を決めます。
using UnityEngine;
// ตัดสินผลแมตช์: ฝ่ายที่เหลือเลือดหรือยืนอยู่หลังอีกฝ่ายตายคือผู้ชนะ
public class RoundManager : MonoBehaviour
{
[SerializeField] private Fighter player;
[SerializeField] private Fighter enemy;
// คืน "Player" หรือ "Enemy" เมื่อมีคนตาย, คืน null ถ้าการต่อสู้ยังไม่จบ
public string GetWinner()
{
if (enemy.IsDead && !player.IsDead) return "Player";
if (player.IsDead && !enemy.IsDead) return "Enemy";
if (player.IsDead && enemy.IsDead) return "Draw";
return null;
}
}
ステップ7:シーンを組み立て、プレイヤーを操作する
1. シーンに地面とライトを置きます。
2. `Player` という GameObject を作り、`characterPath` を `Characters/Fighter` にした `ModelLoader` と `Fighter` を追加します。
3. `ModelLoader` の Animator を使って `Fighter` の `Init` を呼び、体力を100から始めます。通常はシーン用の小さなスクリプトで行います。
4. 同じ方法で `Enemy` を作り、`Target` を `Player` にした `FighterAI` を追加します。
5. `RoundManager` を作り、両方の戦闘員を設定します。
この簡単な操作では、左クリックで攻撃、右クリック長押しでガードします。
using UnityEngine;
// ผู้เล่นควบคุมด้วยเมาส์: คลิกซ้ายโจมตี คลิกขวาค้างเพื่อป้องกัน
public class PlayerInput : MonoBehaviour
{
[SerializeField] private Fighter self;
[SerializeField] private Fighter opponent;
private void Update()
{
if (Input.GetMouseButtonDown(0))
{
self.Attack(opponent);
}
self.SetBlocking(Input.GetMouseButton(1));
}
}
ステップ8:自動テスト
テストは FPS の記事と同じ方法で行います。スタブファイル(`FightStubs.cs`)と24項目のテストファイル(`FightTests.cs`)はタイ語版にあります。テストでは、範囲内と範囲外の攻撃、クールダウン、ガード、死亡、死んだキャラクターが攻撃できないこと、AIの接近と攻撃、プレハブが見つからない場合と Animator がない場合のモデル読み込み、勝敗判定を確認します。
コンパイルと実行の手順:スタブ、テストファイル、4つのスクリプト(`ModelLoader.cs`、`Fighter.cs`、`FighterAI.cs`、`RoundManager.cs`)を `build` フォルダにコピーし、コピーからだけ `using UnityEngine;` を削除します。新しいC#コンパイラでコンパイルし(例:`csc -langversion:latest -out:FightTests.exe *.cs`)、`FightTests.exe` を実行します。
テスト結果
PASS attack in range: attack starts
PASS attack in range: defender loses 10 health (100 to 90)
PASS attack in range: defender plays Hit animation
PASS attack: attacker plays Attack animation
PASS attack out of range: no damage (still 100)
PASS cooldown: first attack succeeds
PASS cooldown: second attack at 0.3 s is refused
PASS cooldown: only one hit landed (90)
PASS cooldown: attack after 0.7 s is allowed
PASS block: damage halved (100 to 95)
PASS death: fighter is dead at 0 health
PASS death: Die animation is played
PASS dead fighter cannot attack
PASS damage to a dead fighter is ignored
PASS AI approaches the player when far
PASS AI attacks when in range
PASS model loader: loads the prefab from Resources
PASS model loader: finds the Animator on the model
PASS model loader: missing file returns false
PASS model loader: missing file logs an error
PASS model loader: prefab without Animator returns false
PASS round: no winner while both are alive
PASS round: player wins when enemy is dead
PASS round: enemy wins when player is dead
Passed 24 / Failed 0
これらの結果はスタブによるもので、Unity によるものではありません。モデルの見た目、アニメーションのタイミング、実際の命中判定はエディタで確認してください。
テストが防いだ不具合:初期の `Fighter` は `Mathf.Max(0, health - finalDamage)` を使っていました。`Mathf.Max` は float を受け取り、`health` は int なので、これはコンパイルできません。直接の減算と0での下限制限に直しました。
ステップ9:エディタでテストする
**Play** を押して確認します。
- プレイヤーのモデルが Animator 付きで読み込まれ、Idle を再生する。
- 左クリックで Punch が再生され、相手の体力が減る。
- 右クリック長押しで Block が再生され、ダメージが減る。
- 相手は遠くから近づき、近くで攻撃する。
- 体力が0になると Die が再生される。
よくある問題
- **モデルが表示されない:** プレハブが `Assets/Resources/Characters/` にあり、`characterPath` の名前が完全に一致するか確認します。
- **コンソールに Animator がないと表示される:** プレハブに Animator コンポーネントがあり、コントローラーが設定されているか確認します。
- **アニメーションが切り替わらない:** Animator Controller のトリガー名がコードと完全に一致しているか(`Attack`、`Hit`、`Die`)確認します。
- **モデルが浮く、向きがおかしい:** リグが Humanoid か、クリップの Root Motion の設定を確認します。
- **AI が動かない:** AI の Target に プレイヤーが入っているか、AI の `Fighter` に Animator があるか確認します。
次のステップ
基本の戦闘ができたら、コンボ(連続技)、ジャンプと回避、選べる複数のキャラクター、ヒット時のサウンドやパーティクルなどを追加できます。
まとめ
コードでモデルを読み込むと、キャラクターを柔軟に選べます。Animator はアニメーションを戦闘ロジックから切り離します。戦闘システムは、戦闘員、AI、勝敗判定、モデル読み込みの4つに分かれ、それぞれ単独でテストできます。テストは、エディタを開く前にコンパイルエラーを見つけてくれます。