このページは編集中です。更新が保留になっている可能性があります。

Fusion Razor Madness

概要

Fusion Razor Madness サンプルは、8人以上のプレイヤー向けのティックベースのプラットフォーマーレーシングゲームです。プレイヤーの動きはスムーズかつ正確で、レベルに向かって壁ジャンプする機能もあり、ジャンプして危険を回避する際に良好なコントロール感覚と満足感をもたらしてくれます。

トップに戻る

ダウンロード

バージョン リリース日 ダウンロード
1.1.1 Jun 14, 2022 Fusion Razor Madness 1.1.1 Build 8

トップに戻る

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

プレイヤーの正確な予測

プラットフォーマーの動きを扱う場合、プレイヤーに自分の判断の結果をすぐに見て感じてもらうことが重要です。それを考慮して、プレイヤーの動きは、スナップショットの位置に完全に一致する予測されたクライアント物理を使用します。

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

Setting Client Predicted Physics in the Network Project Config
Network Project ConfigでClient Predicted Physicsを設定。

次に、PlayerScriptNetworkRigidbody2D 補間データソースを Predicted に設定します。入力機関のみがローカルに予測され、プロキシはスナップショット補間によって更新されます。

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

トップに戻る

ジャンプロジックの改善

入力と現在のジャンプの状態から、より良い力を用いて、重いが制御可能な感触をプレイヤーに与えることができます。

この関数は FixedUpdateNetwork() の中で呼び出すことで、再シミュレーションができるようになります。さらに、特定のtickに対して全てのクライアントを同じように同期させるために、Unityの通常の Time.deltaTime の代わりに Runner.DetaTime (Fusion専用) を使用しなければなりません。

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;
    }
}

こうすることで、より高くジャンプできるようになり、壁面を滑るときはゆっくり落ちるようになりました。

トップに戻る

死亡状態の同期

OnChanged コールバックを使用すると、プロキシのグラフィックは、クライアント側で予測/シミュレーションされた死亡ではなく、サーバによって確認された死亡によって無効にされます。

[Networked(OnChanged = nameof(OnSpawningChange))]
private NetworkBool Respawning { get; set; }
public static void OnSpawningChange(Changed<PlayerBehaviour> changed)
{
    if (changed.Behaviour.Respawning)
    {
        changed.Behaviour.SetGFXActive(false);
    }
    else
    {
        changed.Behaviour.SetGFXActive(true);
    }
}

トップに戻る

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

NetworkBehaviour から派生して NetworkObject に保持することで、プレイヤーに関するあらゆる [Networked] データを保持するクラスを作成することができます。

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 スクリプトのインスタンスを持っています)。この NetworkObjectSpawned()Runner.SetPlayerObject 関数でこの PlayerRef のメインオブジェクトとして設定されます。

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() メソッドを呼び出し、該当する NetworkObjectPlayerData コンポーネントを探せば、データを取得することが可能です。

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;
    }
}

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

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

トップに戻る

観戦モード

規定人数に達する前にプレイヤーがレースを終了すると、観戦モードになります。観戦中は自分のキャラクターを操作することができず、カメラは好きなプレイヤーを追うことができます。観戦中のプレイヤーは、矢印キーを使って残りのプレイヤーの視界を移動することができます。

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

トップに戻る

障害物

固定ノコギリ

最もシンプルなノコギリはUnity GameObjectで、そのノコギリは`FixedNetworkUpdate()'のようなティックセーフな方法で検出されます。

OnCollisionEnterOnCollisionExitは再シミュレーションの際に信頼できないことに注意してください。 ### 回転ノコギリ すべてのクライアント間で同期するようにNetworkTransformコンポーネントを使用した回転ノコギリです。OnCollisionEnterOnCollisionExit` は再シミュレーションの際に信頼できないことに注意してください。

トップに戻る

回転ノコギリ

すべてのクライアント間で同期するように NetworkTransform コンポーネントを使用した回転ノコギリです。これは、再シミュレーションの際に安全なように [Networked] プロパティを持つ FixedUpdateNetwork で円周上の位置を計算し、それを適用します。

[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 スクリプトごとに一度だけ定義され、決して変更されないので、通常の Unity のプロパティにすることができます。

トップに戻る

移動するノコギリ

移動するノコギリは、回転するノコギリと同じ原理を使用しています。しかし、円周上の位置ではなく、エディターで定義された位置のリストを使い、それらの間で位置を補間します。

[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: SFXと音楽ファイルが含まれます。
  • Photon:Fusionパッケージです。
  • Physics Materials:プレイヤーのフィジックス素材。
  • Prefab:プロジェクトで使用されているすべてのプレハブ。最も重要なのはPlayerプレハブ。
  • Scene:ロビーとレベルのシーン。
  • Scriptable Objects:オーディオチャンネルやオーディオアセットなど、使用されるスクリプタブルオブジェクトが含まれる。
  • Scripts:Demoの核。Scriptsフォルダは、ロジックのカテゴリに細分化されている。
  • URP:プロジェクトで使用されたUniversal Render Pipeline Assets。

トップに戻る

ロビー

ロビーはNetwork Debug Start GUIを改良したものを使用しています。希望するニックネームを入力した後、プレイヤーは一人用のゲームをプレイするか、ゲームをホストするか、クライアントとして既存の部屋に参加するかを選択することができます。

Lobby Menu
ロビーメニュー

Inside the Room View
ルームビューの内側。

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

トップに戻る

ゲーム開始

ホストがゲームを開始すると、次のレベルは LoadingManager スクリプトによって選択されます。runner.SetActiveScene(scenePath) を使用して、希望するレベルを読み込みます。

N.B.: ホストのみが NetworkRunner にアクティブなシーンを設定することができます。

LevelBehaviour.Spawned() メソッドを使用して、PlayerSpawner はロビーに登録されているすべてのプレイヤーをスポーンして、現在の Input Authority を与えるよう要求されます。

5秒後にプレイヤーを解放し、レベルの読み込みが終了しているかどうかに関係なくレースを開始します。これは、何か問題が発生したときに、個々のクライアントのロードプロセスの不一致による無限のロード時間を避けるためです。

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

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

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

トップに戻る

入力の処理

Fusion は Unity の標準的な入力処理メカニズムを使ってプレイヤーの入力をキャプチャし、ネットワークに送信できるデータ構造に格納し、 FixedUpdateNetwork() メソッドでこのデータ構造を使って動作します。この例では InputController クラスが InputData 構造体を使って実装していますが、実際の状態の変更は PlayerMovement クラスと PlayerBehaviour クラスに委ねています。

トップに戻る

レースを終了する

LevelBehaviourは上位3人のIDを取得するためにwinnerの配列を保持します。

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

プレイヤーがゴールラインを通過すると、 LevelBehaviour に通知されます。次に LevelBehaviour は正しい勝者数に達したかどうかをチェックします。達している場合、そのレベルは終了し、結果が表示されます。


ドキュメントのトップへ戻る