Skip to content

Minimal server wiring

This is the smallest main that boots an authoritative tick loop and starts serving clients. It sets the tick rate and listen address, registers the connect, disconnect, and command hooks, and calls Run. Spawning entities and writing gameplay come next, on Your first entity.

synced.NewServer returns two things: the golem.Server you run, and a runtime handle (rt) that exposes the command and event helpers generated from your schemas. It also wires the internal plumbing for you, such as serializing removed entities.

Imports below use placeholder module paths; replace them with your module and generated package paths.

package main
import (
"context"
"log"
"github.com/demiurgos-hub/golem-engine/golem"
"example.com/mygame/internal/synced" // generated go-server integration
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
srv, rt := synced.NewServer(golem.ServerConfig{
TickRate: 20,
Addr: ":8080",
StaticDir: "./public", // optional: serve static assets over HTTP
})
srv.OnConnect(func(sess *golem.Session) {
// A player connected. Spawn their entity here; see "Your first entity".
})
srv.OnDisconnect(func(sess *golem.Session) {
// A player left. Remove their entity with srv.DeleteEntity(id).
})
srv.OnTick(func(dt float64, s *golem.Server) {
// System-level logic that isn't tied to one entity: spawn waves,
// match timers, global rules. Per-entity gameplay lives on entity types.
_ = dt
})
// Present when your schemas define commands.
rt.Commands.BindAllHandlers(func(sess *golem.Session, err error) {
log.Printf("session %d command error: %v", sess.ID, err)
})
if err := srv.Run(ctx); err != nil && err != context.Canceled {
log.Fatal(err)
}
}

With Addr set, Run starts the tick loop plus integrated networking and broadcasts entity changes to clients after every tick. Cancel ctx (for example on SIGINT) for clean shutdown; Run then returns ctx.Err().

rt.Commands is present when your schemas define commands. BindAllHandlers installs the command handlers on the server and takes one callback for decode or dispatch errors; register your per-command handlers on rt.Commands before calling it, and see Client commands for the routing details. For raw, non-command client bytes, register srv.OnMessage(func(sess *golem.Session, data []byte) { … }) instead.

Because synced.NewServer sets the removal serializer for you, removed entities serialize correctly out of the box. If you construct golem.NewServer directly instead of going through the generated helper, call the generated SetRemovalSerializer yourself before any entity is removed, or Run returns an error when it tries to flush a removal.

If you omit Addr, no HTTP server is started. Mount the server’s transport handler on your own mux (the same Server still owns sessions and the hooks you registered):

import "net/http"
// Serve over WebSocket so a standard net/http server can host the endpoint.
srv := golem.NewServer(golem.ServerConfig{
TickRate: 20,
Transport: golem.TransportWebSocket,
Path: "/ws",
})
mux := http.NewServeMux()
mux.Handle("/ws", srv.Handler())
go func() { _ = http.ListenAndServe(":8080", mux) }()
if err := srv.Run(ctx); err != nil && err != context.Canceled {
log.Fatal(err)
}

This example uses WebSocket because a plain net/http server can host it. golem’s default transport is WebTransport, which runs over HTTP/3 and needs a different server setup. Choosing a transport and configuring TLS and allowed browser origins is its own topic, covered in Channels and transports.

See also Typical workflow.