Skip to content

Ebiten integration

Use the Ebiten integration when your client is a 2D Go game and you want generated Golem entity state to drive your drawables. The setup has two parts: go-client generates the typed protocol client, and ebiten generates one view scaffold per entity so your game code can attach sprites, animations, or other Ebiten drawing objects.

Add both integrations to golem.yaml:

integrations:
go-client:
out: internal/client/
package: client
ebiten:
out: internal/views/
package: views
protocol_import: "example.com/mygame/internal/client"

Run golem-bake. The go-client output contains the transport-ready generated client, and the ebiten output contains view scaffolds such as player_view.go. The protocol_import value is the Go import path for the generated go-client package.

The generated views use golem-ebiten, and your game loop uses Ebiten:

Terminal window
go get golem-engine/golem-go-client
go get golem-engine/golem-ebiten
go get github.com/hajimehoshi/ebiten/v2

If you vendor or publish Golem under a different module path, use the matching path in golem_import and your Go imports.

Create the generated client once, wrap it in golem-ebiten.Game, and call the helper from your Ebiten Update and Draw methods:

package game
import (
"context"
"github.com/hajimehoshi/ebiten/v2"
"example.com/mygame/internal/client"
"example.com/mygame/internal/views"
golemebiten "golem-engine/golem-ebiten"
golemclient "golem-engine/golem-go-client"
)
type Game struct {
realtime *golemebiten.Game
}
func NewGame(ctx context.Context) (*Game, error) {
c := client.CreateClient()
rt := golemebiten.NewGame(golemebiten.GameConfig{
Client: c.GameClient,
ConnectionOptions: func(ctx context.Context) (golemclient.ConnectOptions, error) {
cfg, err := golemclient.FetchRealtimeConfig(ctx, "http://localhost:8080/api/realtime-config", nil)
if err != nil {
return golemclient.ConnectOptions{}, err
}
return golemclient.ConnectOptionsFromRealtimeConfig(
cfg,
golemclient.WithQueryParam("token", sessionToken),
)
},
})
c.Entities.RegisterPlayer(func(id int64) client.PlayerClientEntity {
view := views.NewPlayerView(id)
rt.AddView(view)
return view
})
if err := rt.Connect(ctx); err != nil {
return nil, err
}
return &Game{realtime: rt}, nil
}
func (g *Game) Update() error {
return g.realtime.Update(context.Background())
}
func (g *Game) Draw(screen *ebiten.Image) {
g.realtime.Draw(screen)
}

The Player names in this example come from your entity YAML. Register entity factories before connecting so snapshots received during the handshake create the right view types. ConnectionOptions runs for the initial connection and each reconnect attempt, so it is the right place to fetch fresh realtime config or rotate auth values.

Each generated XxxView embeds the generated SyncedXxx state holder, gives you lifecycle hooks, and calls SyncToDrawable after each full state or delta:

type PlayerView struct {
*client.SyncedPlayer
Drawable golemebiten.Drawable
}
func (v *PlayerView) OnSpawn() {}
func (v *PlayerView) OnRemove() {}
func (v *PlayerView) SyncToDrawable() {}

Edit the generated scaffold or embed it from your own type. A small drawable can implement Draw, Destroy, and optionally SetPosition:

type Sprite struct {
img *ebiten.Image
x float32
y float32
}
func (s *Sprite) SetPosition(x, y float32) {
s.x, s.y = x, y
}
func (s *Sprite) Draw(screen *ebiten.Image) {
opts := &ebiten.DrawImageOptions{}
opts.GeoM.Translate(float64(s.x), float64(s.y))
screen.DrawImage(s.img, opts)
}
func (s *Sprite) Destroy() {}

Assign it in OnSpawn:

func (v *PlayerView) OnSpawn() {
v.Drawable = &Sprite{img: playerImage}
v.SyncToDrawable()
}

The default SyncToDrawable checks whether Drawable implements SetPosition(x, y) and copies PosX() and PosY() into it. Add your own animation, facing, health bar, or interpolation logic in the generated view when your schema fields require it.

If an event schema targets an entity type, the generated view includes an OnXxx stub. Override it to trigger effects on the local drawable:

func (v *PlayerView) OnHit(e *client.HitEvent) {
spawnDamageNumber(v.PosX(), v.PosY(), e.Amount)
}

When the event uses foi_only: true, the entity is in this client’s field of interest when the method fires. For non-FOI-filtered entity events, guard any drawable-specific code in case the entity is not currently visible.

For the simplest desktop loop, use WebSocket and configure the server with StateUpdateLaneStream. For WebTransport, connect with TransportWebTransport and the /wt URL; the generated Go client applies compact datagram state automatically while the Ebiten view code stays the same.

When your server exposes /api/realtime-config, prefer the ConnectionOptions pattern above. It keeps Ebiten reconnects aligned with the server’s current transport URL and any development certificate hashes. If you already know the endpoint, a static Connection still works:

rt := golemebiten.NewGame(golemebiten.GameConfig{
Client: c.GameClient,
Connection: golemclient.ConnectOptions{
Transport: golemclient.TransportWebSocket,
URL: "ws://localhost:8080/ws",
},
})

See also Go client, Channels and transports, and State updates.