[Fusion] 102 - Setting Up A Scene

2023. 4. 12. 19:17학부 강의/SW종합설계

0. 참고 자료

 

 

Fusion 102 - Setting Up A Scene | Photon Engine

Fusion 102 will explain how to set up a basic networked scene. At the end of this section, the project will contain: consult the manual for an...

doc.photonengine.com

 

 


1. 퓨전 실행

 

퓨전을 실행하려면 Fusion NetworkRunner에서 StartGame 메서드를 호출해야 한다.

 

Scene에 컴포넌트가 있거나 코드에서 추가해야 한다.

  1. Unity에서 기본 신을 열고
  2. 새로운 빈 GameObject를 생성합니다. (이름은 자유지만 BasicSpawner라고 가정한다.)
  3. 그것에 새로운 스크립트 컴포넌트를 추가합니다.
  4. 스크립트 이름을 BasicSpawner로 지정합니다.
  5. 스크립트를 열고 BasicSpawner 클래스에 INetworkRunnerCallbacks 인터페이스를 implement하고 제공하는 메서드의 스텁을 추가한다.

 

csharpCopy code
public class BasicSpawner : MonoBehaviour, INetworkRunnerCallbacks
{
  public void OnPlayerJoined(NetworkRunner runner, PlayerRef player) { }
  public void OnPlayerLeft(NetworkRunner runner, PlayerRef player) { }
  public void OnInput(NetworkRunner runner, NetworkInput input) { }
  public void OnInputMissing(NetworkRunner runner, PlayerRef player, NetworkInput input) { }
  public void OnShutdown(NetworkRunner runner, ShutdownReason shutdownReason) { }
  public void OnConnectedToServer(NetworkRunner runner) { }
  public void OnDisconnectedFromServer(NetworkRunner runner) { }
  public void OnConnectRequest(NetworkRunner runner, NetworkRunnerCallbackArgs.ConnectRequest request, byte[] token) { }
  public void OnConnectFailed(NetworkRunner runner, NetAddress remoteAddress, NetConnectFailedReason reason) { }
  public void OnUserSimulationMessage(NetworkRunner runner, SimulationMessagePtr message) { }
  public void OnSessionListUpdated(NetworkRunner runner, List<SessionInfo> sessionList) { }
  public void OnCustomAuthenticationResponse(NetworkRunner runner, Dictionary<string, object> data) { }
  public void OnHostMigration(NetworkRunner runner, HostMigrationToken hostMigrationToken) { }
  public void OnReliableDataReceived(NetworkRunner runner, PlayerRef player, ArraySegment<byte> data) { }
  public void OnSceneLoadDone(NetworkRunner runner) { }
  public void OnSceneLoadStart(NetworkRunner runner) { }
}

NetworkRunner는 Fusion 네트워크의 핵심이다.

 

NetworkRunnerBasicSpawnerINetworkRunnerCallbacks를 구현한다는 것을 자동으로 감지하고BasicSpawner 클래스와 자동으로 상호 작용한다. (INetworkRunnerCallbacks을 implement한 BasicSpawnerNetworkRunner의 콜백된다.)

 

async void StartGame(GameMode mode)
{
  // Create the Fusion runner and let it know that we will be providing user input
  _runner = gameObject.AddComponent<NetworkRunner>();
  _runner.ProvideInput = true;

  // Start or join (depends on gamemode) a session with a specific name
  await _runner.StartGame(new StartGameArgs()
  {
    GameMode = mode,
    SessionName = "TestRoom",
    Scene = SceneManager.GetActiveScene().buildIndex,
    SceneManager = gameObject.AddComponent<NetworkSceneManagerDefault>()
  });}
  • _runner = gameObject.AddComponent<NetworkRunner>();
    : StartGame메서드는 먼저 Fusion NetworkRunner를 생성한다.
  • _runner.ProvideInput = true;
    : NetworkRunner에게 클라이언트가 입력을 제공할 것을 알린다.
  • await _runner.StartGame(new StartGameArgs(){ … }); : 세션을 만들기 위해 필요한 설정값 전달.

 

Fusion은 여러 가지 네트워크 토폴로지를 지원하지만, 이 소개에서는 호스트 모드에 집중한다. (호스트 모드는 하나의 네트워크 Peer가 서버와 클라이언트를 겸하며 네트워크 세션을 생성하고 나머지 Peer는 기존 세션에 참여하는 클라이언트입니다.)

 

이를 수용하기 위해 사용자가 게임을 호스팅 할지 아니면 기존 세션에 참여할지 선택할 수 있어야 한다.

 

간단한 UI를 만들기 위해 다음 예제에서는 Unity IMGUI를 사용합니다.

 

BasicSpawner.cs에 다음 메서드를 추가한다.

 

private NetworkRunner _runner;

private void OnGUI()
{
  if (_runner == null)
  {
    if (GUI.Button(new Rect(0,0,200,40), "Host"))
    {
        StartGame(GameMode.Host);
    }
    if (GUI.Button(new Rect(0,40,200,40), "Join"))
    {
        StartGame(GameMode.Client);
    }
  }
}

OnGUI() 함수는 이를 포함하는 스크립트가 활성화되면 Update()함수와 마찬가지로 프레임마다 호출됩니다.

 

 

IMGUI 기본 사항 - Unity 매뉴얼

이 섹션에서는 Unity의 IMGUI(Immediate Mode GUI) 시스템을 사용하여 Controls 를 스크립팅해야 하는 필요성에 대해 설명합니다.

docs.unity3d.com

 

아직까진 싱글 플레이와 다를 바 없다.

 

실행하면 사용자가 새 세션을 호스트하고 추가 인스턴스가 해당 세션에 참여할 수 있지만, 상호 작용이 없고 데이터가 전송되지 않기 때문이다.

 


2. 플레이어 아바타 생성

 

  1. PlayerPreFab이라는 빈 게임 오브젝트를 생성한다.
  2. NetworkObject 컴포넌트를 추가한다.

 

NetworkObject를 추가하면 네트워크 상에서 유일무이하게 인식된다.

 

NetworkCharacterControllerPrototype을 사용하면 빠르게 컨트롤러를 구현할 수 있다. (단 Unity의 CharacterController 컴포넌트가 필요하다. 보통 자동으로 추가되어 있다.)

 

네트워크 통신의 특성상 입력값 그대로 움직이면 부드럽고 일정한 움직임을 구현하기 힘들다.

 

부드러운 움직임을 위해선 다음과 같은 작업이 필요하다.

  1. Add a standard Unity Cube as a child object to the PlayerPrefab
  2. Rename it Body
  3. Remove the Collider
  4. Drag the Body game object to the Interpolation Target property of the NetworkCharacterControllerPrototype on the parent.

 

정상적으로 수행하면 다음과 같아진다.

 

 

이렇게 수정된 PlayerPrefab을 다시 프리팹으로 만들어라. 그리고 기존의 프리팹을 지워라.

(Save your project to have Fusion bake the new Network object, then drag it to the project folder to create the avatar prefab, and delete the object from the scene.)


3. Spawning The Avatar

 

게임이 hosted mode로 동작하기 때문에 모든 오브젝트를 spawn하는 권한은 host에게만 있다. 그렇기에 모든 플레이어 아바타는 반드시 세션에 참가할 때 호스트에 의해서 spawn되어야 한다.

 

편리하게도, INnetworkRunnerCallbacks 인터페이스의 OnPlayerJoined 메서드가 정확하게 경우에 호출된다.

 

비슷하게 플레이어가 접속을 종료하면 OnPlayerLeft가 호출된다.

 

비어있는 OnPlayerJoinedOnPlayerLeft stubs를 다음과 같은 코드로 채워라.

 

[SerializeField] private NetworkPrefabRef _playerPrefab;
private Dictionary<PlayerRef, NetworkObject> _spawnedCharacters = new Dictionary<PlayerRef, NetworkObject>();

public void OnPlayerJoined(NetworkRunner runner, PlayerRef player)
{
  if (runner.IsServer)
  {
    // Create a unique position for the player
    Vector3 spawnPosition = new Vector3((player.RawEncoded%runner.Config.Simulation.DefaultPlayers)*3,1,0);
    NetworkObject networkPlayerObject = runner.Spawn(_playerPrefab, spawnPosition, Quaternion.identity, player);
    // Keep track of the player avatars so we can remove it when they disconnect
    _spawnedCharacters.Add(player, networkPlayerObject);
  }
}

public void OnPlayerLeft(NetworkRunner runner, PlayerRef player)
{
  // Find and remove the players avatar
  if (_spawnedCharacters.TryGetValue(player, out NetworkObject networkObject))
  {
    runner.Despawn(networkObject);
    _spawnedCharacters.Remove(player);
  }
}

 

마지막 매개변수가 다르다는 점을 제외하면 runner.Spawn()은 유니티의 Instantiate()를 대신한다.

(This should all look quite familiar as it essentially replaces Unity's Instantiate() method with runner.Spawn() which takes a similar set of parameters, except for the last one. )

 

마지막 매개변수는 아바타에 입력을 할 권한을 가진 플레이어에 대한 참조다. 이것은 오브젝트를 소유하는 것과는 다르다. 이것에 대해서는 추후에 설명한다.

(The last parameter is a reference to the player that will be allowed to provide input for the avatar - it is important to note that this is not the same as "owning" the object. More on this in a second.)

 

아바타 프리팹을 BasicSpawner의 field에 추가하는 것을 잊지 마라.

(Don't forget to go back to the Unity Editor and drag and drop the created prefab avatar into the Player Prefab field of the BasicSpawner.)

 


4. Collecting Input

 

입력 권한을 가진고 있다고 해서 클라이언트가 곧바로 오브젝트에 네트워크로 공유되는 상태값을 변경할 수는 없다.

(Having Input Authority does not allow a client to directly modify the network state of the object. )

 

대신에, 호스트에게 값을 변경하게 요청할 수 있다.

(Instead, it may provide an input structure that the host will then interpret in order to update the network state.)

 

일부 클라이언트의 입력이 위의 과정을 거치지 않고 곧바로 적용되는 경우도 있지만, 여전히 host의 영향 아래에 있는 local prediction에 불과하다.

(The client may also apply the input locally to provide instant feedback to the user, but this is just a local prediction which may be overruled by the host.)

 

입력을 전달하기 위해서 모으려면, 데이터 구조가 반드시 정의되어있어야 한다. 아래의 구조를 따르는 NetworkInputData.cs라는 이름의 파일을 새로 만든다.

(Before input can be collected from the user, a data structure must be defined to hold the input. Create a new file named NetworkInputData.cs with the following struct in it:)

 

using Fusion;
using UnityEngine;

public struct NetworkInputData : INetworkInput
{
  public Vector3 direction;
}

단순한 예제로 이동 방향을 표시하는 Vector3를 사용했다. 이 예시는 사용 대역폭 비용이 적다.

(For simplicity this examples uses a vector to indicate desired movement direction, but know that there are less bandwidth expensive ways of doing this.)

 

예시는 이동 방향당 하나의 비트를 가지는 비트 필드를 가집니다.

(For example a bitfield with one bit per direction.)

 

Fusion은 오직 실제로 변화가 있는 입력 데이터만 전송한다. 그러니 지나치게 최적화에 집착하지 말라.

(Do note that Fusion will compress the input and only send data that actually changes, so don't go too crazy with premature optimization.)

 

Fusion의 OnInput callback의 의해서 조사될 때 클라이언트는 유저의 입력을 모아야 한다. 그래서 BasicSpawner.csOnInput() stub을 다음과 같이 수정한다.

(The client needs to collect input from the user when polled by Fusion in the OnInput callback, so go back to the BasicSpawner.cs and replace the OnInput() stub with the following:)

 

public void OnInput(NetworkRunner runner, NetworkInput input)
{
  var data = new NetworkInputData();

  if (Input.GetKey(KeyCode.W))
    data.direction += Vector3.forward;

  if (Input.GetKey(KeyCode.S))
    data.direction += Vector3.back;

  if (Input.GetKey(KeyCode.A))
    data.direction += Vector3.left;

  if (Input.GetKey(KeyCode.D))
    data.direction += Vector3.right;

  input.Set(data);
}

로컬 클라이언트에게서 표준 유니티 입력을 모아서 앞에서 정의한 데이터 구조에 저장한다.

(Again, this should look quite familiar - the handler is using standard Unity input to collect and store input from the local client in the structure that was defined previously.)

 

input.Set(data);는 앞서 완성한 입력 데이터 스트럭쳐를 Fusion에게 전달하고, Fusion은 호스트로 하여금 클라이언트가 입력 권한을 가진 오브젝트들에 대하여 적용하게 한다.

(The last line of this method passes the pre-filled input structure to Fusion which will then make it available to the host and any object this client have Input Authority over.)

 


5. Applying Input

 

마지막으로 수집한 입력 데이터를 플레이어 아바타에 적용시켜 보자.

 

  1. PlayerPrefab을 선택한다.
  2. Player라는 이름의 새로운 스크립트 컨포넌트를 추가한다.
  3. 새 스크립트를 열고 MonoBehaviourNetworkBehaviour로 변경한다.
  4. FixedUpdateNetwork()를 Implement(구현)한다. 이제 behaviour는 Fusion simulation loop의 일부로 포함된다.

 

using Fusion;

public class Player : NetworkBehaviour
{
  public override void FixedUpdateNetwork(){}
}

FixedUpdateNetwork는 매 simulation tick마다 호출된다. FixedUpdateNetwork는 랜더링 프레임 당 몇 번씩이나 실행될 수 있다.

(FixedUpdateNetwork gets called on every simulation tick. This can happen multiple times per rendering frame as Fusion applies an older confirmed network state and then re-simulates from that tick all the way up to the currently (predicted) local tick.)

 

매 tick마다 올바른 입력이 적용되려면 FixedUpdateNetwork을 활용해라.

(Input should be applied in FixedUpdateNetwork to make sure that the correct input is applied for each tick.)

 

Fusion은 틱마다 입력을 획득하기 위해서 GetInput()이라는 간단한 메서드를 제공한다.

(Fusion provides a simple method to acquire the input for the relevant tick, aptly named GetInput().)

 

입력을 획득하면, NetworkCharacterControllerPrototype은 아바타이 transform에 실제 움직임을 적용하기 위해서 호출된다.

(Once the input has been acquired, the NetworkCharacterControllerPrototype is called to apply the actual movement to the avatar transform.)

 

The complete Player class looks like this:

 

using Fusion;

public class Player : NetworkBehaviour
{
  private NetworkCharacterControllerPrototype _cc;

  private void Awake()
  {
    _cc = GetComponent<NetworkCharacterControllerPrototype>();
  }

  public override void FixedUpdateNetwork()
  {
    if (GetInput(out NetworkInputData data))
    {
      data.direction.Normalize();
      _cc.Move(5*data.direction*Runner.DeltaTime);
    }
  }
}

Note the provided input is normalized to prevent cheating.

 


6. 결론

 

네트워크에 연결되면 플레이어 아바타를 생성한다.

 

이를 위해서 BasicSpawner.cs, NetworkInputData.cs, Player.cs 코드 및 avatar Prefab을 생성한다.

 

 


가. BasicSpawner.cs

 

신을 열고 새로운 빈 GameObject를 생성합니다. (이름은 자유지만 BasicSpawner라고 가정한다.)

 

거기에 이름이 BasicSpawner인 새로운 스크립트 컴포넌트를 추가합니다.

 

//BasicSpawner.cs
using System;
using System.Collections.Generic;
using Fusion;
using Fusion.Sockets;
using UnityEngine;
using UnityEngine.SceneManagement;

namespace Fusion102
{
    public class BasicSpawner : MonoBehaviour, INetworkRunnerCallbacks 
    {
        private NetworkRunner _runner;

        private void OnGUI()
        {
            if (_runner == null)
            {
                if (GUI.Button(new Rect(0,0,200,40), "Host"))
                {
                    StartGame(GameMode.Host);
                }
                if (GUI.Button(new Rect(0,40,200,40), "Join"))
                {
                    StartGame(GameMode.Client);
                }
            }
        }

        async void StartGame(GameMode mode)
        {
            // Create the Fusion runner and let it know that we will be providing user input
            _runner = gameObject.AddComponent<NetworkRunner>();
            _runner.ProvideInput = true;

            // Start or join (depends on gamemode) a session with a specific name
            await _runner.StartGame(new StartGameArgs()
            {
                GameMode = mode, 
                SessionName = "TestRoom", 
                Scene = SceneManager.GetActiveScene().buildIndex,
                SceneManager = gameObject.AddComponent<NetworkSceneManagerDefault>()
            });
        }

        [SerializeField] private NetworkPrefabRef _playerPrefab; // Character to spawn for a joining player
        private Dictionary<PlayerRef, NetworkObject> _spawnedCharacters = new Dictionary<PlayerRef, NetworkObject>();

        public void OnPlayerJoined(NetworkRunner runner, PlayerRef player)
        {
            if (runner.IsServer)
            {
                Vector3 spawnPosition = new Vector3((player.RawEncoded%runner.Config.Simulation.DefaultPlayers)*3,1,0);
                NetworkObject networkPlayerObject = runner.Spawn(_playerPrefab, spawnPosition, Quaternion.identity, player);
                _spawnedCharacters.Add(player, networkPlayerObject);
            }
        }

        public void OnPlayerLeft(NetworkRunner runner, PlayerRef player)
        {
            if (_spawnedCharacters.TryGetValue(player, out NetworkObject networkObject))
            {
                runner.Despawn(networkObject);
                _spawnedCharacters.Remove(player);
            }
        }

        public void OnInput(NetworkRunner runner, NetworkInput input)
        {
            var data = new NetworkInputData();

            if (Input.GetKey(KeyCode.W))
                data.direction += Vector3.forward;

            if (Input.GetKey(KeyCode.S))
                data.direction += Vector3.back;

            if (Input.GetKey(KeyCode.A))
                data.direction += Vector3.left;

            if (Input.GetKey(KeyCode.D))
                data.direction += Vector3.right;

            input.Set(data);
        }

        public void OnInputMissing(NetworkRunner runner, PlayerRef player, NetworkInput input) { }
        public void OnShutdown(NetworkRunner runner, ShutdownReason shutdownReason) { }
        public void OnConnectedToServer(NetworkRunner runner) { }
        public void OnDisconnectedFromServer(NetworkRunner runner) { }
        public void OnConnectRequest(NetworkRunner runner, NetworkRunnerCallbackArgs.ConnectRequest request, byte[] token) { }
        public void OnConnectFailed(NetworkRunner runner, NetAddress remoteAddress, NetConnectFailedReason reason) { }
        public void OnUserSimulationMessage(NetworkRunner runner, SimulationMessagePtr message) { }
        public void OnSessionListUpdated(NetworkRunner runner, List<SessionInfo> sessionList) { }
        public void OnCustomAuthenticationResponse(NetworkRunner runner, Dictionary<string, object> data) { }
        public void OnHostMigration(NetworkRunner runner, HostMigrationToken hostMigrationToken) { }
        public void OnReliableDataReceived(NetworkRunner runner, PlayerRef player, ArraySegment<byte> data) { }
        public void OnSceneLoadDone(NetworkRunner runner) { }
        public void OnSceneLoadStart(NetworkRunner runner) { }
    }
}

 

BasicSpawnerOnGUI()에서 방을 생성할지 참가할지 선택하고 StartGame()를 호출한다.

 

StartGame()에서는 NetworkRunner 게임오브젝트를 추가한다.

 

NetworkRunner는 전달받은 정보를 바탕으로 방을 생성한다.

 

NetworkRunnerBasicSpawnerINetworkRunnerCallbacks를 구현한다는 것을 자동으로 감지하고 BasicSpawner 클래스와 자동으로 상호 작용한다. (INetworkRunnerCallbacks을 implement한 BasicSpawnerNetworkRunner의 콜백된다.)

 

BasicSpawner에서 INetworkRunnerCallbacksOnPlayerJoined, OnPlayerLeft, OnPlayerLeft을 implement 한다.

 

  • OnPlayerJoined : 플레이어 입장 시 NetworkRunner가 호출. _spawnedCharacters 딕셔너리에 플레이어 추가. avatar prefab을 spawn한다. 이 작업은 오직 host만 수행한다.
  • OnPlayerLeft : 플레이어 퇴장 시 NetworkRunner가 호출. _spawnedCharacters 딕셔너리에 플레이어 삭제. avatar prefab을 despawn한다.
  • OnInput : 플레이어가 입력 시 NetworkRunner가 호출. 입력값을 수집해서 NetworkInputData.cs에 저장한 다음 host에게 전달한다. 호스트는 전달받은 요청을 수행하고 작업이 완료되면 Client들은 변화를 동기화한다.

 

스크립트를 열고 BasicSpawner 클래스에 INetworkRunnerCallbacks인터페이스와 필요한 모든 메서드의 스텁을 추가한다.

 


나. NetworkInputData.cs

 

//NetworkInputData.cs
using Fusion;
using UnityEngine;

namespace Fusion102
{
    public struct NetworkInputData : INetworkInput
    {
        public Vector3 direction;
    }
}

 

  • OnInput()에서 수집한 입력값을 저장하는 파일이다.

 


다. Player.cs

//Player.cs
using Fusion;

namespace Fusion102
{
    public class Player : NetworkBehaviour
    {
        private NetworkCharacterControllerPrototype _cc;

        private void Awake()
        {
            _cc = GetComponent<NetworkCharacterControllerPrototype>();
        }

        public override void FixedUpdateNetwork()
        {
            if (GetInput(out NetworkInputData data))
            {
                data.direction.Normalize();
                _cc.Move(5*data.direction*Runner.DeltaTime);
            }
        }
    }
}

 

  • avatar prefab이 보유한 컴포넌트 중 하나다.
  • FixedUpdateNetwork()는 매 simulation tick마다 실행되어 클라이언트의 요청을 수행한다.

 


'학부 강의 > SW종합설계' 카테고리의 다른 글

[SW종합설계] 과제 신청서 (보호)  (0) 2023.05.10
[SW종합설계] 팀 Github  (0) 2023.05.10
[Fusion] 설치  (0) 2023.04.12
[Unity] 오브젝트 라이프사이클  (0) 2023.04.04
[Unity] 게임의 구조와 C#  (0) 2023.04.04