Kotlin 3
I’m inching forward in my learning of Kotlin and TornadoFX. Progress is happening, but so slowly that I can’t write as I usually do.
Most of my programming articles are contemporaneous with the programming. I write what I intend to do, try to do it, describe whatever disaster or occasional success follows. So far, with Kotlin, I’ve not been able to do that very well, if I ever do it well. Let’s say I’ve only been able to do it very much less well than usual. The reason is that I really don’t have a good grasp of Kotlin, TornadoFX, or IDEA, so I am continually fumbling, searching the Internet, or asking for help on the Friday Night Slack. The result is a very few lines of code over a period of two or three hours, with most of the time consumed by me screaming, crying, begging, or cursing. Not good article material, even by my standards.
I am hopeful that in a few more days or weeks, I’ll get my stuff together enough to start writing coherently—again, by my standards. Until then, it’ll be more like occasional single frame shots of a skier experiencing a yard sale, q.v.
Let’s get to it. I’ll try to explain where I think I am.
Current Status
I’m building whatever it is based on a starter project that Hill has produced for us Kotlin beginners, which displays a little something. Right now, my version displays this:
If that were a movie, you’d see the bucket rotating around the center of the diagonal lines. It’s going clockwise, which is suggesting to me that angles increase clockwise, unless of course my code is wrong, which is entirely possible. Let’s explore the code in a bit of detail, so that I can rehearse in my mind what I (think I) have learned.
IDEA is set to run MakerMain when I start the program:
package org.geepawhill.starter
import sun.net.www.protocol.css.Handler
import tornadofx.*
import java.net.URL
class MakerMain : App(MakerView::class)
fun main(args: Array<String>) {
URL.setURLStreamHandlerFactory(Handler.HandlerFactory())
launch<MakerMain>(args)
}
Boilerplate as far as I’m concerned, especially the URL bit. I have no idea what that’s about. The launch, I almost understand. No, now that I think about it, I don’t. I’ll treat it as given; it works and I have no need to change it. It’s no fun working without understanding, but given where I am, it’ll have to do. At a guess, that means to start the App, which is based on MakerView. We’ll look at that, because that’s where all the action is.
It turns out that MakerView is a class. We’ll go through it bit by bit, and I’ll say what I understand about it. If you understand more, please do let me know: I need all the help I can get.
class MakerView : View("Raindrops") {
inner class Timer : AnimationTimer() {
private var lastFrame = System.nanoTime()
public var deltaTime = 0.0
public var totalTime = 0.0
override fun handle(now: Long) {
deltaTime = (now - lastFrame).toDouble() / 1000000.0
totalTime += deltaTime
lastFrame = now
pulse(deltaTime, totalTime)
}
}
Here we declare our view and set its title. Then there’s an inner class, a subclass of AnimationTimer. The handle
function will get executed about 60 times a second, and what it’ll do is call puule
a method in the MakerView class, passing in delta and total time. (The calculation of total time is, let’s say, one way of doing it. I wrote that.)
Now we’ll skip down to root
. Each of these View things needs to define root, and our definition looks like this:
override val root = pane {
minWidth = SCREEN_WIDTH
maxWidth = SCREEN_WIDTH
minHeight = SCREEN_HEIGHT
maxHeight = SCREEN_HEIGHT
background = Background(BackgroundFill(Color.LIGHTBLUE, null, null))
isFocusTraversable = true
addEventHandler(KeyEvent.KEY_PRESSED) { event: KeyEvent ->
input.handle(event)
}
addEventHandler(MouseEvent.MOUSE_CLICKED) { _ ->
requestFocus()
}
this += bucket
this += littleBucket
//this += markOne
this += diag
this += otherDiag
//this += markTwo
}
This is override
because root
is defined in the superclass and our job is to provide it. All the things we assign to are variables in View.
The first four are fairly obvious and the constants are defined in our code. Maybe I’ll show that later but they are just a bunch of Doubles. (Why not integers? Some Java reason about screen virtualizing, I don’t know.)
Background is light blue, you’ve seen that. I suppose I could look up isFocusTraversable
but I honestly have no desire to understand that yet. Someday I may regret that decision, or reverse it, or both.
The two event handlers set us up to receive key presses in our input handler, and arrange that we have focus when the window is clicked, because if we don’t do that, we won’t receive the key presses.
Then we set up our display, which is done by these lines:
this += bucket
this += littleBucket
this += diag
this += otherDiag
The this +=
is the TornadoFX way of adding an element to the current view, pane, or whatever your class is. It is a convenience: in JavaFX you’d be saying addSomething
, I guess. More important to us is to understand what those things are. Here they are:
val diag = Line(0.0,0.0, SCREEN_WIDTH, SCREEN_HEIGHT)
val otherDiag = Line(0.0, SCREEN_HEIGHT, SCREEN_WIDTH, 0.0)
val bucket = imageview {
x = SCREEN_HALF - BUCKET_HALF
y = SCREEN_HEIGHT - BUCKET_HEIGHT
image = Image("/bucket.png")
}
var dir = 1
val littleBucket = imageview {
x = 0.0
y = 0.0
scaleX = 0.5
scaleY = 0.5
image = Image("/bucket.png")
}
Starting at the top, diag
and otherDiag
are, well, lines, one from top left to bottom right and the other from top right to bottom left. I put those in for two reasons, first, to learn how to draw a line, and second, to identify the center of the pane so that I could better judge whether things were doing as I hoped. This was necessitated, in part, by the fact that images are drawn at their top left corner, not their center, so I needed a bunch of fiddly adjustments.
Next we have the bucket, which is the large one you saw in the picture. We just say where it is to go. There’s some of the fiddly stuff.
The bucket code was provided by Hill as part of the starter app, which is going to collect raindrops. We’ve forked that and we’re going in a different direction entirely.
I added the little bucket, and were it not for some code that follows, it would start out at the top left of the screen, I think, like this:
That’s really all we’re displaying right now. There are a couple of other display objects left over in the code, left over from Hill’s starter. We’ll ignore those, since we’re not using them.
Now let’s see why the bucket rotates. (I’ll try to do a movie to show you but I haven’t ever recorded the Mac screen so I’m not sure about that …)
Recall that the timer was supposed to call pulse
, a method in our View subclass that looks like this:
private fun pulse(deltaTime: Double, totalTime: Double) {
input.popTo(deltaTime, this::handleKey)
moveLittleBucket(totalTime)
}
We’ll ignore the input, because we don’t care what the keys do … yet … and we see that we call moveLittleBucket
:
private fun moveLittleBucket(totalTime: Double) {
val theta = totalTime / 1000.0
val centerX = SCREEN_WIDTH / 2 - BUCKET_WIDTH / 2
val centerY = SCREEN_HEIGHT / 2- BUCKET_HEIGHT / 2
val r = 100.0
val x2 = r * cos(theta)
val y2 = r * sin(theta)
littleBucket.x = x2 + centerX
littleBucket.y = y2 + centerY
}
Not that much to see here, it’s just that we compute the “center” position for the bucket (adjusted by the half-wirdh / half-height because of the upper left thing), then we (try) to compute the x and y location at a radius of 100 from the center. Standard r sine theta cosine theta stuff. Then we set the bucket’s x and y accordingly.
And … it moves.
For today’s implementation, I want to do just one thing. I want the bucket itself to rotate so as to always stay perpendicular to the line from center to bucket. After literally hours of research yesterday, I’ve found that this will work:
private fun moveLittleBucket(totalTime: Double) {
val theta = totalTime / 1000.0
val centerX = SCREEN_WIDTH / 2 - BUCKET_WIDTH / 2
val centerY = SCREEN_HEIGHT / 2- BUCKET_HEIGHT / 2
val r = 100.0
val x2 = r * cos(theta)
val y2 = r * sin(theta)
littleBucket.x = x2 + centerX
littleBucket.y = y2 + centerY
littleBucket.rotate = Math.toDegrees(theta)
}
We have to convert to degrees here, because reasons: it has to be in degrees, but our angle theta, going into sine and cosine, is treated as radians. Thanks, Java, and most every other graphical framework on the planet.
But now it does as I intended:
OK, let’s sum up this report.
Summary
This report brings my articles up to sync with the code, though the code was mostly done yesterday over a period of hours spent complaining and occasionally typing. It should be clear that I am slowly, every so slowly, getting familiar with Kotlin and TornadoFX.
I think it likely that we will switch from TornadoFX to a game library or framework of some kind. That’ll be fine with me, it’ll be similar but different.
Looking at this code, I now have a flying bucket that orbits the center of the screen. Reminds me of Spacewar! and Asteroids. My next effort, probably not today, will be to arrange for the bucket to actually accelerate and decelerate, with a sun there at the center, with gravity.
My current thinking is that I’ll do the gravity first, and start the bucket at a random position near the top of the screen, probably with a small starting velocity, and let gravity do its work, curving the bucket’s path. Then Ill move from there to turning it and accelerating with the keyboard, because the keyboard is actually available in the sample Hill gave us.
I believe that what I should do, and I’m not sure just how to do it, is that I should create a Ship class (or a Bucket class, or a BucketShip class) and use it to calculate position and angle based on velocity. Essentially we’ll be extracting a class from the littleBucket
code and related variables.
I’m not totally sure yet how to do that with these tools, but it should be pretty much the same as every other toolset, except all new menus, keystrokes and words. I’m sure I’ll sort it out, because I know what I want, even if I don’t know yet how to say it in Kotlin.
All that’s for another day. For nows, you’re current with my coding status. And you can perhaps kind of see how a very experienced developer begins to learn a new system, by making it do things.
- In Another World
- In another world, I have had the chance to observe people new to programming, trying to learn the scripting language of that world. I’ve noticed that they start with an existing program that does something similar to what they want, or just any random starting program. Then they modify it a little bit, changing “Hello World” to “Hello Ron”. After a while they learn some new incantation and add it in. Slowly, ever so slowly, they begin to understand how things work. Sometimes they even get good at it.
-
I have an advantage, I argue, in that I know what program structures want to be, so that even when I don’t know the language, I have a decent sense of what the shape of the thing should be. (This sense is more or less decent, depending on how close the language at hand is to languages I have already learned (and often forgotten)).
-
I think the pattern is always the same: start from a sample and slowly learn to make it do what you want, and slowly learn how to make it “better” in the sense of good code for that language.
-
Another possibility is that I’m the only person who experiences that, but I don’t think so.
Stop back next time and watch me fumble, perhaps a bit more adroitly.