Closed imthatgin closed 3 months ago
Listing components is possible via Archetype. For example:
for _, c := range someEntry.Archetype().Layout().Components() {
switch c.Id() {
case components.Position.Id():
data := components.Position.Get(someEntry)
// do serialization
}
Is there a way to do this more generically? The use case: I'm adding a SyncComponent onto entities that need to be synced across the network, and then that sync component has an internal list of components to sync, so that SyncComponent specifies that (example) HealthComponent should be synced. I'm looking for a way to query the ECS for all this data, without necessarily knowing the concrete types ahead of time.
Thanks for the clarification. I think we can add new API, for example:
for _, c := syncComponents {
world.Each(c, func(data any) {
// process each data
})
}
What do you think?
That could work, as long as the type information is available somehow
Would you mind sharing why you need type information?
It's just a thought experiment, but there could be other approach if we don't want to deal with any
type.
type SyncAction struct{}
func handleSyncHealthHandler(action *SyncAction, healthData *components.HealthData) {
}
world.RegisterHandler(serializeHealth)
world.Dispatch(&SyncAction{}, component.Health) // Call handleSyncHealthHandler handler for all entities' health data
My use case is that I want to be able to just MakeSynced(entity, ...componentTypes []IComponentType)
and then it handles syncs for you, without having to implement anything specific to the entity-archetype
The handler / dispatch concept seems fairly powerful as a general feature, so maybe it should be implemented regardless!
I'm unsure as to the best way to model a networked ECS, so I'm happy to receive and consider your suggestions. I've looked at https://docs.rs/bevy_replicon/latest/bevy_replicon/ which lets you sync by simply marking an entity for replication.
I thought that the replicon feature for Beby might be a bit overwhelming to implement in Donburi right now. Below is an idea of how we will be able to handle serialization and deserialization with a new sync
package. What do you think?
// server
type Health struct {}
func (h Health) Serialize() ([]byte, error) {}
func DeserializeHealth(data []byte) (Health, error) {}
components.New(Health).SetDeserializer(DeserializeHealth)
donburi.Add(someEntry, sync.Serializable, sync.Config{
Components: donburi.Component[]{components.Health}
})
data, err := sync.Serialize(someEntry)
// client
err := sync.Sync(someEntry, data)
I thought that the replicon feature for Beby might be a bit overwhelming to implement in Donburi right now. Below is an idea of how we will be able to handle serialization and deserialization with a new
sync
package. What do you think?// server type Health struct {} func (h Health) Serialize() ([]byte, error) {} func DeserializeHealth(data []byte) (Health, error) {} components.New(Health).SetDeserializer(DeserializeHealth) donburi.Add(someEntry, sync.Serializable, sync.Config{ Components: donburi.Component[]{components.Health} }) data, err := sync.Serialize(someEntry) // client err := sync.Sync(someEntry, data)
I agree that donburi should not implement a sync feature, but a way to acquire the components and their data is needed for my library on top of donburi. I am not sure about this example, as it depends on the actual implementations.
Do you have some thoughts or ideas on the API design apart from above examples? Implementation wouldn't be too hard regardless the API design.
Would you mind sharing why you need type information?
It's just a thought experiment, but there could be other approach if we don't want to deal with
any
type.type SyncAction struct{} func handleSyncHealthHandler(action *SyncAction, healthData *components.HealthData) { } world.RegisterHandler(serializeHealth) world.Dispatch(&SyncAction{}, component.Health) // Call handleSyncHealthHandler handler for all entities' health data
This seems to be the most flexible, along with the world.Each idea, however I need to be able to access the Entity id in order to bundle the components that should be synced into a package to network.
@im-gin Right. Let's fix the parameter to something like this.
package dispatch
type Action[T any, U any] struct {
Entity donburi.Entity
Action *T,
Data *U
}
Usage:
type SyncAction struct{}
func handleSyncHealthHandler(action *dispatch.Action[SyncAction, *components.HealthData]) {
// ...
}
world.RegisterHandler(serializeHealth)
world.Dispatch(&SyncAction{}, component.Health)
@im-gin Right. Let's fix the parameter to something like this.
package dispatch type Action[T any, U any] struct { Entity donburi.Entity Action *T, Data *U }
Usage:
type SyncAction struct{} func handleSyncHealthHandler(action *dispatch.Action[SyncAction, *components.HealthData]) { // ... } world.RegisterHandler(serializeHealth) world.Dispatch(&SyncAction{}, component.Health)
I agree, let's try something like this, unless you have any better ideas!
I have no better idea than this right now. I'd very much welcome a contribution if anyone wants to work on it.
I have been looking at possible other ways to implement my ideal code pattern.
I think that if we had a way to use entry.Component() but have it return an interface{} | *WhateverInstanceOfData
it could work the way I need to. Is it possible to implement a complimentary method that uses ctype's typ field to fetch the actual object value, without needing the generic argument? This would allow me to fetch components at runtime without having the generic arguments in my library code.
Thanks! I think we can use a similar pattern to the one used in the database/sql
package. Specifically, if a component's data implements the Scanner
and Valuer
interfaces, we can use methods like entry.Value(components.Health)
and entry.Scan(components.Health, data)
to interact with the component's data. What do you think?
For reference, here are the relevant interfaces for database/sql
.
Valuer: https://pkg.go.dev/database/sql/driver#Valuer Scanner: https://pkg.go.dev/database/sql#Scanner
I was able to get my implementation to work as desired by adding this method to entry.go:
func GetComponents(e *Entry) []any {
archetypeIdx := e.loc.Archetype
s := e.World.StorageAccessor().Archetypes[archetypeIdx]
cs := s.ComponentTypes()
var instances []any
for _, ctyp := range cs {
instancePtr := e.Component(ctyp)
componentType := ctyp.Typ()
val := reflect.NewAt(componentType, instancePtr)
valInstance := reflect.Indirect(val).Interface()
instances = append(instances, valInstance)
}
return instances
}
(As well as implementing storage.Archetype.ComponentTypes()
(Layout().componentTypes) and ComponentType[T].Typ()
which returns the unexported fields.
Ah I see what you need to do now. Looks good to me.
I'm trying to build a world state packet for server-client ECS, and need to build a list of entities with their component data, and I'm looking for a way to do that, but donburi does not seem to have an API I can use to query arbitrary component data, or just to serialize an entity