Component Systems for Games

2025-01-03

I just got through a major redesign of Agony Forge's classes for characters and items, in preparation for adding Non-Player Characters (NPCs). Rooms are a bit of a different animal so they didn't get a redesign. For items and characters there was something called a "prototype" that I'm now calling a "template", which rooms don't have. The basic idea is that you build something, for example a sword. You want to stamp out a bunch of copies of that sword in the game. You don't want to individually build every sword in the game by hand, obviously. Thus, you edit the template in the editor, not the actual item.

I haven't implemented area based permissions yet but what I'm planning is that your area has a limited range of IDs assigned to it. In old Circle/Smaug based MUDs you'd get an area number with 100 IDs in it. For example, if your area was number 30 you could create any room, item or creature between 3000 and 3099. Area 31 would have 3100 to 3199. 100 numbers was often not large enough to fully realize the idea for an area so I'm hoping to come up with something more flexible than that, but that's the general concept I'm starting with.

We have two kinds of items and characters: a template and instances. Templates have a buildInstance() method that knows how to copy their properties into a new instance. Whenever you bring an item or a character into the game, you've created an instance. It has a reference back to its template so we know where it came from, but it also has an automatically assigned ID number in the database. Characters work the same way: when you make a new character, you're actually making a template, even though that fact is hidden from you. When you log into the game, it creates an instance of the template for you. When you close your browser the instance gets deleted but the template stays.

When I add NPCs I will have multiple kinds of characters that need slightly different information. I can handle that with components. Players and NPCs have mostly the same data but a few different things. Players have session IDs, some websocket stuff and usernames, while NPCs will need behaviors and other state to help determine their actions. Items and characters also share some of the same information, like location information about where they are in the world. Using inheritance to represent all of this resulted in a lot of duplicated code, which is why I landed on a component system. It's something that is commonly used in games of all kinds. If you've built anything in Unity, Unreal or even Roblox you've already seen component systems at work.

In Agony Forge items and characters all inherit from AbstractMudObject, which is a simple container for four different kinds of components:

  1. Player - information about a human player's connection to the game.
  2. Character - information about a character, like stats, species and profession.
  3. Item - information about an item, like where it can be worn.
  4. Location - information about where something exists in the game world, like what room it's in or who is holding it.

Every item and character has a Location. Anything without one doesn't exist anywhere in the game world. Items have an Item component, player characters (PCs) have both a Player and a Character. NPCs will have a Character and a fifth component I haven't created yet to hold information about their behavior. That's the beauty of a component system: it's modular and expandable. I can create new kinds of components without disturbing the existing systems, and I can reuse components like Location in several places.

In Unity everything is a GameObject with other components attached to it. I debated doing the same with Agony Forge. Reducing everything to a MudObject was possible, but I decided to keep it as MudCharacter and MudItem for one reason: it lets me search the database for characters and items separately without doing any filtering, because they're on separate database tables. It means I can have the same IDs for their templates: Item 1010 and NPC 1010 can both exist without ID collisions, which is important for how area permissions work.

So far the only downside of the component system has been that it makes unit testing complicated. Every object is now built out of several different objects, and they all have to be mocked two or three layers deep. I think I can ease some of that pain by building factory methods to construct fully formed mocks, but I do have to admit it is more complex to test than the old system. If I can find an interesting, elegant way to solve that problem I'll talk about it in a later article.

There you have it! When you're in IEDIT in Agony Forge, you're editing an Item template. When you do CREATE to make an item, you're telling the game to create an instance of your template by copying all of its values into the new item. When you get rid of it with PURGE you're deleting the instance but the template is still there. This structure lets us share code, like Location, between different kinds of objects. It lets us easily create different subtypes of objects without complex inheritance or duplicating code, such as Player Characters and Non-Player Characters, just by giving them different components.

Go to Comments
[ Copyright 2024 Scion Altera ]