This document is about: QUANTUM 3
SWITCH TO

Kinematic Character Controller (KCC)

はじめに

Kinematic Character Controller(略称KCC)は、独自ルールに従ってワールド内を移動するキャラクターに使用されます。物理演算に基づく移動ではなくKCCを使用することで、より精密な操作や素早い移動が可能になります。この概念はあらゆるゲームの中核をなすものの、ゲームプレイとの関連性によって、その定義はゲームごとに大きく異なります。したがって、Quantum SDKに含まれるKCCはあくまで出発点として、特定の状況で最適な結果を得るために独自のKCCを作成する必要が出てくるでしょう。

Quantumには2つのKCCが組み込まれており、1つは2D(横スクロール)、もう1つは3Dです。このAPIによってキャラクターは、地形を移動する・階段を登る・斜面を滑り降りる・移動する床を利用することができます。

KCCは移動ベクトルを計算する際、静的/動的オブジェクトの両方の物理データを考慮します。オブジェクトはキャラクターの移動を遮断し、環境オブジェクトとのコリジョンコールバックも同様にトリガーされます。

重要: Quauntumには3D KCCアドオンも用意されていて、組み込み版よりも完成度が高く、クエリに球体シェイプではなくカプセルシェイプを使用します。また、非常に柔軟な処理構造によって、ゲーム固有のニーズに合わせて独自ロジックを定義する方法をサポートします。
3D KCCが必要なプロジェクトでは、組み込み版ではなくアドオンの使用を推奨します。詳細な情報についてはこちらをご覧ください。

要件

エンティティでKCCを追加/使用するには、エンティティにTransformコンポーネントが付いている必要があります。PhysicsBodyを使用することもできますが必須ではありません。物理システムの影響で意図しない動作が生じる可能性があるため、KCCとPhysicsBodyの併用は通常は推奨されません。

Quantumの物理に関する基本情報については、最初に「物理」ドキュメントをご覧ください。

Raycast & ShapeOverlap

KCCの移動の計算にはShapeOverlapのみ(2Dは円、3Dは球)が使用されるため、KCCコンポーネントのみが付いたエンティティへのRaycast無視されます
エンティティをレイキャスト対象にしたい場合は、PhysicsColliderを付ける必要があります。

備考

このページは、2D/3D両方のKCCについて説明します。

CharacterControllerコンポーネント

以下のどちらかの方法で、エンティティにCharacterControllerコンポーネントを追加できます。

  • Unity上のエンティティプロトタイプにCharacterControllerコンポーネントを追加する
  • コードからCharacterControllerコンポーネントを追加する
KCC 2D and 2D Components in Unity
Unityエディター上でエンティティプロトタイプにCharacterController2D/3Dを追加する

コードからCharacterControllerコンポーネントを追加するには、次の例に従います。

C#

// 2D KCC
var kccConfig = frame.FindAsset<CharacterController2DConfig>(KCC_CONFIG_PATH);
var kcc = new CharacterController2D();
kcc.Init(frame, kccConfig)
f.Add(entity, kcc);

// 3D KCC
var kccConfig = frame.FindAsset<CharacterController3DConfig>(KCC_CONFIG_PATH);
var kcc = new CharacterController3D();
kcc.Init(frame, kccConfig)
f.Add(entity, kcc);

備考

このコンポーネントは、作成後に初期化する必要があります。有効な初期化方法は以下の通りです。

  • (コード)パラメーターなしでInit()メソッドを呼び出して、Assets/Resources/DB/ConfigsDefaultCharacterControllerをロードする
  • (コード)パラメーター付きでInit()メソッドを呼び出して、渡されたCharacterControllerConfigをロードする
  • (エディター)Character ControllerコンポーネントのConfigスロットにCharacterControllerConfigを追加する

CharacterControllerConfig

コンテキストメニューCreate > Quantum > Assets > Physics > CharacterController2D/3DからKCC設定アセットを作成します。

デフォルトConfigアセット

デフォルトの2D/3D KCC Confingアセットは、Assets/QuantumUser/Resources/フォルダー内にあります。3D KCC Configは次のようなものです。

KCC 3D Default Config
DefaultConfigKCC3D設定アセット

Configのフィールドの簡易説明

  • Offsetは、エンティティの位置を基準にしたKCCのローカル位置を定義するために使用されます。通常はKCCの中心をキャラクターの足元に配置するために使用されます。注意:KCCはキャラクターを「移動」させるために使用されるので、必ずしもキャラクター全身を包含する必要はありません。
  • Radiusはキャラクターの境界を定義し、キャラクターの水平サイズを包含する必要があります。これは、キャラクターが特定方向に移動できるか?壁が移動を妨げているか?階段を登れるか?斜面を滑り降りるか?などを判断するために使用されます。
  • Max Penetrationは、キャラクターが他の物理オブジェクトにめり込む際の動きをスムーズにします。キャラクターがこの値を超えると、強制的な修正が適用されて正しい位置に移動します。この値を0に設定すると、すべての修正が完全かつ即時に適用されますが、それによって動きがカクつく可能性があります。
  • Extentは、衝突が事前に検知される半径を定義します。
  • Max Contactsは、KCCによって計算される接触点の数を指定するために使用されます。通常は1で問題なく動作し、パフォーマンスが高い選択肢です。動きがカクつく場合は、2に設定してみてください(オーバーヘッドがわずかに発生します)。
  • Layer Maskは、KCCが実行する物理クエリで考慮されるコライダーのレイヤーを定義します。
  • Air ControlTrueに切り替えると、KCCが地面に接触していない場合でも移動操作を実行できるようになります。
  • Accelerationはキャラクターの加速度を定義します。
  • Base Jump Impulseは、KCCのJump()メソッド呼び出し時のジャンプ力を定義します。メソッドに値が渡されなかった場合は、この値が使用されます。
  • Max Speedは、キャラクターの水平方向の最高速度を制限します。
  • Gravityによって、KCCに重力加速度を適用します。
  • Max Slopeは、キャラクターが登降可能な最大角度(degrees)を定義します。
  • Max Slope Speedは、垂直落下ではなく斜面落下の移動時に、キャラクターが斜面を滑り降りる速度を制限します。

CharacterControllerのAPI

以下のAPIは3D KCCに焦点を当てていますが、2Dと3DのAPIは非常に似ています。

プロパティとフィールド

CharacterControllerコンポーネントは次のようなフィールドを持ちます。

C#

public FP MaxSpeed { get; set;}
public FPVector3 Velocity { get; set;} 
public bool Grounded { get; set;}
public AssetRef Config { get;}

ヒント

MaxSpeedは初期化後にキャッシュされた値です。これは実行時に変更する(例:ダッシュ移動時)ことができます。

API

各KCCコンポーネントは次のメソッドを持ちます。

C#

// 初期化
public void Init(FrameBase frame, CharacterController3DConfig config = null);

// ジャンプ
public void Jump(FrameBase frame, bool ignoreGrounded = false, FP? impulse = null);

// 移動
public void Move(FrameBase frame, EntityRef entity, FPVector3 direction, IKCCCallbacks3D callback = null, int? layerMask = null, Boolean? useManifoldNormal = null, FP? deltaTime = null);

// 生データ
public static CharacterController3DMovement ComputeRawMovement(Frame frame, EntityRef entity, Transform3D* transform, CharacterController3D* kcc, FPVector3 direction, IKCCCallbacks3D callback = null, int? layerMask = null, bool? useManifoldNormal = null);

JumpMoveメソッドはプロトタイピングに便利ですが、ComputeRawMovementは独自の移動を作成するための重要な情報を提供します。Quantumが提供するKCCの例では、ComputeRawMovementの情報が内部のステアリングメソッドComputeRawSteerで使用され、Moveで使用されるステアリングが計算されます。

重要:
以下のJump()Move()ComputeRawSteer()の実装は、ゲームの要件に合わせた独自実装を作成するための理解を深めるために示すものです。

CharacterController3DMovement

ComputeRawMovement()は、ShapeOverlapを実行してデータを処理し、ステアリングに必要な環境データを計算します。このメソッドで返されるCharacterController3DMovement構造体は、キャラクターの移動に適用できます。提供される移動データは、独自のステアリング実装の作成にも使用できます。

CharacterController3DMovement構造体は次の情報を持ちます。

C#

public enum CharacterMovementType
{
    None, // 接地中で方向指定なし
    FreeFall, // Radius内に接触なし
    SlopeFall, // Radius内で1つ以上の地面に接触あり(重力方向に対する法線 <= maxSlopeAngle)、この種の接触なしで「Grounded」になる可能性もある(CharacterController3DMovementのGroundedプロパティを参照)
    Horizontal, // 地面には接触していないが、1つ以上の横方向の接触がある(重力方向に対する法線 > maxSlopeAngle)
}

public struct CharacterController3DMovement
{
  public CharacterMovementType Type;
  
  // ユニークな最接近接触面の法線
  public FPVector3 NearestNormal;
  
  // すべての接触面の平均法線
  public FPVector3 AvgNormal;
  
  // 接地とみなされる最接近接触面の法線
  public FPVector3 GroundNormal;
  
  // 水平方向移動時の面の接線(GroundNormalの導出された方向に対する)、またはCharacterMovementType.FreeFall時の正規化された目標の方向
  public FPVector3 Tangent;
  
  // 重力方向に対する最接近接触面の法線から計算された面の接線(自身の現在速度は考慮しない)
  public FPVector3 SlopeTangent;
  
  // Radius内すべての接触面からの累積の投影補正(オーバーシュートを避けるため内積で補償する)
  public FPVector3 Correction;
  
  // Radius内の最接近接触面の最大のめり込み値
  public FP Penetration;
  
  // "拡張"された半径を使用して、この値とGroundNormalを割り当てることで、わずかに不規則な地形上を移動する際の接地状態の振動を回避する
  public Boolean Grounded;
  
  // Radius内の接触面数
  public int Contacts;
}

ComputeRawMovement()は、Move()メソッドで使用されます。

Jump()

この実装はあくまで一例です。

Jumpは単にKCCの現在速度に力を加えて、内部的なComputeRawSteerメソッドで処理されるJumpedブール値を切り替えます。

C#

public void Jump(FrameBase frame, bool ignoreGrounded = false, FP? impulse = null) {
    if (Grounded || ignoreGrounded) {
        Velocity.Y.RawValue = impulse?.RawValue ?? frame.FindAsset<CharacterController3DConfig>(Config.Id).BaseJumpImpulse.RawValue;
        Jumped = true;
    }
}

Move()

この実装はあくまで一例です。

Move()は、キャラクターの新しい位置を計算する際に、以下の要素を考慮します。

  • 現在位置
  • 方向
  • 重力
  • ジャンプ
  • 斜面
  • その他

これらすべての要素は、Init()メソッドに渡される設定アセットで定義できます。これは、テレイン・メッシュコライダー・プリミティブを持つFPS/TPS/アクションゲームのプロトタイピングに便利です。

備考: このメソッドは、すべてを計算して最終的にFPVector3の結果を返すため、移動そのものはほとんど制御することはできません。移動をより細かく制御するには、ComputeRawMovement()を使用して独自のステアリングと移動を作成してください。

C#

public void Move(Frame frame, EntityRef entity, FPVector3 direction, IKCCCallbacks3D callback = null, int? layerMask = null, Boolean? useManifoldNormal = null, FP? deltaTime = null) {
  Assert.Check(frame.Has<Transform3D>(entity));

  var transform = frame.GetPointer<Transform3D>(entity);
  var dt        = deltaTime ?? frame.DeltaTime;

  CharacterController3DMovement movementPack;
  fixed (CharacterController3D* thisKcc = &this) {
    movementPack = ComputeRawMovement(frame, entity, transform, thisKcc, direction, callback, layerMask, useManifoldNormal);
  }

  ComputeRawSteer(frame, ref movementPack, dt);

  var movement = Velocity * dt;
  if (movementPack.Penetration > FP.EN3) {
    var config = frame.FindAsset<CharacterController3DConfig>(Config.Id);
    if (movementPack.Penetration > config.MaxPenetration) {
      movement += movementPack.Correction;
    } else {
      movement += movementPack.Correction * config.PenetrationCorrection;
    }
  }

  transform->Position += movement;
}

ComputeRawSteer()

ステアリングは、キャラクターの位置・半径・速度に基づいて移動を計算し、必要に応じて移動を補正します。

この実装はあくまで一例です。

ComputeRawSteerは内部メソッドで、キャラクターが現在実行している移動タイプに基づいて、移動計算の大部分を行います。KCCの例では、MoveComputeRawMovementからmovementPack値をリクエストして、それらをComputeRawSteerに渡します。

C#

private void ComputeRawSteer(FrameThreadSafe f, ref CharacterController3DMovement movementPack, FP dt) {

  Grounded = movementPack.Grounded;
  var config = f.FindAsset(Config);
  var minYSpeed = -FP._100;
  var maxYSpeed = FP._100;
  
  switch (movementPack.Type) {
  
    // 自由落下
    case CharacterMovementType.FreeFall:
    
      Velocity.Y -= config._gravityStrength * dt;
      
      if (!config.AirControl || movementPack.Tangent == default(FPVector3)) {
        Velocity.X = FPMath.Lerp(Velocity.X, FP._0, dt * config.Braking);
        Velocity.Z = FPMath.Lerp(Velocity.Z, FP._0, dt * config.Braking);
      } else {
        Velocity += movementPack.Tangent * config.Acceleration * dt;
      }
      
      break;
      
    // 接地中の移動
    case CharacterMovementType.Horizontal:
      
      // 接線方向の速度を適用する
      Velocity += movementPack.Tangent * config.Acceleration * dt;
      var tangentSpeed = FPVector3.Dot(Velocity, movementPack.Tangent);
      
      // 現在速度から接線へ線形補間する
      var tangentVel = tangentSpeed * movementPack.Tangent;
      var lerp = config.Braking * dt;
      Velocity.X = FPMath.Lerp(Velocity.X, tangentVel.X, lerp);
      Velocity.Z = FPMath.Lerp(Velocity.Z, tangentVel.Z, lerp);
      
      // このフレームでキャラクターがジャンプしていない場合のみ、垂直方向の速度を線形補間する
      // そうでなければ、軽くジャンプする
      if (Jumped == false) {
        Velocity.Y = FPMath.Lerp(Velocity.Y, tangentVel.Y, lerp);
      }
      
      // 接線方向の速度を最大速度で制限する
      var tangentSpeedAbs = FPMath.Abs(tangentSpeed);
      if (tangentSpeedAbs > MaxSpeed) {
        Velocity -= FPMath.Sign(tangentSpeed) * movementPack.Tangent * (tangentSpeedAbs - MaxSpeed);
      }
      
      break;
      
    // 急勾配から滑落する
    case CharacterMovementType.SlopeFall:
    
      Velocity += movementPack.SlopeTangent * config.Acceleration * dt;
      minYSpeed = -config.MaxSlopeSpeed;
      
      break;
      
    // 移動せずに減速する
    case CharacterMovementType.None:
    
      var lerpFactor = dt * config.Braking;
      
      if (Velocity.X.RawValue != 0) {
        Velocity.X = FPMath.Lerp(Velocity.X, default, lerpFactor);
        if (FPMath.Abs(Velocity.X) < FP.EN1) {
          Velocity.X.RawValue = 0;
        }
      }
      
      if (Velocity.Z.RawValue != 0) {
        Velocity.Z = FPMath.Lerp(Velocity.Z, default, lerpFactor);
        if (FPMath.Abs(Velocity.Z) < FP.EN1) {
          Velocity.Z.RawValue = 0;
        }
      }
      
      // このフレームでキャラクターがジャンプしていない場合のみ、垂直方向の速度を0に戻す
      // そうでなければ、軽くジャンプする
      if (Velocity.Y.RawValue != 0 && Jumped == false) {
        Velocity.Y = FPMath.Lerp(Velocity.Y, default, lerpFactor);
        if (FPMath.Abs(Velocity.Y) < FP.EN1) {
          Velocity.Y.RawValue = 0;
        }
      }
      
      minYSpeed = 0;
      
      break;
  }
  
  // 垂直方向の速度を制限する
  if (movementPack.Type != CharacterMovementType.Horizontal) {
    var h = Velocity.XZ;
    
    if (h.SqrMagnitude > MaxSpeed * MaxSpeed) {
      h = h.Normalized * MaxSpeed;
    }
    
    Velocity.X = h.X;
    Velocity.Y = FPMath.Clamp(Velocity.Y, minYSpeed, maxYSpeed);
    Velocity.Z = h.Y;
  }
  
  // ジャンプ状態をリセットする
  Jumped = false;
}

コリジョンコールバック

KCCがコライダーとの接触を検知するたびに、コールバックがトリガーされます。

C#

public interface IKCCCallbacks2D
{
    bool OnCharacterCollision2D(FrameBase f, EntityRef character, Physics2D.Hit hit);
    void OnCharacterTrigger2D(FrameBase f, EntityRef character, Physics2D.Hit hit);
}

public interface IKCCCallbacks3D
{
    bool OnCharacterCollision3D(FrameBase f, EntityRef character, Physics3D.Hit3D hit);
    void OnCharacterTrigger3D(FrameBase f, EntityRef character, Physics3D.Hit3D hit);
}

コールバックを受け取ってその情報を使用するには、システムで対応するIKCCCallbacksインターフェースを実装します。

重要: コリジョンコールバックはブール値を返します。これによって衝突を無視するかを判断できます。falseを返すと、キャラクターは衝突した物理オブジェクトをすり抜けます。

コールバックの実装に加えて、移動メソッドにIKCCCallbacksオブジェクトを渡す必要があります。コリジョンコールバックを使用したコードスニペットは以下の通りです。

C#

namespace Quantum
{
  using Quantum.Core;
  using Quantum.Physics3D;

  public unsafe class SampleSystem : SystemMainThreadFilter<SampleSystem.Filter>, IKCCCallbacks3D
  {
    public struct Filter
    {
      public EntityRef EntityRef;
      public CharacterController3D* KCC;
    }

    public bool OnCharacterCollision3D(FrameBase f, EntityRef character, Hit3D hit)
    {
      // 衝突情報を読み込んで、これを無視するかどうかを判断する
      return true;
    }

    public void OnCharacterTrigger3D(FrameBase f, EntityRef character, Hit3D hit)
    {
    }

    public override void Update(Frame frame, ref Filter filter)
    {
      // 略
      // 最後のパラメーターにIKCCCallbacks3Dを追加する(今回のケースでは、このシステム自身)
      // CharacterController3D.Move(, input->Direction, this);
      filter.KCC->Move(frame, filter.EntityRef, input->Direction, this);
      // 略
    }
  }
}
Back to top