8
\$\begingroup\$

I am currently creating a small hobby project to get back into game development, and I have decided to structure my entities using an ECS (Entity Component System). This implementation of an ECS is structured like so:

  • Entity: In my case it's a unique int identifier that is used as a key to a list of components.
  • Component: Holds only data, e.g. the Position component holds an x and y coordinate, and the Movement component holds a speed and direction variable.
  • System: Handles components, e.g. it takes the Position and Movement components and adds the speed and direction to the position's x and y coordinates.

This works fine, but now I wish to implement scripting into my games, in the form of a scripting language. In previous projects I've used an OOP implementation of game objects, which meant that scripting was pretty straight forwards. For example, a simple script could look something like this:

function start()
    local future = entity:moveTo(pos1)
    wait(future)

    local response = entity:showDialog(dialog1)
    if wait(response) == 1 then
        local itemStack = entity:getInventory():removeItemByName("apple", 1)
        world:getPlayer():getInventory():addItemStack(itemStack)
    else
        entity:setBehavior(world:getPlayer(), BEHAVIOR_HOSTILE)
    end
end

However, when using an ECS, the entity itself does not have any functions like moveTo or getInventory, instead the above script written in ECS style would look something like this:

 function start()
    local movement = world:getComponent(MOVEMENT, entity)
    movement:moveTo(pos1)

    local position = world:getComponent(POSITION, entity)
    local future = Future:untilEquals(position.pos, pos1)
    wait(future)

    local dialogComp = world:getComponent(DIALOG, entity)
    local response = dialogComp:showDialog(dialog1)

    if wait(response) == 1 then
        local entityInventory = world:getComponent(INVENTORY, entity)
        local playerInventory = world:getComponent(INVENTORY, world:getPlayer())
        local itemStack = entityInventory:removeItemByName("apple", 1)
        playerInventory:addItemStack(itemStack)
    else
        local entityBehavior = world:getComponent(BEHAVIOR, entity)
        local playerBehavior = world:getComponent(BEHAVIOR, world:getPlayer())
        entityBehavior:set(playerBehavior, BEHAVIOR_HOSTILE)
    end
end

This is a lot more verbose compared to the OOP version, which is not desirable when the scripting is aimed towards mostly non-programmers (players of the game).

One solution would be to have some sort of wrapper object that encapsulates an Entity and supplies functions such as moveTo directly, and handles the rest internally, although such a solution seems sub-optimal since it takes a lot of work to cover all the components, and every time a new component is added you would need to change the wrapper object with new functions.

To all game developers that has implemented scripting in an ECS before - how did you do it? The main focus here is usability for the end user, with as little "maintenance" cost as possible (preferably you don't need to change it every time you add a component).

\$\endgroup\$
  • \$\begingroup\$ I'll write this as a comment because I'm a little vague on your exact implementation (I assume C++). Couldn't you utilize templates somewhere here? To apply the X component against the Y component? I imagine then that components would need to override the base "apply" method and specialise it for the types of components that can be applied to it. Relying on SFINAE would ensure that it works when it's meant to. Or it could be specialised in the System class/s to allow the components to remain data structs. \$\endgroup\$ – NeomerArcana Jul 22 at 23:05
  • \$\begingroup\$ Why not expose the moveTo method as a part of the underlying system in your use case, e.g. MovementSystem? This way not only then can you use it in the scripts you write but you can then also use it as a part of the C++ code too where you need it. So yes you'll have to expose new methods as new systems are added, but that's to be expected as its entirely new behavior these systems introduce anyway. \$\endgroup\$ – Naros Jul 23 at 14:07
  • \$\begingroup\$ I haven't had the chance to do it, but would it be feasible to add "shortcuts" only to those more commonly performed operations? \$\endgroup\$ – Vaillancourt Jul 24 at 1:37
0
\$\begingroup\$

ECS has its pros and cons. User-friendly scripting is not one of its pros.

The problem ECS solves is the ability to have a large number of similar things in your game at the same time while retaining performance. But this solution comes at a cost - the cost of an easy to use architecture. It is not the best architecture for every game.

For example, ECS would have been a fine choice for Space Invaders, but not so much for PacMan.

So not exactly the answer you were looking for, but its possible that ECS is just not the right tool for your job.

If you add a wrapper, watch the overhead cost. If you end up removing the performance boost of ECS in your wrapper, then you have the worst of both worlds.


But to directly answer your question - "To all game developers that has implemented scripting in an ECS before - how did you do it?"

Pretty much exactly as you are doing it, without a wrapper. Entities have nothing but an identifier. Components have nothing but data. Systems have nothing but logic. Systems that accept entities with the required components run through. Add systems, entities, and components freely.

I once used a framework with a fourth aspect, called a blackboard. It was basically a way for systems to communicate with eachother. It created more problems than it solved.


Related: Should I implement Entity Component System in all my projects?

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.