Skip to content

Commit

Permalink
Scene graph optimizations (#198)
Browse files Browse the repository at this point in the history
* graph: add four new signals to Node and CanvasItem (onEnabled, onDisabled, onVisible, and onInvisible)

graph: update Container node to handle new visibility / enable state changes for measuring

graph: rework slightly how destroying children works

samples: expand UiPlayground

* graph: add missing visibility check to BoxContainer
  • Loading branch information
LeHaine committed Nov 14, 2022
1 parent 1026819 commit 5812f4a
Show file tree
Hide file tree
Showing 11 changed files with 284 additions and 110 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -297,14 +297,6 @@ open class SceneGraph<InputType>(
initialized = true
}

/**
* Destroy all the children in [root].
*/
fun destroyRoot() {
root.children.forEach { it.destroy() }
root.nodes.updateLists()
}

/**
* Renders the entire tree.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@ import com.lehaine.littlekt.math.Mat4
import com.lehaine.littlekt.math.MutableVec2f
import com.lehaine.littlekt.math.Vec2f
import com.lehaine.littlekt.math.geom.Angle
import com.lehaine.littlekt.util.Signal
import com.lehaine.littlekt.util.TripleSignal
import com.lehaine.littlekt.util.signal
import com.lehaine.littlekt.util.signal3v
import kotlin.js.JsName
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.sin
Expand Down Expand Up @@ -94,6 +97,38 @@ abstract class CanvasItem : Node() {
*/
val onDebugRender: TripleSignal<Batch, Camera, ShapeRenderer> = signal3v()

/**
* List of 'onVisible' callbacks called when [onVisible] is called. Add any additional callbacks directly to this list.
* The main use is to add callbacks directly to nodes inline when building a [SceneGraph] vs having to extend
* a class directly.
*
* ```
* node {
* onVisible += {
* // visibility logic
* }
* }
* ```
*/
@JsName("onVisibleSignal")
val onVisible: Signal = signal()

/**
* List of 'onInvisible' callbacks called when [onInvisible] is called. Add any additional callbacks directly to this list.
* The main use is to add callbacks directly to nodes inline when building a [SceneGraph] vs having to extend
* a class directly.
*
* ```
* node {
* onInvisible += {
* // invisibility logic
* }
* }
* ```
*/
@JsName("onInvisibleSignal")
val onInvisible: Signal = signal()

/**
* Shows/hides the node if it is renderable.
*/
Expand Down Expand Up @@ -387,9 +422,17 @@ abstract class CanvasItem : Node() {
_visible = value
nodes.forEach {
if (it is CanvasItem) {
it._visible = value
it.visible(value)
}
}

if (_visible) {
onVisible()
onVisible.emit()
} else {
onInvisible()
onInvisible.emit()
}
}
return this
}
Expand All @@ -400,7 +443,7 @@ abstract class CanvasItem : Node() {
shapeRenderer: ShapeRenderer,
renderCallback: ((Node, Batch, Camera, ShapeRenderer) -> Unit)?,
) {
propagateDebugRender(batch,camera,shapeRenderer,renderCallback)
propagateDebugRender(batch, camera, shapeRenderer, renderCallback)
}


Expand Down Expand Up @@ -432,6 +475,7 @@ abstract class CanvasItem : Node() {
propagateRender(batch, camera, shapeRenderer, renderCallback)
propagatePostRender(batch, camera, shapeRenderer)
}

/**
* Internal pre rendering that needs to be done on the node that shouldn't be overridden. Calls [propagatePreRender] method.
*/
Expand Down Expand Up @@ -512,6 +556,17 @@ abstract class CanvasItem : Node() {
*/
open fun debugRender(batch: Batch, camera: Camera, shapeRenderer: ShapeRenderer) {}


/**
* Called when [visible] is set to `true`.
*/
protected open fun onVisible() = Unit

/**
* Called when [visible] is set to `false`.
*/
protected open fun onInvisible() = Unit

override fun callInput(event: InputEvent<*>) {
if (!enabled || !insideTree || isDestroyed) return

Expand Down Expand Up @@ -832,6 +887,8 @@ abstract class CanvasItem : Node() {
onRender.clear()
onPostRender.clear()
onDebugRender.clear()
onVisible.clear()
onInvisible.clear()
}

/**
Expand Down
57 changes: 54 additions & 3 deletions core/src/commonMain/kotlin/com/lehaine/littlekt/graph/node/Node.kt
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,38 @@ open class Node : Comparable<Node> {

val onChildEnteredTree: SingleSignal<Node> = signal1v()

/**
* List of 'onEnabled' callbacks called when [onEnabled] is called. Add any additional callbacks directly to this list.
* The main use is to add callbacks directly to nodes inline when building a [SceneGraph] vs having to extend
* a class directly.
*
* ```
* node {
* onEnable += {
* // visibility logic
* }
* }
* ```
*/
@JsName("onEnabledSignal")
val onEnabled: Signal = signal()

/**
* List of 'onDisabled' callbacks called when [onDisabled] is called. Add any additional callbacks directly to this list.
* The main use is to add callbacks directly to nodes inline when building a [SceneGraph] vs having to extend
* a class directly.
*
* ```
* node {
* onDisabled += {
* // invisibility logic
* }
* }
* ```
*/
@JsName("onDisabledSignal")
val onDisabled: Signal = signal()

private fun propagateExitTree() {
nodes.forEach {
it.propagateExitTree()
Expand Down Expand Up @@ -738,7 +770,14 @@ open class Node : Comparable<Node> {
if (_enabled != value) {
_enabled = value
nodes.forEach {
it._enabled = value
it.enabled(value)
}
if (_enabled) {
onEnabled()
onEnabled.emit()
} else {
onDisabled()
onDisabled.emit()
}
}
return this
Expand All @@ -764,8 +803,8 @@ open class Node : Comparable<Node> {
fun destroy() {
_enabled = false
_isDestroyed = true
nodes.forEach {
it.destroy()
while (nodes.isNotEmpty()) {
nodes[0].destroy()
}
onDestroy()
onDestroy.emit()
Expand All @@ -779,6 +818,8 @@ open class Node : Comparable<Node> {
onAddedToScene.clear()
onChildEnteredTree.clear()
onChildExitedTree.clear()
onEnabled.clear()
onDisabled.clear()
}

/**
Expand Down Expand Up @@ -874,6 +915,16 @@ open class Node : Comparable<Node> {
*/
protected open fun unhandledInput(event: InputEvent<*>) = Unit

/**
* Called when [enabled] is set to `true`.
*/
protected open fun onEnabled() = Unit

/**
* Called when [enabled] is set to `false`.
*/
protected open fun onDisabled() = Unit

/**
* @return a tree string for all the child nodes under this [Node].
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,8 @@ class NodeList {

updateLists()

nodes.fastForEach {
it.destroy()
while (nodes.isNotEmpty()) {
nodes[0].destroy()
}
nodes.clear()
sortedNodes.clear()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,25 +71,27 @@ abstract class BoxContainer : Container() {
minSizeCache.clear()

nodes.forEach {
if (it is Control && it.enabled && it.visible && !it.isDestroyed) {
val minSize: Int
val willStretch: Boolean

if (vertical) {
stretchMin += it.combinedMinHeight.toInt()
minSize = it.combinedMinHeight.toInt()
willStretch = it.verticalSizeFlags.isFlagSet(SizeFlag.EXPAND)
} else {
stretchMin += it.combinedMinWidth.toInt()
minSize = it.combinedMinWidth.toInt()
willStretch = it.horizontalSizeFlags.isFlagSet(SizeFlag.EXPAND)
}
if (it is Control) {
if (it.enabled && it.visible && !it.isDestroyed) {
val minSize: Int
val willStretch: Boolean

if (vertical) {
stretchMin += it.combinedMinHeight.toInt()
minSize = it.combinedMinHeight.toInt()
willStretch = it.verticalSizeFlags.isFlagSet(SizeFlag.EXPAND)
} else {
stretchMin += it.combinedMinWidth.toInt()
minSize = it.combinedMinWidth.toInt()
willStretch = it.horizontalSizeFlags.isFlagSet(SizeFlag.EXPAND)
}

if (willStretch) {
stretchAvail += minSize
stretchRatioTotal += it.stretchRatio
if (willStretch) {
stretchAvail += minSize
stretchRatioTotal += it.stretchRatio
}
minSizeCache[it] = MinSizeCache(minSize = minSize, willStretch = willStretch, finalSize = minSize)
}
minSizeCache[it] = MinSizeCache(minSize = minSize, willStretch = willStretch, finalSize = minSize)
childrenCount++
}
}
Expand Down Expand Up @@ -182,47 +184,47 @@ abstract class BoxContainer : Container() {
first = true
var idx = 0

for (i in 0 until childrenCount) {
val child = children[i]

if (child is Control && child.enabled && child.visible && !child.isDestroyed) {
val msc = minSizeCache[child] ?: continue
if (first) {
first = false
} else {
ofs += separation
}
if (childrenCount > 0) {
nodes.forEach { child ->
if (child is Control && child.enabled && child.visible && !child.isDestroyed) {
val msc = minSizeCache[child] ?: return@forEach
if (first) {
first = false
} else {
ofs += separation
}

val from = ofs
var to = ofs + msc.finalSize
val from = ofs
var to = ofs + msc.finalSize

if (msc.willStretch && idx == childrenCount - 1) {
// adjust so the last one always fits perfect
// compensating for numerical imprecision
to = if (vertical) height.toInt() else width.toInt()
}
if (msc.willStretch && idx == childrenCount - 1) {
// adjust so the last one always fits perfect
// compensating for numerical imprecision
to = if (vertical) height.toInt() else width.toInt()
}

val size = to - from
val tx: Float
val ty: Float
val tWidth: Float
val tHeight: Float
val size = to - from
val tx: Float
val ty: Float
val tWidth: Float
val tHeight: Float

if (vertical) {
tx = 0f
ty = from.toFloat()
tWidth = width
tHeight = size.toFloat()
} else {
tx = from.toFloat()
ty = 0f
tWidth = size.toFloat()
tHeight = height
}

if (vertical) {
tx = 0f
ty = from.toFloat()
tWidth = width
tHeight = size.toFloat()
} else {
tx = from.toFloat()
ty = 0f
tWidth = size.toFloat()
tHeight = height
fitChild(child, tx, ty, tWidth, tHeight)
ofs = to
idx++
}

fitChild(child, tx, ty, tWidth, tHeight)
ofs = to
idx++
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ inline fun SceneGraph<*>.centerContainer(callback: @SceneGraphDslMarker CenterCo
open class CenterContainer : Container() {
override fun onSortChildren() {
nodes.forEach {
if (it is Control && it.enabled && it.visible && !it.isDestroyed) {
if (it is Control && it.enabled && !it.isDestroyed) {
val newX = floor((width - it.combinedMinWidth) * 0.5f)
val newY = floor((height - it.combinedMinHeight) * 0.5f)
fitChild(it, newX, newY, it.combinedMinWidth, it.combinedMinHeight)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ open class Container : Control() {
super.onChildAdded(child)
if (child !is Control) return

child.onVisible.connect(this, ::onChildMinimumSizeChanged)
child.onInvisible.connect(this, ::onChildMinimumSizeChanged)
child.onEnabled.connect(this, ::onChildMinimumSizeChanged)
child.onDisabled.connect(this, ::onChildMinimumSizeChanged)
child.onSizeFlagsChanged.connect(this, ::queueSort)
child.onMinimumSizeChanged.connect(this, ::onChildMinimumSizeChanged)

Expand All @@ -74,6 +78,10 @@ open class Container : Control() {
override fun onChildRemoved(child: Node) {
super.onChildRemoved(child)
if (child !is Control) return
child.onVisible.disconnect(this)
child.onInvisible.disconnect(this)
child.onEnabled.disconnect(this)
child.onDisabled.disconnect(this)
child.onSizeFlagsChanged.disconnect(this)
child.onMinimumSizeChanged.disconnect(this)

Expand Down
Loading

0 comments on commit 5812f4a

Please sign in to comment.