[Unity3D] #6 - Fusion 2-1 | 기본 설정

qweasfjbv·2024년 7월 20일

Unity3D

목록 보기
6/7

개요


만들던 게임을 Fusion 2를 사용해서 멀티플레이가 가능한 게임으로 만들어 보겠습니다.
Fusion 2 가 아예 처음이기 때문에 photon 사이트의 튜토리얼을 따라하면서 코드를 수정해보겠습니다.

구현



우선, Setup Networking in the Scene 으로 Prototype Network StartPrototype Runner 를 추가해줍니다.


그 후에, 멀티플레이에 필요한 오브젝트들에 NetworkObject, NetworkTransform, CharacterController 를 적절히 부착해줍니다.

BoxController.cs

public class BoxController : NetworkBehaviour

NetworkBehaviour 를 상속받습니다.

내부적으로, NetworkBehavior -> SimulationBehavior -> Behavior -> MonoBehavior 를 상속받기 때문에 일반적으로 에러는 일어나지 않습니다.

그 후에, Update 을 수정해야 합니다.

    private KeyCode _pressedKeyCode = KeyCode.None;
    private void Update()
    {
        foreach (KeyCode key in arrowKeys)
        {
            if (Input.GetKey(key))
            {
                _pressedKeyCode = key; return;
            }
        }
    }

    private bool isSynced = false;
    private bool isScaleSynced = false;
    public override void FixedUpdateNetwork()
    {

        if (HasStateAuthority == false) return;

        if (!isScaleSynced)
        {
            transform.localScale = Vector3.one;
            isScaleSynced = true;
        }

        if (!isJumping)
        {
            if (!isSynced)
            {
                transform.position = jumpTarget;
                transform.rotation = targetRotation;
                isSynced = true;
            }

            if (_pressedKeyCode != KeyCode.None)
            {
                isSynced = false;
                GetKeyInput(_pressedKeyCode);
            }
        }
        
        _pressedKeyCode = KeyCode.None;

    }

Update에서 바로 함수를 호출하는 것이 아니라, bool 변수를 조작해서 FixedUpdateNetwork 함수 내부에서 호출하도록 합니다.
특히, transformposition, rotation, scale 은 코루틴 내에서 변경해도 동기화가 정확히 되지 않을 수 있습니다. ( 참고자료 )

따라서, transformbool 변수를 통해서 FixedUpdateNetwork() 함수 내에서 동기화할 수 있도록 합니다.


GridInfo, MapGrid


[Serializable]
public struct NetworkGridInfo : INetworkStruct{

    [Networked] public Vector2Int Pos { get; set; }
    [Networked] public int Height { get; set; }
    [Networked] public GridState State { get; set; }
    [Networked] public ColorSet colorset { get; set; }


    public NetworkGridInfo(Vector2Int pos, int height, int colorIdx, GridState state)
    {
        this.Pos = pos;
        this.Height = height;

        if (state < 0) this.State = 0;
        else this.State = state;


        colorset = new ColorSet(ColorConstants.COLORARR[colorIdx]);
    }

}


public class MapGrid : NetworkBehaviour, IAfterSpawned
{
    [SerializeField, Networked]
    public NetworkGridInfo NetworkedGridInfo { get; set; }


    // Process Grid According to GridState
    public void InitMapGrid(NetworkGridInfo info)
    {
        NetworkedGridInfo = new NetworkGridInfo(info.Pos, info.Height, info.colorset.GetColorIdx(), info.State);

        RPC_UpdateGridVisuals();
    }

    [Rpc(RpcSources.All, RpcTargets.All)]
    public void RPC_UpdateGridVisuals()
    {
        transform.localScale = Vector3.one * Constant.GRID_SIZE;
        transform.position = new Vector3(transform.position.x, NetworkedGridInfo.Height * Constant.GRID_SIZE - transform.localScale.y / 2 - Constant.BOX_SIZE / 2, transform.position.z);

        if (GameManagerEx.Instance.IsColorBlind)
        {
            GetComponent<Renderer>().material.mainTexture = Managers.Resource.GetDiceTexture(NetworkedGridInfo.colorset.GetColorIdx());
        }
        else
        {
            GetComponent<Renderer>().material.color = NetworkedGridInfo.colorset.GetColor();
        }
    }
    
    public void SetGridInfo(NetworkGridInfo info)
    {
        NetworkedGridInfo = info;

        RPC_UpdateGridVisuals();
    }
    
    // ...
    
}

위에는 각 그리드의 정보를 저장하는 NetworkGridInfo 입니다.
Custom DataType을 네트워크상에 정보를 동기화시키기 위해서는 INetworkStruct를 상속해야 합니다.

아래에는 각 그리드 객체를 조작하는 MapGrid 입니다.
그리드 정보를 받은 이후나 정보가 변경된 이후에, RPC 호출을 사용하여 모든 클라이언트가 변경사항을 볼 수 있도록 했습니다.


MapGenerator


// ...

    [UnitySerializeField, Networked, Capacity(50)]
    public NetworkDictionary<Vector2Int, MapGrid> NetworkedMapGrids => default;

    [Networked]
    public int NetworkedCurMapWidth { get; set; }

// ...
	
    // 각 클라이언트가 처음 접속했을 때 정보 시각화
    public override void Spawned()
    {
        UpdateMapGrids();
    }

    public void UpdateMapGrids()
    {
        for (int i = 0; i < NetworkedCurMapWidth; i++)
        {
            for (int j = 0; j < NetworkedCurMapWidth; j++)
            {
                var tmpGrid = NetworkedMapGrids[new Vector2Int(i, j)];
                if (tmpGrid != null)
                {
                    tmpGrid.RPC_UpdateGridVisuals();
                }
            }
        }
    }

// ...

    public void GenerateMap(GameType type,  int n, NetworkRunner runner)
    {
		// ...
        
        NetworkGridInfo grid;

        NetworkedMapGrids.Clear();
        for (int i = 0; i < mapArrs.Count; i++)
        {
            grid = mapArrs[i];
            var sp = runner.Spawn(gridPrefab, new Vector3(grid.Pos.x, 0, grid.Pos.y) * Constant.GRID_SIZE, Quaternion.identity,
               inputAuthority: null,
               (runner, NO) => NO.GetComponent<MapGrid>().InitMapGrid(grid));

            NetworkedMapGrids.Set(new Vector2Int(grid.Pos.y, grid.Pos.x), sp.GetComponent<MapGrid>());

        }

        NetworkedCurMapWidth = mapResource.width;
        return;
    }

맨 위의 변수 부분에 모든 클라이언트가 공유해야할 정보 앞에 [Networked] 어트리뷰트를 통해 동기화 합니다.

UpdateMapGrids 는 각 클라이언트가 처음 접속했을 때 호출되게 함으로써, SharedModeMasterClient 가 조작한 정보를 들어오자마자 볼 수 있도록 하기 위함입니다.

GenerateMap 은 이전에 Instantiate으로 생성하던 프리팹들을 runner.Spawn 함수를 통해 생성하도록 했습니다. 반대로, Destroy 로 삭제하던 오브젝트들도 Despawn 을 통해 없애도록 했습니다.


PlayerSpawner


public class PlayerSpawner : SimulationBehaviour, IPlayerJoined{


    public void PlayerJoined(PlayerRef player)
    {
        if (player == Runner.LocalPlayer)
            MapRestart();
    }

    public GameObject PlayerPrefab;

    public NetworkObject currentPlayer;
    public int idx;

    public void MapRestart()
    {
        GameManagerEx.Instance.spawner = this;
        
        if (currentPlayer != null)
            Runner.Despawn(currentPlayer);

        if (GameManagerEx.Instance.CurGameType== GameType.MULTI && Runner.IsSharedModeMasterClient || GameManagerEx.Instance.CurGameType!= GameType.MULTI)
            CreateMap();

        MapGenerator.Instance.SetStageName(GameManagerEx.Instance.CurGameType, GameManagerEx.Instance.CurLv);
        CameraController.Instance.SetQuaterView(Managers.Resource.GetCamPos(GameManagerEx.Instance.CurGameType, GameManagerEx.Instance.CurLv));
        PlayerSpawn();

    }

    public void PlayerSpawn()
    {
        currentPlayer = Runner.Spawn(PlayerPrefab, new Vector3(0, 0, 0), Quaternion.identity);
        currentPlayer.GetComponent<BoxController>().SetBoxController(new Vector2Int(0, 0), 0);
    }

    public void CreateMap()
    {
        MapGenerator.Instance.GenerateMap(GameManagerEx.Instance.CurGameType, GameManagerEx.Instance.CurLv, Runner);
    }

}

PlayerJoined 콜백 함수를 통해 플레이어가 들어오면 맵을 만들거나, 맵이 만들어져 있다면 플레이어만 스폰하는 PlayerSpawner 클래스입니다.

여기까지 수정하고 실행해보겠습니다.

마무리


Photon 을 처음 사용해봐서 그런지, 오류가 많이 발생해서 구현하는데 시간이 꽤 걸렸습니다.
발생한 오류에 대해서는 다른 글로 정리해서 올리도록 하겠습니다.


참고자료


https://doc.photonengine.com/ko-kr/fusion/current/tutorials/shared-mode-basics/overview
https://doc.photonengine.com/fusion/current/concepts-and-patterns/networked-controller-code

0개의 댓글