GitHub Repo

The Kotlin game appears to me to have a different scale factor between asteroids and ship. I don’t see yet what’s going on. I discover that I’ve done a very poor job. Don’t read this, it embarrasses me.

This morning I made two pictures of the objects, once on the iPad from my Codea Lua version of the game, and one from the Kotlin version. They are not the same, and I think it’s the Lua one that’s right

Lua

Lua ship notably smaller than smallest asteroid

Kotlin

kotlin ship notably larger than smallest asteroid

Observations

It’s easy to see that the kotlin ship is larger than the small asteroid, and smaller in the Lua version. Based on my reading of the original assembly code, which i can nearly understand, the ship as displayed should be smaller than the smallest asteroid. I have been building up some detailed notes, including this picture:

detailed drawing of ship and asteroid

Is the ship too large or the asteroid not small enough? It’s more than a little hard to say.

In the Lua program, the asteroids are drawn at scale 4, 8 and 16, and the ship is drawn at scale 2.

Those numbers plus the details of the drawings make the smallest asteroid 32 pixels across and the ship, 28 counting the flare, which is not shown in the picture. The Lua values in draw start at scale 1, i.e. pixels.

What’s going on in the Kotlin version?

The Kotlin program draws each object isolated, and does not set scale beforehand:

    fun cycle(elapsedSeconds: Double, drawer: Drawer? = null) {
        val deltaTime = elapsedSeconds - lastTime
        lastTime = elapsedSeconds
        tick(deltaTime)
        beginInteractions()
        processInteractions()
        finishInteractions()
        drawer?.let {draw(drawer)}
    }

    private fun draw(drawer: Drawer)
        = knownObjects.forEach {drawer.isolated { it.subscriptions.draw(drawer) } }

Therefore each object starts its drawing at scale 1.

Ship sets the overall scale to 30:

    fun draw(drawer: Drawer) {
        drawer.translate(position)
//        drawKillRadius(drawer)
        drawer.scale(30.0, 30.0)
        drawer.strokeWeight = strokeWeight
        drawer.scale(dropScale, dropScale)
        drawer.rotate(heading )
        drawer.stroke = ColorRGBa.WHITE
        drawer.lineStrip(points)
        if ( accelerating ) {
            displayAcceleration = (displayAcceleration + 1)%3
            if ( displayAcceleration == 0 ) {
                drawer.strokeWeight = 2.0*strokeWeight
                drawer.lineStrip(flare)
            }
        }
    }

It does that because our universe size is 10,000 x 10,000, so the ship is too small to see. In AsteroidView, we do similarly:

    fun draw(asteroid: Asteroid, drawer: Drawer) {
        drawer.stroke = ColorRGBa.WHITE
        drawer.strokeWeight = 16.0
        drawer.fill = null
        val sizer = 30.0
        drawer.scale(sizer, sizer)
        val sc = asteroid.scale()
        drawer.scale(sc,sc)
        drawer.rotate(asteroid.heading)
        drawer.stroke = ColorRGBa.WHITE
        drawer.strokeWeight = 8.0/30.0/sc
        drawer.scale(1.0, -1.0)
        drawer.lineStrip(rock)
    }

Then we scale again, by asteroid.scale():

    fun scale() =2.0.pow(splitCount)

splitCount is 0, 1, or 2, so the result of the call to scale() should be 1, 2, or 4. If I print the values in display, I get

asteroid sets scale 4.0
asteroid sets scale 2.0
asteroid sets scale 1.0

I have a plan, but not one that I like a great deal:

First, change the asteroid scale factor to be double what it is now:

    fun scale() =2.0.pow(splitCount)*2.0

That will make the asteroids look huge on screen.

asteroids huge

Now change all the objects not to apply the scale factor of 30.

This is not as easy as one might like, because in Splat, for example:

class SplatView(lifetime: Double) {
    private val rot = Random.nextDouble(0.0, 360.0)
    private var sizeTween = Tween(20.0, 100.0, lifetime)
    private var radiusTween = Tween(30.0, 5.0, lifetime)
    private val points = listOf(
        Point(-2.0, 0.0), Point(-2.0, -2.0), Point(2.0, -2.0),
        Point(3.0, 1.0), Point(2.0, -1.0), Point(0.0, 2.0),
        Point(1.0, 3.0), Point(-1.0, 3.0), Point(-4.0, -1.0),
        Point(-3.0, 1.0)
    )

    fun draw(splat: Splat, drawer: Drawer) {
        drawer.stroke = splat.color
        drawer.fill = splat.color
        drawer.rotate(rot)
        for (point in points) {
            val size = sizeTween.value(splat.elapsedTime)
            val radius = radiusTween.value(splat.elapsedTime)
            drawer.circle(size*point.x, size*point.y, radius)
        }
    }
}

The values in the Tweens are implicitly scaled. So … I’ll divide them by 30.

    private var sizeTween = Tween(0.66, 3.33, lifetime)
    private var radiusTween = Tween(1.0, 0.166, lifetime)

I am definitely keeping a save point here.

In Ship.draw, I remove the scaling to 30. In AsteroidView I remove two lines that set the scale to 30.

In Saucer.draw:

    fun draw(drawer: Drawer) {
        drawer.translate(position)
//        drawKillRadius(drawer)
        drawer.stroke = ColorRGBa.GREEN
        val sc = 45.0
        drawer.scale(sc, -sc)
        drawer.strokeWeight = 8.0 / sc
        drawer.lineStrip(SaucerPoints)
    }

That needs the 30 factored out. I think however, that it should be a 2, not a 1.5, based on the proportions in the original game.

I’m not clear what to do about this, in Score:

    private fun drawFreeShips(drawer: Drawer) {
        drawer.isolated {
            val ship = Ship(Point.ZERO)
            ship.heading = -90.0
            translate(250.0, 900.0)
            drawer.scale(1 / U.DROP_SCALE, 1 / U.DROP_SCALE)
            for (i in 1..shipCount) { // ships remaining
                translate(1000.0, 0.0)
                drawer.isolated {
                    ship.draw(drawer)
                }
            }
        }
    }

It might be just fine, we’ll have to wait and see.

And in my main:

        extend {
            val worldScale = width/10000.0
            drawer.fontMap = font
            drawer.scale(worldScale, worldScale)
            game.cycle(seconds, drawer)
        }

I’ll add the 30 back in there:

        extend {
            val worldScale = width/10000.0
            drawer.fontMap = font
            drawer.scale(worldScale, worldScale)
            drawer.scale(30.0, 30.0)
            game.cycle(seconds, drawer)
        }

I am hopeful that this gets me back to where I was.

I was wrong about Splat scale, that has to go back. Revert that file and do over.

Testing in game, nothing displays. Got to revert and find a better way.

There is a lesson here, which I will shortly draw out.

Again

Change Asteroid scale to double. Think briefly that that doesn’t directly affect the kill radius. The killRadius starts at 500 and goes down by half. Let’s change that to 1000 and add it to the list of things needing fixing. That plus the scale change looks right. Commit to lock the scale change. “Asteroid scale doubled to be right size. KillRadius is randomly 1000 and down.”

Now if I were to remove the sizing scaling from AsteroidView, and move it up, why would that not be OK? Ah, I see the reason things went badly. Here’s Asteroid.draw:

    fun draw(drawer: Drawer) {
        drawer.fill = null
        drawer.translate(position)
        view.draw(this, drawer)
    }

I see two ways to go here. One is to scale the position to reverse the overall scale and set it ahead of translation. A second way is to do the scaling everywhere, but after translation. Thing is, not all objects have a position that we can refer to.

Let’s create a Universal constant for the overall scale factor (the 30, not the one that adjusts positions into the frame).

    const val DRAW_SCALE = 30.0

Now we’ll get people to use that factor as needed for their own purposes:

class Asteroid
    fun draw(drawer: Drawer) {
        drawer.fill = null
        drawer.translate(position)
        drawer.scale(U.DRAW_SCALE, U.DRAW_SCALE)
        view.draw(this, drawer)
    }

class AsteroidView
    fun draw(asteroid: Asteroid, drawer: Drawer) {
        drawer.stroke = ColorRGBa.WHITE
        drawer.strokeWeight = 16.0
        drawer.fill = null
        val sc = asteroid.scale()
        drawer.scale(sc,sc)
        drawer.rotate(asteroid.heading)
        drawer.stroke = ColorRGBa.WHITE
        drawer.strokeWeight = 8.0/30.0/sc
        drawer.scale(1.0, -1.0)
        drawer.lineStrip(rock)
    }

This just isn’t good enough. We can’t make everyone be responsible for setting the overall scaling. Let’s push on for now. I think I can safely commit. But there must be tests failing because of that asteroid kill radius. Just one, and I adjust the input. Commit: Asteroid sets U.DRAW_SCALE.

Now Ship:

        drawer.scale(U.DRAW_SCALE, U.DRAW_SCALE)

Should be just fine. Looks good. Commit: Ship sets U.DRAW_SCALE

Let’s see what happens if we change that value now.

The kill radii for asteroids are now off.

    override val killRadius: Double = 1000.0 * U.DRAW_SCALE/30.0,

This is about right. Still have the note to do something better.

The Splats are probably double the size they should be. I can hack them as I did the kill radii.

    private val ratio = U.DRAW_SCALE/30.0
    private var sizeTween = Tween(20.0*ratio, 100.0*ratio, lifetime)
    private var radiusTween = Tween(30.0*ratio, 5.0*ratio, lifetime)

This is all way too ad hoc, random values flying about. Need to get in better shape.

We need to sort out saucer scaling. It’s about double the right size now.

    fun draw(drawer: Drawer) {
        drawer.translate(position)
//        drawKillRadius(drawer)
        drawer.stroke = ColorRGBa.GREEN
        val sc = 45.0
        drawer.scale(sc, -sc)
        drawer.strokeWeight = 8.0 / sc
        drawer.lineStrip(SaucerPoints)
    }

I was trying to make it about 1.5 times ship size. Fix this to match our current scheme:

    fun draw(drawer: Drawer) {
        drawer.translate(position)
        drawer.scale(U.DRAW_SCALE, U.DRAW_SCALE)
//        drawKillRadius(drawer)
        drawer.stroke = ColorRGBa.GREEN
        val sc = 1.5
        drawer.scale(sc, -sc)
        drawer.strokeWeight = 8.0 / U.DRAW_SCALE*sc
        drawer.lineStrip(SaucerPoints)
    }

I forgot to adjust the strokeWeight the first time around.

Let’s think about its size for a moment.

private val saucerPoints = listOf(
    Point(-2.0, 1.0), Point(2.0, 1.0), Point(5.0, -1.0),
    Point(-5.0, -1.0), Point(-2.0, -3.0), Point(2.0, -3.0),
    Point(5.0, -1.0), Point(2.0, 1.0), Point(1.0, 3.0),
    Point(-1.0, 3.0), Point(-2.0, 1.0), Point(-5.0, -1.0),
    Point(-2.0, 1.0)
)

It extends from -5 to +5 in x, and from -2 to 3 in y. So it is 10 wide. The Ship is scaled to double, so it is 14 wide, so 1.5 for the Saucer makes it about the same size as the ship.

I want to review the original code and my Lua code to see what they do. My Lua code sets it to twice its nominal size for the large sauce. I adjust to that, winding up here:

    fun draw(drawer: Drawer) {
        drawer.translate(position)
        drawer.scale(U.DRAW_SCALE, U.DRAW_SCALE)
//        drawKillRadius(drawer)
        drawer.stroke = ColorRGBa.GREEN
        val sc = 2.0
        drawer.scale(sc, -sc)
        drawer.strokeWeight = 2.0 / U.DRAW_SCALE*sc
        drawer.lineStrip(saucerPoints)
    }

Let’s commit this and sum up. “Game appears to be adjusted to U.DRAW_SCALE through out. Still sloppy.”

Summary

I am embarrassed at the poor quality of scaling in this game. There is one fundamental issue that offers some small forgiveness: Since not all objects have a position to be set to, the scaling of the individual display items must be left to them (or so I believe at this writing). But that doesn’t excuse the literal 30.0 and even worse 45.0 (30 times 1.5) values lying around. And the kill radii look to be sort of laid in by eyeball instead of being based on the “actual” dimensions of the things in hand. The kill radii need to match up to the size of the object as displayed, but could really be done just as well in absolute values of object position (or so I believe at this writing).

This code is hard to understand and hard to change. It’s hard to make it right. I have no excuse, but I think I have at least a partial explanation.

The original game is based on fixed small sizes for the objects: the Ship displays 28 pixels wide, for example, and asteroids are 32, 64, or 128 wide. The original game was played on a space of the order of 1000x1000 pixels.

I chose to make my game play on a space of 10,000 x 10,000 pixels, thinking in terms of higher-resolution displays such as we have these days. My monitor is running at 1920 by 1080, and could go double that if my eyes were that good. If I did that I couldn’t read anything, so I don’t.

Anyway, positions from 0-10,000 need to be scaled into the window size. I’m presently drawing a 1000x1000 window. But I did some things wrong far less well than I might have. Let’s list a few:

  1. If the original game was played on roughly 1000x1000 and we want to emulate it, choosing 10,000x10,000 was probably not a good idea.
  2. If we insist on 10,000, the objects need to be scaled up massively to be visible, which means that the original game’s scaling of 1, 2, 4, or 8 doesn’t cut it, and it leads to a bump in the logic, because motion is being done at the 10,000 scale and drawing at some other scale.
  3. Having kind of given up the mapping from points to reality, I assigned estimated kill radii rather than basing them and the calculation on the actual object size in the universe.
  4. I must have gotten in a hurry, even though I certainly was not feeling pressure to deliver, because there are magic numbers all around in the code.
  5. It’s possible, in my defense, that once overall scale and object scale are broken apart (10,000 points in space, and a ship defined to be 14 points long), position calculations and display calculations are forced to be separate, even though they are coupled. Since I made that decision, and wrote all the code, it’s not much of a defense. My point is, sometimes once a design decision is in place, it starts to seem natural, and each individual accommodation to the decision seems simple enough. “OK, here we scale to 45 to be 1.5 times bigger than the standard 30.” Never mind that the standard 30 is weird, we’re used to it.

I could go on.

It’s not that bad …

Now there’s no big reason to whip myself viciously. The only discernible flaw in the game as of this morning was that I thought the ship was too large relative to the asteroids. I chose to double the asteroids, because that’s more in the direction of the original historical code. I could just as well have halved the ship’s size and moved on, nothing to see here. The look at feel would be right.

The code would still have weird numbers in it, but they’d all be consistent. You’d never have known, and if I hadn’t dug so deeply into what was going on, I might not either. There’s only one more real change needed, the small saucer, and that is trivial to do. It might even already be in there code. If it looked too small, a simple adjustment would fix it.

So it’s not that bad.

But it’s not up to my standard.

Today’s changes make sense as an improvement: We decide to adjust the size of the asteroids, and that makes us want to adjust the overall display factor … and that turns out to be awkward.

If changes are difficult, the design isn’t as good as it could be. This design isn’t as good as it could be.

Today, I don’t know what to do about it. Perhaps, by tomorrow, I will.

For now, I go off grumbling but with an improved system. Played well enough to win the day, but not well enough to feel great about things.

Oh well, sometimes the bear bites me. Some time in the past, I hid some bears in the code, and one of them bit me today. Ouch.

See you next time!