Fusion Razor Madness

개요

Fusion Razor Madness 샘플은 8명 이상의 플레이어를 위한 틱 기반 플랫폼 레이싱 게임입니다. 매끄럽고 정밀한 플레이어의 움직임과 수평을 향해 벽 점프를 할 수 있는 능력은 점프할 때 좋은 제어감과 만족감을 가져다주고 모든 종류의 위험을 피합니다.

메인 화면으로

다운로드

버전 릴리즈 일자 다운로드
1.1.1 Jun 14, 2022 Fusion Razor Madness 1.1.1 Build 8

메인 화면으로

Networked Platformer 2D 컨트롤러

정확한 플레이어 예측

플랫포머 이동을 다룰 때, 플레이어가 결정의 즉각적인 결과를 보고 느끼게 하는 것이 중요합니다. 이러한 점을 염두에 두고 플레이어 이동은 스냅샷 위치와 완벽하게 일치하는 예측 클라이언트 물리학을 사용합니다.

클라이언트 측 예측을 활성화하려면 Network Project Config으로 이동하고 Server physics ModeClient Prediction으로 설정하십시오.

Setting Client Predicted Physics in the Network Project Config
네트워크 프로젝트 구성에서 클라이언트 예측 물리학 설정.

그런 다음 PlayerScript에서 NetworkRigidbody2D 보간 데이터 소스를 Predicted으로 설정합니다. 입력 권한만 로컬에서 예측되며 프록시는 여전히 스냅샷 보간을 통해 업데이트됩니다.

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

메인 화면으로

더 좋은 점프 논리

입력 및 현재 점프 상태에서 힘을 더 잘 사용하여 플레이어에게 무겁지만 제어 가능한 느낌을 줄 수 있습니다.

재시뮬레이션을 수행하려면 FixedUpdateNetwork()에서 이 함수를 호출해야 합니다. 지정된 틱에 대해 모든 클라이언트를 동일한 방식으로 동기화하려면 유니티의 일반 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);
    }
}

메인 화면으로

플레이어 데이터를 간직하기 위한 네트워크 객체

플레이어와 관련된 [Networked] 데이터를 NetworkBehaviour에서 도출해 NetworkObject에 보관하는 클래스를 만들 수 있습니다.

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

메인 화면으로

장애물

고정된 톱

가장 간단한 톱은 FixedNetworkUpdate()와 같은 안전한 방법으로 충돌이 감지되는 유니티 GameObject 일 뿐입니다.

OnCollisionEnterOnCollisionExit은 재시뮬레이션에서는 신뢰할 수 없다는 것을 기억하세요.

메인 화면으로

톱 회전

NetworkTransform 컴포넌트를 사용하여 모든 클라이언트 간에 동기화하는 회전하는 톱입니다. FixedUpdateNetwork 에서 [Networked] 속성을 가진 원의 위치를 계산하여 재시뮬레이션 시 안전하도록 합니다.

[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 스크립트에 대해 한 번만 정의되고 변경되지 않으므로 일반 유니티 속성이 될 수 있습니다.

메인 화면으로

톱 이동하기

톱 이동은 회전 톱과 동일한 원리를 사용하지만, 원의 위치 대신 편집기에서 정의된 위치 목록을 사용하고 위치 사이에 위치를 보간합니다.

[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: 플레이어 물리 머터리얼.
  • Prefabs: 프로젝트에 사용된 모든 프리팹, 가장 중요한 것은 플레이어 프리팹입니다.
  • Scenes: 로비와 레벨 씬.
  • Scriptable Objects: 오디오 채널 및 오디오 에셋과 같이 사용되는 스크립트가 들어 있습니다.
  • Scripts: 데모, 스크립트 폴더의 핵심도 논리 범주로 세분됩니다.
  • URP: T프로젝트에 사용되는 범용 렌더 파이프라인 자산입니다.

메인 화면으로

로비

로비에서는 네트워크 디버깅 시작 GUI를 수정해 사용하고 있으며, 원하는 닉네임을 입력한 후 1인용 게임, 호스트 게임, 기존 룸에 클라이언트로 참여할 수 있습니다.

Lobby Menu
로비 메뉴.

Inside the Room View
룸 뷰 내부.

이때 호스트는 룸에 있는 각 플레이어의 데이터를 사용하여 NetworkObject를 생성합니다. 플레이어 데이터를 보유하는 네트워크 개체에 나와 있습니다. 참여 후 플레이어 명단이 표시됩니다. 게임 시작 버튼을 눌러야만 게임을 시작할 수 있습니다.

메인 화면으로

게임 시작

호스트가 게임을 시작하면 LoadingManager 스크립트에 의해 다음 레벨이 선택됩니다. runner.SetActiveScene(scenePath)를 사용하여 원하는 수준을 로드합니다.

주의: 호스트만 NetworkRunner에서 활성 씬을 설정할 수 있습니다.

LevelBehaviour.Spawned() 메소드를 사용하여 PlayerSpawner는 로비에 등록된 모든 플레이어를 생성하고 현재 입력 권한을 부여합니다.

공평하게 말하면, 5초 후에 플레이어들은 레벨 로딩이 완료 되었는지 여부에 관계없이 레이싱을 시작하기 위해 나타납니다. 이는 문제가 발생할 경우 로드 프로세스에서 개별 클라이언트의 불일치로 인한 무한 로드 시간을 방지하기 위한 것입니다.

그 시간은 TickTimer로 계산됩니다.

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

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

메인 화면으로

입력 처리

Fusion은 유니티의 표준 입력 처리 메커니즘을 사용하여 플레이어 입력을 캡처하여 네트워크를 통해 전송할 수 있는 데이터 구조에 저장한 다음 FixedUpdateNetwork() 메소드로 이 데이터 구조를 처리합니다. 이 예에서 이 모든 것은 InputData 구조체를 사용하여 InputController 클래스에 의해 구현되지만, 실제 상태 변화는 PlayerMovementPlayerBehaviour 클래스에 전달됩니다.

메인 화면으로

경주 끝내기

LevelBehaviour는 상위 3명의 플레이어의 ID를 얻기 위해 일련의 우승자를 유지합니다.

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

플레이어가 결승선을 통과하면 LevelBehaviour를 알려줍니다. 그런 다음 LevelBehaviour는 정확한 승자 수에 도달했는지 확인합니다. 도달하면 레벨이 끝나고 결과가 표시됩니다.


기술문서 TOP으로 돌아가기