yohamta / donburi

Just another ECS library for Go/Ebitengine
https://pkg.go.dev/github.com/yohamta/donburi
Other
232 stars 21 forks source link

Getting a list of entity's components #125

Closed imthatgin closed 3 months ago

imthatgin commented 3 months ago

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

yohamta commented 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
}
imthatgin commented 3 months ago

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.

yohamta commented 3 months ago

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?

imthatgin commented 3 months ago

That could work, as long as the type information is available somehow

yohamta commented 3 months ago

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
imthatgin commented 3 months ago

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

imthatgin commented 3 months ago

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.

yohamta commented 3 months ago

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)
imthatgin commented 3 months ago

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.

yohamta commented 3 months ago

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.

imthatgin commented 3 months ago

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.

yohamta commented 3 months ago

@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)
imthatgin commented 3 months ago

@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!

yohamta commented 3 months ago

I have no better idea than this right now. I'd very much welcome a contribution if anyone wants to work on it.

imthatgin commented 3 months ago

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.

yohamta commented 3 months ago

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

imthatgin commented 3 months ago

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.

yohamta commented 3 months ago

Ah I see what you need to do now. Looks good to me.