GitHub Repo

I managed to make a test work in the Ship project (its new name). Let’s see what we can predict about a game design.

Details can be found in Questions, where I’ll record details of configuration and such, more for my benefit than yours, though if you’re following along at home, that article might help you get things going. I welcome answers to my concerns there, or questions that should be added.

Yesterday, I got enough configuration done so that I could write a simple test on an extracted Ship object. In this exercise with OPENRNDR, I’m envisioning a kind of Spacewar game, or maybe asteroids, with a space ship flying around, perhaps subject to gravity. I don’t know yet quite where I’ll take it, probably more in the direction of asteroids, which can be played by a single player.

Anyway, here’s what we’ve got now:

class ShipTest {
    @Test
    fun `Ship Happens`() {
        val ship = Ship(100.0)
        ship.step()
        assertThat(ship.realPosition).isEqualTo(Vector2(1.0,1.0))
    }

    @Test
    fun `hook up`() {
        assertThat(1+1).isEqualTo(2)
    }
}

class Ship(private val radius: Double) {
    var realPosition: Vector2 = Vector2(0.0, 0.0)
    var pointing: Double = 0.0

    fun cycle(drawer: Drawer, seconds: Double) {
        drawer.isolated {
            update(drawer, seconds)
            draw(drawer)
        }
    }

    private fun draw(drawer: Drawer) {
        drawer.fill = ColorRGBa.MEDIUM_SLATE_BLUE
        drawer.rectangle(-radius/2.0,-radius/2.0, radius, radius)
    }

    private fun update(drawer: Drawer, seconds: Double) {
        val sunPosition = Vector2(drawer.width / 2.0, drawer.height / 2.0)
        val orbitRadius = drawer.width/4.0
        val orbitPosition = Vector2(cos(seconds), sin(seconds)) *orbitRadius
        realPosition = orbitPosition+sunPosition
        drawer.translate(realPosition)
        val size = cos(seconds)
        drawer.scale(size,size)
        drawer.rotate(45.0+Math.toDegrees(seconds))
    }

    fun step() {
        realPosition += Vector2(1.0,1.0)
    }
}

At this point, the only thing I’m testing is that the step function moves the ship by one pixel. The update is of course moving it all the time. And the ship is just a plain square, not particularly ship-shape. We’re still just getting started and still learning how OPENRNDR wants to be used.

I’d like to get to a point where the ship flies in “space” coordinates, units to be defined, with acceleration and velocity. I think we’ll assume the usual toroidal space, so that when the ship flies off the top of space it returns at the bottom, and so on. But we’ll use abstract coordinates, not pixels.

Pixels or other scaling to the window will be handled elsewhere. The exact breakout of all this stuff remains to be defined. I don’t have enough sense of what all OPENRNDR can do to be at all confident that I can pick a design at this point. That’s fine, evolving the design is what we’re all about.

Today, let’s make a few tentative decisions. Um …

  1. Space is 10000x10000 units in size. We assume that the graphics system will use a square window.

  2. If we are to move across the screen in, say, two seconds, then the maximum velocity of the ship is about 5000 units/second. Let’s assume will express that as a fraction of the universe width somehow. So max velocity might be 0.5 (widths per second).

  3. If it is to take, oh, 5 seconds to accelerate from 0 to full speed, acceleration will be about 0.1 widths per second per second.

This might be enough to allow us to do some tests. And it makes clear that the Ship will want to know the delta time between updates. Should the ship remember that, or should it be provided? I think all the objects we create will likely want elapsed time, so let’s assume that we’ll provide it. And, honestly, I don’t think they’re going to want total time but you never know: there might be a game timer or something that needs that? For now, let’s change the program so that Ship gets deltaTime.

    program {
        val image = loadImage("data/images/pm5544.png")
        val font = loadFont("data/fonts/default.otf", 64.0)
        val ship = Ship(width/8.0)
        var lasttime: Double = 0.0
        var deltaTime: Double = 0.0

        extend {
            deltaTime = seconds - lasttime
            drawer.drawStyle.colorMatrix = tint(ColorRGBa.WHITE.shade(0.2))
            drawer.image(image)

            drawer.fill = null
            drawer.stroke = ColorRGBa.WHITE
            drawer.circle(width/2.0,height/2.0, width/4.0)

            drawer.fill = ColorRGBa.WHITE
            drawer.fontMap = font
            drawer.text("${deltaTime}", width/2.0, height/2.0)
            ship.cycle(drawer,deltaTime)
            lasttime = seconds
        }
    }

Now, of course, the Ship just stays in one place approximately 1/60th of the way around. I think that’s fine for now, we’re TDDing its behavior and as yet is has none.

Let’s ignore the current screen display and get the Ship to behave in a fashion we might like. I do think we’ll keep the screen display up to date with the TDD code, because it’s fun to watch and we do want to get a sense for the maximum speed and acceleration and such.

I recast the original test, which was just there to be sure I could talk to the ship.

    @Test
    fun `Ship Happens`() {
        val ship = Ship(100.0)
        ship.velocity = Vector2(120.0,120.0)
        ship.move(1.0/60.0)
        assertThat(ship.realPosition).isEqualTo(Vector2(1.0,1.0))
    }

I chose a velocity evenly divisible by 60. I’m really sure that these doubles are not going to work out, but we’ll see.

I actually expect that the ship will be at 20,20 when this test works, but I’m leaving it at 1,1 so that I’m sure of enough failures. You know how it is, you just try and try but never fail …

I give Ship a velocity member and this function:

    fun move(deltaTime: Double) {
        realPosition += velocity*deltaTime
    }

Test. Fails:

expected: Vector2(x=1.0, y=1.0)
 but was: Vector2(x=2.0, y=2.0)

Ah yes. One 60th of 120 is 2. Not 20. Sweet. Set the expectation to Vector2(2.0,2.0) and we’re green.

Let’s change the display code to expect the ship to move itself.

I think the rule will be that when the program cycles, you get called on move with the deltaTime and then draw with the drawer. That may turn out not to be what we want. We’ll find out.

No, I think we’ll stick with cycle, but we’ll pass in the deltaTime as well as the seconds. For now, that looks to be easier to sort out.

And I’m going to remove the old update, and rename move to update, because it’s a more common term for this kind of thing.

I’ve added a couple of tests for wrapping around, and code to support it:

    @Test
    fun `capping works high`() {
        val ship = Ship(100.0)
        ship.velocity = Vector2(120.0, 120.0)
        ship.realPosition = Vector2(9999.0, 5000.0)
        ship.update(1.0/60.0)
        assertThat(ship.realPosition.x).isEqualTo(1.0)
        assertThat(ship.realPosition.y).isEqualTo(5002.0)
    }

    @Test
    fun `capping works low`() {
        val ship = Ship(100.0)
        ship.velocity = Vector2(-120.0, -120.0)
        ship.realPosition = Vector2(1.0, 5000.0)
        ship.update(1.0/60.0)
        assertThat(ship.realPosition.x).isEqualTo(9999.0)
        assertThat(ship.realPosition.y).isEqualTo(4998.0)
    }

And …

    fun update(deltaTime: Double) {
        val proposedPosition = realPosition + velocity*deltaTime
        realPosition = cap(proposedPosition)
    }

    fun cap(v: Vector2): Vector2 {
        return Vector2(cap(v.x), cap(v.y))
    }

    fun cap(coord: Double): Double {
        return (coord+10000.0)%10000.0
//        if (coord < 0.0) return coord + 10000.0
//        if (coord > 10000.0) return coord - 10000.0
//        return coord
    }

The commented-out code was my naive version. I then ripped off the modulo version from my Asteroids program in Codea. I mean re-used. Yeah, that’s the ticket. Reuse

Tests are green: I think that velocity and wrap-around are working.

Now to see about getting this on the screen. I need to think about translation and scaling.

Presumably we want the center of the ship coordinates, 5000,5000 to map to the center of the screen. And we want our 10000 ship coordinates to fit into our screen size, of, way, 800 pixels square.

So it seems to me that the basic screen scaling is 800/10000, and so therefore the ship draw should be roughly this (without any scaling of ship size):

    private fun draw(drawer: Drawer) {
        val worldScale = drawer.width / 10000.0
        val center = Vector2(drawer.width/2.0, drawer.height/2.0)
        drawer.fill = ColorRGBa.MEDIUM_SLATE_BLUE
        drawer.translate(realPosition*worldScale)
        drawer.rectangle(-radius/2.0,-radius/2.0, radius, radius)
    }

Now if in the program, I gave the ship a velocity, it should move across the screen, and it should wrap.

    program {
        val image = loadImage("data/images/pm5544.png")
        val font = loadFont("data/fonts/default.otf", 64.0)
        val ship = Ship(width/8.0)
        ship.velocity = Vector2(1200.0, 600.0) // <--- temp for testing
        var lasttime: Double = 0.0
        var deltaTime: Double = 0.0

        extend {
            deltaTime = seconds - lasttime
            drawer.drawStyle.colorMatrix = tint(ColorRGBa.WHITE.shade(0.2))
            drawer.image(image)
            ship.cycle(drawer, seconds, deltaTime)
            lasttime = seconds
        }
    }

And this works as I expect based on my tests:

blue square migrates across and down, wrapping at edges

This is a great place to stop on a Saturday. Let’s sum up.

Summary

I have a few reasonable tests that check the update command, checking that the velocity correctly updates the ship’s “real position”, including wrap around. The checks give me a base on which to stand with regard to testing. It offers the prospect that I can do more microtests rather than resolve all my concerns by looking at the screen. Looking will still be necessary, mostly to judge timing. With any decent tests, we should be sure that collisions are registered and such without looking at the picture.

We’ll see. I am hopeful, not without reason.

The 10000.0 for universe size is just a number typed in. It’ll want to be some kind of defined constant, or high-level variable. We’ll see.

There will need to be work done on the scale of drawing the ship itself (and, of course what it looks like). And i think we have the scaling we’re doing in the wrong place: it should be handled for all objects. I think we can probably set the scale once and for all up in program and then have everything just fit in. Should we try that?

OK, let me commit this and try it, though I think I’m really timed out on energy.

Hmm, I got a new push dialog that I’ve never seen before. Some config issue? We’ll see.

Anyway move scaling where it belongs. We’d like the ship to just draw itself at its native coordinates:

    private fun draw(drawer: Drawer) {
        val center = Vector2(drawer.width/2.0, drawer.height/2.0)
        drawer.fill = ColorRGBa.MEDIUM_SLATE_BLUE
        drawer.translate(realPosition)
        drawer.rectangle(-radius/2.0,-radius/2.0, radius, radius)
    }

Now if I just set scale in program, will it do the right thing?

        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)
            lasttime = seconds
        }

In fact it does, except that now the ship is a very tiny square. I’ll just create it with a larger radius:

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

Now it has a reasonable size:

ship of reasonable size

I’ll commit this: display is scaled at top, ship thinks in its world coordinates.

We’ll call it a morning. See you next time!