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 '19 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 '19 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 '19 at 1:37
1
\$\begingroup\$

You could create a system ScriptExecutionSystem which operates on all entities with a Script component. It obtains all components of the entity which could be useful to expose to the scripting system and passes those to the scripted function.

Another approach would be to get your users to also embrace ECS and allow them to define their own components and implement their own systems using the scripting language.

\$\endgroup\$
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\$
0
\$\begingroup\$

With ECS you can break down to a single responsibility, so any entity that moves would want two data components : a MoveComponent, and a MoveSpeedComponent.

using System;
using Unity.Entities;

[Serializable]
public struct MoveForward : IComponentData{}
////////////////////////////////////////////
using System;
using Unity.Entities;

[Serializable]
public struct MoveSpeed : IComponentData
{
public float Value;
}
///////////////////////////////////////////

so now in your conversion you add those components to your entities

public class MoveForwardConversion : MonoBehaviour, IConvertGameObjectToEntity
{
public float speed = 50f;

public void Convert(Entity entity, EntityManager manager,       GameObjectConversionSystem conversionSystem)
{
    manager.AddComponent(entity, typeof(MoveForward));

    MoveSpeed moveSpeed = new MoveSpeed { Value = speed };
    manager.AddComponentData(entity, moveSpeed);     
}

Now that we have conversion and data we can move to the system, I removed the input system for readability but if you would like to learn more on the input system I will have that all listed in my article next week on unity connect.

using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;

namespace Unity.Transforms
{
public class MoveForwardSystem : JobComponentSystem
{
    [BurstCompile]
    [RequireComponentTag(typeof(MoveForward))]
    struct MoveForwardRotation : IJobForEach<Translation, MoveSpeed>
    {
        public float dt;

        public void Execute(ref Translation pos, [ReadOnly] ref MoveSpeed speed)
        {
            pos.Value = pos.Value + (dt * speed.Value);            
           // pos.Value.z += playerInput.Horizontal;
        }
    }
}

note that the above class is using the Unity.Mathmatics. This is great for being able to use different math functions that you are used to working with in the normal systems. With that all in line you can now work on the entities behavior- again I removed the input here but this is all much better explained in the article.

using Unity.Entities;
using UnityEngine;

public class EntityBehaviour : MonoBehaviour, IConvertGameObjectToEntity
{
public float speed = 22f;

void Update()
{
    Vector3 movement = transform.forward * speed * Time.deltaTime;
}
public void Convert(Entity entity, EntityManager manager, GameObjectConversionSystem conversionSystem)
{   
    //set speed of the entity
    MoveSpeed moveSpeed = new MoveSpeed { Value = speed };
    //take horizontal inputs for entites
    //PlayerInput horizontalinput = new PlayerInput { Horizontal = Input.GetAxis("Horizontal") };

    //add move component to entity
    manager.AddComponent(entity, typeof(MoveForward));
    //add component data  
    manager.AddComponentData(entity, moveSpeed);
   // manager.AddComponentData(entity, horizontalinput);
}
}

Now you can introduce entities that will move forward at a speed.

But also this will move every entity with this behavior so you can introduce tags, for instance if you added a PlayerTag, then only the entity with the playerTag IComponentData will be able to perform the MoveForward if I only want to move the player like the example below.

I will get deeper into that also in the article but it looks like this in a typical ComponentSystem

    Entities.WithAll<PlayerTag>().ForEach((ref Translation pos) =>
    {
        pos = new Translation { Value =  /*PlayerPosition*/ };
    });

Much of this is explained pretty well in the Angry Dots presentation with Mike Geig, if you have not seen yet I recommend checking it out. I will point to my article as well after it's up. Should really be helpful to get several of those things you are used to working with working how you would like in ECS.

\$\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.