学習記事一覧 · Unity

Unity本格入門 実装編:Chapter 6 キャラクターを作ってみよう

題材・出典: 技術評論社刊『作って学べる Unity本格入門[Unity 6対応版]』(賀好 昭仁 著)に基づく学習補助の解説です。書籍の代替提供を目的とせず、コード掲載は学習上必要な範囲にとどめます。本シリーズ目次

対象読者:Unity エディタの基本操作を理解しており、C# のコードを追いながら読める方
この記事では、いきのこバトル(IkinokoBattle)の PlayerController.cs を中心に、3D キャラクターの操作・カメラ追尾・アニメーション切り替えの実装を読み解きます。書籍 Chapter 6(6-1〜6-5) に対応します。


記事の目次

おすすめの読み方

セクション一覧

ポイント一覧


題材の範囲

Chapter 6 のテーマは「プレイヤーキャラクターを動かす」です。いきのこバトルの主人公「Query-Chan SD」は、以下のすべての要素を組み合わせて操作可能になっています。

書籍の節 テーマ IkinokoBattle での実装
6-1 キャラクターをインポートする Query-Chan SD (FBX) を Prefab として配置
6-2 ゲームオブジェクトの動かし方 CharacterController.Move で重力・移動を処理
6-3 Input Action とスクリプト PlayerController.cs + InputSystem_Actions.inputactions
6-4 Cinemachine のカメラ追尾 CinemachineVirtualCamera でキャラを Follow
6-5 Mecanim アニメーション Animator Controller + SetFloatSetTrigger

この記事では PlayerController.cs を主役として読み進めます。Animator・CharacterControllerPlayerInputMobAttackPlayerStatus の 5 コンポーネントが連携する様子を追います。


プロジェクトの構成(今回のファイル)

スクリプト

ファイル 役割
PlayerController.cs 移動・ジャンプ・攻撃のメインスクリプト
MobStatus.cs HP・状態(Normal/Attack/Die)の管理基底クラス
PlayerStatus.cs MobStatus の派生クラス(死亡時にゲームオーバーシーンへ遷移)
MobAttack.cs 攻撃判定とアニメーションイベントの処理

その他のアセット

アセット 役割
InputSystem_Actions.inputactions Move・Jump・Attack 等のアクション定義ファイル
Query-Chan SD Prefab プレイヤーの 3D モデル(FBX + Animator)
Animator Controller 移動・攻撃・死亡アニメーションのステートマシン

クラス関係(この記事で登場する範囲)

PlayerController          ← 今回のメインスクリプト
  ├─ CharacterController  ← 移動・重力・接地判定(Unity 標準コンポーネント)
  ├─ PlayerInput          ← 入力の受け取り(新 Input System)
  ├─ PlayerStatus         ← HP・状態管理(MobStatus を継承)
  │    └─ MobStatus       ← 基底クラス(IsMovable・IsAttackable・Damage)
  ├─ MobAttack            ← 攻撃処理(AttackIfPossible・アニメーションイベント)
  └─ Animator             ← アニメーション再生(MoveSpeed・Attack・Die)

PlayerController は「入力を受け取り、他のコンポーネントに命令を出す司令塔」です。移動・HP・攻撃・アニメーションをそれぞれ専門のコンポーネントに任せ、自身はそれらをつなぐ役割に徹しています。


コードを全部見てみよう

using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.InputSystem;
[RequireComponent(typeof(CharacterController))]
[RequireComponent(typeof(PlayerInput))]
[RequireComponent(typeof(PlayerStatus))]
[RequireComponent(typeof(MobAttack))]
public class PlayerController : MonoBehaviour
{
    [SerializeField] private Animator animator;
    [SerializeField] private float moveSpeed = 3;
    [SerializeField] private float jumpPower = 3;
    private CharacterController _characterController;
    private Transform _transform;
    private InputAction _jump;
    private InputAction _move;
    private InputAction _attack;
    private Vector3 _moveVelocity;
    private PlayerStatus _status;
    private MobAttack _mobAttack;
    private void Start()
    {
        _characterController = GetComponent<CharacterController>();
        _transform = transform;
        _status = GetComponent<PlayerStatus>();
        _mobAttack = GetComponent<MobAttack>();
        var input = GetComponent<PlayerInput>();
        input.currentActionMap.Enable();
        _move   = input.currentActionMap.FindAction("Move");
        _jump   = input.currentActionMap.FindAction("Jump");
        _attack = input.currentActionMap.FindAction("Attack");
    }
    private void Update()
    {
        if (_attack.WasPressedThisFrame())
        {
            _mobAttack.AttackIfPossible();
        }
        if (_status.IsMovable)
        {
            var moveValue = _move.ReadValue<Vector2>();
            _moveVelocity.x = moveValue.x * moveSpeed;
            _moveVelocity.z = moveValue.y * moveSpeed;
            _transform.LookAt(_transform.position + new Vector3(_moveVelocity.x, 0, _moveVelocity.z));
        }
        else
        {
            _moveVelocity.x = 0;
            _moveVelocity.z = 0;
        }
        if (_characterController.isGrounded)
        {
            if (_jump.WasPressedThisFrame())
            {
                _moveVelocity.y = jumpPower;
            }
        }
        else
        {
            _moveVelocity.y += Physics.gravity.y * Time.deltaTime;
        }
        _characterController.Move(_moveVelocity * Time.deltaTime);
        animator.SetFloat("MoveSpeed",
            new Vector3(_moveVelocity.x, 0, _moveVelocity.z).magnitude);
    }
}

① FBX インポートと Rig 設定(Humanoid / Generic)

FBX とは

3D モデルのデータ形式のひとつです。メッシュ(形状)・テクスチャ参照・ボーン(骨格)・アニメーションをまとめて持てます。いきのこバトルでは Query-Chan SD の FBX が Players フォルダにインポートされています。

Rig タイプの設定

FBX を選択して Inspector の Rig タブを開くと Animation Type を選べます。

Rig タイプ 特徴 使いどころ
Humanoid Unity が人型ボーン構造を自動マッピングする。アニメーションのリターゲット(他モデルへの転用)が可能 人型キャラクター全般
Generic ボーン構造を自由に使える。リターゲットなし 四足動物・機械など非人型
Legacy 旧アニメーションシステム 古い資産の流用のみ

IkinokoBattle の Query-Chan SD は Humanoid で設定されており、他の Humanoid モデルのアニメーションをそのまま流用できます。

Prefab の配置

FBX をシーンにドラッグするか、Assets 上で右クリック → Create → Prefab として登録します。MainScene では Query-Chan SD Prefab に PlayerControllerCharacterControllerPlayerInput 等のコンポーネントを追加した状態で配置されています。


[RequireComponent] で依存コンポーネントをコードで明示する

PlayerController.cs の冒頭に 4 行の属性があります。

[RequireComponent(typeof(CharacterController))]
[RequireComponent(typeof(PlayerInput))]
[RequireComponent(typeof(PlayerStatus))]
[RequireComponent(typeof(MobAttack))]

[RequireComponent] の 2 つの効果

  1. 自動アタッチPlayerController を GameObject にアタッチすると、指定したコンポーネントが自動的に一緒にアタッチされます
  2. 削除防止[RequireComponent] で指定されたコンポーネントを手動で外そうとすると Unity が警告を出して削除を阻止します

なぜ重要か

PlayerControllerGetComponent<CharacterController>()GetComponent<PlayerStatus>()Start() で呼び出し、その結果を使い続けます。これらが存在しない状態で実行されると NullReferenceException になります。[RequireComponent] はその「前提条件の不備」を開発中に早期に検出する仕組みです。

設計の意図:コードを読むだけで「このスクリプトは何に依存しているか」がすぐわかります。README のようなドキュメントが不要になります。


PlayerInputFindAction でアクションを取得する

新 Input System とは

Unity 2019 以降に登場した入力管理システムです。キーボード・ゲームパッド・タッチスクリーンの違いを吸収し、「Move」「Attack」のようなアクション名で入力を扱えます。

.inputactions ファイル

Assets/InputSystem_Actions.inputactions にアクションの定義が保存されています。IkinokoBattle では以下のアクションが定義されています。

アクション名 デフォルト入力
Move Value (Vector2) WASD・矢印キー・左スティック
Jump Button Space・南ボタン
Attack Button マウス左クリック
Look Value (Vector2) マウス移動・右スティック

Start() でのアクション取得コード

var input = GetComponent<PlayerInput>();
input.currentActionMap.Enable();         // アクションマップを有効化
_move   = input.currentActionMap.FindAction("Move");
_jump   = input.currentActionMap.FindAction("Jump");
_attack = input.currentActionMap.FindAction("Attack");

GetComponent<PlayerInput>()PlayerInput コンポーネントを取得し、そこから currentActionMap(現在のアクションマップ)を有効化します。FindAction("名前") でアクション名からアクションオブジェクトを取り出し、フィールドにキャッシュしています。

なぜキャッシュするかFindActionUpdate() の中で毎フレーム呼ぶのはコストがかかります。Start() で一度取得してフィールドに持っておくのが定石です。


ReadValue<Vector2>()CharacterController.Move で移動・重力を実装する

移動の流れ

if (_status.IsMovable)
{
    var moveValue = _move.ReadValue<Vector2>();
    _moveVelocity.x = moveValue.x * moveSpeed;
    _moveVelocity.z = moveValue.y * moveSpeed;
    _transform.LookAt(_transform.position + new Vector3(_moveVelocity.x, 0, _moveVelocity.z));
}

_move.ReadValue<Vector2>() で現在の Move アクションの値(スティックの倒し具合やキーの押し具合)を -1〜+1 の 2D ベクトルとして受け取ります。X 成分を 3D の X 方向、Y 成分(前後入力)を 3D の Z 方向に割り当てているのがポイントです。3D ゲームでは「奥行き方向」は Z 軸だからです。

重力の実装

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

CharacterController は Rigidbody を使わないため、重力の計算を自前でコードに書く必要があります。地面に接していない(!isGrounded)間、毎フレーム Physics.gravity.y(約 -9.81 m/s²)を Y 速度に加算し続けることで自然落下を再現しています。

Physics.gravity.y * Time.deltaTime とすることでフレームレートに依存しない重力加速度になります。

CharacterController.Move で実際に動かす

_characterController.Move(_moveVelocity * Time.deltaTime);

Move() は「このフレームで移動させたい距離(メートル)」を渡します。速度(m/s)に Time.deltaTime(秒)を掛けて「このフレームの移動距離」に変換しています。CharacterController はスロープや壁との衝突を自動処理してくれるため、自前で当たり判定を書く必要はありません。


WasPressedThisFrame で「押した瞬間だけ」を判定する

新 Input System では、ボタン入力に 3 種類の判定があります。

メソッド タイミング 使いどころ
IsPressed() 押している間、毎フレーム true 長押し判定、押しっぱなし処理
WasPressedThisFrame() 押した瞬間の 1 フレームだけ true 攻撃・ジャンプなど「1 回だけ実行」
WasReleasedThisFrame() 離した瞬間の 1 フレームだけ true 離したときのイベント処理
if (_attack.WasPressedThisFrame())
{
    _mobAttack.AttackIfPossible();
}

攻撃は「押した瞬間に 1 回だけ」発動させたいので WasPressedThisFrame() が適切です。もし IsPressed() を使うと、ボタンを押し続けている間、毎フレーム AttackIfPossible() が呼ばれ続けます。

ジャンプも同様

if (_jump.WasPressedThisFrame())
{
    _moveVelocity.y = jumpPower;
}

ジャンプも同じく「押した瞬間」だけ Y 速度を設定します。isGrounded チェックとセットで「空中ジャンプ不可」も実現しています。


⑥ Cinemachine の CinemachineVirtualCamera でキャラクターを追尾する

Cinemachine とは

Unity 公式のカメラ制御パッケージです。プログラムなしで「キャラクターを追うカメラ」「部屋を見渡すカメラ」「ダイナミックなカットシーン」などを設定できます。

Virtual Camera の基本設定

  1. Package Manager から Cinemachine をインポート(Unity 6 ではデフォルト搭載)
  2. GameObject → Cinemachine → CinemachineVirtualCamera で Virtual Camera を配置
  3. Inspector で以下を設定
項目 設定 意味
Follow Player の Transform カメラが追いかける対象
Look At Player の Transform カメラが向く対象
Body 3rd Person Follow キャラの背後を追いかける挙動

Shoulder Offset と Distance

3rd Person Follow の設定では、カメラをキャラクターの肩後ろに配置する Shoulder Offset と距離 Camera Distance を調整することで、TPS(三人称視点)の構図を作ります。

コードが不要な理由:Cinemachine が Follow に設定した Transform を毎フレーム自動追尾するため、PlayerController にカメラ制御のコードは一切ありません。カメラとキャラクターのロジックが完全に分離されています。


animator.SetFloat / SetTrigger でアニメーションを切り替える

Animator Controller のステートマシン

Animator Controller は「アニメーションの状態遷移図」です。各アニメーションクリップをノード(State)として配置し、条件(Transition)でつなぎます。

IkinokoBattle のプレイヤーには少なくとも以下のパラメータが使われています。

パラメータ名 使われ方
MoveSpeed Float 0 で待機アニメーション、大きいほど走りアニメーション
Attack Trigger 攻撃アニメーションへの遷移
Die Trigger 死亡アニメーションへの遷移

移動速度でアニメーションをブレンドする

animator.SetFloat("MoveSpeed",
    new Vector3(_moveVelocity.x, 0, _moveVelocity.z).magnitude);

_moveVelocity の XZ 成分の大きさ(magnitude)を MoveSpeed として渡します。止まっているときは 0、移動中は moveSpeed(=3)前後の値になります。Animator Controller の Blend Tree を使うと、値に応じて「歩き」と「走り」をなめらかに補間できます。

SetFloatSetTrigger の違い

メソッド 挙動
SetFloat(name, value) 継続した数値 毎フレーム更新し続ける。「現在の速さ」など連続値に使う
SetTrigger(name) 一瞬だけ ON になる信号 呼ばれた次のフレームで自動的に OFF。「攻撃開始」「死亡」など1回だけ起こすイベントに使う
SetBool(name, value) ON/OFF の状態 手動で ON/OFF を切り替える。「しゃがみ中」など持続する状態に使う

攻撃と死亡は「一瞬だけトリガーする信号」なので SetTrigger が使われています。


コードの流れを整理しよう

Start() の初期化フロー

PlayerController.Start()
  ├─ GetComponent<CharacterController>() → _characterController にキャッシュ
  ├─ GetComponent<PlayerStatus>()        → _status にキャッシュ
  ├─ GetComponent<MobAttack>()           → _mobAttack にキャッシュ
  └─ GetComponent<PlayerInput>()
       └─ currentActionMap.Enable()
            ├─ FindAction("Move")   → _move にキャッシュ
            ├─ FindAction("Jump")   → _jump にキャッシュ
            └─ FindAction("Attack") → _attack にキャッシュ

Update() の毎フレーム処理フロー

PlayerController.Update()
  ├─ [攻撃判定]
  │    _attack.WasPressedThisFrame()
  │         → true なら _mobAttack.AttackIfPossible()

  ├─ [移動処理]
  │    _status.IsMovable ?
  │         → YES:_move.ReadValue<Vector2>() で XZ 速度を更新
  │                _transform.LookAt() で進行方向に向く
  │         → NO :XZ 速度を 0 にリセット(攻撃中・死亡中は止まる)

  ├─ [重力ジャンプ]
  │    _characterController.isGrounded ?
  │         → YES(地上):WasPressedThisFrame() でジャンプ入力確認
  │         → NO(空中):Physics.gravity.y * deltaTime を Y 速度に加算

  ├─ _characterController.Move(_moveVelocity * Time.deltaTime)
  │                               ↑ 実際にキャラクターを動かす

  └─ animator.SetFloat("MoveSpeed", XZ速度の大きさ)
                                ↑ 移動速度をアニメーションに反映

自分でカスタマイズしてみよう!

課題 A:移動速度と慣性を追加する

現在の実装は「入力を離すと即座に止まる」仕様です。Mathf.Lerp を使って徐々に速度を変化させると、より滑らかな動きになります。

// 現在の速度から目標速度へ線形補間
float smoothTime = 0.1f;  // 追従時間(短いほどキビキビ)
_moveVelocity.x = Mathf.Lerp(_moveVelocity.x, moveValue.x * moveSpeed, Time.deltaTime / smoothTime);
_moveVelocity.z = Mathf.Lerp(_moveVelocity.z, moveValue.y * moveSpeed, Time.deltaTime / smoothTime);

課題 B:ダッシュ機能を追加する

Sprint アクション(.inputactions にすでに定義されている)を使って、押している間だけ速度を上げます。

private InputAction _sprint;
// Start() に追加
_sprint = input.currentActionMap.FindAction("Sprint");
// Update() の移動処理を変更
float currentSpeed = _sprint.IsPressed() ? moveSpeed * 2f : moveSpeed;
_moveVelocity.x = moveValue.x * currentSpeed;
_moveVelocity.z = moveValue.y * currentSpeed;

課題 C:UI クリック時に攻撃しないようにする

現在のコードにコメントアウトされている行があります。

// if (_attack.WasPressedThisFrame() && !EventSystem.current.IsPointerOverGameObject())

EventSystem.current.IsPointerOverGameObject() は「マウスカーソルが UI の上にあるか」を判定します。この条件を有効にすると、メニューやボタンをクリックしたときに誤って攻撃が発動しなくなります。コメントを外して動作を確認してみましょう。


まとめ

ポイント キーワード
① FBX は Humanoid Rig で人型ボーンを自動マッピング リターゲット可能
[RequireComponent] で依存関係をコードで表明する 安全・自己文書化
PlayerInput + FindAction でアクションをキャッシュ 新 Input System
ReadValue<Vector2>() → XZ 変換 → CharacterController.Move Time.deltaTime で速度→距離
WasPressedThisFrame で「押した瞬間だけ」発動 攻撃・ジャンプの基本パターン
⑥ Cinemachine Virtual Camera でコード不要の追尾カメラ Follow / Look At
SetFloat で連続値、SetTrigger で単発イベントをアニメーションに渡す Animator Controller の Blend Tree

Chapter 6 の核心は「入力・物理・アニメーションをそれぞれ専門コンポーネントに任せ、PlayerController はそれらをつなぐ司令塔として薄く保つ」設計思想です。次回 Chapter 7 では、この設計の対となる「敵キャラクターの AI・NavMesh・スポーン」を読み解きます。


← シリーズ目次に戻る
← 前の記事:Chapter 5 ゲームの舞台
次の記事 → 第3回:Chapter 7 敵キャラクターを作って動きを付けよう


最終更新:2026年4月