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

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

Denis Kondratev15m2024/04/22
Read on Terminal Reader

長すぎる; 読むには

この記事では、Unity で 2D キャラクター コントローラーを作成するプロセスを詳しく説明し、キャラクターの動きを簡素化する `Rigidbody2D` の新しい `Slide` メソッドの統合に焦点を当てています。セットアップ、物理的な動作、衝突処理、および動きの制約をカバーし、さまざまな 2D プラットフォーマー ゲームに適応および拡張できるキャラクター コントローラーを開発するための基礎ガイドを提供します。
featured image - Unity で 2D キャラクター コントローラーを作成する方法: パート 1
Denis Kondratev HackerNoon profile picture
0-item
1-item
2-item


2D プラットフォーム ゲームの重要な要素の 1 つはメイン キャラクターです。メイン キャラクターの動き方や操作方法は、ゲームの雰囲気を大きく左右します。居心地の良い昔ながらのゲームでも、ダイナミックなスラッシャー ゲームでも、キャラクター コントローラーの作成は重要な初期段階です。


この記事では、キャラクターをゼロから作成し、物理法則を順守しながらレベル内を移動する方法を教えていくプロセスを徹底的に検証します。キャラクター コントローラーの作成経験がすでにある場合でも、Unity 2023 のイノベーションについて学ぶことに興味を持つでしょう。驚いたことに、待望のSlideメソッドがRigidbody2Dコンポーネントに追加されました。これにより、Kinematic モードでRigidbody2Dをより効果的に使用できるようになり、キャラクター コントローラーの作成が大幅に簡素化されます。以前は、この機能はすべて手動で実装する必要がありました。


記事を読むだけでなく実際に試してみたい場合は、GitHub リポジトリTreasure Huntersからレベル テンプレートをダウンロードすることをお勧めします。このリポジトリには、キャラクターをテストするための必要なアセットと準備済みレベルがすでに含まれています。


人格の基礎を築く

私たちのプラットフォーム ゲームでは、ゲームには垂直面と水平面のみがあり、重力は厳密に下向きに向くというルールを設定しました。これにより、特にベクトル数学を詳しく調べたくない場合は、初期段階でのプラットフォーム ゲームの作成が大幅に簡素化されます。


将来的には、Unity での 2D プラットフォーム ゲームのメカニズムの作成を探求するプロジェクト Treasure Hunters で、これらのルールから逸脱する可能性があります。ただし、これは別の記事で取り上げます。


移動は水平面に沿って行われるという決定に基づいて、キャラクターのベースは長方形になります。傾斜面を使用する場合は、カプセル型のコライダーと、スライドなどの追加のメカニズムを開発する必要があります。


まず、シーンに空のオブジェクトを作成し、Captain というRigidbody2Dを付けます。これがメイン キャラクターになります。オブジェクトにRigidbody2DおよびBoxCollider2Dコンポーネントを追加します。Rigidbody2D タイプを Kinematic に設定して、Unity の組み込み物理機能を活用しながらキャラクターの動きを制御できるようにします。また、Freeze Rotation Z オプションを有効にして、Z 軸に沿ったキャラクターの回転をロックします。


設定は以下の図のようになります。


では、船長の外見を追加しましょう。Assets/Textures/Treasure Hunters/Captain Clown Nose/Sprites/Captain Clown Nose/Captain Clown Nose with Sword/09-Idle Sword/Idle Sword 01.png でテクスチャを探し、そのピクセル/単位の値を 32 に設定します。この解像度でテクスチャが作成されるため、このコースではこの値を頻繁に使用します。Sprite Mode を Single に設定し、便宜上、Pivot を Bottom に設定します。[適用] ボタンをクリックして変更を適用することを忘れないでください。すべての設定が正しく行われていることを確認してください。



この記事では、キャラクターアニメーションについては触れないので、今のところはスプライトを 1 つだけ使用します。Captain オブジェクトで、Appearance というネストされたオブジェクトを作成し、それに Sprite Renderer コンポーネントを追加して、以前に構成したスプライトを指定します。



船長の画像を拡大すると、スプライト設定が間違っているためにかなりぼやけていることに気付きました。これを修正するには、プロジェクト ウィンドウで Idle Sword 01 テクスチャを選択し、フィルター モードをポイント (フィルターなし) に設定します。これで、画像の見栄えがかなり良くなりました。


キャラクターコライダーの設定

現時点では、衝突装置の位置と船長のイメージが一致しておらず、衝突装置は私たちのニーズに対して大きすぎることが判明しました。

これを修正し、キャラクターのピボットを下部に正しく配置しましょう。このルールをゲーム内のすべてのオブジェクトに適用して、サイズに関係なく同じレベルに配置しやすくします。


このプロセスを簡単に管理するには、以下に示すように、ピボットを設定したトグル ツール ハンドル位置を使用します。


次のステップは、ヒーローのコライダーを調整して、ピボット ポイントが正確に中央下部になるようにすることです。コライダーのサイズは、キャラクターの寸法と正確に一致する必要があります。必要な精度を実現するには、コライダーのオフセットとサイズのパラメータ、およびネストされた外観オブジェクトの位置を調整します。


コライダーの Offset.X パラメータには特に注意してください。この値は厳密に 0 である必要があります。これにより、オブジェクトの中心に対するコライダーの対称的な配置が保証されます。これは、Transform.Scale.X の値が -1 と 1 に変更される、後続のキャラクターの左右の回転にとって非常に重要です。コライダーは所定の位置に留まり、視覚的な回転は自然に見えるはずです。


キャラクターの物理的動作の紹介

まず第一に、主人公、NPC、敵など、キャラクターに物理的な世界とやりとりする方法を教えることが重要です。たとえば、キャラクターは平らな面を歩いたり、そこから飛び降りたり、重力の力を受けて地面に引き戻されたりできる必要があります。


Unity には、ボディの動きを制御し、衝突を処理し、オブジェクトに外部力の効果を追加する物理エンジンが組み込まれています。これはすべて、 Rigidbody2Dコンポーネントを使用して実装されています。ただし、キャラクターの場合、物理世界と対話しながら、開発者がオブジェクトの動作をより細かく制御できる、より柔軟なツールがあると便利です。


先ほども述べたように、Unity の以前のバージョンでは、開発者はこうしたロジックをすべて自分で実装する必要がありました。しかし、Unity 2023 では、 Rigidbody2Dコンポーネントに新しいメソッドSlideが追加され、実行された動きに関する必要な情報をすべて提供しながら、物理オブジェクトを柔軟に制御できるようになりました。


まず、物理世界でキャラクターを動かすための基本ロジックを含むCharacterBodyクラスを作成しましょう。このクラスは、キャラクターにすでに追加されているKinematicモードでRigidbody2Dを使用します。まず最初に、このコンポーネントへの参照を追加します。


 public class CharacterBody : MonoBehaviour { [SerializeField] private Rigidbody2D _rigidbody; }


キャラクターの動きのダイナミクスのために、重力が通常よりも強く作用する必要がある場合があります。これを実現するために、初期値が 1 の重力影響係数を追加します。この係数は 0 未満にすることはできません。


 [Min(0)] [field: SerializeField] public float GravityFactor { get; private set; } = 1f;


また、どのオブジェクトを通行不能な表面と見なすかを定義する必要があります。これを行うには、必要なレイヤーを指定できるフィールドを作成します。


 [SerializeField] private LayerMask _solidLayers;


重力などの外部の影響によりキャラクターの速度が過度に高くなるのを防ぐために、キャラクターの速度を制限します。初期値を 30 に設定し、インスペクターで 0 未満の値を設定する機能を制限します。


 [Min(0)] [SerializeField] private float _maxSpeed = 30;


表面に沿って移動する場合、キャラクターと表面の間の距離が十分に小さい場合は、キャラクターが常に表面に張り付くようにする必要があります。


 [Min(0)] [SerializeField] private float _surfaceAnchor = 0.01f;


ゲーム内の表面は水平または垂直のみにすることにしましたが、念のため、キャラクターが安定して立つことができる表面の最大傾斜角度を初期値 45 度で指定します。


 [Range(0, 90)] [SerializeField] private float _maxSlop = 45f;


インスペクターを通じて、キャラクターの現在の速度と状態も確認したいので、 SerializeField属性を持つ 2 つのフィールドを追加します。


 [SerializeField] private Vector2 _velocity; [field: SerializeField] public CharacterState State { get; private set; }


はい、ここでは、まだ定義されていない新しいエンティティCharacterStateを導入しました。これについては後で詳しく説明します。


キャラクターの状態

プラットフォーマーの開発を簡素化するために、メインのキャラクター状態を 2 つだけ定義しましょう。

1 つ目はGroundedで、キャラクターが地面にしっかりと立っている状態です。この状態では、キャラクターは地面に沿って自由に移動したり、地面から飛び降りたりすることができます。


2 つ目はAirborneです。これは自由落下状態で、キャラクターは空中にいます。この状態でのキャラクターの行動は、プラットフォーム ゲームの詳細によって異なります。現実に近い一般的なケースでは、キャラクターは初期の運動量の影響を受けて移動し、その行動に影響を与えることはできません。ただし、プラットフォーム ゲームでは、利便性とゲームプレイのダイナミクスを優先して物理法則が単純化されることがよくあります。たとえば、多くのゲームでは、自由落下中でもキャラクターの水平方向の動きを制御できます。私たちの場合、これも可能であり、人気のダブル ジャンプのメカニズムも使用して、空中でさらにジャンプすることができます。


キャラクターの状態をコードで表現してみましょう。


 /// <summary> /// Describes the state of <see cref="CharacterBody"/>. /// </summary> public enum CharacterState { /// <summary> /// The character stays steady on the ground and can move freely along it. /// </summary> Grounded, /// <summary> /// The character is in a state of free fall. /// </summary> Airborne }


他にも多くの状態が存在する可能性があることは注目に値します。たとえば、ゲームに傾斜面が含まれている場合、キャラクターは、左右に自由に移動することはできませんが、斜面を滑り降りることはできるスライド状態になる可能性があります。このような状態では、キャラクターはジャンプして斜面から押し出すこともできますが、傾斜の方向にのみジャンプできます。別の考えられるケースは、垂直の壁に沿ってスライドすることです。この場合、重力の影響は弱まり、キャラクターは水平方向に押し出すことができます。


移動速度制限

すでにプライベート フィールド_velocityを定義していますが、キャラクターの最大速度を制限しながら、この値を外部から取得および設定できるようにする必要があります。そのためには、指定された速度ベクトルを最大許容速度と比較する必要があります。


これは、速度ベクトルの長さ、つまり数学的にはその大きさを計算することで実行できますVector2構造体には、これを行うためのmagnitudeプロパティがすでに含まれています。したがって、渡されたベクトルの大きさが最大許容速度を超える場合、ベクトルの方向は維持しながら大きさを制限する必要があります。このためには、 _maxSpeedに正規化された速度ベクトルを掛けます (正規化されたベクトルとは、方向は同じですが大きさが 1 のベクトルです)。


コード内では次のようになります。


 public Vector2 Velocity { get => _velocity; set => _velocity = value.magnitude > _maxSpeed ? value.normalized * _maxSpeed : value; }


さて、ベクトルの大きさがどのように計算されるかを詳しく見てみましょう。ベクトルの大きさは次の式で定義されます。



平方根の計算は、リソースを大量に消費する操作です。ほとんどの場合、速度は最大値を超えることはありませんが、それでもサイクルごとに少なくとも 1 回はこの比較を実行する必要があります。ただし、ベクトルの大きさの 2 乗と最大速度の 2 乗を比較すると、この操作を大幅に簡素化できます。


このため、最大速度の二乗を格納するための追加フィールドを導入し、 Awakeメソッドでそれを 1 回計算します。


 private float _sqrMaxSpeed; private void Awake() { _sqrMaxSpeed = _maxSpeed * _maxSpeed; }


速度設定をより最適に実行できるようになりました。


 public Vector2 Velocity { get => _velocity; set => _velocity = value.sqrMagnitude > _sqrMaxSpeed ? value.normalized * _maxSpeed : value; }


これにより、不要な計算を回避し、キャラクターの移動速度の処理パフォーマンスが向上します。


剛体移動法

先ほど述べたように、Unity は新しいSlide()メソッドを追加しました。これにより、 CharacterBodyの開発が大幅に簡素化されます。ただし、このメソッドを使用する前に、オブジェクトが空間内で移動するルールを定義する必要があります。この動作は、 Rigidbody2D.SlideMovement構造によって設定されます。


新しいフィールド_slideMovementを導入し、その値を設定しましょう。


 private Rigidbody2D.SlideMovement _slideMovement; private void Awake() { _sqrMaxSpeed = _maxSpeed * _maxSpeed; _slideMovement = CreateSlideMovement(); } private Rigidbody2D.SlideMovement CreateSlideMovement() { return new Rigidbody2D.SlideMovement { maxIterations = 3, surfaceSlideAngle = 90, gravitySlipAngle = 90, surfaceUp = Vector2.up, surfaceAnchor = Vector2.down * _surfaceAnchor, gravity = Vector2.zero, layerMask = _solidLayers, useLayerMask = true, }; }



maxIterationsは、衝突の結果としてオブジェクトが方向を変えることができる回数を決定することを説明することが重要です。たとえば、キャラクターが壁の横の空中にいて、重力が作用しているときにプレイヤーがキャラクターを右に移動させようとします。したがって、 Slide()メソッドを呼び出すたびに、右下方向の速度ベクトルが設定されます。壁にぶつかると、モーション ベクトルが再計算され、オブジェクトは下方向に移動し続けます。


このような状況で、 maxIterations値が 1 に設定されている場合、オブジェクトは壁にぶつかって停止し、事実上そこに留まってしまいます。


maxIterationslayerMaskの値は以前に定義されています。他のフィールドの詳細については、 公式の構造ドキュメントを参照してください。


最後に、キャラクターを動かす

これで、Captain を動かすための準備がすべて整いました。これを、物理処理用に設計された Unity のコールバックであるFixedUpdateで実行します。過去数年間で、Unity チームは 2D 物理処理を大幅に改善しました。現在、処理はUpdateコールバックで実行することも、必要なメソッドを独自に呼び出すことで実行することもできます。


ただし、この例では、従来の実績のあるFixedUpdateメソッドを使用します。先に進む前に、 Time.fixedDeltaTimeの値について少し説明しておく必要があります。


ゲーム物理の予測可能性を確保するために、シミュレーションは一定の時間間隔で反復して実行されます。これにより、FPS の変化や遅延がオブジェクトの動作に影響を与えないことが保証されます。


各サイクルの開始時に、物体に対する重力の影響を考慮します。重力は自由落下加速度のベクトルによって与えられるため、時間Δtにわたる物体の速度Δvの変化は次の式で計算できます。




ここで、 a物体の一定の加速度です。この場合、導入した係数Physics2D.gravity * GravityFactorを考慮すると、これは重力による加速度です。したがって、 Δv次のように計算できます。


 Time.fixedDeltaTime * GravityFactor * Physics2D.gravity


速度を変更した場合の最終結果は次のようになります。


 Velocity += Time.fixedDeltaTime * GravityFactor * Physics2D.gravity;


これで、キャラクターのリジッドボディの動きを実行できます。


 var slideResults = _rigidbody.Slide( _velocity, Time.fixedDeltaTime, _slideMovement);


変数slideResults SlideResults構造体の値であり、移動の結果を保存します。この結果の主なフィールドは、移動中に表面と衝突した結果であるslideHitと、キャラクターが安定した表面に立っているかどうかを判断するのに役立つ下向きのキャストの結果であるsurfaceHitです。


衝突の処理

表面と衝突する場合、その表面に向かうキャラクターの速度を制限することが重要です。簡単な例として、キャラクターが地面に静止している場合、重力の影響で速度を上げ続けるべきではありません。各サイクルの終わりに、速度はゼロである必要があります。同様に、上向きに移動して天井にぶつかると、キャラクターは垂直方向の速度をすべて失い、下向きに移動し始める必要があります。


衝突の結果であるslideHitsurfaceHit 、衝突面の法線を含むRaycastHit2D構造体の値によって表されます。


速度制限は、元の速度ベクトルの衝突法線への投影を速度ベクトル自体から減算することで計算できます。これはドット積を使用して行われます。この操作を実行するメソッドを記述してみましょう。


 private static Vector2 ClipVector(Vector2 vector, Vector2 hitNormal) { return vector - Vector2.Dot(vector, hitNormal) * hitNormal; }


次に、このメソッドをFixedUpdateに統合します。ここで、 surfaceHitについては、オブジェクトが表面上にあるかどうかを判断するキャストが常に地面との接触をチェックするために実行されるため、下向きの場合のみ速度を制限します。


 private void FixedUpdate() { Velocity += Time.fixedDeltaTime * GravityFactor * Physics2D.gravity; var slideResults = _rigidbody.Slide( _velocity, Time.fixedDeltaTime, _slideMovement); if (slideResults.slideHit) { _velocity = ClipVector(_velocity, slideResults.slideHit.normal); } if (_velocity.y <= 0 && slideResults.surfaceHit) { var surfaceHit = slideResults.surfaceHit; _velocity = ClipVector(_velocity, surfaceHit.normal); } }


この実装により、キャラクターの動きを正しく管理し、さまざまな表面との衝突時に不要な加速を回避し、ゲーム内のキャラクターの動きを予測可能かつスムーズに保つことができます。


キャラクターの状態の判定

各サイクルの終わりに、キャラクターが固体表面上にいるか(接地状態)、または自由落下中(または、定義したように、制御された落下、つまり空中状態)にあるかを判断する必要があります。


キャラクターが Grounded 状態にあると見なすには、まず、垂直速度が 0 または負でなければなりません。これは、 _velocity.yの値によって決まります。


もう 1 つの重要な基準は、キャラクターの足の下のサーフェスの存在です。これは、Rigidbody の動きの結果、つまりsurfaceHitの存在を通じて識別されます。


3 番目の要素は表面の傾斜角度です。これは、この表面の法線、つまりsurfaceHit.normalの値に基づいて分析します。この角度を、キャラクターが安定して立つことができる表面の最大角度である_maxSlopと比較する必要があります。


完全に垂直な表面の場合、法線は厳密に水平になります。つまり、そのベクトル値は (1, 0) または (-1, 0) になります。水平面の場合、法線の値は (0, 1) になります。傾斜角が小さいほど、 yの値は大きくなります。角度alphaの場合、この値は次のように計算できます。



角度は度で与えられ、関数 $\cos$ はラジアンを必要とするため、式は次のように変換されます。



このため、新しいフィールドを導入し、 Awakeメソッドで計算してみましょう。


 private float _minGroundVertical; private void Awake() { _minGroundVertical = Mathf.Cos(_maxSlop * Mathf.PI / 180f); //... }


ここで、上記のすべての条件をチェックしながら、 FixedUpdateのコードを更新してみましょう。


 if (_velocity.y <= 0 && slideResults.surfaceHit) { var surfaceHit = slideResults.surfaceHit; Velocity = ClipVector(_velocity, surfaceHit.normal); State = surfaceHit.normal.y >= _minGroundVertical ? CharacterState.Grounded : CharacterState.Airborne; } else { State = CharacterState.Airborne; }


このロジックにより、キャラクターが地面にいるかどうかを正確に判断し、その状態の変化に正しく対応できるようになります。


キャプテンにCharacterBodyを追加する

これで CharacterBody コンポーネントの準備ができたので、最後のステップはそれを Captain に追加することです。シーンで Captain オブジェクトを選択し、それにCharacterBodyコンポーネントを追加します。

上の図に示すように、Rigidbody を設定することを忘れないでください。重力係数を 3 に設定し、ソリッド レイヤーの既定オプションを選択します。


これで、ゲームを開始し、Velocity にさまざまな値を設定して、キャラクターがシーン内をどのように移動するか実験することができます。

とりあえずまとめ

もちろん、キャラクターのコントロールを追加する必要があります。ただし、この記事はすでにかなり長くなっているため、新しい入力システムを使用したキャラクターのコントロールについては、次の記事「Unity でキャラクター コントローラー 2D を作成する: パート 2」で詳しく説明します。


この記事で説明されている完全なプロジェクトは、こちらからダウンロードできます: Treasure Hunters 。問題があれば、実際にすべてを確認してください。キャラクター コントローラーの開発は、ゲームのさらなる開発を決定するため、2D プラットフォーマーを作成する上で重要な側面です。メイン ヒーローや敵の動作に新しい機能を追加する容易さに影響します。したがって、独自のゲームを独自に開発できるようにするには、基本を理解することが非常に重要です。