more writing,,.,;
This commit is contained in:
parent
58e276cac5
commit
2b3ad3bd11
2 changed files with 22 additions and 11 deletions
|
@ -10,7 +10,7 @@ Everything in this article is drawn directly from my experiences working in game
|
|||
## Theseus' Shield
|
||||
In the context of game design and development, object oriented data modeling can feel intuitive. Players, NPCs, and Enemies can all derive from a common "Creature" class which handle things like health and damage. Weapons, armor, and consumables can derive from a common Item class which handle inventory management.
|
||||
|
||||
Thinking exclusively in terms of hierarchal, nested patterns can come at a cost: software brittleness and inflexible design. Let's use our base `Item` class as an example.
|
||||
Thinking exclusively in terms of hierarchal, nested patterns can come at a cost: software brittleness and inflexible design. Let's use an `Item` class as an example.
|
||||
|
||||
```cs
|
||||
abstract class Item {
|
||||
|
@ -49,7 +49,7 @@ interface ICollectable {
|
|||
|
||||
interface IDamaging {
|
||||
// by default, we'll just pass the damage directly to
|
||||
// the damage handler
|
||||
// the receiving damage handler
|
||||
void ApplyDamage(IDamageable damageable, float value) {
|
||||
damageable.ReceiveDamage(this, value);
|
||||
}
|
||||
|
@ -130,14 +130,18 @@ There is still a timing issue with this approach however. Events can be raised a
|
|||
|
||||
Depending on the engine and implementation, there may be a few options for solving this. Rather than using `OnTick`, the damage can be handled in `OnLateTick` which runs after all `OnTick` systems have been processed (Unity's version of these methods are `Update` and `LateUpdate`). In Unity the `DamageEvent` singleton script could have its order explicitly modified in the settings or with the `DefaultExecutionOrder` attribute. In Godot, this would likely be resolved by moving the `DamageEvent` node lower in the tree since nodes are processed from top to bottom while resolving children first.
|
||||
|
||||
An alternative solution would be to identify the behavior which raises code events and isolate it into its own singleton. Doing this would allow it to easily be ordered before the damage handling system.
|
||||
An alternative solution would be to identify the behavior which raises code events and isolate it into its own singleton. Doing this would allow it to easily be ordered before the damage handling system. We'll revisit this idea later.
|
||||
|
||||
## Reading game state
|
||||
## Legion-oriented programming
|
||||
Managing a game state as it grows in size and complexity is *difficult*. It's not uncommon for a game to have dozens, hundreds, or thousands of active entities at a time. In many cases, it makes sense to decouple separate-but-related behaviors into their own systems to make them easier to manage.
|
||||
|
||||
One such separation would be game logic and UI. Rather than directly coupling the player's health to the UI representation of the player's health, it makes sense to have them communicate via some type of messaging system. An event seems like a natural fit.
|
||||
|
||||
```cs
|
||||
public class PlayerSpawnedArgs : EventArgs {
|
||||
public readonly Player Player;
|
||||
}
|
||||
|
||||
public class HealthChangedArgs : EventArgs {
|
||||
public readonly float PreviousValue;
|
||||
public readonly float CurrentValue;
|
||||
|
@ -145,6 +149,8 @@ public class HealthChangedArgs : EventArgs {
|
|||
|
||||
class Healthbar : UIElement {
|
||||
private float _currentHealth;
|
||||
// use getter/setter to automatically redraw
|
||||
// the UI on state changes
|
||||
private float currentHealth {
|
||||
get => _currentHealth;
|
||||
set {
|
||||
|
@ -160,9 +166,9 @@ class Healthbar : UIElement {
|
|||
PlayerSpawnEvent.Register(OnPlayerSpawned);
|
||||
}
|
||||
|
||||
private void OnPlayerSpawned(object sender, Player player) {
|
||||
private void OnPlayerSpawned(object sender, PlayerSpawnedArgs args) {
|
||||
// subscribe specifically to that player's events
|
||||
player.health.Register(OnHealthChanged);
|
||||
args.Player.health.Register(OnHealthChanged);
|
||||
}
|
||||
|
||||
private void OnHealthChanged(object sender, HealthChangedArgs args) {
|
||||
|
@ -171,20 +177,21 @@ class Healthbar : UIElement {
|
|||
}
|
||||
```
|
||||
|
||||
This should work fine but it's unfortunate it needs to manage subscriptions to two events in order to get the updates it needs. We could circumvent this by making health updates dispatched globally but that wold come at the cost of checking *every* health update just to find the player. This solution would work, however, if every entity with health had a healthbar.
|
||||
This will work for many cases but it's unfortunate it needs to manage subscriptions to two events in order to get the updates it needs. If we ever needed to add more than one player that renders a healthbar, this solution would no longer be sufficient. We could circumvent this by making health updates dispatched globally but that wold come at the cost of checking *every* health update just to find a player. This solution would be much more appealing, however, if every entity with health had a healthbar. One happy medium would be to create a more specific global event for player health updates. `Healthbar` can then simply register to that event without needing to concern itself with the particulars of spawning.
|
||||
|
||||
global variable, then array
|
||||
With all this event-driven programming comes another downside: events are difficult to debug. Events are opaque, short-lived, and have unpredictable interactions by design. 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.
|
||||
|
||||
use examples of multiplayer, then enemies with health bars
|
||||
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.
|
||||
|
||||
## Enter ECS
|
||||
ECS solves many of the problems presented by object oriented programming patterns but it's still in its infancy.
|
||||
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 making some problems diminish or even disappear. This is especially helpful if the affected problems are persistent.
|
||||
|
||||
## Issues with ECS
|
||||
Mental modeling is hard. Complex queries are hard. ECS is hard.
|
||||
|
||||
## A Solution?
|
||||
something something game state as a database, expressive querying
|
||||
https://ajmmertens.medium.com/why-it-is-time-to-start-thinking-of-games-as-databases-e7971da33ac3
|
||||
|
||||
## Going forward
|
||||
continued evolution of ecs which can broadly respond to specific use cases and needs
|
||||
|
@ -198,7 +205,7 @@ continued evolution of ecs which can broadly respond to specific use cases and n
|
|||
* improvements and specific optimizations to my prototype
|
||||
|
||||
### What I'm not looking for
|
||||
* criticisms about my specific prototype implementation
|
||||
* criticisms and discussion about my specific prototype implementation
|
||||
|
||||
|
||||
{% endblock article %}
|
||||
|
|
|
@ -22,9 +22,13 @@
|
|||
<header>
|
||||
<h1 class="header">{{ this.title }}</h1>
|
||||
<small class="muted">
|
||||
{% if this.created_date %}
|
||||
<time datetime="{{ this.created_date }}">
|
||||
Posted on {{ this.created_date | date(format=date_format) }}
|
||||
</time>
|
||||
{% else %}
|
||||
Draft
|
||||
{% endif %}
|
||||
</small>
|
||||
</header>
|
||||
{% block article %}
|
||||
|
|
Loading…
Reference in a new issue