This document is about: QUANTUM 3
SWITCH TO

QBall

Level 4

概述

QBall範例是一個自上而下的3v3競技場搏擊者。傳球,將對手趕出競技場,在混亂、閃電般快速的比賽中與敵方球隊得分。它透過分屏最多支持4名本機玩家。輸入緩衝和能力啟動延遲可以在更高的ping值下實現流暢的多人遊戲體驗。

此示例由MicroverseLabs工作室開發。

下載

版本 發佈日期 下載

技術資訊

  • Unity:2021.3.18f1。
  • 平台:PC (Windows / Mac)

焦點

技術

  • 多個本機玩家利用預設的Quantum功能。
  • 輸入編碼(Vector2為位元組)。
  • 視圖中快速移動的球的自定義插值。
  • 分屏多人遊戲(本機+線上)。

遊戲遊玩

  • 不同的能力組。
  • 可用能力會根據控球情况而變化。
  • 多個本機玩家。
  • 土狼時間。

畫面截圖

本機玩家

UI和配對

該示例使用預設Quantum Demo Manu的修改版本。主要新增功能是連接畫面上的本機玩家計數下拉式功能表。

Game Start

啟動連接時使用SqlLobbyFilter,以便將玩家的最大數量限制為6,同時考慮到本機玩家。

C#

public const string LOCAL_PLAYERS_PROP_KEY = "LP";
public const string TOTAL_PLAYERS_PROP_KEY = "C0";

public static readonly TypedLobby SQL_LOBBY = new TypedLobby("customSqlLobby", LobbyType.SqlLobby);

C#

protected override void OnConnect(QuantumMenuConnectArgs connectArgs, ref MatchmakingArguments args)
{
    args.RandomMatchingType = MatchmakingMode.FillRoom;
    args.Lobby = LocalPlayerCountManager.SQL_LOBBY;
    args.CustomLobbyProperties = new string[] { LocalPlayerCountManager.TOTAL_PLAYERS_PROP_KEY };
    args.SqlLobbyFilter = $"{LocalPlayerCountManager.TOTAL_PLAYERS_PROP_KEY} <= {Input.MAX_COUNT - _localPlayersCountSelector.GetLastSelectedLocalPlayersCount()}";
}

連接到房間後,主用戶端會將自訂屬性與玩家總數保持同步(包括所有連接用戶端的其他本機玩家)。

C#

private void UpdateRoomTotalPlayers()
{
    if (_connection != null && _connection.Client.InRoom && _connection.Client.LocalPlayer.IsMasterClient)
    {
        int totalPlayers = 0;
        foreach (var player in _connection.Client.CurrentRoom.Players.Values)
        {
            if (player.CustomProperties.TryGetValue(LOCAL_PLAYERS_PROP_KEY, out var localPlayersCount))
            {
                totalPlayers += (int)localPlayersCount;
            }
        }

        _connection.Client.CurrentRoom.SetCustomProperties(new PhotonHashtable
        {
            { TOTAL_PLAYERS_PROP_KEY, totalPlayers }
        });
    }
}

本機玩家初始化

當遊戲開始時,會根據本機玩家的數量實例化不同的配置預製件。在配置預製件中,每個本機玩家都有自己的CameraUIPlayerInputPlayerInput會自動為每個本機玩家分配不同的輸入裝置。如果有多個本機玩家,主玩家總是會被分配滑鼠和鍵盤,任何其他玩家都會得到一個不同的控制器(控制器需要在遊戲開始前插入)。

能力

概述

每個能力的狀態資料都存儲在一個Ability結構中,該結構包含幾個計時器和一個AbilityData資產引用。

C#

struct Ability
{
    [ExcludeFromPrototype] AbilityType AbilityType;

    [ExcludeFromPrototype] CountdownTimer InputBufferTimer;
    [ExcludeFromPrototype] CountdownTimer DelayTimer;
    [ExcludeFromPrototype] CountdownTimer DurationTimer;
    [ExcludeFromPrototype] CountdownTimer CooldownTimer;

    asset_ref<AbilityData> AbilityData;
}

Ability結構存儲在AbilityInventory組件的數組中。

C#

component AbilityInventory
{
    [ExcludeFromPrototype] ActiveAbilityInfo ActiveAbilityInfo;

    // Same order as AbilityType enum also used for activation priority
    [Header("Ability Order: Block, Dash, Attack, ThrowShort, ThrowLong, Jump")]
    array<Ability>[6] Abilities;
}

單個AbilitySystem透過將所有相關狀態資料傳遞給相應的AbilityData資產,以資料驅動的方式更新所有能力。

C#

public override void Update(Frame frame, ref Filter filter)
{
    QuantumDemoInputTopDown input = *frame.GetPlayerInput(filter.PlayerStatus->PlayerRef);

    for (int i = 0; i < filter.AbilityInventory->Abilities.Length; i++)
    {
        AbilityType abilityType = (AbilityType)i;
        ref Ability ability = ref filter.AbilityInventory->Abilities[i];
        AbilityData abilityData = frame.FindAsset<AbilityData>(ability.AbilityData.Id);

        abilityData.UpdateAbility(frame, filter.EntityRef, ref ability);
        abilityData.UpdateInput(frame, ref ability, input.GetAbilityInputWasPressed(abilityType));
        abilityData.TryActivateAbility(frame, filter.EntityRef, filter.PlayerStatus, ref ability);
    }
}

基礎的AbilityData實現負責啟動能力邏輯並更新其狀態,而所有不可變的能力特定資料和邏輯都是透過使用多態性在派生的AbilityData資產中實現的。這種設定允許所有能力邏輯都是自包含的,創建新能力就像編寫其獨特的邏輯一樣簡單,而不需要任何模範程式碼。

輸入緩衝

當檢測到能力輸入而不是立即嘗試啟動能力時,會啟動InputBufferTimer。然後,AbilityData會檢查計時器是否在運行每一幀,以啟動該能力。這既可以提供更流暢的玩家體驗,也有助於在某些情況下減輕高延遲。例如,如果玩家在短跑中間並試圖投球,他們的輸入通常會被消耗掉,而不會發生任何事情-輸入緩衝會在短跑結束後立即啟動投擲能力,並會提前一點發送給其他遠程玩家,以便及時到達並防止預測失誤。

啟動延遲

當一個能力被啟動時,它首先進入延遲狀態,為輸入到達其他遠程玩家提供一些時間,並防止預測失誤。為了使本機玩家能夠感受到響應,他們的動畫會立即觸發,並持續整個延遲+實際持續時間。

持球時的不同能力

沒有球,球員就可以獲得進攻性的一拳和防守性的阻擋能力。持球時,他們被短投和長投能力所取代。不可用的能力仍然會在每個刷新中更新,因此可以勾選它們的InputBufferTimerCooldownTimer。這與持球時移動速度的降低相結合,激勵了傳球或依靠隊友的保護。

一拳

一拳功能使用具有多個尺寸不斷增大的球體的複合命中檢測形狀,以創建錐形命中框。它同時應用了擊倒和眩暈狀態效果。擊倒可以由多個玩家連結在一起,被擊倒到虛空中會導致短暫的暫停,然後重生。

阻擋能力

阻擋能力在持續期間完全封鎖所有攻擊。

投擲能力

由於所有能力都只針對一個目標方向,因此短傳和長傳可以更好地控制。

衝刺能力

衝刺允許由動畫曲線驅動的快速移動。任何自定義移動都需要相對於當前玩家位置進行計算,以便允許多個自定義移動疊加在一起,並進行KCC碰撞器穿透校正。

C#

if (abilityState.IsActive)
{
    AbilityInventory* abilityInventory = frame.Unsafe.GetPointer<AbilityInventory>(entityRef);
    Transform3D* transform = frame.Unsafe.GetPointer<Transform3D>(entityRef);
    CharacterController3D* kcc = frame.Unsafe.GetPointer<CharacterController3D>(entityRef);

    FP lastNormalizedPosition = DashMovementCurve.Evaluate(lastNormalizedTime);
    FPVector3 lastRelativePosition = abilityInventory->ActiveAbilityInfo.CastDirection * DashDistance * lastNormalizedPosition;

    FP newNormalizedTime = ability.DurationTimer.NormalizedTime;
    FP newNormalizedPosition = DashMovementCurve.Evaluate(newNormalizedTime);
    FPVector3 newRelativePosition = abilityInventory->ActiveAbilityInfo.CastDirection * DashDistance * newNormalizedPosition;

    transform->Position += newRelativePosition - lastRelativePosition;
}

跳躍能力

跳躍也是一種能力,因此它可以從輸入緩衝和啟動延遲中受益。輸入緩衝對它特別有用,因為它允許下一跳在接地前不久排隊。跳躍的啟動延遲遠低於其他能力,因為否則會感覺沒有反應。

角色控制器

KCC配置

根據玩家的狀態,有3種不同的KCC配置可供應用。第一個只是預設行為,允許正常移動。第二種是在持球時使用的,它降低了玩家的移動速度和跳躍高度。第三種是在能力使用和被擊倒時應用的。它可以防止所有基於輸入的運動和重力,並允許透過程式碼進行完全控制。仍然執行KCC->Move()方法,以防止玩家在被程式碼移動時進入障礙物內部。

C#

public unsafe void UpdateKCCSettings(Frame frame, EntityRef playerEntityRef)
{
    PlayerStatus* playerStatus = frame.Unsafe.GetPointer<PlayerStatus>(playerEntityRef);
    AbilityInventory* abilityInventory = frame.Unsafe.GetPointer<AbilityInventory>(playerEntityRef);
    CharacterController3D* kcc = frame.Unsafe.GetPointer<CharacterController3D>(playerEntityRef);

    CharacterController3DConfig config;

    if (playerStatus->IsKnockbacked || abilityInventory->HasActiveAbility)
    {
        config = frame.FindAsset<CharacterController3DConfig>(NoMovementKCCSettings.Id);
    }
    else if (playerStatus->IsHoldingBall)
    {
        config = frame.FindAsset<CharacterController3DConfig>(CarryingBallKCCSettings.Id);
    }
    else
    {
        config = frame.FindAsset<CharacterController3DConfig>(DefaultKCCSettings.Id);
    }

    kcc->SetConfig(frame, config);
}

土狼時間

為了獲得更好的遊戲體驗,並儘量減少玩家在平台之間跳躍時的錯誤,有一個「土狼時間」機制。它允許玩家在升空後不久正常跳躍。每當玩家落地時,都會啟動一個JumpCoyoteTimer。當玩家試圖跳躍而不是檢查是否落地時,我們會檢查是否JumpCoyoteTimer.IsRunning

視圖插值

當球被握住時,它的實際位置在玩家的中心,它的物理被禁用,並且不能以任何管道進一步操縱。這允許視圖暫時控制球,並透過動畫移動其圖形。一旦球被玩家接住或釋放,它的變換就會在真實空間和動畫空間之間快速插值。

C#

public unsafe class BallEntityView : QuantumEntityView
{
    private float _interpolationSpaceAlpha;

    public void UpdateSpaceInterpolation()
    {
        // . . .
        UpdateInterpolationSpaceAlpha(isBallHeldByPlayer);

        if (_interpolationSpaceAlpha > 0f)
        {
            Vector3 interpolatedPosition = Vector3.Lerp(_lastBallRealPosition, _lastBallAnimationPosition, _interpolationSpaceAlpha);
            Quaternion interpolatedRotation = Quaternion.Slerp(_lastBallRealRotation, _lastBallAnimationRotation, _interpolationSpaceAlpha);

            transform.SetPositionAndRotation(interpolatedPosition, interpolatedRotation);
        }
    }

    private void UpdateInterpolationSpaceAlpha(bool isBallHeldByPlayer)
    {
        float deltaChange = _spaceTransitionSpeed * Time.deltaTime;
        if (isBallHeldByPlayer)
        {
            _interpolationSpaceAlpha += deltaChange;
        }
        else
        {
            _interpolationSpaceAlpha -= deltaChange;
        }

        _interpolationSpaceAlpha = Mathf.Clamp(_interpolationSpaceAlpha, 0f, 1f);
    }
}

重力規模

當投球是為了在沒有抛物線的情況下進行低傳球,也沒有大幅增加投球力時,球暫時不受重力的影響。投球後,使用曲線將其GravityScale從0快速插值到1,以便將控制權交還給物理系統並獲得更真實的結果。

C#

private void UpdateBallGravityScale(Frame frame, ref Filter filter, BallHandlingData ballHandlingData)
{
    if (filter.BallStatus->GravityChangeTimer.IsRunning)
    {
        FP gravityScale = ballHandlingData.ThrowGravityChangeCurve.Evaluate(filter.BallStatus->GravityChangeTimer.NormalizedTime);
        filter.PhysicsBody->GravityScale = gravityScale;

        filter.BallStatus->GravityChangeTimer.Tick(frame.DeltaTime);
        if (filter.BallStatus->GravityChangeTimer.IsDone)
        {
            ResetBallGravity(frame, filter.EntityRef);
        }
    }
}

自定義橫向摩擦力

當球在地面上彈跳/滾動時,會對其施加額外的橫向摩擦,以便在投擲時對其行進距離進行更精確的控制,並防止其不斷從邊緣滾入空隙。

C#

public void OnCollisionEnter3D(Frame frame, CollisionInfo3D info)
{
    if (frame.Unsafe.TryGetPointer(info.Entity, out BallStatus* ballStatus))
    {
        ballStatus->HasCollisionEnter = true;
    }
}

public void OnCollision3D(Frame frame, CollisionInfo3D info)
{
    if (frame.Unsafe.TryGetPointer(info.Entity, out BallStatus* ballStatus))
    {
        ballStatus->HasCollision = true;
    }
}

private void HandleBallCollisions(Frame frame, ref Filter filter, BallHandlingData ballHandlingData)
{
    if (!filter.PhysicsBody->IsKinematic)
    {
        if (filter.BallStatus->HasCollisionEnter)
        {
            filter.PhysicsBody->Velocity.X *= ballHandlingData.LateralBounceFriction;
            filter.PhysicsBody->Velocity.Z *= ballHandlingData.LateralBounceFriction;

            frame.Events.OnBallBounced(filter.EntityRef);
        }

        if (filter.BallStatus->HasCollision)
        {
            filter.PhysicsBody->Velocity.X *= ballHandlingData.LateralGroundFriction;
            filter.PhysicsBody->Velocity.Z *= ballHandlingData.LateralGroundFriction;
        }
    }

    filter.BallStatus->HasCollisionEnter = false;
    filter.BallStatus->HasCollision = false;
}

輸入

輸入由Unity的輸入系統包處理。在Quantum碼方面,以QuantumDemoInputTopDown為基礎。

C#

// DSL Definition
[ExcludeFromPrototype]
struct QuantumDemoInputTopDown {
    FPVector2 MoveDirection;
    FPVector2 AimDirection;
    button Left;
    button Right;
    button Up;
    button Down;
    button Jump;
    button Dash;
    button Fire;
    button AltFire;
    button Use;
}

C#

// Example use case
public override void Update(Frame frame, ref Filter filter)
{
    QuantumDemoInputTopDown input = *frame.GetPlayerInput(filter.PlayerStatus->PlayerRef);
    // . . .
}

相機

相機由Cinemachine透過CinemachineTargetGroup控制,以便使用更高的本機玩家權重和更大的球半徑來聚焦所有演員,從而可以輕鬆地拍攝所有動作。

第三方資產

此示例包括第三方免費資產。您可以在各自的網站上為自己的項目獲取完整的套裝軟體:

Back to top