BplusTools.SceneTree
— ModuleA scene-tree implementation, agnostic as to its memory layout. You can embed it in an ECS, or just throw Nodes into a Vector.
BplusTools.SceneTree.Node
— TypeThe data on a node in a scene tree. It's recommended to stick with the ID-based interface described below, rather than holding onto Node
instances. Otherwise you could miss a cache update!
If you don't use a Context for your scene graph, then you can omit it from function calls.
Transforms
Note that getting transform data can cause a node's cache to be updated within the owning Context.
local_transform(node_id, context=nothing)::Mat4
world_transform(node_id, context=nothing)::Mat4
world_inverse_transform(node_id, context=nothing)::Mat4
Operations
set_parent(node_id, new_parent_id, context=nothing; preserve_space = Spaces.self)
- You can pass a null ID for the new parent to make the child into a root node.
Iteration
siblings(node_id, context, include_self = true)
children(node, context)
parents(node, context)
family(node, context, include_self = true)
: uses an efficient depth-first search.family_breadth_first(node, context, include_self = true)
: uses a breadth-first search that works well for smaller trees.family_breadth_first_deep(node, context, include_self = true; buffer = NodeID[ ])
: uses a breadth-first search that works better for larger trees.
Utilities
try_deref_node(node_id, context)::Optional{TNode}
BplusTools.SceneTree.children
— FunctionIterates over the children of a node.
BplusTools.SceneTree.deref_node
— MethodGiven a node's unique identifier (assumed to be non-null), retrieves the node itself. If you don't need a 'Context' for your node IDs to be dereferenced, you can define an overload with only the ID parameter.
BplusTools.SceneTree.disconnect_parent
— MethodRemoves the given node from under its parent. Returns a copy of this node (does not update it in the context), but its parent and siblings will be modified. This is part of the implementation for some other functions, and it has some quirks:
- It won't invalidate the node's cached world-space data like you'd expect.
BplusTools.SceneTree.family
— FunctionIterates over all nodes underneath this one, in Depth-First order. Must take the root node by its ID, not the Node
instance.
For Breadth-First order (which is slower), see family_breadth_first
.
BplusTools.SceneTree.family_breadth_first
— MethodIterates over all nodes underneath this one, in Breadth-first order. Must take the root node by its ID, not the Node
instance.
This search is slower, but sometimes Breadth-First is useful. By default, a technique is used that has no heap allocations, but scales poorly for large trees. For a heap-allocating technique that works better on large trees, see family_breadth_first_deep
.
BplusTools.SceneTree.family_breadth_first_deep
— MethodIterates over all nodes underneath this one, in Breadth-first order. Must take the root node by its ID, not the Node
instance.
This search is slower than Depth-First, but sometimes Breadth-First is useful.
In this version, a heap-allocated queue is used to efficiently iterate over large trees. For smaller trees, consider using family_breadth_first
, which will not allocate.
BplusTools.SceneTree.invalidate_world_space
— MethodInvalidates the cached world-space data. May or may not include the rotation; for example if the node moved but didn't rotate, then there's no need to invalidate its world rotation.
Returns a copy of this node (does not update it in the context), but its children will be modified.
BplusTools.SceneTree.is_deep_child_of
— MethodGets whether a node is somewhere inside the 'family' of another node.
BplusTools.SceneTree.is_null_id
— MethodChecks whether a node ID is 'null'. By default, compares it to null_node_id(T)
with triple-equals (===
).
BplusTools.SceneTree.local_transform
— FunctionGets (or calculates) the local-space transform of this node. The node's Context is required for this operation.
BplusTools.SceneTree.local_transform
— MethodGets (or calculates) the local-space transform of this node, and updates the node's cache if necessary. Returns whether the node's cache was updated, and the new version of the node (which you should write back into the Context if the flag is true).
BplusTools.SceneTree.null_node_id
— MethodCreates a 'null' node of the given ID type. If the ID type is unsigned-integer, returns 0. If the ID type is signed-integer, returns -1. If the ID type can be Nothing
, returns nothing
.
BplusTools.SceneTree.on_rooted
— MethodCallback for when a node becomes a 'root' node, meaning it's parent-less.
BplusTools.SceneTree.on_uprooted
— MethodCallback for when a node gains a parent, and is no longer a 'root'.
BplusTools.SceneTree.parents
— FunctionIterates over the parent, grand-parent, etc. of a node.
BplusTools.SceneTree.set_parent
— MethodChanges a node's parent. If the new parent ID is null, the child is turned into a root node.
Preserves either the world-space or local-space transform of this node. Local-space is the default, because preserving world-space has more floating-point error.
BplusTools.SceneTree.siblings
— FunctionIterates over the siblings of a node, from start to finish, optionally ignoring the given one.
BplusTools.SceneTree.start_sibling_iter
— MethodFinds the start of a siblings iteration, or nothing
if there are no elements.
BplusTools.SceneTree.try_deref_node
— FunctionGets a node, or nothing
if the handle is null.
BplusTools.SceneTree.update_node
— MethodUpdates a node's value in its original storage space. The Node
type is immutable, so the original has to be replaced with a new copy.
Optionally takes the user-defined 'context'.
BplusTools.SceneTree.world_inverse_transform
— FunctionGets (or calculates) the inverse-world-space transform of this node, and updates the node's cache if necessary. The node's Context is required for this operation.
BplusTools.SceneTree.world_inverse_transform
— MethodA version of this function used internally. Returns whether the node's cache was updated, and the updated data.
BplusTools.SceneTree.world_transform
— FunctionGets (or calculates) the world-space transform of this node. The node's Context is required for this operation.
BplusTools.SceneTree.world_transform
— MethodA messy implementation version; you can call it if you really want but consider the other overload.
Gets (or calculates) the world-space transform of this node. This call may update cached data in its parent, grandparent, etc.
If this node's own cache needs updating, then this function also returns 'true' along with the updated version of this node (which you should write back into the Context).
BplusTools.Fields
— ModuleA data representation of continuous math, with multidimensional inputs and outputs, and the ability to get analytical derivatives of the math.
Useful for procedural generation and shader stuff.
For example, to generate an RGBA texture using each pixel's UV coordinate you'd want an AbstractField{2, 4, Float32}
.
BplusTools.Fields.AbstractField
— TypeA function of (Vec{NIn, F} -> Vec{NOut, F}
. For example, 3D perlin noise could be a Field{3, 1, Float32}
.
BplusTools.Fields.AbstractMathField
— TypeHolds a tuple of arguments, and represents a specific math expression
BplusTools.Fields.AggregateField
— TypeA field that has its own identity, but is definable in terms of simpler Fields.
If you define one of these, it's recommended to also implement dsl_from_field()
and field_from_dsl()
.
BplusTools.Fields.AppendField
— TypeCombines multiple fields together into a single, higher-dimensional output
BplusTools.Fields.CeilField
— TypeRounds values up to the nearest integer.
BplusTools.Fields.ClampField
— Typeclamp(x, min=0, max=1)
BplusTools.Fields.ConstantField
— TypeOutputs a constant value
BplusTools.Fields.ConstantField
— MethodCreates a constant field matching the output type of another field, with all components set to the given value(s).
BplusTools.Fields.ConversionField
— TypeTranslates data from one float-type to another. You can't really get different float types within a single field expression, but the full DSL allows you to compose multiple field expresions together.
BplusTools.Fields.DslContext
— TypeMeta-information that's needed to parse the field DSL.
BplusTools.Fields.DslState
— TypeA lookup for variables, arrays, etc. Gets modified during DSL parsing.
BplusTools.Fields.FloorField
— TypeRounds values down to the nearest integer.
BplusTools.Fields.FractField
— TypeGets the fractional part of a number, always positive (e.x. fract(-0.1) == 0.9
).
BplusTools.Fields.GradientField
— TypeSamples the gradient of a field, in a given direction. The direction can be an Integer
representing the position axis, a vector (AbstractField{NIn, NIn, F}
) that is dotted with the gradient, or Nothing
if the output should be a Vec with all components flattened together (this is most useful for 1D fields, where the gradient is just like a position input).
BplusTools.Fields.GradientField
— MethodConstructor for a GradientField that appends all the output gradients together into one vector
BplusTools.Fields.MaxField
— Typemax(a, b)
BplusTools.Fields.MinField
— Typemin(a, b)
BplusTools.Fields.ModField
— TypeFloating-point modulo: mod(a, b) == a % b
. The behavior with negative numbers matches Julia's %
operator.
BplusTools.Fields.MultiField
— TypeA sequence of fields. Each one will be sampled into an array and exposed to subsequent fields in the form of a TextureField
.
BplusTools.Fields.PerlinField
— TypePerlin noise, in any number of dimensions.
BplusTools.Fields.PosField
— TypeOutputs the input position
BplusTools.Fields.PowField
— Typepow(x, y) = x^y
.
If x is negative and y isn't an integer (e.x. pow(-1, 0.5)
), this function is undefined and just returns 0. If you want it to default to something other than 0, pass that as a third argument.
BplusTools.Fields.StepField
— Typestep(a, t)
outputs 1 if t >= a
, or 0
otherwise.
BplusTools.Fields.SwizzleField
— TypeSwaps the components of a field's output
BplusTools.Fields.TanField
— TypeTangent trig function
BplusTools.Fields.TextureField
— TypeSamples from a 'texture', in UV space (0-1). Pixels are positioned similarly to GPU textures, for example each pixel's center in UV space is (pixel_idx_0_based + 0.5) / texture_size
.
BplusTools.Fields.TruncField
— TypeGets the integer part of a number (e.x. trunc(-2.8) == -2.0
).
BplusTools.Fields.GradientType
— MethodThe type of a field's gradient (i.e. per-component derivative).
For example, a texture-like field (2D input, 3D float output) has a Vec{2, v3f}
gradient – along each of the two physical axes, the rate of change of each color channel.
BplusTools.Fields.dsl_from_field
— MethodConverts an AbstractField
into the custom DSL (as a Julia AST).
BplusTools.Fields.field_from_dsl
— MethodParses an AbstractField
from the custom DSL, given as a Julia AST
BplusTools.Fields.field_from_dsl_expr
— MethodParses an AbstractField
from the custom DSL, as a specific type of Julia Expr
BplusTools.Fields.field_from_dsl_func
— MethodParses an AbstractField
from the custom DSL, as a Julia function/operator call
BplusTools.Fields.field_gradient_epsilon
— MethodPicks an epsilon value for computing the gradient of a field with finite-differences
BplusTools.Fields.field_input_count
— MethodCounts the number of inner fields this field has
BplusTools.Fields.field_input_get
— MethodRetrieves a field within the given tree of field expressions, using an enumeration of child indices through the field expression tree.
Note that this is type-unstable!
BplusTools.Fields.field_input_get
— MethodGets one of the inner fields (as counted by field_input_count()
)
BplusTools.Fields.field_input_set
— MethodReturns a copy of this field, with one of its inner fields replaced, using an enumeration of child indices to pinpoint the field to replace within the expression tree.
Note that this is type-unstable!
BplusTools.Fields.field_input_set
— MethodReturns a copy of this field, with the given input field replaced
BplusTools.Fields.field_visit_depth_first
— FunctionVisits each abstract field starting at some outer field, depth-first. Invokes your visitor function, providing:
- The field itself (usually a copy, as fields are supposed to be immutable types)
- A temporary list of the path to this field, as a sequence of indices traversing the field tree.
Note that the first visited node will be the root, and its path will be an empty list.
For optimized memory use, you can provide a pre-allocated buffer.
Note that this function is type-unstable! It's recommended to make your lambda' first parameter @nospecialize
to reduce JIT overhead.
BplusTools.Fields.get_field
— MethodGets a field's value at a specific position.
If the field implements prepare_field()
, then that prepared data may get passed in. Otherwise, the third parameter will be nothing
.
BplusTools.Fields.get_field_gradient
— MethodGets a field's derivative at a specific position, per-component.
Defaults to using finite-differences, a numerical approach.
If the field implements prepare_field()
, then that prepared data may get passed in. Otherwise, the third parameter will be nothing
.
BplusTools.Fields.linear_sample_axis
— MethodRecursively performs linear sampling across a specific axis. Takes as input the interpolant between min and max pixels, the min/max pixel coordinates for all axes up to the current one, and the constant pixel coordinates for all axes past the current one.
BplusTools.Fields.prepare_field
— MethodPrepares to calculate many values for a field. The return value will be passed into get_field()
.
BplusTools.Fields.process_inputs
— MethodPre-processes the inputs of a field, so that 1D outputs are stretched to match the dimensions of the other outputs.
BplusTools.Fields.sample_field!
— FunctionSamples from a MultiField
, by running its full sequence and then sampling from its finale
BplusTools.Fields.sample_field!
— MethodFills an array by sampling the given field.
BplusTools.Fields.sample_field
— MethodCreates and fills a grid using the given field. Optional arguments are the same as sample_field!()
.
BplusTools.Fields.wrap_component
— MethodApplies a TextureField's wrapping to a given component of a position, in UV space (0-1).
BplusTools.Fields.wrap_index
— MethodApplies a TextureField's wrapping to the given pixel index component, returning a value between 1
and range_max_inclusive
.
BplusTools.Fields.@field
— MacroDefines a single Julia field.
Takes 3 or 4 arguments:
- The input dimensions, as an integer.
- The number type being used (e.x.
Float32
). - [Optional] An instance of a
DslState
- The field's value (e.x.
perlin(pos.yzx * 7)
)
Sample uses:
# Define a 3D voxel grid, i.e. 3D input to 1D output.
# The value of the grid comes from Perlin noise.
perlin_field = @field 3 Float32 perlin(pos)
# Define an "image" field, a.k.a. 2D pixel coordinates to 3D color (RGB).
# Use half-precision, a.k.a. 16-bit float.
# The previous field is available for reference through a special lookup.
field_lookups = Dict(:my_perlin => Stack{AbstractField}([ perlin_field ]))
array_lookups = Dict{Symbols, Array}()
dsl_state = DslState(field_lookups, array_lookups)
image_field = @field(2, Float16, dsl_state,
# Convert the perlin noise down to 16-bit float,
# then spread its scalar value across all 3 color channels.
(my_perlin => Float16).xxx
)
BplusTools.Fields.@make_math_field
— MacroShort-hand for generating an AbstractMathField
. Sample syntax:
@make_math_field Add "+" begin
INPUT_COUNT = 2
value = input_values[1] + input_values[2]
gradient = input_gradients[1] + input_gradients[2]
end
In the above example, the new struct is named AddField
, and the corresponding DSL function/operator is +
.
You construct an instance of AddField
by providing it with the input fields (and without any type parameters). Input fields with 1D outputs will be automatically promoted to the dimensionality of the other fields' outputs.
The full set of definitions you can provide is as follows:
- (Required)
INPUT_COUNT = [value]
defines how many inputs the node can have. Valid formats:- A constant:
INPUT_COUNT = 3
- A range:
INPUT_COUNT = 2:2:6
- A min bound:
INPUT_COUNT = 3:∞
(using the char \infty) - An explicit set:
INPUT_COUNT = { 0, 2, 3 }
- A constant:
- (Required)
value = [expr]
computes the value of the field, given the following parameters:field
: your field instance.NIn
,NOut
,F
,TFields<:Tuple
: The type parameters for your field, including the tuple of its inputs (all of which have the sameNIn
,NOut
, andF
types).pos::Vec{NIn, F}
: the position within the field being sampled.input_values::NTuple{_, Vec{NOut, F}}
: the value of each input field at this position.- If you don't need this, you can improve performance by disabling it with
VALUE_CALC_ALL_INPUT_VALUES
(see below).
- If you don't need this, you can improve performance by disabling it with
prep_data::Tuple
: the output ofprepare_field
for each input field.
gradient = [expr]
computes the gradient of the field, given the following parameters:field
: your field instanceNIn
,NOut
,F
,TFields<:Tuple
: The type parameters for your field, including the tuple of its inputs (all of which have the sameNIn
,NOut
, andF
types).pos::Vec{NIn, F}
: the position input into the field.input_gradients::NTuple{_, Vec{NIn, Vec{NOut, F}}}
: the gradient of each input field at this position.- If you don't need this, you can improve performance by disabling it with
GRADIENT_CALC_ALL_INPUT_GRADIENTS
(see below).
- If you don't need this, you can improve performance by disabling it with
input_values::NTuple{_, Vec{NOut, F}}
: the value of each input field at this position.- Not provided by default; you must enable it with
GRADIENT_CALC_ALL_INPUT_VALUES
(see below).
- Not provided by default; you must enable it with
prep_data::Tuple
: the output ofprepare_field
for each input field.
VALUE_CALC_ALL_INPUT_VALUES = [true|false]
. If true (the default value), then the computation ofvalue
has access to a local var,input_values
, containing all input fields' values. This can be disabled for performance if your math op doesn't always need every input's value.GRADIENT_CALC_ALL_INPUT_VALUES = [true|false]
. If true (default value is false), then the computation ofgradient
has access to a local var,input_values
, containing all input fields' values. This is disabled by default for performance; enable if your math op needs every input's value to compute its own gradient.GRADIENT_CALC_ALL_INPUT_GRADIENTS = [true|false]
. If true (the default value), then the computation ofgradient
has access to a local var,input_gradients
, containing all input fields' gradients. This can be disabled for performance if your math op doesn't always need every input's gradient.
BplusTools.Fields.@multi_field
— MacroDefines a sequence of fields, each one getting sampled into an array/texture and used by subsequent fields, culminating in a final field which can sample from all of the previous ones.
For the syntax of each field, refer to the @field
macro.
Sample usage:
@multi_field begin
# 2D perlin noise, sampled into a 128x128 texture.
perlin1 = 128 => @field(2, Float32, perlin(pos * 20))
# 3D perlin noise, sampled into a 32x32x128 texture.
perlin2 = {32, 32, 128} => @field(3, Float32, perlin(pos * 20 * { 1, 1, 10 }))
# The output field:
@field(3, Float32,
(perlin1{pos.xy}) *
(perlin2{pos})
)
end
BplusTools.BplusTools
— ModuleVarious helpers to make B+ games
BplusTools.CachedData
— TypeA cached version of some file data
BplusTools.Cam3D
— TypeA simple 3D camera representation
BplusTools.Cam3D_Input
— TypeThe user inputs used to set a 3D camera
BplusTools.Cam3D_Settings
— TypeThe movement/turning settings for the 3D camera.
BplusTools.FileAssociations
— TypeA set of files, and their last-known values for 'last time modified'
BplusTools.FileCacher
— TypeKeeps track of file data of a certain type and automatically checks it for changes. Has the following interface:
- Update it and check for modified files with
check_disk_modifications!()
.- Ideally, call this every frame.
- If at least one file changed, it returns true, and also raises a callback to reload each changed file.
- Get a file with
get_cached_data!()
. You can provide both relative and absolute paths. - Get the canonical representation of a file path within the cacher with
get_cached_path()
.
You can configure the cacher in a few ways:
reload_response(path[, old_data]) -> new_data[, dependent_files]
is called when either a file is being loaded for the first time, OR a file has changed and should be reloaded.- Returns the new file, and optionally an iterator of 'dependent' files. whose changes will also trigger a reload.
- If your response throws an error then
error_response
, mentioned below, is invoked.
error_response(path, exception, trace[, old_data]) -> [fallback_data]
is called when a file fails to load or re-load.- By default it
@error
s and returnsnothing
. - You can change it to, for example, return a fallback 'error data' object, or the previous version of the object.
- By default it
check_interval_ms
is a range of wait times in between checking files for changes. A wait time is randomly chosen for each file, to prevent them from all checking the disk at once.relative_path
is the prefix for relative paths.- By default, it's the process's current location.
BplusTools.OrthographicProjection
— TypeA flat perspective where parallel lines appear parallel on the screen. Useful for shadowmaps, 2D views, and isometric views. Specified as the range of values visible in camera view space; usually XY is centered around {0, 0} and Z goes from 0 to some world distance like 1000.
BplusTools.PerspectiveProjection
— TypeA traditional 3D perspective where lines converge to a point as they move away from the camera
BplusTools.cam_projection_mat
— MethodComputes the projection matrix for the given camera.
BplusTools.cam_rightward
— MethodGets the vector for this camera's right-ward direction.
BplusTools.cam_update
— MethodUpdates the given camera with the given user input, returning its new state. Also returns the new Settings, which may be changed.
BplusTools.cam_view_mat
— MethodComputes the view matrix for the given camera.
BplusTools.check_disk_modifications!
— FunctionFor a variety of objects, this checks all their known files' last-modified timestamp, and returns whether any of them have changed.
BplusTools.get_cached_data!
— MethodRetrieves the given file data, using a cached version if available.
Returns nothing
if the file can't be loaded and no fallback error value was provided to the cacher.
BplusTools.make_data_cache
— MethodLoads the given file and its cache metadata
BplusTools.@using_bplus_tools
— MacroImports all Tools B+ modules
BplusTools.ECS
— ModuleA simple ECS modeled after Unity3D'
BplusTools.ECS.COMPONENT_CODE
— ConstantIf PRINT_COMPONENT_CODE
is set, this global will hold the generated expression that was printed
BplusTools.ECS.PRINT_COMPONENT_CODE
— ConstantTo see the printout of new defined components, set this to some output stream
BplusTools.ECS.AbstractComponent
— TypeSome mutable struct representing a bundle of entity data and logic
BplusTools.ECS.Entity
— TypeAn organized collection of components
BplusTools.ECS.World
— TypeAn ordered collection of entities, including accelerated lookups for groups of components
BplusTools.ECS._Entity
— TypeAn organized collection of components.
The World
type is defined afterwards, so it's hidden through a type parameter. You should refer to this type using the alias Entity
.
BplusTools.ECS.add_component
— MethodAny other components that are required by the new component will be added first, if not in the entity already.
BplusTools.ECS.count_components
— MethodCounts the number of instances of the given component in the given entity.
Note that for UnionAll types the operation is a bit slower and requires a data structure; you may pass in a buffer to avoid heap allocations in this case.
BplusTools.ECS.count_components
— MethodCounts all components in the world of the given type.
For UnionAll types, the operation is a bit slower and requires a data structure; you may pass in a buffer to avoid heap allocations in this case.
BplusTools.ECS.create_component
— MethodCreates a new component that will be attached to the given entity.
Any dependent components named in require_components()
will already be available, except in recursive cases where multiple components require each other.
By default, invokes the component's constructor with any provided arguments.
BplusTools.ECS.destroy_component
— MethodCleans up a component that was attached to the given entity
BplusTools.ECS.get_component
— MethodIn Debug mode, throws an error if there is more than one of the given type of component for the given entity
BplusTools.ECS.get_component
— MethodGets a singleton component, assumed to be the only one of its kind. Returns its owning entity as well.
BplusTools.ECS.get_component_types
— MethodReturns a tuple of the component type, its parent type, etc. up to (but not including) AbstractComponent
BplusTools.ECS.get_components
— MethodGets an iterator of all instances of the given component attached to the given entity.
Note that for UnionAll types the operation is a bit slower and requires a data structure; you may pass in a buffer to avoid heap allocations in this case.
BplusTools.ECS.get_components
— MethodGets an iterator of all instances of the given component in the entire world. Each element is a Tuple{T, Entity}
.
Note that for UnionAll types the operation is a bit slower and requires a data structure; you may pass in a buffer to avoid heap allocations in this case.
BplusTools.ECS.is_entitysingleton_component
— MethodGets whether an entity can hold more than one of the given type of component.
If your components inherit from another abstract component type, it's illegal for the abstract type to return a different value than the concrete child types.
BplusTools.ECS.is_worldsingleton_component
— MethodGets whether a world can have more than one of the given type of component.
If your components inherit from another abstract component type, it's illegal for the abstract type to return a different value than the concrete child types.
BplusTools.ECS.remove_component
— MethodThis is allowed even if the component is required by another one. It's up to you to make sure your components either handle that or avoid that!
NOTE: the named keywords are for internal use; do not use them.
BplusTools.ECS.require_components
— MethodGets the types of components required by the given component.
If your components inherit from another abstract component type, it's illegal for the abstract type to specify requirements unless the concrete child types also specify them.
If you name an abstract component type as a requirement, make sure to define create_component()
for that abstract type (i.e. a default to use if the component doesn't already exist).
BplusTools.ECS.tick_component
— MethodUpdates the given component attached to the given entity.
Note that the entity reference is only given for convenience; the component will always have the same Entity owner that it did when it was created.
BplusTools.ECS.@component
— MacroA very covenient way to define a component.
The basic syntax is this:
@component Name [<: Parent] [attributes...] begin
field1::Float32
field2
# By default, a component is constructed by providing each of its fields, in order.
# Inherited fields come after the new fields.
# However, you can override this like so:
function CONSTRUCT(f)
# All functions within a @component can reference "this", "entity", and "world".
this.field1 = Float32(f)
this.field2 = length(world.entities)
# You must call SUPER to invoke the parent constructor.
SUPER(3, f+4)
end
function DESTRUCT() # Optionally take a Bool for whether the owning entity is being destroyed
println("Delta entities: ", length(world.entities) - this.field2)
end
function TICK()
this.field1 += world.delta_seconds
end
# FINISH_TICK() exists, but is only useful for abstract components (see below).
Attributes are enclosed in braces. Here are the supported ones:
{abstract}
: this component is an abstract type (see below).{entitySingleton}
: only one of this component can exist on an entity.{worldSingleton}
: only one of this component can exist in the entire world.{require: a, b, c}
: other components must exist on this entity, and will be added if they aren't already
This macro also provides an Object-Oriented architecture, where abstract components can add fields, behavior, "Promises" (abstract functions) and "Configurables" (virtual functions) to their concrete children. Here is a detailed example of an abstract component:
"Some kind of animated motion using float position data. Only one maneuver can run at a time."
@component Maneuver{F<:AbstractFloat} {abstract} {entitySingleton} {require: Position} begin
pos_component::Position{F}
duration::F
progress_normalized::F
# Because this type has a custom constructor, all child types must have one too.
# They also must invoke SUPER(...) exactly once, with these arguments.
function CONSTRUCT(duration_seconds::F)
this.progress_normalized = 0
this.duration = duration_seconds
this.pos_component = get_component(entity, Position)
end
# If you want to provide a default instance of this component,
# for when it's required but not already existing on an entity,
# you can implement this (pretend a child maneuver exists called 'TurningManeuver'):
DEFAULT() = TurningManeuver(3.5, has_component(entity, PreferLeft) ? -90 : 90)
# Non-abstract child components must implement this.
# Abstract child components may choose to implement this for their children.
# In the latter case, the concrete children can further override the behavior,
# and invoke `SUPER()` to get the original parent implementation.
# If `SUPER()` is called with no arguments,
# then the arguments given to the child implementation are automatically forwarded.
# If a child implements this, then a grand-child implements it with different parameter types,
# the child and grand-child can both participate in overload resolution,
# meaning children can extend or specialize parent promises.
@promise finish_maneuver(last_time_step::F)
# Child components may choose to override this.
# It must return a bool.
# It indicates whether to cut off the maneuver early.
# When overriding this, you can invoke SUPER() to get the implementation of your parent.
# If `SUPER()` is called with no arguments,
# then the arguments given to the child implementation are automatically forwarded.
# If a child implements this with different parameter types,
# the parent and child component can both participate in overload resolution,
# meaning children can extend or specialize a parent's configurables.
@conigurable should_stop()::Bool = false
# This abstract base type handles the timing for its children.
# Base class TICK() is called before children's TICK().
function TICK()
this.progress_normalized += world.delta_seconds / this.duration
end
# After all tick logic is done (including children), check whether the animation is finished.
# Base class FINISH_TICK() runs after children's FINISH_TICK().
function FINISH_TICK()
if this.should_stop()
remove_component(entity, this)
elseif this.progress_normalized >= 1
# Handle any overshoot of the target, then destroy self:
overshoot_seconds = (@f32(1) - this.progress_normalized) * this.duration
this.finish_maneuver(-overshoot_seconds)
remove_component(entity, this)
end
end
end
Finally, here is an example of StrafingManeuver
, a child of Maneuver
that only uses Float32:
@component StrafingManeuver <: Maneuver{Float32} {require: MovementSpeed} begin
speed_component::MovementSpeed
dir::v3f
function CONSTRUCT(duration::Float32, dir::v3f)
SUPER(duration)
this.speed_component = get_component(entity, MovementSpeed)
this.dir = dir
end
# Implement the maneuver's interface.
finish_maneuver(last_time_step::Float32) =
strafe(this, last_time_step) # Uses a helper function defined below
should_stop() =
do_collision_checking(entity) # Some function that checks for this entity colliding with geometry
TICK() = strafe(this) # Uses a helper function defined below
end
# Helper function for applying the maneuver.
function strafe(s::StrafingManeuver, time_step)
# In a normal Julia function, things are a bit less convenient.
entity = s.entity
world = s.world
if isnothing(time_step)
time_step = world.delta_seconds
end
s.pos_component.value += s.speed_component.value * time_step * s.dir
end
As a complement to the usual reflection data provided by Julia, you may access component reflection data (such as the declared @promise
s) through the interface at the top of this file, src/ecs/macros.jl. However note that this is an internal interface which may change in the future.