레이저 매드니스
개요
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