See the flyweight-components branch for details. (Yeah, I know, not strictly flyweight per definition).
Public API changes
These are the first changes to the API which will eventually break compatibility with the original artemis API.
These commits change a lot under the hood, but the public API changes have been kept to a minimum, instead relying on Artemis to handle it internally.
Entities create their own components
Entity#addComponent
methods have been deprecated, superseded by Entity#createComponent(Class<Component>)
. It returns the newly created component; so constructor initialization is no longer possible, neither is passing previously constructed components (might change this in the future, for non-Pooled and non-Packed components). It's ugly, but quite hard to get around. Fluent setters could work, but those are a pain to generate in Eclipse. Not sure about other IDE:s.
Entity#addComponent
still works like before, except it throws an Exception if a PackedComponent is passed to it.
New component types
PooledComponent
Nothing much to say about it, it has a reset()
method, requires a zero-argument constructor. Automatically reclaimed during removal. Only permits non-final fields.
PackedComponent
Right now, PackedComponents have to conform to a memory-friendly definition to be useful, which is a little cumbersome. The most notable ramifications for PackedComponent types:
- No direct field access
- Some boilerplate methods defined by PackedComponent
- It easy to mistakenly work on the wrong entity when working on same-type components of different entities. A
PackedComponentMapper
keeps its own view of the PackedComponent, so different mappers working on the same type will never interfere (assuming the component definition is valid).
For sake of efficiency, a single backing array per component type should be used to store all data. This becomes a little awkward when different primitive types are employed, requiring some bit masking and shifting.
It should be noted that PackedComponents only ever make sense for scenarios where a huge number of entities are processed. It makes debugging more difficult, requiring helper methods for debugging etc.
A simple example:
package com.github.junkdog.hallucinolog.component;
import com.artemis.PackedComponent;
import com.badlogic.gdx.math.Vector2;
public class Position extends PackedComponent {
private static final int COMPONENT_SIZE = 2;
private int entityId; // should be instance field
private int index; // should be instance field
private static DumbUnsafeFloatArray mem = new DumbUnsafeFloatArray();
@Override
public void reset() {
mem.items[index + 0] = 0;
mem.items[index + 1] = 0;
}
public float x() {
return mem.items[index + 0];
}
public Position x(float x) {
return this;
}
public float y() {
return mem.items[index + 1];
}
public Position xy(float x, float y) {
mem.items[index + 0] = x;
mem.items[index + 1] = y;
return this;
}
public Position y(float y) {
mem.items[index + 1] = y;
return this;
}
public Position addX(float x) {
mem.items[index + 0] += x;
return this;
}
public Position addY(float y) {
mem.items[index + 1] += y;
return this;
}
public Position add(float x, float y) {
mem.items[index + 0] += x;
mem.items[index + 1] += y;
return this;
}
public PackedComponent setEntityId(int entityId) {
this.entityId = entityId;
this.index = entityId * COMPONENT_SIZE;
if ((mem.items.length - 1) <= index) mem.grow();
return this;
}
private static class DumbUnsafeFloatArray {
float[] items;
public DumbUnsafeFloatArray() {
items = new float[64];
}
private void grow() {
float[] old = items;
items = new float[(old.length * 2)];
System.arraycopy(old, 0, items, 0, old.length);
}
}
}
So, this is a little awkward and prone to making mistakes. As noted in the #9, I intend to create a bytecode transformation plugin to automate most of this work, relying on annotations to specify transformations during compile-time.
This approach would make it easier to write up PackedComponents, but introduce even more difficult debugging since your IDE of choice probably won't figure out that the class has changed. On the flip-side though, reverting the component to a normal Component
is as simple as removing the annotation.
package com.github.junkdog.hallucinolog.component;
import com.artemis.PackedComponent;
import com.badlogic.gdx.math.Vector2;
@PackedComponentWeaver
public class Position extends Component {
private float x;
private float y;
public float x() {
return x;
}
public Position x(float x) {
this.x = x;
return this;
}
public float y() {
return mem.items[index + 1];
}
public Position xy(float x, float y) {
this.x = x;
this.y = y;
return this;
}
public Position y(float y) {
this.y = y;;
return this;
}
public Position addX(float x) {
this.x += x;
return this;
}
public Position addY(float y) {
this.y += y;
return this;
}
public Position add(float x, float y) {
this.x += x;
this.y += y;
return this;
}
}
@PooledComponentWeaver
could be used to transform Components into PooledComponents too, removing the reset()
method boilerplate.
For now though, until #9 is resolved, components must be manually declared and extend the appropriate class.