플레이어 입력
소개
Fusion은 매 틱마다 플레이어 입력을 수집하고, 수집된 입력 데이터를 히스토리 버퍼에 저장하며, 이 데이터를 서버로 자동으로 복제하는 메커니즘을 제공합니다.
Fusion은 주로 클라이언트 측 예측을 가능하게 하기 위해 이 메커니즘을 제공합니다. 틱 입력은 틱 시뮬레이션(FixedUpdateNetwork()
)에서 사용되며, 예측을 수행하는 클라이언트(HasInputAuthority == true
)와 서버 모두에서 일관된 결과를 생성하는 데 사용됩니다. 클라이언트의 히스토리 버퍼는 틱의 재시뮬레이션에 사용됩니다.
입력 구조체 정의
입력 구조체는 다음과 같은 제약 조건을 가집니다:
INetworkInput
을 상속해야 합니다.- 기본 타입과 구조체만 포함할 수 있습니다.
- 입력 구조체와 포함된 모든 구조체는 최상위 구조체여야 합니다. (즉, 클래스 내에 중첩될 수 없음)
- 부울(boolean) 값을 사용할 때는
bool
대신NetworkBool
을 사용해야 합니다. C#은 플랫폼 간에bool
크기를 일관되게 강제하지 않으므로,NetworkBool
을 사용하여 이를 단일 비트로 올바르게 직렬화합니다.
Fusion은 구조체의 타입을 지능적으로 매핑합니다. 이를 통해 게임의 서로 다른 플레이 모드나 다른 부분에 대해 서로 다른 구조체를 사용할 수 있습니다. 입력을 해제(unwrapping)할 때, Fusion은 올바른 타입의 사용 가능한 입력만 반환합니다.
C#
public struct MyInput : INetworkedInput {
public Vector3 aimDirection;
}
버튼
NetworkButtons
는 INetworkInput
구조체에서 버튼 입력을 저장하기 위한 편리한 래퍼를 제공하는 특수 타입입니다.
입력 구조체에 버튼을 추가하려면, 간단히 다음 단계를 수행하세요:
- 버튼에 대한 열거형(enum)을 생성합니다. (중요: 반드시 명시적으로 정의하고 0부터 시작해야 합니다.)
INetworkInput
에NetworkButtons
변수를 추가합니다.
C#
enum MyButtons {
Forward = 0,
Backward = 1,
Left = 2,
Right = 3,
}
public struct MyInput : INetworkInput {
public NetworkButtons buttons;
public Vector3 aimDirection;
}
NetworkButtons
변수에서 값을 할당하고 읽기 위한 API는 다음과 같습니다:
void Set(int button, bool state)
: 버튼의 열거형 값과 상태(눌림 = true, 눌리지 않음 = false)를 입력으로 받습니다.bool IsSet(int button)
: 버튼의 열거형 값을 입력으로 받아 해당 버튼의 boolean 상태를 반환합니다.
NetworkButtons
타입은 상태를 저장하지 않으므로 버튼의 이전 상태에 대한 메타데이터를 보유하지 않습니다. NetworkButtons
가 제공하는 다음 메서드를 사용하려면 버튼의 이전 상태를 추적해야 합니다. 이를 위해 각 플레이어의 이전 상태를 [Networked]
로 저장하는 간단한 방법을 사용할 수 있습니다.
C#
public class PlayerInputConsumerExample : NetworkBehaviour {
[Networked] public NetworkButtons ButtonsPrevious { get; set; }
// Full snippet in the GetInput() section further down.
}
이 방식으로 현재 버튼 상태와 이전 상태를 비교하여 버튼이 방금 눌렸거나 방금 해제되었는지를 평가할 수 있습니다.
NetworkButtons GetPressed(NetworkButtons previous)
: 방금 눌린 모든 버튼의 값을 반환합니다.NetworkButtons GetReleased(NetworkButtons previous)
: 방금 해제된 모든 버튼의 값을 반환합니다.(NetworkButtons, NetworkButtons) GetPressedOrReleased(NetworkButtons previous)
: 방금 눌렸거나 해제된 버튼 값을 튜플로 반환합니다.
중요: 버튼 값을 할당할 때 반드시 Input.GetKey()
를 사용해야 합니다. Input.GetKeyDown()
또는 Input.GetKeyUp()
은 Fusion 틱(Fusion ticks)과 동기화되지 않으므로 놓칠 가능성이 있으므로 사용하지 마세요.
입력 폴링
Fusion은 로컬 클라이언트를 폴링 하여 미리 정의된 입력 구조체를 채웁니다. Fusion Runner는 항상 단일 입력 구조체만 추적하므로, 예상치 못한 동작을 방지하기 위해 입력 폴링을 한곳에서 구현하는 것이 강력히 권장됩니다.
Fusion Runner는 INetworkRunnerCallbacks.OnInput()
메서드를 호출하여 입력을 폴링 합니다. OnInput()
의 구현은 선택한 데이터로 INetworkInput
을 상속하는 구조체를 채울 수 있습니다. 채워진 구조체는 제공된 NetworkInput
의 Set()
을 호출하여 Fusion에 다시 전달됩니다.
중요:
- 여러 폴링 사이트가 있을 경우, 마지막 입력 구조체 버전을 제외한 모든 버전이 덮어쓰이게 됩니다.
- 입력은 오직 로컬에서만 폴링 됩니다(모든 모드에서 동일).
SimulationBehaviour / NetworkBehaviour
SimulationBehaviour
또는 NetworkBehaviour
컴포넌트에서 OnInput()
을 사용하려면, INetworkRunnerCallbacks
인터페이스를 구현하고, NetworkRunner.AddCallbacks()
를 호출하여 로컬 Runner에 콜백을 등록해야 합니다.
C#
public class InputProvider : SimulationBehaviour, INetworkRunnerCallbacks {
public void OnEnable(){
if(Runner != null){
Runner.AddCallbacks( this );
}
}
public void OnInput(NetworkRunner runner, NetworkInput input) {
var myInput = new MyInput();
myInput.Buttons.Set(MyButtons.Forward, Input.GetKey(KeyCode.W));
myInput.Buttons.Set(MyButtons.Backward, Input.GetKey(KeyCode.S));
myInput.Buttons.Set(MyButtons.Left, Input.GetKey(KeyCode.A));
myInput.Buttons.Set(MyButtons.Right, Input.GetKey(KeyCode.D));
myInput.Buttons.Set(MyButtons.Jump, Input.GetKey(KeyCode.Space));
input.Set(myInput);
}
public void OnDisable(){
if(Runner != null){
Runner.RemoveCallbacks( this );
}
}
}
MonoBehaviour 및 순수 C# 스크립트
일반 C# 스크립트나 MonoBehaviour
에서 입력을 폴링 하려면 다음 단계를 따르세요:
INetworkRunnerCallbacks
와OnInput()
을 구현합니다.NetworkRunner
의AddCallbacks()
를 호출하여 스크립트를 등록합니다.
C#
public class InputProvider : Monobehaviour, INetworkRunnerCallbacks {
public void OnEnable(){
var myNetworkRunner = FindObjectOfType<NetworkRunner>();
myNetworkRunner.AddCallbacks( this );
}
public void OnInput(NetworkRunner runner, NetworkInput input) {
// Same as in the snippet for SimulationBehaviour and NetworkBehaviour.
}
public void OnDisable(){
var myNetworkRunner = FindObjectOfType<NetworkRunner>();
myNetworkRunner.RemoveCallbacks( this );
}
}
유니티 새로운 입력 시스템
새로운 유니티 입력 시스템을 사용하려면 과정은 동일하지만, 생성된 입력 액션에서 오는 입력을 수집해야 합니다.
입력 액션을 생성하고 원하는 버튼을 정의한 후, C# 클래스를 생성하고 코드에서 이를 인스턴스화합니다. 또한, PlayerInput
클래스에서 제공되는 이벤트를 사용할 수도 있지만, 이 경우 입력은 OnInput()
에서 사용하기 위해 로컬 캐시에 저장되어야 합니다.
목표는 새로운 입력 시스템에서 오는 버튼 상태를 OnInput()
에서 수집하는 것이며, 기존 입력 시스템이 아닙니다. 따라서 시스템 설정을 제외하면 나머지 과정은 기본적으로 동일합니다.
C#
public class InputProvider : SimulationBehaviour, INetworkRunnerCallbacks {
// creating a instance of the Input Action created
private PlayerActionMap _playerActionMap = new PlayerActionMap();
public void OnEnable(){
if(Runner != null){
// enabling the input map
_playerActionMap.Player.Enable();
Runner.AddCallbacks(this);
}
}
public void OnInput(NetworkRunner runner, NetworkInput input)
{
var myInput = new MyInput();
var playerActions = _playerActionMap.Player;
myInput.buttons.Set(MyButtons.Jump, playerActions.Jump.IsPressed());
input.Set(myInput);
}
public void OnDisable(){
if(Runner != null){
// disabling the input map
_playerActionMap.Player.Disable();
Runner.RemoveCallbacks( this );
}
}
}
낮은 틱 레이트에서 입력 폴링
낮은 틱 레이트에서 입력을 수집하려면 유니티의 Update 함수를 사용하여 구조체에 기록된 입력을 누적하고, 이후 이를 사용할 수 있도록 해야 합니다.
OnInput
에서 이 구조체를 읽고, input.Set()
호출을 통해 Fusion에 올바르게 전송한 뒤, 다음 틱 입력을 누적하기 위해 구조체를 초기화합니다.
C#
public class InputProvider : SimulationBehaviour, INetworkRunnerCallbacks {
// Local variable to store the input polled.
MyInput myInput = new MyInput();
public void OnEnable() {
if(Runner != null) {
Runner.AddCallbacks( this );
}
}
public void Update()
{
if (Input.GetMouseButtonDown(0)) {
myInput.Buttons.Set(MyButtons.Attack, true);
}
if (Input.GetKeyDown(KeyCode.Space)) {
myInput.Buttons.Set(MyButtons.Jump, true);
}
}
public void OnInput(NetworkRunner runner, NetworkInput input) {
input.Set(myInput);
// Reset the input struct to start with a clean slate
// when polling for the next tick
myInput = default;
}
}
UI와 함께 입력 폴링
UI를 사용한 입력 폴링은 위에서 설명한 로직과 동일합니다. UI를 통해 호출된 메서드에서 NetworkButton
을 설정하고, OnInput
에서 이를 읽고 초기화합니다.
입력 읽기
시뮬레이션은 이전에 폴링 된 입력을 기반으로 현재 네트워크 상태를 새로운 상태로 변경하기 위해 입력을 읽을 수 있습니다. Fusion은 입력 구조체를 네트워크를 통해 동기화하며, 입력 권한을 가진 클라이언트와 상태 권한을 가진 클라이언트에서 이를 시뮬레이션 중에 사용할 수 있도록 합니다.
입력을 폴링 하는 것과 달리, 입력을 읽는 것은 필요한 만큼 다양한 위치에서 수행할 수 있습니다.
참고: 플레이어 입력은 입력 권한과 상태 권한을 가진 클라이언트에만 사용할 수 있습니다. HostMode
와 ServerMode
에서는 플레이어 클라이언트와 호스트/서버가 이 권한을 가지며, SharedMode
에서는 동일한 클라이언트가 권한을 가집니다.
한 클라이언트의 입력을 다른 클라이언트에서 읽는 것은 불가능합니다. 따라서 입력에 의존하는 변경 사항은 반드시 [Networked]
상태로 저장되어 다른 클라이언트에서 복제될 수 있어야 합니다.
GetInput()
입력 구조체를 가져오려면, GetInput(out T input)
을 호출하세요. 이 호출은 입력 권한을 가진 객체를 제어하는 NetworkBehaviour
의 FixedUpdateNetwork()
에서 수행됩니다(예: 플레이어 이동을 제어하는 컴포넌트). GetInput()
호출은 이전에 OnInput()
에서 채워진 동일한 입력 구조체를 제공합니다.
GetInput()
호출이 실패(즉, false 반환)하는 경우는 다음과 같습니다:
- 클라이언트가 상태 권한이나 입력 권한을 가지지 않은 경우
- 요청된 입력 타입이 시뮬레이션에 존재하지 않는 경우
게임 모드별 정보:
HostMode
와ServerMode
에서는 주어진 틱(tick)의 입력이 플레이어와 호스트/서버 시뮬레이션에서만 사용할 수 있습니다. 입력은 플레이어 간 공유되지 않습니다.SharedMode
에서는OnInput()
와GetInput()
패턴을 유지하는 것이 좋지만, 중앙 권한이 없으므로 로컬 시뮬레이션에서만 로컬 플레이어의 입력에 접근할 수 있습니다. 입력은 플레이어 간 공유되지 않습니다.
C#
using Fusion;
using UnityEngine;
public class PlayerInputConsumerExample : NetworkBehaviour {
[Networked] public NetworkButtons ButtonsPrevious { get; set; }
public override void FixedUpdateNetwork() {
if (GetInput<MyInput>(out var input) == false) return;
// compute pressed/released state
var pressed = input.Buttons.GetPressed(ButtonsPrevious);
var released = input.Buttons.GetReleased(ButtonsPrevious);
// store latest input as 'previous' state we had
ButtonsPrevious = input.Buttons;
// movement (check for down)
var vector = default(Vector3);
if (input.Buttons.IsSet(MyButtons.Forward)) { vector.z += 1; }
if (input.Buttons.IsSet(MyButtons.Backward)) { vector.z -= 1; }
if (input.Buttons.IsSet(MyButtons.Left)) { vector.x -= 1; }
if (input.Buttons.IsSet(MyButtons.Right)) { vector.x += 1; }
DoMove(vector);
// jump (check for pressed)
if (pressed.IsSet(MyButtons.Jump)) {
DoJump();
}
}
void DoMove(Vector3 vector) {
// dummy method with no logic in it
}
void DoJump() {
// dummy method with no logic in it
}
}
Runner.TryGetInputForPlayer()
NetworkBehaviour
외부에서 입력을 읽으려면 NetworkRunner.TryGetInputForPlayer<T>(PlayerRef playerRef, out var input)
를 호출하면 됩니다. 이 메서드는 INetworkInput
타입 외에도 입력을 가져올 플레이어를 지정해야 합니다.
참고: GetInput()
과 동일한 제한 사항이 적용됩니다. 즉, 입력 권한(Input Authority)을 가진 클라이언트나 서버/호스트만 지정된 플레이어의 입력을 가져올 수 있습니다.
C#
var myNetworkRunner = FindObjectOfType<NetworkRunner>();
// Example for local player if script runs only on the client
if(myNetworkRunner.TryGetInputForPlayer<MyInput>(myNetworkRunner.LocalPlayer, out var input)){
// do logic
}
권한에 대한 주의점
완전한 시뮬레이션 권한을 보장하려면 입력 구조체를 채우는 OnInput()
에서만 입력 값을 수집해야 합니다. 입력을 기반으로 실행할 로직은 반드시 GetInput()
에서 완전히 처리해야 합니다.
예를 들어, 탄환 발사를 위한 작업은 다음과 같이 분리됩니다:
OnInput()
: 플레이어의 발사 버튼 값을 저장합니다.GetInput()
: 발사 버튼이 눌렸는지 확인하고 눌렸다면 탄환을 발사합니다.
피어당 여러 플레이어
종종 "couch", "split-screen", 또는 "local" 멀티플레이어라고 불립니다. 하나의 피어(예: 여러 컨트롤러가 연결된 게임 콘솔)에서 여러 플레이어가 입력을 제공하는 동시에 온라인 멀티플레이어 게임에 참여하는 것이 가능합니다. Fusion은 하나의 피어에 있는 모든 플레이어를 단일 PlayerRef
로 취급합니다(PlayerRef
는 네트워크 피어를 식별하며, 개별 플레이어를 구분하지 않습니다). 따라서 "플레이어"가 무엇인지, 그리고 각 플레이어가 어떤 입력을 제공하는지는 개발자가 정의해야 합니다.
이 사용 사례를 처리하는 한 가지 방법은 INetworkInput
구조체를 정의할 때, 각 플레이어에 대해 중첩된 INetworkStruct
를 포함하는 것입니다.
C#
public struct PlayerInputs : INetworkStruct
{
// All player specific inputs go here
public Vector2 dir;
}
public struct CombinedPlayerInputs : INetworkInput
{
// For this example we assume 4 players max on one peer
public PlayerInputs PlayerA;
public PlayerInputs PlayerB;
public PlayerInputs PlayerC;
public PlayerInputs PlayerD;
// Example indexer for easier access to nested player structs
public PlayerInputs this[int i]
{
get {
switch (i) {
case 0: return PlayerA;
case 1: return PlayerB;
case 2: return PlayerC;
case 3: return PlayerD;
default: return default;
}
}
set {
switch (i) {
case 0: PlayerA = value; return;
case 1: PlayerB = value; return;
case 2: PlayerC = value; return;
case 3: PlayerD = value; return;
default: return;
}
}
}
}
여러 플레이어를 위한 입력 수집:
C#
public class CouchCoopInput : MonoBehaviour, INetworkRunnerCallbacks
{
public void OnInput(NetworkRunner runner, NetworkInput input)
{
// For this example each player (4 total) has one Joystick.
var myInput = new CombinedPlayerInputs();
myInput[0] = new PlayerInputs() { dir = new Vector2( Input.GetAxis("Joy1_X"), Input.GetAxis("Joy1_Y")) };
myInput[1] = new PlayerInputs() { dir = new Vector2( Input.GetAxis("Joy2_X"), Input.GetAxis("Joy2_Y")) };
myInput[2] = new PlayerInputs() { dir = new Vector2( Input.GetAxis("Joy3_X"), Input.GetAxis("Joy3_Y")) };
myInput[3] = new PlayerInputs() { dir = new Vector2( Input.GetAxis("Joy4_X"), Input.GetAxis("Joy4_Y")) };
input.Set(myInput);
}
// (removed unused INetworkRunnerCallbacks)
}
시뮬레이션을 위한 입력 받기:
C#
public class CouchCoopController : NetworkBehaviour
{
// Player index 0-3, indicating which of the 4 players
// on the associated peer controls this object.
private int _playerIndex;
public override void FixedUpdateNetwork()
{
if (GetInput<CombinedPlayerInputs>(out var input))
{
var dir = input[_playerIndex].dir;
// Convert joystick direction into player heading
float heading = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg;
transform.rotation = Quaternion.Euler(0f, heading - 90, 0f);
}
}
}
Back to top