This document is about: QUANTUM 2
SWITCH TO

Kinematic Character Controller (KCC)

概述

一個運動學角色控制器,簡稱KCC,用於在世界中根據角色本身的規則集來移動一個角色。使用一個KCC而非物理/力基礎的移動,將允許更緊密的控制及敏捷的移動。雖然那些概念是所有遊戲的核心,它們在定義上有很大差異,這是因為它們與整體遊戲遊玩有關。因此,應將Quantum SDK中包含的KCC視為起點;然而,遊戲開發人員可能將必須建立他們自己的起點,以針對他們的特定內容來取得最佳的結果。

Quantum附有兩個預先組建的KCC,一個針對2D(橫向捲軸)而一個針對3D移動。API允許角色來在地形上移動、爬上階梯、滑下斜坡,以及使用移動平台。

在計算移動向量時,KCC考量靜態及動態物件的物理資料。物件將阻擋及定義角色的移動。也將觸發與環境物件的碰撞回調。

要求

為了使用或新增一個KCC到一個實體,實體必須已經有一個 轉換 元件。一個 物理主體 可以 被使用,但 是必要的;一般不建議使用附有一個KCC的 物理主體,因為物理系統可能會影響它,並導致意料外的移動。

如果您還不熟悉Quantum的物理,請先參閱 物理 文檔。

射線投射及形狀重疊

KCC只使用形狀重疊——圓形針對2D以及球體針對3D——來計算其移動。因此只有一個KKC元件的實體將被射線投射 忽略。 如果實體要進行射線投射,它也必須附有物理碰撞器

注意事項

本頁面涵蓋2D及3D KCC。

角色控制器元件

您可以新增角色控制器元件到您的實體,方法是:

  • 新增「角色控制器」元件到Unity中的實體原型;或,
  • 透過程式碼新增「角色控制器」元件。
kcc 2d and 2d components in unity
角色控制器2D及3D元件附加於一個在Unity編輯器中的實體原型。

為了透過程式碼新增角色控制器,請遵循以下示例:

C#

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

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

注意事項

在建立元件之後,必須初始化元件。可用的初始化選項有:

  • (程式碼)Init()方法 不附有 參數,它將從Assets/Resources/DB/Configs載入DefaultCharacterController
  • (程式碼)Init()方法 附有 參數,它將在CharacterControllerConfig中載入已傳送。
  • (編輯器)新增CharacterControllerConfigCharacter Controller元件中的Config槽。

角色控制器設定

透過Create > Quantum > Assets > Physics > CharacterController2D/3D下的內容選單,建立您自己的KCC設定資產。

預設設定資產

預設2D及3D KCC設定資產位於aAssets/Resources/DB/Configs資料夾之中。這裡是3D KCC設定看起來的樣子:

kcc 3d default config
預設角色控制器3D設定資產。

設定欄位的一個簡要的說明

  • 位移 用於根據實體位置來定義KCC本機位置。常用於將KCC的中心定位在角色的腳部。請記得:KCC用於 移動 角色,所以它不一定要封裝角色的全身。
  • 半徑 定義了角色的邊界,並且應該包圍角色的水平大小。這用於知道一個角色是否可以移動到特定方向,一個牆壁是否阻擋移動,一個階梯是否可以爬上,或一個斜坡是否可以滑下。
  • 最大穿透 平順了移動,這是在一個角色穿透其他物理物件的時候。如果角色通過最大穿透,將應用硬修復並將其貼齊到正確的位置。減少此值到0,將完全且立即應用所有修正;這可能導致不規則的移動。
  • 範圍 定義了一個半徑,其中預先偵測碰撞。
  • 最大聯絡 用於選擇KCC運算的聯絡點數量。1通常將順利運作,並且是最有效能的選項。如果您遇到不穩定的移動,試著設定這個到2;額外負荷是可忽略的。
  • 圖層遮罩 定義了KCC執行的物理査詢應考慮哪些碰撞器圖層。
  • 空中控制 切換為True,而KCC能夠在離開地面時執行移動調整。
  • 加速 定義了角色的加速速度。
  • 基本跳躍脈衝 定義了調用KCC Jump()方法時的脈衝強度。如果沒有傳送值到方法,將使用此值。
  • 最大速度 定義了角色最大水平速度。
  • 重力 應用一個重力到KCC。
  • 最大坡度 定義了角色可以走上和走下的最大角度,以度為單位。
  • 最大斜坡速度 在移動類型為斜坡下降而不是水平下降時,限制角色沿斜坡滑下的速度。

角色控制器API

以下顯示的API聚焦於3D KCC。而2D及3D API其實非常相似。

屬性及欄位

各個角色控制器元件都有這些欄位。

C#

public FP MaxSpeed { get; set;}
public FPVector3 Velocity { get; set;}
public bool Grounded { get; set;}
public FP CurrentSpeed { get;}
public AssetGUID ConfigId { get;}

訣竅

最大速度 是在初始化之後被快取的值。它因此可以在運行階段被修正,比如在執行快衝的時候。

API

各個KCC元件有以下方法:

C#

// Initialization
public void Init(FrameBase frame, CharacterController3DConfig config = null);

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

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

// Raw Information
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提供關鍵資訊來建立您自己的自訂移動。在示例中KCC由Quantum提供,來自ComputeRawMovement的資訊用於內部轉向方法ComputeRawSteer,以計算Move中使用的轉向。

重要事項: 以下展示Jump()Move()ComputeRawSteer()的執行,以促進理解並協助建立適合於特定遊戲需求的自訂執行。

角色控制器3D移動

ComputeRawMovement()透過執行一個形狀重疊並且處理資料,來計算轉向所需的環境資料。此方法傳回一個CharacterController3DMovement架構,其之後可被應用到角色移動。提供的移動資料也可用於建立一個自訂轉向執行。

CharacterController3DMovement架構持有以下資訊:

C#

public enum CharacterMovementType
{
    None, // grounded with no desired direction passed
    FreeFall, // no contacts within the Radius
    SlopeFall, // there is at least 1 ground contact within the Radius, specifically a contact with a normal angle vs -gravity <= maxSlopeAngle). It is possible to be "grounded" without this type of contact (see Grounded property in the CharacterController3DMovement)
    Horizontal, // there is NO ground contact, but there is at least one lateral contact (normal angle vs -gravity > maxSlopeAngle)
}

public struct CharacterController3DMovement
{
  public CharacterMovementType Type;

  // the surface normal of the closest unique contact
  public FPVector3 NearestNormal;

  // the average normal from all contacts
  public FPVector3 AvgNormal;

  // the normal of the closest contact that qualifies as ground
  public FPVector3 GroundNormal;

  // the surface tangent (from GroundNormal and the derived direction) for Horizontal move, or the normalized desired direction when in CharacterMovementType.FreeFall
  public FPVector3 Tangent;

  // surface tangent computed from closest the contact normal vs -gravity (does not consider current velocity of CC itself).
  public FPVector3 SlopeTangent;

  // accumulated projected correction from all contacts within the Radius. It compensates with dot-products to NOT overshoot.
  public FPVector3 Correction;

  // max penetration of the closest contact within the Radius
  public FP Penetration;

  // uses the EXTENDED radius to assign this Boolean AND the GroundedNormalas to avoid oscilations of the grounded state when moving over slightly irregular terrain
  public Boolean Grounded;

  // number of contacts within Radius
  public int Contacts;
}

ComputeRawMovement()Move()方法使用。

跳躍()

這只是一個參照執行。

Jump簡單地新增一個脈衝到KCC的目前速度,並且切換 已跳躍 布林值,其將被內部ComputeRawSteer方法處理。

C#

public void Jump(FrameBase frame, bool ignoreGrounded = false, FP? impulse = null) {

  if (Grounded || ignoreGrounded) {

    if (impulse.HasValue)
      Velocity.Y.RawValue = impulse.Value.RawValue;
    else {
      var config = frame.FindAsset(Config);
      Velocity.Y.RawValue = config.BaseJumpImpulse.RawValue;
    }

    Jumped = true;
  }
}

移動()

這只是一個參照執行。

Move()在計算角色的新位置時考慮以下因素:

  • 目前位置
  • 方向
  • 重力
  • 跳躍
  • 坡度
  • 其他資訊

所有這些方面都可以在傳送到Init()方法的設定資產中定義。這對於原型第一人稱射擊/第三人稱射擊/動作遊戲非常方便,這些遊戲有地形、網格碰撞器及原始物件。

注意: 因為它計算所有事情並且傳回一個最終的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是一個內部方法,其根據角色目前正在執行的移動類型來進行大量移動計算。在示例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) {

    // FreeFall
    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;

    // Grounded movement
    case CharacterMovementType.Horizontal:

      // apply tangent velocity
      Velocity += movementPack.Tangent * config.Acceleration * dt;
      var tangentSpeed = FPVector3.Dot(Velocity, movementPack.Tangent);

      // lerp current velocity to 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);

      // we only lerp the vertical velocity if the character is not jumping in this exact frame,
      // otherwise it will jump with a lower impulse
      if (Jumped == false) {
        Velocity.Y = FPMath.Lerp(Velocity.Y, tangentVel.Y, lerp);
      }

      // clamp tangent velocity with max speed
      var tangentSpeedAbs = FPMath.Abs(tangentSpeed);
      if (tangentSpeedAbs > MaxSpeed) {
        Velocity -= FPMath.Sign(tangentSpeed) * movementPack.Tangent * (tangentSpeedAbs - MaxSpeed);
      }

      break;

    // Sliding due to excessively steep slope
    case CharacterMovementType.SlopeFall:

      Velocity += movementPack.SlopeTangent * config.Acceleration * dt;
      minYSpeed = -config.MaxSlopeSpeed;

      break;

    // No movement, only deceleration
    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;
        }
      }

      // we only lerp the vertical velocity back to 0 if the character is not jumping in this exact frame,
      // otherwise it will jump with a lower impulse
      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;
  }

  // horizontal is clamped elsewhere
  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;
  }

  // reset jump state
  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#

public unsafe class SampleSystem : SystemMainThread, IKCCCallbacks3D
{
  public bool OnCharacterCollision3D(FrameBase f, EntityRef character, Physics3D.Hit3D hit) {
    // read the collision information to decide if this should or not be ignored
    return true;
  }

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

  public override void Update(Frame f) {
    // [...]
    // adding the IKCCCallbacks3D as the last parameter (this system, in this case)
    var movement = CharacterController3D.Move((Entity*)character, input->Direction, this);
    // [...]
}
Back to top