Advertisement

ECS - Storing a function in a component

Started by September 18, 2021 02:57 PM
8 comments, last by LorenzoGatti 3 years, 2 months ago

I'm writing a game using the Java Artemis-odb ECS library, and have been stuck on this design question for a few days: what is the proper way to store a function/command in an ECS?

The use-case is that pressing a key should activate the currently held item in the player's Equipment component. This would have an entityId linking it to an entity with a Useable component. The question I have is what should be stored in Usable that could preform a variety of effects (shooting a gun, gaining health etc.) Another consideration is that the component should ideally be serializable.

Right now, my current thought is to use the event system, and have Usable return an EventFactory which EquipmentSystem will call to get a new event & send it. This would be listened to by GunSystem or HealthSystem etc.

It seems a little gross though, and making an ItemEvent subclass, ItemEventFactory subclass, and ItemEventListener function seems more heavyweight than necessary.

Is there a better way to store an action/function inside a component?

Thanks!

ECS in the first place means “no functions in components” because every funcionality should be handled by a system. So instead of forcing a workaround egeinst the ECS pattern, why don't you have an action event?

Input by design is always event driven, so the input data is by nature coming from an event source. All you now have to do is to let your event system handle that action. It might then trigger an update on the action system.

If I were in your situation, I'd have several kinds of different ECS approaches. One for the game itself, which is updated every frame and always batching all components of a kind and the other would be an event pipeline which has different systems attached to, where those systems decide which event they want to process. So the pipeline could have an ItemTriggerSystem attached to it where the input data is the button pressed, the system when performs a lookup to the player component to find out which item is attached there and then passes another “event” onto the pipeline which is processed by the corresponding item system

Advertisement

@shaarigan I guess the problem I'm having is that I have both the ECS loop and event queue loop, but I'm not sure what should be stored in the item's component so that when the system looks it up, the right event will be passed and the right system will get it.

Right now, I have the component storing an Event Factory, but that seems awkward. I'm also considering some sort of enum and a big file mapping the enum to a function to be called by the Item System.

Ideally, this would be generic enough that it could work for a Collision handler or Input handling etc.

The end dream is to be able to create new items by randomizing the components and the values of the components

You should never need a big enum. Polymorphism can pretty much always replace a big switch statement over enums, and is almost always better.

If you don't want to create a small utility/anonymous class for each thing you want to bind, consider a closure/lambda function.

enum Bool { True, False, FileNotFound };

@hplus0603 I'd love a lambda. There would be onCollision components that have a lambda, onUse functions, onKeyUpPressed etc.. Unfortunately, it seems to go against the ECS paradigm of only having data in components, and additionally I'm not sure how well they serialize. That's why I'm considering weird hacks like a giant switch statement in a system that maps enum → lambda

@hplus0603 I'm also not quite sure the right way to recreate polymorphism in ECS, but that's another thing I definitely miss having. Is there a good way to use it in ECS?

Advertisement

[quote]it seems to go against the ECS paradigm of only having data in components[/quote]

There are many flavors. If you are hard-core “data only,” then you do need to be able to store an “event” and then post that event to the event queue whenever activation happens. That also means your triggers will be asynchronous, not synchronous, which is good for decoupling but can cause interesting timing bugs. The question then becomes: “What are the valid values for the ‘event.type’ field?” which ends up being a schema question, really not that different from “what are the valid shader parameters” or “which image file formats do I support?”

Most ECS systems in practice end up putting soft, glue logic into the components themselves. Components derive from some interface that makes “Managing components and attachment” easier for objects and editors, and derive some other interface based on what kind of component it is (particles, collision, sound, etc) for what the subsystem needs. At runtime, the “heavy processing” (crunching numbers, mixing samples, drawing meshes, and so on) still happens in the pointed-at subsystems, but simpler things like “thing activated” or “user clicked” or whatever, can happen in code for the component.

Another way of supporting lambdas with “data only” is to store the function name to invoke as data, and have a function name→pointer lookup table somewhere. Every invokable function gets registered through some mechanism (perhaps during subsystem init,) and when loading the component, the function pointer is resolved from the name. This is still “data,” with special semantics – not that different from how you'll need special code to handle the users input controls, or the gearing and throttle of an engine, or whatever.

enum Bool { True, False, FileNotFound };

spencerflem said:
I guess the problem I'm having is that I have both the ECS loop and event queue loop, but I'm not sure what should be stored in the item's component so that when the system looks it up, the right event will be passed and the right system will get it

Usually you have the frame update, which is a batch processing of certain set of components. This may be the transform component for example which is bundled with a physics or velocity component to determine the direction and speed an entity moves, or the mesh component which tells the renderer what mesh to display.

Then there is the event loop as I wrote, which is simply a storage of events like input or something and processed on every frame. The difference is that if you handle an event, it isn't batched with all components of the same kind but you have a single component or a set of components which are involved. In your case, from a naive standpoint, I'd get the player entity and the item component from a certain input and let a system decide what to do, based on the item itself. You may for example put an event into your event system, which contains the item ID and a general event ID for let's say “use” or something. Your event handlers may then consider if they want to handle the event based on the item ID you passed and so you could simply hook new actions into the game.

We're using something similar but for our build tool. It receives some input and has a data driven pipeline which has processor units attached to it. Those units handle a certain type of event and/or data and could themselves also pass events/data to the pipeline which is handled by other PUs

If your events are similar enough, what you store per component could be, instead on an ugly object, a bare function pointer. The system processing your components can then call these functions with the same parameters (possibly just the component).

The two “queues” can be decoupled by a list of e.g. "Useables" to "use", populated by processing commands and events and querying what entities they apply to (e.g. “attack” → player entity → weapon in use) and processed component-wise as usual.

Omae Wa Mou Shindeiru

This topic is closed to new replies.

Advertisement