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.NativeArray
on 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 Tag
s, 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
Structure
class with the same variable names.
- Any static
Void
function with metadata@:banker.useEntity
is converted to a method which finds a new available entity and sets initial values. - Any other static
Void
function is converted to an iterator method, which iterates all entities that are currently in use. - You should not write
return
explicitly in these functions as the expressions are simply copied intowhile
loops.
- 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: Bool
in any iterator function.
Then writedisuse = true
under 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.onSynchronize
or@: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
Structure
class. - 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.externalFactory
to the variable for enabling to pass any factory function instead of constant value when instanciating the AoSoA class.
- If a field has
@:banker.chunkLevel
metadata, 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.chunkLevelFactory
you can specify a factory function(chunkCapacity: Int) -> ?
for a chunk-level variable. In that case@:banker.chunkLevel
metadata 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_level
to 500 or more to show debug logs during the class generation. - By adding metadata
@:banker.verified
to yourStructure
class, 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); // true
You can specify an initial value with any type by adding a variable that is either:
- named
initialValue
, or - added
@:banker.initialValue
metadata.
@: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_level
to 500 or more to show debug logs during the generation. - You can suppress debug logs without changing the whole log level by adding
@:banker.verified
metadata 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:
NaiveSet
Reference
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