HelloWorldGuide

A minimal Alteruna Multiplayer program for every supported platform: Unity, Godot, C# (.NET), C++ and C.

Hello world

This guide builds the same small program on every platform Alteruna Multiplayer supports. Two copies of the program find each other on the local network, join the same room, and say hello to each other by synchronizing a small piece of state. Pick your platform in the tabs; the choice applies per section.

You don't need an account to try it. The examples use a made-up project ID with authentication turned off, which works on a local network. For play over the internet, see Going online below.

1. Install

 
// In the Unity Editor: Window → Package Manager → + → Add package from git URL
https://github.com/Alteruna/multiplayer-sdk-unity-package.git
 
# Download alteruna-multiplayer-godot-<version>.zip from
# https://github.com/Alteruna/multiplayer-releases/releases and extract it into your project,
# so addons/alteruna sits in the project root. Restart the editor.
# Then create a scene with a Node, and attach hello_world.gd to it.
 
dotnet new console -n HelloWorld
cd HelloWorld
dotnet add package Alteruna.Multiplayer
 
# Download alteruna-multiplayer-cpp-<version>-<platform>.zip from
# https://github.com/Alteruna/multiplayer-releases/releases and extract it.
# CMakeLists.txt next to hello.cpp:
cmake_minimum_required(VERSION 3.16)
project(hello CXX)

set(CMAKE_CXX_STANDARD 17)

find_package(Alteruna REQUIRED)

add_executable(hello hello.cpp)
target_link_libraries(hello PRIVATE Alteruna::Multiplayer)

# Copy the shared library next to the executable.
add_custom_command(TARGET hello POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different
    $<TARGET_FILE:Alteruna::Multiplayer> $<TARGET_FILE_DIR:hello>)

# Configure with: cmake -S . -B build -DAlteruna_DIR=<extracted zip>/cmake
 
# Download alteruna-multiplayer-cpp-<version>-<platform>.zip from
# https://github.com/Alteruna/multiplayer-releases/releases and extract it.
# The same zip serves C and C++. CMakeLists.txt next to hello.c:
cmake_minimum_required(VERSION 3.16)
project(hello C)

find_package(Alteruna REQUIRED)

add_executable(hello hello.c)
target_link_libraries(hello PRIVATE Alteruna::Multiplayer)

# Copy the shared library next to the executable.
add_custom_command(TARGET hello POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different
    $<TARGET_FILE:Alteruna::Multiplayer> $<TARGET_FILE_DIR:hello>)

# Configure with: cmake -S . -B build -DAlteruna_DIR=<extracted zip>/cmake

2. Write the program

The Godot, C#, C++ and C versions are the same program: run with host, it opens a room and announces it on the local network; run without arguments, it joins the first room it finds. Each program owns a synchronizable, an object with an id that writes its state in Serialize and reads it in Unserialize. Here the state is a single line of text, which is printed when it arrives.

C#
using Alteruna.Multiplayer.Unity;
using Alteruna.Multiplayer.Unity.EventArgument;
using UnityEngine;

// Add this component to any GameObject in a scene that has a Multiplayer Manager.
public class HelloWorld : AttributesSync
{
    private void Start()
    {
        // Multiplayer is the scene's MultiplayerManager.
        Multiplayer.OnRoomJoined.AddListener(OnRoomJoined);
    }

    private void OnRoomJoined(RoomJoinedEvent e)
    {
        // Runs SayHello on every user in the room, including this one.
        BroadcastRemoteMethod(nameof(SayHello), "Hello world from " + e.User.Name + "!");
    }

    [SynchronizableMethod]
    private void SayHello(string text)
    {
        Debug.Log(text);
    }
}
GDScript
extends Node

# Run the project twice: once with "host" as argument (godot -- host), once without.
var service := AlterunaService.new()
var is_host := "host" in OS.get_cmdline_user_args() or "host" in OS.get_cmdline_args()
var text := "Hello world from %s!" % ("host" if is_host else "client")


func _ready() -> void:
    add_child(service)

    # Peers need the same project ID. With authenticate off, any GUID works on a local network.
    service.project_id = "5b0a7e3c-9f2d-4c61-8e4a-2d7f1c9b3a10"
    service.authenticate = false

    # "hello" is synchronized between peers: get_state sends our text, set_state receives theirs.
    service.register_synchronizable("hello", func(): return text, func(value): print(value))
    service.room_joined.connect(func(_room, _me): service.sync("hello"))
    service.room_list_updated.connect(_on_room_list_updated)

    # The host announces its room on the local network; clients listen for it.
    service.start()
    if is_host:
        service.open_port(20000, AlterunaService.DISCOVERY_BROADCAST)
        service.join_own_room()
    else:
        service.open_port(0, AlterunaService.DISCOVERY_LISTEN)


# A client joins the first room it discovers.
func _on_room_list_updated() -> void:
    var rooms := service.get_rooms()
    if not is_host and not service.in_room() and not rooms.is_empty():
        service.join_room(rooms[0].session_id)
C#
using System;
using System.Threading;
using Alteruna.Multiplayer.Core;
using Alteruna.Multiplayer.Core.MethodArguments;
using Alteruna.Multiplayer.Core.Packets;

// Run "HelloWorld host" in one terminal and "HelloWorld" in another.
class HelloWorld : ServiceListener, ISynchronizableElement, IAdaptiveSerializable
{
    static readonly Service Service = new Service();
    static bool _isHost;
    string _text;

    // The id identifies this object on every peer.
    public Guid CodecID => new Guid("8f1c2a6e-3d47-4b8e-9a21-6f0e5c7d1b01");
    public IAdaptiveSerializable Synchronizable => this;

    public void Serialize(ITransportStreamWriter writer, SerializeInfo info) => writer.Put(_text);
    public void Unserialize(ITransportStreamReader reader, UnserializeInfo info) => Console.WriteLine(reader.GetString());

    static void Main(string[] args)
    {
        _isHost = args.Length > 0 && args[0] == "host";
        var hello = new HelloWorld { _text = "Hello world from " + (_isHost ? "host" : "client") + "!" };
        hello.AddSynchronizable(hello);

        // Peers need the same project ID. auth: false allows any GUID on a local network.
        Service.VerifyProjectID("5b0a7e3c-9f2d-4c61-8e4a-2d7f1c9b3a10", null, false);
        Service.Start(hello);

        // The host announces its room on the local network; clients listen for it.
        Service.OpenPort(new Service.OpenPortArgs
        {
            Port = _isHost ? (ushort)20000 : (ushort)0,
            Discovery = _isHost
                ? Service.OpenPortArgs.DiscoveryMode.BroadcastAvailability
                : Service.OpenPortArgs.DiscoveryMode.AcceptBroadcasts
        });

        if (_isHost) Service.JoinOwnRoom(new RoomArgs());

        while (true)
        {
            Service.Update(); // sends and receives network data, fires callbacks
            Thread.Sleep(8);  // roughly 120 Hz
        }
    }

    // A client joins the first room it discovers.
    public override void OnRoomListUpdated(Service service)
    {
        if (!_isHost && !service.InRoomOrJoining && service.AvailableRooms.Count > 0)
            service.JoinRoom(service.AvailableRooms[0]);
    }

    // Send our text to everyone in the room.
    public override void OnRoomJoined(Room room, User me) => Service.Sync(this);
}
C++
// Run "hello host" in one terminal and "hello" in another.
#include <alteruna.hpp>

#include <chrono>
#include <cstdio>
#include <string>
#include <thread>

// The id identifies this object on every peer.
const auto HELLO_ID = alteruna::Guid::parse("8f1c2a6e-3d47-4b8e-9a21-6f0e5c7d1b01");

struct HelloWorld : alteruna::Listener, alteruna::Synchronizable {
    alteruna::Service* service = nullptr;
    bool is_host = false;
    std::string text;

    bool serialize(alteruna::Writer& w, uint8_t, bool) override { w.put(text); return true; }
    void unserialize(alteruna::Reader& r, uint8_t, bool, uint32_t) override { std::printf("%s\n", r.get_string().c_str()); }

    // A client joins the first room it discovers.
    void on_room_list_updated() override {
        auto rooms = service->rooms();
        if (!is_host && !service->in_room() && !rooms.empty()) service->join_room(rooms[0].session_id);
    }

    // Send our text to everyone in the room.
    void on_room_joined(const alteruna::RoomInfo&, const alteruna::UserInfo&) override { service->sync(HELLO_ID); }
};

int main(int argc, char** argv) {
    HelloWorld hello;
    hello.is_host = argc > 1 && std::string(argv[1]) == "host";
    hello.text = std::string("Hello world from ") + (hello.is_host ? "host" : "client") + "!";

    alteruna::Service service(&hello);
    hello.service = &service;

    // Peers need the same project ID. auth = false allows any GUID on a local network.
    service.verify_project_id("5b0a7e3c-9f2d-4c61-8e4a-2d7f1c9b3a10", {}, false);
    service.start();
    service.register_synchronizable(HELLO_ID, hello);

    // The host announces its room on the local network; clients listen for it.
    alteruna::PortConfig port;
    port.port = hello.is_host ? 20000 : 0;
    port.discovery = hello.is_host ? AU_DISCOVERY_BROADCAST_AVAILABILITY : AU_DISCOVERY_ACCEPT_BROADCASTS;
    service.open_port(port);

    if (hello.is_host) service.join_own_room();

    for (;;) {
        service.update();                                           // sends and receives network data, fires callbacks
        std::this_thread::sleep_for(std::chrono::milliseconds(8)); // roughly 120 Hz
    }
}
C
/* Run "hello host" in one terminal and "hello" in another. */
#include <alteruna.h>

#include <stdio.h>
#include <string.h>

#ifdef _WIN32
#include <windows.h>
#define sleep_ms(ms) Sleep(ms)
#else
#include <unistd.h>
#define sleep_ms(ms) usleep((ms) * 1000)
#endif

static au_service* service;
static au_guid hello_id;
static int is_host;
static char text[64];

static int32_t serialize(void* user_data, au_writer* writer, uint8_t lod, int32_t force_sync)
{
    au_writer_put_string(writer, text);
    return 0;
}

static void unserialize(void* user_data, au_reader* reader, uint8_t lod, int32_t force_sync, uint32_t length)
{
    printf("%s\n", au_reader_get_string(reader));
}

/* A client joins the first room it discovers. */
static void on_room_list_updated(void* user_data)
{
    au_room_info room;
    if (!is_host && au_service_in_room(service) == 0 && au_service_get_room(service, 0, &room) == AU_OK)
        au_service_join_room(service, room.session_id, 0);
}

/* Send our text to everyone in the room. */
static void on_room_joined(void* user_data, const au_room_info* room, const au_user_info* me)
{
    au_service_sync(service, &hello_id, AU_USER_ALL, AU_RELIABLE);
}

int main(int argc, char** argv)
{
    is_host = argc > 1 && strcmp(argv[1], "host") == 0;
    snprintf(text, sizeof(text), "Hello world from %s!", is_host ? "host" : "client");

    au_callbacks callbacks = {0};
    callbacks.on_room_list_updated = on_room_list_updated;
    callbacks.on_room_joined = on_room_joined;
    service = au_service_create(AU_DEVICE_CLIENT, &callbacks);

    /* Peers need the same project ID. auth = 0 allows any GUID on a local network. */
    au_service_verify_project_id(service, "5b0a7e3c-9f2d-4c61-8e4a-2d7f1c9b3a10", NULL, 0);
    au_service_start(service, NULL);

    /* The id identifies this object on every peer. */
    au_guid_parse("8f1c2a6e-3d47-4b8e-9a21-6f0e5c7d1b01", &hello_id);
    au_service_register_synchronizable(service, &hello_id, serialize, unserialize, NULL);

    /* The host announces its room on the local network; clients listen for it. */
    au_port_config port;
    au_port_config_init(&port);
    port.port = is_host ? 20000 : 0;
    port.discovery = is_host ? AU_DISCOVERY_BROADCAST_AVAILABILITY : AU_DISCOVERY_ACCEPT_BROADCASTS;
    au_service_open_port(service, &port, NULL);

    if (is_host) au_service_join_own_room(service, NULL);

    for (;;) {
        au_service_update(service); /* sends and receives network data, fires callbacks */
        sleep_ms(8);                /* roughly 120 Hz */
    }
}

3. Run it

Start two copies on the same machine or on the same local network.

 
// 1. In a new scene, right click in the Hierarchy: Alteruna → Multiplayer Manager.
// 2. Add the Room Menu prefab (Alteruna Multiplayer SDK → Prefabs → Room Menu).
// 3. Add the HelloWorld component to any GameObject.
// 4. Make a build (File → Build And Run) and also press Play in the Editor.
// 5. Press Start in the Room Menu in one of them, and Join the room in the other.
// The Console of each instance logs "Hello world from <name>!" for every user that joins.
 
godot --path . -- host     # first terminal
godot --path .             # second terminal
# Or in the editor: Debug → Customize Run Instances, run 2 instances, and give one the argument "host".
 
dotnet run -- host     # first terminal
dotnet run             # second terminal
 
cmake --build build
./build/hello host     # first terminal (build\Debug\hello.exe host on Windows)
./build/hello          # second terminal
 
cmake --build build
./build/hello host     # first terminal (build\Debug\hello.exe host on Windows)
./build/hello          # second terminal

The host prints Hello world from client! and the client prints Hello world from host!.

The client's hello is sent by its Sync call when it joins the room. The host never sends anything explicitly: when a user joins a room, everyone already in it sends the newcomer the current state of their synchronizables (a force sync), so the host's text arrives on its own.

How it works

  • Project ID. Peers only connect to peers with the same project ID. With authentication off (auth = false), any GUID works on a local network.
  • Update loop. Nothing happens in the background: sending, receiving and every callback happen in Update (update, au_service_update). Call it often; the examples run it at roughly 120 Hz. Unity's Multiplayer Manager does this for you.
  • Synchronizables. A synchronizable is matched across peers by its id, so every peer registers it with the same id. Sync serializes it and sends it to the others in the room. In Unity, an AttributesSync component does this for you: [SynchronizableField] fields are synchronized automatically, and a [SynchronizableMethod] runs on every user when called with BroadcastRemoteMethod, including the sender.
  • Discovery. The host broadcasts its room on the local network and clients listen for it. Use AddKnownDevice (add_known_device, au_service_add_known_device) to connect to an address directly instead.

Going online

To play over the internet, register a project in the Alteruna Multiplayer portal, use its project ID with authentication on, and join a room through Alteruna's servers, for example with JoinOnDemandRoom (join_on_demand_room, au_service_join_on_demand_room) instead of hosting one locally. In Unity, register the license under Window → Alteruna → Register license; the Room Menu then lists online rooms.

Next steps

  • Unity: the User Guide and the Example Game Guide.
  • Synchronizing state: Developer Guide. Outside Unity, the same synchronizable pattern scales to any state: write more fields in Serialize and read them back in the same order.
  • C and C++: the README.md and samples/cpp in the SDK zip.

Discord

Join the Alteruna Discord server to find additional resources and other developers to chat with!

See Also