UnityでFPS(一人称シューター)を作る:マウス視点、移動、射撃、テスト
一人称シューター(FPS)は、キャラクターの目を通して世界を見るゲームです。基本となる要素は、マウスによる視点操作、WASDによる移動、レイキャストによる射撃、そして体力を持つ的です。この記事では、新しいプロジェクトから、テストに合格するコードまでを一つずつ作ります。
注:コードは元のタイ語版のコメントを残しています。Unityエディタでは実行していません。テストは、この記事のために作ったUnity APIのスタブ(模擬ライブラリ)で行っており、スクリプトのロジックだけを確認しています。スタブの全文はタイ語版の記事にあります。
必要なもの
- Unity Hub と最新のLTS版Unityエディタ
- VS Code または Visual Studio
- 2D版の記事の基礎(GameObject とコンポーネント)があると理解が早まります
ステップ1:3Dプロジェクトを作り、入力を設定する
1. Unity Hub で **New project** を押し、**Universal 3D**(URP)または **3D**(Built-in)のテンプレートを選んで、`MyFPS` と名付けます。
2. **Edit → Project Settings → Player** を開き、**Active Input Handling** を **Both** または **Input Manager (Old)** にして、求められたらエディタを再起動します。
3. **Edit → Project Settings → Input Manager** に **Mouse X**、**Mouse Y**、**Horizontal**、**Vertical** の軸と **Jump** ボタンがあることを確認します。標準のテンプレートには含まれています。
ステップ2:床と的を作る
1. Hierarchy を右クリックして **3D Object → Plane** を選びます。名前を `Ground` にし、Scale を `X = 5, Y = 1, Z = 5` にします。
2. もう一度右クリックして **3D Object → Cube** を選びます。名前を `Target` にし、約10メートル先、たとえば Position `Z = 10`、`Y = 1` に置きます。
3. キューブに **Target** コンポーネント(ステップ6で作成)を付けます。キューブには Box Collider があり、レイが当たるために必要です。
ステップ3:プレイヤーを作る
1. Hierarchy で **Create Empty** を選び、名前を `Player` にし、Position を `Y = 1` にします。
2. `Player` に **Character Controller** を追加します。Rigidbody を使わずにキャラクターを動かせます。
3. `Player` を右クリックして **Camera** を選び、子オブジェクトにします。Position を `Y = 0.6` にして、目の高さから見えるようにします。
4. 元の Main Camera は削除します。子のカメラがその役割を担います。
構成は `Player` → `Camera` になります。`Player` には後で `MouseLook`、`FPSMovement`、`Weapon` も付けます。
ステップ4:マウスで視点を動かす
`MouseLook` という名前のスクリプトを作り、次のコードを貼り付けます。
using UnityEngine;
// หมุนกล้องตามเมาส์: ก้ม-เงยที่กล้อง (pitch) และหันซ้าย-ขวาที่ตัวละคร (yaw)
public class MouseLook : MonoBehaviour
{
[SerializeField] private float sensitivity = 2f; // ความไวเมาส์
[SerializeField] private float minPitch = -80f; // ก้มลงได้สุดกี่องศา
[SerializeField] private float maxPitch = 80f; // เงยขึ้นได้สุดกี่องศา
[SerializeField] private Transform body; // ตัวละครที่ต้องหมุนตามแนวนอน
private float pitch;
public float Pitch => pitch;
private void Update()
{
float mouseX = Input.GetAxis("Mouse X") * sensitivity;
float mouseY = Input.GetAxis("Mouse Y") * sensitivity;
// เมาส์ขึ้นคือเงยหน้า จึงต้องลบค่า mouseY
pitch = Mathf.Clamp(pitch - mouseY, minPitch, maxPitch);
transform.localRotation = Quaternion.Euler(pitch, 0f, 0f);
body.Rotate(0f, mouseX, 0f);
}
}
ポイント
- 上下の回転はカメラに適用し、体は傾けません。
- 左右の回転は体に適用し、歩く方向が視線に合うようにします。
- `Mathf.Clamp` で上下の角度を制限し、視点が裏返らないようにします。
Inspector で、カメラに付いた `MouseLook` の **Body** 欄に `Player` をドラッグします。
ステップ5:WASDで移動し、ジャンプする
`FPSMovement` という名前のスクリプトを作り、次のコードを貼り付けて `Player` に付けます。
using UnityEngine;
// เดินแบบ FPS ด้วย WASD โดยใช้ CharacterController (ไม่ใช้ Rigidbody)
[RequireComponent(typeof(CharacterController))]
public class FPSMovement : MonoBehaviour
{
[SerializeField] private float speed = 6f; // ความเร็วเดิน
[SerializeField] private float jumpHeight = 1.5f; // ความสูงกระโดด (เมตร)
[SerializeField] private float gravity = -20f; // แรงโน้มถ่วง
private CharacterController controller;
private float verticalVelocity;
public bool IsGrounded => controller.isGrounded;
private void Awake()
{
controller = GetComponent<CharacterController>();
}
private void Update()
{
// แกน x คือซ้าย-ขวา และแกน z คือหน้า-หลัง ทั้งคู่อิงทิศทางที่ตัวละครหันอยู่
float h = Input.GetAxis("Horizontal");
float v = Input.GetAxis("Vertical");
Vector3 move = transform.right * h + transform.forward * v;
// จำกัดความยาวเวกเตอร์ไม่ให้เดินเฉียงเร็วกว่าเดินตรง
if (move.magnitude > 1f) move = move.normalized;
controller.Move(move * speed * Time.deltaTime);
// แรงโน้มถ่วงและการกระโดด
if (controller.isGrounded && verticalVelocity < 0f)
{
verticalVelocity = -2f; // กดให้ติดพื้นเล็กน้อย
}
if (Input.GetButtonDown("Jump") && controller.isGrounded)
{
verticalVelocity = Mathf.Sqrt(jumpHeight * -2f * gravity);
}
verticalVelocity += gravity * Time.deltaTime;
controller.Move(new Vector3(0f, verticalVelocity * Time.deltaTime, 0f));
}
}
ポイント
- `CharacterController.Move` は壁への衝突を自動で処理するため、位置を直接書き換えるより FPS に向いています。
- 斜め移動は速度を制限する必要があります。制限しないと、斜めは約41パーセント速くなります(2の平方根)。
- `Sqrt(jumpHeight * -2 * gravity)` の式は、指定した高さに届く初速を求めます。
ステップ6:レイキャストの銃と体力を持つ的
ヒットスキャン方式の銃は、銃口から前方へレイを飛ばします。レイが `Target` コンポーネントを持つものに当たれば、その的の体力が減ります。
`Weapon` という名前のスクリプトを作り、`Player` に付けます。
using UnityEngine;
// ปืนแบบ hitscan: ยิงรังสีจากปากกระบอกปืน ถ้าโดนเป้าหมายก็ลดเลือด
public class Weapon : MonoBehaviour
{
[SerializeField] private Transform muzzle; // จุดปากกระบอกปืน
[SerializeField] private float range = 100f; // ระยะยิงสูงสุด
[SerializeField] private int damage = 25; // ดาเมจต่อนัด
[SerializeField] private float fireInterval = 0.2f; // เวลาขั้นต่ำระหว่างนัด (วินาที)
[SerializeField] private int maxAmmo = 10; // กระสุนเต็มซอง
private int ammo;
private float nextFireTime;
public int Ammo => ammo;
private void Awake()
{
ammo = maxAmmo;
}
private void Update()
{
if (Input.GetMouseButtonDown(0))
{
TryShoot();
}
}
// คืน true ถ้ายิงจริง, false ถ้ากระสุนหมดหรือยังยิงไม่ได้
public bool TryShoot()
{
if (ammo <= 0 || Time.time < nextFireTime) return false;
ammo--;
nextFireTime = Time.time + fireInterval;
RaycastHit hit;
if (Physics.Raycast(muzzle.position, muzzle.forward, out hit, range))
{
Target target = hit.collider.GetComponent<Target>();
if (target != null)
{
target.TakeDamage(damage);
}
}
return true;
}
}
カメラの子として `Muzzle` という空のオブジェクトを作り、銃の前に置きます。それを `Weapon` の **Muzzle** 欄にドラッグします。
`Target` という名前のスクリプトを作り、キューブに付けます。
using UnityEngine;
// เป้าหมายที่ถูกยิงได้: มีเลือด เมื่อเลือดหมดจะตาย
public class Target : MonoBehaviour
{
[SerializeField] private int maxHealth = 100;
private int health;
public int Health => health;
public bool IsDead => health <= 0;
private void Awake()
{
health = maxHealth;
}
public void TakeDamage(int amount)
{
if (IsDead) return;
health -= amount;
if (health < 0) health = 0;
if (IsDead)
{
Die();
}
}
private void Die()
{
// ในเกมจริงอาจใช้ Destroy(gameObject) หรือเล่นแอนิเมชันตาย
gameObject.SetActive(false);
}
}
コードについての注意:`Target` の最初のバージョンは `health = Mathf.Max(0, health - amount);` と書いていました。これはコンパイルできません。`Mathf.Max` は float を返しますが、`health` は int だからです。コンパイラが再生テストの前にこの誤りを検出しました。上のバージョンは直接引き算し、0 未満にならないよう制限しています。
ステップ7:自動テスト
テストは、Unity APIのスタブ(`UnityStubs.cs`、タイ語版の記事に全文あり)とテストファイル `FPSTests.cs` を使います。マウス視点、移動、ジャンプ、射撃、連射間隔、弾数の上限、的の死亡を確認します。
コンパイルと実行の手順:
1. `build` フォルダを作り、`UnityStubs.cs`、`FPSTests.cs` と4つのスクリプトをコピーします。
2. コピーした中だけで `using UnityEngine;` の行を削除します。Unityプロジェクトの本物のスクリプトでは残しておきます。
3. 新しいC#コンパイラでコンパイルし(例:`csc -langversion:latest -out:FPSTests.exe *.cs`)、`FPSTests.exe` を実行します。
テスト結果
PASS mouse up: pitch becomes -2 (looking up)
PASS mouse up for long: pitch stops at minPitch (-80)
PASS mouse right: body turns 2 degrees
PASS walk forward 1 second: moves about 6 units
PASS diagonal walk: speed capped at about 6 (not 8.5)
PASS jump from ground: rises above the ground in the first frame
PASS shoot: fires when ammo is available
PASS shoot hit: target health drops by damage (100 to 75)
PASS shoot: ammo decreases from 10 to 9
PASS fire interval: second shot within 0.2 s is blocked
PASS fire interval: ammo is not spent on the blocked shot
PASS 4 hits of 25: target is dead
PASS 4 hits of 25: target object is deactivated
PASS empty magazine: after 10 shots the 11th is refused
Passed 14 / Failed 0
この結果はUnityではなくスタブによるものです。物理、衝突、アニメーション、実際のカメラの動きは、エディタで Play を押して確認する必要があります。
ステップ8:エディタでテストする
**Play** を押して確認します。
- マウスを動かすと、カメラが左右に回り、上下に傾きます。-80度や80度を超えません。
- W、A、S、D で、向いている方向に移動します。
- Space でジャンプします。
- 左クリックで射撃します。4発当てると的の体力がなくなり、消えます。
- 10発撃った後は、クリックしても発射されません。
よくある問題
- **カメラが回らない:** Active Input Handling の設定を確認し、`MouseLook` の Body 欄に `Player` が入っているか確認します。
- **壁をすり抜ける:** 壁にコライダーがあるか、Character Controller がキャラクターに合っているかを確認します。
- **弾が当たらない:** Muzzle が前方(Z軸)を向いているか、的にコライダーと `Target` コンポーネントがあるかを確認します。
- **Mathf.Max のエラー:** int の変数に `Mathf.Max` を使わないでください。記事の書き方を使います。
まとめ
FPSには主に4つの仕組みがあります。視点の操作、移動、レイキャストによる射撃、体力を持つ的です。それぞれが小さなスクリプトで、個別にテストできます。テストによって、再生テストの前にコンパイルエラーを見つけられました。次のステップとしては、動く敵、画面の弾数表示、リロードなどが考えられます。