multiplayerState struct

Fields:

  • Players (map[string]multiplayerPlayer) - json:"players"
  • Bullets ([]multiplayerBullet) - json:"bullets"
  • Winner (string) - json:"winner"

multiplayerPlayer struct

Fields:

  • ID (string) - json:"id"
  • X (float64) - json:"x"
  • Y (float64) - json:"y"
  • VX (float64) - json:"vx"
  • VY (float64) - json:"vy"
  • AimX (float64) - json:"aimX"
  • AimY (float64) - json:"aimY"
  • Color (string) - json:"color"
  • Lives (int) - json:"lives"
  • Alive (bool) - json:"alive"
  • Cooldown (float64) - json:"-"

multiplayerBullet struct

Fields:

  • X (float64) - json:"x"
  • Y (float64) - json:"y"
  • VX (float64) - json:"vx"
  • VY (float64) - json:"vy"
  • Owner (string) - json:"owner"
  • Life (float64) - json:"-"

RegisterMultiplayerHost function

RegisterMultiplayerHost exposes the multiplayer arena netcode server.

Show/Hide Function Body
{
	initial := multiplayerState{Players: make(map[string]multiplayerPlayer)}
	srv := netcode.NewServer(multiplayerChannel, initial, applyMultiplayerCommand)
	host.Register(srv.HostComponent())

	ticker := time.NewTicker(multiplayerTick)
	go func() {
		defer ticker.Stop()
		var tick int64
		for range ticker.C {
			tick += int64(multiplayerTick / time.Millisecond)
			srv.Update(func(state *multiplayerState) {
				stepMultiplayer(state, multiplayerTick.Seconds())
			})
			srv.Broadcast(tick)
		}
	}()
}

applyMultiplayerCommand function

Parameters:

  • state *multiplayerState
  • cmd any
Show/Hide Function Body
{
	payload, ok := cmd.(map[string]any)
	if !ok {
		return
	}
	session, _ := payload["session"].(string)
	if session == "" {
		return
	}
	if state.Players == nil {
		state.Players = make(map[string]multiplayerPlayer)
	}

	action, _ := payload["type"].(string)
	switch action {
	case "join":
		ensurePlayer(state, session)
		state.Winner = ""
		return
	}

	player := ensurePlayer(state, session)
	if !player.Alive {
		state.Players[session] = player
		return
	}

	dx := floatFrom(payload["dx"])
	dy := floatFrom(payload["dy"])
	mag := math.Hypot(dx, dy)
	if mag > 1 {
		dx /= mag
		dy /= mag
		mag = 1
	}
	player.VX = dx * playerSpeed
	player.VY = dy * playerSpeed

	aimX := floatFrom(payload["aimX"])
	aimY := floatFrom(payload["aimY"])
	if aimLen := math.Hypot(aimX, aimY); aimLen > 0 {
		player.AimX = aimX / aimLen
		player.AimY = aimY / aimLen
	} else if mag > 0 {
		norm := math.Hypot(dx, dy)
		if norm > 0 {
			player.AimX = dx / norm
			player.AimY = dy / norm
		}
	}

	shoot := boolFrom(payload["shoot"])
	if shoot && player.Cooldown <= 0 && player.Alive {
		ax, ay := player.AimX, player.AimY
		if math.Hypot(ax, ay) == 0 {
			ax, ay = 0, -1
			player.AimX, player.AimY = ax, ay
		}
		bullet := multiplayerBullet{
			X:     player.X + ax*(playerRadius+bulletRadius),
			Y:     player.Y + ay*(playerRadius+bulletRadius),
			VX:    ax * bulletSpeed,
			VY:    ay * bulletSpeed,
			Owner: session,
		}
		state.Bullets = append(state.Bullets, bullet)
		player.Cooldown = shootCooldownSeconds
	}

	state.Players[session] = player
}

stepMultiplayer function

Parameters:

  • state *multiplayerState
  • dt float64
Show/Hide Function Body
{
	if state.Players == nil {
		state.Players = make(map[string]multiplayerPlayer)
	}

	for id, player := range state.Players {
		if player.Cooldown > 0 {
			player.Cooldown -= dt
			if player.Cooldown < 0 {
				player.Cooldown = 0
			}
		}
		if player.Alive {
			player.X += player.VX * dt
			player.Y += player.VY * dt
			if player.X < playerRadius {
				player.X = playerRadius
			}
			if player.X > arenaWidth-playerRadius {
				player.X = arenaWidth - playerRadius
			}
			if player.Y < playerRadius {
				player.Y = playerRadius
			}
			if player.Y > arenaHeight-playerRadius {
				player.Y = arenaHeight - playerRadius
			}
		}
		state.Players[id] = player
	}

	newBullets := make([]multiplayerBullet, 0, len(state.Bullets))
	for _, bullet := range state.Bullets {
		bullet.X += bullet.VX * dt
		bullet.Y += bullet.VY * dt
		bullet.Life += dt
		if bullet.Life > bulletLifetime {
			continue
		}
		if bullet.X < 0 || bullet.X > arenaWidth || bullet.Y < 0 || bullet.Y > arenaHeight {
			continue
		}

		hit := false
		for id, player := range state.Players {
			if !player.Alive || id == bullet.Owner {
				continue
			}
			if overlaps(bullet.X, bullet.Y, player.X, player.Y, playerRadius+bulletRadius) {
				player.Lives--
				if player.Lives < 0 {
					player.Lives = 0
				}
				if player.Lives <= 0 {
					player.Alive = false
					player.VX, player.VY = 0, 0
				}
				state.Players[id] = player
				hit = true
				break
			}
		}
		if !hit {
			newBullets = append(newBullets, bullet)
		}
	}
	state.Bullets = newBullets

	aliveCount := 0
	lastAlive := ""
	for id, player := range state.Players {
		if player.Alive {
			aliveCount++
			lastAlive = id
		}
	}
	if aliveCount == 1 {
		state.Winner = lastAlive
	} else if aliveCount == 0 {
		state.Winner = ""
	} else {
		state.Winner = ""
	}
}

ensurePlayer function

Parameters:

  • state *multiplayerState
  • session string

Returns:

  • multiplayerPlayer

References:

Show/Hide Function Body
{
	if player, ok := state.Players[session]; ok {
		return player
	}
	spawnX := playerRadius + rand.Float64()*(arenaWidth-2*playerRadius)
	spawnY := playerRadius + rand.Float64()*(arenaHeight-2*playerRadius)
	player := multiplayerPlayer{
		ID:    session,
		X:     spawnX,
		Y:     spawnY,
		AimX:  0,
		AimY:  -1,
		Color: pickColor(state),
		Lives: maxLives,
		Alive: true,
	}
	state.Players[session] = player
	return player
}

pickColor function

Parameters:

  • state *multiplayerState

Returns:

  • string
Show/Hide Function Body
{
	used := make(map[string]bool, len(state.Players))
	for _, player := range state.Players {
		used[player.Color] = true
	}
	for _, color := range colorPalette {
		if !used[color] {
			return color
		}
	}
	return colorPalette[rand.Intn(len(colorPalette))]
}

overlaps function

Parameters:

  • ax float64
  • ay float64
  • bx float64
  • by float64
  • radius float64

Returns:

  • bool
Show/Hide Function Body
{
	dx := ax - bx
	dy := ay - by
	return dx*dx+dy*dy <= radius*radius
}

floatFrom function

Parameters:

  • v any

Returns:

  • float64
Show/Hide Function Body
{
	switch val := v.(type) {
	case float64:
		return val
	case float32:
		return float64(val)
	case int:
		return float64(val)
	case int64:
		return float64(val)
	}
	return 0
}

boolFrom function

Parameters:

  • v any

Returns:

  • bool
Show/Hide Function Body
{
	switch val := v.(type) {
	case bool:
		return val
	case float64:
		return val != 0
	case int:
		return val != 0
	}
	return false
}

init function

Show/Hide Function Body
{
	rand.Seed(time.Now().UnixNano())
}

ncState struct

Fields:

  • X (float64) - json:"x"

RegisterNetcodeHost function

RegisterNetcodeHost sets up a netcode server broadcasting position updates.

Show/Hide Function Body
{
	srv := netcode.NewServer("NetcodeHost", ncState{}, func(s *ncState, cmd any) {
		if m, ok := cmd.(map[string]any); ok {
			if dx, ok := m["dx"].(float64); ok {
				s.X += dx
			}
		}
	})
	host.Register(srv.HostComponent())
	go func() {
		ticker := time.NewTicker(50 * time.Millisecond)
		var tick int64
		for range ticker.C {
			tick += 50
			srv.Broadcast(tick)
		}
	}()
}

RegisterSSCHost function

Show/Hide Function Body
{
	var counter int
	host.Register(host.NewHostComponent("SSCHost", func(_ map[string]any) any {
		return map[string]any{"value": counter}
	}))
	go func() {
		ticker := time.NewTicker(5 * time.Second)
		for range ticker.C {
			counter++
			host.Broadcast("SSCHost", map[string]any{"value": counter})
		}
	}()
}

RegisterTwitchOAuthHost function

Show/Hide Function Body
{
	host.Register(host.NewHostComponent("TwitchOAuthHost", func(payload map[string]any) any {
		code, _ := payload["code"].(string)
		if code == "" {
			return nil
		}
		fmt.Println("Payload:", payload)
		client, err := helix.NewClient(&helix.Options{
			ClientID:     os.Getenv("TWITCH_CLIENT_ID"),
			ClientSecret: os.Getenv("TWITCH_CLIENT_SECRET"),
			RedirectURI:  "https://localhost:8081/examples/twitch/callback",
		})
		if err != nil {
			return map[string]any{"status": err.Error()}
		}
		resp, err := client.RequestUserAccessToken(code)
		if err != nil {
			return map[string]any{"status": err.Error()}
		}
		token := resp.Data.AccessToken
		fmt.Println("Access token:", token)
		if token == "" {
			return map[string]any{"status": "missing token"}
		}
		return map[string]any{"status": "token received", "accessToken": token}
	}))
}

math import

Import example:

import "math"

math/rand import

Import example:

import "math/rand"

time import

Import example:

import "time"

github.com/rfwlab/rfw/v1/host import

Import example:

import "github.com/rfwlab/rfw/v1/host"

github.com/rfwlab/rfw/v1/netcode import

Import example:

import "github.com/rfwlab/rfw/v1/netcode"

time import

Import example:

import "time"

github.com/rfwlab/rfw/v1/host import

Import example:

import "github.com/rfwlab/rfw/v1/host"

github.com/rfwlab/rfw/v1/netcode import

Import example:

import "github.com/rfwlab/rfw/v1/netcode"

time import

Import example:

import "time"

github.com/rfwlab/rfw/v1/host import

Import example:

import "github.com/rfwlab/rfw/v1/host"

fmt import

Import example:

import "fmt"

os import

Import example:

import "os"

github.com/nicklaw5/helix/v2 import

Import example:

import "github.com/nicklaw5/helix/v2"

Imported as:

helix

github.com/rfwlab/rfw/v1/host import

Import example:

import "github.com/rfwlab/rfw/v1/host"