Unity本格入門 実装編:Chapter 6 キャラクターを作ってみよう
題材・出典: 技術評論社刊『作って学べる Unity本格入門[Unity 6対応版]』(賀好 昭仁 著)に基づく学習補助の解説です。書籍の代替提供を目的とせず、コード掲載は学習上必要な範囲にとどめます。本シリーズ目次
対象読者:Unity エディタの基本操作を理解しており、C# のコードを追いながら読める方
この記事では、いきのこバトル(IkinokoBattle)のPlayerController.csを中心に、3D キャラクターの操作・カメラ追尾・アニメーション切り替えの実装を読み解きます。書籍 Chapter 6(6-1〜6-5) に対応します。
記事の目次
おすすめの読み方
セクション一覧
- 題材の範囲
- プロジェクトの構成(今回のファイル)
- クラス関係(この記事で登場する範囲)
- コードを全部見てみよう
- ポイント解説(①〜⑦)
- コードの流れを整理しよう
- 自分でカスタマイズしてみよう!
- まとめ
ポイント一覧
- ① FBX インポートと Rig 設定(Humanoid / Generic)
- ②
[RequireComponent]で依存コンポーネントをコードで明示する - ③
PlayerInputとFindActionでアクションを取得する - ④
ReadValue<Vector2>()とCharacterController.Moveで移動・重力を実装する - ⑤
WasPressedThisFrameで「押した瞬間だけ」を判定する - ⑥ Cinemachine の
CinemachineVirtualCameraでキャラクターを追尾する - ⑦
animator.SetFloat / SetTriggerでアニメーションを切り替える
題材の範囲
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 + SetFloat・SetTrigger |
この記事では PlayerController.cs を主役として読み進めます。Animator・CharacterController・PlayerInput・MobAttack・PlayerStatus の 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 に PlayerController・CharacterController・PlayerInput 等のコンポーネントを追加した状態で配置されています。
② [RequireComponent] で依存コンポーネントをコードで明示する
PlayerController.cs の冒頭に 4 行の属性があります。
[RequireComponent(typeof(CharacterController))]
[RequireComponent(typeof(PlayerInput))]
[RequireComponent(typeof(PlayerStatus))]
[RequireComponent(typeof(MobAttack))][RequireComponent] の 2 つの効果
- 自動アタッチ:
PlayerControllerを GameObject にアタッチすると、指定したコンポーネントが自動的に一緒にアタッチされます - 削除防止:
[RequireComponent]で指定されたコンポーネントを手動で外そうとすると Unity が警告を出して削除を阻止します
なぜ重要か
PlayerController は GetComponent<CharacterController>() や GetComponent<PlayerStatus>() を Start() で呼び出し、その結果を使い続けます。これらが存在しない状態で実行されると NullReferenceException になります。[RequireComponent] はその「前提条件の不備」を開発中に早期に検出する仕組みです。
設計の意図:コードを読むだけで「このスクリプトは何に依存しているか」がすぐわかります。README のようなドキュメントが不要になります。
③ PlayerInput と FindAction でアクションを取得する
新 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("名前") でアクション名からアクションオブジェクトを取り出し、フィールドにキャッシュしています。
なぜキャッシュするか:
FindActionをUpdate()の中で毎フレーム呼ぶのはコストがかかります。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 の基本設定
Package ManagerからCinemachineをインポート(Unity 6 ではデフォルト搭載)GameObject → Cinemachine → CinemachineVirtualCameraで Virtual Camera を配置- 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 を使うと、値に応じて「歩き」と「走り」をなめらかに補間できます。
SetFloat と SetTrigger の違い
| メソッド | 型 | 挙動 |
|---|---|---|
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月