Kotlin 122
What shall we do today, Brain? Let’s see if we can get this baby under some semblance of control. Spoiler: It’s ALIVE!!!
I begin by frightening myself. I upgraded to MacOS Ventura whatever.zero, on day zero, and all went well. This morning, when I tabbed to IDEA, no project was visible. I clicked what I thought was this project in recent … and all my code was gone! Oh, no, that message I got from Git must have done something terrible.
Fortunately, the project window was just hiding. Whew. I’d hate to have to chase down a problem of that kind. Now, as I was saying …
The ship, any Flyer really, has an instance of Controls. Such a thing has member Booleans accelerate
, left
, right
, fire
, and when a Flyer calls control
, this happens:
class Controls
fun control(obj:Flyer, deltaTime: Double): List<Flyer> {
turn(obj,deltaTime)
accelerate(obj,deltaTime)
return fire(obj)
}
The inner methods check the flags and if they’re true, send various messages to the ship, which they have received as a parameter, which accelerate, or turn the receiver. And if fire
is properly toggled, a missile type of Flyer is created and returned to be added to the game’s list of Flyers.
Therefore, in theory, if I knew how to wire the keyboard into a Kotlin / OPENRNDR program, I could detect the keyboard changes, set the flags, and the ship should begin to operate. Unfortunately, as is so often the case, practice and theory are not in alignment. I don’t know how to access the keyboard, and I don’t really know quite what I’d do if I could. I think there is an example on the OPENRNDR site. Come along with me while we do some research1.
Here, conveniently, we find a short description of mouse and keyboard event handling, as is provided in OPENRNDR.
For our purposes, I think we want to listen to keyDown
and keyUp
events. We need four keys, meaning left, right, accelerate, fire. On the version of Asteroids and Spacewar that I have played, I am used to using my left middle finger for left, left index for right, right index for accelerate, right middle for fire. We could use any keys for that purpose. I think, just because Sister Mary Invicta taught me to keep my fingers on the home keys, we’ll try d=left, f=right, j=accelerate, k=fire.
From the examples, it looks like we just put a keyboard listen into the program
part:
fun main() = application {
program {
keyboard.keyDown.listen {
// -- it refers to a KeyEvent instance here
// -- compare the key value to a predefined key constant
if (it.key == KEY_ARROW_LEFT) {}
}
}
}
I think I’ll start with some prints, just to get a feel for what’s up.
I put this into the game’s main:
program {
val image = loadImage("data/images/pm5544.png")
val font = loadFont("data/fonts/default.otf", 64.0)
val game = Game().also { it.createContents() }
keyboard.keyDown.listen {
println("Key ${it.name} down")
}
keyboard.keyUp.listen {
println("Key ${it.name} up")
}
extend {
val worldScale = width/10000.0
drawer.scale(worldScale, worldScale)
game.cycle(drawer,seconds)
}
Run it, see what it does. Oh, nice, it works as advertised:
Key a down
Key a up
Key d down
Key d up
Key f down
Key f up
Key j down
Key k down
Key k up
Key k down
Key k up
Key k down
Key k up
Key j up
OK then. Presumably we “just” need to create a controls, pass it in to the game to be given to the ship, and then set its Booleans. Here goes:
program {
val image = loadImage("data/images/pm5544.png")
val font = loadFont("data/fonts/default.otf", 64.0)
val controls = Controls()
val game = Game().also { it.createContents(controls) }
...
fun createContents(controls: Controls) {
val ship = newShip(controls)
add(ship)
add(ShipMonitor(ship))
for (i in 0..7) {
val pos = Vector2(random(0.0, 10000.0), random(0.0,10000.0))
val vel = Vector2(1000.0, 0.0).rotate(random(0.0,360.0))
val asteroid = Flyer.asteroid(pos,vel )
add(asteroid)
}
}
private fun newShip(controls: Controls): Flyer {
return Flyer.ship(Vector2(5000.0, 5000.0), controls)
}
Pass it on in. Now, I think that the ship has a Controls instance that the program
has a grip on. It remains to do the events:
keyboard.keyDown.listen {
when (it.name) {
"d" -> {controls.left = true}
"f" -> {controls.right = true}
"j" -> {controls.accelerate = true}
"k" -> {controls.fire = true}
}
}
keyboard.keyUp.listen {
when (it.name) {
"d" -> {controls.left = false}
"f" -> {controls.right = false}
"j" -> {controls.accelerate = false}
"k" -> {controls.fire = false}
}
}
This ought to “just work” and it almost does, except that if it is firing a missile, I’m not seeing it. I’ll share a video when it all works. I note that the acceleration is dead slow. That’s a constant in the Controls, so I can tweak it. But what is up with firing? Let’s look more closely at the code:
private fun fire(obj: Flyer): List<Flyer> {
// too tricky? deponent denieth accusation.
val result: MutableList<Flyer> = mutableListOf()
if (fire && !holdFire ) result.add(createMissile(obj))
holdFire = fire
return result
}
private fun createMissile(obj: Flyer): Flyer {
val missileKillRadius = 10.0
val missileOwnVelocity = Vector2(SPEED_OF_LIGHT / 100.0, 0.0)
val missilePos = obj.position + Vector2(2.0 * missileKillRadius, 0.0).rotate(obj.heading)
val missileVel = obj.velocity + missileOwnVelocity
return Flyer(missilePos, missileVel, missileKillRadius, 0, false, ShipView(),)
}
Hm why does it have that velocity, and why does its view say ShipView()
. The latter, presumably, because I didn’t want to draw the missile yet.
I’m going to put a print in here just to make sure it’s working. Then I’ll look carefully for a ship that I wasn’t expecting.
But isn’t that SPEED_OF_LIGHT/100 questionable? I don’t know. One worry at a time.
OK, my prints say it is firing. I notice that turning is going the opposite of what I expect. Fix controls, reversing the signs:
private fun turn(obj: Flyer, deltaTime: Double) {
if (left) obj.turnBy(-rotationSpeed*deltaTime)
if (right) obj.turnBy(rotationSpeed*deltaTime)
}
As for the firing, I was really sure that we had testing adding a missile. Yes, we do. I want some more reassurance.
Some Confusion
After a bit of printing, I found that though the Controls do try to return the missile to be added, no one was paying attention. I’m not sure why it works in the tests and not here, but I think that’s not relevant, because … when I modified the code to add the newbies, I get an error “(ConcurrentModificationException)”, which tells me that I can’t go adding things into my flyers while I’m looping over them.
I can resolve this in a couple of ways. One would be to copy the flyers before iterating. That’s not efficient, but it might be a quick sanity check.
Ah. The copy trick gets past the exception:
fun copyFlyers(): MutableList<IFlyer> {
val result = mutableListOf<IFlyer>()
result.addAll(flyers)
return result
}
fun forEach(f: (IFlyer)->Unit) = copyFlyers().forEach(f)
That gives me enough breathing room to do some printing and become sure that I am creating the missiles, and on the next iteration, they’re gone. That finally triggers the notion in my mind that they might be materializing too close to the ship and therefore mutually destroying it … and my ShipMonitor immediately replaces the ship, hence it appears that no missile has been fired.
So I materialize the missile well beyond the ship and voila! we get a second ship-like thing, creeping across the screen. The ship on the right is the missile.
OK. That was a long digression. There were these issues at least:
- The missile was not being added to flyers during game play. Still not sure why the tests didn’t find that.
- Adding the missile to the flyers resulted in a concurrency issue, since we were iterating the list , on the game interrupt, at the same time we added to it.
- The missile was materializing so close to the ship that it was destroying it.
- The ShipMonitor immediately replaced the ship, so that I didn’t notice it was destroyed.
It’s rare that there are even two simultaneous problems in my code, so that it took me a while to drill down to all of them.
One thing I’ve noticed is that the missile velocity does not take the ship’s heading into account, another thing that should have been tested. And I need to tweak the speed of the missiles, and other values, such as ship acceleration and turning rate. The missile code now:
private fun fire(obj: Flyer): List<Flyer> {
// too tricky? deponent denieth accusation.
val result: MutableList<Flyer> = mutableListOf()
if (fire && !holdFire ) result.add(createMissile(obj))
holdFire = fire
return result
}
private fun createMissile(obj: Flyer): Flyer {
val missileKillRadius = 10.0
val missileOwnVelocity = Vector2(SPEED_OF_LIGHT / 3.0, 0.0).rotate(obj.heading)
val missilePos = obj.position + Vector2(obj.killRadius + 2 * missileKillRadius, 0.0).rotate(obj.heading)
val missileVel = obj.velocity + missileOwnVelocity
return Flyer(missilePos, missileVel, missileKillRadius, 0, false, MissileView())
}
class MissileView: FlyerView {
override fun draw(missile: Flyer, drawer: Drawer) {
drawer.stroke = ColorRGBa.WHITE
drawer.fill = ColorRGBa.WHITE
drawer.scale(3.0,3.0)
drawer.circle(Vector2.ZERO, missile.killRadius)
}
}
And there are some tweaked numbers also:
val acceleration = Vector2(1000.0, 0.0)
val rotationSpeed = 180.0
Those are in Controls
. With all this in place, the game actually runs, and you can shoot the asteroids:
So, that’s good. And in the middle there, I had to run an errand, so let’s sum up until I have time to refresh my brain. What has happened?
Summary
We were able to wire up the controls quite quickly:
val controls = Controls()
val game = Game().also { it.createContents(controls) }
keyboard.keyDown.listen {
when (it.name) {
"d" -> {controls.left = true}
"f" -> {controls.right = true}
"j" -> {controls.accelerate = true}
"k" -> {controls.fire = true}
}
}
keyboard.keyUp.listen {
when (it.name) {
"d" -> {controls.left = false}
"f" -> {controls.right = false}
"j" -> {controls.accelerate = false}
"k" -> {controls.fire = false}
}
}
There were some wiring problems between the controls and the game, notably that the game wasn’t paying attention to the objects coming back from the controls: the idea is that the controls can (and do) spawn new objects into the game, and the game needs to add them into the Flyers.
That led to a confusing error message objecting to the fact that we were trying to add to a collection while it was being iterated. The objection may or may not have related to the thread we were on: I think not, I think it is probably just not allowed. Anyway, that led me on a chase which I mostly spred you, until I figured out that the issue was the list being iterated.
I — I’d say we, but you don’t want the blame for this — “fixed” the problem by copying the Flyers list before iterating it. I modify that, removing the copy and changing my adding code:
fun update(deltaTime: Double) {
val adds = mutableListOf<IFlyer>()
flyers.forEach {
adds.addAll(it.update(deltaTime))
}
flyers.addAll(adds)
}
Now we’re not adding new things until we’re done with forEach
that does the updates.
Other than a few tweaks of numbers, that actually went rather smoothly. I’ve made a note to improve the missile testing, since I didn’t discover that no one was paying attention to new objects coming back from update.
I freely grant that I was confused for longer than I’d like to be, perhaps as much as a half hour. But let’s also note that once wired up, the game nearly worked, and did work once I got the missile to stop materializing too close to the ship.
Certainly there were some problems, but TDDing the game without looking at the video has worked out rather well. I think I’ll continue along those lines.
Let me emphasize that: Historically, I’ve felt that TDD didn’t apply well to programs that were mostly visual, like Asteroids. However, most of this program has been written with TDD and without looking at a screen rendering of the game’s look and feel. A couple of days ago I got to check how big the asteroids and ships were. Almost everything but the “does it look right” has been done with TDD … and I like the result. I will keep working that way in future.
Features left to be implemented, tuning to be done. But it’s alive, and I am happy.
It has been ten or twelve days, so it’s not even a normal human’s working week since we started, and the game runs. I call that good!2
See you next time!
-
Seriously, couldn’t I have done the research already? Sure, but I want to make it clear to my readers that we all have the need, right in the middle of things, to look things up and learn enough to take the next step. I’m not faking ignorance here. I am ignorant, of many things. (And of many other things, I do have a decent clue.) ↩
-
Yes, probably we could have had a little circle flying among larger circles in a couple of days if we already knew OPENRNDR and didn’t mind what a mess we made. We, on the other hand, have a rather nicely designed game. I’m here for sustainable pace aided by decent design along the way. YMMV. So there, speed demons! ↩