Quantcast
Channel: GameDev.net
Viewing all articles
Browse latest Browse all 17825

The Entity-Part Framework: Basics

$
0
0
This implementation of the entity-component pattern has evolved based off my experience using it in my own games and studying numerous articles and implementations on the entity-component pattern. It is generic enough that you can reuse your components in multiple games and decouple your game objects, i.e. entities, from high-level game logic and systems. I like to use the word 'part' instead of 'component' because it allows for shorter class names when creating component classes.

Explaining the Concept


Traditional object-oriented programming requires you to cram all functionality into one GameObject class, or create several game object classes that have duplicate code. With the entity-part pattern, you can reuse code and make your game objects more dynamic by thinking of them as a bunch of interconnected parts.

Traditional OO Approach (Inheritance): Let's say you have a Monster class. The class contains a few variables, such as those for health, damage, and position. If you want a new type of Monster that flies, you derive a FlyingMonster class from the Monster class. If you want a spellcasting Monster, you derive a SpellMonster class from the Monster class. The problem arises when you want a spellcasting flying monster. You now need to decide whether you want to derive SpellFlyingMonster from FlyingMonster or SpellMonster. Furthermore, you will need to copy and paste code from the FlyingMonster or SpellMonster class to provide the functionality that the SpellFlyingMonster is missing from its parent class.

Entity-Part Approach: With the entity-part pattern, you think of a monster as an entity made up of several parts. You do not need separate classes for Monster, FlyingMonster, SpellMonster, and SpellFlyingMonster. For example, to create a FlyingMonster, you create an entity and add a health part, a damage part, and a flying part. If later on, you want it to cast spells, you add the spell part with one line of code. You can create dozens of monster types by mixing and matching parts.

Example: A Flying SpellCasting Monster


It's best to illustrate how the entity-part framework works using the following example game/simulation. The example project is attached to this article. The example is in Java, but the C++ code for the Entity and Part classes is also attached.

The Main class contains the logic to initialize and run our game. It creates a monster and a helpless villager. It then uses a basic game loop and updates the entities. While running the application, the state of the game will be printed to the console by the monster entity such as the monster's health, villager's health, and monster's height as the result from flying.

As you can see, it is very easy to create new types of monsters once you write the code for the parts. For example, we can create a nonflying monster by removing the line of code that attaches the flying part. The MonsterControllerPart is the AI for the monster entity and the target passed into its constructor is the entity that will be attacked. We can make a friendly monster by passing in an enemy Entity into the MonsterControllerPart constructor instead of the helpless villager.

public class Main {

	// main entry to the game application
	public static void main(String[] args) throws InterruptedException
	{
		Entity villager = createVillager();
		Entity monster = createMonster(villager);
		
		// very basic game loop
		while (true) {
			villager.update(1);
			monster.update(1);
			Thread.sleep(1000);
		}
	}
	
	// factory method for creating a monster
	public static Entity createMonster(Entity target) {
		Entity monster = new Entity();
		monster.attach(new StatsPart(100, 2));
		// If we don't want our monster to fly, simply uncomment this line.
		monster.attach(new FlyingPart(20));
		// If we don't want our monster to cast spells, simply uncomment this line.
		monster.attach(new SpellsPart(5));
		monster.attach(new MonsterControllerPart(target));
		monster.initialize();
		return monster;
	}
	
	// factor method for creating an innocent villager
	public static Entity createVillager() {
		Entity villager = new Entity();
		villager.attach(new StatsPart(50, 0));
		villager.initialize();
		return villager;
	}
	
}

MonsterControllerPart code, which serves as the AI and behavior for the monster, includes attacking its target, saying stuff, and attempting to use spells. All of your parts must derive from the Part class. Optionally, parts such as the MonsterControllerPart can override the initialize, cleanup, and update methods to provide additional functionality. These methods are called when its parent entity gets respectively initialized, cleaned up, or updated. Notice that parts can access other parts of its parent entity, e.g., entity.get(StatsPart.class).

public class MonsterControllerPart extends Part {
	
	private Entity target;
	
	public MonsterControllerPart(Entity target) {
		this.target = target;
	}
	
	@Override
	public void initialize() {
		System.out.println("I am alive!");
	}
	
	@Override
	public void cleanup() {
		System.out.println("Nooo I am dead!");
	}
	
	@Override
	public void update(float delta) {
		StatsPart myStatsPart = entity.get(StatsPart.class);
		
		// if target has stats part, damage him
		if (target.has(StatsPart.class)) {
			StatsPart targetStatsPart = target.get(StatsPart.class);
			target.get(StatsPart.class).setHealth(targetStatsPart.getHealth() - myStatsPart.getDamage());
			System.out.println("Whomp!  Target's health is " + targetStatsPart.getHealth());
		}
		
		// if i have spells, heal myself using my spells
		if (entity.has(SpellsPart.class)) {
			entity.get(SpellsPart.class).castHeal();
			System.out.println("Healed myself!  Now my health is " + myStatsPart.getHealth());
		}
	}

}

General-purpose StatsPart keeps track of important RPG stats such as health and damage. This is used by both the Monster and the Villager entity. It is best to keep general-purpose variables such as health and damage together because they will be used by most entities.

public class StatsPart extends Part {
	
	private float health;
	private float damage;
	
	public StatsPart(float health, float damage) {
		this.health = health;
		this.damage = damage;
	}
	
	public float getHealth() {
		return health;
	}
	
	public void setHealth(float health) {
		this.health = health;
	}
	
	public float getDamage() {
		return damage;
	}
	
}

SpellsPart class gives the Monster a healing spell to cast.

public class SpellsPart extends Part {

	private float healRate;
	
	public SpellsPart(float healAmount) {
		this.healRate = healAmount;
	}
	
	public void castHeal() {
		StatsPart statsPart = entity.get(StatsPart.class);
		statsPart.setHealth(statsPart.getHealth() + healRate);
	}
	
}

FlyingPart code allows the Monster can fly to new heights.

public class FlyingPart extends Part {

	private float speed;
	// in more sophisticated games, the height could be used to tell if an entity can be attacked by a grounded opponent.
	private float height = 0;
	
	public FlyingPart(float speed) {
		this.speed = speed;
	}
	
	@Override
	public void update(float delta) {
		height += speed * delta;
		System.out.println("Goin up!  Current height is " + height);
	}
	
}

The Entity-Part Code


The following code blocks are for the Entity class and the Part class. These two classes are the base classes you need for the entity-part framework.

Entity class:
/**
 * Made up of parts that provide functionality and state for the entity.
 * There can only be one of each part type attached.
 * @author David Chen
 *
 */
public class Entity {
	
	private boolean isInitialized = false;
	private boolean isActive = false;
	private Map<Class<? extends Part>, Part> parts = new HashMap<Class<? extends Part>, Part>();
	private List<Part> partsToAdd = new ArrayList<Part>();
	private List<Class<? extends Part>> partsToRemove = new ArrayList<Class<? extends Part>>();
	
	/**
	 * @return If the entity will be updated.
	 */
	public boolean isActive() {
		return isActive;
	}
	
	/**
	 * Sets the entity to be active or inactive.
	 * @param isActive True to make the entity active.  False to make it inactive.
	 */
	public void setActive(boolean isActive) {
		this.isActive = isActive;
	}
	
	/**
	 * @param partClass The class of the part to check.
	 * @return If there is a part of type T attached to the entity.
	 */
	public <T extends Part> boolean has(Class<T> partClass) {
		return parts.containsKey(partClass);
	}
	
	/**
	 * @param partClass The class of the part to get.
	 * @return The part attached to the entity of type T.
	 * @throws IllegalArgumentException If there is no part of type T attached to the entity.
	 */
	@SuppressWarnings("unchecked")
	public <T extends Part> T get(Class<T> partClass) {
		if (!has(partClass)) {
			throw new IllegalArgumentException("Part of type " + partClass.getName() + " could not be found.");
		}
		return (T)parts.get(partClass);
	}
	
	/**
	 * Adds a part.
	 * @param part The part.
	 */
	public void attach(Part part) {
		if (has(part.getClass())) {
			throw new IllegalArgumentException("Part of type " + part.getClass().getName() + " already exists.");
		}
		
		parts.put(part.getClass(), part);
		part.setEntity(this);
		
		if (isInitialized) {
			part.initialize();
		}
	}
	
	/**
	 * If a part of the same type already exists, removes the existing part.  Adds the passed in part.
	 * @param part The part.
	 */
	public void replace(Part part) {
		if (has(part.getClass())) {
			detach(part.getClass());
		}
		
		if (isInitialized) {
			partsToAdd.add(part);
		}
		else {
			attach(part);
		}
	}
	
	/**
	 * Removes a part of type T if it exists.
	 * @param partClass The class of the part to remove.
	 */
	public <T extends Part> void detach(Class<T> partClass) {
		if (has(partClass) && !partsToRemove.contains(partClass)) {
			partsToRemove.add(partClass);
		}
	}
	
	/**
	 * Makes the entity active.  Initializes attached parts.
	 */
	public void initialize() {
		isInitialized = true;
		isActive = true;
		for (Part part : parts.values()) {
			part.initialize();
		}
	}
	
	/**
	 * Makes the entity inactive.  Cleans up attached parts.
	 */
	public void cleanup() {
		isActive = false;
		for (Part part : parts.values()) {
			part.cleanup();
		}
	}
	
	/**
	 * Updates attached parts.  Removes detached parts and adds newly attached parts.
	 * @param delta Time passed since the last update.
	 */
	public void update(float delta) {
		for (Part part : parts.values()) {
			if (part.isActive()) {
				part.update(delta);
			}
		}
		
		while (!partsToRemove.isEmpty()) {
			remove(partsToRemove.remove(0));
		}
		
		while (!partsToAdd.isEmpty()) {
			attach(partsToAdd.remove(0));
		}
	}
	
	private <T extends Part> void remove(Class<T> partClass) {
		if (!has(partClass)) {
			throw new IllegalArgumentException("Part of type " + partClass.getName() + " could not be found.");
		}
		parts.get(partClass).cleanup();
		parts.remove(partClass);
	}
	
}

Part class:
/**
 * Provides partial functionality and state for an entity.
 * @author David Chen
 *
 */
public abstract class Part {

	private boolean isActive = true;
	protected Entity entity;
	
	/**
	 * @return If the part will be updated.
	 */
	public final boolean isActive() {
		return isActive;
	}
	
	/**
	 * @return The entity the part is attached to.
	 */
	public final Entity getEntity() {
		return entity;
	}
	
	/**
	 * Sets the entity the part is attached to.
	 * @param entity The entity.
	 */
	public final void setEntity(Entity entity) {
		this.entity = entity;
	}
	
	/**
	 * Initialization logic.
	 */
	public void initialize() {
	}
	
	/**
	 * Cleanup logic.
	 */
	public void cleanup() {
	}
	
	/**
	 * Update logic.
	 * @param delta Time since last update.
	 */
	public void update(float delta) {
	}
	
}

Usage Notes


Inactive entities: The isActive flag is useful when you want to keep an Entity in memory, but don't want it to be in the game. For example, you might set dead entities to inactive to cache them for faster entity creation or set offscreen entities to inactive to reduce CPU usage.

Entities referenced by pointer instead of ID: Many implementations of the entity-component pattern have the entity store an ID. I think the entities should be completely decoupled from entity management and not have to store an ID. I recommend using smart pointers or references to keep track of entities instead of integer IDs.

Only one of each part type: You can only have one of each type of part attached to an entity. Parts should be seen as pieces of functionality for your entity rather than individual items for the entity to use. For example, if you want more than one spell, keep a list of spells in the SpellsPart and create a castSpell method that takes in an int or enum parameter as the spell to cast.

Do not always need to check if the entity has a part before getting it: In the example project, the MonsterControllerPart doesn't check if its entity has a StatsPart because all monsters are expected to have some health. However, it checks if it has a SpellsPart because not all monsters are spellcasters. If you want to be safe, you can check if the Entity has the required parts in the Part.initialize method.

Conclusion


That's the basics of the entity-part framework, but there are other important topics related to entity interaction that I will write articles on very soon:

Entity Manager - For managing entity creation, updating, and cleanup.
Event Manager - To allow entities, systems, and parts to communicate with each other by subscribing and publishing events.
Entity Query - Provides a way to search for entities in the gameworld using their attributes. For example, if you have an explosion spell and you want to get enemies in proximity to the explosion, you would use the entity query.

Article Update Log


12 Mar 2014: Added C++ version of code
9 Mar 2014: Added additional code samples

Viewing all articles
Browse latest Browse all 17825

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>