この記事では、Unity で 2D プラットフォーマーのキャラクター コントローラーの開発を続け、コントロールの構成と最適化の各ステップを徹底的に検証します。 前回の記事「 」では、物理的な動作や基本的な動きなど、キャラクターの基礎を作成する方法について詳しく説明しました。次は、入力処理や動的なカメラ追従など、より高度な側面について説明します。 Unity で 2D キャラクター コントローラーを作成する方法: パート 1 この記事では、Unity の新しい入力システムの設定、キャラクターを制御するためのアクティブなアクションの作成、ジャンプの有効化、プレイヤーのコマンドに対する適切な応答の確保について詳しく説明します。 この記事で説明したすべての変更を自分で実装したい場合は、この記事の基礎が含まれている「 」リポジトリ ブランチをダウンロードできます。または、最終結果を含む「 」ブランチをダウンロードすることもできます。 Character Body Character Controller 入力システムの設定 キャラクターを制御するコードを書き始める前に、プロジェクトで入力システムを構成する必要があります。このプラットフォームでは、数年前に導入された Unity の新しい入力システムを選択しました。このシステムは、従来のシステムよりも優れているため、今でも有効です。 入力システムは、入力処理に対してよりモジュール化され、柔軟なアプローチを提供するため、開発者はさまざまなデバイスのコントロールを簡単に設定し、追加の実装オーバーヘッドなしでより複雑な入力シナリオをサポートできます。 まず、Input System パッケージをインストールします。メインメニューから「ウィンドウ」→「パッケージ マネージャー」を選択して、パッケージ マネージャーを開きます。Unity レジストリ セクションで、「Input System」パッケージを見つけて、「インストール」をクリックします。 次に、「編集」→「プロジェクト設定」メニューからプロジェクト設定に移動します。「プレーヤー」タブを選択し、「アクティブ入力処理」セクションを見つけて、「入力システム パッケージ (新規)」に設定します。 これらの手順を完了すると、Unity は再起動を促します。再起動すると、船長のコントロールを構成する準備がすべて整います。 入力アクションの作成 フォルダーで、メイン メニューから入力アクションを作成します: 。ファイルに「Controls」という名前を付けます。 設定 Assets → Create → Input Actions Unity の入力システムは、開発者がキャラクターやゲーム要素のコントロールを構成できる強力で柔軟な入力管理ツールです。さまざまな入力デバイスをサポートしています。作成する入力アクションにより、集中的な入力管理が可能になり、セットアップが簡素化され、インターフェースがより直感的になります。 ファイルをダブルクリックして編集用に開き、「Character」という名前のキャラクター コントロールの を追加します。 コントロール アクション マップ Unity のアクション マップは、ゲーム内で特定のタスクを実行するためにさまざまなコントローラーやキーにリンクできるアクションのコレクションです。これはコントロールを整理する効率的な方法であり、開発者はコードを書き直すことなく入力を割り当てたり調整したりできます。詳細については、公式の を参照してください。 入力システム ドキュメント 最初のアクションは「移動」と呼ばれます。このアクションは、キャラクターの移動方向を定義します。 を「値」に設定し、 を「ベクター 2」に設定すると、4 方向への移動が可能になります。 アクション タイプ コントロール タイプ このアクションにバインディングを割り当てるには、[上/下/右/左の複合を追加] を選択し、使い慣れた WASD キーをそれぞれの方向に割り当てます。 「 」をクリックして設定を保存することを忘れないでください。この設定により、「移動」アクションのバインディングを、たとえば矢印キーやゲームパッドのジョイスティックに再割り当てできるようになります。 アセットを保存 次に、新しいアクション「ジャンプ」を追加します。 「ボタン」のままにし、新しい 「押す」を追加し、 「押して放す」に設定します。これは、ボタンの押下と放しの両方をキャプチャする必要があるためです。 アクション タイプは インタラクション トリガー動作を これで、キャラクター制御スキームは完了です。次のステップは、これらのアクションを処理するコンポーネントを作成することです。 キャラクターを左右に動かす キャラクター制御用に作成した入力アクションを コンポーネントにリンクして、制御コマンドに従ってキャラクターがシーン内をアクティブに移動できるようにします。 CharacterBody これを実行するには、移動制御を担当するスクリプトを作成し、わかりやすくするために 名前を付けます。このスクリプトでは、まずいくつかの基本フィールドを定義します。スクリプトによって直接制御される コンポーネント への参照を追加します。 CharacterController CharacterBody _characterBody また、キャラクターの移動速度 ( ) とジャンプの高さ ( ) のパラメータも設定します。さらに、 フィールドの目的を定義します。 _speed _jumpHeight _stopJumpFactor 多くの 2D プラットフォーム ゲームでは、ジャンプの高さを制御できることに気付いたかもしれません。ジャンプ ボタンを長く押すほど、キャラクターはより高くジャンプします。基本的に、ジャンプの開始時に最初の上向きの速度が適用され、ボタンを放すとこの速度が減少します。_stopJumpFactor ジャンプ ボタンを放したときに上向きの速度がどれだけ減少するかを決定します。 _stopJumpFactor 以下に記述するコードの例を示します。 // CharacterController.cs public class CharacterController : MonoBehaviour { [SerializeField] private CharacterBody _characterBody; [Min(0)] [SerializeField] private float _speed = 5; [Min(0)] [SerializeField] private float _jumpHeight = 2.5f; [Min(1)] [SerializeField] private float _stopJumpFactor = 2.5f; } 次に、キャラクターを左右に移動する機能を実装します。移動ボタンを押している間、キャラクターは障害物に関係なく指定された移動速度を維持する必要があります。これを実現するには、スクリプトに変数を追加して、表面に沿った現在の移動速度 (またはキャラクターが空中にいる場合は単に水平方向) を保存します。 // CharacterController.cs private float _locomotionVelocity; コンポーネントでは、この速度を設定するメソッドを導入します。 CharacterBody // CharacterBody.cs public void SetLocomotionVelocity(float locomotionVelocity) { Velocity = new Vector2(locomotionVelocity, _velocity.y); } 私たちのゲームには傾斜面がないので、この方法は非常に簡単です。より複雑なシナリオでは、物体の状態と表面の傾斜を考慮する必要があります。今のところは、速度の垂直成分を維持し、水平方向の 座標のみを変更します。 x 次に、すべてのフレームの メソッドでこの値を設定します。 Update // CharacterController.cs private void Update() { _characterBody.SetLocomotionVelocity(_locomotionVelocity); } 入力アクションからの信号を処理するメソッドを定義します。 Move // CharacterController.cs public void OnMove(InputAction.CallbackContext context) { var value = context.ReadValue<Vector2>(); _locomotionVelocity = value.x * _speed; } アクションは として定義されているため、コンテキストは、どのキーが押されたか、または放されたかに応じてベクトル値を提供します。たとえば、 キーを押すと、メソッド ベクトル (1, 0) を受け取ります。D と 同時に押すと、(1, 1) になります。すべてのキーを放すと、値 (0, 0) で トリガーされます。 Move Vector2 D OnMove D W OnMove キーの場合、ベクトル (-1, 0) になります。OnMove メソッドでは、受信したベクトルの水平成分を取得し、指定された移動速度 を掛けます。 A OnMove _speed キャラクターにジャンプを教える まず、 コンポーネントにジャンプの処理を教える必要がある。そのためには、ジャンプを担当するメソッドを追加します。 CharacterBody // CharacterBody.cs public void Jump(float jumpSpeed) { Velocity = new Vector2(_velocity.x, jumpSpeed); State = CharacterState.Airborne; } 私たちの場合、この方法は簡単です。垂直速度を設定し、すぐにキャラクターの状態を に変更します。 Airborne 次に、キャラクターがジャンプする速度を決定する必要があります。ジャンプの高さはすでに定義されており、重力が常に体に作用することがわかっています。これに基づいて、最初のジャンプ速度は次の式を使用して計算できます。 ここで、 ジャンプの高さ、 は重力加速度です。CharacterBody コンポーネントに存在する重力乗数も考慮します。初期ジャンプ速度を定義する新しいフィールドを追加し、次のように計算します。 h CharacterBody g // CharacterController.cs private float _jumpSpeed; private void Awake() { _jumpSpeed = Mathf.Sqrt(2 * Physics2D.gravity.magnitude * _characterBody.GravityFactor * _jumpHeight); } キャラクターが現在ジャンプしているかどうかを追跡し、適切なタイミングでジャンプ速度を制限できるようにするための別のフィールドが必要になります。 さらに、プレイヤーが着地するまでジャンプボタンを押し続ける場合、このフラグを自分でリセットする必要があります。これは メソッドで実行されます。 Update // CharacterController.cs private bool _isJumping; private void Update() { if (_characterBody.State == CharacterState.Grounded) { _isJumping = false; } //... } 次に、 アクションを処理するメソッドを記述します。 Jump // CharacterController.cs public void OnJump(InputAction.CallbackContext context) { if (context.started) { Jump(); } else if (context.canceled) { StopJumping(); } } アクションはボタンなので、ボタンの押下が開始されたか ( )、終了したか ( ) をコンテキストから判断できます。これに基づいて、ジャンプを開始または停止します。 Jump context.started context.canceled ジャンプを実行する方法は次のとおりです。 // CharacterController.cs private void Jump() { if (_characterBody.State == CharacterState.Grounded) { _isJumping = true; _characterBody.Jump(_jumpSpeed); } } ジャンプする前に、キャラクターが地面にいるかどうかを確認します。地面にいる場合は、 フラグを設定し、 で体をジャンプさせます。 _isJumping _jumpSpeed 次に、ジャンプ停止動作を実装します。 // CharacterController.cs private void StopJumping() { var velocity = _characterBody.Velocity; if (_isJumping && velocity.y > 0) { _isJumping = false; _characterBody.Velocity = new Vector2( velocity.x, velocity.y / _stopJumpFactor); } } フラグがアクティブな場合のみ、ジャンプを停止します。もう 1 つの重要な条件は、キャラクターが上向きに移動している必要があることです。これにより、下向きに移動中にジャンプ ボタンが放された場合に落下速度が制限されるのを防ぎます。すべての条件が満たされた場合、 フラグをリセットし、垂直速度を の係数で減らします。 _isJumping _isJumping _stopJumpFactor キャラクターの設定 すべてのコンポーネントの準備ができたので、シーン内の オブジェクトに コンポーネントと コンポーネントを追加します。3D キャラクターを制御するために設計された標準の Unity コンポーネントではなく、作成した コンポーネントを選択してください。 Captain PlayerInput CharacterController CharacterController には、キャラクターの既存の コンポーネントを割り当てます。 には、以前に作成した フィールドに設定します。 CharacterController CharacterBody PlayerInput コントロールを Actions 次に、PlayerInput コンポーネントを構成して、CharacterController から適切なメソッドを呼び出します。エディターでイベントとキャラクターのセクションを展開し、対応するメソッドを Move アクションと Jump アクションにリンクします。 これで、ゲームを実行し、構成されたすべてのコンポーネントがどのように連携するかをテストする準備が整いました。 カメラの動き ここで、キャラクターがどこへ行ってもカメラが追従するようにする必要があります。Unity には、カメラ管理のための強力なツールである が用意されています。 Cinemachine Cinemachine は、Unity のカメラ制御のための革新的なソリューションであり、ゲームプレイのニーズに適応する動的で適切に調整されたカメラ システムを作成するための幅広い機能を開発者に提供します。このツールを使用すると、キャラクターの追跡、自動フォーカス調整などの複雑なカメラ技術を簡単に実装でき、あらゆるシーンに活気と豊かさを加えることができます。 まず、シーン内の オブジェクトを見つけて、そこに コンポーネントを追加します。 Main Camera CinemachineBrain 次に、シーン内に という名前の新しいオブジェクトを作成します。これは、プロのカメラマンのように船長を カメラになります。これに コンポーネントを追加します。Follow フィールドを船長に設定し、 フィールドで を選択し、 パラメータを 4 に設定します。 CaptainCamera 追う CinemachineVirtualCamera Body Framing Transposer Lens Ortho Size さらに、キャラクターに対するカメラのオフセットを定義 別のコンポーネント、 も必要になります。Y 値を 1.5、 値を -15 に設定します。 する CinemachineCameraOffset Z それでは、カメラがキャラクターをどのように追従するかをテストしてみましょう。 かなりうまくいったと思います。カメラが時々少しカクつくことに気づきました。これを修正するために、Main Camera オブジェクトの Blend Update Method フィールドを FixedUpdate に設定しました。 ジャンプの向上 更新されたメカニズムをテストしてみましょう。走ったり、連続ジャンプしたりしてみてください。経験豊富なゲーマーは、ジャンプが常に認識されるわけではないことに気付くかもしれません。ほとんどのゲームでは、これは問題ではありません。 着地の正確なタイミングを予測してジャンプ ボタンをもう一度押すのは難しいことがわかりました。着地の少し前にジャンプ ボタンを押して、着地と同時にキャラクターがジャンプできるようにすることで、ゲームをもっと寛容にする必要があります。この動作は、ゲーマーが慣れている動作と一致します。 これを実装するために、機会があればジャンプをトリガーできる時間枠を表す新しい変数 を導入します。 _jumpActionTime // CharacterController.cs [Min(0)] [SerializeField] private float _jumpActionTime = 0.1f; ジャンプ アクション ウィンドウの終了を示す フィールドを追加しました。つまり、 に達するまで、機会があればキャラクターはジャンプします。 アクション ハンドラーも更新しましょう。 _jumpActionEndTime _jumpActionEndTime Jump // CharacterController.cs private float _jumpActionEndTime; public void OnJump(InputAction.CallbackContext context) { if (context.started) { if (_characterBody.State == CharacterState.Grounded) { Jump(); } else { _jumpActionEndTime = Time.unscaledTime + _jumpActionTime; } } else if (context.canceled) { StopJumping(); } } ジャンプ ボタンが押されたとき、キャラクターが地面にいる場合はすぐにジャンプします。そうでない場合は、ジャンプを実行できる時間枠を保存します。 メソッド自体から 状態チェックを削除しましょう。 Jump Grounded // CharacterController.cs private void Jump() { _isJumping = true; _characterBody.Jump(_jumpSpeed); } ジャンプ停止メソッドも適用します。着地前にボタンが放された場合はジャンプが発生しないため、 をリセットします。 _jumpActionEndTime // CharacterController.cs private void StopJumping() { _jumpActionEndTime = 0; //... } キャラクターが着地したことを確認し、ジャンプをトリガーするのはいつでしょうか? 状態は で処理され、アクションの処理は後で行われます。 か かに関係なく、着地とジャンプの間に 1 フレームの遅延が発生する可能性があり、これは顕著です。 CharacterBody FixedUpdate Update FixedUpdate 着地時に即座に反応するために、 に イベントを追加します。最初の引数は前の状態、2 番目の引数は現在の状態になります。 CharacterBody StateChanged // CharacterBody.cs public event Action<CharacterState, CharacterState> StateChanged; 状態変更イベントをトリガーするように状態管理を調整し、 を書き換えます。 FixedUpdate // CharacterBody.cs [field: SerializeField] private CharacterState _state; public CharacterState State { get => _state; private set { if (_state != value) { var previousState = _state; _state = value; StateChanged?.Invoke(previousState, value); } } } また、 で が処理される方法も改良しました。 FixedUpdate surfaceHit // CharacterBody.cs private void FixedUpdate() { //... if (_velocity.y <= 0 && slideResults.surfaceHit) { var surfaceHit = slideResults.surfaceHit; Velocity = ClipVector(_velocity, surfaceHit.normal); if (surfaceHit.normal.y >= _minGroundVertical) { State = CharacterState.Grounded; return; } } State = CharacterState.Airborne; } では、 イベントをサブスクライブし、ハンドラーを追加します。 CharacterController StateChanged // CharacterController.cs private void OnEnable() { _characterBody.StateChanged += OnStateChanged; } private void OnDisable() { _characterBody.StateChanged -= OnStateChanged; } private void OnStateChanged(CharacterState previousState, CharacterState state) { if (state == CharacterState.Grounded) { OnGrounded(); } } から 状態チェックを削除し、 に移動します。 Update Grounded OnGrounded // CharacterController.cs private void Update() { _characterBody.SetLocomotionVelocity(_locomotionVelocity); } private void OnGrounded() { _isJumping = false; } ここで、ジャンプをトリガーする必要があるかどうかを確認するコードを追加します。 // CharacterController.cs private void OnGrounded() { _isJumping = false; if (_jumpActionEndTime > Time.unscaledTime) { _jumpActionEndTime = 0; Jump(); } } が現在の時刻より大きい場合は、ジャンプ ボタンが最近押されたことを意味するため、 リセットしてジャンプを実行します。 _jumpActionEndTime _jumpActionEndTime 次に、キャラクターで連続ジャンプを試してみましょう。ジャンプ ボタンの反応が良くなり、キャラクターの操作がスムーズになっていることに気づくでしょう。ただし、下の図に示すコーナーなどの特定の状況では、 状態でわずかな遅延が発生し、ジャンプ チェーンが中断されることがわかりました。 Grounded これに対処するために、 コンポーネントの フィールドを 0.01 ではなく 0.05 に設定しました。この値は、ボディが 状態に入るための表面までの最小距離を表します。 CharacterBody Surface Anchor Grounded 崖ジャンプ 垂直面から走りながらジャンプしようとすると、必ずしもうまくいかないことに気づいたかもしれません。ジャンプ ボタンが反応しないように感じることがあります。 これは、2D プラットフォーム ゲーム用の 開発する際の微妙な点の 1 つです。プレイヤーは、ジャンプ ボタンを押すのが少し遅れた場合でもジャンプできる必要があります。この概念は奇妙に思えるかもしれませんが、ほとんどのプラットフォーム ゲームはこのように機能します。その結果、以下のアニメーションに示すように、キャラクターが空中を蹴っているように見えます。 キャラクター コントローラーを このメカニズムを実装してみましょう。キャラクターが 状態を失った後もジャンプできる時間枠 (秒単位) を格納する新しいフィールドを導入します。 Grounded // CharacterController.cs [Min(0)] [SerializeField] private float _rememberGroundTime = 0.1f; また、 状態が「忘れられる」までのタイムスタンプを保存するための別のフィールドも追加します。 Grounded // CharacterController.cs private float _lostGroundTime; この状態は、 イベントを使用して追跡されます。この目的のために、 ハンドラーを調整します。 CharacterBody OnStateChanged // CharacterController.cs private void OnStateChanged(CharacterState previousState, CharacterState state) { if (state == CharacterState.Grounded) { OnGrounded(); } else if (previousState == CharacterState.Grounded) { _lostGroundTime = Time.unscaledTime + _rememberGroundTime; } } キャラクターが 状態を失ったのは意図的なジャンプによるものか、それとも別の理由によるものかを見分けることが重要です。_isJumping フラグはすでにありますが、これは冗長なアクションを防ぐために 呼び出されるたびに無効になります。 Grounded _isJumping StopJumping 余分な キャンセルはゲームプレイに影響しないので、別のフラグを導入しないことにしました。自由に実験してみてください。_isJumping フラグは、キャラクターがジャンプ後に着地したときにのみクリアされるようになりました。それに応じてコードを更新しましょう。 _isJumping // CharacterController.cs private void StopJumping() { _jumpActionEndTime = 0; var velocity = _characterBody.Velocity; if (_isJumping && velocity.y > 0) { _characterBody.Velocity = new Vector2( velocity.x, velocity.y / _stopJumpFactor); } } 最後に、 メソッドを修正します。 OnJump // CharacterController.cs public void OnJump(InputAction.CallbackContext context) { if (context.started) { if (_characterBody.State == CharacterState.Grounded || (!_isJumping && _lostGroundTime > Time.unscaledTime)) { Jump(); } else { _jumpActionEndTime = Time.unscaledTime + _jumpActionTime; } } else if (context.canceled) { StopJumping(); } } 垂直面から飛び降りてもゲームプレイのリズムが乱れることはなくなり、一見不合理に思えるにもかかわらず、はるかに自然に感じられるようになりました。キャラクターは文字通り空気を蹴って、論理的に思えるよりも遠くまで行くことができます。しかし、これはまさに私たちのプラットフォーム ゲームに必要なことです。 キャラクター反転 最後の仕上げは、キャラクターを移動方向に向けることです。これを最も簡単な方法で実装します。つまり、キャラクターのスケールを x 軸に沿って変更します。負の値を設定すると、キャプテンは反対方向を向きます。 まず、元のスケールが 1 と異なる場合に備えて保存しておきます。 // CharacterController.cs public class CharacterController : MonoBehaviour { //... private Vector3 _originalScale; private void Awake() { //... _originalScale = transform.localScale; } } ここで、左または右に移動するときに、正または負のスケールを適用します。 // CharacterController.cs public class CharacterController : MonoBehaviour { public void OnMove(InputAction.CallbackContext context) { //... // Change character's direction. if (value.x != 0) { var scale = _originalScale; scale.x = value.x > 0 ? _originalScale.x : -_originalScale.x; transform.localScale = scale; } } } 結果をテストしてみましょう。 まとめ この記事はかなり詳細になりましたが、2D プラットフォーマーにおけるキャラクター制御の重要な側面をすべて網羅することができました。参考までに、最終結果はリポジトリの「 」ブランチで確認できます。 Character Controller この記事や前回の記事が役に立ったり、気に入っていただけた場合は、GitHub で「いいね!」や「スター」を付けていただけると嬉しいです。問題が発生した場合や間違いを見つけた場合は、遠慮なくご連絡ください。ご清聴ありがとうございました。