この記事では、Unity で 2D プラットフォーマーのキャラクター コントローラーの開発を続け、コントロールの構成と最適化の各ステップを徹底的に検証します。
前回の記事「 Unity で 2D キャラクター コントローラーを作成する方法: パート 1 」では、物理的な動作や基本的な動きなど、キャラクターの基礎を作成する方法について詳しく説明しました。次は、入力処理や動的なカメラ追従など、より高度な側面について説明します。
この記事では、Unity の新しい入力システムの設定、キャラクターを制御するためのアクティブなアクションの作成、ジャンプの有効化、プレイヤーのコマンドに対する適切な応答の確保について詳しく説明します。
この記事で説明したすべての変更を自分で実装したい場合は、この記事の基礎が含まれている「 Character Body 」リポジトリ ブランチをダウンロードできます。または、最終結果を含む「 Character Controller 」ブランチをダウンロードすることもできます。
キャラクターを制御するコードを書き始める前に、プロジェクトで入力システムを構成する必要があります。このプラットフォームでは、数年前に導入された Unity の新しい入力システムを選択しました。このシステムは、従来のシステムよりも優れているため、今でも有効です。
入力システムは、入力処理に対してよりモジュール化され、柔軟なアプローチを提供するため、開発者はさまざまなデバイスのコントロールを簡単に設定し、追加の実装オーバーヘッドなしでより複雑な入力シナリオをサポートできます。
まず、Input System パッケージをインストールします。メインメニューから「ウィンドウ」→「パッケージ マネージャー」を選択して、パッケージ マネージャーを開きます。Unity レジストリ セクションで、「Input System」パッケージを見つけて、「インストール」をクリックします。
次に、「編集」→「プロジェクト設定」メニューからプロジェクト設定に移動します。「プレーヤー」タブを選択し、「アクティブ入力処理」セクションを見つけて、「入力システム パッケージ (新規)」に設定します。
これらの手順を完了すると、Unity は再起動を促します。再起動すると、船長のコントロールを構成する準備がすべて整います。
設定フォルダーで、メイン メニューから入力アクションを作成します: Assets → Create → Input Actions 。ファイルに「Controls」という名前を付けます。
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; }
Move
アクションはVector2
として定義されているため、コンテキストは、どのキーが押されたか、または放されたかに応じてベクトル値を提供します。たとえば、 D
キーを押すと、メソッドOnMove
ベクトル (1, 0) を受け取ります。D D
とW
同時に押すと、(1, 1) になります。すべてのキーを放すと、値 (0, 0) でOnMove
トリガーされます。
A
キーの場合、ベクトルOnMove
(-1, 0) になります。OnMove メソッドでは、受信したベクトルの水平成分を取得し、指定された移動速度_speed
を掛けます。
まず、 CharacterBody
コンポーネントにジャンプの処理を教える必要がある。そのためには、ジャンプを担当するメソッドを追加します。
// CharacterBody.cs public void Jump(float jumpSpeed) { Velocity = new Vector2(_velocity.x, jumpSpeed); State = CharacterState.Airborne; }
私たちの場合、この方法は簡単です。垂直速度を設定し、すぐにキャラクターの状態をAirborne
に変更します。
次に、キャラクターがジャンプする速度を決定する必要があります。ジャンプの高さはすでに定義されており、重力が常に体に作用することがわかっています。これに基づいて、最初のジャンプ速度は次の式を使用して計算できます。
ここで、 h CharacterBody
ジャンプの高さ、 gは重力加速度です。CharacterBody コンポーネントに存在する重力乗数も考慮します。初期ジャンプ速度を定義する新しいフィールドを追加し、次のように計算します。
// 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); } }
_isJumping
フラグがアクティブな場合のみ、ジャンプを停止します。もう 1 つの重要な条件は、キャラクターが上向きに移動している必要があることです。これにより、下向きに移動中にジャンプ ボタンが放された場合に落下速度が制限されるのを防ぎます。すべての条件が満たされた場合、 _isJumping
フラグをリセットし、垂直速度を_stopJumpFactor
の係数で減らします。
すべてのコンポーネントの準備ができたので、シーン内のCaptainオブジェクトにPlayerInput
コンポーネントとCharacterController
コンポーネントを追加します。3D キャラクターを制御するために設計された標準の Unity コンポーネントではなく、作成したCharacterController
コンポーネントを選択してください。
CharacterController
には、キャラクターの既存のCharacterBody
コンポーネントを割り当てます。 PlayerInput
には、以前に作成したコントロールをActions
フィールドに設定します。
次に、PlayerInput コンポーネントを構成して、CharacterController から適切なメソッドを呼び出します。エディターでイベントとキャラクターのセクションを展開し、対応するメソッドを Move アクションと Jump アクションにリンクします。
これで、ゲームを実行し、構成されたすべてのコンポーネントがどのように連携するかをテストする準備が整いました。
ここで、キャラクターがどこへ行ってもカメラが追従するようにする必要があります。Unity には、カメラ管理のための強力なツールであるCinemachineが用意されています。
Cinemachine は、Unity のカメラ制御のための革新的なソリューションであり、ゲームプレイのニーズに適応する動的で適切に調整されたカメラ システムを作成するための幅広い機能を開発者に提供します。このツールを使用すると、キャラクターの追跡、自動フォーカス調整などの複雑なカメラ技術を簡単に実装でき、あらゆるシーンに活気と豊かさを加えることができます。
まず、シーン内のMain Cameraオブジェクトを見つけて、そこにCinemachineBrain
コンポーネントを追加します。
次に、シーン内にCaptainCameraという名前の新しいオブジェクトを作成します。これは、プロのカメラマンのように船長を追うカメラになります。これにCinemachineVirtualCamera
コンポーネントを追加します。Follow フィールドを船長に設定し、 BodyフィールドでFraming Transposerを選択し、 Lens Ortho Sizeパラメータを 4 に設定します。
さらに、キャラクターに対するカメラのオフセットを定義する別のコンポーネント、 CinemachineCameraOffset
も必要になります。Y 値を 1.5、 Z値を -15 に設定します。
それでは、カメラがキャラクターをどのように追従するかをテストしてみましょう。
かなりうまくいったと思います。カメラが時々少しカクつくことに気づきました。これを修正するために、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; //... }
キャラクターが着地したことを確認し、ジャンプをトリガーするのはいつでしょうか? CharacterBody
状態はFixedUpdate
で処理され、アクションの処理は後で行われます。 Update
かFixedUpdate
かに関係なく、着地とジャンプの間に 1 フレームの遅延が発生する可能性があり、これは顕著です。
着地時に即座に反応するために、 CharacterBody
にStateChanged
イベントを追加します。最初の引数は前の状態、2 番目の引数は現在の状態になります。
// 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
状態でわずかな遅延が発生し、ジャンプ チェーンが中断されることがわかりました。
これに対処するために、 CharacterBody
コンポーネントのSurface Anchor
フィールドを 0.01 ではなく 0.05 に設定しました。この値は、ボディが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; } }
キャラクターがGrounded
状態を失ったのは意図的なジャンプによるものか、それとも別の理由によるものかを見分けることが重要です。_isJumping フラグはすでにありますが、これは冗長なアクションを防ぐために_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 で「いいね!」や「スター」を付けていただけると嬉しいです。問題が発生した場合や間違いを見つけた場合は、遠慮なくご連絡ください。ご清聴ありがとうございました。