diff --git a/src/lib/content/blogPosts.ts b/src/lib/content/blogPosts.ts index 18e0139..1c016fa 100644 --- a/src/lib/content/blogPosts.ts +++ b/src/lib/content/blogPosts.ts @@ -1,6 +1,16 @@ import type { BlogPost } from '$lib/model/BlogPost'; import { asset } from '$app/paths'; +export const dataDrivenSystems: BlogPost = { + id: 'data-driven-systems', + title: 'Game engines, Sliced bread, and data-driven systems', + description: 'Designing data-driven systems by separating behavior from content.', + date: '16/03/2026', + image: asset('/images/data-driven-systems/sliced-bread-yaml.png'), + tags: ['games', 'overengineering'], + path: '/(app)/blog/(posts)/data-driven-systems', +}; + export const piano: BlogPost = { id: 'piano', title: 'Video game piano', @@ -34,16 +44,4 @@ export const foolsball: BlogPost = { path: '/(app)/blog/(posts)/foolsball', }; -export const blogPosts: BlogPost[] = [ - // piano, - // { - // id: 'unique-puzzle', - // title: 'Unique Puzzles', - // description: 'A short story about the awesomeness of puzzles', - // date: '01/01/2026', - // image: '', - // tags: ['games'], - // path: '/(app)/blog/(posts)/puzzles', - // }, - beatBox, -]; +export const blogPosts: BlogPost[] = [dataDrivenSystems, beatBox]; diff --git a/src/routes/(app)/blog/(posts)/data-driven-systems/+page.svx b/src/routes/(app)/blog/(posts)/data-driven-systems/+page.svx new file mode 100644 index 0000000..732cc7a --- /dev/null +++ b/src/routes/(app)/blog/(posts)/data-driven-systems/+page.svx @@ -0,0 +1,100 @@ + + +Years ago I released the [Incremental Game Template](https://123ishatest.github.io/igt-docs/) (IGT) which I then called "*A collection of useful scripts to help you develop incremental games*". +While that was technically true, it served mostly as an archive for me to store all the game development work I had done over the years. +As the template grew it not only contained more game mechanics, but also more concrete implementations of my games. +It became more and more difficult to separate mechanics from content, making it nearly impossible for people who are not me to benefit from the project. + +Today we will dive into those distinctions alongside the lessons I've learned deprecating IGT, and how my new game engine [Ludiek](https://ludiek.123ishatest.com) does everything way better. + +## Behaviour vs Content +Over the years I have become a big proponent of separating behaviour from content. +This is most prominent in applications with a large amount of content, such as games. +But I believe keeping this divide in mind will help you design cleaner applications no matter what you are creating! + +- *Behaviour* encompasses the logic of your application, decisions that need to be taken based on the state and content we are operating on. +- *Content* is the static data of your application. Be it items in an RPG, routing tables in an SPA, Content in a CMS. It is *not* state. + +One advantage becomes immediately obvious, the implementation of behaviour is pure and deterministic. +This improves testability, composability and maintainability! + +## In practice +Enough theory crafting, what does it look like in practice? What went wrong with IGT? +IGT contained functionality for `Items` and `Inventory` as most games use some sort of an inventory system. +In its most basic form it is a way to track how many you have of certain items. +Some items have special interactions: Materials can be processed, Armour can be equipped, and Consumables can be... consumed. +Here is how we would define a consumable `Bread` item in IGT, it restores 10 health when eaten. + + +```ts +class SlicedBread extends Consumable { + public name = "Sliced Bread" + + public consume() { + player.restoreHealth(10) + } +} +``` + +On first sight it looks sensible, there is little boilerplate and the consume function is succinct. +But what if the `consume` function grows more complex? +What if we have different kinds of food that all trigger the same code when consumed? We'd have to repeat the consuming logic everywhere. +But even worse, this game content is now defined in code, meaning game designers need to dive into the codebase to balance the game. +They don't like that. + +Let's separate the behavior from the content! We need to restore health based on the provided amount. +```ts +class RestoreHealth extends ConsumableEffect { + type: 'restore-health'; + + public args: { + amount: number, + } + + public apply(args) { + player.restoreHealth(args.amount) + } +} +``` + +And the content becomes much cleaner! + +```yaml +# sliced-bread.item.yaml +name: Sliced Bread +onConsume: + type: restore-health + amount: 10 +``` + +While there is an initial investment of having to create the `RestoreHealth`, we now have it forever. +Every item can be turned into a consumable by adding 3 lines of data, and no extra code. + +That is the essence of data-driven design: Isolate behaviors as small as possible, and let your static data compose it to achieve your desired behaviour! + +Game designers often believe that data-driven games lose some of their magic. +When all your content has to adhere to rigid systems, unique mechanics are more difficult to implement. +While this could happen, realise you are expecting your developers not to overengineer a system that can support even your wildest dreams. + +## Ludiek + +Speaking of wild dreams, with this new data-driven approach I have been designing and building [_Ludiek_](https://ludiek.123ishatest.com/), the game engine that focuses on the fun. +It is the spiritual successor of IGT, but with a plugin-based architecture so you can mix and match whatever you need! + +Today we mostly discussed moving from level 1 towards level 2, but Ludiek pushes all the way to level 3! + +```ts +// level 1 // level 2 // level 3 // Level ∞ +if (money > 100) { if (money > upgrade.cost)) { if (canAfford(upgrade.cost)) { do() + money -= 100 money -= upgrade.cost; spend(upgrade.cost) + power += 2; power += upgrade.powerGain; apply(upgrade.benefit) +} } } +``` + +With these abstractions we can create plugins that operate on the most abstract of concepts. + +If you are curious, you can read more on it [here](https://ludiek.123ishatest.com/docs/introduction/welcome), or stay tuned for the next blog post! + +Thanks for reading! diff --git a/static/images/data-driven-systems/sliced-bread-yaml.png b/static/images/data-driven-systems/sliced-bread-yaml.png new file mode 100644 index 0000000..56150d2 Binary files /dev/null and b/static/images/data-driven-systems/sliced-bread-yaml.png differ