Skip to content

Instantly share code, notes, and snippets.

@schosin
Created May 10, 2025 05:00
Show Gist options
  • Select an option

  • Save schosin/4b99879ff6077aaabf9929e0cfc05a4e to your computer and use it in GitHub Desktop.

Select an option

Save schosin/4b99879ff6077aaabf9929e0cfc05a4e to your computer and use it in GitHub Desktop.
Stupid-ECS TestApplication
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;
}
}
@schosin
Copy link
Author

schosin commented May 10, 2025

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/.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment