0

I want to setup my objects to be composed of components that can be added and removed so I have more flexibility in how I set them up.

A simple example would be some object that can have components attached such as:

MoveComponent //lets the object move around
HealthComponent //allows the object to take damage and be destroyed

So say i have some object like Player and i want to impact damage to it, how do i first check it can take damage by checking it has a HealthComponent and then also call a method in the HealthComponent to apply the damage.

My first thought was have each component register the to the Player object and store them in a HashSet<IComponent> but if I use an interface for the polymorphic benefits to add them to a collection theres not really much else in common with them and I would have to loop through the hash set every time to check if for example the HealthComponent exists and then if it does, cast it from the interface to the type required and call the Damage() function.

Such as:

public bool CanTakeDamange()
{
   foreach(var component in components)
   {
      if(component is HealthComponent) return true; 
   }
}
public void ApplyDamange(float damageValue)
{
   if(!CanTakeDamange)return;
   foreach(var component in components)
   {
      if(component is HealthComponent) 
      { 
         ((HealthComponent)component).ApplyDamage(damageValue);
         return;
      } 
   }
}

This does not seem like a smart approach to me, lots of loops every single time, as well as type checking and casting. Is this actually how people do component based systems to allow for a more decoupled setup?

Or is there a smarter way more streamlined way to do this?

4
  • Lots of these loops can be done once during initialization.
    – Euphoric
    Commented Jan 17, 2021 at 7:14
  • How so ? Every time i want to apply damage i would surely need to loop to get the HealthComponent in order to apply the damage?
    – WDUK
    Commented Jan 17, 2021 at 7:22
  • Every time components change, you can just loop through them, find the HealthComponent and save it into a property. Once you need the component, you just use the one saved in the property. And assuming components change rarely, this shouldn't have much performance impact.
    – Euphoric
    Commented Jan 17, 2021 at 8:14
  • @Euphoric: Storing the health component separately defeats the purpose of having the list of components to begin with. What you're advocating for is essentially no different from only having the property and simply not setting it when the player shouldn't have a health component.
    – Flater
    Commented Feb 17, 2021 at 16:14

3 Answers 3

0

In a system where you have generic components as you suggest, All components have the same method. there is no casting or type checking.

eg

IComponent
{
    Update()
}


Health : IComponent
{
    private Player p;
    Update()
    {
        //check for collision
        p.Health -= 10
    }
}

Move : IComponent
{
    private Player p;

    Update()
    {
        //check for keypress
        p.X += 10;    
    }
}

Its the Engine which loops through all the components, not the scripting layer

GameEngine
{
    Update()
    {
        foreach(c in components) { c.update() }
    }
}

In each case the Player private variable or whatever links to other components are configured during setup.

eg:

Move : IComponent
{
    SetUp()
    {
       foreach(c in this.gameObject.Components)
       {
           if(c is Player) { this.player = c}
       }
    }
}

This avoids your problematic loop from the scripting side.

In the case where some components have a special methods, say colliders the GameEngine can keep these in a separate list.

This avoids the problematic loop from the game engine side.

GameEngine
{
    Update()
    {
        foreach(c in components) { c.update() }
        foreach(c in colliders) { //check for collision c.CollisionWith(otherC) }
    }
}

The GameEngine is forced to use this structure because it has to cope with a second scripting layer, where the methods, relationships, number and type of objects simply can't be known in advance.

However!

If you are writing the scripting layer, then you should avoid this architecture. Instead use my first answer and dynamically create the components from your scripting objects when you load the game. eg

Game
{
    StartUp()
    {
       var l = loadLevel();
       foreach(p in l.Players)
       {
           //create player prefab
           //set the various properties as needed
           prebab.Components.Get<Health>().player = p;
       }
    }
}

You can thus program in a sensible OOP way in your scripting layer and simply use the components as an interface with the game engine.

0

Since you've mentioned its name in one of the comments here, I assume you are aware of Entity Component System modeling, so I won't delve too much into the specifics.

If you want runtime coupling of components, as opposed to design-time coupling, then a logical consequence here is that you're going to have to look these components up during runtime as well. You can't rely on a class definition to already know whether your entity has a component (or not), since that wasn't yet known at design time.

At design time, all you know is that your entity can contain components. You don't know which ones, and whenever you try to access one you have to manually check if there is one to be accessed. These are all givens based on the requirements you've defined.

My first thought was have each component register the to the Player object and store them in a HashSet but if I use an interface for the polymorphic benefits to add them to a collection theres not really much else in common with them and I would have to loop through the hash set every time to check if for example the HealthComponent exists and then if it does, cast it from the interface to the type required and call the Damage() function.

That is precisely what you're going to have to do. Your requirements have forced this design.

Like I established before, given your requirements there's no way to pre-emptively know whether an entity contains a component of a given type. This is specifically what you requested by having runtime coupling. Therefore, the code you write during design time cannot possible account for specific component types.

If it could account for specific types, then that is by definition design-time coupling. If this is the case, then Ewan's answer of using (design-time) composition is the better approach. The rest of my answer presumes that this is not what you want.

This does not seem like a smart approach to me, lots of loops every single time, as well as type checking and casting. Is this actually how people do component based systems to allow for a more decoupled setup?

But that doesn't mean your code cannot be improved on.

  • Instead of using a generic collection type, you can derive a component collection which extends several features related to finding components.
    • It's often better to use composition instead of inheritance when creating a custom collection class, but this is one of the cases where inheritance is acceptable because you'll still want the ability to treat is as the base collection that it is. If you disagree, use composition instead.
    • I'm using List<T> in my example, but feel free to use any other collection type you prefer.
  • Instead of iterations, you can use LINQ. That's still using iterations behind the scenes, but it keeps your code easier to read.
    • Note also that LINQ uses yield return behavior, i.e. it does not iterate the entire collection unless it has to.

Your code could for example be rewritten to:

public class ComponentList : List<IComponent>
{
    public bool Has<TComponent>() where TComponent : IComponent
    {
        return this.Any(x => x is TComponent);
    }

    public IEnumerable<TComponent> Get<TComponent>() where TComponent : IComponent
    {
        return this
                .Where(x => x is TComponent)
                .Select(x => x as TComponent);
    }

    public void UpdateAll<TComponent>(Action<TComponent> updateAction)
         where TComponent : IComponent
    {
        foreach(var component in Get<TComponent>()
        {
            updateAction(component);
        }
    }

    public void Update<TComponent>(Action<TComponent> updateAction)
         where TComponent : IComponent
    {
        var component = Get<TComponent>().SingleOrDefault();
        
        if(component != null)
            updateAction(component);
    }

    public void TryUpdate<TComponent>(Action<TComponent> updateAction)
         where TComponent : IComponent
    {
        var component = Get<TComponent>().Single();
        
        updateAction(component);
    }
}

And then your entity contains a property of this type:

public class Player
{
    public ComponentList Components { get; set; }
}

And then you can use the nice premade functions that look up the component for you.

Player player = GetPlayerEntity();

// If player is expected to have a HealthComponent
player.Components.Update<HealthComponent>(c => c.ReduceHealth(30));

// If you're not sure if player has a HealthComponent
player.Components.TryUpdate<HealthComponent>(c => c.ReduceHealth(30));

// If player can have multiple HealthComponents
player.Components.UpdateAll<HealthComponent>(c => c.ReduceHealth(30));

This is just a rudimentary example. Depending on whether your entities can have 0/1/many components of a given type, the implementation will vary.

But the main takeaway here is that you can create some helper methods which will do the component lookup for you, so that you don't have to constantly write the same logic over and over. And instead you can focus solely on what you're trying to do with this component (in my example, c.ReduceHealth(30)).

0

Composition is more..

public class Character : COmponent
{
    MoveComponent Mover;
    HealthComponent Health;

    public void OnHit()
    {
         this.Move.MoveBack(10)
         this.Health.TakeDamage(10)
     }
}

There's not much point having a list of Component if you have to search through and cast to the child class when you want to do stuff.

This pattern means I can make a second class,which uses only the parts I wish, rather than having a forced dependancy on, in this case MoveComponent

public class DestructableScenery : Component
{
     HealthComponent Health;
     public void OnHit()
     {
           this.Health.TakeDamage(30);
     }
}

Now your game can iterate a list of components and call OnHit() on each, there is no type checking or needless loops.

Alternatively if you want the Health and Move objects to be the components, there is no need for a player class at all. Just make all Components have the same Update() method called by the game, health components check for collisions and apply damage, move components check for keypresses and move

8
  • But this is hard typed, I want to be able to detach or attach components and not have scripts break because of dependencies on objects, and this feels more like traditional oop design rather than an entity component design.
    – WDUK
    Commented Jan 17, 2021 at 21:43
  • your example object has an ApplyDamage method. You have already hardcoded the dependency.
    – Ewan
    Commented Jan 17, 2021 at 23:43
  • That was my example of why I do not believe it is a smart approach, and that I wanted to find a way to decouple the dependency more but was unsure on how to structure the design to do it.
    – WDUK
    Commented Jan 18, 2021 at 5:27
  • you dont want to decouple a dependency that you need to use. You want to decouple from other classes which are redundant. Thats why you use composition as I outline over inheritance or the way you are attempting to do things
    – Ewan
    Commented Jan 18, 2021 at 12:35
  • @Ewan: You are talking about design time (de)coupling), but OP is talking about runtime coupling. OP's expectations are correct, ECS is a tried and true pattern for this. It's not always the best approach, and I agree that in most cases traditional OOP is better, but you can't just blindly supplant OP's approach with yours. It's not objectively superior, it just different.
    – Flater
    Commented Jan 18, 2021 at 13:56

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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