게임 엔진 통합¶
목차 | 이전: 오류 처리 | 다음: Node Protobuf 송수신
다른 언어로 보기 — C++ · C#/.NET · Java · Kotlin · Node/TypeScript
이 장을 읽고 나면
엔진과 빌드 대상에 맞는 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를 보낸다.
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()를 실행할 때 처리된다.
_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 참가를 시작한다.
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는 단방향으로 보낸다.
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가 화면을 갱신한다.
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);
}