Jump to content
  • Advertisement
  • 03/19/14 02:42 PM
    Sign in to follow this  

    Entities-Parts I: Game Objects

    General and Gameplay Programming



    I. Game Objects (current) II. Interactions III. Serialization This article is the first in a series of articles about the practical application of the entity-component pattern to game programming. This article, in particular, describes what an entity and a component are, how they interact with eachother, and what the advantages of the entity-component pattern are over some traditional methods of creating gameobjects. Also, it will describe the Entities-Parts framework which is simply a collection of reusable classes and concepts for entity-component interaction. Since I started implementing the entity-component model several years ago, it has evolved based off of experience using it in my games and studying various articles and other implementations. It has not been tested in large scale games such as RPGs, so by no means is it perfect. The implementation and the articles will keep evolving as my knowledge increases and I develop more games. As part of the framework, I will also provide an implementation of the entity and component and an example rpg-battle system showing interactions between entities and components. The example will be built on in the next articles. The implementation is provided in Java and C++ w/boost. 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 prefer to use the word 'part' instead of 'component' because it is shorter.

    The Problem

    Some approaches to creating game objects require you to cram all functionality into one GameObject class, or create several game object classes that have duplicate code. With the entity-component pattern, you can reuse code and make your game objects dynamic by thinking of them as a bunch of interconnected parts. Deep Inheritance Hierarchy Approach: 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. Monolithic Class Approach: Another approach is creating a single class that represents and contains all the functionality of your game objects, i.e. the GameObject. While this solves the issue of having duplicate code that exists in deep inheritance hierarchies, this class can quickly get out of hand as it becomes responsible for flying, spells, health, position, etc. Maintaining it will be near impossible and gameobjects will be required to hold data for functionality they don't need. For example, a monster that only flies will still need to keep a collection of spells. Solution - Entity-Component Approach: With the entity-component 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.

    Explaining the Concept

    Note: The approach I present is fundamentally different to ECS (Entity Component Systems). In ECS, components only store the data, but do not provide the functionality. Instead, the functionality is provided by external systems. See http://en.wikipedia.org/wiki/Entity_component_system. It is natural to categorize gameobjects into monsters, items, and walls. Notice that these objects have similar attributes and functionality. They all need to be drawn, hold a physical space in the world, and collide with the player in some way. The line between different categories of gameobjects tend to blur as the gameobjects become more complex. For example, adding movement and damaging spikes to a wall, basically turns it into an enemy, e.g. Thwomp in Mario. In the entity-component pattern, game objects are described as entities that contain several attributes and functionality. The components/parts provide an abstraction for the attributes and functionality. Parts can be functional, e.g. control the behavior the entity, or just hold attributes that other systems and parts can reference, e.g. a health stat. The entity is responsible for managing parts as well as their lifetimes, i.e. initializing, updating, and cleaning up parts. There are many benefits of this approach: code reusability, addition/removal of attributes in a game object during runtime, and ease in generating new game object types in complex games such as MMORPGs.

    Example: Roleplaying Game (RPG) Battle

    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. 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 to 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, Part> parts = new HashMap, Part>(); private List partsToAdd = new ArrayList(); private List> partsToRemove = new ArrayList>(); /** * @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 boolean has(Class 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 get(Class 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 void detach(Class 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 void remove(Class 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) { } }


    This concludes the basics of the entity-component pattern and the implementation. Also, you got to see how to use the implementation's API in the example. As you can see, entities and components are powerful tools for creating game objects, but they can't stand on their own without higher level systems. In the example project, notice that the villager's health went below zero but wasn't removed from the game. Also, the villager Entity had to be passed into the MonsterControllerPart constructor which would be an issue if the monster Entity had to be created first. These issues are outside the scope of this article but will be addressed by the next article, which describes how to implement high-level game logic to manage entity interactions with the help of these classes: Event manager - Provides a centralized hub for event subscription and publication between systems, entities, and parts. For example, this will allow the villager to tell the entity manager that it has died. Entity manager - Manages entities by adding newly created ones, removing dead entities, and updating them. For example, this will remove the dead villager from the gameworld. AI System - Manages AI behavior of an Entity that is dependent on the presence of other entities. For example, this will provide the monster entity knowledge of available targets. Battle System - Provides the rules of the game, e.g., calculating how much damage is dealt, knowing when an Entity is dead, etc. The example will be expanded so that instead of simulating a 1 on 1 fight, a team of monsters will go up against a team of villagers. The entities will have varying classes (melee specialist, ranger, flying ranger, summoner, support mage, etc). The next article will deal with entity management and interactions: Interactions.

    Article Update Log

    5 Apr 2014: Shortened title. 22 Mar 2014: Added C++ w/boost version of code and fixed a memory leak bug in the C++ version. 22 Mar 2014: Changed title from Basics to Game Objects to be more inline with the content of the article 12 Mar 2014: Added C++ version of code 9 Mar 2014: Added additional code samples

      Report Article
    Sign in to follow this  

    User Feedback

    Create an account or sign in to leave a review

    You need to be a member in order to leave a review

    Create an account

    Sign up for a new account in our community. It's easy!

    Register a new account

    Sign in

    Already have an account? Sign in here.

    Sign In Now

    There are no reviews to display.

  • Advertisement

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

GameDev.net is your game development community. Create an account for your GameDev Portfolio and participate in the largest developer community in the games industry.

Sign me up!