튜토리얼: 플레이어 캐릭터 동기화하기

룰을 작성하지 않고, 각 플레이어 캐릭터의 위치와 RPC를 Unity에서 동기화하기까지의 절차

이 튜토리얼에서는 룸에 참가한 플레이어가 각자 자신의 캐릭터를 움직이고, 다른 플레이어의 화면에도 똑같이 표시되는 작은 룸을 만듭니다.

  • 접속하면 자신의 캐릭터가 1체 생성됩니다.
  • 방향키(WASD)로 자신의 캐릭터를 움직이면, 다른 플레이어의 화면에서도 부드럽게 움직입니다.
  • 스페이스 키를 누르면 전원의 화면에서 그 캐릭터가 순간적으로 커집니다(RPC).

서버 타입은 sync를 사용합니다. 룸의 룰을 작성할 필요는 없습니다. 서버 타입의 차이는 구성과 선택 방법을 참조해 주세요.

작업 흐름

순서작업 장소만드는 것
1. 네임스페이스를 설정한다GS2Realtime 네임스페이스 설정
2. 캐릭터의 Prefab을 만든다Unity Editor이동과 RPC 스크립트를 붙인 Prefab
3. 룸에 접속한다Unity EditorPrefab Catalog와, 접속하여 자신의 캐릭터를 생성하는 씬
4. 동작을 확인한다Unity(클라이언트 2개)—
5. 공개에 대비해 제한한다GS2오브젝트 카탈로그

GS2 SDK for Unity를 도입한 Unity 프로젝트(Unity 2021.3 이상)에서 작업합니다. 동기화 컴포넌트는 SDK에 포함되어 있으므로 추가 패키지는 필요하지 않습니다.

1. 네임스페이스를 설정한다

GS2-Realtime 네임스페이스에서 서버 타입을 지정합니다.

설정 내용설정 항목지정하는 값
서버 타입serverTypesync

룸 준비(GS2-Matchmaking과 RoomWant)는 다른 서버 타입과 같습니다.

2. 캐릭터의 Prefab을 만든다

스크립트 준비하기

자신의 캐릭터를 움직이는 스크립트입니다. Transform을 움직이기만 하면 NetworkTransform이 다른 참가자에게 위치를 전송합니다. 스페이스 키로 전원에게 RPC를 전송합니다.

using Gs2.Realtime.Sync;
using UnityEngine;

// NetworkEntity의 Controlled Behaviours에 등록하여, 자신의 캐릭터에서만 동작시킨다
public sealed class PlayerMover : MonoBehaviour
{
    [SerializeField] float speed = 3f;

    void Update()
    {
        var move = new Vector3(Input.GetAxisRaw("Horizontal"), 0, Input.GetAxisRaw("Vertical"));
        // Transform을 움직이기만 하면 NetworkTransform이 다른 참가자에게 전송한다
        transform.position += move.normalized * (speed * Time.deltaTime);

        if (Input.GetKeyDown(KeyCode.Space))
            GetComponent<NetworkEntity>().Rpc(nameof(PlayerEmote.Wave), RpcTarget.All);
    }
}

RPC로 호출되는 메서드에는 [NetworkRpc]를 붙입니다. IEnumerator를 반환하면 코루틴으로 실행됩니다.

using System.Collections;
using Gs2.Realtime.Sync;
using UnityEngine;

public sealed class PlayerEmote : MonoBehaviour
{
    // 전원의 화면에서 그 캐릭터가 순간적으로 커진다
    [NetworkRpc]
    public IEnumerator Wave(NetworkRpcInfo info)
    {
        Debug.Log("Wave from connection " + info.Sender);
        transform.localScale = Vector3.one * 1.5f;
        yield return new WaitForSeconds(0.3f);
        transform.localScale = Vector3.one;
    }
}

info.Sender는 RPC를 보낸 플레이어의 접속 ID입니다. 서버가 설정하므로 보낸 쪽이 위조할 수 없습니다.

Prefab 만들기

  1. 캐릭터의 외형이 될 Prefab(Capsule 등)을 만들고, 루트에 다음 컴포넌트를 추가합니다.

    컴포넌트설정
    NetworkEntitySchema는 비워 둔다(위치만 동기화하므로)
    NetworkTransformPosition Axes는 All, Rotation Axes는 None
    PlayerMover위의 스크립트
    PlayerEmote위의 스크립트
  2. NetworkEntity > Controlled Behaviours에 PlayerMover를 등록합니다. PlayerMover는 자신이 소유한 캐릭터에서만 활성화됩니다. 등록하지 않으면 자신의 화면에 있는 다른 플레이어의 캐릭터에도 자신의 키 입력이 적용되어 버립니다.

위치와 회전은 SDK에 표준 필드가 있으므로, 동기화 스키마 에셋이 없어도 동기화할 수 있습니다. HP 같은 독자적인 값을 동기화할 때는 독자적인 값 표시하기를 참조해 주세요.

3. 룸에 접속한다

NetworkWorld 배치하기

  1. Project 창의 Create > GS2 > Realtime > Prefab Catalog에서 카탈로그를 만듭니다.
  2. 카탈로그의 Prefabs에 Id 1과 순서 2의 Prefab을 등록합니다. Prefab ID는 오브젝트의 종류를 나타내는 번호이며, 다른 클라이언트는 이 번호로 표시할 Prefab을 선택합니다.
  3. 씬에 빈 GameObject를 만들고 NetworkWorld를 추가한 후, Prefab Catalog에 카탈로그를 지정합니다.
  4. 지면이 될 Plane과 위에서 내려다보는 Camera를 배치합니다.

접속 스크립트 추가하기

NetworkWorld와 같은 GameObject에 NetworkSession과 다음 스크립트를 추가합니다. 룸의 접속 정보(IP 주소·포트·암호화 키)와 액세스 토큰은 게임 서버 접속 정보 취득의 방법으로 취득하여 전달합니다.

using Gs2.Realtime.Sync;
using UnityEngine;

[RequireComponent(typeof(NetworkWorld), typeof(NetworkSession))]
public sealed class PlayerRoomConnection : MonoBehaviour
{
    const uint PlayerPrefab = 1; // Prefab Catalog에 등록한 Id

    NetworkWorld world;
    NetworkSession session;

    // 이벤트는 한 번만 등록합니다(접속할 때마다 등록하면 재접속 시 핸들러가 중복됩니다)
    void Awake()
    {
        world = GetComponent<NetworkWorld>();
        session = GetComponent<NetworkSession>();
        // 룸의 상태를 반영하면 자신의 캐릭터를 생성한다.
        // 접속이 끊기면 캐릭터는 삭제되므로, 재접속할 때마다 다시 만든다
        session.Ready += () => world.Instantiate(
            PlayerPrefab,
            new Vector3(Random.Range(-3f, 3f), 0, Random.Range(-3f, 3f)),
            Quaternion.identity);
        session.Disconnected += reason => Debug.Log("접속이 종료되었습니다: " + reason);
    }

    public void Connect(string accessToken, string ipAddress, int port, string encryptionKey)
    {
        session.Connect(accessToken, ipAddress, port, encryptionKey);
    }
}

핵심은 다음 3가지입니다.

  • 자신의 캐릭터는 world.Instantiate로 만듭니다. Unity의 Instantiate로 만든 GameObject는 다른 참가자에게 공유되지 않습니다. world.Instantiate의 반환값은 그 자리에서 자신의 소유(IsMine)로 움직일 수 있습니다. 룸이 생성을 확정할 때까지의 이동이나 RPC는 보류되며, 확정 후에 전송됩니다.
  • 다른 플레이어의 캐릭터는 NetworkWorld가 만듭니다. 룸에서 도착한 생성을 보고 카탈로그의 Prefab으로부터 GameObject를 만들고, 수신한 위치로 보간하여 움직입니다.
  • 접속하면 그 시점의 상태가 도착합니다. 나중에 참가한 플레이어에게도 이미 참가한 플레이어의 캐릭터가 그 시점의 위치에 표시됩니다. 반영이 끝나면 Ready가 호출됩니다.

4. 동작을 확인한다

같은 룸에 2개의 클라이언트에서 접속합니다. 같은 userId로 접속하면 먼저 접속한 쪽이 대체되므로, 서로 다른 userId의 액세스 토큰을 사용해 주세요.

  1. 양쪽 화면에 캐릭터 2체가 표시되는 것을 확인합니다.
  2. 한쪽에서 캐릭터를 움직여, 다른 쪽 화면에서도 같은 캐릭터가 부드럽게 움직이는 것을 확인합니다. 다른 쪽의 키 입력으로는 자신의 캐릭터만 움직입니다.
  3. 한쪽에서 스페이스 키를 눌러, 양쪽 화면에서 그 캐릭터가 순간적으로 커지는 것을 확인합니다.
  4. 한쪽의 접속을 끊고, 다른 쪽 화면에서 그 캐릭터가 사라지는 것을 확인합니다. 다시 접속하면 새로운 캐릭터가 생성됩니다.

5. 공개에 대비해 제한한다

지금까지의 설정에서는 룸이 플레이어가 생성하는 Prefab이나 호출하는 RPC를 검사하지 않습니다. 개조한 클라이언트로부터의 생성이나 RPC를 제한하려면 네임스페이스 설정의 syncSetting.objectCatalog에 오브젝트 카탈로그를 설정합니다.

  1. Prefab Catalog 에셋을 오른쪽 클릭하고 Copy Object Catalog JSON을 선택합니다. 등록한 Prefab의 동기화할 필드와 RPC의 ID가 JSON으로 클립보드에 복사됩니다.
  2. 그 JSON을 syncSetting.objectCatalog에 설정합니다.

설정하면 등록한 Prefab만 생성할 수 있고, methods에 등록한 RPC만 소유자가 호출할 수 있게 됩니다. RPC의 ID는 메서드 이름으로 정해지므로, 메서드 이름을 변경했다면 JSON을 다시 만들어 주세요.

다음에 읽을 내용