Compare commits

...

2 commits

Author SHA1 Message Date
edbe63a665 moving to desktop 2024-12-10 22:48:59 -06:00
5aa842ca38 add css for tables; improve mobile readability 2024-12-10 12:15:38 -06:00
5 changed files with 78 additions and 32 deletions

View file

@ -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. 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 ## 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. 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); 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 // so they're all handled at the same time
private void OnTick(float deltaTime) { private void OnTick(float deltaTime) {
// for simplicity we'll dequeue everything each frame // 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. 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 ## 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. 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 ### 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 ```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. 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 | | Entity | Name | Health.Current | Health.Max |
| ------ | -------- | -------------- | ---------- | | ------ | -------- | -------------- | ---------- |
| 0 | "Player" | 25 | 50 | | 0 | "Kain" | 100 | 100 |
| 1 | "Kain" | 100 | 100 | | 1 | "Raziel" | 5 | 75 |
| 2 | "Raziel" | 5 | 75 | | 2 | "Janos" | 0 | 1000 |
| 3 | "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 ### 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 ## Issues with ECS
Mental modeling is hard. Complex queries are hard. ECS is hard. 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 ### Not on topic but will still accept
* improvements and specific optimizations to my prototype * 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 ### 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 %} {% endblock article %}

View file

@ -10,7 +10,7 @@
<div class="column"> <div class="column">
<div class="container"> <div class="container">
<div class="column align-center"> <div class="column align-center">
<nav class="card main-nav"> <nav class="box main-nav">
<li><a href="/">Index</a></li> <li><a href="/">Index</a></li>
<li><a href="/blog">Blog</a></li> <li><a href="/blog">Blog</a></li>
<li><a href="/projects">Projects</a></li> <li><a href="/projects">Projects</a></li>
@ -31,10 +31,10 @@
</div> </div>
</div> </div>
<div class="primary column"> <div class="primary column">
<div class="card has-moon"> <div class="box has-moon">
<h1 class="header">welcome!</h1> <h1 class="header">welcome!</h1>
<div class="card-content"> <div class="box-content">
<div class="card-image bubble tail right"> <div class="box-image bubble tail right">
<div class="hide-overflow"> <div class="hide-overflow">
<img class="hover-zoom tooltip left-top" src="/static/img/pfp.webp" width="100" height="100"></img> <img class="hover-zoom tooltip left-top" src="/static/img/pfp.webp" width="100" height="100"></img>
</div> </div>
@ -50,7 +50,7 @@
<p>not everything that i make has the same focus but links to those projects will be annotated with content warnings.</p> <p>not everything that i make has the same focus but links to those projects will be annotated with content warnings.</p>
</div> </div>
</div> </div>
<div class="card has-moon"> <div class="box has-moon">
<div class="row align-center"> <div class="row align-center">
<h1 class="header">blog</h1> <h1 class="header">blog</h1>
&nbsp;&nbsp;<a style="font-size: 150%" href="/blog"></a> &nbsp;&nbsp;<a style="font-size: 150%" href="/blog"></a>
@ -68,7 +68,7 @@
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
<div class="card has-moon"> <div class="box has-moon">
<div class="row align-center"> <div class="row align-center">
<h1 class="header">projects</h1> <h1 class="header">projects</h1>
&nbsp;&nbsp;<a style="font-size: 150%" href="/projects"></a> &nbsp;&nbsp;<a style="font-size: 150%" href="/projects"></a>

View file

@ -1,2 +1,2 @@
<footer class="card center-text">© Kitsune Cafe {{ now() | date(format="%Y") }}</footer> <footer class="box center-text">© Kitsune Cafe {{ now() | date(format="%Y") }}</footer>

View file

@ -3,7 +3,7 @@
{% block body %} {% block body %}
{{ super() }} {{ super() }}
<div class="column"> <div class="column">
<nav class="card row justify-center"> <nav class="box row justify-center">
<li><a href="/">Index</a></li> <li><a href="/">Index</a></li>
<li><a href="/blog">Blog</a></li> <li><a href="/blog">Blog</a></li>
<li><a href="/projects">Projects</a></li> <li><a href="/projects">Projects</a></li>
@ -18,7 +18,7 @@
<li><a href="mailto:rowan@kitsu.cafe">Email</a></li> <li><a href="mailto:rowan@kitsu.cafe">Email</a></li>
</nav> </nav>
<div class="container"> <div class="container">
<div class="card"> <div class="box">
{% block content %} {% block content %}
{% endblock %} {% endblock %}
</div> </div>

View file

@ -157,6 +157,19 @@ article header {
margin-bottom: 1rem; margin-bottom: 1rem;
} }
table {
width: 100%;
}
table thead {
background-color: var(--tertiary-color);
}
table tr:nth-child(2n) {
background-color: var(--dark-grey);
}
@media only screen and (min-width: 740px) { @media only screen and (min-width: 740px) {
nav.main-nav { nav.main-nav {
flex-direction: column; flex-direction: column;
@ -222,19 +235,24 @@ article header {
justify-content: space-between; justify-content: space-between;
} }
.card { .box {
background-color: var(--background-color); background-color: var(--background-color);
border: 1px solid var(--quaternary-color);
margin: 0.75rem;
padding: 1rem; padding: 1rem;
border-radius: 4px;
} }
.card.has-moon { @media only screen and (min-width: 740px) {
.box {
border: 1px solid var(--quaternary-color);
border-radius: 4px;
margin: 0.75rem;
}
}
.box.has-moon {
position: relative; position: relative;
} }
.card.has-moon::before { .box.has-moon::before {
content: ''; content: '';
position: absolute; position: absolute;
top: 18px; top: 18px;
@ -272,7 +290,7 @@ article header {
margin-left: .25rem; margin-left: .25rem;
} }
.card-image { .box-image {
margin: 0.25rem; margin: 0.25rem;
margin-right: 0.75rem; margin-right: 0.75rem;
float: left; float: left;
@ -280,17 +298,17 @@ article header {
width: 100px; width: 100px;
} }
.card-content { .box-content {
display: inline-block; display: inline-block;
padding-left: 1rem; padding-left: 1rem;
} }
.card-image img { .box-image img {
transition: transform .25s ease; transition: transform .25s ease;
image-rendering: smooth; image-rendering: smooth;
} }
.card-image img:hover { .box-image img:hover {
transform: scale(1.1) translateY(-5px); transform: scale(1.1) translateY(-5px);
} }