paint-brush
Unity で 2D キャラクター コントローラーを作成する方法: パート 2@deniskondratev
182 測定値

Unity で 2D キャラクター コントローラーを作成する方法: パート 2

Denis Kondratev17m2024/12/08
Read on Terminal Reader

長すぎる; 読むには

この記事では、新しい入力システムのセットアップ、ジャンプの仕組みの改善、シームレスなカメラ追従など、Unity 2D キャラクター コントローラーを強化する方法について説明します。
featured image - Unity で 2D キャラクター コントローラーを作成する方法: パート 2
Denis Kondratev HackerNoon profile picture

この記事では、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 DW同時に押すと、(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で処理され、アクションの処理は後で行われます。 UpdateFixedUpdateかに関係なく、着地とジャンプの間に 1 フレームの遅延が発生する可能性があり、これは顕著です。


着地時に即座に反応するために、 CharacterBodyStateChangedイベントを追加します。最初の引数は前の状態、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); } } }


また、 FixedUpdatesurfaceHitが処理される方法も改良しました。


 // 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 で「いいね!」や「スター」を付けていただけると嬉しいです。問題が発生した場合や間違いを見つけた場合は、遠慮なくご連絡ください。ご清聴ありがとうございました。