Kotlin 257 - Performance?
GitHub Decentralized Repo
GitHub Centralized Repo
GitHub Flat Repo
ECS is supposed to be more performant because of memory locality. Does memory locality matter on my computer? Let’s try a simple way to find out. Also: We kill some asteroids.
I write these tests as my first attempt:
var size = 5000
var numbers = Array<Int>(size) { it }
val iterations = 1000
class PerformanceTests {
@BeforeEach
fun `set up array`() {
numbers = Array<Int>(size) { it }
}
@Test
fun `local access`() {
val second = 1
val action = {
for (i in 0..iterations) {
numbers[0] = numbers[second] + 1
numbers[second] = numbers[0] + 1
}
}
val nano = measureNanoTime(action)
println("local $nano")
}
@Test
fun `distant access`() {
val second = size - 1
val action = {
for (i in 0..iterations) {
numbers[0] = numbers[second] + 1
numbers[second] = numbers[0] + 1
}
}
val nano = measureNanoTime(action)
println("distant $nano")
}
}
In the local one, I mess about with positions 0 and 1 in the array. In the distant one, I do the same operations on positions 0 and size-1, 4999.
Curiously, the distant answer is almost always fastest:
local 300875
distant 281167
Needless to say, this surprises me. There is some variability, and once I saw a much larger number, but generally the local is a bit slower than the distant.
The trick with measuring hardware performance is to be sure you haven’t just written notably slower code in one case than the other.
I’ll have to think about this, to see if I can come up with another test that has a better chance of showing a memory caching effect. Nothing creative comes to mind. I’ll do another test if I have, or am given, another simple idea.
I did do a different kind of test yesterday. I created 1000 asteroids and ran the game with all of them on screen. Response time um deteriorated substantially, with what looked to be a frame rate of about 2 frames per second. That said, the game could run 100 asteroids with what seemed to be no trouble at all.
Features
We need to get closer to feature parity here, so as to have a sense of how this design compares with our previous two. Let’s start with a simple couple of things, asteroid size and stroke width. Right now our asteroids are small and the lines on the screen are thick:
Just to warm up, let’s sort out the line thickness.
fun draw(
spaceObject: SpaceObject,
drawer: Drawer,
) {
drawer.isolated {
drawer.translate(spaceObject.x, spaceObject.y)
drawer.scale(4.00, 4.0)
drawer.rotate(spaceObject.angle)
drawer.stroke = ColorRGBa.WHITE
drawer.strokeWeight = 0.25
drawer.lineStrip(spaceObject.type.points)
}
}
I tried strokeWeight
of 1.0 and it looked the same, and since scale
is 4, I tried 0.25 and it looks good. I’ll show you in a moment, after fixing up the asteroid scale or giving up.
Asteroids come in three sizes, 1, 2, and 4, doubling each time. (Or, in operation, halving as they split.) SpaceObjects, as of today, are defined like this:
data class SpaceObject(
val type: SpaceObjectType,
var x: Double,
var y: Double,
var dx: Double,
var dy: Double,
var angle: Double = 0.0,
var active: Boolean = true,
) {
var components: MutableList<Component> = mutableListOf()
}
Let’s see if we can put scale
inside, not as a creation parameter.
data class SpaceObject(
...
) {
var scale = 1.0
var components: MutableList<Component> = mutableListOf()
}
And let’s define asteroids at scale 4.
private fun newAsteroid(): SpaceObject
= SpaceObject(SpaceObjectType.ASTEROID, 0.0, 0.0, 0.0, 0.0, 0.0, false)
.also { it.scale = 4.0}
And in draw, apply the internal scale:
fun draw(
spaceObject: SpaceObject,
drawer: Drawer,
) {
drawer.isolated {
drawer.translate(spaceObject.x, spaceObject.y)
drawer.scale(4.00, 4.0)
drawer.scale(spaceObject.scale, spaceObject.scale)
drawer.rotate(spaceObject.angle)
drawer.stroke = ColorRGBa.WHITE
drawer.strokeWeight = 0.25
drawer.lineStrip(spaceObject.type.points)
}
}
And try it. I expect the lines will be too thick now, and they are:
OK, divide the object scale back out:
fun draw(
spaceObject: SpaceObject,
drawer: Drawer,
) {
drawer.isolated {
val scale = 4.0 *spaceObject.scale
drawer.translate(spaceObject.x, spaceObject.y)
drawer.scale(scale, scale)
drawer.rotate(spaceObject.angle)
drawer.stroke = ColorRGBa.WHITE
drawer.strokeWeight = 1.0/scale
drawer.lineStrip(spaceObject.type.points)
}
}
That does the trick:
We’ll probably need to adjust overall scale, but 4.0 seems good enough to me for now.
OK, Now What?
Right, that wasn’t what you’d call amazing progress. What should we do now?
If missiles could kill asteroids, we’d have a game. We also have some weird stuff going on with missiles. I think that some of them are reserved for the saucer and that we’re using plain vanilla integer magic values to do it:
fun withAvailableMissile(action: (SpaceObject) -> Unit) {
for ( i in 2..5) {
if (!spaceObjects[i].active) return action(spaceObjects[i])
}
}
Right. SpaceObjects begins with 6 missiles and we skip two of them. This code knows that we have six and that two of them are for the saucer. This is really not good, but it’s early days and we can sort it later. Let’s do a fairly naive collision function. The rules will be that missiles can only collide with asteroids, for now.
I’m going to do this in a very crummy ad-hoc way, for one simple reason: I don’t see yet how I want it to work with this fixed-array setup. So we’ll spike it. However … I’m troubled by the fact that I don’t have a better picture of things. We’ll reflect on that later.
After we move everyone, let’s do collisions:
fun gameCycle ...
) {
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)
}
for (spaceObject in spaceObjects) {
if (spaceObject.active) draw(spaceObject, drawer)
}
checkCollisions()
}
And a bit of code:
private fun checkCollisions() {
val firstMissile = 0
val lastMissile = 5
val firstAsteroid = 7
val lastAsteroid = spaceObjects.size-1
for (m in firstMissile..lastMissile) {
val missile = spaceObjects[m]
if ( missile.active) {
val missilePos = Vector2(missile.x, missile.y)
for (a in firstAsteroid..lastAsteroid) {
val asteroid = spaceObjects[a]
if (asteroid.active) {
val asteroidPos = Vector2(asteroid.x, asteroid.y)
val killDist = 16.0*asteroid.scale + 1
val dist = missilePos.distanceTo(asteroidPos)
if ( dist <= killDist ) {
asteroid.active = false
missile.active = false
}
}
}
}
}
}
So that’s about ugly as as anything I’ve done so far this week, but it is also nearly good. (It’s not perfect, because I think one missile could kill two overlapping asteroids as it stands.) However it does work, in the sense that when a missile hits an asteroid, both vanish.
There’s a bug. If you’ll watch closely, right before I shoot the final asteroid in the movie above, I fire a missile that quickly dies. That shouldn’t happen. It makes me think that there’s an invisible asteroid in the way and that we killed it again. I don’t think that should be possible, looking at the code. It seems to me that the code only checks active missiles and active asteroids.
A quick print convinces me that it’s not the new code doing it. Then what is? How about the timer?
private fun updateTimer(timer: Timer, deltaTime: Double) {
with(timer) {
if ( ! entity.active) return
time -= deltaTime
if (time <= 0.0) {
entity.active = false
time = timer.startTime
}
}
}
fun fireMissile() {
Controls.fire = false
withAvailableMissile { missile ->
setPosition(missile, Vector2(U.MissileOffset, 0.0).rotate(Ship.angle))
setVelocity(missile, Vector2(U.MissileSpeed, 0.0).rotate(Ship.angle))
missile.active = true
}
}
We’re relying on the timer being reset when the missile times out: we’re not initializing it when we fire. It seems possible that we could wind up with the timer holding a smaller than usual value. Let’s have a new function, deactivate
that we use to deactivate space objects.
fun deactivate(entity: SpaceObject) {
entity.active = false
for (component in entity.components) {
when (component) {
is Timer -> {
component.time = component.startTime
}
}
}
}
Now to fix all the deactivation folx to call that.
private fun checkCollisions() {
val firstMissile = 0
val lastMissile = 5
val firstAsteroid = 7
val lastAsteroid = spaceObjects.size-1
for (m in firstMissile..lastMissile) {
val missile = spaceObjects[m]
if ( missile.active) {
val missilePos = Vector2(missile.x, missile.y)
for (a in firstAsteroid..lastAsteroid) {
val asteroid = spaceObjects[a]
if (asteroid.active) {
val asteroidPos = Vector2(asteroid.x, asteroid.y)
val killDist = 16.0*asteroid.scale + 1
val dist = missilePos.distanceTo(asteroidPos)
if ( dist <= killDist ) {
deactivate(asteroid)
deactivate(missile)
}
}
}
}
}
}
private fun deactivateAsteroids() {
spaceObjects.filter { it.type == SpaceObjectType.ASTEROID }.forEach { deactivate(it) }
}
private fun updateTimer(timer: Timer, deltaTime: Double) {
with(timer) {
if ( ! entity.active) return
time -= deltaTime
if (time <= 0.0) {
deactivate(entity)
time = timer.startTime
}
}
}
That last one duplicates the timer setting but we’ll let that ride. That should fix the bug. I’m a bit irritated that I haven’t a test for the case but other than a direct check on the timer, I don’t quite see how to do it. Let’s get this mess committed and then assess how badly we’ve performed. Commit: spiked version allows missile to kill asteroid.
Summary
I’m going to wrap this up right about here, unless in summing up I make some changes. What have we wrought? Curiously, I think we have rudimentary missile-asteroid interaction in place, and it’s just about right: it even handles smaller asteroids than the big ones we’re running now.
However, the code is pretty nasty. Let’s look at it again. You might want to put on goggles for this:
private fun checkCollisions() {
val firstMissile = 0
val lastMissile = 5
val firstAsteroid = 7
val lastAsteroid = spaceObjects.size-1
for (m in firstMissile..lastMissile) {
val missile = spaceObjects[m]
if ( missile.active) {
val missilePos = Vector2(missile.x, missile.y)
for (a in firstAsteroid..lastAsteroid) {
val asteroid = spaceObjects[a]
if (asteroid.active) {
val asteroidPos = Vector2(asteroid.x, asteroid.y)
val killDist = 16.0*asteroid.scale + 1
val dist = missilePos.distanceTo(asteroidPos)
if ( dist <= killDist ) {
deactivate(asteroid)
deactivate(missile)
}
}
}
}
}
}
Those six right braces at the end speak volumes. This is not nice code. It’s iterating by index, and using magic numbers 0, 5, and 7 to boot. Then it checks the thing it fetches to see if it is active and then, finally, checks distance and decides what to do.
The good news, perhaps, is that it makes most of the values it uses somewhat explicit, so we’ll be in a position to do better.
Let’s try something.
private fun checkCollisions() {
val firstMissile = 0
val lastMissile = 5
val firstAsteroid = 7
val lastAsteroid = spaceObjects.size-1
for (missile in spaceObjects.slice(firstMissile..lastMissile)) {
if ( missile.active) {
val missilePos = Vector2(missile.x, missile.y)
for (a in firstAsteroid..lastAsteroid) {
val asteroid = spaceObjects[a]
if (asteroid.active) {
val asteroidPos = Vector2(asteroid.x, asteroid.y)
val killDist = 16.0*asteroid.scale + 1
val dist = missilePos.distanceTo(asteroidPos)
if ( dist <= killDist ) {
deactivate(asteroid)
deactivate(missile)
}
}
}
}
}
}
We slice the space objects to get just the missiles we might care about. That avoids one bit of subscripting. Now let’s filter.
private fun checkCollisions() {
val firstMissile = 0
val lastMissile = 5
val firstAsteroid = 7
val lastAsteroid = spaceObjects.size - 1
for (missile in spaceObjects.slice(firstMissile..lastMissile)
.filter { it.active }) {
val missilePos = Vector2(missile.x, missile.y)
for (a in firstAsteroid..lastAsteroid) {
val asteroid = spaceObjects[a]
if (asteroid.active) {
val asteroidPos = Vector2(asteroid.x, asteroid.y)
val killDist = 16.0 * asteroid.scale + 1
val dist = missilePos.distanceTo(asteroidPos)
if (dist <= killDist) {
deactivate(asteroid)
deactivate(missile)
}
}
}
}
}
Somewhat better. Do again:
private fun checkCollisions() {
val firstMissile = 0
val lastMissile = 5
val firstAsteroid = 7
val lastAsteroid = spaceObjects.size - 1
for (missile in spaceObjects.slice(firstMissile..lastMissile)
.filter { it.active }) {
val missilePos = Vector2(missile.x, missile.y)
for (asteroid in spaceObjects.slice(firstAsteroid..lastAsteroid)
.filter { it.active }) {
val asteroidPos = Vector2(asteroid.x, asteroid.y)
val killDist = 16.0 * asteroid.scale + 1
val dist = missilePos.distanceTo(asteroidPos)
if (dist <= killDist) {
deactivate(asteroid)
deactivate(missile)
}
}
}
}
That’s surely better than we had previously. If we were doing methods, I’d put methods like activeMissiles
on SpaceObject. We could still have the functions, I suppose, but we’ll leave that for another day.
One important issue that I see now is that there was and is no test for this collision logic, and it is surely the most complicated logic in the whole game, so we sorely need tests. Now that we have a sketch of how to do it, my limited cognitive ability might be able to come up with something for next time.
Beyond that, magic numbers, and a very sore lack of methods. We’re pretending not to have them, however, so there you go. Otherwise we’d have getPosition
on SpaceObject, and so on. In due time we’ll at least convert our x and y to a vector, I should think.
I think the biggest issue is in my brain. This design doesn’t offer me places to hang things. When I have objects, I can break up the function of the program into objects that make sense and partition the logic down. And the objects have functions attached, and when a new function needs to exist, it’s pretty easy to decide which object should implement it.
Here, I have fewer handles, I just have top level functions, broken out into a few files. Some of the functions can be private, just becuase of whatever file they are in, but some can’t even be private. Without one of my main organizational tools, I find that I have to proceed in a more tentative fashion. That’s interesting. I wonder what other handles I could have here … and I wonder what other handles I rely on that I’m not aware of.
Bottom line, I’ve written some legacy code today, but it’s not that awful and it’s a real start toward dealing with collisions. It’s easy to see where the splitting logic should go, and it might just be that it’ll be easy to put in.
We’ll find out soon, but not today. The evil of today is sufficient thereto.
See you next time!
Wait …
Let’s extract some functions just to see if we like what we get.
private fun checkCollisions() {
val firstMissile = 0
val lastMissile = 5
val firstAsteroid = 7
val lastAsteroid = spaceObjects.size - 1
checkAllMissiles(firstMissile, lastMissile, firstAsteroid, lastAsteroid)
}
private fun checkAllMissiles(
firstMissile: Int,
lastMissile: Int,
firstAsteroid: Int,
lastAsteroid: Int
) {
for (missile in spaceObjects.slice(firstMissile..lastMissile)
.filter { it.active }) {
val missilePos = Vector2(missile.x, missile.y)
checkAllAsteroids(firstAsteroid, lastAsteroid, missilePos, missile)
}
}
private fun checkAllAsteroids(
firstAsteroid: Int,
lastAsteroid: Int,
missilePos: Vector2,
missile: SpaceObject
) {
for (asteroid in spaceObjects.slice(firstAsteroid..lastAsteroid)
.filter { it.active }) {
checkOneAsteroid(asteroid, missilePos, missile)
}
}
private fun checkOneAsteroid(
asteroid: SpaceObject,
missilePos: Vector2,
missile: SpaceObject
) {
val asteroidPos = Vector2(asteroid.x, asteroid.y)
val killDist = 16.0 * asteroid.scale + 1
val dist = missilePos.distanceTo(asteroidPos)
if (dist <= killDist) {
deactivate(asteroid)
deactivate(missile)
}
}
Better? Worse? Maybe better. Less text nesting anyway, but of course the execution nesting is the same as it ever was.
If you have a strong preference, let me know! Or any other random idea.