Skip to content

Game Engine Integration

Contents | Previous: Godot C# | Next: Node Protobuf Messaging

After reading this chapter

You can choose the connector for an engine and build target, then find the installation, packet exchange, packaging guides, and runnable Engine Lobby sample.

A game client connects to a STREAM server. Unity uses a different connector for each build target. Unreal and Godot C# use the C++ and .NET connectors, respectively. Follow the chapter for your engine for installation, connection, sending and receiving, and main-thread dispatch. The packaging column points to the package and build steps for each target.

Engine and target Connector Usage guide Packaging
Unity native .NET Unity native Unity sample native build
Unity WebGL TypeScript connector and UPM adapter Unity WebGL Unity sample WebGL build
Unreal C++ connector's Unreal plugin Engine adapters C++ packaging
Godot C# .NET Godot C# The Godot C# chapter does not cover deployment
Godot C++ The C++ connector's GDExtension adapter Engine adapters C++ packaging
Axmol The C++ connector's Axmol adapter Engine adapters C++ packaging
Cocos Creator web TypeScript connector Browser Cocos Creator sample

1. Engine Lobby Sample

The Engine Lobby contract defines the packets exchanged by the server and clients. Its implementation structure table decides which engine clients exist and which mirror repository ships each one. This chapter quotes the Unity and Unreal clients.

The Unity sample uses the same client source for native and WebGL builds. It connects, waits for a JoinReq response, and sends a 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();

The On<ChatNotify> callback runs when Update() calls 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;
    }
);

The Unreal client creates the plugin connector, connects, and starts joining the lobby after a PingReq response.

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 waits for a response. ChatMsg is a one-way send.

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));
}

The connector is pumped in Tick(). Its ChatNotify delegate updates the display.

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);
}