Data structures.
Requires Haxe 4 (developed with v4.1.3).
Fixed-length array. Unlike the standard haxe.ds.Vector,
- Read-only type and Writable type are strictly separated.
- Uses
hl.NativeArrayon HashLink target.
Internally based on the vector type above.
- Stack / List
- Queue / Deque
- Set / Map
- ...
No allocation/GC by adding/removing/iterating elements.
For profiling usage ratio of data collection objects.
Generates node/list classes, either singly or doubly linked.
Generates AoSoA (Array of Structures of Arrays) from any user-defined class.
Generates a map-like class from any user-defined enum abstract type.
Some other small features like ObjectPool classes.
Unlike the general data containers,
- No automatic expanding (simply crashes if it's full)
- No iterators (because they have overheads and invoke GC)
Internally uses assertion feature of sneaker library, which means:
- Boundary checks in debug build, and
- Unsafe, but efficient access in release build
Primarily intended for use in game programming.
Suited for following situations:
- Lots of iteration
- Frequent access/changes
- Need to eliminate (or reduce, at least) Garbage Collection pauses
- Reducing overhead is more important than time complexity
- All of this is nothing but reinventing the wheel!
- Don't know much about other libraries/frameworks
- Developed within a few weeks and not yet very well tested
- Super-unstable!!!
Fixed-length array.
Collection classes with 1 type parameter.
Collection classes with 2 type parameters.
Provides object pool classes:
ObjectPool<T>SafeObjectPool<T>
Say you have a class named Actor, an example of ObjectPool would be:
import banker.pool.ObjectPool;
class Main {
static function main() {
final factory = () -> new Actor();
final pool = new ObjectPool<Actor>(10, factory);
final actorA = pool.get();
final actorB = pool.get();
// pool.size == 8
pool.put(actorA);
pool.put(actorB);
// pool.size == 10
}
}SafeObjectPool does boundary checks and does not crash even if it is empty/full
(note that it requires additional memory allocation when trying to get from an empty pool).
ObjectLender is an alternative kind of object pool which can collect() objects and force them to be used again.
You can also create your own pool classes by extending ObjectPoolBase or ObjectPoolBuffer class.
If the compiler flag banker_watermark_enable is set, "watermark" feature is activated.
This is a simple profiling feature for all data collection objects (in container, map and pool packages)
that have limited capacity.
It automatically records the maximum usage (size to capacity ratio) per instance group,
which enables you to check and adjust the capacity of each data collection object.
Instances are grouped by the name of Tag that is attached to each instance.
About the Tags, see also the sneaker library which underlies this feature.
Just set the compiler flag banker_watermark_enable, and the profiling runs automatically.
To see the result, call the below whenever you like:
banker.watermark.Watermark.printData();First create your own class for queue nodes and implement SinglyLinkable interface.
This automatically adds some fields which enables you to link instances of that class.
package mypackage;
class MyQueueNode implements banker.link.SinglyLinkable {
public final myValue: Int;
public function new(myValue: Int) {
this.myValue = myValue;
}
}Then create your own class for a queue and apply the build macro as below.
This adds some fields such as enqueue()/dequeue()/forEach().
package mypackage;
@:build(banker.link.SinglyLinkedQueue.from(mypackage.MyQueueNode))
class MyQueue {}Similar to above:
package mypackage;
class MyDequeNode implements banker.link.DoublyLinkable {
public final myValue: Int;
public function new(myValue: Int) {
this.myValue = myValue;
}
}package mypackage;
@:build(banker.link.DoublyLinkedDeque.from(mypackage.MyDequeNode))
class MyDeque {}First create your entity class, and implement banker.aosoa.Structure,
which enables you to use an AoSoA (Array of Structures of Arrays)
generated from your original class.
See also:
Data-oriented design
Caveats:
The main purpose is improving the performance, however I still don't know much about low-level programming and I might be doing everything wrong!
Here move()/use() are user-defined functions;
and Position/Velocity are user-defined values.
You can define any variables and functions for your purpose.
Define any class (Actor here, which has x/y position data) and implement banker.aosoa.Structure.
import banker.vector.WritableVector as Vec;
class Actor implements banker.aosoa.Structure {
/**
This will append a method `use(initialX, initialY)` to the AoSoA class.
**/
@:banker.useEntity
static function use(
x: Vec<Float>, y: Vec<Float>, i: Int, initialX: Float, initialY: Float
): Void {
x[i] = initialX;
y[i] = initialY;
}
/**
This will append a method `print()` (without arguments) to the AoSoA.
**/
static function print(x: Float, y: Float): Void {
trace('{ x: $x, y: $y }');
}
/**
This will append `moveHorizontal(dx)`.
**/
static function moveHorizontal(
x: Vec<Float>,
dx: Float,
i: Int
): Void {
x[i] += dx;
}
/** This will be converted to a vector. **/
var x: Float = 0;
/** Ditto. **/
var y: Float = 0;
}Then define Chunk/AoSoA classes as below.
Their fields are generated by the build macro.
import banker.vector.WritableVector as Vec; // same import as the Structure
@:build(banker.aosoa.Chunk.fromStructure(Actor))
class ActorChunk {}@:build(banker.aosoa.Aosoa.fromChunk(ActorChunk))
class ActorAosoa {}Now you can create an AoSoA by new ActorAosoa(chunkCapacity, chunkCount);.
class Main {
static function main() {
// (2 entities per Chunk) * (3 Chunks) = (max 6 entities)
final actors = new ActorAosoa(2, 3);
trace("Use 4 entities and print them.");
for (i in 0...4) actors.use(i, i); // set both x and y to i
actors.synchronize(); // Necessary for reflecting any change
actors.print();
trace("Move all and print again.");
actors.moveHorizontal(10); // x += 10 for each
actors.synchronize();
actors.print();
}
}Main.hx:6: Use 4 entities and print them.
Actor.hx:26: { x: 0, y: 0 }
Actor.hx:26: { x: 1, y: 1 }
Actor.hx:26: { x: 2, y: 2 }
Actor.hx:26: { x: 3, y: 3 }
Main.hx:11: Move all and print again.
Actor.hx:26: { x: 10, y: 0 }
Actor.hx:26: { x: 11, y: 1 }
Actor.hx:26: { x: 12, y: 2 }
Actor.hx:26: { x: 13, y: 3 }
- An AoSoA consists of multiple Chunks (or SoA: Structure of Arrays).
- Each chunk has a fixed capacity and consists of vector data that are converted from the original
Structureclass with the same variable names.
- Any static
Voidfunction with metadata@:banker.useEntityis converted to a method which finds a new available entity and sets initial values. - Any other static
Voidfunction is converted to an iterator method, which iterates all entities that are currently in use. - You should not write
returnexplicitly in these functions as the expressions are simply copied intowhileloops.
- Arguments that match any of the variable names are internally provided in the AoSoA/Chunk so you don't need to pass them manually.
- Define an argument with the original type (e.g.
x: Float) to get READ access. - Define an argument with the vector type (e.g.
x: banker.vector.WritableVector<Float>) for WRITE access. - If you need WRITE access, you also have to include a special argument
i: Int.
Then use it as an index for writing to vectors. - For disusing (releasing) an entity, define a special argument
disuse: Boolin any iterator function.
Then writedisuse = trueunder any condition. This will release the entity the next time you callsynchronize()(below). - You can also include any chunk-level variable (see below) in arguments. This automatically declares local variables before the loop and saves the change (if not
final) after the loop, so that you don't need to manually accessthis.myChunkLevelVariable.
- Each AoSoA instance has a method
synchronize(), which reflects use/disuse/other changes of entities.
The changes are buffered and are not reflected unless you call this. - If you have any function (either entity-level iterator or chunk-level method) with metadata
@:banker.onSynchronizeor@:banker.onCompleteSynchronize, that function is automatically called for each chunk before/after the synchronization whensynchronize()is called.
- Type hint is mandatory when declaring variables in your
Structureclass. - You can set an initializing value at the declaration, e.g.
var x: Float = 0, which will be used for every entity. - Add metadata
@:banker.factory(anyFactoryFunction)to the variable to use the factory function instead of filling all entities with the same value. The function should be() -> ?.
Alternatively, add metadata@:banker.factoryWithId(anyFactoryFunction)to use a factory function of type(id: ChunkEntityId) -> ?. - If you provide neither an inital value nor a factory, you have to pass the initial value to
new()when instanciating the AoSoA class. - Add metadata
@:banker.externalFactoryto the variable for enabling to pass any factory function instead of constant value when instanciating the AoSoA class.
- If a field has
@:banker.chunkLevelmetadata, it will be copied to the Chunk class without converting to vectors or iterators (written above). - Static variables are automatically considered as chunk-level.
- By adding metadata
@:banker.chunkLevelFactoryyou can specify a factory function(chunkCapacity: Int) -> ?for a chunk-level variable. In that case@:banker.chunkLevelmetadata can be omitted.
There are built-in variables as below (which can also be used as arguments in user-defined functions):
| level | variable | description |
|---|---|---|
| chunk | chunkId: Int |
The id of the chunk that is unique in an AoSoA. |
| entity | entityId: Int |
The id of the entity that is unique in an Chunk. |
| entity | id: banker.aosoa.ChunkEntityId |
The id of the entity that is unique in an AoSoA. |
Note that entityId may not be identical to the physical index in variable vectors.
For example, if your entity has a variable x: Float:
class Main {
static function getX(aosoa: YourAosoa, chunkId: Int, entityId: Int): Float {
final chunk = aosoa.chunks[chunkId];
final index = chunk.entityIdReadIndexMap[entityId];
final x = chunk.x[index];
return x;
}
}Or using an abstract type banker.aosoa.ChunkEntityId:
class Main {
static function getX(aosoa: YourAosoa, id: ChunkEntityId): Float {
final chunk = aosoa.getChunk(id);
final index = chunk.getReadIndex(id);
final x = chunk.x[index];
return x;
}
}- Set the compiler flag
sneaker_macro_log_levelto 500 or more to show debug logs during the class generation. - By adding metadata
@:banker.verifiedto yourStructureclass, you can suppress debug logs for that class individually, without changing the whole log level.
| Metadata | Category | Description |
|---|---|---|
| @:banker.useEntity | method | Mark function as a "use" method |
| @:banker.factory | variable | Specifies a factory function for initializing each element of vector |
| @:banker.factoryWithId | variable | Ditto |
| @:banker.externalFactory | variable | Enables to pass any factory function to new() for inializing each element of vector (or the variable itself if chunk-level) |
| @:banker.readOnly | variable | Prevents an entity-level variable from providing WRITE access |
| @:banker.hidden | field | Prevents to be copied to Chunk/AoSoA |
| @:banker.swap | variable | Swap elements (instead of overwriting) when disusing entity |
| @:banker.chunkLevel | field | Mark as a chunk-level field |
| @:banker.chunkLevelFinal | variable | Mark as a chunk-level final variable |
| @:banker.chunkLevelFactory | variable | Specifies function (chunkCapacity: Int) -> ? for initializing a chunk-level variable |
| @:banker.onSynchronize | method | Mark method so that it is automatically called before every synchronization |
| @:banker.onCompleteSynchronize | method | Mark method so that it is automatically called after every synchronization |
| @:banker.verified | class | Mark as verified |
Provides a build macro for creating a map-like class from an enum abstract type.
Say you have an enum abstract like this:
enum abstract MyEnumAbstract(Int) {
final A;
final B;
final C;
}You can create a class with build macro banker.finite.FiniteKeys.from(),
where all enum values are converted to a variable with any specified type.
Without providing any initial value, each variable has Bool type, initialized with false.
@:build(banker.finite.FiniteKeys.from(MyEnumAbstract))
class MySet {}final mySet = new MySet();
trace(mySet.A); // false
mySet.A = true;
trace(mySet.A); // trueYou can specify an initial value with any type by adding a variable that is either:
- named
initialValue, or - added
@:banker.initialValuemetadata.
@:build(banker.finite.FiniteKeys.from(MyEnumAbstract))
class MyMap {
static final initialValue: Int = 0;
}You can also specify a function, which will be treated as a factory function for initializing each variable.
@:build(banker.finite.FiniteKeys.from(MyEnumAbstract))
class MyMap2 {
static function initialValue(key: MyEnumAbstract): Int {
return switch key {
case A: 1;
case B: 2;
case C: 3;
};
}
}The class will also have some methods such as get(key), set(key, value) and forEach(callback).
By adding @:banker.final metadata to the class,
- All generated variables will be declared as
final, and - The class will not have setter methods.
Similar to the aosoa package,
- Set the compiler flag
sneaker_macro_log_levelto 500 or more to show debug logs during the generation. - You can suppress debug logs without changing the whole log level by adding
@:banker.verifiedmetadata to your class.
Types for bit/byte sequences.
Bits(based onInt)Bytes(fast, limited and unsafe version ofhaxe.io.Bytes)
Some other small types are avaliable in this package:
NaiveSetReference
Regarding packages container, map, pool and aosoa:
If you are using completion server, sometimes it might go wrong and raise odd errors due to reusing of macro context.
In that case you may have to reboot it manually (if VSCode, >Haxe: Restart Language Server or >Developer: Reload Window).
| library | flag | description |
|---|---|---|
| banker | banker_watermark_enable | Enables watermark mode (see above). |
| banker | banker_generic_disable | Disables @:generic meta. |
| sneaker | sneaker_macro_log_level | Threshold for filtering macro logs. 500 or more to show all, less than 300 to hide WARN/INFO/DEBUG logs. |
| sneaker | sneaker_macro_message_level | Similar to above. See sneaker for more details. |
- sinker v0.5.0 or compatible
- prayer v0.1.3 or compatible
- sneaker v0.11.0 or compatible
- ripper v0.4.0 or compatible
See also: FAL Haxe libraries