This document is about: FUSION 2
SWITCH TO

Razor Madness

Level 4

概要

Fusion Razor Madness サンプルは、8人以上のプレイヤー向けのターン制プラットフォームレーシングゲームです。スムーズで正確なプレイヤーの動きと、レベルに向かって壁をジャンプする能力が組み合わさり、ジャンプしてさまざまな障害物を避ける際に良好なコントロール感と満足感を得ることができます。

ダウンロード

バージョン リリース日 ダウンロード
2.0.1 Jun 17, 2024 Fusion Razor Madness 2.0.1 Build 575

ネットワーク対応プラットフォーマー 2D コントローラー

正確なプレイヤープリディクション

プラットフォーマーの動きを扱う際には、プレイヤーが自分の選択の即時的な結果を見たり感じたりできることが重要です。この点を考慮し、プレイヤーの動きにはスナップショット位置と完全に一致する予測されたクライアント物理を使用しています。

クライアント側の予測を有効にするには、Network Project Config に移動し、Server physics ModeClient Prediction に設定してください。

Setting Client Predicted Physics in the Network Project Config
Setting Client Predicted Physics in the Network Project Config.

次に、PlayerScriptNetworkRigidbody2D の補間データソースを Predicted に設定します。入力権限のみが ローカルで 予測される一方で、プロキシはスナップショット補間を通じて更新されます。

C#

public override void Spawned(){
    if(Object.HasInputAuthority)
    {
        // Set Interpolation data source to predicted if is input authority
        _rb.InterpolationDataSource = InterpolationDataSources.Predicted;
    }
}

より良いジャンプロジック

入力と現在のジャンプ状態を考慮することで、プレイヤーに重さのある、しかし制御可能な感覚を与えるために力をより効果的に使用することが可能になります。

この関数は FixedUpdateNetwork() 内で呼び出すことが重要で、これにより再シミュレーションが行えるようになります。また、特定の Fusion 用の Runner.DeltaTime を使用する必要があり、Unity の通常の Time.deltaTime の代わりに使用して、各クライアントを特定のティックで同じように同期させる必要があります。

C#

private void BetterJumpLogic(InputData input)
{
    if (_isGrounded) { return; }
    if (_rb.Rigidbody.velocity.y < 0)
    {
        if (_wallSliding && input.AxisPressed())
        {
            _rb.Rigidbody.velocity += Vector2.up * Physics2D.gravity.y * (wallSlidingMultiplier - 1) * Runner.DeltaTime;
        }
        else
        {
            _rb.Rigidbody.velocity += Vector2.up * Physics2D.gravity.y * (fallMultiplier - 1) * Runner.DeltaTime;
        }
    }
    else if (_rb.Rigidbody.velocity.y > 0 && !input.GetState(InputState.JUMPHOLD))
    {
        _rb.Rigidbody.velocity += Vector2.up * Physics2D.gravity.y * (lowJumpMultiplier - 1) * Runner.DeltaTime;
    }
}

このようにして、プレイヤーは望む場合により高くジャンプでき、壁を滑るときはゆっくりと落下することができます。

デス状態の同期

ChangeDetector を使用することで、プロキシのグラフィックはサーバーによって確認された死亡によって無効化され、クライアント側で予測またはシミュレーションされた死亡(サーバーによって確認されないもの)では無効にされません。これにより、サーバーが許可したときにプロキシのグラフィックを再び有効にすることも可能になります。

C#

[Networked]
private NetworkBool Respawning { get; set; }

public override void Render()
{
    foreach (var change in _changeDetector.DetectChanges(this))
    {
        switch (change)
        {
            case nameof(Respawning):
                SetGFXActive(!Respawning);
                break;
        }
    }
}

プレイヤーデータを保持するネットワークオブジェクト

NetworkBehaviour を継承したクラスを作成することで、プレイヤーに関連する任意の [Networked] データを保持することが可能です。このクラスは NetworkObject 上に保持できます。

C#

public class PlayerData: NetworkBehaviour
{
    [Networked]
    public string Nick { get; set; }
    [Networked]
    public NetworkObject Instance { get; set; }

    [Rpc(sources: RpcSources.InputAuthority, targets: RpcTargets.StateAuthority)]
    public void RPC_SetNick(string nick)
    {
        Nick = nick;
    }

    public override void Spawned()
    {
        if (Object.HasInputAuthority)
            RPC_SetNick(PlayerPrefs.GetString("Nick"));

        DontDestroyOnLoad(this);
        Runner.SetPlayerObject(Object.InputAuthority, Object);
        OnPlayerDataSpawnedEvent?.Raise(Object.InputAuthority, Runner);
    }
}

この場合、必要なのはプレイヤーの Nick のみで、入力権限を持つ現在の NetworkObject への参照が必要です。OnPlayerDataSpawnedEvent は、このサンプルにおけるロビーの同期を処理するためのカスタムイベントです。

プレイヤーが参加すると、Nick はテキスト入力フィールドや別のソースから設定され、その後 NetworkObject プレハブが生成されます(このオブジェクトには PlayerData スクリプトのインスタンスが含まれています)。この NetworkObject は、Spawned() 内の Runner.SetPlayerObject 関数を使用して、自身をこの PlayerRef のメインオブジェクトとして設定します。

C#

public void OnPlayerJoined(NetworkRunner runner, PlayerRef player)
{
    if (runner.IsServer)
    {
        runner.Spawn(PlayerDataNO, inputAuthority: player);
    }

    if (runner.LocalPlayer == player)
    {
        LocalRunner = runner;
    }

    OnPlayerJoinedEvent?.Raise(player, runner);
}

特定のプレイヤーからデータが必要な場合は、NetworkRunner.TryGetPlayerObject() メソッドを呼び出して、対象の NetworkObject 上の PlayerData コンポーネントを探すことで取得できます。

C#

public PlayerData GetPlayerData(PlayerRef player, NetworkRunner runner)
{
    NetworkObject NO;
    if (runner.TryGetPlayerObject(player, out NO))
    {
        PlayerData data = NO.GetComponent<PlayerData>();
        return data;
    }
    else
    {
        Debug.LogError("Player not found");
        return null;
    }
}

このデータは必要に応じて使用したり/または操作することができます。

C#

   //e.g
    PlayerData data = GetPlayerData(player, Runner);
    Runner.despawn(data.Instance);
    string playerNick = data.Nick;

スペクテーターモード

プレイヤーが必要な数の勝者に達する前にレースを完了すると、スペクテーターモードに入ります。観戦中は自分のキャラクターを操作できず、カメラは選択したプレイヤーを追従することができます。観戦しているプレイヤーは、矢印キーを使って残りのプレイヤーの視点に移動することができます。

C#

/// <summary>
/// Set player state as spectator.
/// </summary>
public void SetSpectating()
{
    _spectatingList = new List<PlayerBehaviour>(FindObjectsOfType<PlayerBehaviour>());
    _spectating = true;
    CameraTarget = GetRandomSpectatingTarget();
}

private void Update()
{
    if (_spectating)
    {
        if (Input.GetKeyDown(KeyCode.RightArrow))
        {
            CameraTarget = GetNextOrPrevSpectatingTarget(1);
        }
        else if (Input.GetKeyDown(KeyCode.LeftArrow))
        {
            CameraTarget = GetNextOrPrevSpectatingTarget(-1);
        }
    }
}

private void LateUpdate()
{
    if (CameraTarget == null)
    {
        return;
    }

    _step = Speed * Vector2.Distance(CameraTarget.position, transform.position) * Time.deltaTime;

    Vector2 pos = Vector2.MoveTowards(transform.position, CameraTarget.position + _offset, _step);
    transform.position = pos;
}

障害物

固定ののこぎり

最もシンプルなのこぎりは、ユニティの GameObject で、衝突は FixedNetworkUpdate() のようにティックセーフな方法で検出されます。

OnCollisionEnterOnCollisionExit は、再シミュレーションでは信頼できないことを念頭に置いてください。

回転するのこぎり

回転するのこぎりは、NetworkTransform コンポーネントを使用して、すべてのクライアント間で同期を取ります。 FixedUpdateNetwork で円上の位置を計算し、再シミュレーションに安全な [Networked] プロパティを使用して適用します。

C#

[Networked] private int Index { get; set; }

public override void FixedUpdateNetwork()
{
    transform.position = PointOnCircle(_radius, Index, _origin);
    _line.SetPosition(1, transform.position);
    Index = Index >= 360 ? 0 : Index + (1 * _speed);
}

public static Vector2 PointOnCircle(float radius, float angleInDegrees, Vector2 origin)
{
    // Convert from degrees to radians via multiplication by PI/180        
    float x = (float)(radius * Mathf.Cos(angleInDegrees * Mathf.PI / 180f)) + origin.x;
    float y = (float)(radius * Mathf.Sin(angleInDegrees * Mathf.PI / 180f)) + origin.y;

    return new Vector2(x, y);
}

変更可能なすべてのプロパティで、位置計算に使用されるものは [Networked] にすることを確認してください。_speed は各 RotatingSaw スクリプトでエディター内で一度だけ定義され、変更されないため、通常のユニティプロパティとして扱うことができます。

動くのこぎり

動くのこぎりは、回転するのこぎりと同じ原則を使用します。しかし、円上の位置の代わりに、エディターで定義された位置のリストを使用し、それらの位置間を補間して移動します。

C#

[Networked] private float _delta { get; set; }
[Networked] private int _posIndex { get; set; }
[Networked] private Vector2 _currentPos { get; set; }
[Networked] private Vector2 _desiredPos { get; set; }

public override void FixedUpdateNetwork()
{
    transform.position = Vector2.Lerp(_currentPos, _desiredPos, _delta);
    _delta += Runner.DeltaTime * _speed;

    if (_delta >= 1)
    {
        _delta = 0;
        _currentPos = _positions[_posIndex];
        _posIndex = _posIndex < _positions.Count - 1 ? _posIndex + 1 : 0;
        _desiredPos = _positions[_posIndex];
    }
}

ランタイムで変更可能で、位置計算に影響を与えるすべてのプロパティは、引き続き [Networked] としてマークすることを忘れないでください。

プロジェクト

フォルダー構造

プロジェクトはカテゴリフォルダーに分かれています。

  • Arts: プロジェクトで使用される全てのアートアセット、タイルマップアセットおよびアニメーションファイルを含みます。
  • Audio: 効果音および音楽ファイルを含みます。
  • Photon: Fusionパッケージ。
  • Physics Materials: プレイヤーの物理マテリアル。
  • Prefabs: プロジェクトで使用される全てのプレハブが含まれ、最も重要なのはプレイヤーのプレハブです。
  • Scenes: ロビーおよびレベルシーン。
  • Scriptable Objects: 音声チャンネルや音声アセットなどで使用されるスクリプタブルが含まれます。
  • Scripts: デモのコア部分で、スクリプトフォルダーもロジックカテゴリに分かれています。
  • URP: プロジェクトで使用されるユニバーサルレンダーパイプラインアセット。

ロビー

ロビーは、Network Debug Start GUI の修正バージョンを使用しています。プレイヤーは希望するニックネームを入力した後、シングルプレイヤーゲームをプレイする、ゲームをホストする、または既存のルームにクライアントとして参加することを選択できます。

Lobby Menu
Lobby Menu.
Inside the Room View
Inside the Room View.

この時点で、ホストはルーム内の各プレイヤーのデータを持つ NetworkObject を作成します。プレイヤーデータを保持するネットワークオブジェクト で示されたように、ルームに参加した後、プレイヤーのリストが表示されます。ゲームを開始できるのはホストのみで、Start Game ボタンを押す必要があります。

ゲーム開始

ホストがゲームを開始すると、次のレベルは LoadingManager スクリプトによって選択されます。欲しいレベルをロードするために、runner.SetActiveScene(scenePath) を使用します。

注意: アクティブなシーンを NetworkRunner に設定できるのはホストのみです。

LevelBehaviour.Spawned() メソッドを使用して、PlayerSpawner にロビーで登録されたすべてのプレイヤーをスポーンするようリクエストし、彼らに現在の Input Authority を与えます。

公平を期すために、5秒後にプレイヤーはレースを開始するために解放されます。これは、レベルのロードが完了していなくても行われます。これにより、何か問題が発生した際に、個々のクライアントのロードプロセスの違いによる無限のロード時間を避けることができます。

この秒数は TickTimer によってカウントされます。

C#

[Networked]
private TickTimer StartTimer { get; set; }

private void SetLevelStartValues()
{
    //...
    StartTimer = TickTimer.CreateFromSeconds(Runner, 5);
}

入力の処理

Fusionは、Unityの標準の入力処理メカニズムを使用してプレイヤーの入力をキャプチャし、それをネットワーク上で送信できるデータ構造に格納し、その後 FixedUpdateNetwork() メソッド内でこのデータ構造を使用します。この例では、すべてが InputController クラスによって InputData 構造体を使用して実装されており、実際の状態変化は PlayerMovement および PlayerBehaviour クラスに委譲されます。

レースの終了

LevelBehaviour は、上位3人のプレイヤーのIDを取得するために、勝者の配列を保持しています。

C#

[Networked, Capacity(3)] private NetworkArray<int> _winners => default;
public NetworkArray<int> Winners { get => _winners; }

プレイヤーがフィニッシュラインを越えると、LevelBehaviour に通知します。LevelBehaviour は、適切な数の勝者に達しているかを確認し、達していればレベルは終了し、結果が表示されます。

サードパーティアセット

Razor Madness サンプルには、各クリエイターから提供された複数のアセットが含まれています。完全なパッケージは、それぞれのサイトから自身のプロジェクトで取得できます。

重要: 商業プロジェクトで使用するには、各クリエイターからライセンスを購入する必要があります。

Back to top