Strix 액션 게임 샘플

Strix Unity SDK를 다운로드하고 초기 Strix 설정을 완료했다면, 문서 나머지 부분을 읽어 SDK와 그 이용법에 관한 사항을 더 확인해도 좋습니다.

Strix를 이용하는 기존 프로젝트에 관한 기본 설명으로 바로 들어가고 싶다면 여기서 시작하면 됩니다. 이 페이지에는 StrixActionGameSample이라고 하는 샘플 게임을 이용한 튜토리얼이 있습니다. 이것은 Strix Unity SDK의 zip 파일에 들어 있습니다.

이 튜토리얼을 보려면 갖고 계신 Unity를 실행해야 합니다.

샘플 폴더

이 샘플은 StrixUnitySDK > samples 폴더에 들어 있습니다. Unity Editor로 StrixActionGameSample 유니티 프로젝트를 로드할 수 있습니다.

참고

Strix 플러그인은 이미 StrixActionGameSample로 가져온 상태이므로, SDK 가져오기 단계를 따로 할 필요가 없습니다.

Unity 이용자라면 각자 원하는 방법으로 StrixActionGameSample 프로젝트를 준비해 두면 됩니다. Unity 프로젝트를 열 때 도움이 필요하다면 이 페이지를 참조해도 됩니다.

샘플 설명

SampleScene을 열어서 튜토리얼을 시작해 보겠습니다.

View of the sample game

이 샘플에 들어 있는 게임은 플레이어 캐릭터, NPC, 기본 환경으로 된 간단한 3인칭 슈팅 게임입니다. 설명을 위해 연결 로직은 StrixConnectUI 프리팹에서 처리합니다.

참고

SampleScene을 찾기 어렵다면 부록을 참고하기 바랍니다.

서버 정보

Properties for StrixConnectGUI

StrixConnectUI > StrixConnectPanel 개체를 선택하고 StrixConnectGUI 스크립트를 찾습니다.

Host, Port, Application Id 속성이 보입니다. 이 속성들은 스크립트가 서버 연결을 위해 이용하는 서버 정보입니다. Strix Cloud 설정에 나와 있는 순서에 따라 서버를 설정합니다. Strix Cloud에서 관련 값을 가져와서 이 값을 구성요소의 속성으로 설정합니다.

참고

Host는 Strix Cloud에서 마스터 호스트 이름으로 불리며, 애플리케이션의 서버 탭에서 이용할 수 있습니다. (0123.game.strixcloud.net과 같은 인터넷 도메인 이름입니다.)

Port는 Strix Cloud를 이용할 때는 항상 9122입니다.

Application ID는 Strix Cloud 애플리케이션 대시보드의 정보 탭에서 이용할 수 있습니다.

최초 플레이

서버 정보가 추가되면 샘플을 시험할 수 있습니다. 플레이를 누르고 텍스트 상자에 플레이어 이름을 추가하면 됩니다. 이어서 [연결] 버튼을 눌러 서버에 연결합니다. 이제는 돌아다니며 NPC와 게임에 들어오는 다른 플레이어를 쏘면 됩니다.

연결

게임 시험을 마치면 빠져 나옵니다.

서버 연결은 단순명료합니다. 원하는 에디터에서 StrixConnectGUI.cs 스크립트를 엽니다.

// 설명을 위해 몇 줄 생략

public class StrixConnectGUI : MonoBehaviour {
    public string host = "127.0.0.1";
    public int port = 9122;
    public string applicationId = "00000000-0000-0000-0000-000000000000";
    public Level logLevel = Level.INFO;
    public InputField playerNameInputField;
    public Text statusText;
    public Button connectButton;
    public UnityEvent OnConnect;

    public void Connect() {
        LogManager.Instance.Filter = logLevel;

        StrixNetwork.instance.applicationId = applicationId;
        StrixNetwork.instance.playerName = playerNameInputField.text;
        StrixNetwork.instance.ConnectMasterServer(host, port, OnConnectCallback, OnConnectFailedCallback);

        statusText.text = "Connecting MasterServer " + host + ":" + port;

        connectButton.interactable = false;
    }

    private void OnConnectCallback(StrixNetworkConnectEventArgs args)
    {
        statusText.text = "Connection established";

        OnConnect.Invoke();

        gameObject.SetActive(false);
    }

    private void OnConnectFailedCallback(StrixNetworkConnectFailedEventArgs args) {
        string error = "";

        if (args.cause != null) {
            error = args.cause.Message;
        }

        statusText.text = "Connect " + host + ":" + port + " failed. " + error;
        connectButton.interactable = true;
    }
}

스크립트는 먼저 MonoBehaviour를 상속하여 사용자의 속성을 몇 가지 정의합니다.

Connect 메서드에서는 StrixNetwork에서 applicationIdplayerName 값을 설정합니다. 이어서 hostport 인수가 있는 ConnectMasterServer 메서드를 호출합니다. 이것이 애플리케이션의 마스터 서버와 연결을 담당합니다.

이중에서 관심 대상은 ConnectMasterServer 함수의 나머지 두 인수입니다. OnConnectCallbackOnConnectFailedCallback입니다. 연결 성공 또는 실패 시 호출되는 메서드입니다. 보시다시피 OnConnectCallbackOnConnect UnityEvent를 호출하여 게임 플레이를 진행합니다.

OnClick event of ConnectButton

Unity 에디터에서 StrixConnectUI > StrixConnectPanel > Horizontal > ConnectButton를 보면 위 스크립트의 Connect 메서드를 호출하는 On Click 이벤트가 보입니다.

OnConnect event on StrixConnectGUI

StrixConnectPanel에서는 OnConnect UnityEvent의 StrixConnectGUI 스크립트 요소가 보입니다. 스크립트에서는 연결이 성공하면 이것이 트리거된다는 것을 알 수 있습니다. 이것에 연결되는 것이 StrixEnterRoom.EnterRoom 메서드로, 이것은 아래 스크립트 요소에서 Strix Enter Room 스크립트를 열면 볼 수 있습니다.

// 설명을 위해 몇 줄 생략

public class StrixEnterRoom : MonoBehaviour {

    public int capacity = 4;
    public string roomName = "New Room";
    public UnityEvent onRoomEntered;
    public UnityEvent onRoomEnterFailed;

    public void EnterRoom() {
        StrixNetwork.instance.JoinRandomRoom(StrixNetwork.instance.playerName, args => {
            onRoomEntered.Invoke();
        }, args => {
            CreateRoom();
        });
    }

    private void CreateRoom() {
        RoomProperties roomProperties = new RoomProperties {
            capacity = capacity,
            name = roomName
        };

        RoomMemberProperties memberProperties = new RoomMemberProperties {
            name = StrixNetwork.instance.playerName
        };


        StrixNetwork.instance.CreateRoom(roomProperties, memberProperties, args => {
            onRoomEntered.Invoke();
        }, args => {
            onRoomEnterFailed.Invoke();
        });
    }
}

EnterRoom 메서드는 마스터 서버 연결이 성공하면 호출되며, 즉시 JoinRandomRoom을 호출하여 방과 연결을 시도합니다. 이 호출에서는 앞서 설정해 둔 플레이어 이름과 성공 실패 핸들러가 필요합니다. 성공 시 onRoomEntered UnityEvent를 호출합니다. 실패 시 CreateRoom 메서드를 호출합니다.

CreateRoom 메서드에서는 설정된 용량과 방 이름으로 RoomProperties 개체가 만들어지고 플레이어 이름으로 RoomMemberProperties 개체가 만들어집니다. 이 둘은 Strix에서 방을 만들 때 필요합니다. 이어서 StrixNetwork 인스턴스에서 관련 인수로 CreateRoom을 호출합니다.

여기서도 성공과 실패 콜백(Strix 함수에서는 자주 보임)이 사용됩니다. (이 샘플의 경우, 트리거되는 이벤트에 로직이 없습니다)

단순명료한 연결 메서드입니다.

  1. 마스터 서버에 연결합니다.

  2. 방에 입장합니다.

  3. 이것이 실패하면 방을 만듭니다. (방을 만들면 만든 이는 자동으로 방에 입장합니다.)

복제

Strix는 클라이언트 간에 개체를 복제하기가 아주 쉽습니다. unitychan 캐릭터에 Strix Replicator 요소를 연결했습니다. 이렇게 하면 이 캐릭터가 클라이언트끼리 복제됩니다. 플레이어 캐릭터이기 때문에 다른 플레이어가 게임 월드에서 우리를 볼 수 있습니다.

복제되는 장면에는 개체가 두 가지 더 있습니다. NPCBall이 그것입니다. Strix Replicator 요소도 있습니다. 그러나 두 가지 중요한 차이가 있습니다.

Instantiable By set to Room Owner

첫째, 이 두 개체의 Replicator는 Instantiable By 속성이 방장으로 설정되어 있습니다. 이것은 방장만 개체를 복제할 수 있다고 Strix에게 지시하는 것입니다. 다른 플레이어가 방에 입장하면 자기 개체를 다른 클라이언트에게 복제하지 않으며 방장의 개체의 레플리카만 보게 됩니다.

둘째, Connection Closed BehaviourChange Ownership으로 설정됩니다. 이 개체를 인스턴스화하는 클라이언트의 연결이 닫히면 Strix가 이 개체의 주인을 변경합니다.

이 두 설정은 Ball과 NPC에게 중요합니다. 클라이언트 게임마다 클라이언트당 캐릭터가 하나여야 합니다. 그러나 클라이언트 게임마다 Ball과 NPC는 하나만 있어야 합니다. 플레이어 모두가 공유하는 월드의 개체들이기 때문입니다. 위 옵션으로는 (방장만 이 개체들을 소유하므로) 클라이언트마다 하나씩만 인스턴스화할 수 있습니다. 또, 방장의 연결이 해제되면 소유권이 이전되어 상실되지 않게 할 수 있습니다.

이것이 플레이어의 개체와 월드/서버의 개체가 다른 점입니다.

움직임

Strix는 Strix Replicator 요소가 있는 개체를 복제합니다. 단, 위치를 자동으로 업데이트하지는 않습니다. 이를 위해 NPC와 unitychan 캐릭터에는 그것을 담당하는 Strix Movement Synchronizer가 있습니다. Strix Movement Synchronizer는 캐릭터가 부드럽게 움직이게 하는 역할을 합니다.

Ball에는 Strix Transform Sync가 있습니다. 이 요소는 단순한 물체에 더 적합한 움직임 로직입니다. 움직임의 보간/보외를 실시하지 않기 때문입니다.

이 요소에 이렇게 설정을 한 상태에서 플레이를 해 보면 어떻게 되는지 알 수 있습니다. 게임이 클라이언트를 가리지 않고 원활하게 움직이려면 대개는 추가 구성이 필요합니다.

애니메이션

unitychan과 NPC 모두 Animator가 있으며, Strix에서는 이것도 동기화할 수 있습니다. Strix Animation Sync 요소는 Animator가 있을 때 애니매이션 동기화를 합니다.

게임 플레이 동기화

Strix가 개체와 움직임, 애니메이션을 어떻게 복제하는지 알았으므로, Strix가 게임 플레이 동작을 어떻게 복제하는지 살펴보겠습니다. 이것은 3인칭 슈팅 게임이므로 플레이어들은 총을 쏩니다.

이 게임에서 중요한 요소는 체력과 총알 두 가지입니다. 플레이어가 캐릭터에 마우스를 대고 좌클릭을 하면 총알이 나갑니다. 이 총알이 캐릭터에 맞으면 체력이 줄어듭니다.

unitychan 개체에는 FireBullet이라고 하는 스크립트가 있습니다.

public class FireBullet : StrixBehaviour {
    public GameObject bullet;
    private PlayerStatus playerStatus;

    // 이것으로 초기화
    void Start () {
        playerStatus = GetComponent<PlayerStatus>();
    }

    // Update는 프레임당 한 번 호출
    void Update () {
        if (!isLocal) {
            return;
        }

        if (playerStatus != null && playerStatus.health <= 0) {
            return;
        }

        if (Input.GetButtonDown("Fire1")) {
            GameObject instance = Instantiate(bullet);
            Transform firePos = transform.Find("FirePos");

            BulletControl bulletControl = instance.GetComponent<BulletControl>();
            bulletControl.owner = gameObject;

            instance.transform.position = firePos.position;
            instance.transform.rotation = firePos.rotation;
        }
    }
}

이 스크립트는 StrixBehaviour 클래스에서 상속한 것입니다. StrixBehaviour 클래스는 Unity의 표준 MonoBehaviour에서 상속하여 Strix 이용 시 몇 가지 추가 기능을 제공합니다.

이 스크립트는 프레임 업데이트를 일일이 확인하고 isLocal 속성을 이용하여 그것이 로컬 개체에서 실행 중인지, 복제된 것에서 실행 중인지 판단합니다. 이것은 중요합니다. 다른 플레이어가 소유하고 있는 레플리카에서 다른 플레이어가 이 로직에 액세스하면 안 되기 때문입니다. 액세스는 해당 개체를 소유하고 있는 플레이어로 한정해야 합니다.

스크립트는 발사 버튼이 눌리는지 확인하고, 눌리면 총알과 BulletControl 개체를 만듭니다.

BulletControl.cs를 보면 BulletControl이 총알의 전진을 담당한다는 것을 알 수 있습니다. 여기에는 중요한 타격 로직도 들어 있습니다.

// 41-43행
if (playerStatus != null && playerStatus.isLocal) {
    playerStatus.RpcToRoomOwner("OnHit");
}

이 개체는 타격의 대상이 로컬 개체인지 확인하고, 이어서 RpcToRoomOwner("OnHit")를 호출합니다.

RPC(Remote Procedure Call)는 다른 클라이언트에 메시지를 보내 기계에 있는 개체에 메서드를 호출하라고 지시합니다. 이것은 클라이언트 간에 액션을 동기화할 때 대단히 유용합니다.

여기서 BulletControl 클래스는 방장에게 메시지를 보내 PlayerStatus 개체의 OnHit 메서드를 hitObject에 호출하라고 지시합니다. 이 hitObject는 방장과 다른 기계에 있을 수도 있지만 PlayerStatusStrixBehaviour이고 캐릭터에는 StrixReplicator가 있으므로 Strix는 방장 게임에서 어떤 개체에 메서드를 호출할지 정확히 알고 있습니다.

즉, 총알이 발사되어 캐릭터에 맞으면 방장 게임에서 복제된 캐릭터의 PlayerStatus에 대해 OnHit 메서드를 호출한다는 뜻입니다.

이제 PlayerStatus 스크립트를 살펴보겠습니다.

// 설명을 위해 몇 줄 생략

public class PlayerStatus : StrixBehaviour {
    [StrixSyncField]
    public int health = 100;
    public int maxHealth = 100;
    public float recoverTime = 3;
    private Animator animator;
    private float deadTime = 0;

    // 이것으로 초기화
    void Start() {
        animator = GetComponent<Animator>();
    }

    // Update는 프레임당 한 번 호출
    void Update() {
        if (!isLocal) {
            return;
        }

        if (health <= 0 && Time.time >= deadTime + recoverTime) {
            RpcToAll("SetHealth", maxHealth);
        }
    }

    [StrixRpc]
    public void OnHit() {
        int value = health - 10;

        if (value < 0) {
            value = 0;
        }

        RpcToAll("SetHealth", value);
    }

    [StrixRpc]
    public void SetHealth(int value) {
        if (value < 0) {
            value = 0;
        } else if (value > maxHealth) {
            value = maxHealth;
        }

        animator.SetInteger("Health", value);

        if (animator != null) {
            if (value < health) {
                if (value <= 0) {
                    animator.SetTrigger("Dead");
                } else {
                    animator.SetTrigger("Damaged");
                }
            }

            if (value > 0 && health <= 0) {
                Respawn();
            }
        }

        if(health != value) {
            if (value <= 0) {
                deadTime = Time.time;
            }
        }

        health = value;
    }

    private void Respawn() {
        if (!CompareTag("Player") && isLocal) {
            transform.position = new Vector3(Random.Range(-40.0f, 40.0f), 2, Random.Range(-40.0f, 40.0f));
        }
    }
}

OnHit 메서드를 보기 전에 health 필드를 보겠습니다. 이 변수는 캐릭터의 체력을 나타냅니다. 그 위에 [StrixSyncField] 속성이 있습니다. [StrixSyncField]는 Strix에게 개체와 레플리카 간에 이 변수의 동기화를 지시합니다. 이렇게 하면 원 개체의 체력값이 레플리카에 복제됩니다. 이렇게 하면 간단하게 Strix로 값을 동기화할 수 있습니다.

이제 OnHit 메서드를 보면, 여기에도 [StrixRpc]라는 속성이 있음을 알 수 있습니다. 그러면 메서드가 Strix에 등록되고 RPC로 호출할 수 있습니다.

OnHit는 방장의 월드에서 레플리카 캐릭터에 호출됩니다. 이것은 체력값에서 데미지를 일부 차감하여 다른 RPC를 전송하고, 이번에는 SetHealth를 모든 클라이언트에게 보냅니다. 레플리카에 대한 호출이므로 이 체력값 업데이트는 이것이 실제로 방장이 소유한 캐릭터가 아닌 한 모든 클라이언트에 자동으로 동기화되지는 않습니다.

참고

RpcToAll 메서드는 RPC를 모든 클라이언트(그 RPC를 보낸 클라이언트 포함)에게 보냅니다.

Strix RPC는 인수를 취할 수 있고 SetHealth는 맞은 캐릭터에 새 체력값을 보냅니다. 이 메서드는 캐릭터 레플리카의 모든 PlayerStatus에 호출됩니다. 그리고 체력값을 설정하고 사망과 리스폰 로직을 처리합니다.

요약하면 다음과 같습니다.

  1. 플레이어가 자기 월드에서 총알을 발사합니다.

  2. 다른 캐릭터에 맞으면 Strix는 방장에게 RPC를 보내 방장의 월드에 있는 해당 캐릭터가 맞았다고 알려줍니다.

  3. 방장은 해당 캐릭터에서 데미지를 차감하고 이 변동사항을 레플리카 전체와 해당 캐릭터의 오리지널에게 알려줍니다.

히트 탐지는 로컬로 유지됩니다. 그래서 정확하며, 데미지 계산은 방장만 할 수 있어 비동기화 문제가 없습니다. 종합하면, 히트는 처리할 메시지가 n(게임 내 클라이언트 수) + 1만큼 필요합니다.

결론

Strix는 네트워크 게임용 기능이 다양합니다. 위 샘플은 그 기능들을 간단히 살펴본 것으로, 전체를 심도 있게 다룬 것은 아닙니다. Strix의 작동 방식에 관해 더 궁금한 점이나 Strix로 게임에 기능을 구현하는 방법을 알고 싶다면 본 문서를 계속 읽어 주시기 바랍니다.