Quillraven / Fleks

Fast, lightweight, multi-platform entity component system in Kotlin
MIT License
174 stars 19 forks source link

Support for Tags / empty components #118

Closed LobbyDivinus closed 11 months ago

LobbyDivinus commented 11 months ago

Hi, I am not sure in which regard these fit into a light weight ECS after all, but they are a great feature to "tag" entities without using the memory a traditional component incurrs.

What are tags?

Basically empty components that could be defined using Kotlin's objects. There can only be one instance per Tag type and all the ECS has to store about it is whether a given entity has said tag. Implementation wise this would work like the ComponentMapper but with an internal bit array instead of a full blown component array (comparable to how ComponentMappers worked before the recent change).

Motivation

The advantage over using components for tagging (we could reuse a single object to share at least that part): Right now each component mapper will hold on array of up to size N for N being the max ever used enttiy id. Let's say that takes 8 bytes per array entry leading to a size of 8N bytes. A tag mapper implemented by a bit array would only use 0.125N bytes (that's a factor of 64).

Let's assume N = 1024 and 10 components, of which 5 could be replaced by simple tags. N = 1024 with Size
10 components 10 * 8 * 1024 bytes = 80 kb
5 components + 5 tags 5 * 8 * 1024 bytes + 5 * 0.125 * 1024bytes = 40 kb + 640 bytes

I omitted some details like that regular components will use extra memory for each instance since that's not part of the consideration here.

How it could be done

It could be implemented along side component mappers. I am not sure if it would make sense to use a common interface for component and tag types to allow to use them interchangeably in family definitions.

Quillraven commented 11 months ago

I had a similar idea a couple of months ago and just recently I tried to add it together with the new entity version solution that we now have to safely reference entities in components (2.5-SNAPSHOT).

It might be easier now to implement tags because entities are now a data class instead of a value class. It might be as easy as adding a BitArray of tags to the entity class (and of course the cleanup when the entity gets removed by clearing the BitArray). Also, imo a tag can be simply represented as a constant UInt meaning you can use e.g. an enum for it. No need for Fleks to assing an id to a tag, like we do it for components. Something like:

enum class MyTags {
    PLAYER_CONTROLLED,
    HAS_COLLISION,
}

entity.tag[PLAYER_CONTROLLED] = true
entity.tag[HAS_COLLISION] = false

// and in Fleks we'd need something like
operator fun set(tag : Enum, value: Boolean) = tagBitArray[tag.ordinal()] = value

However, I did a test locally and tried to integrate the component mask directly into the entity which had a big negative impact on the performance. I didn't understand why but it could be similar for tags. If so then an approach similar to the component mask of an entity is better.

As you mentioned: I see a benefit for it because creating components without data is a little bit tidious and "overwhelming" for a simple "tagging" functionality. It works but I think tags would be easier.

I also have use-cases in my examples projects where I "tag" e.g. entities that are player controlled or which have a collision.


With that being said, I, unfortunately don't have a lot of time at the moment to work on features that require little bit of brain power and time ;) I might be able to do it in december during x-mas holiday.

But as always, everyone is welcome to create a PR. I definitely have the time to review it!

LobbyDivinus commented 11 months ago

Thanks for the consideration, that's what I suspected. Would that solution with a bit field in the entity class still allow for fast iteration by using tags as filters? That'd be my main usecase for them.

Quillraven commented 11 months ago

Ah you mean you still want to use it to define a family of an IteratingSystem?

LobbyDivinus commented 11 months ago

Ideally yes. E.g. you could put a "player" tag on one entity and then use a family of all entities that have a mesh component and the player tag. In this scenario the family would only contain a single entry.

Either way, I see that it's complicated to incorporate into the current API. The easiest way would be to put a flag into ComponentType and use that to decide what storage medium (full fletched array of components vs bit field) to use in ComponentService. But I'd consider this a hacky approach given that it'd rely on a flag to select an implementation.

Quillraven commented 11 months ago

Ah okay, if we still want to create families then it is of course trickier but it makes sense to do that Ideally we can come up with an approach that does not require to duplicate all the stuff that we have for components like the family creation functions that take ComponentType as argument if I remember correctly and also the family contains method.

Not sure how easy that is. I personally would like to have tags as an enum because it is easy to use and extend and in the end it shouldn't be more than a unique id. But not sure if we can get an easy and nice kotlin syntax going with that.

Definitely needs some time to prepare a draft for that and try it out.

Or do you want to go for a class approach meaning that a tag is a class that most likely needs to extend something?

What would be your wish scenario and syntax for the different use cases?

LobbyDivinus commented 11 months ago

Speaking of drafting it out, I came up with a solution that works the following way: I replaced ComponentType with an interface TrackableType. That interface only defines that there's an id and provides a factory method for a so called Tracker.

Trackers replace the raw components array used in ComponentsHolder and provide either the previous implemention of a components array, or an implementation using a bit field for a single value in case of tags. So what's differentiating Components from Tags is whether they inherit from ComponentType or from TagType (both inherit from DefaultTrackableType that implements the previous ComponentType id logic; DefaultTrackableType in return implements the TrackableType interface).

By using a TrackableType interface it is possible to define tags using an enum:

    private object TagTestComponent : TagComponent<TagTestComponent>()

    enum class TagTestEnum : TrackableComponentType<TagTestEnum> by TagComponent() {
        A,
        B,
        C
    }

    private  class RegularTestComponent : Component<RegularTestComponent> {
        override fun type() = RegularTestComponent
        companion object : ComponentType<RegularTestComponent>()
    }

TagComponent provides the default implementation that is trivial for tags which makes them easier to define.

The call site of tags in world, entities and families looks exactly the same as for Components, they are of type Component after all.

The changes can be seen in this fork

Quillraven commented 11 months ago

You are very quick 😅 I have an idea in my head as well that I want to try today in the evening when wife and son are sleeping.

I will then also check your fork and will give some feedback by tomorrow morning.

But from a quick glimpse that looks like a clean solution 👍

LobbyDivinus commented 11 months ago

Thank you! I think my solution involves a bit too much abstraction over ComponentType which makes things more complicated. On the other hand I like that it is usable with enum classes, too.

Sorry, I somehow failed to answer your questions above.

Or do you want to go for a class approach meaning that a tag is a class that most likely needs to extend something?

I prefer to be able to do it both, as a class approach similar to usual components or with enums. Not necessarily with the ability of extending other classes.

It's probably a good idea to support the use of multiple, independent enum tag classes to help decouple functionality (we want to keep "engine" tags separate from game logic related ones). In cases were the tag enum classes would only have one entry it makes sense to use an object instead.

What would be your wish scenario and syntax for the different use cases?

For most cases I want them to be used in the same way as components. Basically for ease of use. The code presented above does this by handling tags as singleton component objects for which only one instance has to be stored in the Components holder.

Consequentially, entity[TagType] returns the TagType object itself for all entities that have the tag set and null otherwise. So contains can be used to query it and entity += and -= for setting / clearing.

The most important thing to me is to be able to use them in families like regular Components. That way I can use tags to partition my entities into groups that are faster to iterate through with less overhead than regular Components. From what I understand your solution achieves this and even gets rid of the bit mask my solution stores in the TagTracker class. So I'd call that a win.

Quillraven commented 11 months ago

Thank you! I think my solution involves a bit too much abstraction over ComponentType which makes things more complicated. On the other hand I like that it is usable with enum classes, too.

I had a look at your fork and it gave me the idea to also add support for enums in the PR draft - thank you for that! Otherwise, I agree with you that the abstraction is too much for my taste. I usually try to avoid complex class hierarchies/abstractions because at some point the usually tend to get overwhelming and confusing.

Let me know what you think about the draft, if something needs to be added/adapted.

Fyi: ComponentsHolder are not used with tags. It is just setting/clearing a bit in the BitArray. For me that makes sense because a tag does not hold any relevant information. It is like an empty component. For that reason, for me, it also does not make sense to retrieve the tag itself because it is anyway a singleton object (either an object or an enum) and that is globally available. I also don't see a benefit to retrieve this instance.

For me it is sufficient to know if an entity has the tag or not. Or what would be the purpose to get the tag instance itself?

LobbyDivinus commented 11 months ago

I am glad that it was helpful to you. For me the draft looks ready to go, actually (plus minor details like doc and maybe special functions for tags).

Also, I think the naming of the new classes and functions involved nails it quite well.