【Unity本格入門 Unity6対応版】Chapter6までの振り返り


目次

― 写経を“理解・応用・設計”に変える学習ドリル一式

この記事の目的

  • Chapter1〜6で学んだ要素(基礎操作/C#基礎/Input System/移動・ジャンプ・当たり)をつなげて理解する
  • PlayerControllerコードを題材に、コードを読む→変える→検証するの習慣を作る
  • Chapter7以降(AI/NaviMesh, UI, SE/エフェクト, チューニング)に備えて設計思考の入口に立つ

1. 学びの地図(Chapter1〜6の要点を“技術→行動”で整理)

技術トピック“できる行動”への翻訳
1Unity/ゲーム開発の全体像目的と完成像を言語化し、最小機能のプロトタイプを切り出す
2Hub導入/プロジェクト作成/旧新入力の違い再現性ある環境構築Input Systemの初期セットアップ
3C#基礎(型・制御構造・クラス・ライフサイクル)仕様を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行だけ変える → 予測 → 実測 → 考察)

予測(どう変わる?)→ 実測(挙動/ログ/動画)→ 考察(なぜそうなる?次は?)

  1. 向きの抑制:LookAt(…) をコメントアウト → ストレーフ移動の可否を確認。
  2. ジャンプ調整:jumpPower を 1.5 / 6 に切り替え → 滞空時間の体感差を記録。
  3. 重力の強化:Physics.gravity = new Vector3(0, -20f, 0) をStartで上書き。
  4. 移動の慣性:地上でも y のみでなく x/z にLerp減衰(滑り感)を入れる。
  5. 入力サンプリング: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. デバッグ道場(“動かない”の原因を特定する手順)

  1. アタッチ漏れ:[RequireComponent] で防御。PlayerInputのDefault MapAction名をUIで再確認。
  2. バインド漏れ:Move の 2D Vector が WASD/矢印/LeftStickに結びついているか。
  3. 衝突層:CharacterController の Center/Radius/Step Offset/Slope Limit が地形に適正か。
  4. フレーム依存:Time.timeScale 操作・deltaTime 抜けをチェック。
  5. ログ→最短経路: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); // 反映
    }
}

セットアップ手順

  1. シーンに Player を置く
    • CharacterController を付与(Auto Sync Transforms ON推奨)
    • PlayerInput を付与し、Action Map に Move (2D Vector) と Jump (Button) を用意
  2. MovementConfig を作成して数値を設定
  3. コンポーネント構成
    • InputSystemPlayerInput(IPlayerInput 実装)
    • CharacterMover(MovementConfig を割当)
    • PlayerController の inputProvider に InputSystemPlayerInput をドラッグ
  4. (任意)カメラ相対移動にしたい場合は CharacterMover の desired 箇所を差し替え
  5. ストレーフにしたい場合は 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) セットアップ手順(チームでの運用想定)

  1. プロジェクト内でSOを作るCreate > Game > Movement Config → MovementConfig.asset を作成(複数プロファイルOK)例:Player_Default, Player_Heavy, Player_Light など。
  2. 割り当て
    • Playerに CharacterMover を付け、config に SO をドラッグ。
    • PlayerController.inputProvider に InputSystemPlayerInput をアサイン。
  3. デザイナの調整フロー
    • インスペクターで moveSpeed / accel / decel / jumpPower / gravity を調整。
    • “重たい操作感”=accel小・decel小、“キビキビ”=accel大・decel大
    • 空中の効きは airControl。落下スピードは maxFallSpeed で安全に制限。
  4. プロファイル切り替え
    • テスト用に 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分)

  1. CharacterController と Rigidbody の使い分けを説明。
  2. Update と FixedUpdate を混在させると何が起きやすい?
  3. SerializeField を使う意義(publicでない利点)。
  4. currentActionMap.Enable() を忘れたときの症状は?
  5. deltaTime を掛け忘れるとどうなる?
  6. スロープで上れない時、どのパラメータを疑う?
  7. ジャンプの“気持ちよさ”を上げる数値の方向性を2つ。
  8. 入力の同時押し(W+D)時の正規化対応をどうする?
  9. 画面外カメラで位置が飛ぶとき、何を疑う?
  10. “地上判定”の別実装を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つ以上の独自改造(数値/処理)を入れて挙動を比較した

訪問数 22 回, 今日の訪問数 22回

学習

Posted by hidepon