Created
May 10, 2025 05:00
-
-
Save schosin/4b99879ff6077aaabf9929e0cfc05a4e to your computer and use it in GitHub Desktop.
Stupid-ECS TestApplication
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import java.util.ArrayList; | |
| import java.util.Arrays; | |
| import java.util.BitSet; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.concurrent.ConcurrentHashMap; | |
| import java.util.concurrent.atomic.AtomicInteger; | |
| public class TestApplication { | |
| private final Api api = new Impl(); | |
| public static void main(String[] args) { | |
| System.out.println("-- Simple test"); | |
| new TestApplication().simple(); | |
| System.out.println(); | |
| System.out.println("-- Relation test (sealed arguments)"); | |
| new TestApplication().relations(); | |
| System.out.println("-- Relation test (object arguments)"); | |
| new TestApplication().relationsObject(); | |
| } | |
| private void simple() { | |
| // Create some entities | |
| createEntity(new Foo(), new Bar()); | |
| createEntity(new Foo(), Baz.A); | |
| createEntity(new Foo(), Baz.B); | |
| // Test some compositions | |
| api.createComposition(Foo.class, Bar.class).process((entityId, foo, baz) -> System.out.println("FooBar: %d (%s, %s)".formatted(entityId, foo, baz))); | |
| api.createComposition(Foo.class, Baz.class).process((entityId, foo, baz) -> System.out.println("FooBaz: %d (%s, %s)".formatted(entityId, foo, baz))); | |
| } | |
| private void relations() { | |
| // Create some entities | |
| createEntity(new Foo(), new Bar()); | |
| createEntity(new Foo(), Baz.A); | |
| createEntity(new Foo(), Baz.B); | |
| createEntity(new Foo(), Api.relation(Attacks.ATTACKS, Faction.HORDE)); | |
| createEntity(new Foo(), Api.relation(Attacks.ATTACKS, Faction.ALLIANCE)); | |
| // Test composition | |
| api.createComposition(Api.component(Foo.class), Api.relation(Attacks.ATTACKS, Faction.ALLIANCE)) | |
| // explicit types to show that I want the relation type at this position | |
| .process((int entityId, Foo foo, Relation<Attacks, Faction> relation) -> System.out.println("Attacks alliance: %d (%s, %s)".formatted(entityId, foo, relation))); | |
| } | |
| private void relationsObject() { | |
| // Create some entities | |
| createEntity(new Foo(), new Bar()); | |
| createEntity(new Foo(), Baz.A); | |
| createEntity(new Foo(), Baz.B); | |
| createEntity(new Foo(), Api.relation(Attacks.ATTACKS, Faction.HORDE)); | |
| createEntity(new Foo(), Api.relation(Attacks.ATTACKS, Faction.ALLIANCE)); | |
| // Test composition (requires types to be explicit) | |
| api.<Foo, Relation<Attacks, Faction>>createComposition(Foo.class, Api.relation(Attacks.ATTACKS, Faction.ALLIANCE)) | |
| // explicit types to show that I want the relation type at this position | |
| .process((int entityId, Foo foo, Relation<Attacks, Faction> relation) -> System.out.println("Attacks alliance: %d (%s, %s)".formatted(entityId, foo, relation))); | |
| System.out.println("- Swapped arguments due to refactoring, forgot type parameters"); | |
| api.<Foo, Relation<Attacks, Faction>>createComposition(Api.relation(Attacks.ATTACKS, Faction.ALLIANCE), Foo.class) | |
| // explicit types to show that I want the relation type at this position | |
| .process((int entityId, Foo foo, Relation<Attacks, Faction> relation) -> System.out.println("Attacks alliance: %d (%s, %s)".formatted(entityId, foo, relation))); | |
| } | |
| private int createEntity(Object... components) { | |
| var entityId = api.createEntity(components); | |
| System.out.println("Entity %d: %s".formatted(entityId, Arrays.toString(components))); | |
| return entityId; | |
| } | |
| record Foo() { | |
| } | |
| record Bar() { | |
| } | |
| enum Baz { | |
| A, B | |
| } | |
| enum Attacks { | |
| ATTACKS | |
| } | |
| enum Faction { | |
| HORDE, ALLIANCE | |
| } | |
| } | |
| interface Api { | |
| int createEntity(Object... components); | |
| <T1, T2> Composition<T1, T2> createComposition(Class<T1> component1, Class<T2> component2); | |
| <T1, T2> Composition<T1, T2> createComposition(ComponentType<T1> component1, ComponentType<T2> component2); | |
| <T1, T2> Composition<T1, T2> createComposition(Object component1, Object component2); | |
| /* | |
| * TODO idea: Wildcards will be implemented by not passing an instance, but a Class<?> and it will match like in stringWildcardMatches | |
| * | |
| * Since the arguments for the test will always be ComponentTypes, this can be optimized with an IntBag like already done. | |
| * Note though that Wildcard-Relations cannot be assigned to an entity, but only used for composition! | |
| */ | |
| static boolean stringWildcardMatches() { | |
| return String.class.isAssignableFrom(String.class) || String.class.isInstance(List.of()); | |
| } | |
| /* | |
| * THIS is the kicker. | |
| * | |
| * If sealed interfaces were to allow permitting "external" types and treat them like non-sealed, | |
| * this would simplify the API for the end user a lot: | |
| * | |
| * api.createComposition(Foo.class, Api.relation(r, t)); | |
| * | |
| * See Impl#createComponent for the other side of the coin. | |
| */ | |
| sealed interface ComponentType<T> permits ComponentType.RegularType, Relation /*, Class<T> */ { | |
| record RegularType<T>(Class<T> clazz) implements ComponentType<T> { | |
| } | |
| } | |
| static <T> ComponentType.RegularType<T> component(Class<T> clazz) { | |
| return new ComponentType.RegularType<>(clazz); | |
| } | |
| static <R, T> Relation<R, T> relation(R relation, T target) { | |
| return new Relation<>(relation, target); | |
| } | |
| } | |
| class Impl implements Api { | |
| // managed elsewhere | |
| private final List<Entity> entities = new ArrayList<>(); | |
| private final Map<Api.ComponentType<?>, Component<?>> components = new ConcurrentHashMap<>(); | |
| private final AtomicInteger nextId = new AtomicInteger(1); | |
| @Override | |
| @SuppressWarnings({ "unchecked", "rawtypes" }) | |
| public int createEntity(Object... components) { | |
| var entityId = nextId.getAndIncrement(); | |
| var bitset = new BitSet(); | |
| for (var component : components) { | |
| // sad raw type | |
| Component mapper = switch (component) { | |
| case Relation<?, ?> relation -> getComponent(relation); | |
| default -> getComponent(component); | |
| }; | |
| mapper.set(entityId, component); | |
| bitset.set(mapper.id()); | |
| } | |
| var entity = new Entity(entityId, bitset); | |
| this.entities.add(entity); | |
| return entity.id; | |
| } | |
| @Override | |
| public <T1, T2> Composition<T1, T2> createComposition(Class<T1> component1, Class<T2> component2) { | |
| var mapper1 = getComponent(component1); | |
| var mapper2 = getComponent(component2); | |
| var entities = getEntities(mapper1, mapper2); | |
| return new CompositionImpl<>(entities, mapper1, mapper2); | |
| } | |
| // @Override | |
| public <T1, T2> Composition<T1, T2> createComposition(Api.ComponentType<T1> component1, Api.ComponentType<T2> component2) { | |
| var mapper1 = getComponent(component1); | |
| var mapper2 = getComponent(component2); | |
| var entities = getEntities(mapper1, mapper2); | |
| return new CompositionImpl<>(entities, mapper1, mapper2); | |
| } | |
| @SuppressWarnings({ "unchecked", "rawtypes" }) | |
| public <T1, T2> Composition<T1, T2> createComposition(Object component1, Object component2) { | |
| var mapper1 = switch (component1) { | |
| case Class<?> clazz -> getComponent(clazz); | |
| case Api.ComponentType<?> type -> getComponent(type); | |
| default -> throw new IllegalArgumentException("component1 must be a Class<?> or ComponentType<?>, but was: " + component1.getClass().getSimpleName()); | |
| }; | |
| var mapper2 = switch (component2) { | |
| case Class<?> clazz -> getComponent(clazz); | |
| case Api.ComponentType<?> type -> getComponent(type); | |
| default -> throw new IllegalArgumentException("component2 must be a Class<?> or ComponentType<?>, but was: " + component1.getClass().getSimpleName()); | |
| }; | |
| var entities = getEntities(mapper1, mapper2); | |
| return new CompositionImpl<>(entities, (Component) mapper1, (Component) mapper2); | |
| } | |
| private List<Integer> getEntities(Component<?>... mappers) { | |
| var bitset = new BitSet(); | |
| for (var mapper : mappers) { | |
| bitset.set(mapper.id()); | |
| } | |
| return this.entities.stream() | |
| .filter(entity -> containsAll(entity.components, bitset)) | |
| .map(Entity::id) | |
| .toList(); | |
| } | |
| /* | |
| * custom bitset makes this easier, especially since there is "containsAll", "containsOne", "containsNone" in the actual API for matching entities, | |
| * seperating matching (additional "spec" argument) and component extracting (class and ComponentType) | |
| */ | |
| private boolean containsAll(BitSet first, BitSet test) { | |
| var bit = test.nextSetBit(0); | |
| while (bit != -1) { | |
| if (!first.get(bit)) { | |
| return false; | |
| } | |
| bit = test.nextSetBit(bit + 1); | |
| } | |
| return true; | |
| } | |
| @SuppressWarnings("unchecked") // sad cast | |
| private <T> Component<T> getComponent(T component) { | |
| return getComponent((Class<T>) component.getClass()); | |
| } | |
| private <T> Component<T> getComponent(Class<T> clazz) { | |
| return getComponent(new Api.ComponentType.RegularType<>(clazz)); | |
| } | |
| @SuppressWarnings("unchecked") | |
| private <T> Component<T> getComponent(Api.ComponentType<T> component) { | |
| return (Component<T>) components.computeIfAbsent(component, this::createComponent); | |
| } | |
| @SuppressWarnings({ "rawtypes", "unchecked" }) | |
| private <T> Component<T> createComponent(Api.ComponentType<T> component) { | |
| return switch (component) { | |
| // case Class<T> clazz -> new RegularComponent<>(nextId.getAndIncrement(), new ArrayList<>(entities.size())); | |
| case ComponentType.RegularType(Class<T> clazz) -> new RegularComponent<>(nextId.getAndIncrement(), new ArrayList<>(entities.size())); | |
| case Relation relation -> new RelationComponent(nextId.getAndIncrement(), new ArrayList<>(entities.size())); | |
| }; | |
| } | |
| record Entity(int id, BitSet components) { | |
| } | |
| } | |
| record Relation<R, T>(R relation, T target) implements Api.ComponentType<Relation<R, T>> { | |
| } | |
| interface Composition<T1, T2> { | |
| interface Consumer<T1, T2> { | |
| void consume(int entityId, T1 component1, T2 component2); | |
| } | |
| void process(Consumer<T1, T2> consumer); | |
| } | |
| class CompositionImpl<T1, T2> implements Composition<T1, T2> { | |
| // kept up-to-date from Impl when entities added/removed, or components added/removed | |
| private final List<Integer> entities; | |
| private final Component<T1> component1; | |
| private final Component<T2> component2; | |
| CompositionImpl(List<Integer> entities, Component<T1> component1, Component<T2> component2) { | |
| this.entities = entities; | |
| this.component1 = component1; | |
| this.component2 = component2; | |
| } | |
| @Override | |
| public void process(Composition.Consumer<T1, T2> consumer) { | |
| for (var entityId : entities) { | |
| consumer.consume(entityId, component1.get(entityId), component2.get(entityId)); | |
| } | |
| } | |
| } | |
| sealed interface Component<T> { | |
| int id(); | |
| void set(int entityId, T component); | |
| T get(int entityId); | |
| } | |
| record RegularComponent<T>(int id, List<T> components) implements Component<T> { | |
| @Override | |
| public void set(int entityId, T component) { | |
| // quick and dirty hack for demonstration | |
| while (entityId + 1 > components.size()) { | |
| components.add(null); | |
| } | |
| components.set(entityId, component); | |
| } | |
| @Override | |
| public T get(int entityId) { | |
| return entityId < components.size() ? components.get(entityId) : null; | |
| } | |
| } | |
| record RelationComponent<R, T>(int id, List<Relation<R, T>> components) implements Component<Relation<R, T>> { | |
| @Override | |
| public void set(int entityId, Relation<R, T> component) { | |
| // quick and dirty hack for demonstration | |
| while (entityId + 1 > components.size()) { | |
| components.add(null); | |
| } | |
| components.set(entityId, component); | |
| } | |
| @Override | |
| public Relation<R, T> get(int entityId) { | |
| return entityId < components.size() ? components.get(entityId) : null; | |
| } | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Simplified example of the API and a mock implementation I discussed in https://www.reddit.com/r/java/comments/1kiplhs/using_sealed_types_for_union_types/.