This document is about: FUSION 2
SWITCH TO

레이저 매드니스

Level 4

개요

Fusion 레이저 매드니스 샘플은 8명 이상의 플레이어를 위한 틱 기반의 플랫폼 레이싱 게임입니다. 부드럽고 정확한 플레이어 움직임과 함께 레벨을 향해 월 점프하는 능력이 결합되어 모든 종류의 위험을 피할 때 뛰어난 제어력과 만족감을 가져다줍니다.

다운로드

버전 릴리즈 일자 다운로드
2.0.0 Dec 05, 2023 Fusion Razor Madness 2.0.0 Build 350

네트워크 플랫폼 게임 2D 컨트롤러

정확한 플레이어 예측

플랫폼 게임 이동을 다룰 때 플레이어가 자신의 결정에 따른 즉각적인 결과를 보고 느끼게 하는 것이 중요합니다. 이를 염두에 두고 플레이어 운동은 스냅샷 위치와 완벽하게 일치하는 예측된 클라이언트 물리를 사용합니다.

클라이언트 측 예측을 활성화하려면 Network Project Config로 이동하여 Server physics ModeClient 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()에서 이 함수를 호출하여 재시뮬레이션을 수행하는 것이 중요합니다. 또한 모든 클라이언트를 동일한 방식으로 동기화하려면 유니티의 일반적인 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;
    }
}

이렇게 하면 플레이어가 원하는 대로 더 높이 점프할 수 있고 벽을 미끄러질 때 천천히 넘어질 수 있습니다.

죽음 상태 동기화

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 스크립트의 인스턴스와). 이 NetworkObjectSpawned()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;
}

장애물

고정 톱

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

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]인지 확인합니다. _speedRotatingSaw 스크립트마다 편집기에서 한 번만 정의되며 절대 변경되지 않으므로 일반 유니티 속성이 될 수 있습니다.

이동 톱

움직이는 톱은 회전하는 톱과 같은 원리를 사용하지만 원 위의 위치 대신 편집기에 정의된 위치 목록을 사용하고 위치를 그 사이에 보간합니다.

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

로비

로비에서는 Network Debug Start GUI를 변형해 사용합니다. 사용자가 원하는 닉네임을 입력하면 한 명의 플레이어가 게임을 하거나, 게임을 진행하거나 기존 룸에서 클라이언트로 참여하는 방식 중 하나를 선택할 수 있습니다.

로비 메뉴
로비 메뉴.
룸 뷰 내부
룸 뷰 내부.

이때 호스트는 자신의 데이터를 가지고 룸에 있는 각 플레이어의 NetworkObject를 생성하게 됩니다. 플레이어 데이터를 갖고 있기 위한 네트워크 객체에 나와 있는 것처럼 말입니다. 룸 참여 후에는 플레이어 목록이 표시됩니다. 게임 시작 버튼을 눌러 진행자만 게임을 시작할 수 있습니다.

게임 시작

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

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

PlayerSpawnerLevelBehaviour.Spawned() 메소드로 로비에 등록된 모든 플레이어를 스폰하고 현재 사용 중인 입력 권한을 부여합니다.

공평하게 , 레벨 로딩을 완료했는지 여부와 관계없이 5초 후에 플레이어를 풀어 경기를 시작합니다. 이는 로딩 과정에서 개별 클라이언트 불일치로 인한 무한 로딩 시간을 피하기 위함입니다.

이 초는 TickTimer에 의해 계산됩니다.

C#

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

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

입력 처리

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

경주 끝내기

LevelBehaviour은 상위 3명의 아이디를 얻기 위해 일련의 우승자들을 유지하고 있습니다.

C#

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

플레이어가 결승선을 통과하면 LevelBehaviour을 알려줍니다. LevelBehaviour은 정확한 숫자에 도달했는지 확인한 후 레벨이 끝나면 결과가 표시됩니다.

타사 에셋

레이저 매드니스 샘플에는 각 제작자가 제공하는 몇 가지 에셋이 포함되어 있습니다. 전체 패키지는 각 사이트에서 자신의 프로젝트를 위해 구입할 수 있습니다:

중요: 상업적 프로젝트에 사용하려면 각 창작자에게 라이선스를 구입해야 합니다.

Back to top