Entities-Parts II: Interactions

Published April 17, 2014 by David Chen, posted by dave19067
Do you see issues with this article? Let us know.
Advertisement

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

Cancel Save
0 Likes 4 Comments

Comments

LeGerman
Thanks man, this article was very good, it resolved some issues that I was having while creating the base of an entity-comp engine that I am doing for a game. The only question that I have is which do you think it's a better approach in a large scale, have the update logic of the components in the components class itself or have a System per behavior that process all the entities that have all the necessary components for that behavior and use the components classes just as holder for the data (with no logic in them)?
April 20, 2014 07:07 AM
dave19067

Good question. I don't have experience working on a large-scale game, but I think you can combine both approaches in a game/engine:

In the Entities-Parts implementation, components have access to other components if they're from the same entity. Put update logic in a component if it only needs to reference it's member variables or components of it's parent entity, e.g. the RestorePart in the example.

If you have logic that requires references to components from multiple entities or knowledge of the outside world, probably best to put the logic in a high-level class such as a system. In the example, the BattleSystem is used to handle A.I. and targeting logic because they require access to a list of entities.

April 20, 2014 07:11 PM
J. Faraday

Cool stuff. This will definitely be useful to me as a pseudo-code-type example to reference for the future. I found this very helpful and informative. I'll definitely be checking out your other articles / posts.

May 03, 2014 06:41 PM
nonchiedercilaparola

with “EntitySystem” used in “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.” did you mean “EntityManager” right? from where EventSystem come out?

And with “EntityCreate” use in “In the entity manager's constructor, it called a method to listen for the EntityCreate event.” do you mean “EntityCreateEvent”?

March 23, 2024 06:15 PM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!

The 2nd article in the series extends the RPG example so that it will simulate two teams battling against each other. This article will describe several techniques to handle interactions between entities such as those involved in the example.

Advertisement

Other Tutorials by dave19067

Advertisement