Kotlin 276 - Moving Toward System
GitHub Decentralized Repo
GitHub Centralized Repo
GitHub Flat Repo
Let’s start moving our new Timers into a separate table. That should be “easy”.
I figure I’ll just build a top-level TimerTable sort of thing and add Timers into it from addComponent
. I’ll leave them as components in the entity as well, which will keep things running as we transition, then move them out after the table works.
var Score: Int = 0
lateinit var SpaceObjects: Array<SpaceObject>
lateinit var Ship: SpaceObject
lateinit var Saucer: SpaceObject
var TimerTable: List<Timer> = mutableListOf<Timer>()
Then:
fun addComponent(entity: SpaceObject, component: Component) {
entity.components.add(component)
if (component is Timer ) TimerTable += component
}
Should leave me green. Now let’s change this:
private fun updateEverything(
spaceObjects: Array<SpaceObject>,
deltaTime: Double,
width: Int,
height: Int
) {
for (spaceObject in spaceObjects) {
for (component in spaceObject.components) update(component, deltaTime)
if (spaceObject.type == SpaceObjectType.SHIP) applyControls(spaceObject, deltaTime)
move(spaceObject, width, height, deltaTime)
}
}
fun update(component: Component, deltaTime: Double) {
when (component) {
is SaucerTimer -> {
updateSaucerTimer(component, deltaTime)
}
is Timer -> {
with(component) {
if (entity.active == processWhenActive) {
time -= deltaTime
if (time <= 0.0) {
action(this)
time = delayTime
}
}
}
}
}
}
We want not to update the Timer in update
, and to do it separately in updateEverything
. Like this:
private fun updateEverything(
spaceObjects: Array<SpaceObject>,
deltaTime: Double,
width: Int,
height: Int
) {
for (timer in TimerTable) {
with(timer) {
if (entity.active == processWhenActive) {
time -= deltaTime
if (time <= 0.0) {
action(this)
time = delayTime
}
}
}
}
for (spaceObject in spaceObjects) {
for (component in spaceObject.components) update(component, deltaTime)
if (spaceObject.type == SpaceObjectType.SHIP) applyControls(spaceObject, deltaTime)
move(spaceObject, width, height, deltaTime)
}
}
I don’t really like that much nesting, but I think we want that written out in long form for best memory usage? I think tests will break now, because the Timer isn’t being called in update
. Let me see if the game works.
The game works fine. I’m truly sorry but I need to break out a function from that update, so that I can test. And the nesting drives me up the wall.
private fun updateEverything(
spaceObjects: Array<SpaceObject>,
deltaTime: Double,
width: Int,
height: Int
) {
for (timer in TimerTable) {
updateTimer(timer, deltaTime)
}
for (spaceObject in spaceObjects) {
for (component in spaceObject.components) update(component, deltaTime)
if (spaceObject.type == SpaceObjectType.SHIP) applyControls(spaceObject, deltaTime)
move(spaceObject, width, height, deltaTime)
}
}
fun updateTimer(timer: Timer, deltaTime: Double) {
with(timer) {
if (entity.active == processWhenActive) {
time -= deltaTime
if (time <= 0.0) {
action(this)
time = delayTime
}
}
}
}
Now all those calls saying update(timer
can say updateTimer(timer
. Changing those, plus adjusting the test that ensures you only get four missiles at a time results in green.
Commit: Timers are now kept in TimerTable and processed all at once. Step toward TimerSystem.
Now I can remove the addition of the Timer component to the entity, changing this:
fun addComponent(entity: SpaceObject, component: Component) {
entity.components.add(component)
if (component is Timer ) TimerTable += component
}
To this:
fun addComponent(entity: SpaceObject, component: Component) {
if (component is Timer ) TimerTable += component
else entity.components.add(component)
}
Should be green. A test fails that was looking for the timer in the components:
@Test
fun `timer resets on deactivate`() {
val missile = newMissile()
missile.active = true
val timer = missile.components.find { it is Timer }!! as Timer
val originalTime = timer.time
updateTimer(timer, 0.5)
assertThat(timer.time).describedAs("didn't tick down").isEqualTo(originalTime - 0.5, within(0.01))
deactivate(missile)
assertThat(timer.time).describedAs("didn't reset").isEqualTo(originalTime)
}
I can search for that in the TimerTable but I suspect there’s more than one, because I’m never clearing it.
Let’s do this:
@Test
fun `timer resets on deactivate`() {
TimerTable = emptyList()
val missile = newMissile()
missile.active = true
val timer = TimerTable.first()
val originalTime = timer.time
updateTimer(timer, 0.5)
assertThat(timer.time).describedAs("didn't tick down").isEqualTo(originalTime - 0.5, within(0.01))
deactivate(missile)
assertThat(timer.time).describedAs("didn't reset").isEqualTo(originalTime)
}
Got the “didn’t reset message”. That’s an honest error, need to fix deactivate:
fun deactivate(entity: SpaceObject) {
entity.active = false
for (component in entity.components) {
when (component) {
is Timer -> {
component.time = component.delayTime
}
is SaucerTimer -> {
component.time = U.SaucerDelay
}
}
}
if (entity.type == SpaceObjectType.SHIP) shipGoneFor = 0.0
}
We’ll have to fetch its Timer from the TimerTable now. I’m really glad I virtuously wrote that test.
Now of course we could save the components in the object for just this occasion. But let’s not.
fun deactivate(entity: SpaceObject) {
entity.active = false
for (component in entity.components) {
when (component) {
is SaucerTimer -> {
component.time = U.SaucerDelay
}
}
}
for (timer in TimerTable) {
if (timer.entity == entity ) timer.time = timer.delayTime
}
if (entity.type == SpaceObjectType.SHIP) shipGoneFor = 0.0
}
Green. Commit: adjust deactivate to use TimerTable. Remove Timer from component list in SpaceObject.
Let’s sum up.
Summary
We aren’t quite to the point where we can say we have a System … or are we? We have a separate table of Timers, which we process in a batch. (We have broken out the code in the loop, to allow for easier testing, and perhaps a performance fanatic would not do that, but I’d rather have the tests.)
The change from built-in component to separate table went smoothly, never really breaking anything serious, but we did need to adjust some tests. That’s no surprise, since we changed a pretty central design decision about where the Timers live.
I like it. See you next time!