open graph updates; draft post updates
This commit is contained in:
parent
5f745782b0
commit
58e276cac5
6 changed files with 101 additions and 26 deletions
|
@ -1,10 +1,11 @@
|
|||
{% extends "../../../layouts/post.html" %}
|
||||
|
||||
{% block article %}
|
||||
|
||||
## 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.
|
||||
|
||||
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
|
||||
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 {
|
||||
// by default, we'll just pass the damage directly to
|
||||
// the damage handler
|
||||
void ApplyDamage(IDamageable damageable, float value) {
|
||||
damageable.ReceiveDamage(this, value);
|
||||
}
|
||||
|
@ -79,10 +82,14 @@ public class DamageEventArgs : EventArgs {
|
|||
public readonly float Value;
|
||||
}
|
||||
|
||||
// only one of these should exist and should be globally
|
||||
// accessible so we make it a singleton
|
||||
// this is more of a dispatcher, but that's
|
||||
// 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 {
|
||||
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;
|
||||
|
||||
private PriorityQueue<DamageEventArgs, int> queue = new();
|
||||
|
@ -99,6 +106,8 @@ class DamageEvent {
|
|||
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) {
|
||||
// for simplicity we'll dequeue everything each frame
|
||||
// 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.
|
||||
|
||||
## In search of loot
|
||||
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.
|
||||
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.
|
||||
|
||||
## 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
|
||||
ECS solves many of the problems presented by object oriented programming patterns but it's still in its infancy.
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
title = "A Query about Game State"
|
||||
#created_date = "2024-11-30"
|
||||
description = "An exploration into game architecture and how I would like to see it evolve."
|
||||
|
||||
|
|
|
@ -25,4 +25,3 @@ Hopefully this lets me manage the complexity for whenever I want to add a new fe
|
|||
## 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.
|
||||
{% endblock article %}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
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 %}
|
||||
|
||||
|
|
|
@ -1,22 +1,27 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
||||
<head>
|
||||
{% block head %}
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<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="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">
|
||||
<title>{% block title %}Kitsune Cafe{% endblock title %}</title>
|
||||
<link rel="stylesheet" href="/static/css/main.css">
|
||||
{% endblock head %}
|
||||
</head>
|
||||
<body>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="full-width container">
|
||||
{% block body %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,5 +1,21 @@
|
|||
{% 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 %}
|
||||
{% set date_format = "%d %B, %Y" %}
|
||||
<article>
|
||||
|
|
Loading…
Reference in a new issue