Razor Madness
概要
Fusion Razor Madness サンプルは、8人以上のプレイヤー向けのティックベースのプラットフォーマーレーシングゲームです。プレイヤーの動きはスムーズかつ正確で、レベルに向かって壁ジャンプする機能もあり、ジャンプして危険を回避する際に良好なコントロール感覚と満足感をもたらしてくれます。
ダウンロード
バージョン | リリース日 | ダウンロード | ||
---|---|---|---|---|
1.1.6 | Apr 12, 2023 | Fusion Razor Madness 1.1.6 Build 162 |
ネットワーク型プラットフォーマー 2Dコントローラ
プレイヤーの正確な予測
プラットフォーマーの動きを扱う場合、プレイヤーに自分の判断の結果をすぐに見て感じてもらうことが重要です。それを考慮して、プレイヤーの動きは、スナップショットの位置に完全に一致する予測されたクライアント物理を使用します。
クライアント側の予測を有効にするには、Network Project Config
に移動して、Server physics Mode
をClient Prediction
に設定します。
次に、PlayerScript
で NetworkRigidbody2D
補間データソースを Predicted
に設定します。入力機関のみがローカルに予測され、プロキシはスナップショット補間によって更新されます。
C#
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専用) を使用しなければなりません。
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;
}
}
こうすることで、より高くジャンプできるようになり、壁面を滑るときはゆっくり落ちるようになりました。
死亡状態の同期
OnChanged
コールバックを使用すると、プロキシのグラフィックは、クライアント側で予測/シミュレーションされた死亡ではなく、サーバによって確認された死亡によって無効にされます。
C#
[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]
データを保持するクラスを作成することができます。
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;
}
障害物
固定ノコギリ
最もシンプルなノコギリはUnity GameObjectで、そのノコギリは`FixedNetworkUpdate()'のようなティックセーフな方法で検出されます。
OnCollisionEnterと
OnCollisionExitは再シミュレーションの際に信頼できないことに注意してください。 ### 回転ノコギリ すべてのクライアント間で同期するように
NetworkTransform コンポーネントを使用した回転ノコギリです。
OnCollisionEnterと
OnCollisionExit` は再シミュレーションの際に信頼できないことに注意してください。
回転ノコギリ
すべてのクライアント間で同期するように NetworkTransform
コンポーネントを使用した回転ノコギリです。これは、再シミュレーションの際に安全なように [Networked]
プロパティを持つ FixedUpdateNetwork
で円周上の位置を計算し、それを適用します。
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
スクリプトごとに一度だけ定義され、決して変更されないので、通常の Unity のプロパティにすることができます。
移動するノコギリ
移動するノコギリは、回転するノコギリと同じ原理を使用しています。しかし、円周上の位置ではなく、エディターで定義された位置のリストを使い、それらの間で位置を補間します。
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: SFXと音楽ファイルが含まれます。
- Photon:Fusionパッケージです。
- Physics Materials:プレイヤーのフィジックス素材。
- Prefab:プロジェクトで使用されているすべてのプレハブ。最も重要なのはPlayerプレハブ。
- Scene:ロビーとレベルのシーン。
- Scriptable Objects:オーディオチャンネルやオーディオアセットなど、使用されるスクリプタブルオブジェクトが含まれる。
- Scripts:Demoの核。Scriptsフォルダは、ロジックのカテゴリに細分化されている。
- URP:プロジェクトで使用されたUniversal Render Pipeline Assets。
ロビー
ロビーはNetwork Debug Start GUI
を改良したものを使用しています。希望するニックネームを入力した後、プレイヤーは一人用のゲームをプレイするか、ゲームをホストするか、クライアントとして既存の部屋に参加するかを選択することができます。
このとき、ホストはルーム内の各プレイヤーのデータを保持する NetworkObject
を作成します。プレイヤーデータを保持するネットワークオブジェクトにあるように、プレイヤーデータを保持するネットワークオブジェクトを作成します。
ルームに参加すると、プレイヤーのリストが表示されます。Start Game ボタンを押すと、ホストだけがゲームを開始することができます。
ゲーム開始
ホストがゲームを開始すると、次のレベルは LoadingManager
スクリプトによって選択されます。runner.SetActiveScene(scenePath)
を使用して、希望するレベルを読み込みます。
N.B.: ホストのみが 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を取得するためにwinnerの配列を保持します。
C#
[Networked, Capacity(3)] private NetworkArray<int> _winners => default;
public NetworkArray<int> Winners { get => _winners; }
プレイヤーがゴールラインを通過すると、 LevelBehaviour
に通知されます。次に LevelBehaviour
は正しい勝者数に達したかどうかをチェックします。達している場合、そのレベルは終了し、結果が表示されます。