“What shall we do today, Brain?”

Yes, well. First, let’s clean up the code from yesterday, and then see what opportunities present themselves. Oh, and I think it may be time to extend CodeaUnit to do a better job with floats. But first, the mess we have:

-- Asteroids
-- RJ 20200511

function setup()
    print("Hello Asteroids!")
    Asteroids = {}
    local asteroid = {}
    asteroid.pos = vec2(math.random(WIDTH), math.random(HEIGHT))
    asteroid.angle = math.random()*2*math.pi
    table.insert(Asteroids,asteroid)
    asteroid = {}
    asteroid.pos = vec2(math.random(WIDTH), math.random(HEIGHT))
    asteroid.angle = math.random()*2*math.pi
    table.insert(Asteroids,asteroid)
    Vel = 1.5
end

function draw()
    background(40, 40, 50)
    stroke(255)
    fill(40, 40,50)
    strokeWidth(2)
    rectMode(CENTER)
    for i,asteroid in ipairs(Asteroids) do
        rect(asteroid.pos.x, asteroid.pos.y, 120)
        local step = vec2(Vel,0):rotate(asteroid.angle)
        asteroid.pos = asteroid.pos + step
        if asteroid.pos.x > WIDTH then asteroid.pos.x = asteroid.pos.x - WIDTH end
        if asteroid.pos.x < 0 then asteroid.pos.x = asteroid.pos.x + WIDTH end
        if asteroid.pos.y > HEIGHT then asteroid.pos.y = asteroid.pos.y - HEIGHT end
        if asteroid.pos.y < 0 then asteroid.pos.y = asteroid.pos.y + HEIGHT end
    end
end

This needs a bit of work, including but not limited to:

  • Duplication in creating asteroids
  • Creating more than two …
  • Organize the draw function
  • improve the wrap-around logic

Let’s begin with creation to warm up our fingers and mind.

-- Asteroids
-- RJ 20200511

local Asteroids = {}
local Vel = 1.5

function setup()
    print("Hello Asteroids!")
    for i = 1,10 do
        table.insert(Asteroids, createAsteroid())
    end
end

function createAsteroid()
    local a = {}
    a.pos = vec2(math.random(WIDTH), math.random(HEIGHT))
    a.angle = math.random()*2*math.pi
    return a
end

I moved the formerly global Asteroids and Vel to be local to the Main tab, extracted creation from one of the in-line creations into createAsteroid(), with a shorter name for the local table, then called that function ten times to create ten asteroids.

ten asteroids

That works a treat. Commit “ten asteroids a-flying”.

Clean Up Draw

Now let’s clean up that draw function:

function draw()
    background(40, 40, 50)
    stroke(255)
    fill(40, 40,50)
    strokeWidth(2)
    rectMode(CENTER)
    for i,asteroid in ipairs(Asteroids) do
        rect(asteroid.pos.x, asteroid.pos.y, 120)
        local step = vec2(Vel,0):rotate(asteroid.angle)
        asteroid.pos = asteroid.pos + step
        if asteroid.pos.x > WIDTH then asteroid.pos.x = asteroid.pos.x - WIDTH end
        if asteroid.pos.x < 0 then asteroid.pos.x = asteroid.pos.x + WIDTH end
        if asteroid.pos.y > HEIGHT then asteroid.pos.y = asteroid.pos.y - HEIGHT end
        if asteroid.pos.y < 0 then asteroid.pos.y = asteroid.pos.y + HEIGHT end
    end
end

The draw function has a number of oddities that we can improve:

  • The adjustment of pos to be in bounds is naive. I wrote it that way because I could do it without thinking very hard, but it’s not at all nifty. I have an idea about it.
  • The inside of the loops that draws the asteroids should be extracted to a separate function.
  • We could use a more clear separation of the setup of the general display, and that of the asteroids. Codea offers some organizing functions and we should use them.

I think what I’ll do is extract the whole loop that draws the asteroids, and then refine from there.

Wait! Look again at the draw function. Everything in it, except for the background setting, is about drawing the asteroids. It may be overkill, but the right thing to do is to extract all that:

function draw()
    background(40, 40, 50)
    drawAsteroids()
end

function drawAsteroids()
    stroke(255)
    fill(40, 40,50)
    strokeWidth(2)
    rectMode(CENTER)
    for i,asteroid in ipairs(Asteroids) do
        rect(asteroid.pos.x, asteroid.pos.y, 120)
        local step = vec2(Vel,0):rotate(asteroid.angle)
        asteroid.pos = asteroid.pos + step
        if asteroid.pos.x > WIDTH then asteroid.pos.x = asteroid.pos.x - WIDTH end
        if asteroid.pos.x < 0 then asteroid.pos.x = asteroid.pos.x + WIDTH end
        if asteroid.pos.y > HEIGHT then asteroid.pos.y = asteroid.pos.y - HEIGHT end
        if asteroid.pos.y < 0 then asteroid.pos.y = asteroid.pos.y + HEIGHT end
    end
end

Now I think I’ll extract the inside of the loop:

function drawAsteroids()
    stroke(255)
    fill(40, 40,50)
    strokeWidth(2)
    rectMode(CENTER)
    for i,asteroid in ipairs(Asteroids) do
        drawAsteroid(asteroid)
    end
end

function drawAsteroid(asteroid)
    rect(asteroid.pos.x, asteroid.pos.y, 120)
    local step = vec2(Vel,0):rotate(asteroid.angle)
    asteroid.pos = asteroid.pos + step
    if asteroid.pos.x > WIDTH then asteroid.pos.x = asteroid.pos.x - WIDTH end
    if asteroid.pos.x < 0 then asteroid.pos.x = asteroid.pos.x + WIDTH end
    if asteroid.pos.y > HEIGHT then asteroid.pos.y = asteroid.pos.y - HEIGHT end
    if asteroid.pos.y < 0 then asteroid.pos.y = asteroid.pos.y + HEIGHT end
end

I did run the program after each of these extractions and it still works as advertised. Now, inside draw, we have the drawing bit and the position adjusting bit. That offers an immediate issue and a larger one.

The larger issue is that it’s often a good idea to draw everything, and then adjust everything. If you intermix drawing and adjusting, it’s possible to get odd effects like bullets coming out offset from the ship or shots appearing to hit but not scoring. I’m not going to worry about that yet, but will try to keep it in mind.

The smaller issue is just to clean up that adjustment code. I’m going to break it out first:

function drawAsteroid(asteroid)
    rect(asteroid.pos.x, asteroid.pos.y, 120)
    local step = vec2(Vel,0):rotate(asteroid.angle)
    moveAsteroid(asteroid)
end

function moveAsteroid(asteroid)
    asteroid.pos = asteroid.pos + step
    if asteroid.pos.x > WIDTH then asteroid.pos.x = asteroid.pos.x - WIDTH end
    if asteroid.pos.x < 0 then asteroid.pos.x = asteroid.pos.x + WIDTH end
    if asteroid.pos.y > HEIGHT then asteroid.pos.y = asteroid.pos.y - HEIGHT end
    if asteroid.pos.y < 0 then asteroid.pos.y = asteroid.pos.y + HEIGHT end
end

Right, well, that didn’t quite work. I needed to move the calculation of step:

function moveAsteroid(asteroid)
    local step = vec2(Vel,0):rotate(asteroid.angle)
    asteroid.pos = asteroid.pos + step
    if asteroid.pos.x > WIDTH then asteroid.pos.x = asteroid.pos.x - WIDTH end
    if asteroid.pos.x < 0 then asteroid.pos.x = asteroid.pos.x + WIDTH end
    if asteroid.pos.y > HEIGHT then asteroid.pos.y = asteroid.pos.y - HEIGHT end
    if asteroid.pos.y < 0 then asteroid.pos.y = asteroid.pos.y + HEIGHT end
end

That works. Time to commit: “break out functions”.

Well, this is nice. I can immediately move that call to moveAsteroid up, into the draw loop. I think this makes more sense:

function draw()
    background(40, 40, 50)
    drawAsteroids()
end

function drawAsteroids()
    stroke(255)
    fill(40, 40,50)
    strokeWidth(2)
    rectMode(CENTER)
    for i,asteroid in ipairs(Asteroids) do
        drawAsteroid(asteroid)
        moveAsteroid(asteroid)
    end
end

function drawAsteroid(asteroid)
    rect(asteroid.pos.x, asteroid.pos.y, 120)
end

function moveAsteroid(asteroid)
    local step = vec2(Vel,0):rotate(asteroid.angle)
    asteroid.pos = asteroid.pos + step
    if asteroid.pos.x > WIDTH then asteroid.pos.x = asteroid.pos.x - WIDTH end
    if asteroid.pos.x < 0 then asteroid.pos.x = asteroid.pos.x + WIDTH end
    if asteroid.pos.y > HEIGHT then asteroid.pos.y = asteroid.pos.y - HEIGHT end
    if asteroid.pos.y < 0 then asteroid.pos.y = asteroid.pos.y + HEIGHT end
end

Everything still good. Better commit again: “call move in draw loop”.

There is a bit of a style issue here, whether we like two calls in that loop or not but it is so nuanced that I’m not going to mention it.

Let’s improve that keep on the screen code. For this I plan to write a test, because I’m sure this idea will work but wouldn’t bet my career or cat on it.

The idea is that an expression like this should keep a value in bounds [0,bound):

value = (value + bound)%bound

I reason as follows. value%bound will move value back inside the bound if it is positive and beyond the bound, otherwise leave it alone. Since our values will only go a “little bit” negative, just the size of a step, value+bound is either within bounds or beyond. So the expression should always limit us to the bound. Therefore:

Well, therefore, we need a test because if you have to reason that much to justify something, your friends may not get it and even if they do, they, too, could be wrong. Let’s see:

        _:test("Bounds function", function()
            _:expect(putInBounds(100,1000)).is(100)
            _:expect(putInBounds(1000,1000)).is(0)
            _:expect(putInBounds(1001,1000)).is(1)
            _:expect(putInBounds(-1,1000)).is(999)
        end)

function putInBounds(value, bound)
    return (value+bound)%bound
end

Those tests pass, so I can make this change:

function moveAsteroid(asteroid)
    local step = vec2(Vel,0):rotate(asteroid.angle)
    local pos = asteroid.pos + step
    asteroid.pos = vec2(putInBounds(pos.x, WIDTH), putInBounds(pos.y, HEIGHT))
end

Time to commit, in fact probably past time. Commit “putInBounds”.

Here’s the whole main tab for review:

-- Asteroids
-- RJ 20200511

local Asteroids = {}
local Vel = 1.5

function setup()
    print("Hello Asteroids!")
    for i = 1,10 do
        table.insert(Asteroids, createAsteroid())
    end
end

function createAsteroid()
    local a = {}
    a.pos = vec2(math.random(WIDTH), math.random(HEIGHT))
    a.angle = math.random()*2*math.pi
    return a
end

function draw()
    background(40, 40, 50)
    drawAsteroids()
end

function drawAsteroids()
    stroke(255)
    fill(40, 40,50)
    strokeWidth(2)
    rectMode(CENTER)
    for i,asteroid in ipairs(Asteroids) do
        drawAsteroid(asteroid)
        moveAsteroid(asteroid)
    end
end

function drawAsteroid(asteroid)
    rect(asteroid.pos.x, asteroid.pos.y, 120)
end

function moveAsteroid(asteroid)
    local step = vec2(Vel,0):rotate(asteroid.angle)
    local pos = asteroid.pos + step
    asteroid.pos = vec2(putInBounds(pos.x, WIDTH), putInBounds(pos.y, HEIGHT))
end

function putInBounds(value, bound)
    return (value+bound)%bound
end

What elsel? I mentioned doing graphics in a more organized way. Let’s think about that.

As we draw lots of different things, we often need to fiddle with various drawing parameters. Remember that when I changed from a circle asteroid to a square, I needed to adjust the stroke width. Similarly, we may need to adjust colors and so on, if we ever move away from our current 1979 monochrome display.

Codea offers two pairs of functions to push and pop style values and the current transformation matrix. We’re not using the matrix yet but we very shortly will. And we’re certainly using styles. Look at drawAsteroids:

function drawAsteroids()
    stroke(255)
    fill(40, 40,50)
    strokeWidth(2)
    rectMode(CENTER)
    for i,asteroid in ipairs(Asteroids) do
        drawAsteroid(asteroid)
        moveAsteroid(asteroid)
    end
end

We set up a stroke color and width, a fill color, and a rectangle mode … and we don’t set them back. That means that some other bit of drawing may find itself using a stroke or color that it doesn’t want.

Now we could argue that everyone should set up whatever they want, and they should. However, it’s rude to change things and leave them changed when we can easily put them back, like this:

function draw()
    pushStyle()
    background(40, 40, 50)
    drawAsteroids()
    popStyle()
end

function drawAsteroids()
    pushStyle()
    stroke(255)
    fill(40, 40,50)
    strokeWidth(2)
    rectMode(CENTER)
    for i,asteroid in ipairs(Asteroids) do
        drawAsteroid(asteroid)
        moveAsteroid(asteroid)
    end
    popStyle()
end

If we push and pop style (and matrix if we use it) in all our functions that change styles around, our program is less vulnerable to surprise changes.

Right now it seems like we’re safe. But hold on for a hypothetical but realistic possibility.

Our game, if we follow the original, will display the score at the top of the screen. Suppose that we were to set up the score to use a white fill color for the text. Our asteroids use a grey fill to match the background.

Now suppose the ship shots an asteroid. We detect that as we loop over the asteroids drawing them and we split the asteroid and update the score. Then we continue drawing … and all the rest of the asteroids get a white fill.

Now we could argue that everyone should have been more careful, or that things should have been done in another order. We can “should” all over. But pushing and popping as a matter of course avoids all these concerns.

Pushing and popping is the neighborly thing to do. So we’ll start doing it. Even so we may forget, but we’ll at least try to be good neighbors.

Commit: “push/pop style”.

Go back and take a look at the video with the ten square asteroids. Note that the asteroids seem to pass in front of one another. This is because they are filled with the same color as the background, so that later-drawn ones appear to be on top.

Well, that’s wrong. Why? Because the original Asteroids was written on a vector display, and so it only drew the lines, not the background. To get that effect, let’s change our asteroid backgrounds to be transparent. That’s the fill() function:

function drawAsteroids()
    pushStyle()
    stroke(255)
    fill(0,0,0, 0)
    strokeWidth(2)
    rectMode(CENTER)
    for i,asteroid in ipairs(Asteroids) do
        drawAsteroid(asteroid)
        moveAsteroid(asteroid)
    end
    popStyle()
end

Note the new fill is fill(0,0,0, 0). The fourth parameter is opacity and zero is fully transparent. So now it looks like this:

ten transparent

It looks less good, certainly, but it’s more authentic. We’re doing an homage to the 1979 Asteroids, so that’s what we want. Commit: “transparent squares”.

Summing Up

I was planning to put a little spin on the asteroids as they move along, but a review of the original game footage tells me that the asteroids didn’t spin. Now, I do plan to extend our game beyond the original but only after we get “close enough” to the original. Then we’ll see how easy or hard it is to extend our little program.

So we’ll close here today on our improved code and next time we’d better start working on the ship.

See you then, I hope!