A new approach to steering. And an apology.

First the apology. I’m working on this on Tuesday, June 2. I’ll publish it on the 3rd. I know I’m supposed to shut up and listen today. I’ll do that as much as I can bear. I’m sorry that I can’t bear as much as you do.

Programming is my gift and my shelter. It’s where I go when the going gets tough. It’s where I’m going right now.

Now the steering. I’ve tried a number of options for steering the ship, including at least these:

  • Two buttons, analogous to the old game’s buttons. I’ve tried these in various positions.
  • Sliding finger up-down or left-right to change the ship’s rotation. I’ve tried this as a trigger, a scaled trigger, and the further you move the more it rotates.
  • Tilting the iPad and using Gravity to rotate the ship.

None of these has been quite the thing. On the Codea forum, @West implemented a suggestion where your first touch defined a center, and then the position of your finger after that defined the direction that your ship pointed. I played with his example and liked it, but I felt it missed the “spirit” of Asteroids in two ways: Asteroids ship rotation speed is constant, and it is on-off oriented due to the two buttons.

Well, I’m about to build something much like @West’s idea, and my experiments with it so far make me think I’m going to like it. So, there, I do too listen!

Experiments

I built a few small experiments. One concern I had was that @West’s idea starts with you defining the center of a compass, and then dragging out to a point on a sort of radius around that center. Then you’d rotate around the center as you turn the ship.

That means that your first turn setting is touch-drag in the direction you want the ship to point, and all the rest are dragging around a circle (or roundish shape, the algorithm doesn’t care). So I would like to avoid that first touch-drag. That could be done by defining the center ahead of time and showing it on the screen. And that’s my plan.

Here are a couple of pics of an experiment. This first one shows the screen before you touch it. The dot is the center of the “circle” you’re to draw.

center point

This second pic shows where I most recently touched, a ways away from the center. I’ve drawn a line to that point and displayed its value, because I was using the natural reach of my thumb while holding the iPad, to estimate where things should go.

finger point

Another concern I have with @West’s notion is that it directly sets the aim of the ship rather than controlling the direction of constant-rate rotation. My current plan is to just get over that concern, though if it really seems to spoil the game, we’ll try something else.

I did some other experiments. You’re probably aware that three points define a circle. So, it seemed to me, if you were spinning your thumb in a circle, maybe a little math could find the center, and if your circle was at all regular, it would tend to converge.

Here’s a “little math”, the Codea code for the center of a circle defined by three points:

function center(P1, P2, P3)
    local x1 = P1.x
    local y1 = P1.y
    local x2 = P2.x
    local y2 = P2.y
    local x3 = P3.x
    local y3 = P3.y
    local x12y12 = x1*x1 + y1*y1
    local x22y22 = x2*x2 + y2*y2
    local x32y32 = x3*x3 + y3*y3
    local a = x1*(y2-y3) - y1*(x2-x3) + x2*y3 - x3*y2
    local b = x12y12*(y3-y2) + x22y22*(y1-y3) + x32y32*(y2-y1)
    local c = x12y12*(x2-x3) + x22y22*(x3-x1) + x32y32*(x1-x2)
    local x = -b/(2*a)
    local y = -c/(2*a)
    return vec2(x,y)
end

Yes, that was fun, and yes, I did find that on the Internet. Better yet, it really didn’t work very well, because the wobble in your circle makes things jump around. Possibly some kind of weighted solution would have worked.

Then I thought, well, the center of gravity of the points around a circle is pretty much the center of the circle, so why not find the center that way. That actually works rather well, with a weighted average.

Those experiments convinced me to try what you see in the pictures above: a defined center and the line between that center and your touch defines the angle of the ship. So that’s what we’ll do today.

If this seems good, I’ll leave it in. If it seems promising but not quite good, well, we can tune it or see what’s better.

I’m feeling rather good about the idea, because so far it has worked better than any ideas of mine. Yay, @West!

Here we go.

Thumb Wheel

I just decided to call this section “thumb wheel”. It’s not quite the right name for what we’re doing but it’ll do as a heading.

Here’s what we have for touch now. In Main, we just record all the active touches:

function touched(touch)
    if U.attractMode and touch.state == ENDED then U:startGame(ElapsedTime) end
    if touch.state == ENDED or touch.state == CANCELLED then
        Touches[touch.id] = nil
    else
        Touches[touch.id] = touch
    end
end

Then, in Button, we define and check them:

function createButtons()
    local dx=75
    local dy=200
    table.insert(Buttons, {x=dx, y=dy, radius=dx, name="left"})
    table.insert(Buttons, {x=dy, y=dx, radius=dx, name="right"})
    table.insert(Buttons, {x=WIDTH-dx, y=dy, radius=dx, name="fire"})
    table.insert(Buttons, {x=WIDTH-dy, y=dx, radius=dx, name = "go"})
end

function checkButtons()
    U.button.left = false
    U.button.right = false
    U.button.go = false
    U.button.fire = false
    for id,touch in pairs(Touches) do
        for i,button in ipairs(Buttons) do
            if touch.pos:dist(vec2(button.x,button.y)) < button.radius then
                U.button[button.name]=true
            end
        end
    end
end

Here, we’ll just remove the left-right guys and do something else. For completeness, let me mention that the “go” button is checked directly by the Ship.

Hm. As I think about this, what if we make one big “turn” button down in the lower left corner, where we want the user to touch and draw their circle? Then if the touch is in the big circle (or maybe even near it and outside, we do our turning math.

I think I like that.

function createButtons()
    local dx=75
    local dy=200
    table.insert(Buttons, {x=125, y=125, radius=125, name="turn"})
    table.insert(Buttons, {x=WIDTH-dx, y=dy, radius=dx, name="fire"})
    table.insert(Buttons, {x=WIDTH-dy, y=dx, radius=dx, name = "go"})
end

Gives us this:

big turn button

I like it. The touch logic will be weird, because we don’t just want the on-off behavior of the other buttons. Let’s look at the code and think what to do:

function checkButtons()
    U.button.left = false
    U.button.right = false
    U.button.go = false
    U.button.fire = false
    for id,touch in pairs(Touches) do
        for i,button in ipairs(Buttons) do
            if touch.pos:dist(vec2(button.x,button.y)) < button.radius then
                U.button[button.name]=true
            end
        end
    end
end

This code, with the init adjusted and then left to its own devices, will turn the “left” and “right” flags off and leave them off, and turn the “turn” flag on when the button is touched. (We’ll want to initialize it to be off.

Leaving left and right turned off will mean that whatever’s going on with them is harmless, giving us time to clean it up as we need to. Let’s make the init change and then record the touch’s position if it’s “turn”:

function checkButtons()
    U.button.left = false
    U.button.right = false
    U.button.turn = false
    U.button. turnPos = nil
    U.button.go = false
    U.button.fire = false
    for id,touch in pairs(Touches) do
        for i,button in ipairs(Buttons) do
            if touch.pos:dist(vec2(button.x,button.y)) < button.radius then
                U.button[button.name]=true
                if button.name == "turn" then
                    U.button.turnPos = touch.pos
                end
            end
        end
    end
end

It seems to me that with this code, U.button.turn will be true if we’re touching in the turn button, and U.button.turnPos will have the position of the touch, which will be nil if there is none.

Let’s go see where the ship turns itself:

function Ship:move()
    if U.button.left then self.radians = self.radians + U:adjustedRotationStep() end
    if U.button.right then self.radians = self.radians - U:adjustedRotationStep() end
    if U.button.fire then if not self.holdFire then self:fireMissile() end end
    if not U.button.fire then self.holdFire = false end
    self:actualShipMove()
end

We might as well remove the left/right stuff. And what we want to do here is to set the ship’s rotation (called radians) to be the same as the angle of the line from the button center to the touch.

function Ship:move()
    if U.button.turn then
        self:turn()
    end
    if U.button.fire then if not self.holdFire then self:fireMissile() end end
    if not U.button.fire then self.holdFire = false end
    self:actualShipMove()
end

function Ship:turn()
    local center = U.button.turnCenter
    local pos = U.button.turnPos
    local angle = math.atan2(pos.y-center.y, pos.x-center.x)
    self.radians = angle
end

I added U.button.turnCenter to access the center. It’s presently set every time through the button checker but we can fix that.

The mysterious thing is that this works! No sense saving angle in a local, so:

function Ship:turn()
    local center = U.button.turnCenter
    local pos = U.button.turnPos
    self.radians = math.atan2(pos.y-center.y, pos.x-center.x)
end

I don’t love the look of the firing logic, so will clean it up a bit.

function Ship:move()
    if U.button.turn then self:turn() end
    if U.button.fire and not self.holdFire then self:fireMissile() end
    if not U.button.fire then self.holdFire = false end
    self:actualShipMove()
end

You may not like those in-line if statements, but I’m old, and I do. YMMV, and also my house my rules.

Now to change the init of turnCenter:

function createButtons()
    local dx=75
    local dy=200
    local cen = vec2(125,125)
    U.button.turnCenter = cen
    table.insert(Buttons, {x=cen.x, y=cen.y, radius=125, name="turn"})
    table.insert(Buttons, {x=WIDTH-dx, y=dy, radius=dx, name="fire"})
    table.insert(Buttons, {x=WIDTH-dy, y=dx, radius=dx, name = "go"})
end

One more thing. The buttons turn opaque red when they are pressed. The small ones aren’t too bad but the big one hides a lot. Let’s make that color much more transparent, and the normal fill as well:

function drawButtons()
    pushStyle()
    ellipseMode(RADIUS)
    textMode(CENTER)
    stroke(255)
    strokeWidth(1)
    for i,b in ipairs(Buttons) do
        pushMatrix()
        pushStyle()
        translate(b.x,b.y)
        if U.button[b.name] then
            fill(128,0,0, 32) -- <---
        else
            fill(128,128,128,32) -- <---
        end
        ellipse(0,0, b.radius)
        fill(255)
        fontSize(30)
        text(b.name,0,0)
        popStyle()
        popMatrix()
    end
    popStyle()
end

That looks much better, good enough for now. Time to commit: “big turn button”.

What Else?

I’ve done a few experiments “off-line”, that is, on my other iPad, and have made some notes on things to do. They include:

  • Move the better sounds to the released zip file. This is easy but a pain. I’ll try to do it today, I promise. *Mod works better than I realized, which means the code to keep things on screen can be improved.
  • Asteroids need to start at an edge now that new waves are implemented. Otherwise they can rez on top of the ship, making the game irritating and unfair.
  • The ship top speed should be raised a bit.
  • The ship should coast to a stop.
  • The ship behaves as if it has zero cross-section: an asteroid has to run over the center to kill it.

Let’s do the mod thing. In Codea, x mod y is defined as:

x%y = x - math.floor(x/y)*y

I’m sure that’s as clear to you as it is to me. The good news is that that means that x%y is always between 0 and y-1. So we can use that fact here:

function Universe:moveObject(anObject)
    local pos = anObject.pos + self.processorRatio*anObject.step
    anObject.pos = vec2(self:keepInBounds(pos.x, WIDTH), self:keepInBounds(pos.y, HEIGHT))    
end

function Universe:keepInBounds(value, bound)
    return (value+bound)%bound
end

And that can become:

function Universe:moveObject(anObject)
    local pos = anObject.pos + self.processorRatio*anObject.step
    anObject.pos = vec2(pos.x%WIDTH, pos.y%HEIGHT)
end

The `keepInBounds’ function is now used only in the tests. Because I was uncertain about how mod worked, I’ve changed that test rather than delete it, as a verification of how Lua works:

        _:test("Verify mod works as advertised", function()
            _:expect(100%1000).is(100)
            _:expect(1000%1000).is(0)
            _:expect(1001%1000).is(1)
            _:expect(-1%1000).is(999)
        end)

So that item can be checked off. Anything else easy enough to do before lunch?

Top speed. I’ve tested speed and think that speed 6 instead of 3 is better in this code:

function Ship:actualShipMove()
    if U.button.go then
        U:playStereo(U.sounds.thrust, self)
        local accel = vec2(0.015,0):rotate(self.radians)
        self.step = self.step + accel
        self.step = maximize(self.step, 3)
    end
    self:finallyMove()
end

So I’ll set that 3 to 6. That reminds me, we should make that into a universal constant. But not today.

I guesstimated that speed based on watching Asteroids videos and observing how long it takes to get across the screen at what looked like full blast. I think it’s pretty close.

Our ship coasts forever. If the “go” button is not down, the ship should slow down. Apparently there is friction in outer space. Who knew?

We’ll try this:

function Ship:actualShipMove()
    if U.button.go then
        U:playStereo(U.sounds.thrust, self)
        local accel = vec2(0.015,0):rotate(self.radians)
        self.step = self.step + accel
        self.step = maximize(self.step, 6)
    else
        self.step = self.step*0.99
    end
    self:finallyMove()
end

So that’ll reduce speed to 99 percent of what it was, on every draw cycle. I’ll leave it to the reader to figure out when it’ll get to zero. Hint: never, but it’ll be pretty slow pretty fast.

0.99 brings you to nearly a halt in half the width of the screen. That’s too fast. I could fiddle with that number. It might be nice to apply a standard deceleration factor to the speed. We have acceleration of 0.015, and if I’m reading the 6502 code correctly, the deceleration is half the acceleration.

The thing is, even if we did create a deceleration vector and subtract it from speed, when would we stop? We can’t stop when the vector is zero, it may never get there.

I think we’re stuck with twiddling that 0.99 until we like it. I’ll do that and let you know what I come up with.

0.995 is about 3/4 of the screen to stop from full speed. Does that need to be scaled by the processor ratio? I suspect it does. I note, however, that we’re not scaling acceleration either.

Let’s think about this. Here’s how the ratio is defined:

    self.processorRatio = DeltaTime/0.0083333

So if the processor is slow, DeltaTime is larger and the ratio is larger. So surely the acceleration step should be larger:

        local accel = vec2(0.015,0):rotate(self.radians)*U.processorRatio

But what about the deceleration? Do we want it to be twice as much as well? Clearly not, since two times almost 1 is almost 2 so we’d speed up. What we “want” is to apply the deceleration twice if the ratio is two. So we want, I reason almost without foundation, our base deceleration to the power of the processor ratio.

So …

function Ship:actualShipMove()
    if U.button.go then
        U:playStereo(U.sounds.thrust, self)
        local accel = vec2(0.015,0):rotate(self.radians)*U.processorRatio
        self.step = self.step + accel
        self.step = maximize(self.step, 6)
    else
        self.step = self.step*(0.995^U.processorRatio)
    end
    self:finallyMove()
end

I’m not entirely sure of this. Despite two degrees in mathematics, I’m not very good at arithmetic and I’m often not all that sure about things like this. But I’m sure enough to release it and try it on my slower iPad and see what I get.

I think that’ll do it for today. Commit is: “improve bounds, accel/decel”.

Summing Up

Well. I like the new turning approach. I notice that there is a note for me on the Codea forum, @Bri_G has a different idea on controls for me to look at. We’ll see about that, maybe it’s a great one.

Everything went pretty smoothly today. That’s good, as there’s enough excitement in the world for me.

I’ll post this tomorrow. See you next time!

Zipped Code