HyperLap2D is built on top of Artemis-odb, a high-performance Entity Component System (ECS) framework. Unlike traditional Object-Oriented Programming (OOP) where you use inheritance (e.g., class Player extends GameObject), ECS uses composition.
Understanding this architecture is key to writing clean, performant, and scalable game logic.
The Core Concepts
ECS consists of three main pillars:
- Entities: They are just unique Integer IDs. They are distinct “things” in your world (a player, a tree, a bullet). They have no methods and no data directly attached to them.
- Components: They are pure data holders (POJOs). They contain no logic. They define the “what” of an entity (e.g.,
EnemyComponent,HealthComponent). - Systems: They contain the logic. They process entities that possess a specific set of components. They define the “how” (e.g.,
MovementSystem,RenderSystem). - Scripts: Specific logic attached to a single entity, useful for input handling or complex actor-specific behavior (like a Player Controller).
1. Entities
In Artemis-odb, an Entity is not a heavy object; it is simply an int.
When you use the SceneLoader, it creates entities for you based on the JSON data.
// Retrieving an entity ID using ItemWrapper
int playerEntityId = root.getChild("player").getEntity();
2. Components (Data)
Components act as “Aspects” or “Properties” of an entity. If an entity has a TransformComponent and a SpriteComponent, it will be rendered. If you add a PhysicsBodyComponent, it becomes a physical object.
HyperLap2D Standard Components: The runtime provides standard components for rendering (handling coordinates, sizes, tints, etc.).
Creating Custom Components: For your game logic, you should create your own components.
import com.artemis.PooledComponent;
public class HealthComponent extends PooledComponent {
public float currentHealth = 100;
public float maxHealth = 100;
public boolean isDead = false;
@Override
protected void reset() {
currentHealth = 100;
isDead = false;
}
}
Best Practice: Keep components strictly for data. Do not add methods like
takeDamage()inside a Component. That logic belongs in a System.
3. Systems (Logic)
Systems are where the magic happens. A System usually iterates over all entities that match a specific “Family” of components.
For example, a HealthSystem might care about entities that have both HealthComponent and TransformComponent.
import com.artemis.systems.IteratingSystem;
import com.artemis.annotations.All;
@All({HealthComponent.class, TransformComponent.class})
public class HealthSystem extends IteratingSystem {
// Artemis automatically injects ComponentMappers for fast access
protected ComponentMapper<HealthComponent> mHealth;
protected ComponentMapper<TransformComponent> mTransform;
@Override
protected void process(int entityId) {
HealthComponent health = mHealth.get(entityId);
// Logic example:
if (health.currentHealth <= 0 && !health.isDead) {
health.isDead = true;
System.out.println("Entity " + entityId + " just died at " + mTransform.get(entityId).x);
}
}
}
4. Scripts (Specific Logic)
While Systems are great for global logic, they can be verbose for specific behaviors like a Player Controller or a Button. Scripts bridge the gap between ECS and OOP, allowing you to attach logic to a single entity.
HyperLap2D offers BasicScript and PhysicsBodyScript. The latter is powerful because it wraps Box2D collision listeners specifically for the entity it is attached to.
Registering your Systems
To make your system run, you must add it to the SceneConfiguration before loading the scene.
SceneConfiguration config = new SceneConfiguration();
// Add your custom logic systems
config.addSystem(new PlayerControlSystem());
config.addSystem(new HealthSystem()); // The system we created above
mSceneLoader = new SceneLoader(config);
Component Mappers
You might have noticed ComponentMapper in the example above. In Artemis-odb, you never want to retrieve components manually inside a loop (it’s slow).
Instead, you define a ComponentMapper field. Artemis injects dependencies automatically when the system is added to the world.
// Fast retrieval
mHealth.get(entityId).currentHealth -= 10;
Communication between Systems
Since Systems are decoupled, how do they talk to each other?
- Flags in Components: One system sets a boolean flag in a component (e.g.,
jumpRequested = true), and another system processes it and resets it. - Events: You can use a dedicated Event System, but keeping logic inside the ECS loop is usually cleaner.
System vs Script: What to choose?
| Feature | System | Script |
|---|---|---|
| Scope | Global (All entities with X components) | Local (Attached to specific entity) |
| Use Case | Gravity, Rendering, AI Swarms, Cooldowns | Player Input, UI Buttons, Boss Phases |
| Data Access | Fast Iteration via Mappers | Direct access via Mappers + Local State |
Best Practices for Game Structure
- Granularity: Don’t make a giant
GameComponentwith all data. Split it intoStatsComponent,InventoryComponent. - Systems do one thing: Avoid a massive
GameLogicSystem. Create aMovementSystem,CombatSystem,UISystem. - Separation of Concerns:
- Use Systems for rules of the world (e.g., “Bullets damage things”).
- Use Scripts for intentions of the actor (e.g., “I want to shoot a bullet”).
- Communication:
- Scripts should often just modify Component data (e.g., set
isJumping = true). - Systems should react to that data (e.g., process the jump movement).
- Scripts should often just modify Component data (e.g., set
- Tags vs Components: Use Tags (via
MainItemComponent) for creating Components for behavior (like “Walkable”).