From e860ee2dcfc1e50a76c65e457a323c51e435f6b9 Mon Sep 17 00:00:00 2001 From: damienfamed75 Date: Fri, 10 Jan 2025 15:23:55 -0500 Subject: [PATCH 01/12] update dependencies --- esync/srvsync/esync_server.go | 9 +++---- examples/client/client.go | 2 +- go.mod | 5 ++-- go.sum | 11 ++++----- router/network_client.go | 3 ++- router/router.go | 7 +++--- transports/ws_client_transport.go | 5 ++-- transports/ws_server_transport.go | 5 ++-- typemapper/typemapper.go | 39 ++++++++++++++++++++++++++++++- wrapws/client.go | 3 ++- wrapws/event_handler.go | 3 ++- wrapws/server.go | 3 ++- 12 files changed, 68 insertions(+), 27 deletions(-) diff --git a/esync/srvsync/esync_server.go b/esync/srvsync/esync_server.go index 25d366b..6489fd0 100644 --- a/esync/srvsync/esync_server.go +++ b/esync/srvsync/esync_server.go @@ -4,15 +4,16 @@ import ( "bytes" "context" "fmt" + "reflect" + "slices" + "sync" + "sync/atomic" + "github.com/leap-fish/necs/esync" "github.com/leap-fish/necs/router" "github.com/yohamta/donburi" "github.com/yohamta/donburi/component" "golang.org/x/sync/errgroup" - "reflect" - "slices" - "sync" - "sync/atomic" ) var NetworkIdCounter = atomic.Uint64{} diff --git a/examples/client/client.go b/examples/client/client.go index afe8eda..bf0eea9 100644 --- a/examples/client/client.go +++ b/examples/client/client.go @@ -3,10 +3,10 @@ package main import ( "log" + "github.com/coder/websocket" "github.com/leap-fish/necs/examples/shared" "github.com/leap-fish/necs/router" "github.com/leap-fish/necs/transports" - "nhooyr.io/websocket" ) func main() { diff --git a/go.mod b/go.mod index 2dbaa82..df7330b 100644 --- a/go.mod +++ b/go.mod @@ -3,17 +3,16 @@ module github.com/leap-fish/necs go 1.23.0 require ( - github.com/hashicorp/go-msgpack v1.1.6 + github.com/coder/websocket v1.8.12 + github.com/hashicorp/go-msgpack/v2 v2.1.2 github.com/stretchr/testify v1.9.0 github.com/yohamta/donburi v1.15.4 golang.org/x/sync v0.8.0 - nhooyr.io/websocket v1.8.17 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 8f3601a..3e53636 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,12 @@ +github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= +github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/hashicorp/go-msgpack v1.1.6 h1:ww1OX2NJCNixx9/GB+Kp5NrYVlB6cSxa06RGrvjOwbw= -github.com/hashicorp/go-msgpack v1.1.6/go.mod h1:iZfHWkHZuSrKnYYeBmJCgA/NttRmHeuccTEPp3rDGhM= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/hashicorp/go-msgpack/v2 v2.1.2 h1:4Ee8FTp834e+ewB71RDrQ0VKpyFdrKOjvYtnQ/ltVj0= +github.com/hashicorp/go-msgpack/v2 v2.1.2/go.mod h1:upybraOAblm4S7rx0+jeNy+CWWhzywQsSRV5033mMu4= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= @@ -26,5 +25,3 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -nhooyr.io/websocket v1.8.17 h1:KEVeLJkUywCKVsnLIDlD/5gtayKp8VoCkksHCGGfT9Y= -nhooyr.io/websocket v1.8.17/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= diff --git a/router/network_client.go b/router/network_client.go index 543c37f..a8d9975 100644 --- a/router/network_client.go +++ b/router/network_client.go @@ -3,7 +3,8 @@ package router import ( "context" "fmt" - "nhooyr.io/websocket" + + "github.com/coder/websocket" ) type NetworkClient struct { diff --git a/router/router.go b/router/router.go index ea49de4..cc555e5 100644 --- a/router/router.go +++ b/router/router.go @@ -5,11 +5,12 @@ import ( "crypto/rand" "errors" "fmt" - "github.com/leap-fish/necs/typeid" - "github.com/leap-fish/necs/typemapper" - "nhooyr.io/websocket" "reflect" "sync" + + "github.com/coder/websocket" + "github.com/leap-fish/necs/typeid" + "github.com/leap-fish/necs/typemapper" ) var ( diff --git a/transports/ws_client_transport.go b/transports/ws_client_transport.go index aced821..6792c14 100644 --- a/transports/ws_client_transport.go +++ b/transports/ws_client_transport.go @@ -2,11 +2,12 @@ package transports import ( "context" + "time" + + "github.com/coder/websocket" "github.com/leap-fish/necs/router" "github.com/leap-fish/necs/wrapws" "golang.org/x/sync/errgroup" - "nhooyr.io/websocket" - "time" ) type WsClientTransport struct { diff --git a/transports/ws_server_transport.go b/transports/ws_server_transport.go index 91d06fd..df9b117 100644 --- a/transports/ws_server_transport.go +++ b/transports/ws_server_transport.go @@ -3,10 +3,11 @@ package transports import ( "context" "fmt" + "time" + + "github.com/coder/websocket" "github.com/leap-fish/necs/router" "github.com/leap-fish/necs/wrapws" - "nhooyr.io/websocket" - "time" ) type WsServerTransport struct { diff --git a/typemapper/typemapper.go b/typemapper/typemapper.go index 9ca0ba6..f9d933a 100644 --- a/typemapper/typemapper.go +++ b/typemapper/typemapper.go @@ -3,9 +3,10 @@ package typemapper import ( "bytes" "fmt" - "github.com/hashicorp/go-msgpack/codec" "reflect" "sync" + + "github.com/hashicorp/go-msgpack/v2/codec" ) // TypeMapper is used to map between registered IDs and components and @@ -108,6 +109,22 @@ func (db *TypeMapper) Serialize(component any) ([]byte, error) { return nil, err } + // if customEncoder, ok := component.(EncodeDecoder); ok { + // encoded, err := customEncoder.Encode() + // if err != nil { + // return nil, err + // } + + // // if _, err := encodeBuf.Write(encoded); err != nil { + // // return nil, err + // // } + // if err := encoder.Encode(encoded); err != nil { + // return nil, err + // } + + // fmt.Printf("message: %#v\n", encodeBuf.Bytes()) + // } else { + // } if err := encoder.Encode(component); err != nil { return nil, err } @@ -117,6 +134,8 @@ func (db *TypeMapper) Serialize(component any) ([]byte, error) { // Deserialize a component by decoding its ID, and then the actual struct. func (db *TypeMapper) Deserialize(data []byte) (any, error) { + // buf := bytes.NewBuffer(data) + // decoder := codec.NewDecoder(buf, db.handle) decoder := codec.NewDecoderBytes(data, db.handle) var id uint @@ -130,6 +149,24 @@ func (db *TypeMapper) Deserialize(data []byte) (any, error) { } instanced := reflect.New(component).Interface() + // if customDecoder, ok := instanced.(EncodeDecoder); ok { + // var remaining []byte + // if err := decoder.Decode(remaining); err != nil { + // return nil, err + // } + // // remaining, err := io.ReadAll(buf) + // // if err != nil { + // // fmt.Printf("read all buf: %s\n", err) + // // return nil, err + // // } + + // if err := customDecoder.Decode(remaining); err != nil { + // return nil, err + // } + + // fmt.Printf("instance: %v\n", instanced) + // } else { + // } if err := decoder.Decode(instanced); err != nil { return nil, err } diff --git a/wrapws/client.go b/wrapws/client.go index ea5ef85..4c1857b 100644 --- a/wrapws/client.go +++ b/wrapws/client.go @@ -3,8 +3,9 @@ package wrapws import ( "context" "io" - "nhooyr.io/websocket" "time" + + "github.com/coder/websocket" ) type WebSocketClient struct { diff --git a/wrapws/event_handler.go b/wrapws/event_handler.go index 608f6eb..5663d7f 100644 --- a/wrapws/event_handler.go +++ b/wrapws/event_handler.go @@ -2,7 +2,8 @@ package wrapws import ( "context" - "nhooyr.io/websocket" + + "github.com/coder/websocket" ) type EventHandler interface { diff --git a/wrapws/server.go b/wrapws/server.go index 752857d..31120cd 100644 --- a/wrapws/server.go +++ b/wrapws/server.go @@ -4,8 +4,9 @@ import ( "context" "io" "net/http" - "nhooyr.io/websocket" "time" + + "github.com/coder/websocket" ) const maxMessageReadTime = time.Second * 30 From 11b0bfedaeeb08101c1b7f2b4be75707e89f67fc Mon Sep 17 00:00:00 2001 From: damienfamed75 Date: Fri, 10 Jan 2025 15:25:32 -0500 Subject: [PATCH 02/12] remove unused code --- typemapper/typemapper.go | 34 ---------------------------------- 1 file changed, 34 deletions(-) diff --git a/typemapper/typemapper.go b/typemapper/typemapper.go index f9d933a..8b38904 100644 --- a/typemapper/typemapper.go +++ b/typemapper/typemapper.go @@ -109,22 +109,6 @@ func (db *TypeMapper) Serialize(component any) ([]byte, error) { return nil, err } - // if customEncoder, ok := component.(EncodeDecoder); ok { - // encoded, err := customEncoder.Encode() - // if err != nil { - // return nil, err - // } - - // // if _, err := encodeBuf.Write(encoded); err != nil { - // // return nil, err - // // } - // if err := encoder.Encode(encoded); err != nil { - // return nil, err - // } - - // fmt.Printf("message: %#v\n", encodeBuf.Bytes()) - // } else { - // } if err := encoder.Encode(component); err != nil { return nil, err } @@ -149,24 +133,6 @@ func (db *TypeMapper) Deserialize(data []byte) (any, error) { } instanced := reflect.New(component).Interface() - // if customDecoder, ok := instanced.(EncodeDecoder); ok { - // var remaining []byte - // if err := decoder.Decode(remaining); err != nil { - // return nil, err - // } - // // remaining, err := io.ReadAll(buf) - // // if err != nil { - // // fmt.Printf("read all buf: %s\n", err) - // // return nil, err - // // } - - // if err := customDecoder.Decode(remaining); err != nil { - // return nil, err - // } - - // fmt.Printf("instance: %v\n", instanced) - // } else { - // } if err := decoder.Decode(instanced); err != nil { return nil, err } From 84e46f500ea3c0a2d2dbb3c5945478dbc8deabe0 Mon Sep 17 00:00:00 2001 From: damienfamed75 Date: Fri, 10 Jan 2025 15:28:00 -0500 Subject: [PATCH 03/12] more unused code --- typemapper/typemapper.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/typemapper/typemapper.go b/typemapper/typemapper.go index 8b38904..7961bda 100644 --- a/typemapper/typemapper.go +++ b/typemapper/typemapper.go @@ -118,8 +118,6 @@ func (db *TypeMapper) Serialize(component any) ([]byte, error) { // Deserialize a component by decoding its ID, and then the actual struct. func (db *TypeMapper) Deserialize(data []byte) (any, error) { - // buf := bytes.NewBuffer(data) - // decoder := codec.NewDecoder(buf, db.handle) decoder := codec.NewDecoderBytes(data, db.handle) var id uint From 6c6edee5296f45013b64944bf29af1a41d8f688e Mon Sep 17 00:00:00 2001 From: damienfamed75 Date: Sat, 25 Jan 2025 14:02:06 -0500 Subject: [PATCH 04/12] rough outline of client-side interpolation --- esync/clisync/esync_client.go | 53 ++++++++++++- esync/clisync/interpolation.go | 141 +++++++++++++++++++++++++++++++++ esync/esync_shared.go | 58 +++++++++++++- esync/srvsync/esync_server.go | 14 ++++ typemapper/componentmapper.go | 83 +++++++++++++++++++ 5 files changed, 344 insertions(+), 5 deletions(-) create mode 100644 esync/clisync/interpolation.go create mode 100644 typemapper/componentmapper.go diff --git a/esync/clisync/esync_client.go b/esync/clisync/esync_client.go index aaae617..ea356d7 100644 --- a/esync/clisync/esync_client.go +++ b/esync/clisync/esync_client.go @@ -2,10 +2,12 @@ package clisync import ( "fmt" + "reflect" + "time" + "github.com/leap-fish/necs/esync" "github.com/leap-fish/necs/router" "github.com/yohamta/donburi" - "reflect" ) func clientUpdateWorldState(world donburi.World, state esync.WorldSnapshot) error { @@ -26,7 +28,8 @@ func clientUpdateWorldState(world donburi.World, state esync.WorldSnapshot) erro } func applyEntityDiff(world donburi.World, networkId esync.NetworkId, components []any) { - var ctypes = make([]donburi.IComponentType, 0) + ctypes := make([]donburi.IComponentType, 0) + var interp *esync.InterpData for _, componentData := range components { componentType := reflect.TypeOf(componentData) @@ -35,8 +38,17 @@ func applyEntityDiff(world donburi.World, networkId esync.NetworkId, components // TODO: Add back erroring here continue } + + if componentType == reflect.TypeOf(&esync.InterpData{}) { + fmt.Printf("found interp data\n") + interp = any(ctype).(*esync.InterpData) + continue + } + ctypes = append(ctypes, ctype) + } + _ = interp // testing stuff out here entity := esync.FindByNetworkId(world, networkId) var entry *donburi.Entry @@ -45,6 +57,10 @@ func applyEntityDiff(world donburi.World, networkId esync.NetworkId, components } entry = world.Entry(entity) + now := time.Now() + + // calculate average latency and the delay index + calculateDelay(now) if entry != nil && world.Valid(entity) { for i := 0; i < len(components); i++ { @@ -52,11 +68,42 @@ func applyEntityDiff(world donburi.World, networkId esync.NetworkId, components if data == nil { panic("meow") } - entry.SetComponent(ctypes[i], esync.ComponentFromVal(ctypes[i], data)) + + ok := esync.RegisteredInterpType(reflect.TypeOf(data)) + if !ok || !entry.HasComponent(esync.InterpComponent) { + entry.SetComponent(ctypes[i], esync.ComponentFromVal(ctypes[i], data)) + continue + } + + key := esync.LookupId(reflect.TypeOf(data)) + + // Add the base value for this component if it doesn't have one + if !entry.HasComponent(ctypes[i]) { + entry.SetComponent(ctypes[i], ctypes[i].New()) + } + + if !entry.HasComponent(multiHistoryComponent) { + donburi.Add(entry, multiHistoryComponent, &multiHistoryData{ + history: make(map[uint][]componentTimeData), + }) + } + + multHistory := multiHistoryComponent.Get(entry) + multHistory.history[key] = append(multHistory.history[key], componentTimeData{ + value: data, + ts: now, + }) + + // Shift the positions if we've reached the limit + if len(multHistory.history[key]) > MaxHistorySize { + multHistory.history[key] = multHistory.history[key][1:] + } } } } +const MaxHistorySize = 32 + func RegisterClient(world donburi.World) { router.On[esync.WorldSnapshot](func(sender *router.NetworkClient, message esync.WorldSnapshot) { err := clientUpdateWorldState(world, message) diff --git a/esync/clisync/interpolation.go b/esync/clisync/interpolation.go new file mode 100644 index 0000000..4518243 --- /dev/null +++ b/esync/clisync/interpolation.go @@ -0,0 +1,141 @@ +package clisync + +import ( + "fmt" + "math" + "reflect" + "time" + "unsafe" + + "github.com/leap-fish/necs/esync" + "github.com/yohamta/donburi" + "github.com/yohamta/donburi/ecs" + "github.com/yohamta/donburi/filter" +) + +var ( + multiHistoryComponent = donburi.NewComponentType[multiHistoryData]() +) + +type InterpolationData struct { + Components []uint32 +} + +type componentTimeData struct { + value any + ts time.Time +} + +type multiHistoryData struct { + // Key is the component type + history map[uint][]componentTimeData +} + +func keyForType(typ reflect.Type) uint { + return esync.LookupId(typ) +} + +var ( + requests int64 + totalLatency float64 + avgLatency float64 + delay int + lastSnapshot time.Time = time.Now() +) + +func calculateDelay(now time.Time) { + requests++ + totalLatency += float64(time.Since(lastSnapshot)) + + avgLatency = totalLatency / float64(requests) + + delay = int(math.Floor(avgLatency / float64(time.Second))) + lastSnapshot = now +} + +func NewInterpolateSystem() ecs.System { + query := donburi.NewQuery(filter.Contains( + esync.NetworkIdComponent, + esync.InterpComponent, + multiHistoryComponent, + )) + + return func(ecs *ecs.ECS) { + now := time.Now() + + for e := range query.Iter(ecs.World) { + multiHistory := multiHistoryComponent.Get(e) + interpolated := esync.InterpComponent.Get(e) + + // fmt.Printf("keys: %#v\n", interpolated.ComponentKeys()) + for _, key := range interpolated.ComponentKeys() { + + compType := esync.LookupType(key) + // comp := (reflect.ValueOf(compType).Interface()).(donburi.IComponentType) + comp, ok := esync.Registered(compType) + if !ok { + panic(fmt.Sprintf("unregistered component %T", compType)) + } + + if !e.HasComponent(comp) { + continue + } + + var ( + prev, next, delayed *componentTimeData + ) + + buf := multiHistory.history[key] + if len(buf) <= 1 { + continue // to fix a rare panic we skip this + } + + for i := len(buf) - 1; i >= 0; i-- { + if buf[i].ts.Compare(now) <= 0 { + if len(buf) <= i { + continue + } + + prev = &buf[i] + + if i > 0 { + next = &buf[i-1] + break + } + } + } + // delayed should be our latest component value given our average + // latency delay (in seconds). + delayed = &buf[max(0, len(buf)-1-delay)] + + if prev == nil { + e.SetComponent(comp, unsafe.Pointer(&buf[0].value)) + continue + } + if next == nil { + e.SetComponent(comp, unsafe.Pointer(&buf[len(buf)-1].value)) + continue + } + + // Get the `t` value for our lerp function by getting the difference in + // our prev position and average it by our average latency's position + // compared to our next position. + t := float64(now.Sub(prev.ts)) / float64(delayed.ts.Sub(next.ts)) + + setter := esync.LookupSetter(key) + v := reflect.ValueOf(setter) + _ = v.Call([]reflect.Value{ + reflect.ValueOf(e), + reflect.ValueOf(next.value), + reflect.ValueOf(delayed.value), + reflect.ValueOf(t), + }) + + // gotVal := got[0] + // fmt.Printf("got: %v\n", gotVal) + + // e.SetComponent(comp.typ, unsafe.Pointer(&gotVal)) + } + } + } +} diff --git a/esync/esync_shared.go b/esync/esync_shared.go index b028a7e..506d360 100644 --- a/esync/esync_shared.go +++ b/esync/esync_shared.go @@ -1,13 +1,36 @@ package esync import ( + "reflect" + "unsafe" + "github.com/leap-fish/necs/typemapper" "github.com/yohamta/donburi" "github.com/yohamta/donburi/filter" - "reflect" - "unsafe" ) +var InterpComponent = donburi.NewComponentType[InterpData]() + +type InterpData struct { + Components []uint +} + +func NewInterpData(components ...donburi.IComponentType) *InterpData { + ids := []uint{} + for i := range components { + // typeof := reflect.TypeOf() + ids = append(ids, interpolated.LookupId(components[i].Typ())) + } + + return &InterpData{ + Components: ids, + } +} + +func (i *InterpData) ComponentKeys() []uint { + return i.Components +} + type ComponentId uint type NetworkId uint @@ -18,8 +41,11 @@ type SerializedEntity struct { } type WorldSnapshot []SerializedEntity +type LerpFn[T any] func(*donburi.Entry, T, T, float64) + var NetworkEntityQuery = donburi.NewQuery(filter.Contains(NetworkIdComponent)) +var interpolated = typemapper.NewComponentMapper() var registered = map[reflect.Type]donburi.IComponentType{} var ( @@ -27,6 +53,34 @@ var ( NetworkIdComponent = donburi.NewComponentType[NetworkId]() ) +func RegisterInterpolated[T any](id uint, comp *donburi.ComponentType[T], lerp ...LerpFn[T]) error { + if len(lerp) == 0 { + return interpolated.RegisterInterpolatedComponent(id, comp, nil) + } else { + return interpolated.RegisterInterpolatedComponent(id, comp, lerp[0]) + } +} + +func LookupId(typ reflect.Type) uint { + return interpolated.LookupId(typ) +} + +func LookupType(id uint) reflect.Type { + return interpolated.LookupType(id) +} + +func LookupSetter(id uint) any { + return interpolated.LookupSetter(id) +} + +func RegisteredInterp(id uint) bool { + return interpolated.RegisteredId(id) +} + +func RegisteredInterpType(typ reflect.Type) bool { + return interpolated.RegisteredType(typ) +} + func Registered(componentType reflect.Type) (donburi.IComponentType, bool) { ctype, ok := registered[componentType] return ctype, ok diff --git a/esync/srvsync/esync_server.go b/esync/srvsync/esync_server.go index 6489fd0..891b542 100644 --- a/esync/srvsync/esync_server.go +++ b/esync/srvsync/esync_server.go @@ -42,6 +42,20 @@ func AddNetworkFilter(filter func(client *router.NetworkClient, entry *donburi.E filterFuncs = append(filterFuncs, filter) } +// NetworkInterp sets this entity up for interpolation of the given component types. +// This does *not* a replacement for [srvsync.NetworkSync] and only specifies further +// detail as to how to handle these components. +func NetworkInterp(world donburi.World, entity *donburi.Entity, components ...donburi.IComponentType) { + entry := world.Entry(*entity) + entry.AddComponent(esync.InterpComponent) + esync.InterpComponent.Set(entry, esync.NewInterpData(components...)) + + syncEntMtx.Lock() + defer syncEntMtx.Unlock() + + syncEntities[*entity] = append(syncEntities[*entity], esync.InterpComponent) +} + // NetworkSync marks an entity and a list for network synchronization. // This means that the esync package will automatically try to send state updates to the connected clients. // Note that donburi tags are not supported for synchronization, as they contain no data. diff --git a/typemapper/componentmapper.go b/typemapper/componentmapper.go new file mode 100644 index 0000000..db95888 --- /dev/null +++ b/typemapper/componentmapper.go @@ -0,0 +1,83 @@ +package typemapper + +import ( + "reflect" + "sync" + + "github.com/yohamta/donburi" +) + +type interpolatedComponentData struct { + typ donburi.IComponentType + setter any +} + +type ComponentMapper struct { + lock sync.Mutex + + typeToId map[reflect.Type]uint + idToComponent map[uint]interpolatedComponentData +} + +func NewComponentMapper() *ComponentMapper { + return &ComponentMapper{ + typeToId: make(map[reflect.Type]uint), + idToComponent: make(map[uint]interpolatedComponentData), + } +} + +// RegisterInterpolatedComponent registers the given component and setter with +// the provided ID, note that these IDs don't interfere with the normal esync.Register +func (c *ComponentMapper) RegisterInterpolatedComponent(id uint, comp donburi.IComponentType, lerp any) error { + // if lerp == nil { + // return fmt.Errorf("invalid lerp function") + // } + + c.lock.Lock() + defer c.lock.Unlock() + + c.idToComponent[id] = interpolatedComponentData{ + typ: comp, + setter: lerp, + } + c.typeToId[comp.Typ()] = id + + return nil +} + +func (c *ComponentMapper) LookupSetter(id uint) any { + c.lock.Lock() + defer c.lock.Unlock() + + return c.idToComponent[id].setter +} + +func (c *ComponentMapper) RegisteredType(typ reflect.Type) bool { + c.lock.Lock() + defer c.lock.Unlock() + + _, ok := c.typeToId[typ] + return ok +} + +func (c *ComponentMapper) RegisteredId(id uint) bool { + c.lock.Lock() + defer c.lock.Unlock() + + _, ok := c.idToComponent[id] + return ok +} + +func (c *ComponentMapper) LookupType(id uint) reflect.Type { + c.lock.Lock() + defer c.lock.Unlock() + + return c.idToComponent[id].typ.Typ() +} + +func (c *ComponentMapper) LookupId(typ reflect.Type) uint { + c.lock.Lock() + defer c.lock.Unlock() + + return c.typeToId[typ] +} From 4595ab649090e2f47b5ae45c03353757f658dceb Mon Sep 17 00:00:00 2001 From: damienfamed75 Date: Sat, 25 Jan 2025 15:53:03 -0500 Subject: [PATCH 05/12] small bits of cleanup --- esync/clisync/esync_client.go | 16 ++++------------ esync/clisync/interpolation.go | 22 +++++++--------------- esync/esync_shared.go | 11 ++++++----- 3 files changed, 17 insertions(+), 32 deletions(-) diff --git a/esync/clisync/esync_client.go b/esync/clisync/esync_client.go index ea356d7..9a97f90 100644 --- a/esync/clisync/esync_client.go +++ b/esync/clisync/esync_client.go @@ -10,6 +10,8 @@ import ( "github.com/yohamta/donburi" ) +const MaxHistorySize = 32 + func clientUpdateWorldState(world donburi.World, state esync.WorldSnapshot) error { for _, ent := range state { var components []any @@ -29,7 +31,6 @@ func clientUpdateWorldState(world donburi.World, state esync.WorldSnapshot) erro func applyEntityDiff(world donburi.World, networkId esync.NetworkId, components []any) { ctypes := make([]donburi.IComponentType, 0) - var interp *esync.InterpData for _, componentData := range components { componentType := reflect.TypeOf(componentData) @@ -39,16 +40,8 @@ func applyEntityDiff(world donburi.World, networkId esync.NetworkId, components continue } - if componentType == reflect.TypeOf(&esync.InterpData{}) { - fmt.Printf("found interp data\n") - interp = any(ctype).(*esync.InterpData) - continue - } - ctypes = append(ctypes, ctype) - } - _ = interp // testing stuff out here entity := esync.FindByNetworkId(world, networkId) var entry *donburi.Entry @@ -75,7 +68,7 @@ func applyEntityDiff(world donburi.World, networkId esync.NetworkId, components continue } - key := esync.LookupId(reflect.TypeOf(data)) + key := esync.LookupInterpId(reflect.TypeOf(data)) // Add the base value for this component if it doesn't have one if !entry.HasComponent(ctypes[i]) { @@ -102,12 +95,11 @@ func applyEntityDiff(world donburi.World, networkId esync.NetworkId, components } } -const MaxHistorySize = 32 - func RegisterClient(world donburi.World) { router.On[esync.WorldSnapshot](func(sender *router.NetworkClient, message esync.WorldSnapshot) { err := clientUpdateWorldState(world, message) if err != nil { + panic(err) // TODO: Add back error handling here } diff --git a/esync/clisync/interpolation.go b/esync/clisync/interpolation.go index 4518243..602f52e 100644 --- a/esync/clisync/interpolation.go +++ b/esync/clisync/interpolation.go @@ -31,10 +31,6 @@ type multiHistoryData struct { history map[uint][]componentTimeData } -func keyForType(typ reflect.Type) uint { - return esync.LookupId(typ) -} - var ( requests int64 totalLatency float64 @@ -67,11 +63,8 @@ func NewInterpolateSystem() ecs.System { multiHistory := multiHistoryComponent.Get(e) interpolated := esync.InterpComponent.Get(e) - // fmt.Printf("keys: %#v\n", interpolated.ComponentKeys()) for _, key := range interpolated.ComponentKeys() { - - compType := esync.LookupType(key) - // comp := (reflect.ValueOf(compType).Interface()).(donburi.IComponentType) + compType := esync.LookupInterpType(key) comp, ok := esync.Registered(compType) if !ok { panic(fmt.Sprintf("unregistered component %T", compType)) @@ -122,19 +115,18 @@ func NewInterpolateSystem() ecs.System { // compared to our next position. t := float64(now.Sub(prev.ts)) / float64(delayed.ts.Sub(next.ts)) - setter := esync.LookupSetter(key) + setter := esync.LookupInterpSetter(key) v := reflect.ValueOf(setter) - _ = v.Call([]reflect.Value{ - reflect.ValueOf(e), + values := v.Call([]reflect.Value{ reflect.ValueOf(next.value), reflect.ValueOf(delayed.value), reflect.ValueOf(t), }) - // gotVal := got[0] - // fmt.Printf("got: %v\n", gotVal) - - // e.SetComponent(comp.typ, unsafe.Pointer(&gotVal)) + // Return value from the setter should be the interpolated value + // now set the component. + got := values[0].UnsafePointer() + e.SetComponent(comp, got) } } } diff --git a/esync/esync_shared.go b/esync/esync_shared.go index 506d360..475d677 100644 --- a/esync/esync_shared.go +++ b/esync/esync_shared.go @@ -41,7 +41,8 @@ type SerializedEntity struct { } type WorldSnapshot []SerializedEntity -type LerpFn[T any] func(*donburi.Entry, T, T, float64) +// type LerpFn[T any] func(entry *donburi.Entry, from T, to T, delta float64) +type LerpFn[T any] func(from T, to T, delta float64) *T var NetworkEntityQuery = donburi.NewQuery(filter.Contains(NetworkIdComponent)) @@ -61,19 +62,19 @@ func RegisterInterpolated[T any](id uint, comp *donburi.ComponentType[T], lerp . } } -func LookupId(typ reflect.Type) uint { +func LookupInterpId(typ reflect.Type) uint { return interpolated.LookupId(typ) } -func LookupType(id uint) reflect.Type { +func LookupInterpType(id uint) reflect.Type { return interpolated.LookupType(id) } -func LookupSetter(id uint) any { +func LookupInterpSetter(id uint) any { return interpolated.LookupSetter(id) } -func RegisteredInterp(id uint) bool { +func RegisteredInterpId(id uint) bool { return interpolated.RegisteredId(id) } From 1d5cc359564d0ff3e23df71cf918090e0195e64d Mon Sep 17 00:00:00 2001 From: damienfamed75 Date: Sat, 25 Jan 2025 17:42:07 -0500 Subject: [PATCH 06/12] reduce checks --- esync/clisync/esync_client.go | 23 +++++++++++++++-------- esync/clisync/interpolation.go | 34 ++++++++++++++++++++-------------- esync/esync_shared.go | 12 ++++-------- typemapper/componentmapper.go | 21 ++++++++++++++++++--- 4 files changed, 57 insertions(+), 33 deletions(-) diff --git a/esync/clisync/esync_client.go b/esync/clisync/esync_client.go index 9a97f90..4d1b917 100644 --- a/esync/clisync/esync_client.go +++ b/esync/clisync/esync_client.go @@ -31,8 +31,9 @@ func clientUpdateWorldState(world donburi.World, state esync.WorldSnapshot) erro func applyEntityDiff(world donburi.World, networkId esync.NetworkId, components []any) { ctypes := make([]donburi.IComponentType, 0) + refTypes := make([]reflect.Type, len(components)) - for _, componentData := range components { + for i, componentData := range components { componentType := reflect.TypeOf(componentData) ctype, ok := esync.Registered(componentType) if !ok { @@ -41,6 +42,7 @@ func applyEntityDiff(world donburi.World, networkId esync.NetworkId, components } ctypes = append(ctypes, ctype) + refTypes[i] = componentType } entity := esync.FindByNetworkId(world, networkId) @@ -56,32 +58,37 @@ func applyEntityDiff(world donburi.World, networkId esync.NetworkId, components calculateDelay(now) if entry != nil && world.Valid(entity) { + interpolated := entry.HasComponent(esync.InterpComponent) + for i := 0; i < len(components); i++ { data := components[i] if data == nil { panic("meow") } - ok := esync.RegisteredInterpType(reflect.TypeOf(data)) - if !ok || !entry.HasComponent(esync.InterpComponent) { + ok := esync.RegisteredInterpType(refTypes[i]) + if !ok || !interpolated { entry.SetComponent(ctypes[i], esync.ComponentFromVal(ctypes[i], data)) continue } - key := esync.LookupInterpId(reflect.TypeOf(data)) - + key := esync.LookupInterpId(refTypes[i]) // Add the base value for this component if it doesn't have one if !entry.HasComponent(ctypes[i]) { entry.SetComponent(ctypes[i], ctypes[i].New()) } - if !entry.HasComponent(multiHistoryComponent) { - donburi.Add(entry, multiHistoryComponent, &multiHistoryData{ + // Add a component cache to keep track of historic values for this + // interpolated component + if !entry.HasComponent(timeCacheComponent) { + donburi.Add(entry, timeCacheComponent, &timeCacheData{ history: make(map[uint][]componentTimeData), }) } - multHistory := multiHistoryComponent.Get(entry) + // Append the new value to our historic cache with its associated + // timestamp of when we received this + multHistory := timeCacheComponent.Get(entry) multHistory.history[key] = append(multHistory.history[key], componentTimeData{ value: data, ts: now, diff --git a/esync/clisync/interpolation.go b/esync/clisync/interpolation.go index 602f52e..06db020 100644 --- a/esync/clisync/interpolation.go +++ b/esync/clisync/interpolation.go @@ -14,62 +14,68 @@ import ( ) var ( - multiHistoryComponent = donburi.NewComponentType[multiHistoryData]() + timeCacheComponent = donburi.NewComponentType[timeCacheData]() ) -type InterpolationData struct { - Components []uint32 -} - type componentTimeData struct { value any ts time.Time } -type multiHistoryData struct { - // Key is the component type +// timeCacheData contains a map which the key matches an interpolation +// component key and contains a list of historic values for that type. +type timeCacheData struct { + // map[component key]historic values history map[uint][]componentTimeData } var ( requests int64 - totalLatency float64 + totalLatency int64 avgLatency float64 delay int lastSnapshot time.Time = time.Now() ) +// calculateDelay sets the delay which is an index based on the average latency in seconds func calculateDelay(now time.Time) { requests++ - totalLatency += float64(time.Since(lastSnapshot)) - avgLatency = totalLatency / float64(requests) + totalLatency += int64(time.Since(lastSnapshot)) + avgLatency = float64(totalLatency) / float64(requests) delay = int(math.Floor(avgLatency / float64(time.Second))) lastSnapshot = now } +// NewInterpolateSystem returns an ecs system that should be registered if you +// have any client-side interpolating components. func NewInterpolateSystem() ecs.System { query := donburi.NewQuery(filter.Contains( esync.NetworkIdComponent, esync.InterpComponent, - multiHistoryComponent, + timeCacheComponent, )) return func(ecs *ecs.ECS) { now := time.Now() for e := range query.Iter(ecs.World) { - multiHistory := multiHistoryComponent.Get(e) + if !e.Valid() { + continue + } + + multiHistory := timeCacheComponent.Get(e) interpolated := esync.InterpComponent.Get(e) + // Loop through each of this entry's interpolated components and + // interpolate them using their lerp functions. for _, key := range interpolated.ComponentKeys() { compType := esync.LookupInterpType(key) comp, ok := esync.Registered(compType) if !ok { panic(fmt.Sprintf("unregistered component %T", compType)) } - if !e.HasComponent(comp) { continue } @@ -78,11 +84,11 @@ func NewInterpolateSystem() ecs.System { prev, next, delayed *componentTimeData ) + // Get the historic buffer for this component type. buf := multiHistory.history[key] if len(buf) <= 1 { continue // to fix a rare panic we skip this } - for i := len(buf) - 1; i >= 0; i-- { if buf[i].ts.Compare(now) <= 0 { if len(buf) <= i { diff --git a/esync/esync_shared.go b/esync/esync_shared.go index 475d677..2b4ee59 100644 --- a/esync/esync_shared.go +++ b/esync/esync_shared.go @@ -18,7 +18,6 @@ type InterpData struct { func NewInterpData(components ...donburi.IComponentType) *InterpData { ids := []uint{} for i := range components { - // typeof := reflect.TypeOf() ids = append(ids, interpolated.LookupId(components[i].Typ())) } @@ -41,7 +40,7 @@ type SerializedEntity struct { } type WorldSnapshot []SerializedEntity -// type LerpFn[T any] func(entry *donburi.Entry, from T, to T, delta float64) +// LerpFn is used by the InterpolateSystem to properly lerp your component type LerpFn[T any] func(from T, to T, delta float64) *T var NetworkEntityQuery = donburi.NewQuery(filter.Contains(NetworkIdComponent)) @@ -54,12 +53,9 @@ var ( NetworkIdComponent = donburi.NewComponentType[NetworkId]() ) -func RegisterInterpolated[T any](id uint, comp *donburi.ComponentType[T], lerp ...LerpFn[T]) error { - if len(lerp) == 0 { - return interpolated.RegisterInterpolatedComponent(id, comp, nil) - } else { - return interpolated.RegisterInterpolatedComponent(id, comp, lerp[0]) - } +// RegisterInterpolated maps the component to the provided ID so the +func RegisterInterpolated[T any](id uint, comp *donburi.ComponentType[T], lerp LerpFn[T]) error { + return interpolated.RegisterInterpolatedComponent(id, comp, lerp) } func LookupInterpId(typ reflect.Type) uint { diff --git a/typemapper/componentmapper.go b/typemapper/componentmapper.go index db95888..d6e1736 100644 --- a/typemapper/componentmapper.go +++ b/typemapper/componentmapper.go @@ -1,12 +1,19 @@ package typemapper import ( + "errors" + "fmt" "reflect" "sync" "github.com/yohamta/donburi" ) +var ( + ErrNilLerpFunction = errors.New("lerp function nil") + ErrMalformedLerpFunction = errors.New("malformed lerp function") +) + type interpolatedComponentData struct { typ donburi.IComponentType setter any @@ -29,9 +36,17 @@ func NewComponentMapper() *ComponentMapper { // RegisterInterpolatedComponent registers the given component and setter with // the provided ID, note that these IDs don't interfere with the normal esync.Register func (c *ComponentMapper) RegisterInterpolatedComponent(id uint, comp donburi.IComponentType, lerp any) error { - // if lerp == nil { - // return fmt.Errorf("invalid lerp function") - // } + if lerp == nil { + return fmt.Errorf("must provide lerp function: %w", ErrNilLerpFunction) + } + + typ := reflect.TypeOf(lerp) + if typ.Kind() != reflect.Func { + return fmt.Errorf("lerp must be a function: %w", ErrMalformedLerpFunction) + } + if typ.NumIn() != 3 { + return fmt.Errorf("lerp function must have 3 arguments: %w", ErrMalformedLerpFunction) + } c.lock.Lock() defer c.lock.Unlock() From 89276e050db5bab87f98d373b5bf215fd84485d5 Mon Sep 17 00:00:00 2001 From: damienfamed75 Date: Sat, 25 Jan 2025 17:59:42 -0500 Subject: [PATCH 07/12] comments on added exported functions --- esync/esync_shared.go | 32 +++++++++++++++++++++++++++++++- esync/srvsync/esync_server.go | 2 ++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/esync/esync_shared.go b/esync/esync_shared.go index 2b4ee59..d4223f5 100644 --- a/esync/esync_shared.go +++ b/esync/esync_shared.go @@ -53,27 +53,57 @@ var ( NetworkIdComponent = donburi.NewComponentType[NetworkId]() ) -// RegisterInterpolated maps the component to the provided ID so the +// RegisterInterpolated creates a contract that the client and server understand +// by assigning it an ID that the client knows to interpolate your passed component type +// as well as how to interpolate the component values by passing the lerp function. +// +// For example you can provide the following for a basic Vector2: +// +// var PositionComponent = donburi.NewComponentType[Vector2]() +// +// type Vector2 struct { +// X, Y float64 +// } +// +// func lerp(a, b, t float64) float64 { +// return (1.0-t)*a + b*t +// } +// +// esync.RegisterInterpolated(1, PositionComponent, func(from, to Vector2, delta float64) *Vector2 { +// return &Vector2{ +// X: lerp(from.X, to.X, delta), +// T: lerp(from.Y, to.Y, delta), +// } +// }) func RegisterInterpolated[T any](id uint, comp *donburi.ComponentType[T], lerp LerpFn[T]) error { return interpolated.RegisterInterpolatedComponent(id, comp, lerp) } +// LookInterpId returns the interpolation ID for the given type, if not present +// then 0 is returned. func LookupInterpId(typ reflect.Type) uint { return interpolated.LookupId(typ) } +// LookupInterpType returns the component type for the given interpolation ID, +// if not present then an empty reflect.Type is returned. func LookupInterpType(id uint) reflect.Type { return interpolated.LookupType(id) } +// LookupInterpSetter returns the setter function for the given interpolation ID. +// This should always be type [esync.LerpFn] func LookupInterpSetter(id uint) any { return interpolated.LookupSetter(id) } +// RegisteredInterpId returns true if the given interpolation ID is registered. func RegisteredInterpId(id uint) bool { return interpolated.RegisteredId(id) } +// RegisteredInterpType returns true if the given component type is registered for +// interpolation. func RegisteredInterpType(typ reflect.Type) bool { return interpolated.RegisteredType(typ) } diff --git a/esync/srvsync/esync_server.go b/esync/srvsync/esync_server.go index 891b542..af850dd 100644 --- a/esync/srvsync/esync_server.go +++ b/esync/srvsync/esync_server.go @@ -45,6 +45,8 @@ func AddNetworkFilter(filter func(client *router.NetworkClient, entry *donburi.E // NetworkInterp sets this entity up for interpolation of the given component types. // This does *not* a replacement for [srvsync.NetworkSync] and only specifies further // detail as to how to handle these components. +// +// > The components passed must have been registered beforehand using [esync.RegisterInterpolated] func NetworkInterp(world donburi.World, entity *donburi.Entity, components ...donburi.IComponentType) { entry := world.Entry(*entity) entry.AddComponent(esync.InterpComponent) From cf10ad32d2c939f978d50823299513d037b6fed3 Mon Sep 17 00:00:00 2001 From: damienfamed75 Date: Sat, 25 Jan 2025 21:49:53 -0500 Subject: [PATCH 08/12] use uint8 for interp ids and add custom binary marshalers --- esync/clisync/esync_client.go | 2 +- esync/clisync/interpolation.go | 2 +- esync/esync_shared.go | 47 +++++++++++++++++++++++++++------- typemapper/componentmapper.go | 18 ++++++------- 4 files changed, 49 insertions(+), 20 deletions(-) diff --git a/esync/clisync/esync_client.go b/esync/clisync/esync_client.go index 4d1b917..6094c18 100644 --- a/esync/clisync/esync_client.go +++ b/esync/clisync/esync_client.go @@ -82,7 +82,7 @@ func applyEntityDiff(world donburi.World, networkId esync.NetworkId, components // interpolated component if !entry.HasComponent(timeCacheComponent) { donburi.Add(entry, timeCacheComponent, &timeCacheData{ - history: make(map[uint][]componentTimeData), + history: make(map[uint8][]componentTimeData), }) } diff --git a/esync/clisync/interpolation.go b/esync/clisync/interpolation.go index 06db020..ac89f48 100644 --- a/esync/clisync/interpolation.go +++ b/esync/clisync/interpolation.go @@ -26,7 +26,7 @@ type componentTimeData struct { // component key and contains a list of historic values for that type. type timeCacheData struct { // map[component key]historic values - history map[uint][]componentTimeData + history map[uint8][]componentTimeData } var ( diff --git a/esync/esync_shared.go b/esync/esync_shared.go index d4223f5..c245f20 100644 --- a/esync/esync_shared.go +++ b/esync/esync_shared.go @@ -1,6 +1,8 @@ package esync import ( + "bytes" + "encoding/binary" "reflect" "unsafe" @@ -12,13 +14,40 @@ import ( var InterpComponent = donburi.NewComponentType[InterpData]() type InterpData struct { - Components []uint + Components []uint8 +} + +func (id *InterpData) MarshalBinary() ([]byte, error) { + var buf bytes.Buffer + buf.Grow(len(id.Components)) + + for i := range id.Components { + binary.Write(&buf, binary.LittleEndian, id.Components[i]) + } + + return buf.Bytes(), nil +} + +func (id *InterpData) UnmarshalBinary(data []byte) error { + buf := bytes.NewReader(data) + + id.Components = make([]uint8, buf.Len()) + for i := 0; i < len(id.Components); i++ { + binary.Read(buf, binary.LittleEndian, &id.Components[i]) + } + + return nil } func NewInterpData(components ...donburi.IComponentType) *InterpData { - ids := []uint{} + ids := []uint8{} for i := range components { - ids = append(ids, interpolated.LookupId(components[i].Typ())) + key := interpolated.LookupId(components[i].Typ()) + if key == 0 { + continue + } + + ids = append(ids, key) } return &InterpData{ @@ -26,7 +55,7 @@ func NewInterpData(components ...donburi.IComponentType) *InterpData { } } -func (i *InterpData) ComponentKeys() []uint { +func (i *InterpData) ComponentKeys() []uint8 { return i.Components } @@ -75,30 +104,30 @@ var ( // T: lerp(from.Y, to.Y, delta), // } // }) -func RegisterInterpolated[T any](id uint, comp *donburi.ComponentType[T], lerp LerpFn[T]) error { +func RegisterInterpolated[T any](id uint8, comp *donburi.ComponentType[T], lerp LerpFn[T]) error { return interpolated.RegisterInterpolatedComponent(id, comp, lerp) } // LookInterpId returns the interpolation ID for the given type, if not present // then 0 is returned. -func LookupInterpId(typ reflect.Type) uint { +func LookupInterpId(typ reflect.Type) uint8 { return interpolated.LookupId(typ) } // LookupInterpType returns the component type for the given interpolation ID, // if not present then an empty reflect.Type is returned. -func LookupInterpType(id uint) reflect.Type { +func LookupInterpType(id uint8) reflect.Type { return interpolated.LookupType(id) } // LookupInterpSetter returns the setter function for the given interpolation ID. // This should always be type [esync.LerpFn] -func LookupInterpSetter(id uint) any { +func LookupInterpSetter(id uint8) any { return interpolated.LookupSetter(id) } // RegisteredInterpId returns true if the given interpolation ID is registered. -func RegisteredInterpId(id uint) bool { +func RegisteredInterpId(id uint8) bool { return interpolated.RegisteredId(id) } diff --git a/typemapper/componentmapper.go b/typemapper/componentmapper.go index d6e1736..68b7ac2 100644 --- a/typemapper/componentmapper.go +++ b/typemapper/componentmapper.go @@ -22,20 +22,20 @@ type interpolatedComponentData struct { type ComponentMapper struct { lock sync.Mutex - typeToId map[reflect.Type]uint - idToComponent map[uint]interpolatedComponentData + typeToId map[reflect.Type]uint8 + idToComponent map[uint8]interpolatedComponentData } func NewComponentMapper() *ComponentMapper { return &ComponentMapper{ - typeToId: make(map[reflect.Type]uint), - idToComponent: make(map[uint]interpolatedComponentData), + typeToId: make(map[reflect.Type]uint8), + idToComponent: make(map[uint8]interpolatedComponentData), } } // RegisterInterpolatedComponent registers the given component and setter with // the provided ID, note that these IDs don't interfere with the normal esync.Register -func (c *ComponentMapper) RegisterInterpolatedComponent(id uint, comp donburi.IComponentType, lerp any) error { +func (c *ComponentMapper) RegisterInterpolatedComponent(id uint8, comp donburi.IComponentType, lerp any) error { if lerp == nil { return fmt.Errorf("must provide lerp function: %w", ErrNilLerpFunction) } @@ -60,7 +60,7 @@ func (c *ComponentMapper) RegisterInterpolatedComponent(id uint, comp donburi.IC return nil } -func (c *ComponentMapper) LookupSetter(id uint) any { +func (c *ComponentMapper) LookupSetter(id uint8) any { c.lock.Lock() defer c.lock.Unlock() @@ -75,7 +75,7 @@ func (c *ComponentMapper) RegisteredType(typ reflect.Type) bool { return ok } -func (c *ComponentMapper) RegisteredId(id uint) bool { +func (c *ComponentMapper) RegisteredId(id uint8) bool { c.lock.Lock() defer c.lock.Unlock() @@ -83,14 +83,14 @@ func (c *ComponentMapper) RegisteredId(id uint) bool { return ok } -func (c *ComponentMapper) LookupType(id uint) reflect.Type { +func (c *ComponentMapper) LookupType(id uint8) reflect.Type { c.lock.Lock() defer c.lock.Unlock() return c.idToComponent[id].typ.Typ() } -func (c *ComponentMapper) LookupId(typ reflect.Type) uint { +func (c *ComponentMapper) LookupId(typ reflect.Type) uint8 { c.lock.Lock() defer c.lock.Unlock() From 53d8d7612ba6ada891f4c97857ce980e00e47103 Mon Sep 17 00:00:00 2001 From: damienfamed75 Date: Mon, 27 Jan 2025 00:23:23 -0500 Subject: [PATCH 09/12] change historic map to array --- esync/clisync/esync_client.go | 4 +--- esync/clisync/interpolation.go | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/esync/clisync/esync_client.go b/esync/clisync/esync_client.go index 6094c18..8412217 100644 --- a/esync/clisync/esync_client.go +++ b/esync/clisync/esync_client.go @@ -81,9 +81,7 @@ func applyEntityDiff(world donburi.World, networkId esync.NetworkId, components // Add a component cache to keep track of historic values for this // interpolated component if !entry.HasComponent(timeCacheComponent) { - donburi.Add(entry, timeCacheComponent, &timeCacheData{ - history: make(map[uint8][]componentTimeData), - }) + donburi.Add(entry, timeCacheComponent, &timeCacheData{}) } // Append the new value to our historic cache with its associated diff --git a/esync/clisync/interpolation.go b/esync/clisync/interpolation.go index ac89f48..f8cafd2 100644 --- a/esync/clisync/interpolation.go +++ b/esync/clisync/interpolation.go @@ -26,7 +26,7 @@ type componentTimeData struct { // component key and contains a list of historic values for that type. type timeCacheData struct { // map[component key]historic values - history map[uint8][]componentTimeData + history [math.MaxUint8][]componentTimeData } var ( From b3ff91c54100301fb8edbff4e5fc495e3185b915 Mon Sep 17 00:00:00 2001 From: damienfamed75 Date: Thu, 30 Jan 2025 15:05:02 -0500 Subject: [PATCH 10/12] integrate cleaner with current api --- esync/esync_shared.go | 64 ++++++++++++++++++++--------------- esync/srvsync/esync_server.go | 47 ++++++++++++++++--------- 2 files changed, 68 insertions(+), 43 deletions(-) diff --git a/esync/esync_shared.go b/esync/esync_shared.go index c245f20..e87ca15 100644 --- a/esync/esync_shared.go +++ b/esync/esync_shared.go @@ -82,32 +82,6 @@ var ( NetworkIdComponent = donburi.NewComponentType[NetworkId]() ) -// RegisterInterpolated creates a contract that the client and server understand -// by assigning it an ID that the client knows to interpolate your passed component type -// as well as how to interpolate the component values by passing the lerp function. -// -// For example you can provide the following for a basic Vector2: -// -// var PositionComponent = donburi.NewComponentType[Vector2]() -// -// type Vector2 struct { -// X, Y float64 -// } -// -// func lerp(a, b, t float64) float64 { -// return (1.0-t)*a + b*t -// } -// -// esync.RegisterInterpolated(1, PositionComponent, func(from, to Vector2, delta float64) *Vector2 { -// return &Vector2{ -// X: lerp(from.X, to.X, delta), -// T: lerp(from.Y, to.Y, delta), -// } -// }) -func RegisterInterpolated[T any](id uint8, comp *donburi.ComponentType[T], lerp LerpFn[T]) error { - return interpolated.RegisterInterpolatedComponent(id, comp, lerp) -} - // LookInterpId returns the interpolation ID for the given type, if not present // then 0 is returned. func LookupInterpId(typ reflect.Type) uint8 { @@ -142,9 +116,40 @@ func Registered(componentType reflect.Type) (donburi.IComponentType, bool) { return ctype, ok } +type RegisterOption[T any] func(*donburi.ComponentType[T]) + +// WithInterpFn will utilize the given lerp function for client-side interpolation +// when registering with a component. +// +// For example you can provide the following for a basic Position Component: +// +// var PositionComponent = donburi.NewComponentType[Vector2]() +// +// type Vector2 struct { +// X, Y float64 +// } +// +// func lerp(a, b, t float64) float64 { +// return (1.0-t)*a + b*t +// } +// +// func lerpVec2(from, to Vector2, delta float64) *Vector2 { +// return &Vector2{ +// X: lerp(from.X, to.X, delta), +// T: lerp(from.Y, to.Y, delta), +// } +// } +// +// esync.RegisterComponent(10, Vector2{}, PositionComponent, esync.WithInterpFn(10, lerpVec2)) +func WithInterpFn[T any](id uint8, fn LerpFn[T]) RegisterOption[T] { + return func(ctype *donburi.ComponentType[T]) { + interpolated.RegisterInterpolatedComponent(id, ctype, fn) + } +} + // RegisterComponent registers a component for use with esync. Make sure the client and server have the same definition of components. // Note that ID 1 is reserved for the NetworkId component used by esync. -func RegisterComponent(id uint, component any, ctype donburi.IComponentType) error { +func RegisterComponent[T any](id uint, component any, ctype *donburi.ComponentType[T], opt ...RegisterOption[T]) error { typ := reflect.TypeOf(component) err := Mapper.RegisterType(id, typ) if err != nil { @@ -152,6 +157,11 @@ func RegisterComponent(id uint, component any, ctype donburi.IComponentType) err } registered[typ] = ctype + // Call the options + for _, o := range opt { + o(ctype) + } + return nil } diff --git a/esync/srvsync/esync_server.go b/esync/srvsync/esync_server.go index af850dd..1d9ad96 100644 --- a/esync/srvsync/esync_server.go +++ b/esync/srvsync/esync_server.go @@ -42,27 +42,33 @@ func AddNetworkFilter(filter func(client *router.NetworkClient, entry *donburi.E filterFuncs = append(filterFuncs, filter) } -// NetworkInterp sets this entity up for interpolation of the given component types. -// This does *not* a replacement for [srvsync.NetworkSync] and only specifies further -// detail as to how to handle these components. +// SyncOption acts as an optional function parameter for [NetworkSync] +type SyncOption func(*donburi.Entity) []donburi.IComponentType + +// WithInterp passes the following components along to the belonging NetworkSync +// function as well as specifying that the given components are to be interpolated +// on the client-side. // -// > The components passed must have been registered beforehand using [esync.RegisterInterpolated] -func NetworkInterp(world donburi.World, entity *donburi.Entity, components ...donburi.IComponentType) { - entry := world.Entry(*entity) - entry.AddComponent(esync.InterpComponent) - esync.InterpComponent.Set(entry, esync.NewInterpData(components...)) +// It is assumed that these components have been registered beforehand using +// [esync.RegisterInterpolated]. +func WithInterp(components ...donburi.IComponentType) SyncOption { + return func(entity *donburi.Entity) []donburi.IComponentType { + entry := world.Entry(*entity) + if !entry.HasComponent(esync.InterpComponent) { + entry.AddComponent(esync.InterpComponent) + } - syncEntMtx.Lock() - defer syncEntMtx.Unlock() + esync.InterpComponent.Set(entry, esync.NewInterpData(components...)) - syncEntities[*entity] = append(syncEntities[*entity], esync.InterpComponent) + return append([]donburi.IComponentType{esync.InterpComponent}, components...) + } } // NetworkSync marks an entity and a list for network synchronization. // This means that the esync package will automatically try to send state updates to the connected clients. // Note that donburi tags are not supported for synchronization, as they contain no data. // This will return an error if the entity does not have all the components being synced. -func NetworkSync(world donburi.World, entity *donburi.Entity, components ...donburi.IComponentType) error { +func NetworkSync(world donburi.World, entity *donburi.Entity, components ...any) error { // Increments the Network ID counter to prevent reusing the ids NetworkIdCounter.Add(1) @@ -72,17 +78,26 @@ func NetworkSync(world donburi.World, entity *donburi.Entity, components ...donb entry.AddComponent(esync.NetworkIdComponent) esync.NetworkIdComponent.SetValue(entry, esync.NetworkId(networkId)) + // Create a list of components to sync + var foundComponents []donburi.IComponentType for _, listComponent := range components { - if !entry.HasComponent(listComponent) { - return fmt.Errorf("entity %d does not have the component %s", entry.Id(), listComponent.Name()) + if opt, ok := listComponent.(SyncOption); ok { + foundComponents = append(foundComponents, opt(entity)...) + } + + if comp, ok := listComponent.(donburi.IComponentType); ok { + if !entry.HasComponent(comp) { + return fmt.Errorf("entity %d does not have the component %s", entry.Id(), comp.Name()) + } + foundComponents = append(foundComponents, comp) } } - components = append(components, esync.NetworkIdComponent) + foundComponents = append(foundComponents, esync.NetworkIdComponent) syncEntMtx.Lock() defer syncEntMtx.Unlock() - syncEntities[*entity] = components + syncEntities[*entity] = foundComponents return nil } From 065b2250ce33a8461bc2a63fc3ada93f5e3d0364 Mon Sep 17 00:00:00 2001 From: damienfamed75 Date: Thu, 30 Jan 2025 15:35:20 -0500 Subject: [PATCH 11/12] update comments for clarity --- esync/esync_shared.go | 3 +++ esync/srvsync/esync_server.go | 12 ++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/esync/esync_shared.go b/esync/esync_shared.go index e87ca15..cd79d25 100644 --- a/esync/esync_shared.go +++ b/esync/esync_shared.go @@ -149,6 +149,9 @@ func WithInterpFn[T any](id uint8, fn LerpFn[T]) RegisterOption[T] { // RegisterComponent registers a component for use with esync. Make sure the client and server have the same definition of components. // Note that ID 1 is reserved for the NetworkId component used by esync. +// +// Optionally you may provide an optional [WithInterpFn] to register this component +// for interpolation. func RegisterComponent[T any](id uint, component any, ctype *donburi.ComponentType[T], opt ...RegisterOption[T]) error { typ := reflect.TypeOf(component) err := Mapper.RegisterType(id, typ) diff --git a/esync/srvsync/esync_server.go b/esync/srvsync/esync_server.go index 1d9ad96..4e991b4 100644 --- a/esync/srvsync/esync_server.go +++ b/esync/srvsync/esync_server.go @@ -50,7 +50,7 @@ type SyncOption func(*donburi.Entity) []donburi.IComponentType // on the client-side. // // It is assumed that these components have been registered beforehand using -// [esync.RegisterInterpolated]. +// [esync.RegisterComponent] and [esync.WithInterpFn]. func WithInterp(components ...donburi.IComponentType) SyncOption { return func(entity *donburi.Entity) []donburi.IComponentType { entry := world.Entry(*entity) @@ -64,10 +64,18 @@ func WithInterp(components ...donburi.IComponentType) SyncOption { } } -// NetworkSync marks an entity and a list for network synchronization. +// NetworkSync marks an entity and a list of components for network synchronization. // This means that the esync package will automatically try to send state updates to the connected clients. +// // Note that donburi tags are not supported for synchronization, as they contain no data. // This will return an error if the entity does not have all the components being synced. +// +// Optionally you may provide [WithInterp] with a list of components as well to mark +// those components for interpolation as well as network synchronization. This assumes +// that these components have already been registered for interpolation beforehand +// using [esync.RegisterComponent] and [esync.WithInterpFn]. +// +// > Components that are passed using [WithInterp] do not need to be passed again. func NetworkSync(world donburi.World, entity *donburi.Entity, components ...any) error { // Increments the Network ID counter to prevent reusing the ids NetworkIdCounter.Add(1) From 8d4c819e4a5e3dc98f129d1eb0d9bedf631207d4 Mon Sep 17 00:00:00 2001 From: damienfamed75 Date: Thu, 30 Jan 2025 16:58:40 -0500 Subject: [PATCH 12/12] change id to component map to array --- esync/clisync/interpolation.go | 3 +-- esync/esync_shared.go | 2 +- typemapper/componentmapper.go | 43 ++++++++++++++-------------------- 3 files changed, 19 insertions(+), 29 deletions(-) diff --git a/esync/clisync/interpolation.go b/esync/clisync/interpolation.go index f8cafd2..3929099 100644 --- a/esync/clisync/interpolation.go +++ b/esync/clisync/interpolation.go @@ -122,8 +122,7 @@ func NewInterpolateSystem() ecs.System { t := float64(now.Sub(prev.ts)) / float64(delayed.ts.Sub(next.ts)) setter := esync.LookupInterpSetter(key) - v := reflect.ValueOf(setter) - values := v.Call([]reflect.Value{ + values := setter.Call([]reflect.Value{ reflect.ValueOf(next.value), reflect.ValueOf(delayed.value), reflect.ValueOf(t), diff --git a/esync/esync_shared.go b/esync/esync_shared.go index cd79d25..adb5169 100644 --- a/esync/esync_shared.go +++ b/esync/esync_shared.go @@ -96,7 +96,7 @@ func LookupInterpType(id uint8) reflect.Type { // LookupInterpSetter returns the setter function for the given interpolation ID. // This should always be type [esync.LerpFn] -func LookupInterpSetter(id uint8) any { +func LookupInterpSetter(id uint8) reflect.Value { return interpolated.LookupSetter(id) } diff --git a/typemapper/componentmapper.go b/typemapper/componentmapper.go index 68b7ac2..566b5c3 100644 --- a/typemapper/componentmapper.go +++ b/typemapper/componentmapper.go @@ -3,6 +3,7 @@ package typemapper import ( "errors" "fmt" + "math" "reflect" "sync" @@ -16,20 +17,19 @@ var ( type interpolatedComponentData struct { typ donburi.IComponentType - setter any + setter reflect.Value } type ComponentMapper struct { - lock sync.Mutex + mutex sync.Mutex typeToId map[reflect.Type]uint8 - idToComponent map[uint8]interpolatedComponentData + idToComponent [math.MaxUint8]*interpolatedComponentData } func NewComponentMapper() *ComponentMapper { return &ComponentMapper{ - typeToId: make(map[reflect.Type]uint8), - idToComponent: make(map[uint8]interpolatedComponentData), + typeToId: make(map[reflect.Type]uint8), } } @@ -48,51 +48,42 @@ func (c *ComponentMapper) RegisterInterpolatedComponent(id uint8, comp donburi.I return fmt.Errorf("lerp function must have 3 arguments: %w", ErrMalformedLerpFunction) } - c.lock.Lock() - defer c.lock.Unlock() - - c.idToComponent[id] = interpolatedComponentData{ + c.idToComponent[id] = &interpolatedComponentData{ typ: comp, - setter: lerp, + setter: reflect.ValueOf(lerp), } + + c.mutex.Lock() + defer c.mutex.Unlock() + c.typeToId[comp.Typ()] = id return nil } -func (c *ComponentMapper) LookupSetter(id uint8) any { - c.lock.Lock() - defer c.lock.Unlock() - +func (c *ComponentMapper) LookupSetter(id uint8) reflect.Value { return c.idToComponent[id].setter } func (c *ComponentMapper) RegisteredType(typ reflect.Type) bool { - c.lock.Lock() - defer c.lock.Unlock() + c.mutex.Lock() + defer c.mutex.Unlock() _, ok := c.typeToId[typ] return ok } func (c *ComponentMapper) RegisteredId(id uint8) bool { - c.lock.Lock() - defer c.lock.Unlock() - - _, ok := c.idToComponent[id] - return ok + return c.idToComponent[id] != nil } func (c *ComponentMapper) LookupType(id uint8) reflect.Type { - c.lock.Lock() - defer c.lock.Unlock() - return c.idToComponent[id].typ.Typ() } func (c *ComponentMapper) LookupId(typ reflect.Type) uint8 { - c.lock.Lock() - defer c.lock.Unlock() + c.mutex.Lock() + defer c.mutex.Unlock() return c.typeToId[typ] }