GitHub Repo

Let’s get the display loop running on a Game instead of a couple of ships. Then I’ll see if I can figure out the keyboard connection. Probably.

The display code looks like this (except moving):

fun main() = application {
    configure {
        width = 800
        height = 800
    }

    program {
        val image = loadImage("data/images/pm5544.png")
        val font = loadFont("data/fonts/default.otf", 64.0)
        val ship = FlyingObject.ship(Vector2.ZERO )
        ship.velocity = Vector2(1200.0, 600.0)
        val asteroid = FlyingObject.asteroid(Vector2(1000.0, 1000.0),Vector2(1000.0, 400.0) )
        var lasttime: Double = 0.0
        var deltaTime: Double = 0.0

        extend {
            deltaTime = seconds - lasttime
            val worldScale = width/10000.0
            drawer.scale(worldScale, worldScale)
            drawer.drawStyle.colorMatrix = tint(ColorRGBa.WHITE.shade(0.2))
            drawer.image(image)
            ship.cycle(drawer, seconds, deltaTime)
            asteroid.cycle(drawer, seconds, deltaTime)
            lasttime = seconds
        }
    }
}

This is the template that came with OPENRNDR, which I’ve modified just enough to get it to display the few things I’ve checked on screen, which were wrapping and some looks at the speed of movement of things, plus the graphical look of the ship and the one asteroid design I’ve imported.

You can see that in this version, I just create a couple of objects in the program part, and display them in the extend part, which is the cycle. Oh, and I added the deltaTime variable, because I need it to scale velocity.

We should really create a Game instance, and tell it to do its thing, leaving it to update all the objects and draw them.

There will be other display things to do. One comes to mind: how do things know how to wrap around the screen?

Ah, right. Position and capping thereof are done in world coordinates, so they just cap at 10,000. What if the game window wasn’t square? That would need fixing. For now … I’ll make a note.

I don’t see a really good way to test the main loop here. It’ll be pretty simple, so let’s just edit it carefully and see if we can see how to keep most of the logic outside. We could even move the deltaTime into Game. Might be prudent.

I’m just going in. The Rules will be:

  1. The Game will be created including a call to createGame, where the specific layout of the real game will be;
  2. The loop will call game.cycle(drawer, seconds).

OK, I’m goin’ in. In Game, I now have this:

class Game {
    val flyers = Flyers()
    var lastTime = 0.0

    fun add(fo: FlyingObject) = flyers.add(fo)

    fun colliders() = flyers.pairsSatisfying { f1, f2 -> f1.collides(f2) }

    fun createContents() {
        val ship = FlyingObject.ship(Vector2.ZERO )
        ship.velocity = Vector2(1200.0, 600.0)
        val asteroid = FlyingObject.asteroid(Vector2(1000.0, 1000.0),Vector2(1000.0, 400.0) )
        add(ship)
        add(asteroid)
    }

    fun cycle(drawer: Drawer, seconds: Double) {
        val deltaTime = seconds - lastTime
        lastTime = seconds
        update(deltaTime)
        draw(drawer)
    }

    fun draw(drawer: Drawer) = flyers.forEach {drawer.isolated { it.draw(drawer) } }

    fun update(deltaTime: Double) = flyers.forEach { it.update(deltaTime)}
}

I added the lasttime stuff, createContents, cycle, and draw. I guess I could have tDD’s the create, but in fact that’s going to change as the game grows, so no. I’ll let my betters suggest what I should have done: the Zoom Ensemble meets tonight.

In the template program:

    program {
        val image = loadImage("data/images/pm5544.png")
        val font = loadFont("data/fonts/default.otf", 64.0)
        val game = Game().also { it.createContents() }

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

This should work, and it does. I have the same asteroid and ship drifting across the screen. Perfect. Commit: OPENRNDR template now only creates Game instance and cycles it.

While we are on a roll, let’s bring in the other Rock definitions from Lua and allow asteroids to have all the classical shapes.

The current ShipView just knows one shape:


class AsteroidView: FlyerView {
    override fun draw(ship: FlyingObject, drawer: Drawer) {
        val points = listOf(
            Vector2(4.000000, 2.000000),
            Vector2(3.000000, 0.000000),
            Vector2(4.000000, -2.000000),
            Vector2(1.000000, -4.000000),
            Vector2(-2.000000, -4.000000),
            Vector2(-4.000000, -2.000000),
            Vector2(-4.000000, 2.000000),
            Vector2(-2.000000, 4.000000),
            Vector2(0.000000, 2.000000),
            Vector2(2.000000, 4.000000),
            Vector2(4.000000, 2.000000),
        )
        drawer.scale(30.0, 30.0)
        drawer.scale(4.0,4.0)
        drawer.rotate(ship.heading + 30.0)
        drawer.stroke = ColorRGBa.WHITE
        drawer.strokeWeight = 8.0/30.0/4.0
        drawer.lineStrip(points)
    }

It turns out that there are four in the actual game. I’ll capture them from Lua and then convert them. I’ll spare you the page of vectors here in the article.

As before, I’ll munge them in Sublime to produce what I need, which probably is a collection of lists of vectors. Let’s add a defineRocks method to ShipView, for now.

After more multi-cursoring than anyone should have to do, I have them all defined in the ShipView class. I test each one by displaying it and I notice something odd: they don’t look right to me.

And I figure out why … in OPENRNDR y increases down the screen, and in Codea Lua, y increases up the screen. So they are all mirrored vertically in my humble opinion.

I can fix this:

    override fun draw(ship: FlyingObject, drawer: Drawer) {
        drawer.scale(30.0, 30.0)
        drawer.scale(4.0,4.0)
        drawer.rotate(ship.heading + 30.0)
        drawer.stroke = ColorRGBa.WHITE
        drawer.strokeWeight = 8.0/30.0/4.0
        drawer.scale(1.0, -1.0)
        drawer.lineStrip(rocks[0])
    }

Right. I’ll just scale y back to up for the duration. Should I put a comment on that, or make it into a method called drawAsteroidsRightSideUp? Sorry, no.

Now I would like for each asteroid created to have a different shape, one of the four available. And while I’m at it, I’d like them to fly at random angles.

Honestly, I may be wrong here, but I don’t really see anything better to do than just type this into the Asteroid code … except that there isn’t any such code.

How do we create these babies?

        fun asteroid(pos:Vector2, vel: Vector2, killRad: Double = 400.0, splitCt: Int = 2): FlyingObject {
            return FlyingObject(
                position = pos,
                velocity = vel,
                killRadius = killRad,
                splitCount = splitCt,
                ignoreCollisions = true,
                view = AsteroidView()
            )
        }

We can allow the AsteroidView to know the asteroid’s shape. Let’s start with that.

class AsteroidView: FlyerView {
    private val rock = defineRocks().random()

    override fun draw(asteroid: FlyingObject, drawer: Drawer) {
        drawer.scale(30.0, 30.0)
        drawer.scale(4.0,4.0)
        drawer.rotate(asteroid.heading)
        drawer.stroke = ColorRGBa.WHITE
        drawer.strokeWeight = 8.0/30.0/4.0
        drawer.scale(1.0, -1.0)
        drawer.lineStrip(rock)
    }

When the view is created, it randomly selects a rock shape and uses it thereafter. This may cause trouble when we split the rocks, but that’s for another day.

From a testing viewpoint, not that these views are being tested, it might be best to create the view providing an integer that is used to select the rock shape. For now, I’m not going to worry about that. I am, however, going to comment.

Comment

The past few days have felt ragged to me, since I started playing with the viewing. When I was using tests to drive development, it felt mostly like just chugging along, create a test, make it work. There were some ragged moments, and some code that took some time to improve, but mostly things went very smoothly. What I’m feeling here is not that. I’m just putting code where I think it belongs, and then looking at the game to see if it looks OK. I don’t like the feeling in comparison to the sense of knowing just what’s going on that I get from the TDD.

I haven’t felt such a sharp delineation before, probably because I’ve not made a substantial shift from testing to not testing before. In some apps, I’ve done little testing, but I haven’t suddenly turned off my testing focus.

I don’t like the feeling. I want to get back to solid ground ASAP.

Meanwhile, I’ve enhanced the createGame a bit:

    fun createContents() {
        val ship = FlyingObject.ship(Vector2(5000.0, 5000.0) )
        add(ship)
        for (i in 0..3) {
            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 = FlyingObject.asteroid(pos,vel )
            add(asteroid)
        }
    }

That gives me a moving screen that looks like this:

one ship four asteroids

What’s next?

I was thinking that I might work on wiring the controls up to events in OPENRNDR, but I want to get back into the testing rhythm. So let’s work on …

Collisions and Splitting

Collisions should be detected and dealt with in Game.update. I think I want to change how it operates a bit. “How does it operate”, I ask myself.

The Flyers object can return all the pairs satisfying any given condition, and we find collisions like this:

class Game ...

    fun colliders() = flyers.pairsSatisfying { f1, f2 -> f1.collides(f2) }

I think we’ll find that that’s got a test or two, for example:

    @Test
    fun `collision detection`() {
        val game = Game()
        val a = FlyingObject.asteroid(Vector2(100.0,100.0), Vector2(50.0,50.0))
        game.add(a)
        val s = FlyingObject.ship(Vector2(100.0, 150.0))
        game.add(s)
        assertThat(game.flyers.size).isEqualTo(2)
        val colliders = game.colliders()
        assertThat(colliders.size).isEqualTo(2)
    }

    @Test
    fun `stringent colliders`() {
        val p1 = Vector2(100.0,100.0)
        val p2 = Vector2(500.0, 500.0)
        val game = Game()
        val v = Vector2.ZERO
        val a0 = FlyingObject.asteroid(p1,v) // yes
        game.add(a0)
        val m1 = FlyingObject(p1, v, 10.0, ) // yes
        game.add(m1)
        val s2 = FlyingObject.ship(p1) // yes
        game.add(s2)
        val a3 = FlyingObject.asteroid(p2,v) // no
        game.add(a3)
        val a4 = FlyingObject.asteroid(p2,v) // no
        game.add(a4)
        val colliders = game.colliders()
        assertThat(colliders.size).isEqualTo(3)
    }

    @Test
    fun `create game`() {
        val game = Game()
        val asteroid = FlyingObject.asteroid(Vector2(100.0, 100.0), Vector2(50.0, 50.0))
        val ship = FlyingObject.ship(Vector2(1000.0, 1000.0))
        game.add(asteroid)
        game.add(ship)
        assertThat(game.colliders().size).isEqualTo(0)
        for (i in 1..12*60) game.update(1.0/60.0)
        val x = asteroid.position.x
        val y = asteroid.position.y
        assertThat(x).isEqualTo(100.0+12*50.0, within(0.1))
        assertThat(y).isEqualTo(100.0+12*50.0, within(0.1))
        assertThat(game.colliders().size).isEqualTo(2)
    }

When the game detects a collision on an asteroid, we want to split the thing. When it has already split twice, we intend it to die. And … what about ships? My plan was (if I recall) that a ship has its split count set to zero, so that it cannot split, and so it dies. We have some tests for splitting as well:

    @Test
    fun `asteroid can split`() {
        val full = FlyingObject.asteroid(
            pos = Vector2.ZERO,
            vel = Vector2.ZERO
        )
        val radius = full.killRadius
        val halfSize: List<FlyingObject> = full.split()
        assertThat(halfSize.size).isEqualTo(2)
        val half = halfSize.last() // <---
        assertThat(half.killRadius).isEqualTo(radius/2.0)
        val quarterSize = half.split()
        assertThat(quarterSize.size).isEqualTo(2)
        val quarter = quarterSize.last() // <---
        assertThat(half.killRadius).isEqualTo(radius/4.0)
        val eighthSize = quarter.split()
        assertThat(eighthSize.size).isEqualTo(0)
    }

    @Test
    fun `asteroids get new direction on split`() {
        val startingV = Vector2(100.0,0.0)
        val full = FlyingObject.asteroid(
            pos = Vector2.ZERO,
            vel = startingV
        )
        var fullV = full.velocity
        assertThat(fullV.length).isEqualTo(100.0, within(1.0))
        assertThat(fullV).isEqualTo(startingV)
        val halfSize: List<FlyingObject> = full.split()
        halfSize.forEach {
            val halfV = it.velocity
            assertThat(halfV.length).isEqualTo(100.0, within(1.0))
            assertThat(halfV).isNotEqualTo(startingV)
        }
    }

What happens when we go to split a ship? I see no test for that. Let’s add one.

    @Test
    fun `ships cannot split`() {
        val ship = FlyingObject.ship(Vector2(100.0,100.0))
        val shipSplit = ship.split()
        assertThat(shipSplit).isEmpty()
    }

I think the tentative plan was that when we ask for collisions, we’ll remove all the colliding objects from the Flyers list, and then add in anything we get from splitting those objects. I had better write a test for that soon. But this one … does it run?

Good news! It does.

Now a more complicated test. Let’s set up the Game so that there is just one ship and one asteroid, and they’re colliding. We’ll describe what should happen. We’ll be positing a new method on Game, something about processCollisions or a better name if one comes to mind.

    @Test
    fun `colliding ship and asteroid splits asteroid, loses ship`() {
        val game = Game()
        val asteroid = FlyingObject.asteroid(Vector2(1000.0, 1000.0), Vector2(50.0, 50.0))
        val ship = FlyingObject.ship(Vector2(1000.0, 1000.0))
        game.add(asteroid)
        game.add(ship)
        assertThat(game.flyers.size).isEqualTo(2)
        assertThat(ship).isIn(game.flyers)
        game.processCollisions()
        assertThat(game.flyers.size).isEqualTo(2)
        assertThat(ship).isNotIn(game.flyers)
    }

I really like how directly I can express what I want with these Kotlin tests.

There is no processCollisions on Game, so:

    fun processCollisions() {
        val colliding = colliders()
        flyers.removeAll(colliding)
        for (collider in colliding) {
            val splitOnes = collider.split()
            flyers.addAll(splitOnes)
        }
    }

Writing that made it clear that I want addAll and removeAll in Flyers:

    
    fun addAll(newbies: Iterable<FlyingObject>){
        flyers.addAll(newbies)
    }

    fun removeAll(moribund: Iterable<FlyingObject>){
        flyers.removeAll(moribund.toSet())
    }

I had to recast my test, because I can’t figure out what to implement on Flyers to allow isIn to work:

    @Test
    fun `colliding ship and asteroid splits asteroid, loses ship`() {
        val game = Game()
        val asteroid = FlyingObject.asteroid(Vector2(1000.0, 1000.0), Vector2(50.0, 50.0))
        val ship = FlyingObject.ship(Vector2(1000.0, 1000.0))
        game.add(asteroid)
        game.add(ship)
        assertThat(game.flyers.size).isEqualTo(2)
        assertThat(ship).isIn(game.flyers.flyers)
        game.processCollisions()
        assertThat(game.flyers.size).isEqualTo(2)
        assertThat(ship).isNotIn(game.flyers.flyers)
    }

But it’s green. Commit: Game understands processCollisions().

That feels good. It also feels like, if I were to call processCollisions in Game.cycle, I could wait until an asteroid hits the ship and watch things happen. I can’t resist.

What I see, after waiting for a while, is that the ship disappears, and two asteroids of the same size fly off. We need to scale them according to their splitCount.

The value of splitCount can be 2, 1, or 0. Once I look at the situation 0->1, 1->2, 2->4, it’s clear that scale is 2^splitCount, so:

    override fun draw(asteroid: FlyingObject, drawer: Drawer) {
        drawer.stroke = ColorRGBa.WHITE
        drawer.circle(Vector2.ZERO, asteroid.killRadius)
        val sizer = 30.0
        drawer.scale(sizer, sizer)
        val sc = 2.0.pow(asteroid.splitCount)
        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)
    }

As I watch the game unroll, the asteroids do split now, but I found that the kill radii are a bit off. So I’ve adjusted the values manually, and caused the game to draw circles of size killRadius around everything:

asteroids showing their kill radius

To get this picture, I patched the Game to create a new ship after every collision. That’s not valid in general, but since right now, the only collisions possible are asteroid:ship, it suffices to keep a ship on the screen all the time, so that the asteroids split, split, then disappear as intended.

There I go again looking at the screen, but it needs doing.

And for this morning, I think we’re done. Let’s sum up. Commit: Game creates asteroids and collides with perpetual ship on screen.

Summary

We added a createGame method to set up an initial group of asteroids and ship. That will merit extension as we move toward a complete game. We changed the main template to create a game and run it, which just entails repeatedly calling game.cycle(), where the work, simple as it is, is done:

    fun cycle(drawer: Drawer, seconds: Double) {
        val deltaTime = seconds - lastTime
        lastTime = seconds
        update(deltaTime)
        processCollisions()
        draw(drawer)
    }

We noticed how slow it felt to be checking the display all the time. We implemented processCollisions with a test, then plugged it into the game.

Then, back to fiddling with the graphics, to watch asteroids split on the screen, which led me to see that my estimates of kill radius were not accurate, which led to lots of tweaking of the values and looking at the circles that show where the kill radii are.

I would like to have a more direct understanding of the kill radii: I just tweaked them to look right. It should be possible to sort out what the radius of the circle around the asteroids should be. They are scaled up and down a bit randomly at present. I’ll read the Lua code and maybe do some research on line to see if there’s anything more direct to be understood.

Presently the ship scales itself to 30x its nominal size, and the asteroids start at that same scale (and scale down by 2 on each split). Another arbitrary figure. We’ll see if we can figure something out that’s a bit more like something intentional rather than cut in by eye.

And I need to think about how to manage stroke width vs scale. There are magic numbers in there too.

All that aside, it does look good on the screen. We’re getting close to a game!

See you next time!