open graph updates; draft post updates

This commit is contained in:
Rowan 2024-12-04 03:27:23 -06:00
parent 5f745782b0
commit 58e276cac5
6 changed files with 101 additions and 26 deletions

View file

@ -1,10 +1,11 @@
{% extends "../../../layouts/post.html" %} {% extends "../../../layouts/post.html" %}
{% block article %} {% block article %}
## Preface ## Preface
I'm an indie game dev that has an interest in making tools that I think would improve my life as a game developer in the hopes that it can help others. I'm not an expert in any of the topics that this article covers. I have no formal education in game engine development or architecture, systems programming, or anything else. I went to university for Computer Science with a specialization in game design & development when I was in my early 20s and the only thing I learned is that there are better ways to spend tens of thousands of dollars. I'm an indie game dev that has an interest in making tools that I think would improve my life as a game developer in the hopes that it can help others. I'm not an expert in any of the topics that this article covers. I have no formal education in game engine development or architecture, systems programming, or anything else. I went to university for Computer Science with a specialization in game design & development when I was in my early 20s and the only thing I learned is that there are better ways to spend tens of thousands of dollars.
Everything in this article is drawn directly from my experiences working in tech and game development. Everything in this article is drawn directly from my experiences working in game development.
## Theseus' Shield ## 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. 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.
@ -47,6 +48,8 @@ interface ICollectable {
} }
interface IDamaging { interface IDamaging {
// by default, we'll just pass the damage directly to
// the damage handler
void ApplyDamage(IDamageable damageable, float value) { void ApplyDamage(IDamageable damageable, float value) {
damageable.ReceiveDamage(this, value); damageable.ReceiveDamage(this, value);
} }
@ -79,10 +82,14 @@ public class DamageEventArgs : EventArgs {
public readonly float Value; public readonly float Value;
} }
// only one of these should exist and should be globally // this is more of a dispatcher, but that's
// accessible so we make it a singleton // an unnecessary implementation detail.
// in a real scenario where other systems would be reading
// these events too, this would subscribe to a Mediator object
class DamageEvent { class DamageEvent {
private static readonly DamageEvent instance = new Instance(); // only one of these should exist and should be globally
// accessible so we make it a singleton
private static DamageEvent instance = new Instance();
public static DamageEvent Instance => instance; public static DamageEvent Instance => instance;
private PriorityQueue<DamageEventArgs, int> queue = new(); private PriorityQueue<DamageEventArgs, int> queue = new();
@ -99,6 +106,8 @@ class DamageEvent {
queue.Enqueue(args, priority); queue.Enqueue(args, priority);
} }
// we'll dispatch events with the trick rate
// 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
// and pass it to the damage handler // and pass it to the damage handler
@ -121,8 +130,52 @@ 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. 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.
## In search of loot 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.
Managing game state is difficult. As a game grows, so too does the amount of objects active at any given time. Dozens, hundreds, or even thousands of entities each with their own behaviors, goals, and concerns that interact with each other in different ways. This explosion in complexity can be exceedingly hard to manage even in games that are small in scope. Games, even at their simplest, tend to be *highly* complex.
## Reading game state
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 HealthChangedArgs : EventArgs {
public readonly float PreviousValue;
public readonly float CurrentValue;
}
class Healthbar : UIElement {
private float _currentHealth;
private float currentHealth {
get => _currentHealth;
set {
if (value != _currentHealth) {
_currentHealth = value;
Redraw();
}
}
}
protected void OnInitialize() {
// register to the player spawn event
PlayerSpawnEvent.Register(OnPlayerSpawned);
}
private void OnPlayerSpawned(object sender, Player player) {
// subscribe specifically to that player's events
player.health.Register(OnHealthChanged);
}
private void OnHealthChanged(object sender, HealthChangedArgs args) {
currentHealth = args.CurrentValue;
}
}
```
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.
global variable, then array
use examples of multiplayer, then enemies with health bars
## Enter ECS ## Enter ECS
ECS solves many of the problems presented by object oriented programming patterns but it's still in its infancy. ECS solves many of the problems presented by object oriented programming patterns but it's still in its infancy.

View file

@ -1,3 +1,4 @@
title = "A Query about Game State" title = "A Query about Game State"
#created_date = "2024-11-30" #created_date = "2024-11-30"
description = "An exploration into game architecture and how I would like to see it evolve."

View file

@ -25,4 +25,3 @@ Hopefully this lets me manage the complexity for whenever I want to add a new fe
## What's left ## What's left
There's a bunch of stuff I didn't get to do. Two things I really want are an RSS feed generator, and streaming file inputs. Right now, the library loads the entire file into memory and works on it when it should be streaming it and processing it in chunks. I didn't have the time or energy to work on that, but maybe later. There's a bunch of stuff I didn't get to do. Two things I really want are an RSS feed generator, and streaming file inputs. Right now, the library loads the entire file into memory and works on it when it should be streaming it and processing it in chunks. I didn't have the time or energy to work on that, but maybe later.
{% endblock article %} {% endblock article %}

View file

@ -46,5 +46,6 @@ There's a bunch of things Roxy *doesn't* do
I ended up making bash files to do all that though and they're available in the repository for this website if you'd like to see or use them. I ended up making bash files to do all that though and they're available in the repository for this website if you'd like to see or use them.
The code is a disaster and needs to be refactored. I'll do that the minute I have a really good reason to. For now, it works well enough for me, and I *really* like using it. This is one of the times I'm very happy I decided to make something superfluous for myself. The code is a disaster and needs to be refactored. I'll do that the minute I have a really good reason to. For now, it works well enough for me, and I *really* like using it. This is one of the times I'm very happy I decided to make something superfluous for myself.
{% endblock article %} {% endblock article %}

View file

@ -1,22 +1,27 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head>
<head>
{% block head %} {% block head %}
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge"> <meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta property="og:url" content="https://kitsu.cafe" />
<meta property="og:type" content="website" />
<meta name="description" content="A portfolio and blog for Rowan, a game developer, artist, and musician."> <meta name="description" content="A portfolio and blog for Rowan, a game developer, artist, and musician.">
<meta name="keywords" content="Unity, Bevy, JavaScript, Rust, C#, HTML, CSS, Game Development, Programming, Music, Art"> <meta name="keywords"
content="Unity, Bevy, JavaScript, Rust, C#, HTML, CSS, Game Development, Programming, Music, Art">
<meta name="author" content="Rowan"> <meta name="author" content="Rowan">
<title>{% block title %}Kitsune Cafe{% endblock title %}</title> <title>{% block title %}Kitsune Cafe{% endblock title %}</title>
<link rel="stylesheet" href="/static/css/main.css"> <link rel="stylesheet" href="/static/css/main.css">
{% endblock head %} {% endblock head %}
</head> </head>
<body>
<body>
<div class="full-width container"> <div class="full-width container">
{% block body %} {% block body %}
{% endblock %} {% endblock %}
</div> </div>
</body> </body>
</html>
</html>

View file

@ -1,5 +1,21 @@
{% extends "page.html" %} {% extends "page.html" %}
{% block head %}
{{ super() }}
<meta property="og:title" content="{{ this.title }}" />
<meta property="og:url" content="{{ this.path }}" />
<meta property="og:type" content="article" />
{% if this.locale %}
<meta property="og:locale" content="{{ this.locale }}" />
{% endif %}
{% if this.description %}
<meta property="og:description" content="{{ this.description }}" />
{% endif %}
{% if this.image %}
<meta property="og:image" content="{{ this.image }}" />
{% endif %}
{% endblock %}
{% block content %} {% block content %}
{% set date_format = "%d %B, %Y" %} {% set date_format = "%d %B, %Y" %}
<article> <article>