Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Getting a list of entity's components #125

Closed
imthatgin opened this issue Mar 12, 2024 · 18 comments · Fixed by #128
Closed

Getting a list of entity's components #125

imthatgin opened this issue Mar 12, 2024 · 18 comments · Fixed by #128
Labels
enhancement New feature or request

Comments

@imthatgin
Copy link
Contributor

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
Copy link
Owner

yohamta commented Mar 12, 2024

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
Copy link
Contributor Author

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
Copy link
Owner

yohamta commented Mar 12, 2024

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
Copy link
Contributor Author

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

@yohamta
Copy link
Owner

yohamta commented Mar 12, 2024

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
Copy link
Contributor Author

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
Copy link
Contributor Author

imthatgin commented Mar 12, 2024

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
Copy link
Owner

yohamta commented Mar 13, 2024

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
Copy link
Contributor Author

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
Copy link
Owner

yohamta commented Mar 13, 2024

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
Copy link
Contributor Author

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
Copy link
Owner

yohamta commented Mar 18, 2024

@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
Copy link
Contributor Author

@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 yohamta added the enhancement New feature or request label Mar 22, 2024
@yohamta
Copy link
Owner

yohamta commented Mar 22, 2024

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

@imthatgin
Copy link
Contributor Author

imthatgin commented Mar 22, 2024

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
Copy link
Owner

yohamta commented Mar 22, 2024

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
Copy link
Contributor Author

imthatgin commented Mar 22, 2024

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
Copy link
Owner

yohamta commented Mar 22, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants