레이저 매드니스
개요
Fusion 레이저 매드니스 샘플은 8명 이상의 플레이어를 위한 틱 기반의 플랫폼 레이싱 게임입니다. 부드럽고 정확한 플레이어 움직임과 함께 레벨을 향해 월 점프하는 능력이 결합되어 모든 종류의 위험을 피할 때 뛰어난 제어력과 만족감을 가져다줍니다.
 
         
         
         
         
         
        다운로드
| 버전 | 릴리즈 일자 | 다운로드 | |
|---|---|---|---|
| 2.0.1 | Jun 17, 2024 | Fusion Razor Madness 2.0.1 Build 575 | |
네트워크 플랫폼 게임 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()에서 이 함수를 호출하여 재시뮬레이션을 수행하는 것이 중요합니다. 또한 모든 클라이언트를 동일한 방식으로 동기화하려면 유니티의 일반적인 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 스크립트의 인스턴스와). 이 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;
}
장애물
고정 톱
가장 간단한 톱은 FixedNetworkUpdate()와 같이 틱 안전한 방법으로 충돌이 감지되는 유니티 GameObject일뿐입니다.
OnCollisionEnter 와 OnCollisionExit는 재시뮬레이션에 신뢰할 수 없습니다.
회전 톱
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: sfx 및 음악 파일.
- Photon: Fusion 패키기.
- Physics Materials: 플레이어 물리 머터리얼.
- Prefabs: 프로젝트에 사용된 모든 프리팹, 가장 중요한 것은 플레이어 프리팹입니다.
- Scenes: 로비와 레벨 씬.
- Scriptable Objects: 오디오 채널 및 오디오 에셋과 같이 사용되는 스크립트 테이블이 포함되어 있습니다.
- Scripts: Demo, Scripts 폴더의 핵심은 로직 카테고리에서도 세분화됩니다.
- URP: 프로젝트에 사용된 범용 렌더링 파이프라인 자산입니다.
로비
로비에서는 Network Debug Start GUI를 변형해 사용합니다. 사용자가 원하는 닉네임을 입력하면 한 명의 플레이어가 게임을 하거나, 게임을 진행하거나 기존 룸에서 클라이언트로 참여하는 방식 중 하나를 선택할 수 있습니다.
 
     
    이때 호스트는 자신의 데이터를 가지고 룸에 있는 각 플레이어의 NetworkObject를 생성하게 됩니다. 플레이어 데이터를 갖고 있기 위한 네트워크 객체에 나와 있는 것처럼 말입니다.
룸 참여 후에는 플레이어 목록이 표시됩니다. 게임 시작 버튼을 눌러 진행자만 게임을 시작할 수 있습니다.
게임 시작
호스트가 게임을 시작하면 LoadingManager 스크립트를 통해 다음 레벨을 선택하고 runner.SetActiveScene(scenePath)을 사용하여 원하는 레벨을 로드합니다.
주의: 호스트만이 NetworkRunner에서 활성 씬을 설정할 수 있습니다.
PlayerSpawner는 LevelBehaviour.Spawned() 메소드로 로비에 등록된 모든 플레이어를 스폰하고 현재 사용 중인 입력 권한을 부여합니다.
공평하게 , 레벨 로딩을 완료했는지 여부와 관계없이 5초 후에 플레이어를 풀어 경기를 시작합니다. 이는 로딩 과정에서 개별 클라이언트 불일치로 인한 무한 로딩 시간을 피하기 위함입니다.
이 초는 TickTimer에 의해 계산됩니다.
C#
[Networked]
private TickTimer StartTimer { get; set; }
private void SetLevelStartValues()
{
    //...
    StartTimer = TickTimer.CreateFromSeconds(Runner, 5);
}
입력 처리
Fusion은 유니티의 표준 입력 처리 메커니즘을 사용하여 플레이어의 입력을 캡처하여 네트워크를 통해 전송할 수 있는 데이터 구조체에 저장한 후 FixedUpdateNetwork() 메소드에서 이 데이터 구조체를 제거합니다. 이 예에서는 이 모든 것을 InputData 구조체를 사용하는 InputController 클래스로 구현하지만 실제 상태 변화는 PlayerMovement 및 PlayerBehaviour 클래스로 구분합니다.
경주 끝내기
LevelBehaviour은 상위 3명의 아이디를 얻기 위해 일련의 우승자들을 유지하고 있습니다.
C#
[Networked, Capacity(3)] private NetworkArray<int> _winners => default;
public NetworkArray<int> Winners { get => _winners; }
플레이어가 결승선을 통과하면 LevelBehaviour을 알려줍니다. LevelBehaviour은 정확한 숫자에 도달했는지 확인한 후 레벨이 끝나면 결과가 표시됩니다.
타사 에셋
레이저 매드니스 샘플에는 각 제작자가 제공하는 몇 가지 에셋이 포함되어 있습니다. 전체 패키지는 각 사이트에서 자신의 프로젝트를 위해 구입할 수 있습니다:
- Bakudas의 Generic Dungeon Pack
- Essssam의 Rocky Roads
- o_lobster의 Platform/Metroidvania Pixel Art Asset Pack
중요: 상업적 프로젝트에 사용하려면 각 창작자에게 라이선스를 구입해야 합니다.
Back to top