Playability

As of yesterday, the game is more or less playable. You can fly around in a gravity-less environment and shoot bullets at the other ship. Tozier and I played a little while, and came up with this list of issues. These are in the order we thought of them, not priority order. There are a couple one might consider to be defects, marked as BUG below.

  • rotation too slow
  • Missile capacity
  • Acceleration too slow?
  • Max speed?
  • Buttons
    • Hard to see
    • Hard to keep finger on
    • Bad position
    • Put in corners
  • hyperspace
  • BUG: Firing too fast kills missiles
  • sun / gravity
  • star field
  • Ships bigger?
  • Limit game to stay off keys
  • Hit count / damage amount
  • settings console
  • Information display (bullets left, score, …)
  • BUG: For loop exploded with error from next
  • Asteroids (frogger, etc)
  • Game recycle
  • Player ID

The for loop was interesting. The code that checks pairs of objects to decide if they are colliding threw an exception. Inside the for logic, the next function, which is some internal function of looping. We tried to duplicate it but could not. So that’s weird.

The other issue, that firing too fast kills missiles, is because missiles have a kill radius of 20 pixels, and they rez frequently enough that missile number N isn’t out of the way when N+1 comes out. So we needed to adjust spacing.

Also we looked at the code to see if there was some general improvement to be made. There wasn’t too much learning to be had so we’ll just show you the results. This has nothing to do with the fact that I forgot to push the day before yesterday version to GitHub, so that I can’t show you the old code even if I wanted to. However, new rule: Push to GitHub before starting work, not just at the end. Also, adding this comment to every tab in the project:

-- HAVE YOU PUSHED TO GITHUB TODAY?

We’ll call this Process Idea For Dummies. Excuse me for a moment, I think I’ll commit this to GitHub …

Code improvements, we hope.

Class constants

When we looked at the Ship init, we found some items that seemed like class variables and talked about a couple of ways they might be done. We settled on this:

Ship = class()

    Ship.MaxMissiles = 50
    Ship.TicksBetweenMissiles = 20 -- kill radius (at least) function of missile speed
    Ship.ThrustAmount = vec2(0,0.02)
    Ship.TurnAmount = 1
    Ship.MaxSpeed = 3
-- do more of this

function Ship:init(shipNumber)
    self.name = string.format("Ship %d", shipNumber)
    self.missileLoad = Ship.MaxMissiles
    self.timeLastFired = -Ship.TicksBetweenMissiles
    self.shipNumber = shipNumber
    
    self.pos = self:initialPosition()
    self.heading = self:initialHeading()
    self.vel = vec2(0,0)
    self.controls = { 
        left   = self:controlButton(HEIGHT/5),
        right  = self:controlButton(2*HEIGHT/5),
        thrust = self:controlButton(3*HEIGHT/5),
        fire   = self:controlButton(4*HEIGHT/5)
    }
    addDrawnAtTop(self)
end

There are similar adjustments throughout Ship. Substitute Ship. for self. for things that are common to all ships.

We considered two approaches. It would be possible to declare the class constants as local to the Ship tab, such as:

Ship = class()

    local MaxMissiles = 50
    local TicksBetweenMissiles = 20 -- kill radius (at least) function of missile speed
    local ThrustAmount = vec2(0,0.02)

… and so on. We decided to keep the constants bound to the class, not to the text tab (file) that the class is in. Judgment call, possibly wrong but it feels good so far.

Missile rezzing

Missiles are created by Ships at some interval. But the Missile itself decides where to rez, “a ways out in front” of the ship. The distance wasn’t enough. We did some bar napkin math to decide how it should be. We rearranged some constant values and names and changed how Ship decides whether to fire. Note the comment above about kill radius: if that value is less than kill radius, the bullets collide. (For reasons we do not understand, you can fudge this downward a bit. Well, we just looked and believe that the reason is this code:

function Ship:fireMissile()
    if self.missileLoad > 0 and U.Tick - self.timeLastFired > Ship.TicksBetweenMissiles then
        self.timeLastFired = U.Tick
        self.missileLoad = self.missileLoad - 1 
        Missile(self)
    end
end

Note the greater-than. That means we’ll fire every 21 ticks. So we can get away with 19 between, not 20. We decide to change this to >=. And we test it. Missiles still occasionally die, and the frequency of death changes depending on the angle of the ship, even with the ship not moving.

We printed some values and did manage to find tiny distance differences. You’d expect the distance between missiles to be exactly 20 if they fire every 20 ticks and have velocity 1. Sometimes they had 0.000000003 less than 20. So, rounding in the square root function – probably. But weird. We decide to set the values as follows:

Ship.TicksBetweenMissiles = U.MissileKillDistance + 1

That allows full-rate firing when standing still. If you’re accelerating, missiles can catch up from behind. Deal with it.

However … Ron, at least, finds this surprising and a bit confusing. This, despite the fact that he knows better than ever to use floating point for anything. In Lua, you have no choice, all numbers are float, even numbers like 1 and 20. It’s irritating, though, to have to hack in a value like that. We’ll try to get over it.

Tozier expresses a concern that as we work on maneuverability we may change the dynamics enough to have to revisit this issue. True. We’ll see.

Excuse me while I commit this branch. OK, done.

Maneuverability

Rotation, acceleration, and maximum. velocity are three things we want to tune. Just for fun, we’re going to make parameter sliders and play with them to get values we like.

And, wow!

We did put in a rotation parameter and slide it around to change the rotation rate. It worked nicely and we’re sure we can quickly use the approach to tune the game’s parameters. But then some very weird things started to happen.

Once in a while, the entire Codea app would crash, “out to the desktop”. I’ve seen this very rarely but it happened fairly frequently. And we got a very occasional strange message from the Codea runtime:

invalid key to 'next'
stack traceback:
	[C]: in function 'next'
	Main:45: in function 'collisions'
	Main:36: in function 'draw'

This was inside that double collision loop that we use to decide who has run into what and to kill them:

function collisions()
    local considered = {}
    for _, obj in pairs(U.Drawn) do
        for _, other in pairs(U.Drawn) do
            if considered[obj] and considered[obj][other] then
                -- already done
            else
                if not considered[other] then considered[other] = {} end
                considered[other][obj] = true
                if colliding(obj, other) then
                    obj:die()
                    other:die()
                end
            end
        end
    end
end

Now this is iffy at the best of times, because it is knocking people out of the collection we’re iterating over, when things die. For example, in Missile:

function Missile:die()
    U.Drawn[self] = nil
end

Since that message comes from deep in the Codea / Lua runtime, I was asking about it on the Codea Forum, and in trying to explain what was going on, I realized that despite everything we’ve done so far, the double loop still doesn’t keep us from processing things that are dead. For example, imagine that the outer loop selects a ship, and starts iterating over all the objects to see if any are colliding with it. Suppose that two are. (This is unlikely but possible.) The first collision will kill the Ship, which will remove itself from the collection. But the outer for loop already has the ship in hand and will compare it with whatever other objects remain. The dead ship may kill other things. Maybe that’s good, maybe it’s bad, but for sure it isn’t what we had in mind.

I don’t think that’s what’s causing Codea to throw a hissy. But I do think the current approach to collisions isn’t right. Another concern is that when a ship is killed, it adds an Explosion to the U.Drawn table. Will that Explosion get picked up the next time the inner loop initializes? It sure will. Could it get picked up during the current loop? I don’t think so but it depends on things we don’t know about how pairs() works.

So this needs rework, and I’ll start that in the next section. But first a word from our sponsor:

Weird bugs are bad

I thought TDD and all this fancy incremental design and such was supposed to prevent defects, and that frequent testing was supposed to prevent these weird things that we don’t understand. Tiny steps were supposed to make everything perfect!

Yes, well, nothing makes everything perfect. What does happen is that often we get information earlier, telling us that there is something going on that is flat wrong. It’s one thing to have the design be a bit crufty: we’ll move from clean to dusty to downright grungy over here in the corner, and back, as we iterate. It’s entirely another thing to have something show up as Obviously Wrong.

It does happen. We’d like it to happen early rather than late in the project, and our TDDing and iterating and refactoring help us get things to show up early. Then we can get after them.

Weird system problems may be at the root here. It is entirely possible that there is a bug in Codea, and that we’re exercising it. However, there are few if any obvious bugs in Codea: Simeon. has made it very clean. So if this is a Codea defect, it is one that is rarely exercised. And we’re doing odd things, iterating over collections that are being updated as we go. Let’s back away from odd things, as we always should.

I definitely got that sinking feeling you get when there’s some horrible deep hard to find bug in the system. I recognize the situation. The difference is that we’ve hit it early, we have very little code to worry about, and we have tools to help us. So we take a break and get down to it.

See you next time!