|
|
|
@ -66,7 +66,7 @@ We can use this approach to refactor our existing items and systems to derive/ov
|
|
|
|
|
|
|
|
|
|
Engines like Unity and Godot use variations of the [Entity-Component (EC)](https://gameprogrammingpatterns.com/component.html) pattern (similar to but distinct from Entity-Component-System (ECS)). These patterns favor composition over inheritance by allowing developers to isolate behaviors into discrete components that can be applied to entities. In the Spiked Shield example, a developer could make a "Damage Source" and "Damage Target" component and add both to the item. In essence, this is the same as the interface-based approach.
|
|
|
|
|
|
|
|
|
|
Unfortunately, these patterns suffer issues that are much more difficult to solve.
|
|
|
|
|
Unfortunately, these patterns dont alleviate other issues that are much more difficult to solve.
|
|
|
|
|
|
|
|
|
|
## In the event of my demise
|
|
|
|
|
Due to the nature of video games, important events may need to be handled at any moment. For example, a player may've dealt a fatal blow to a boss enemy on the same frame that they received fatal damage. In which order should this damage be processed? Depending on the handling order, this is likely the difference between clearing a potentially difficult boss battle and needing to do it again.
|
|
|
|
@ -106,7 +106,7 @@ class DamageEvent {
|
|
|
|
|
queue.Enqueue(args, priority);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// we'll dispatch events with the trick rate
|
|
|
|
|
// we'll dispatch events with the tick rate
|
|
|
|
|
// so they're all handled at the same time
|
|
|
|
|
private void OnTick(float deltaTime) {
|
|
|
|
|
// for simplicity we'll dequeue everything each frame
|
|
|
|
@ -181,10 +181,10 @@ This will work for many cases but it's unfortunate it needs to manage subscripti
|
|
|
|
|
|
|
|
|
|
With all this event-driven programming comes another downside: events are opaque and difficult to debug. When an architecture relies heavily on events, it becomes increasingly important to have outstanding documentation. It's an unfortunate sacrifice to be made in exchange for the highly decoupled nature of events. Some game engines have made attempts to offer better insight into event connections but it's often only a mild remedy.
|
|
|
|
|
|
|
|
|
|
In order to better query the state of an event-driven application, [Martin Fowler's Event Sourcing post](https://martinfowler.com/eaaDev/EventSourcing.html) provides some additional respite. It's worth reading if you're working with many events in your game. Essentially, tracking the source of an event *in* the newly raised event allows subscribers to walk the chain of events in reverse to determine additional context.
|
|
|
|
|
In order to better query the state of an event-driven application, [Martin Fowler's Event Sourcing post](https://martinfowler.com/eaaDev/EventSourcing.html) provides some additional respite. It's worth reading if you're working with many events in your game. Essentially, tracking the source of an event *in* the newly raised event allows subscribers to walk the chain of source events in reverse to determine additional context.
|
|
|
|
|
|
|
|
|
|
## Enter ECS
|
|
|
|
|
These were just a few pain points of game architecture that I've come across when making games. I've tried different solutions each time to varying levels of success. While I don't think there exists a one-size-fits-all solution to every architectural decision in video games, I *do* believe that reframing how we think about our architectural goals can make some problems diminish or even disappear. This is especially helpful if the affected problems are persistent across multiple projects.
|
|
|
|
|
These were just a few pain points of game architecture that I've come across when making games. I've tried different solutions each time to varying levels of success. While I don't think there exists a one-size-fits-all solution to every architectural decision in video games, I *do* believe that reframing how we think about our architectural goals can make some problems diminish or even disappear. This is especially helpful if the affected problems were persistent across multiple projects.
|
|
|
|
|
|
|
|
|
|
Entity-Component-System (ECS) is a data-oriented approach that has resolved many of the above issues for me. It comes with its own architectural challenges, especially since the pattern has been rapidly evolving due to its recent explosion of popularity.
|
|
|
|
|
|
|
|
|
@ -202,7 +202,7 @@ Components are often stored contiguously in memory (such as in an array). The en
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### An example
|
|
|
|
|
Let's pause for a moment to consider the relationship between entities and components. In the simplest possible ECS library, we could implement these concepts in the following way.
|
|
|
|
|
Let's pause for a moment to consider the relationship between entities and components. If we were to focus purely on simplicity, we could implement these concepts in the following way.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
```cs
|
|
|
|
@ -220,22 +220,42 @@ Console.WriteLine($"The player's current health is {currentHealth}");
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
I would like to restate that the actual interface will depend on the ECS library and the implementation decisions they've made. Different implementations have different tradeoffs and limitations that may change the internal representation of an entity or component.
|
|
|
|
|
I would like to restate that the actual interface will depend on the library and the decisions its developers have made. Different implementations have different tradeoffs and limitations that may change the internal representation of an entity or component.
|
|
|
|
|
|
|
|
|
|
If we extend our example just a little bit to include multiple components, we would end up with multiple containers (arrays) too. This has an interesting implication in that it allows us to visualize our data in a more intuitive way: a table.
|
|
|
|
|
|
|
|
|
|
| Entity | Name | Health.Current | Health.Max |
|
|
|
|
|
| ------ | -------- | -------------- | ---------- |
|
|
|
|
|
| 0 | "Player" | 25 | 50 |
|
|
|
|
|
| 1 | "Kain" | 100 | 100 |
|
|
|
|
|
| 2 | "Raziel" | 5 | 75 |
|
|
|
|
|
| 3 | "Janos" | 0 | 1000 |
|
|
|
|
|
| 0 | "Kain" | 100 | 100 |
|
|
|
|
|
| 1 | "Raziel" | 5 | 75 |
|
|
|
|
|
| 2 | "Janos" | 0 | 1000 |
|
|
|
|
|
|
|
|
|
|
Our entity is the row ID and each successive column is its associated component data. In cases where an entity does not have a component, we can think of its value as `NULL`.
|
|
|
|
|
|
|
|
|
|
| Entity | Name | Health.Current | Health.Max | Weapon.Name |
|
|
|
|
|
| ------ | -------- | -------------- | ---------- | ------------- |
|
|
|
|
|
| 0 | "Kain" | 100 | 100 | "Soul Reaver" |
|
|
|
|
|
| 1 | "Raziel" | 5 | 75 | "Soul Reaver" |
|
|
|
|
|
| 2 | "Janos" | 0 | 1000 | `NULL` |
|
|
|
|
|
|
|
|
|
|
Our entity is the row ID and each successive column is its associated component data.
|
|
|
|
|
|
|
|
|
|
### Systems
|
|
|
|
|
|
|
|
|
|
A system represents a behavior. This is where game and application logic lives. Systems receive a list of entities and iterate over that list to perform work on their component data. They may also create and destroy entities or attach and remove components.
|
|
|
|
|
A system represents a behavior. This is where game and application logic lives. Systems receive a list of entities and iterate over that list to perform work on their component data. They may also create and destroy entities or attach and remove components. If we want to write a system which applies movement to an entity, we could check for the existence of a `Position` and `Velocity` components.
|
|
|
|
|
|
|
|
|
|
```cs
|
|
|
|
|
foreach(var entity of entities) {
|
|
|
|
|
if (entity.hasComponent(Velocity) && entity.hasComponent(Position)) {
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Going further (Conditional components, Queries)
|
|
|
|
|
Most ECS systems can query the state to refine the list of entities they receive to what is most pertinent. Some have scheduling and ordering systems for systems.
|
|
|
|
|
|
|
|
|
|
## Performance
|
|
|
|
|
its fast
|
|
|
|
|
|
|
|
|
|
## Issues with ECS
|
|
|
|
|
Mental modeling is hard. Complex queries are hard. ECS is hard.
|
|
|
|
@ -254,9 +274,17 @@ continued evolution of ecs which can broadly respond to specific use cases and n
|
|
|
|
|
|
|
|
|
|
### Not on topic but will still accept
|
|
|
|
|
* improvements and specific optimizations to my prototype
|
|
|
|
|
* constructive feedback on the specific contents of this post (eg. code examples)
|
|
|
|
|
|
|
|
|
|
### What I'm not looking for
|
|
|
|
|
* criticisms and discussion about my specific prototype implementation
|
|
|
|
|
* criticism and discussion about my specific prototype implementation
|
|
|
|
|
* paradigm, language, and coding style debates
|
|
|
|
|
* i'm not interested in debating the merits of one paradigm or language versus another
|
|
|
|
|
* i'm extremely not interested in K&R versus Allman indentation style arguments. [yes i am a monster](https://youtu.be/raQ3iHhE_Kk?t=1093) (but that's not a secret)
|
|
|
|
|
* as stated in the preface, all of these are approaches that have worked best for me and how i think. if OO or whatever else works for you, you should use that!
|
|
|
|
|
* criticism about the code examples in this post
|
|
|
|
|
* how best to solve a problem via OOP is not the point of this post
|
|
|
|
|
* any feedback on how to better demonstrate solutions to the example problems are ok though. i want to fairly represent the alternatives
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{% endblock article %}
|
|
|
|
|