Dominion ECS Java

repository·main·Indexed 18 days ago

https://github.com/dominion-dev/dominion-ecs-java

A high-performance, high-concurrency Entity Component System (ECS) library for Java designed for data-oriented programming. It utilizes Struct of Arrays (SoA) and Archetypes to achieve efficient memory layouts and cache coherency. The library includes a Scheduler for sequential and parallel system execution, a Results class for entity querying and filtering, and support for Compositions to streamline entity creation and modification.

Tokens
4.6K
Snippets
10
Records
15
Agent score
63%

What's inside dominion-ecs-java

  1. How Struct of Arrays (SoA) layout is implemented

    main

    Dominion implements a Struct of Arrays (SoA) layout using the ChunkedPool.LinkedChunk class.

    Instead of the traditional Array of Structs (AoS) where all components of an entity are grouped together, SoA stores components of the same data type in separate, contiguous arrays.

    Benefits:

    • Cache Efficiency: Improves data locality and reduces cache misses.
    • SIMD Optimization: Contiguous arrays allow data to be easily aligned for SIMD (Single Instruction, Multiple Data) operations, enabling parallel processing of multiple data elements.
  2. How Archetypes work in Dominion

    main

    Dominion uses the Archetype concept to organize and manage entities based on their component compositions.

    • DataComposition: An aggregation of component types that defines an archetype. When an entity is created or modified, Dominion checks if its component combination matches an existing DataComposition. If so, it joins that archetype; otherwise, a new one is created.
    • Efficiency: Archetypes allow entities with the same component layout to be stored together. This is handled via the ChunkedPool.Tenant class to improve memory locality, enhance cache coherency, and facilitate efficient querying.
    • Querying: Systems can quickly identify and process relevant entities by targeting specific archetypes, reducing the overhead of checking every entity in the world.
  3. Run Dominion Engine benchmarks locally

    main

    To measure the performance of the Dominion Engine on your own hardware, you can run the provided JMH (Java Microbenchmark Harness) benchmarks.

    Prerequisites:

    • A JDK 17 (or newer).
    • Maven installed.
    • The dominion-ecs-java engine must be built and available in your local Maven repository.

    Steps:

    1. Clone the dominion-ecs-java repository.
    2. Build the project using Maven.
    3. Navigate to the dominion-ecs-engine-benchmarks directory.
    4. Execute the DominionBenchmark.All main class.
    mvn clean install
    # Then run the main class:
    # DominionBenchmark.All
  4. Install Dominion ECS via Maven

    main

    To use Dominion in your Java project, ensure you have Java 17 or newer and Maven installed. Add the following dependency to your pom.xml:

    <dependency>
        <groupId>dev.dominion.ecs</groupId>
        <artifactId>dominion-ecs-engine</artifactId>
        <version>0.9.0</version>
    </dependency>
    <dependency>
        <groupId>dev.dominion.ecs</groupId>
        <artifactId>dominion-ecs-engine</artifactId>
        <version>0.9.0</version>
    </dependency>
  5. Run the DarkEntities example app

    main

    DarkEntities is a more advanced example demonstrating a turn-based rogue-like game concept running in a terminal.

    Key features demonstrated in this example include:

    • Creating and managing entities, components, and systems.
    • Implementing a camera and lighting system.
    • Using a lighting system that can fork subsystems and distribute work across multiple worker threads to utilize multiple CPU cores.

    To run this example, ensure you have built Dominion locally and then execute the following command (specifying the main class):

    java -cp dominion-ecs-examples/target/dominion-ecs-examples-0.9.0-SNAPSHOT.jar dev.dominion.ecs.examples.dark.DarkEntities
  6. Quick Start: Create a basic ECS application

    main

    To get started, create a Dominion instance, define your components (as classes or records), create entities, and run systems using a Scheduler.

    Key steps:

    1. Initialize: Use Dominion.create() to create your world.
    2. Create Entities: Use hello.createEntity(name, components...) to add entities with specific data.
    3. Define Systems: Systems are typically implemented as Runnable tasks that use findEntitiesWith(Class... componentTypes) to retrieve entities and their associated components.
    4. Schedule and Run: Create a Scheduler via hello.createScheduler(), schedule your system, and start the loop with tickAtFixedRate(rate).
    public class HelloDominion {
    
        public static void main(String[] args) {
            // creates your world
            Dominion hello = Dominion.create();
    
            // creates an entity with components
            hello.createEntity(
                    "my-entity",
                    new Position(0, 0),
                    new Velocity(1, 1)
            );
    
            // creates a system
            Runnable system = () -> {
                //finds entities
                hello.findEntitiesWith(Position.class, Velocity.class)
                        // stream the results
                        .stream().forEach(result -> {
                            Position position = result.comp1();
                            Velocity velocity = result.comp2();
                            position.x += velocity.x;
                            position.y += velocity.y;
                            System.out.printf("Entity %s moved with %s to %s\n",
                                    result.entity().getName(), velocity, position);
                        });
            };
    
            // creates a scheduler
            Scheduler scheduler = hello.createScheduler();
            // schedules the system
            scheduler.schedule(system);
            // starts 3 ticks per second
            scheduler.tickAtFixedRate(3);
        }
    
        // component types can be both classes and records
    
        static class Position {
            double x, y;
    
            public Position(double x, double y) {/*..."}
    
            @Override
            public String toString() {/*..."}
        }
    
        record Velocity(double x, double y) {
        }
    }
  7. Run the HelloDominion example app

    main

    The HelloDominion app is a basic entry-level example designed to introduce the core concepts of the Dominion ECS. It serves as a 'break the ice' application for new users.

    To run this example, ensure you have built Dominion locally and then execute the following command from your project root:

    java -jar dominion-ecs-examples/target/dominion-ecs-examples-0.9.0-SNAPSHOT.jar
  8. Query entities and compositions using Results

    main

    The Results class is the output of search methods like findEntitiesWith or findCompositionsWith. It acts as a container for entities that match your criteria (component types and optional states).

    Results supports:

    • Filtering: Use .without(Class<?>...) to exclude specific component types or .withAlso(Class<?>...) to further refine the search. Use .withState(S state) to filter by an entity's Enum state.
    • Iteration: Use .stream() for functional-style operations or .iterator() for sequential access.
    // Find all entities with both Position and Velocity
    Results<Entity> movingEntities = dominion.findEntitiesWith(Position.class, Velocity.class);
    
    // Filter results further
    Results<Entity> activeMovingEntities = movingEntities
        .without(GhostComponent.class)
        .withState(EntityState.ACTIVE);
    
    // Process results
    activeMovingEntities.stream().forEach(e -> {
        Position p = e.get(Position.class);
        // ...
    });
  9. Manage system execution with Scheduler

    main

    The Scheduler class is responsible for managing the lifecycle and execution order of systems. Systems are defined as Runnable types (such as lambda expressions) and are executed on every tick.

    Key Execution Models

    • Sequential Execution: Use schedule(Runnable system) to ensure systems run one after another. No more than one task will be active at any given time.
    • Parallel Execution: Use parallelSchedule(Runnable... systems) to run multiple systems concurrently within a single execution slot. These slots are then executed sequentially relative to other slots.
    • Fork-Join Pattern: A system can spawn sub-tasks to run in parallel and wait for them to complete using forkAndJoin or forkAndJoinAll.

    Lifecycle Management

    • Suspension: You can pause a scheduled system using suspend(Runnable system) without losing its position in the execution order. Use resume(Runnable system) to restart it.
    • Shutdown: Use shutDown() to initiate an orderly shutdown. This allows currently submitted systems to finish executing but prevents any new systems from being accepted.
    // Example of scheduling sequential and parallel systems
    Scheduler scheduler = new Scheduler();
    
    scheduler.schedule(() -> System.out.println("System A"));
    scheduler.parallelSchedule(() -> System.out.println("System B"), () -> System.out.println("System C"));
    
    // Start the execution loop
    scheduler.tickAtFixedRate(60);
  10. Create and manage Entities in Dominion

    main

    Entities are unique integer identifiers within a Dominion. You can create them in several ways:

    1. Directly: Pass zero or more POJO components to createEntity.
    2. Prepared: Use a Composition to pre-define the component structure for efficient creation via createPreparedEntity.
    3. Prefab: Use an existing entity as a template via createEntityAs.

    Once created, you can dynamically add/remove components, check for component presence, get component instances, or manage the entity's enabled state and Enum-based state.

    // Direct creation
    Entity entity = dominion.createEntity(new Position(0, 0), new Velocity(1, 1));
    
    // Component management
    entity.add(new Health(100));
    boolean hasHealth = entity.has(Health.class);
    Health h = entity.get(Health.class);
    entity.removeType(Velocity.class);
    
    // State and lifecycle
    entity.setEnabled(false);
    entity.setState(EntityState.ACTIVE);
  11. Use Compositions to prepare entity creation and modification

    main

    A Composition allows you to define a blueprint for component types. This is useful for batching entity creation or performing efficient entity modifications (adding/removing components) without manually managing individual component instances every time.

    • Use of() or ofN() to prepare for creating new entities with specific component types.
    • Use byAdding1AndRemoving() or byAddingNAndRemoving() to prepare for modifying existing entities.
    Dominion dominion = Dominion.create();
    Composition composition = dominion.composition();
    
    // Prepared entity creations
    var compositionOf1 = composition.of(Comp.class);
    Entity entity1 = dominion.createPreparedEntity(compositionOf1.withValue(new Comp(0)));
    
    // Prepared entity changes
    var modifyAdding1 = composition.byAdding1AndRemoving(Comp2.class);
    dominion.modifyEntity(modifyAdding1.withValue(entity1, new Comp2()));
  12. Use the Dominion class as the ECS entry point

    main

    A Dominion is an independent container for all ECS data and serves as the primary entry point for the library. You can create multiple Dominion instances with different names to isolate different game worlds or data sets. Use it to create entities, find entities/compositions, and manage the lifecycle of items.

    Dominion dominion = Dominion.create();
    // or
    Dominion dominion = Dominion.create("MyWorld");