There are three main implementations of nodes in the scene graph that LittleKt offers. Those three are the base Node, a 2D-based node Node2D, and the user interface module of nodes. We will be reviewing Node
and Node2D
here. Check out The User Interface if you want to learn more on the UI.
Node
The Node
contains the base implementation that is used by all nodes in a scene graph. This contains lifecycle methods, render methods, adding/removing children, and any hierarchy changes.
A Node
can contain one or more Signal types which can be used to subscribe to certain events that the node can emit.
For example, the Node
class contains the following signals:
onReady
: is emitted when theready()
callback is invokedonRender
: is emitted when therender()
callback is invokedonDebugRender
: is emitted whenonDebugRender()
callback is invokedonPreUpdate
: is emitted whenpreUpdate()
callback is invokedonUpdate
: is emitted whenupdate()
callback is invokedonPostUpdate
: is emitted whenpostUpdate()
callback is invokedonFixedUpdate
: is emitted whenfixedUpdate()
callback is invoked
These signals can allow us to subscribe to these node lifecycle events without having to create a new class that inherits Node
. This is useful if we want to do something simple with a node without having to go through all the trouble of creating a specific node to do so.
A Node
can also add and remove children directly:
addChild(node)
: will set the parent of the specified node to this nodeaddChildren(nodes)
: adds a list of children to this noderemoveChild(node)
: will remove this node as the parent from the child node
We can also change the parent of a node:
parent(node?)
: set this nodes parent to the new node. The parent can also be null.
Custom Nodes
We can create our own custom nodes by creating a new class that inherits from Node
.
class NewNode : Node {
var health = 5
override fun update(dt: Duration) {
super.update(dt)
// do something?
}
}
If we want our new node to follow the same DSL pattern when creating a graph as the other nodes we can add something like this outside of the class:
// we want both of these - one for creating inside a scene graph context and the other inside a node context
inline fun NewNode.newNode(callback: @SceneGraphDslMarker NewNode.() -> Unit = {}) =
NewNode().also(callback).addTo(this)
inline fun SceneGraph.newNode(callback: @SceneGraphDslMarker NewNode.() -> Unit = {}) = root.newNode(callback)
If we don’t want to spend the time creating these new DSL methods, then we can use a DSL method that accepts another node type instead:
val scene = sceneGraph(context) {
node(NewNode()) { // using the existing Node method instead
health = 10
}
newNode { // using our newly created methods instead
health = 10
}
}
CanvasItem and Node2D
The CanvasItem
class (and the Node2D
class which extends CanvasItem
) contains an implementation on transforming a node in 2D space. The includes position, rotation, and scale. We have access to the local and global versions of these components. Making a change to any of these properties will dirty the hiearchy and update it’s children.
val scene = sceneGraph(context) {
node2d {
x = 10f // local position
node2d {
x = 10f // local position
onReady += {
println(globalX) // outputs 20
}
}
node2d {
globalX = 5f
}
}
}
If a Node2D
is a child to a base Node
, it will not receive any 2D transformation hierachy updates. This includes if the base nodes parent is also a Node2D
. This is due to the fact that a Node
does not have the information to pass along the information to update a Node2D without specifically having to determine if any of its children are in fact a Node2D
. Instead, the Node2D
will assume it is a top-level Node2D
and will act as so.
val scene = sceneGraph(context) {
node {
node2d {
x = 10f
rotation = 90.degrees
node {
node2d {
x = 10f
onReady += {
println(globalX) // outputs 10
println(globalRotation.degrees) // outputs 0
}
}
}
}
}
}
By design, the position
, globalPosition
, scale
, and globalScale
properties return an immutable Vec2f
. This prevents us from accidentally updating the vector directly which would skip over needing to update the hiearchy. If we want to set a component of the vector directly, we can use the x
and y
properties instead:
position
: local position immutable vectorx
: local position xy
: local position yglobalPosition
: global position immutable vectorglobalX
: global position xglobalY
: global position yscale
: local scale immutable vectorscaleX
: local scale xscaleY
: local scale yglobalScale
: global scale immutable vectorglobalScaleX
: global scale xglobalScaleY
: global scale y
Render Sorting
A [Node2D] contains an option to sort it’s child nodes by their y-position setting the ySort
property to true
. All this simply does is set the nodes.sort
property to the internal SORT_BY_Y
comparator.
We can use nodes.sort
to set our own render sorting while keeping the same update order. By default, rendering is done by tree order, top to bottom.
val SORT_BY_Y: Comparator<Node> = Comparator { a, b ->
if (a is CanvasItem && b is CanvasItem) {
return@Comparator a.globalY.compareTo(b.globalY)
}
if (a is CanvasItem) {
return@Comparator 1
}
if (b is CanvasItem) {
return@Comparator -1
}
return@Comparator 0
}
node2d {
nodes.sort = SORT_BY_Y
}
Material
A CanvasItem
contains Material instance that can be used to set shaders, blend modes, and depth/stencil modes. The SceneGraph
handles any changes of the material of a CanvasItem
which will flush the current batch thus increasing by a draw call.
node2d {
material.blendMode = BlendMode.Add
// or we can set shader
material = Material(MyShader())
Blend Mode Types
All the blend modes that can be used in a material are all under the BlendState class. Each type is a singleton object that can be accessed directly as so: BlendState.Alpha
.
Alpha
Opaque
NonPreMultiplied
Add
Subtract
Difference
Multiply
Lighten
Darken
Screen
LinearDodge
LinearBurn
CanvasLayer
A CanvasLayer
node is the node that contains an OrthographicCamera
that is used for rendering any children nodes as well as a Viewport
. This node can be used to render nodes using different viewport and camera dimensions and positions. For example, this can be useful when we want to separate rendering a high resolution UI with a low resolution game. A CanvasLayer can be thought as a render target.
val scene = sceneGraph(context) {
canvasLayer {
viewport = ExtendViewport(480, 270)
// any children now will be rendered using the viewport above!
node2d {
// render my game nodes based on the canvasLayer camera!
}
}
control {
name = "UI
// render my UI based on the scene graph viewport!
}
}