콘텐츠로 이동

게임 엔진 통합

목차 | 이전: Unity WebGL | 다음: Node Protobuf 송수신

이 장을 읽고 나면

엔진과 빌드 대상에 맞는 connector를 고르고, 설치부터 packet 송수신과 배포까지 다루는 가이드와 실행 가능한 Engine Lobby sample을 찾을 수 있다.

게임 client는 STREAM server에 연결한다. Unity는 빌드 대상에 따라 connector가 달라지고, Unreal과 Godot C#은 각각 C++와 .NET connector를 사용한다. 다음 표의 가이드는 설치, 연결, 송수신과 main thread 규칙을 다룬다. 배포 열은 엔진별 package와 빌드 절차를 가리킨다.

엔진과 빌드 대상 connector 사용 가이드 배포
Unity native .NET Unity native Unity sample의 native build
Unity WebGL TypeScript connector와 UPM 어댑터 Unity WebGL Unity sample의 WebGL build
Unreal C++ connector의 Unreal plugin 엔진 어댑터 C++ 배포
Godot C# .NET Godot C# Godot C# 장은 배포 절차를 다루지 않는다
Godot C++ C++ connector의 GDExtension 어댑터 엔진 어댑터 C++ 배포
Axmol C++ connector의 Axmol 어댑터 엔진 어댑터 C++ 배포
Cocos Creator web TypeScript connector 브라우저 Cocos Creator sample

1. Engine Lobby sample

Engine Lobby 계약은 server와 client가 주고받는 packet을 정의한다. 어떤 engine client가 있고 각각 어느 미러 저장소로 제공되는지는 그 계약의 구현 구조 표가 정한다. 이 장은 Unity와 Unreal client의 코드를 인용한다.

Unity sample은 같은 client source를 native와 WebGL 빌드에 사용한다. 다음 코드는 연결한 뒤 JoinReq로 응답을 받고 ChatMsg를 보낸다.

Unity/Assets/EngineLobby/ZLinkClient.cs
await _connector.Connect.Async();
_pumping = true;
Unity/Assets/EngineLobby/ZLinkClient.cs
var joined = await _connector.Request(new JoinReq(playerName)).Async<JoinRes>();
_statusText.text = $"joined as {joined.name} ({joined.actorId})";
Debug.Log($"Engine Lobby JoinRes: actorId={joined.actorId} name={joined.name}");
await _connector.Send(new ChatMsg(firstChat)).Async();

On<ChatNotify> callback은 update()에서 Dispatch.Async()를 실행할 때 처리된다.

Unity/Assets/EngineLobby/ZLinkClient.cs
_chatSubscription = _connector.On<ChatNotify>(
    (message, cancellationToken) =>
    {
        _statusText.text = $"{message.Payload.name}: {message.Payload.text}";
        Debug.Log(
            $"Engine Lobby ChatNotify: actorId={message.Payload.actorId} "
                + $"name={message.Payload.name} text={message.Payload.text}"
        );
        return default;
    }
);

Unreal client는 plugin을 만든 뒤 연결하고, PingReq의 응답을 받으면 lobby 참가를 시작한다.

Unreal/Source/EngineLobby/Private/EngineLobbyClientActor.cpp
void AZLinkClientActor::BeginPlay ()
{
    Super::BeginPlay ();
    Connector = NewObject<UZLinkStreamConnector> (this);
    PacketHandle =
      Connector->On (PacketName (engine_lobby::packet::chat_notify),
                     [this] (const FZLinkStreamPacket &Packet) { HandlePacket (Packet); });
    SetStatus (TEXT ("Engine Lobby: connecting"));
    Connector->Connect (Endpoint);
    if (!Connector->IsConnected ()) {
        SetStatus (TEXT ("Engine Lobby: connection failed"), FColor::Red);
        return;
    }
    SendPing ();
}

JoinReq는 응답을 기다리고, ChatMsg는 단방향으로 보낸다.

Unreal/Source/EngineLobby/Private/EngineLobbyClientActor.cpp
void AZLinkClientActor::SendJoin ()
{
    Connector->RequestJson (
      PacketName (engine_lobby::packet::join_req),
      EncodeStringField (engine_lobby::field::name, PlayerName), 5.0F,
      [this] (const FZLinkStreamRequestResult &Result) { HandleJoinResponse (Result); });
}

void AZLinkClientActor::SendChat ()
{
    Connector->SendJson (PacketName (engine_lobby::packet::chat_msg),
                         EncodeStringField (engine_lobby::field::text, FirstChat));
}

tick()에서 connector를 pump하면 ChatNotify delegate가 화면을 갱신한다.

Unreal/Source/EngineLobby/Private/EngineLobbyClientActor.cpp
void AZLinkClientActor::HandlePacket (const FZLinkStreamPacket &Packet)
{
    const TSharedPtr<FJsonObject> Json = DecodePayload (Packet);
    FString ActorId;
    FString Name;
    FString Text;
    if (!ReadRequiredString (Json, engine_lobby::field::actor_id, ActorId)
        || !ReadRequiredString (Json, engine_lobby::field::name, Name)
        || !ReadRequiredString (Json, engine_lobby::field::text, Text)) {
        SetStatus (TEXT ("Engine Lobby: invalid ChatNotify"), FColor::Red);
        return;
    }
    SetStatus (FString::Printf (TEXT ("%s: %s"), *Name, *Text), FColor::Green);
}