Background
I. Game Objects
II. Interactions (current)
III. Serialization
The previous article in the series, Game Objects, gave a broad overview of entities and parts, an implementation for the Entity and Part class, and a simple example showing a lopsided fight between a monster Entity and a helpless villager Entity. It was basic enough to demonstrate how to use the Entity and Part classes but it didn't address entity management or interaction in detail.
In addition to addressing these topics, the example will be expanded so that the villager's friends will join the battle against a mob of monsters. The previous article focused more on how entities and parts were structured. In this article, I will focus on ways to handle interactions between high-level classes, entities, and parts. Some examples of interactions are A.I. to target enemies or support allies, spell effects such as healing and summoning, and dealing damage to another character.
I will also introduce a new class to the Entity-Parts framework: EntityManager
, and an event system. The implementation of the framework will be provided in Java and C++ w/boost.
Handling Interactions
Interactions between Parts of the same Entity
Putting logic in parts is a good idea when you want to model interactions between parts of the same entity. For example, a part for health regeneration could simply get the health part of its parent entity and increase the health every update step. In the Entities-Parts framework you add logic to parts by overriding the initialize
, update
, or cleanup
methods.
Interactions between Multiple Entities
Putting logic in high-level classes such as systems and managers is a good idea when you want to model interactions between entities. For example, an Entity
such as a warrior will need a list of enemy entities to attack. Implementing all of this logic in parts is difficult because the parts would be responsible for finding references of enemy entities to damage.
As seen in the previous article's MonsterControllerPart
, it is difficult to pass in references to other entities to parts without issues appearing. A better approach is to have a high-level class, e.g. a BattleSystem
, become responsible for finding enemy entities because they inherently keep references to all entities.
Events
Events are commonly used in programming and are not restricted to games. They allow objects to immediately perform actions when something interesting happens. For example, when a missile receives a collision event, its collision event handler method makes it explode. In this fashion, the missile doesn't have to check each frame if it collided with anything, only when the collision happened. Events can sometimes simplify interactions between multiple objects.
Event managers allow an object to wait on a certain event to happen and then act upon it while being decoupled from objects publishing the event. An event manager is a centralized hub of event publication/subscription that allows entities or other systems to simply subscribe to the event manager instead of the individual objects who are publishing the events.
Likewise, the publishers can just publish an event to the event manager and let the event manager do the work of notifying the subscribers of the event. For example, the entity manager listens for an event where a new entity is created. If a new entity is spawned, say by a summon spell, the entity manager receives the event from the event manager and adds the new entity. It doesn't have to contain a reference to the summon spell.
RPG Battle Example (continued)
Now that we've discussed a high-level overview of handling interactions between high-level classes, entities, and parts, let's continue the example from the previous article. The RPG Battle Example has been completely refactored to support a larger battle between two teams of characters: Monsters vs. Villagers.
Each side will have a Meleer, Ranger, Flying Ranger, Support Mage, and Summoner. Monster names start with M. and villager names start with V. Here is output of a round of combat in the updated example. Each character's information is displayed as well as the action it took during the current simulation time:
SIMULATION TIME: 3.0
M.Meleer1 is dead!
M.Ranger1 - Health: 89.25 - Mana: 0.0
Attacking with Bow. 34.0 damage dealt to V.Ranger1
M.FlyingRanger1 - Health: 100.0 - Mana: 0.0
Attacking with Bow. 23.0 damage dealt to V.Ranger1
M.SupportMage1 - Health: 100.0 - Mana: 56.0
Casting Light Heal. Healed 30.0 health on M.Ranger1
M.Summoner1 - Health: 100.0 - Mana: 39.0
Attacking with Staff. 14.0 damage dealt to V.Ranger1
V.Ranger1 - Health: 28.25 - Mana: 0.0
Attacking with Bow. 29.0 damage dealt to M.Ranger1
V.FlyingRanger1 - Health: 100.0 - Mana: 0.0
Attacking with Bow. 21.0 damage dealt to M.Ranger1
V.SupportMage1 - Health: 100.0 - Mana: 34.0
Casting Light Heal. Healed 30.0 health on V.Ranger1
V.Summoner1 - Health: 100.0 - Mana: 39.0
Attacking with Staff. 12.0 damage dealt to M.Ranger1
Demon - Health: 100.0 - Mana: 0.0
Attacking with Claw. 28.0 damage dealt to V.Ranger1
Demon - Health: 100.0 - Mana: 0.0
Attacking with Claw. 21.0 damage dealt to M.Ranger1
Now, let's walk through key sections of the example code.
Character Creation
In the updated example, characters of differing roles are created using a CharacterFactory
. The following is a helper method to create a base/classless character
. Note the parts that are added. They provide an empty entity with attributes that all characters should have such as name, health, mana, stat restoration, alliance (Monster or Villager), and mentality (how the AI reacts to certain situations).
private static Entity createBaseCharacter(String name, float health, float mana, Alliance alliance, Mentality mentality) {
// create a character entity that has parts all characters should have
Entity character = new Entity();
character.attach(new DescriptionPart(name));
character.attach(new HealthPart(health));
character.attach(new ManaPart(mana));
character.attach(new RestorePart(0.01f, 0.03f));
character.attach(new AlliancePart(alliance));
character.attach(new MentalityPart(mentality));
return character;
}
Then, there are methods for creating specific characters such as the flying ranger. The method to create the flying ranger calls createBaseCharacter
method to create a base character with 100 health, 0 mana, and an Offensive Mentality
that tells it to attack with its weapon and ignore defense or support. We add the equipment part with a bow weapon that does 15-35 damage and the flying part to make a base character a flying ranger. Note that weapons with an AttackRange
of FAR
can hit flying entities.
public static Entity createFlyingRanger(String name, Alliance alliance) {
Entity ranger = createBaseCharacter(name, 100, 0, alliance, Mentality.OFFENSIVE);
Weapon bow = new Weapon("Bow", 15, 35, AttackRange.FAR);
ranger.attach(new EquipmentPart(bow));
ranger.attach(new FlyingPart());
return ranger;
}
As you can see, it is relatively easy to create numerous character roles or change existing character roles through reuse of parts. Take a look at the other CharacterFactory
methods to see how other RPG classes are created.
Entity Management
The EntityManager
is a centralized class for entity retrieval, addition, and removal from the game world. In the example, the EntityManager
keeps track of the characters battling eachother. The list of characters is encapsulated in the Entity Manager to prevent it from being accidentally altered or replaced. The game loop uses the entity manager to retrieve all the entities and update them. Then, update is called on the entityManager
so that it updates its entity list according to recently created or removed entities. Main.java:
for (Entity entity : entityManager.getAll()) {
entity.update(delta);
}
entityManager.update();
Events
To create a summoner with a summon spell, we need to find a way to notify the EntityManager
that a new entity has been summoned so the EntityManager
can add it to the battle. This can be accomplished with events. The EventManager
is passed to the summon spell's use method and it calls the notify
method on the EventManager
to notify the EntitySystem
to add the summoned Entity
.
In the entity manager's constructor, it called a method to listen for the EntityCreate
event. The classes that make up the event are the EntityCreateEvent
and the EntityCreateListener
. I didn't create the original event manager class so I can't take credit for it. See Event Manager for the original implementation and details on creating event listener and event classes.
Note: The C++ version of the EventManager
works differently using function bindings instead of event listeners. The comments in the file 'EventManager.h' will show you how to use it. Summon spell:
public class SummonSpell extends Spell {
private Entity summon;
public SummonSpell(String name, float cost, Entity summon)
{
super(name, cost);
this.summon = summon;
}
public void use(EventManager eventManager)
{
HealthPart healthPart = summon.get(HealthPart.class);
healthPart.setHealth(healthPart.getMaxHealth());
eventManager.notify(new EntityCreatedEvent(summon));
System.out.println("\tCasting " + name);
}
}
Event for entity create:
public class EntityCreateEvent implements Event {
private Entity entity;
public EntityCreateEvent(Entity entity) { this.entity = entity; }
@Override
public void notify(EntityCreateListener listener) { listener.create(entity); }
}
EventListener for entity created:
public interface EntityCreateListener {
public void create(final Entity entity);
}
Stat Restoration
In the example, characters regenerate health each timestep. The RestorePart
increases the health and mana of its parent Entity
every time its update method is called. It interacts with the HealthPart
and ManaPart
and updates their state.
public class RestorePart extends Part {
private float healthRestoreRate;
private float manaRestoreRate;
public RestorePart(float healthRestoreRate, float manaRestoreRate) {
this.healthRestoreRate = healthRestoreRate;
this.manaRestoreRate = manaRestoreRate;
}
@Override
public void update(float dt) {
HealthPart healthPart = getEntity().get(HealthPart.class);
float newHealth = calculateRestoredValue(healthPart.getMaxHealth(), healthPart.getHealth(), healthRestoreRate * dt);
healthPart.setHealth(newHealth);
ManaPart manaPart = getEntity().get(ManaPart.class);
float newMana = calculateRestoredValue(manaPart.getMaxMana(), manaPart.getMana(), manaRestoreRate * dt);
manaPart.setMana(newMana);
}
private float calculateRestoredValue(float maxValue, float currentValue, float restoreRate)
{
float manaRestoreAmount = maxValue * restoreRate;
float maxManaRestoreAmount = Math.min(maxValue - currentValue, manaRestoreAmount);
float newMana = currentValue + maxManaRestoreAmount;
return newMana;
}
}
Battle System
The BattleSystem
is where high-level interactions between entities are implemented, e.g. targeting and intelligence. It also contains rules of the game such as when an entity is considered dead.
In the future, we might want to create an AI System to handle targeting and just have the Battle System control the rules of the game. But, for a simple example it's fine as it is. In the following code snippet of the BattleSystem
, note that it is using each character's Mentality
part to specify how it will act in the current turn. The BattleSystem
also resolves the issue from the last example of providing potential targets for attacking and supporting.
public void act(Entity actingCharacter, List characters) {
Mentality mentality = actingCharacter.get(MentalityPart.class).getMentality();
if (mentality == Mentality.OFFENSIVE) {
attemptAttack(actingCharacter, characters);
}
else if (mentality == Mentality.SUPPORT) {
boolean healed = attemptHeal(actingCharacter, characters);
if (!healed) {
attemptAttack(actingCharacter, characters);
}
} else if (mentality == Mentality.SUMMON) {
boolean summoned = attemptSummon(actingCharacter);
if (!summoned) {
attemptAttack(actingCharacter, characters);
}
}
}
In addition to managing AI, it contains the game rules, such as when a character should be removed from the game using the helper method isAlive
and the EntityManager remove
method.
for (Entity character : characters) {
if (!isAlive(character)) {
entityManager.remove(character);
System.out.println(character.get(DescriptionPart.class).getName() + " is dead!");
}
}
Conclusion
Handling interactions between entities can become complex in large-scale games. I hope this article was helpful in providing several approaches to interactions by using high-level classes, adding logic to parts, and using events. The first and second articles addressed the core of creating and using entities and parts.
If you want, take a closer look at of the example code or change it to get a feel of how it manages interactions between entities. Though the CharacterFactory
is fine for small and medium-sized games, it doesn't scale well for large games where potentially thousands of game object types exist.
The next article, Serialization, will describe several approaches to mass creation of game object types using data files and serialization.
Article Update Log
16 April 2014: Initial release