The XY problem is a commonly occurring problem, where trying to fix one thing causes a different issue.
I think you're a few levels deep here, and are essentially faced with an XYZ... problem. The complexity of what you're trying to do is massively out of scope with what you're actually trying to achieve.
So I'm going to take a few steps back here and start from the beginning. Or at least, I'm going to start from what I think your initial X problem was. If I'm wrong, please do correct me.
If all you have is a hammer, everything looks like a nail.
You've learned about inheritance, and are trying to solve everything by using inheritance.
You've labeled yourself as a beginner. I'm not sure how much of a beginner you are, but you should look up a few other approaches:
I picked these two topics for a specific reason: their main focus is on problems that people often first (wrongly) try to solve by using inheritance (and then end up regretting that decision).
If you don't know interfaces or composition, I suggest you read up on the links (other resources can be found on Google too of course). At least understand how they're used, as I will be referring to them in the rest of the answer.
Entity Sprite:
Contains Texture
Contains Position
Contains Alpha
Entity NPC:
Contains "Components of Sprite"
Contains Text
You should be using composition here:
Entity NPC:
Contains a Sprite object
Contains Text
Or, the C# equivalent:
public class Npc
{
public String Text { get; set; }
public Sprite Sprite { get; set; }
}
I read up on ECS following the link you provided, and nothing in the documentation states that components aren't allowed to implement each other. Following that, using one component as the property of another solves all your requirements.
This also means that if Sprite
is updated at a later stage (e.g. a new property is added), that all components with a Sprite
property are updated accordingly.
I think you're taking the explanation of ECS (as per your link) too literally.
Design of an entity
An entity only consists of an ID and a container of components. The idea is to have no game methods embedded in the entity. The container doesn't have to be located physically together with the entity, but should be easy to find and access
You're looking to implement an entity like this:
public class Entity
{
public Guid Id { get; set; }
public List<IComponent> Components { get; set; }
}
And it makes sense, up to a point. Based on your question, and how you've approach your example NPC and Sprite classes, I get the feeling that you think that every property should be a component in and of itself. It shouldn't be.
That is a much too granular approach. What you want to do is create classes (the normal C# way) which have properties and fullfill a goal (implemented through an interface) and which act as a component.
The properties of such a class are not necessarily components in and of themselves! They can be (like the Sprite in NPC, see example above), but they don't have to be components (you can have normal strings, ints, ...)
This feels like a theoretical design, more than a practical one. This is seemingly confirmed by the criticism listed on the page you linked:
Overall, ECS is a mixed personal reflection of orthogonal well-established ideas in general Computer science and Programming language theory.
I'm not going to tell you that ECS doesn't work, or isn't a good solution. But what I am certain of is that a pure ECS approach is much too complex for someone who labels themselves a beginner.
There are other approaches that maintain the intention of ECS, without taking such a hardline stance on the format. Observing the inheritance/interface/composition trichotomy that I've listed, an ECS-like system would heavily favor composition as its main architecture.
Let me show you a simple working example of an ECS-like approach which relies on composition. A short reminder of the important parts of the design:
The ECS architecture uses composition, not complex inheritance trees. An entity will be typically made up of an ID and a list of components that are attached to it. Any type of game object can be created by adding the correct components to an entity. This can also allow the developer to easily add features of one type of object to another, without any dependency issues.
For example, a player entity could have a "bullet" component added to it, and then it would meet the requirements to be manipulated by some "bulletHandler" system, which could result in that player doing damage to things by running into them.
I will use the bullet example later on.
Note
There are things here which can be approached in several ways. Some people prefer an abstract base class. Others prefer using an interface. I'm going to mix and match these based on what I think will be the clearest explanation. That doesn't mean the other option isn't viable.
This example is only an example. There is room for improvement, which I have omitted for the sake of both clarity and brevity.
Entity
Since the main goal of the entity class is to provide a singular data type on which all game objects are based; I'm not going to use inheritance or interfaces here.
Inheritance and interfaces only make functional sense when there are multiple classes that have shared logic/exposed properties and methods.
public class Entity
{
public Guid Id { get; set; }
public List<IComponent> Components { get; set; } = new List<IComponent>();
}
This maintains the goal of an entity: provide a singular type, where every object of that type will be a composition of components.
This class can be expanded with some logic to help you in the future, e.g. some utility methods that help with finding the components you're looking for:
public IComponent GetComponentByUniqueName(string name)
{
return this.Components.SingleOrDefault(c => c.Name == name);
}
public bool HasComponent(string name)
{
return this.Components.Any(c => c.Name == name);
}
public IEnumerable<IComponent> GetComponentsByName(string name)
{
return this.Components.Where(c => c.Name == name);
}
Which methods to add to the entity class is left as an exercise to you.
Component
The entity code already reveals that we'll be using an interface here:
public interface IComponent
{
public string Name { get; set; }
public string Type { get; }
}
For the sake of example, I'm going to assume that a component only lists a name "MyBullet" and a type (e.g. "BulletComponent"). Any other properties are assumed to be specific to a given component type, and can therefore only be accessed by casting the IComponent
object back to its main type.
Secondly, I would suggest relying on a base class here, because the logic for calculating the value of Type
can be abstracted:
public class BaseComponent : IComponent
{
public virtual string Name { get; set; }
public string Type => this.GetType().Name;
}
An example of the bullet component can then be:
public class BulletComponent : BaseComponent
{
//Bullet-specific properties
public bool IsArmorPiercing { get; set; }
public double Damage { get; set; }
}
Seeing it in action
- Making a player entity who has a bullet component.
var myPlayer = new BaseEntity() { Id = new Guid() };
var bulletComp = new BulletComponent()
{
Name = "Player Bullet",
IsArmorPiercing = false,
Damage = 150
};
var myPlayer.Components.Add(bulletComp);
- Making a bullet handler.
public class BulletHandler
{
public bool CanHandle(BaseEntity entity)
{
return entity.HasComponentType("BulletComponent");
}
public void Handle(BaseEntity entity)
{
if(!CanHandle(entity)) return;
var bulletComp = entity.GetComponentByType("BulletComponent") as BulletComponent;
//Further handling, e.g.:
int damageDone = bulletComp.Damage;
}
}
Note: There is a lot of room for improvement here. You can use a generic Handler<T>
(where in this case T
is BulletComponent
), you can avoid the magic string typing, ... This is just a basic example to show you how to check if an entity has a particular component, and how to access its properties.
Update
I seem to have skipped over your mention of using static
here (or I read it and then forgot about it).
You shouldn't be using statics here. It's not what you want. Statics should be used for globally accessible values. As a simple example, the version number ("0.1") of your current application is the same for your entire application, you never want to use two different version numbers at the same time; therefore the version number can be contained in a static scope so everyone uses the same value at all times.
But your components should still retain their (object-specific) individuality. As a simple example, player A's name shouldn't be the same as a player B's name, so their Name
property(/component) shouldn't be static.
I can't think of a place where statics would meaningfully improve the code in this answer.
Note: There may be ulterior reasons for using statics not related to the code in this answer, but that's of course out of scope for my statement here.
static
, they wouldn't have put it into the language in the first place. – Robert Harvey Mar 19 '18 at 21:53static
in C# doesn't fit that category. It sounds like you either don't know how to use it properly, or you drank the TDD Koolaid that makes people allergic tostatic
. – Robert Harvey Mar 19 '18 at 22:05static
context and specify types explicitly. – Robert Harvey Mar 19 '18 at 22:19