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中的實體原型;或,
- 透過程式碼新增「角色控制器」元件。
為了透過程式碼新增角色控制器,請遵循以下示例:
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
中載入已傳送。 - (編輯器)新增
CharacterControllerConfig
到Character Controller
元件中的Config
槽。
角色控制器設定
透過Create > Quantum > Assets > Physics > CharacterController2D/3D
下的內容選單,建立您自己的KCC設定資產。
預設設定資產
預設2D及3D KCC設定資產位於aAssets/Resources/DB/Configs
資料夾之中。這裡是3D KCC設定看起來的樣子:
設定欄位的一個簡要的說明
- 位移 用於根據實體位置來定義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);
Jump
及Move
方法對於原型而言很方便,而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中,Move
從ComputeRawMovement
請求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