Hooray! It's time for more Aimless Ramblings by Matt!
Entity Component Systems (ECS) have become more and more popular over the years. With successes like Overwatch, mainstream game developers have started using this design paradigm to great effect. A "pure" ECS follows a few simple rules:
Not too hard to understand, right? The goal is to forgo inheritance almost completely in favor of having "bags of Components", each of which is controlled by a system. In practice, this paradigm can be very messy unless you know exactly what you're doing. Race conditions and null references are a huge pain in the ass. ECSes (that pluralization felt wrong to type...) are amazing for server-side computation, and can provide huge performance improvements, but I'm honestly not smart enough to write one that would take advantage of these benefits. So what can I do? Enter EC. You'll notice the acronym is one letter off ECS. That's particularly important. ECs are ECSes without Systems. Components contain their own logic, rather than just data. Taking this one step further, an Entity could also be an object with a reference to its own Components. Suddenly, this is looking a bit more manageable without being a tree of inheritance. Unity uses a similar design philosophy, and is generally seen as beginner-friendly.
Let's take a look at an example of a simple Entity class:
//Entity.cs
using System;
using System.Linq;
using System.Collections.Generic;
public class Entity
{
//A unique identifier. Makes it easier to see if two entities are the same.
public readonly int ID = -1;
//Our component list
private List<Component> components;
//Constructor that takes in an ID number.
public Entity(int id)
{
this.ID = id;
}
//Loop through all our components and call their Update() method. We will be using this later.
public void Update(float deltaTime)
{
foreach (Component c in components)
{
c.Update(deltaTime);
}
}
//When we destroy an Entity, we also want to make sure to destroy its Components.
//There may be Components that want to know when an Entity is destroyed.
public void Destroy()
{
foreach (Component c in components)
{
c.Destroy();
}
components.Clear();
}
//Return a component of a specified type
public T GetComponent<T>() where T : Component
{
return (T)components.FirstOrDefault(x => x.GetType() == typeof(T));
}
//Do we have this component type?
public bool HasComponent<T>() where T : Component
{
return components.Any(x => x.GetType() == typeof(T));
}
//Add a component
public T AddComponent<T>(params object[] p) where T : Component
{
//Create a new component of type T, with parameters "p".
T t = (T)Activator.CreateInstance(typeof(T), p);
//Add the component to the list and initialize it.
components.Add(t);
t.Initialize();
return t;
}
//Remove a component of a specified type.
public void RemoveComponent<T>() where T : Component
{
if (components.Any(x => x.GetType() == typeof(T)))
{
components.Remove(components.Find(x => x.GetType() == typeof(T)));
}
}
//Test for equality using IDs.
public static bool operator ==(Entity e1, Entity e2)
{
return e1.ID == e2.ID;
}
public static bool operator !=(Entity e1, Entity e2)
{
return e1.ID != e2.ID;
}
}
Pretty simple, right? We can add, remove, and check for specific Components by type. Keep in mind, with this implementation, entities can only get or remove the first Component of a specific type. I'll leave it as an exercise for the reader on how to combat this problem. If you are having lots of trouble working around this restraint, drop me a line at cynicalapathygames@gmail.com and I'll do my best to walk you through it. I've also left out some boilerplate code, like GetHashCode.
Anyways, let's define a Component:
//Component.cs
public class Component
{
//A reference to our entity.
public readonly Entity entity;
public Component(Entity entity)
{
this.entity = entity;
}
public virtual void Initialize()
{
//Do any initialization here.
}
//We can override this in child classes.
public virtual void Update(float deltaTime)
{
//Empty on purpose. By itself, it does nothing.
}
//Same as above. Maybe child classes will want to know when we are destroyed.
public virtual void Destroy()
{
//Empty on purpose, same as above.
}
}
Even simpler. It just contains a reference to its held entity! I added some virtual methods so Component's children can inherit functionality. Now, we have an Entity class and a Component class, but we can't do much with just this. Let's make a component containing the position of the Entity:
//PositionComponent.cs
public class PositionComponent : Component
{
//Our coordinates
public float x;
public float y;
//A "default" constructor that takes only an Entity.
public PositionComponent(Entity entity) : base(entity)
{
//We don't need to define these default values, but I like to be explicit.
x = 0.0f;
y = 0.0f;
}
//Construct the object, passing the Entity to the base class.
public PositionComponent(Entity entity, int x, int y) : base(entity)
{
this.x = x;
this.y = y;
}
}
Neat, so we have a component that holds two float values that denote a position. I think now is a good time to get into how Entities are constructed. Your preferred implementation may be different than mine, but I like to use a singleton EntityManager class to keep track of all entities. Let's go ahead and write that. We will need to add and remove entities, as well as call their Update() method.
//EntityManager.cs
using System.Linq;
using System.Collections.Generic;
public class EntityManager
{
//The singleton instance for our EntityManager. There will only ever be one.
public static EntityManager instance;
//Our Entities
private List<Entity> entities;
public EntityManager()
{
//Remove old EntityManager, if it exists.
if (instance != null)
{
instance.Clean();
}
//Set the singleton to this instance of EntityManager
instance = this;
entities = new List<Entity>();
}
public Entity AddEntity()
{
//By making NextID static, each call to AddEntity() will increment this number.
//This means every Entity will have its own unique ID.
static int NextID = 0;
Entity entity = new Entity(NextID++);
//Add the new entity to our list.
entities.Add(entity);
}
//Remove an Entity by reference.
public void RemoveEntity(Entity entityToRemove)
{
//If we have an Entity with this ID, we remove it.
if (entities.Contains(entityToRemove))
{
//Destroy the unused Entity.
entityToRemove.Destroy();
entities.Remove(entityToRemove);
}
}
//Remove an Entity by ID.
public void RemoveEntity(int ID)
{
Entity entityToRemove = entities.FirstOrDefault(x => x.ID == ID);
//If we have an Entity with this ID...
if (entityToRemove != default(Entity))
{
//Destroy the unused Entity.
RemoveEntity(entityToRemove);
}
}
//We update all our Entities here, passing deltaTime.
public void Update(float deltaTime)
{
foreach (Entity e in entities)
{
e.Update(deltaTime);
}
}
//Clean out all Entities, destroying them all and clearing the list.
//We do this when we create a new EntityManager.
public void Clean()
{
for (int i = entities.Count - 1; i > 0; i--)
{
entities[i].Destroy();
}
entities.Clear();
}
}
The EntityManager serves as a way for the program to act on all created Entities. We could include methods for Draw(), PostUpdate(), etc. if we really wanted to. I'm doing my best to keep this write-up simple though. Let's look more into what we can do with components. We have a position that, as of yet, does not change. Let's fix that. To make an Entity move, let's give it velocity.
//VelocityComponent.cs
public VelocityComponent : Component
{
public float x;
public float y;
//In order for us to have velocity, we need a position. To save typing here, I'll save a reference in the component itself.
//A better way would be to define what Components this Component requires at compile time.
private PositionComponent position;
//Similar to what we did with PositionComponent, we pass entity to the base.
//We could also give this Component an initial velocity, but I'll skip that for brevity.
public VelocityComponent(Entity entity) : base(entity)
{
x = 0.0f;
y = 0.0f;
}
//Override the Initialize() function from Component. I'm using this to acquire references to necessary components.
public override void Initialize()
{
//Get the reference to the position component
position = entity.GetComponent<PositionComponent>();
//If we don't have one, create a placeholder with default parameters.
if (position == null)
{
position = entity.AddComponent<PositionComponent>(entity);
//We could also write this as:
// "position = entity.AddComponent<PositionComponent>(entity, 0.0f, 0.0f);
}
}
//An external means of giving us velocity.
public void AddVelocity(float vx, float vy)
{
x += vx;
y += vy;
}
//Override Component's Update() method with our own functionality.
public override void Update(float deltaTime)
{
//Simply add our velocity to the position.
position.x += x * deltaTime;
position.y += y * deltaTime;
}
}
Neato! It's a little rough around the edges, but we now have a very simple Entity Component pattern. By creating new components, we can effectively create a "bunde" of functionality very easily. You've probably noticed that this is very similar to what the Unity game engine already does.
Anyways, thank you for enduring this write-up. It's been a while since I've done a pseudo-tutorial, so I may be rusty. Best of luck on your projects!
Quests are a very important part of Axu. Contrary to many "classic" roguelikes, I decided a static storyline would be more in-line with my goals. I grew up with CRPGs of the late 90s, especially Baldur's Gate, and I think the inspiration is fairly obvious. At first, Axu's quests were hard to write. The system I wrote did not at all facilitate the kinds of experiences I wanted the player to have. Things broke, and it was a constant task to fix all these edge cases that kept being reported. A little while back, I decided to do a complete rewrite of the system, and boy was it a good idea to do so. With this blog post, I'd like to outline some of the things I learned (though I'm sure they may be obvious to most) while messing around with different ideas.
Axu's quests consist of several components, but the two most important ones are Goals and Events. Goals are tasks that the player needs to accomplish, such as killing a specific enemy type, going to a place, or handing over an item to an NPC. Quest Goals subscribe to specific events, stemming from actions performed in the world. The C# Action and Func types are perfect for this, as I am able to tell a class to call a specific function with pre-defined arguments whenever something important happens. Each quest has an array of Goals, and upon initialization these goals subscribe to whatever events they would like to know about. Is it a goal to kill an enemy type? Listen for when an NPC is killed, then check to see if it is the correct type. This can be used to see if a goal is not able to be completed. If a target NPC dies, but you were supposed to talk to them, the quest should fail and be removed from your journal. The implementation is pretty simple. New goals are made by deriving from this basic class:
public class Goal : EventContainer
{
protected Quest myQuest;
public virtual void Init()
{
RunEvent(myQuest, QuestEvent.EventType.OnStart);
}
public virtual void Complete()
{
RunEvent(myQuest, QuestEvent.EventType.OnComplete);
//The quest completes the goal. If no completed goals are left, the player is given a reward.
myQuest.CompleteGoal(this);
}
public virtual void Fail()
{
RunEvent(myQuest, QuestEvent.EventType.OnFail);
myQuest.Fail();
}
}
(EventContainer is a simple class that can add or run specific QuestEvents. Removed some code for brevity.)
Events are where the fun happens. They are the logic that is run at specific times. Each quest has three main events: OnStart, OnComplete, and OnFail. The names are pretty self-explanatory for their use. Goals also have these three events, granting much more flexibility. Some of the more complicated quests trigger events between goals, like teleporting the player to a specific location upon taking the quest, opening a door upon completing the first step, then giving a quest to an NPC upon completion. The base QuestEvent class is even shorter than for QuestGoal above. It just needs a virtual function for running an event.
public class QuestEvent
{
public Quest myQuest;
public QuestEvent() { }
public virtual void RunEvent()
{
//Implementation done in derived class.
}
public enum EventType
{
OnStart, OnComplete, OnFail
}
}
So now that we have these two building blocks, we can start to construct some cool stuff. So let's create an event that spawns an NPC:
public class SpawnNPCEvent : QuestEvent
{
readonly string npcID;
readonly Coord worldPos;
readonly int elevation;
readonly Coord localPos;
public SpawnNPCEvent(string nID, Coord wPos, Coord lPos, int ele)
{
npcID = nID;
worldPos = wPos;
localPos = lPos;
elevation = ele;
}
public override void RunEvent()
{
NPC n = new NPC(EntityList.GetBlueprintByID(npcID), worldPos, localPos, elevation);
//Adds the NPC to the quest's list of spawned creatures.
myQuest.SpawnNPC(n);
}
}
And maybe a goal that has us killing all NPCs spawned by this quest:
public class SpecificKillGoal : Goal
{
public SpecificKillGoal(Quest q)
{
myQuest = q;
}
public override void Init()
{
base.Init();
//Subscribe to the event.
EventHandler.instance.NPCDied += NPCKilled;
}
void NPCKilled(NPC n)
{
for (int i = 0; i < myQuest.spawnedNPCs.Count; i++)
{
//UID is the Unique ID (integer) of the NPC.
if (n.UID == myQuest.spawnedNPCs[i])
{
myQuest.spawnedNPCs.Remove(n.UID);
break;
}
}
if (myQuest.spawnedNPCs.Count <= 0)
{
Complete();
}
}
public override void Complete()
{
//Unsubscribe from the event.
EventHandler.instance.NPCDied -= NPCKilled;
base.Complete();
}
public override void Fail()
{
//Unsubscribe from the event.
EventHandler.instance.NPCDied -= NPCKilled;
base.Fail();
}
}
Pretty simple, right? Having goals at specific intervals in each quest allows some level of flexibility. Sure it isn't perfect, but so far I have yet to run into any of the issues plaguing my previous attempts. Let's look at some Json from a more complex quest. This is an arena challenge. You are teleported into the ring, which you cannot escape from. Enemies are spawned in with you, and you need to kill them all. When they are all dead, you may leave. Upon completing the quest, the Arena Master can give you the next challenge. If you die, and are not playing on a permadeath difficulty, the Arena Master will be able to give you the same quest again for another try.
{
"Name" : "Arena Wave 3",
"ID" : "arena2",
"Start Dialogue" : "Wanna step into the arena? Be wary. These fights are to the death.",
"Description" : "Defeat the third wave in the arena.",
"Fail On Death" : true,
"Rewards" : {
"XP" : 60,
"Money" : 100
},
"Steps" : [
{ "Goal" : "Kill Spawned",
"Events" : [
{
"Event" : "OnComplete",
"Remove Blockers" : { "Coordinate" : "Arena", "Elevation" : -1 }
}
]
},
{ "Goal" : "Talk To", "NPC" : "arenamaster" }
],
"Events" : [
{
"Event" : "OnStart",
"Spawn NPC" : { "NPC" : "bandit1", "At" : "Arena", "Ele" : -1, "Pos" : { "x" : 28, "y" : 16 } },
"Spawn NPC" : { "NPC" : "bandit1", "At" : "Arena", "Ele" : -1, "Pos" : { "x" : 28, "y" : 13 } },
"Spawn NPC" : { "NPC" : "bandit0", "At" : "Arena", "Ele" : -1, "Pos" : { "x" : 28, "y" : 15 } },
"Set Elevation" : -1,
"Set Position" : { "x" : 18, "y" : 15 },
"Spawn Blocker" : { "At" : "Arena", "Pos" : { "x" : 23, "y" : 15 }, "Ele" : -1 }
},
{
"Event" : "OnComplete",
"Give Quest" : { "NPC" : "arenamaster", "Quest" : "arena3" }
},
{
"Event" : "OnFail",
"Remove Spawns" : true,
"Remove Blockers" : { "At" : "Arena", "Ele" : -1 },
"Give Quest" : { "NPC" : "arenamaster", "Quest" : "arena2" }
}
],
"End Dialogue" : "Good show, here's your pay. Come back later for more fights."
}
(Sorry about the messy syntax.)
So that's the gist of it. I've left out some cool stuff like goals that can have one of several conditions met, non-sequential quests, and others. I hope you've learned something from my rambling!
Cheers,
Matt (Cynapse)
A few days ago, I showed off a gif of me flicking a switch, and having a door open on the other side of the level. The two were connected by wires, easily identifiable on the floor. The reaction was much more positive and large than I expected, which has prompted this write-up on how it was done. I'll do my best to share my process, though I can attribute the ease of its implementation to Axu's codebase. (The progress shown in the gif was put together in about fifteen minutes.)
First, I'll go over a little of how Axu's archetecture works, as it is important to demonstrate that my particular implementation may not work for all games. There are three kinds of entities present that make up a map: The background tile, the objects in/on that tile, and the creature occupying it. Data about what is in a particular grid tile is stored in a Cell object. The important one here is the MapObject class. These objects are the middle layer, and can serve a variety of purposes. Some objects block creatures from entering their cell, some (like doors) can be interacted with.
The important objects in this scenario are:
When the player interacts with an object like the switch, a "pulse" is sent out to all adjacent Cells. Each Cell then tells all the objects within them to receive it. If the objects receiving the pulse can also send one out, they do so, minus the direction from where it came from. This occurs until the chain is broken by objects that can no longer send out any more pulses. It looks a bit like this:
//MapObject.cs
//Create or continue a pulse
const int MAX_MOVE_COUNT = 250; //The maximum number of "moves" a pulse can make before it is cancelled.
bool canSendPulse; //Can this object send out pulses to adjacent objects?
bool canReceivePulse; //Can we listen for pulses?
bool isOn; //Our current state. Prevents objects from receiving the same pulse more than once.
//For me, this is called from a Lua function set in the object's blueprint.
public void StartPulse()
{
isOn = !isOn;
SendPulses(pos, 0 isOn);
}
void SendPulses(Coordinate previous, int moveCount, bool on)
{
//To prevent excessive loops
if (moveCount >= MAX_MOVE_COUNT)
return;
for (int x = -1; x <= 1; x++)
{
for (int y = -1; y <= 1; y++)
{
//Skip this object's position
if (x == 0 && y == 0)
continue;
//Skip diagonals
if (Math.Abs(x) + Math.Abs(y) > 1)
continue;
//Get the Cell in the specified direction. Null if out of bounds.
Cell c = TileMap.GetCellAt(pos.x + x, pos.y + y);
//If the adjacent cell is not in the previous direction, send it.
if (c != null && c.position != previous)
c.ReceivePulse(pos, moveCount, on);
}
}
}
//Cell.cs
//The Cell then sends the pulse to all its objects.
List<MapObject> mapObjects; //The objects within this Cell.
public void ReceivePulse(Coordinate previous, int moveCount, bool on)
{
foreach (MapObject m in mapObjects)
{
m.ReceivePulse(previous, moveCount, on)
}
}
//MapObject.cs
//Get the pulse from its Cell. Do any necessary logic
public void RecievePulse(Coordinate previous, int moveCount, bool on)
{
if (canReceivePulse)
{
//Calls any logic that is needed for this specific object. Opening doors, Turning on terminals, etc...
if (on != isOn)
Interact();
isOn = on;
//Continues the pulse, keeping previously checked Cell around to make sure the flow doesn't end up going backwards.
if (canSendPulse)
SendPulses(previous, ++moveCount, isOn);
}
}
So that's pretty simple, right? I was initially thinking a flood-fill might be better, but came up with this as a quick test. I'm not sure if there would be any benefit to optimizing this too much, as it runs pretty fast as-is. There are some gotchas to keep in mind while doing this kind of work. First, loops can quickly kill your program, as you might expect. Managing the maximum number of allowed steps can help. Second, keeping track of the previous direction is important. Flowing backwards is an easy way to create an infinite loop. Third, without keeping track of the object's current state, you can have the same signal being sent to the same object multiple times, especially with looping or branching shapes. I had some odd behavior where I thought doors were not being triggered when they were actually being interacted with more than once.
That's pretty much it! If you have any comments, questions or suggestions, hit me up on Twitter @CynApGames, or email me at cynicalapathygames@gmail.com. I'm always up to chatting!
Cheers,
Matt (Cynapse)
A looping section of wires with multiple doors.