学習記事一覧 · Unity本格入門

Unity本格入門:いきのこバトルで学ぶプレイヤーと新 Input System

題材・出典: 技術評論社刊『作って学べる Unity本格入門[Unity 6対応版]』(賀好 昭仁 著)に基づく学習補助の解説です。書籍の代替提供を目的とせず、コード掲載は学習上必要な範囲(必要最小限)にとどめます。利用条件は書籍記載(P.4〜5)および出版社サポート情報に従ってください。本シリーズ目次(書籍・著作の注記)

対象読者:第2回まで読み、メインシーンの流れを把握したあと、操作・移動・攻撃のコードを追いたい方
PlayerController.cs を中心に、PlayerInputInputActionCharacterController のつながりを読み解きます。

前提第2回:メインシーンの時間と昼夜PlayerStatus の満腹や死亡時の GameOver は、ここでは「結果として MainSceneController.Instance.GameOver() が呼ばれる」程度に留め、攻撃・投擲の細部は必要な範囲だけ示します。

補足Assets/PlayerAction.inputactions でアクション名(Move, Fire, Jump, ToggleThrowAxe)が定義されています。エディターでは Player Input コンポーネントがこのアセットを参照します。


記事の目次

ポイント一覧


この記事で扱う範囲

  • 主にPlayerController.csStartUpdate の流れ
  • 深掘りしないMobAttackThrowAxe の内部、OwnedItemsData の保存形式(必要な箇所はコメントで示す)

新 Input System と CharacterController で操作をキャラへ渡すイメージ

説明(学習のヒント)PlayerInput から InputAction を取り、移動・攻撃・ジャンプ・投擲へ振り分けるイメージです。UI 上のクリックと攻撃の切り分けもこの記事で触れます。


コードを全部見てみよう

PlayerController.cs(全文)

using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.InputSystem;
[RequireComponent(typeof(CharacterController))]
[RequireComponent(typeof(PlayerInput))]
[RequireComponent(typeof(PlayerStatus))]
[RequireComponent(typeof(MobAttack))]
public class PlayerController : SingletonMonoBehaviourInSceneBase<PlayerController>
{
    private static readonly int MoveSpeed = Animator.StringToHash("MoveSpeed");
    [SerializeField] private Animator animator;
    [SerializeField] private float moveSpeed = 3;
    [SerializeField] private float jumpPower = 3;
    [SerializeField] private ThrowAxe throwAxePrefab;
    private MobAttack _attack;
    private CharacterController _characterController;
    private InputAction _fire;
    private bool _isReadyToThrow;
    private InputAction _jump;
    private InputAction _move;
    private Vector3 _moveVelocity;
    private PlayerStatus _status;
    private InputAction _toggleThrowAxe;
    private Transform _transform;
    public bool IsReadyToThrow
    {
        get => _isReadyToThrow;
        set
        {
            _isReadyToThrow = value;
            Menu.Instance.IsThrowAxeActive = _isReadyToThrow;
        }
    }
    private void Start()
    {
        _characterController = GetComponent<CharacterController>();
        _transform = transform;
        var input = GetComponent<PlayerInput>();
        input.currentActionMap.Enable();
        _move = input.currentActionMap.FindAction("Move");
        _fire = input.currentActionMap.FindAction("Fire");
        _jump = input.currentActionMap.FindAction("Jump");
        _toggleThrowAxe = input.currentActionMap.FindAction("ToggleThrowAxe");
        _status = GetComponent<PlayerStatus>();
        _attack = GetComponent<MobAttack>();
    }
    private void Update()
    {
        if (!EventSystem.current.IsPointerOverGameObject() && _fire.WasReleasedThisFrame())
        {
            if (IsReadyToThrow)
            {
                if (OwnedItemsData.Instance.GetItem(Item.ItemType.ThrowAxe).Number <= 0)
                {
                    Debug.Log("投げオノを持ってないよ〜");
                }
                else
                {
                    if (Camera.main != null && Mouse.current != null)
                    {
                        var mousePointerRay = Camera.main.ScreenPointToRay(Mouse.current.position.value);
                        if (Physics.Raycast(mousePointerRay, out var hit))
                        {
                            var throwAxe = Instantiate(throwAxePrefab);
                            var target = hit.collider.name == "Terrain"
                                ? hit.point
                                : hit.collider.transform.position + Vector3.up;
                            throwAxe.Initialize(transform.position + Vector3.up, target, 20f);
                            OwnedItemsData.Instance.Use(Item.ItemType.ThrowAxe);
                            OwnedItemsData.Instance.Save();
                            AudioManager.Instance.Play("throw");
                            if (OwnedItemsData.Instance.GetItem(Item.ItemType.ThrowAxe).Number <= 0)
                                IsReadyToThrow = false;
                        }
                    }
                }
            }
            else
            {
                _attack.AttackIfPossible();
            }
        }
        if (_toggleThrowAxe.WasReleasedThisFrame())
            ToggleReadyToThrow();
        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())
            {
                Debug.Log("ジャンプ!");
                _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);
    }
    public void ToggleReadyToThrow()
    {
        if (OwnedItemsData.Instance.GetItem(Item.ItemType.ThrowAxe).Number <= 0)
            IsReadyToThrow = false;
        else
            IsReadyToThrow = !IsReadyToThrow;
        Debug.Log("投擲モード" + (IsReadyToThrow ? "ON" : "OFF"));
    }
}

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

classDiagram direction TB class MonoBehaviour class CharacterController class PlayerInput class PlayerStatus class MobAttack class PlayerController MonoBehaviour <|-- PlayerController PlayerController ..> CharacterController : "RequireComponent" PlayerController ..> PlayerInput : "RequireComponent" PlayerController ..> PlayerStatus : "RequireComponent" PlayerController ..> MobAttack : "RequireComponent"

説明(学習のヒント)PlayerController が1つに対し、[RequireComponent] で揃えたいコンポーネントが4つぶら下がるイメージです。


UMLで整理する(シーケンス)

Update 内で入力を読み、移動・攻撃・重力を CharacterController.Move にまとめる流れを簡略化したものです。

sequenceDiagram participant PC as PlayerController participant PI as PlayerInput participant CC as CharacterController loop Each frame PC->>PI: "InputAction 取得" PI-->>PC: "入力値" PC->>CC: "Move" end

説明(学習のヒント):毎フレームのループの中で、入力を読み取り、最後に CharacterController で移動する流れです。


ポイント①:[RequireComponent] で依存を明示する

CharacterControllerPlayerInputPlayerStatusMobAttack が無いと動かない、とコンポーネントに宣言しています。エディターでスクリプトを付けたときに同時に足りないコンポーネントが追加され、設定漏れを防ぎやすくなります。


ポイント②:PlayerInputFindAction で操作を取得する

var input = GetComponent<PlayerInput>();
input.currentActionMap.Enable();
_move = input.currentActionMap.FindAction("Move");

新 Input System では、アクション名(PlayerActionPlayer マップ内)で InputAction を取得します。キーボード・パッドの割り当ては .inputactions 側で一括管理できます。


ポイント③:ReadValue<Vector2>() と移動速度

var moveValue = _move.ReadValue<Vector2>();
_moveVelocity.x = moveValue.x * moveSpeed;
_moveVelocity.z = moveValue.y * moveSpeed;

2D の入力(WASD やスティック)を、XZ 平面の移動に写しています。moveValue.yZ 方向に使っている点に注意(Input の「前後」が Z)。


ポイント④:CharacterController:重力・ジャンプ・Move

  • 地上ではジャンプ入力で _moveVelocity.y に初速を与える
  • 空中では Physics.gravity.y * Time.deltaTime で落下を加速
  • 最後に _characterController.Move(_moveVelocity * Time.deltaTime)一括移動

教科書シリーズの Rigidbody 物理とは別系統で、キビキビした操作向きのやり方です。


ポイント⑤:WasPressedThisFrame / WasReleasedThisFrame

  • ジャンプ:WasPressedThisFrame()(押した瞬間)
  • 攻撃/投擲の発動:FireWasReleasedThisFrame()(離した瞬間)

「押しっぱなし」と「タップ」を分けたいときに使い分けます。


ポイント⑥:UI 上のクリックを無視して Fire を処理する

if (!EventSystem.current.IsPointerOverGameObject() && _fire.WasReleasedThisFrame())

メニューやボタンがあると、ゲーム世界へのクリックと被りますIsPointerOverGameObject() で「UI の上か」を判定し、UI のときは攻撃処理に入らないようにしています。


ポイント⑦:投擲モードとレイキャスト・Instantiate の概要

投擲スタンバイ中は、Camera.main.ScreenPointToRayPhysics.Raycast地面や敵への当たりを取り、当たった位置を目標に throwAxePrefabInstantiate しています。通常攻撃は _attack.AttackIfPossible() に任せています。MenuOwnedItemsData は UI・所持品と連動する別クラスです。


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

flowchart TD u["Update"] u --> fire{"Fire と UI"} fire -->|"no"| atk["攻撃 or 投擲"] fire -->|"yes"| skip["攻撃スキップ"] u --> toggle{"ToggleThrowAxe"} toggle --> mode["投擲モード切替"] u --> move["Move 入力"] u --> jump["ジャンプ"] u --> grav["重力"] u --> cc["CharacterController.Move"] u --> anim["Animator 速度"]

説明(学習のヒント)Update から複数の処理が出ています。Fire の菱形は「UI の上のクリックか」で分岐するイメージです。


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

挑戦①:moveSpeedjumpPower

Inspector で変えられるので、手触りを変えてみましょう。

挑戦②:移動しない条件

_status.IsMovablefalse のとき水平速度を 0 にしているので、スタンなどの状態を足すときの拡張ポイントになります。


まとめ

  • [RequireComponent]コンポーネントのキャッシュで、Update を軽く保つ
  • PlayerInput から FindActionInputAction を取得し、ReadValue / WasPressed / WasReleased で入力を解釈する
  • CharacterController.Move重力・ジャンプで 3D 移動を構成する
  • EventSystem.IsPointerOverGameObject で UI とゲーム操作の衝突を避ける

次は 第4回:敵スポーンと NavMesh で、敵の出現ループを読みます。


最終更新:2026年4月