diff --git a/actions.proto b/actions.proto new file mode 100644 index 0000000..b059579 --- /dev/null +++ b/actions.proto @@ -0,0 +1,32 @@ +syntax = "proto3"; +package actions; + +option go_package = "gitea.boner.be/bdnugget/goonserver/actions"; + +message Action { + enum ActionType { + MOVE = 0; + } + ActionType type = 1; + int32 x = 2; + int32 y = 3; + int32 player_id = 4; +} + +message ActionBatch { + int32 player_id = 1; + repeated Action actions = 2; + int64 tick = 3; +} + +message PlayerState { + int32 player_id = 1; + int32 x = 2; + int32 y = 3; +} + +message ServerMessage { + int32 player_id = 1; + repeated PlayerState players = 2; + int64 current_tick = 3; +} \ No newline at end of file diff --git a/constants.go b/constants.go index 0061079..dcab734 100644 --- a/constants.go +++ b/constants.go @@ -9,4 +9,13 @@ const ( TileHeight = 2.0 TickRate = 600 * time.Millisecond // Server tick rate (600ms) serverAddr = "localhost:6969" + + // RuneScape-style tick rate (600ms) + ServerTickRate = 600 * time.Millisecond + + // Client might run at a higher tick rate for smooth rendering + ClientTickRate = 50 * time.Millisecond + + // Maximum number of ticks we can get behind before forcing a resync + MaxTickDesync = 5 ) diff --git a/go.mod b/go.mod index f4413b9..2e47712 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module goonscape +module gitea.boner.be/bdnugget/goonscape go 1.23.0 @@ -13,3 +13,5 @@ require ( golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect golang.org/x/sys v0.26.0 // indirect ) + +replace gitea.boner.be/bdnugget/goonserver => ./goonserver diff --git a/go.sum b/go.sum index 868728f..d8f8be3 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -gitea.boner.be/bdnugget/goonserver v0.0.0-20241011195320-f16e8647dc6b h1:hdhCZH0YGqCsnSl6ru+8I7rxvCyOj5pCtf92urwyruA= -gitea.boner.be/bdnugget/goonserver v0.0.0-20241011195320-f16e8647dc6b/go.mod h1:inR1bKrr/vcTba+G1KzmmY6vssMq9oGNOk836VwPa4c= github.com/ebitengine/purego v0.8.0 h1:JbqvnEzRvPpxhCJzJJ2y0RbiZ8nyjccVUrSM3q+GvvE= github.com/ebitengine/purego v0.8.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/gen2brain/raylib-go/raylib v0.0.0-20240930075631-c66f9e2942fe h1:mInjrbJkUglTM7tBmXG+epnPCE744aj15J7vjJwM4gs= diff --git a/goonserver b/goonserver index 1d6d3ab..f91f72c 160000 --- a/goonserver +++ b/goonserver @@ -1 +1 @@ -Subproject commit 1d6d3ab2eadf488d6ec0e5ba85005b3e57e372ea +Subproject commit f91f72c05d779b99c65b86abdfda205ef8e8f7c0 diff --git a/input.go b/input.go index 599c4fa..b8ff046 100644 --- a/input.go +++ b/input.go @@ -3,6 +3,7 @@ package main import ( "fmt" + pb "gitea.boner.be/bdnugget/goonserver/actions" rl "github.com/gen2brain/raylib-go/raylib" ) @@ -40,7 +41,12 @@ func HandleInput(player *Player, camera *rl.Camera) { // Exclude the first tile (current position) if len(path) > 1 { player.TargetPath = path[1:] - player.ActionQueue = append(player.ActionQueue, Action{Type: MoveAction, X: clickedTile.X, Y: clickedTile.Y}) + player.ActionQueue = append(player.ActionQueue, &pb.Action{ + Type: pb.Action_MOVE, + X: int32(clickedTile.X), + Y: int32(clickedTile.Y), + PlayerId: player.ID, + }) } } } diff --git a/network.go b/network.go index 46eb367..3bad96a 100644 --- a/network.go +++ b/network.go @@ -6,7 +6,6 @@ import ( "time" pb "gitea.boner.be/bdnugget/goonserver/actions" - rl "github.com/gen2brain/raylib-go/raylib" "google.golang.org/protobuf/proto" ) @@ -42,39 +41,51 @@ func ConnectToServer() (net.Conn, int32, error) { } func HandleServerCommunication(conn net.Conn, playerID int32, player *Player, otherPlayers map[int32]*Player) { + // Ticker for sending actions aligned with server ticks + ticker := time.NewTicker(ServerTickRate) + defer ticker.Stop() + // Goroutine to handle sending player's actions to the server go func() { - for { + for range ticker.C { if len(player.ActionQueue) > 0 { - // Process the first action in the queue - actionData := player.ActionQueue[0] - action := &pb.Action{ - PlayerId: playerID, - Type: pb.Action_MOVE, - X: int32(actionData.X), - Y: int32(actionData.Y), + // Bundle all actions that occurred during this tick + actions := make([]*pb.Action, 0, len(player.ActionQueue)) + + for _, actionData := range player.ActionQueue { + action := &pb.Action{ + PlayerId: playerID, + Type: pb.Action_MOVE, + X: int32(actionData.X), + Y: int32(actionData.Y), + } + actions = append(actions, action) } - // Serialize the action - data, err := proto.Marshal(action) + // Create a batch message + batch := &pb.ActionBatch{ + PlayerId: playerID, + Actions: actions, + Tick: player.CurrentTick, + } + + // Serialize the batch + data, err := proto.Marshal(batch) if err != nil { - log.Printf("Failed to marshal action: %v", err) + log.Printf("Failed to marshal action batch: %v", err) continue } - // Send action to server + // Send batch to server _, err = conn.Write(data) if err != nil { - log.Printf("Failed to send action to server: %v", err) + log.Printf("Failed to send actions to server: %v", err) return } - // Remove the action from the queue once it's sent - player.ActionQueue = player.ActionQueue[1:] + // Clear the action queue after sending + player.ActionQueue = player.ActionQueue[:0] } - - // Add a delay to match the server's tick rate - time.Sleep(TickRate) } }() @@ -93,27 +104,25 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *Player, ot continue } + // Check for tick synchronization + tickDiff := serverMessage.CurrentTick - player.CurrentTick + if tickDiff > MaxTickDesync { + log.Printf("Client too far behind (tick diff: %d), forcing resync", tickDiff) + player.ForceResync(serverMessage.Players[playerID]) + } + + // Update player's current tick + player.CurrentTick = serverMessage.CurrentTick + // Update other players' states for _, state := range serverMessage.Players { if state.PlayerId != playerID { if otherPlayer, exists := otherPlayers[state.PlayerId]; exists { - // Instead of directly setting position, set up path for smooth movement - targetTile := Tile{X: int(state.X), Y: int(state.Y)} - if otherPlayer.PosTile != targetTile { - otherPlayer.TargetPath = []Tile{targetTile} - } + // Calculate interpolation based on server tick + otherPlayer.UpdatePosition(state, ServerTickRate) } else { // Initialize new player - otherPlayers[state.PlayerId] = &Player{ - PosTile: Tile{X: int(state.X), Y: int(state.Y)}, - PosActual: rl.Vector3{ - X: float32(state.X * TileSize), - Y: float32(state.Y * TileHeight), - Z: float32(state.Y * TileSize), - }, - ID: state.PlayerId, - Speed: 50.0, // Make sure to set the speed for smooth movement - } + otherPlayers[state.PlayerId] = NewPlayer(state) } } } diff --git a/player.go b/player.go index aaf3e9f..7ccf945 100644 --- a/player.go +++ b/player.go @@ -1,6 +1,9 @@ package main import ( + "time" + + pb "gitea.boner.be/bdnugget/goonserver/actions" rl "github.com/gen2brain/raylib-go/raylib" ) @@ -61,3 +64,38 @@ func (player *Player) MoveTowards(target Tile, deltaTime float32) { } } } + +func (p *Player) UpdatePosition(state *pb.PlayerState, tickRate time.Duration) { + targetTile := Tile{X: int(state.X), Y: int(state.Y)} + if p.PosTile != targetTile { + p.PosTile = targetTile + p.LastUpdateTime = time.Now() + p.InterpolationProgress = 0 + p.TargetPath = []Tile{targetTile} + } +} + +func (p *Player) ForceResync(state *pb.PlayerState) { + p.PosTile = Tile{X: int(state.X), Y: int(state.Y)} + p.PosActual = rl.Vector3{ + X: float32(state.X * TileSize), + Y: float32(state.Y * TileHeight), + Z: float32(state.Y * TileSize), + } + p.TargetPath = nil + p.ActionQueue = nil + p.InterpolationProgress = 1.0 +} + +func NewPlayer(state *pb.PlayerState) *Player { + return &Player{ + PosActual: rl.Vector3{ + X: float32(state.X * TileSize), + Y: float32(state.Y * TileHeight), + Z: float32(state.Y * TileSize), + }, + PosTile: Tile{X: int(state.X), Y: int(state.Y)}, + Speed: 50.0, + ID: state.PlayerId, + } +} diff --git a/types.go b/types.go index 29eac1c..27ff261 100644 --- a/types.go +++ b/types.go @@ -1,6 +1,11 @@ package main -import rl "github.com/gen2brain/raylib-go/raylib" +import ( + pb "gitea.boner.be/bdnugget/goonserver/actions" + rl "github.com/gen2brain/raylib-go/raylib" + + time "time" +) type Tile struct { X, Y int @@ -8,24 +13,16 @@ type Tile struct { Walkable bool } -type ActionType int - -const ( - MoveAction ActionType = iota -) - -type Action struct { - Type ActionType - X, Y int // Target position for movement -} - type Player struct { - PosActual rl.Vector3 - PosTile Tile - TargetPath []Tile - Speed float32 - ActionQueue []Action // Queue for player actions - Model rl.Model - Texture rl.Texture2D - ID int32 + PosActual rl.Vector3 + PosTile Tile + TargetPath []Tile + ActionQueue []*pb.Action + Speed float32 + Model rl.Model + Texture rl.Texture2D + ID int32 + CurrentTick int64 + LastUpdateTime time.Time + InterpolationProgress float32 }