【Unity本格入門 Unity6対応版】Chapter6までの振り返り
- 1. ― 写経を“理解・応用・設計”に変える学習ドリル一式
- 2. 1. 学びの地図(Chapter1〜6の要点を“技術→行動”で整理)
- 3. 2. ケース教材:PlayerController を“読む→変える→検証する”
- 4. 3. 実験課題(1行だけ変える → 予測 → 実測 → 考察)
- 5. ■ 応用
- 6. 4. デバッグ道場(“動かない”の原因を特定する手順)
- 7. 5. 設計に一歩:責務分離の小改造(発展)
- 8. 6. スキル定着ドリル(小テスト)
- 9. 7. 口頭試問10(1人1分)
- 9.1. 1) CharacterController と Rigidbody の使い分け
- 9.2. 2) Update と FixedUpdate を混在させると?
- 9.3. 3) SerializeField を使う意義(publicでない利点)
- 9.4. 4) currentActionMap.Enable() を忘れた症状
- 9.5. 5) deltaTime を掛け忘れると?
- 9.6. 6) スロープで上れない時のパラメータ
- 9.7. 7) ジャンプの“気持ちよさ”を上げる数値の方向性×2
- 9.8. 8) 同時押し(W+D)の正規化対応
- 9.9. 9) 画面外カメラで位置が飛ぶとき疑うこと
- 9.10. 10) “地上判定”の別実装(例)
- 10. 8. よくあるハマり/対処
- 11. 9. チーム作業ミニタスク(30〜60分)
- 12. 10. 評価ルーブリック(提出物:動作プロジェクト+検証メモ)
- 13. 11. 次章へのブリッジ(7〜8章の仕込み)
- 14. 付録:チェックリスト
― 写経を“理解・応用・設計”に変える学習ドリル一式
この記事の目的
- Chapter1〜6で学んだ要素(基礎操作/C#基礎/Input System/移動・ジャンプ・当たり)をつなげて理解する
- PlayerControllerコードを題材に、コードを読む→変える→検証するの習慣を作る
- Chapter7以降(AI/NaviMesh, UI, SE/エフェクト, チューニング)に備えて設計思考の入口に立つ
1. 学びの地図(Chapter1〜6の要点を“技術→行動”で整理)
章 | 技術トピック | “できる行動”への翻訳 |
---|---|---|
1 | Unity/ゲーム開発の全体像 | 目的と完成像を言語化し、最小機能のプロトタイプを切り出す |
2 | Hub導入/プロジェクト作成/旧新入力の違い | 再現性ある環境構築とInput Systemの初期セットアップ |
3 | C#基礎(型・制御構造・クラス・ライフサイクル) | 仕様をif/for/クラスに分解し、Update/FixedUpdateで適材適所 |
4 | 企画の作法(罠の回避) | スコープ最小化、締切ファースト、“まず出す” |
5 | 舞台(Terrain/ライティング/Skybox) | 見やすい画面・操作に集中できる環境を作る |
6 | キャラ操作(CharacterController, InputAction, Cinemachine, Animator) | 動く→向く→跳ぶを一貫した入力系で制御し、調整可能パラメータに切る |
メモ:以降の7〜10は敵AI・UI・演出・最適化。ここまでの“動かす筋力”が土台。
2. ケース教材:PlayerController を“読む→変える→検証する”
[RequireComponent(typeof(CharacterController))]
[RequireComponent(typeof(PlayerInput))]
public class PlayerController : MonoBehaviour
{
[SerializeField] private float moveSpeed = 3;
[SerializeField] private float jumpPower = 3;
private CharacterController _characterController;
private Transform _transform;
private Vector3 _moveVelocity;
private InputAction _move;
private InputAction _jump;
private void Start()
{
_characterController = GetComponent<CharacterController>();
_transform = transform;
var input = GetComponent<PlayerInput>();
input.currentActionMap.Enable();
_move = input.currentActionMap.FindAction("Move");
_jump = input.currentActionMap.FindAction("Jump");
}
private void Update()
{
Debug.Log(_characterController.isGrounded ? "地上にいます" : "空中です");
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));
if (_characterController.isGrounded)
{
if (_jump.WasPressedThisFrame())
{
Debug.Log("ジャンプ!");
_moveVelocity.y = jumpPower;
}
}
else
{
_moveVelocity.y += Physics.gravity.y * Time.deltaTime;
}
_characterController.Move(_moveVelocity * Time.deltaTime);
}
}
2.1 “読む”ポイント(口頭で言えるかチェック)
- CharacterController vs Rigidbody:今回の移動は非物理(Move)。押し戻しやスロープ制御は自作が原則。
- Input System:PlayerInput.currentActionMap.Enable() → FindAction(“Move"/"Jump") → ReadValue/WasPressedThisFrame。
- 座標と向き:LookAt(現在位置 + 水平速度ベクトル) → “進行方向を向く”という幾何学。
- 重力:y += g * dt は積分の離散化。jumpPowerは初速。
- deltaTime:移動距離 = 速度×時間、フレーム依存を排除する基本。
3. 実験課題(1行だけ変える → 予測 → 実測 → 考察)
予測(どう変わる?)→ 実測(挙動/ログ/動画)→ 考察(なぜそうなる?次は?)
- 向きの抑制:LookAt(…) をコメントアウト → ストレーフ移動の可否を確認。
- ジャンプ調整:jumpPower を 1.5 / 6 に切り替え → 滞空時間の体感差を記録。
- 重力の強化:Physics.gravity = new Vector3(0, -20f, 0) をStartで上書き。
- 移動の慣性:地上でも y のみでなく x/z にLerp減衰(滑り感)を入れる。
- 入力サンプリング:FixedUpdate へ移動処理を移した場合の違いを比較(入力はUpdate/物理はFixedUpdateが原則)。
ログ例:Debug.Log($"v={_moveVelocity} grounded={_characterController.isGrounded}");
ストレーフ移動(strafe)とは、向いている方向を変えずに左右・前後へ平行移動する操作のことです。
例:敵を正面に捉えたまま、横にすり抜ける/円を描くように横移動する。FPS/TPSやアクションでよく使われます。
何が普通の移動と違う?
- 一般的な「前進回転型」:入力方向へキャラの向きも回す(LookAt など)。
- ストレーフ:キャラの向きは固定(照準やカメラの向きに合わせる等)しつつ、移動ベクトルだけ入力に従わせる。
Unityでの考え方(要点)
- 向き(回転)と移動(平行移動)を分離する。
- 画面(カメラ)基準で right/forward を取り、Input から移動ベクトルを作る。
- 「常に移動方向へ向く」処理(LookAt)を外せば横移動=ストレーフになる。
// Camera 相対ストレーフ移動の最小例(CharacterController想定)
var cam = Camera.main.transform;
Vector3 camF = Vector3.ProjectOnPlane(cam.forward, Vector3.up).normalized;
Vector3 camR = Vector3.ProjectOnPlane(cam.right, Vector3.up).normalized;
Vector2 move = _move.ReadValue<Vector2>(); // x=左右, y=前後
Vector3 moveDir = (camR * move.x + camF * move.y).normalized;
// 向きを固定(例:カメラの向きに合わせる=狙いを前に保つ)
transform.rotation = Quaternion.LookRotation(camF, Vector3.up);
// 平行移動のみ
_characterController.Move(moveDir * moveSpeed * Time.deltaTime);
逆に「入力方向へ常に向かせる」なら LookAt(transform.position + moveDir) を入れる=非ストレーフ。
目的(照準を外さない・見栄え・操作感)に応じて、回転処理を入れるか外すかで切り替えます。
「移動の慣性(Lerp減衰)を入れる」とは、キャラクターの動きをピタッと止めず、少し滑らせるための処理です。
つまり、現実的な「慣性」や「重さ」を感じさせるテクニックです。
■ 現状の動き(慣性なし)
あなたが入力した PlayerController では:
_moveVelocity.x = moveValue.x * moveSpeed;
_moveVelocity.z = moveValue.y * moveSpeed;
としています。
これは、「キーを離した瞬間に速度が0になる」動きです。
結果として、キャラがピタッと止まり、軽い・ロボット的な印象になります。
■ 慣性を入れる(Lerp減衰)
「Lerp(線形補間)」を使って、現在の速度を少しずつ0に近づけることで、
スライドするような“滑り感”を出します。
// 現在の速度 _moveVelocity を、0 に近づけていく
_moveVelocity.x = Mathf.Lerp(_moveVelocity.x, 0, 0.1f);
_moveVelocity.z = Mathf.Lerp(_moveVelocity.z, 0, 0.1f);
この 0.1f の部分は「減速の速さ」です。
値を大きくするとすぐ止まり、小さくすると滑りが長くなります。
■ 条件付きにすると自然
常にLerpすると、動いている最中も速度が減りすぎるので、
入力がないときだけ減速するようにします。
if (moveValue.sqrMagnitude < 0.01f)
{
// 入力がないときだけ慣性で減速
_moveVelocity.x = Mathf.Lerp(_moveVelocity.x, 0, 0.1f);
_moveVelocity.z = Mathf.Lerp(_moveVelocity.z, 0, 0.1f);
}
else
{
// 入力中は即座に目標速度に
_moveVelocity.x = moveValue.x * moveSpeed;
_moveVelocity.z = moveValue.y * moveSpeed;
}
こうすることで、キーを離した瞬間も少し滑るような自然な動きになります。
■ イメージで理解する
状況 | 処理 | 結果 |
---|---|---|
入力中 | 速度 = 入力×スピード | 反応が早く軽快 |
入力を離した瞬間 | 速度 = 徐々に0に近づける(Lerp) | 滑るように減速 |
■ 物理ベースとの違い
Rigidbody を使う場合、物理演算で自動的に慣性が付きます(質量・摩擦・Dragなど)。
一方 CharacterController は自分で速度を決めるスクリプト制御なので、
このように人工的にLerpなどを使って「慣性風」の動きを作る必要があります。
■ 応用
- 空中だけ慣性強めにしたいなら:
float damping = _characterController.isGrounded ? 0.1f : 0.02f;
_moveVelocity.x = Mathf.Lerp(_moveVelocity.x, 0, damping);
_moveVelocity.z = Mathf.Lerp(_moveVelocity.z, 0, damping);
- キャラクターの重さを感じさせたい:damping値を小さく(滑りやすく)。
- 素早く止めたい:damping値を大きく(急停止)。
まとめ
Lerp減衰とは:「キーを離した瞬間に0にせず、少しずつ0に近づける」ことで、動きに重さや自然さを出すテクニックです。
実際に値を変えて「どこまで滑ると気持ちいいか?」を試すと、キャラの操作感の“味付け”がよく分かります。
4. デバッグ道場(“動かない”の原因を特定する手順)
- アタッチ漏れ:[RequireComponent] で防御。PlayerInputのDefault MapとAction名をUIで再確認。
- バインド漏れ:Move の 2D Vector が WASD/矢印/LeftStickに結びついているか。
- 衝突層:CharacterController の Center/Radius/Step Offset/Slope Limit が地形に適正か。
- フレーム依存:Time.timeScale 操作・deltaTime 抜けをチェック。
- ログ→最短経路:WasPressedThisFrame が実際に true になるフレームをログで可視化。
5. 設計に一歩:責務分離の小改造(発展)
5.1 入力/移動の分離
- IPlayerInput(移動Vector2・ジャンプboolを返す)
- CharacterMover(速度計算とMove実行)
- PlayerController は調停役だけにする→ Chapter7以降でアニメ/SE/エフェクトを足しても破綻しにくい形へ。
入力/移動を責務分離した最小サンプルを提示します。
目的:Input Systemの差し替え・数値調整・アニメ/SE追加がしやすい構成にすること。
構成
- IPlayerInput:入力の抽象化(移動ベクトル・ジャンプ押下)
- InputSystemPlayerInput:Input System を使った実装(差し替え可能)
- MovementConfig:移動パラメータの ScriptableObject
- CharacterMover:速度計算・重力・減衰・回転・CharacterController.Move
- PlayerController:1フレームの調停(入力→移動)だけ
1) 入力インターフェース
// IPlayerInput.cs
using UnityEngine;
public interface IPlayerInput
{
/// <summary>左右x/前後y(-1..1)</summary>
Vector2 Move { get; }
/// <summary>このフレームでジャンプが押されたか</summary>
bool JumpPressedThisFrame { get; }
/// <summary>毎フレーム更新(内部キャッシュ更新用)</summary>
void Tick();
}
2) Input System 実装
// InputSystemPlayerInput.cs
using UnityEngine;
using UnityEngine.InputSystem;
[RequireComponent(typeof(PlayerInput))]
public class InputSystemPlayerInput : MonoBehaviour, IPlayerInput
{
private InputAction _move;
private InputAction _jump;
public Vector2 Move { get; private set; }
public bool JumpPressedThisFrame { get; private set; }
void Awake()
{
var pi = GetComponent<PlayerInput>();
var map = pi.currentActionMap;
map.Enable();
_move = map.FindAction("Move");
_jump = map.FindAction("Jump");
}
public void Tick()
{
Move = _move.ReadValue<Vector2>();
JumpPressedThisFrame = _jump.WasPressedThisFrame();
}
}
将来、モバイル用バーチャルパッド実装に差し替える場合は、IPlayerInput を別クラスで実装して差し替えるだけ。
3) パラメータ(ScriptableObject)
// MovementConfig.cs
using UnityEngine;
[CreateAssetMenu(menuName = "Game/Movement Config")]
public class MovementConfig : ScriptableObject
{
[Header("Ground Movement")]
public float moveSpeed = 3f;
[Range(0f, 1f)] public float groundDamping = 0.12f; // 0=滑る,1=急停止
[Header("Air Movement")]
[Range(0f, 1f)] public float airDamping = 0.02f;
[Header("Jump/Gravity")]
public float jumpPower = 3f; // 初速
public float gravityScale = 1.0f; // 1=Physics.gravityそのまま
[Header("Facing")]
public bool faceMoveDirection = true; // 入力方向を向く(ストレーフならfalse)
}
4) 移動ロジック本体
// CharacterMover.cs
using UnityEngine;
[RequireComponent(typeof(CharacterController))]
public class CharacterMover : MonoBehaviour
{
[SerializeField] private MovementConfig config;
private CharacterController _cc;
private Transform _tr;
private Vector3 _velocity; // xz=水平速度, y=鉛直速度
void Awake()
{
_cc = GetComponent<CharacterController>();
_tr = transform;
}
public void ApplyMove(Vector2 input, bool jumpPressed)
{
// 1) 入力→目標水平速度(カメラ基準にしたい場合はここで変換)
Vector3 desired = new Vector3(input.x, 0f, input.y) * config.moveSpeed;
// 2) 減衰(入力が弱い/ゼロのときに滑らせる)
bool grounded = _cc.isGrounded;
float damping = grounded ? config.groundDamping : config.airDamping;
_velocity.x = Mathf.Lerp(_velocity.x, desired.x, input.sqrMagnitude > 0.001f ? 1f : damping);
_velocity.z = Mathf.Lerp(_velocity.z, desired.z, input.sqrMagnitude > 0.001f ? 1f : damping);
// 3) ジャンプ/重力
if (grounded)
{
if (jumpPressed)
_velocity.y = config.jumpPower;
else if (_velocity.y < 0f)
_velocity.y = -2f; // 接地維持のための少しの押し付け
}
else
{
_velocity.y += Physics.gravity.y * config.gravityScale * Time.deltaTime;
}
// 4) 回転(入力方向に向くか?)
if (config.faceMoveDirection)
{
Vector3 facing = new Vector3(_velocity.x, 0f, _velocity.z);
if (facing.sqrMagnitude > 0.0001f)
{
var targetRot = Quaternion.LookRotation(facing, Vector3.up);
_tr.rotation = Quaternion.Slerp(_tr.rotation, targetRot, 0.2f);
}
}
// 5) 実移動
_cc.Move(_velocity * Time.deltaTime);
}
}
ストレーフ移動にしたい場合は、faceMoveDirection = false にするだけ。
「カメラ相対移動」にしたい場合は (2)の desired をカメラ基準ベクトルで組み立てます。
カメラ相対の例(差し替え)
// var desired = new Vector3(input.x, 0f, input.y) * config.moveSpeed; を置き換え
var cam = Camera.main.transform;
Vector3 camF = Vector3.ProjectOnPlane(cam.forward, Vector3.up).normalized;
Vector3 camR = Vector3.ProjectOnPlane(cam.right, Vector3.up).normalized;
Vector3 desired = (camR * input.x + camF * input.y) * config.moveSpeed;
5) 調停役(1フレームの進行)
// PlayerController.cs
using UnityEngine;
[RequireComponent(typeof(CharacterMover))]
public class PlayerController : MonoBehaviour
{
[SerializeField] private MonoBehaviour inputProvider; // IPlayerInput を実装したコンポーネント
private IPlayerInput _input;
private CharacterMover _mover;
void Awake()
{
_mover = GetComponent<CharacterMover>();
_input = inputProvider as IPlayerInput;
if (_input == null)
Debug.LogError("inputProvider は IPlayerInput を実装している必要があります。");
}
void Update()
{
_input.Tick(); // 入力更新
_mover.ApplyMove(_input.Move, _input.JumpPressedThisFrame); // 反映
}
}
セットアップ手順
- シーンに Player を置く
- CharacterController を付与(Auto Sync Transforms ON推奨)
- PlayerInput を付与し、Action Map に Move (2D Vector) と Jump (Button) を用意
- MovementConfig を作成して数値を設定
- コンポーネント構成
- InputSystemPlayerInput(IPlayerInput 実装)
- CharacterMover(MovementConfig を割当)
- PlayerController の inputProvider に InputSystemPlayerInput をドラッグ
- (任意)カメラ相対移動にしたい場合は CharacterMover の desired 箇所を差し替え
- ストレーフにしたい場合は MovementConfig.faceMoveDirection = false
この分離のメリット
- 入力差し替え容易:PC用→スマホ用(バーチャルパッド)へ IPlayerInput 実装を入れ替えるだけ
- 調整担当の分離:スピード・ジャンプ・減衰は ScriptableObject で非エンジニアも調整可能
- 拡張しやすい:Chapter7以降の アニメ/SE/エフェクト は PlayerController にリスナを足すだけで干渉最小
- テスト容易:CharacterMover.ApplyMove() を固定入力でユニットテスト可能
5.2 パラメータの外だし
- moveSpeed/jumpPower/重力/加速度/減衰 を ScriptableObject に。
- チーム開発で数値調整をデザイナに解放。
「数値はScriptableObjectに外だし」して、デザイナがインスペクターで安全に調整できる形のサンプルを用意しました。
(前回の責務分離サンプルに“そのまま差し替え”できます)
1) MovementConfig(ScriptableObject)
// MovementConfig.cs
using UnityEngine;
[CreateAssetMenu(menuName = "Game/Movement Config", fileName = "MovementConfig")]
public class MovementConfig : ScriptableObject
{
[Header("Base Speeds")]
[Tooltip("地上の最大水平速度")]
public float moveSpeed = 3f;
[Tooltip("ジャンプ初速度(上向き)")]
public float jumpPower = 3f;
[Header("Acceleration / Deceleration (per second)")]
[Tooltip("地上:入力がある時の加速度(m/s^2 相当)")]
public float accelGround = 20f;
[Tooltip("空中:入力がある時の加速度")]
public float accelAir = 8f;
[Tooltip("地上:入力がない時の減速度(自然に止まる)")]
public float decelGround = 25f;
[Tooltip("空中:入力がない時の減速度")]
public float decelAir = 5f;
[Header("Damping (0..1, 1=即座)")]
[Range(0f, 1f)] public float groundDamping = 0.0f; // 追加の味付け用(通常0)
[Range(0f, 1f)] public float airDamping = 0.0f;
[Header("Gravity")]
[Tooltip("Physics.gravity を使わず任意の重力を使う?")]
public bool useCustomGravity = false;
[Tooltip("useCustomGravity=true のときに使う重力加速度(例:-9.81)")]
public float customGravityY = -9.81f;
[Tooltip("Physics.gravity.y に掛けるスケール(useCustomGravity=false の時のみ有効)")]
public float gravityScale = 1.0f;
[Tooltip("落下最大速度(速度クランプ)。0以下で無制限")]
public float maxFallSpeed = -30f;
[Header("Control")]
[Tooltip("入力方向に顔を向ける(falseでストレーフ)")]
public bool faceMoveDirection = true;
[Tooltip("空中制御の効き(0..1)。1で地上と同等、0で一切効かない")]
[Range(0f, 1f)] public float airControl = 0.7f;
[Tooltip("接地維持のための下向き押し付け(小さな段差対応)")]
public float groundStickForce = 2f;
}
2) CharacterMover(加速度&減衰対応版)
// CharacterMover.cs
using UnityEngine;
[RequireComponent(typeof(CharacterController))]
public class CharacterMover : MonoBehaviour
{
[SerializeField] private MovementConfig config;
private CharacterController _cc;
private Transform _tr;
private Vector3 _velocity; // xz:水平, y:鉛直
void Awake()
{
_cc = GetComponent<CharacterController>();
_tr = transform;
}
public void ApplyMove(Vector2 input, bool jumpPressed)
{
bool grounded = _cc.isGrounded;
// --- 1) 入力ベクトル → 目標水平速度(ワールド基準) ---
// ※ カメラ相対にしたい場合はここを差し替え(下に例あり)
Vector3 desired = new Vector3(input.x, 0f, input.y) * config.moveSpeed;
// 空中は操舵力を弱める
if (!grounded) desired *= Mathf.Lerp(0f, 1f, config.airControl);
// --- 2) 水平加速度・減速度 ---
float accel = grounded ? config.accelGround : config.accelAir;
float decel = grounded ? config.decelGround : config.decelAir;
float damping = grounded ? config.groundDamping : config.airDamping;
Vector2 curXZ = new Vector2(_velocity.x, _velocity.z);
Vector2 desXZ = new Vector2(desired.x, desired.z);
if (desXZ.sqrMagnitude > 0.0001f)
{
// 入力がある:目標速度へ加速
curXZ = Vector2.MoveTowards(curXZ, desXZ, accel * Time.deltaTime);
}
else
{
// 入力がない:ゼロへ減速
curXZ = Vector2.MoveTowards(curXZ, Vector2.zero, decel * Time.deltaTime);
}
// 追加の味付けとしてのダンピング(通常は0でOK)
curXZ = Vector2.Lerp(curXZ, desXZ, damping);
_velocity.x = curXZ.x;
_velocity.z = curXZ.y;
// --- 3) ジャンプ/重力 ---
if (grounded)
{
if (jumpPressed)
{
_velocity.y = config.jumpPower;
}
else
{
// 接地維持用にわずかに押し付け(段差の乗り越え安定)
if (_velocity.y < 0f) _velocity.y = -config.groundStickForce;
}
}
else
{
float gy = config.useCustomGravity
? config.customGravityY
: Physics.gravity.y * config.gravityScale;
_velocity.y += gy * Time.deltaTime;
if (config.maxFallSpeed < 0f)
_velocity.y = Mathf.Max(_velocity.y, config.maxFallSpeed);
}
// --- 4) 回転(入力方向に向くか) ---
if (config.faceMoveDirection)
{
Vector3 face = new Vector3(_velocity.x, 0f, _velocity.z);
if (face.sqrMagnitude > 0.0001f)
{
var to = Quaternion.LookRotation(face, Vector3.up);
_tr.rotation = Quaternion.Slerp(_tr.rotation, to, 0.2f);
}
}
// --- 5) 実移動 ---
_cc.Move(_velocity * Time.deltaTime);
}
}
カメラ相対(TPS/FPS向け)の desired 差し替え例
var cam = Camera.main.transform;
Vector3 camF = Vector3.ProjectOnPlane(cam.forward, Vector3.up).normalized;
Vector3 camR = Vector3.ProjectOnPlane(cam.right, Vector3.up).normalized;
Vector3 desired = (camR * input.x + camF * input.y) * config.moveSpeed;
if (!grounded) desired *= Mathf.Lerp(0f, 1f, config.airControl);
3) PlayerController(前回と同じ/そのまま)
前回提示の「入力/移動分離」版 PlayerController・IPlayerInput・InputSystemPlayerInput をそのまま使えます。
CharacterMover の config にこの MovementConfig を割り当ててください。
4) セットアップ手順(チームでの運用想定)
- プロジェクト内でSOを作るCreate > Game > Movement Config → MovementConfig.asset を作成(複数プロファイルOK)例:Player_Default, Player_Heavy, Player_Light など。
- 割り当て
- Playerに CharacterMover を付け、config に SO をドラッグ。
- PlayerController.inputProvider に InputSystemPlayerInput をアサイン。
- デザイナの調整フロー
- インスペクターで moveSpeed / accel / decel / jumpPower / gravity を調整。
- “重たい操作感”=accel小・decel小、“キビキビ”=accel大・decel大。
- 空中の効きは airControl。落下スピードは maxFallSpeed で安全に制限。
- プロファイル切り替え
- テスト用に SO を複製し、値を変えて Player に差し替えるだけ。
- ランタイム切り替えが必要なら、CharacterMover に SetConfig(MovementConfig cfg) を追加して差し替え可能。
5) 使い分けの指針(現場メモ)
- “加速度”メイン:挙動の理由が直感的に説明しやすい(時速が滑らかに変化)。
- “ダンピング”は味付け:0〜0.2程度で微調整。多用すると“ブレーキの二重掛け”で手触りが鈍る。
- 空中は別パラメータ:accelAir / decelAir / airControl を分けると調整が速い。
- 重力はSOで統一:レベルごとに重力を変えたい場合も、SO差し替えで安全運用。
必要であれば、このSOにアニメ遷移フラグ(Speed/IsGrounded)書き出し用の閾値や、曲線(AnimationCurve)で加減速を定義する版、CustomEditorで“プリセットボタン(Light/Heavy/Arcade)”付きのインスペクターも用意できます。
6. スキル定着ドリル(小テスト)
Q1(○×) WasPressedThisFrame は“押しっぱなし”の間ずっと true になる。
Q2(記述) LookAt の引数に “現在位置 + 何” を渡している?それで何を表す?
Q3(選択) 固定周期で呼ばれるのはどれ? A) Update B) FixedUpdate C) LateUpdate
Q4(穴埋め) 移動距離は 速度 × ( )。フレーム依存を避けるために何を掛けていますか?
Q5(応用) “空中でだけ水平速度を少し落とす” 処理を1行で書くなら?
<模範解答(例)>
A1:×(押下したそのフレームのみ true)
A2:速度の水平ベクトル(進行方向)。“向く先”を作っている。
A3:B
A4:Time.deltaTime
A5:_moveVelocity.xz *= 0.98f;(実装言語上はベクトル分解してLerp/係数掛け)
7. 口頭試問10(1人1分)
- CharacterController と Rigidbody の使い分けを説明。
- Update と FixedUpdate を混在させると何が起きやすい?
- SerializeField を使う意義(publicでない利点)。
- currentActionMap.Enable() を忘れたときの症状は?
- deltaTime を掛け忘れるとどうなる?
- スロープで上れない時、どのパラメータを疑う?
- ジャンプの“気持ちよさ”を上げる数値の方向性を2つ。
- 入力の同時押し(W+D)時の正規化対応をどうする?
- 画面外カメラで位置が飛ぶとき、何を疑う?
- “地上判定”の別実装を1つ挙げる(Ray/SphereCast 等)。
1) CharacterController と Rigidbody の使い分け
- CharacterController:自前で速度や重力を制御したい・段差/スロープを“ゲーム的”に扱いたい。Kinematic寄り、当たりは取るが完全物理ではない。
- Rigidbody:物理法則(質量・力・反発・摩擦)に従わせたい。Force/AddImpulse/Constraint 等を使う“物理駆動”。
2) Update と FixedUpdate を混在させると?
- 症状:カクつき・入力取りこぼし・速度の不安定化。
- 理由:Updateは可変Δt、FixedUpdateは固定Δt。サンプリング周期がズレる。
- 原則:入力はUpdate、物理(速度の適用/Force)はFixedUpdate。
3) SerializeField を使う意義(publicでない利点)
- 答え:カプセル化を維持しつつインスペクターから調整可。外部から勝手に書き換えられない(API面が安全)。
4) currentActionMap.Enable() を忘れた症状
- 答え:ReadValue/WasPressedThisFrame が常に0/false。入力が一切反映されない。
- 補足:ActionをEnableするか、PlayerInputの通知方式(Send Messages/Unity Events)を使う。
5) deltaTime を掛け忘れると?
- 答え:フレームレート依存になり、FPSが高いほど速く(/低いと遅く)なる。環境で挙動が変わる。
6) スロープで上れない時のパラメータ
- 優先:CharacterController.Slope Limit(傾斜の許容角)、Step Offset(段差乗り越え)、Skin Width。
- 地形側:コライダー形状・段差高さ・摩擦(物理材質)。
7) ジャンプの“気持ちよさ”を上げる数値の方向性×2
- 初速:jumpPower を上げる(立ち上がりをシャキッと)。
- 重力曲線:上昇は弱め/下降は強め(例:上昇中は gravityScale 0.8、下降中は 1.5)でキビキビ着地。
8) 同時押し(W+D)の正規化対応
- 答え:入力ベクトルを正規化してから速度適用。
Vector2 m = _move.ReadValue<Vector2>();
Vector3 dir = new Vector3(m.x,0,m.y);
if (dir.sqrMagnitude > 1e-6f) dir.Normalize();
velocityXZ = dir * moveSpeed;
- 理由:斜めだけ速くなる(√2倍)問題を防ぐ。
9) 画面外カメラで位置が飛ぶとき疑うこと
- カメラ追従の順序:LateUpdateで追従していない(Updateだと被写体より先に動く)。
- 補足:補間ミス、CinemachineのUpdate Method一致、Teleport/ワープ時の補間無効化。
10) “地上判定”の別実装(例)
- Raycast:足元から下へ Raycast(hit) して距離が閾値以内なら接地。
bool GroundedByRay()
{
return Physics.Raycast(transform.position + Vector3.up*0.1f,
Vector3.down, out _, 0.2f, groundMask);
}
- 他:SphereCast/OverlapSphere/CapsuleCast(段差・斜面に強い)。
必要なら、上記をA5プリント1枚のチェックシートや、1問10秒×10問の口頭試問スライド(答えは次スライド)に整えます。
8. よくあるハマり/対処
- 回転がガクガク:LookAt の目標がゼロベクトルになる瞬間を避ける(if(v.sqrMagnitude>0.0001f))。
- 二重入力:Editorの Simulate Touch From Mouse がON+マウス入力で二重化していないかを確認。
- 段差に引っかかる:Step Offset と段差の高さ、Min Move Distance を調整。
- フレームスキップ:可変フレーム時にFixedUpdateへ過度の処理を入れない。
9. チーム作業ミニタスク(30〜60分)
- A. カメラ追従(Cinemachine):Framing Transposer+Composer で被写体中心を保つ。
- B. 最小アニメ:Animator Controller に Idle/Run/Jump(3 state)を作成、Speed/IsGroundedで遷移。
- C. サウンド:ジャンプSEを AudioSource.PlayOneShotで実装、多重再生の閾値を決める。
- D. 数値表:moveSpeed/jumpPower/重力 の3軸で体感コメントを残すテンプレを共同編集。
- E. 検証動画:ビフォー/アフター 10秒×2本を録画 → 所感を1行で提出。
10. 評価ルーブリック(提出物:動作プロジェクト+検証メモ)
観点 | レベル1 | レベル2 | レベル3 |
---|---|---|---|
再現性 | 先生の環境で動かない | 動くが手順が曖昧 | 手順書あり・再現即可能 |
読解力 | 説明ができない | 部分的に説明可 | 行単位で日本語化できる |
実験力 | 数値を変えていない | 1〜2点のみ | 複数軸で比較・考察 |
設計力 | God Script化 | 分割されているが結合強 | 入力/移動/表示の責務分離 |
デバッグ | 勘で修正 | ログを出せる | 再現→ログ→仮説→確認の型 |
11. 次章へのブリッジ(7〜8章の仕込み)
- 7章のNavMeshは「移動の責務分離」をしておくと導入が速い。
- 8章のUIは “見える化(速度・接地・ジャンプ回数)” から始めると設計思考が育つ。
- アニメ/SE/エフェクトは値の調整権限をScriptableObjectで分離すると開発速度が上がる。
付録:チェックリスト
- PlayerInput の Default Map に Move/Jump がある
- currentActionMap.Enable() を呼んでいる
- CharacterController の Center/Radius/StepOffset/SlopeLimit を調整した
- deltaTime を全ての移動に掛けている
- 地上と空中で y の更新式が違う理由を説明できる
- LookAt ゼロベクトル対策を入れた
- ログで速度と接地状態を可視化した
- 1つ以上の独自改造(数値/処理)を入れて挙動を比較した
ディスカッション
コメント一覧
まだ、コメントがありません