I think it’s time to move more toward objects. Part of our mission is to decide what we like. And: ‘No large refactorings!’

I started building Asteroids in a procedural style, because I think that many beginners start that way. My normal practice would have been to go straight to independent objects. Now I’m going to begin to move toward the more object-oriented design. I’m sure it’ll turn out more to my liking. There are at least two things for you to consider.

First, I think we’ll find it easy enough to move things into objects. So it’s worth thinking about whether, in your work, you might be able to improve the design similarly, even such a “major” change as procedural to object-oriented.

Second, since we’ll see the code both ways, we have the opportunity to compare the two forms and get a feeling for which we prefer, and why.

Let’s begin with a review of Missile, which is the closest we have to a decent moving object. Looking at the whole tab, we see this:

-- Missile
-- RJ 20200522

Missiles = {}

function drawMissiles()
    pushStyle()
    pushMatrix()
    fill(255)
    stroke(255)
    for k, missile in pairs(Missiles) do
        missile:draw()
    end
    popMatrix()
    popStyle()
    for k, missile in pairs(Missiles) do
        missile:move()
    end
end

Missile = class()

local MissileVelocity = 2.0

function Missile:init(ship)
    function die()
        self:die()
    end
    self.pos = ship.pos
    self.step = vec2(MissileVelocity,0):rotate(ship.radians) + ship.step
    Missiles[self] = self
    tween(3, self, {}, tween.easing.linear, die)
end

function Missile:die()
    Missiles[self] = nil
end

function Missile:draw()
    ellipse(self.pos.x, self.pos.y, 6)
end

function Missile:move()
    U:moveObject(self)
end

The class itself is pretty simple. It initializes a missile, adds it to the Missiles collection, draws itself, and moves, by calling our new Universal mover. And it dies, by removing itself from the Missiles collection.

There’s some other stuff up there, the Missiles collection and a function that draws all the missiles and moves them. That function is called in Main right now.

We have a nascent new Universe object, and it seems to me that it should be handling things like holding collections, and moving and drawing them. Let’s begin by moving that functionality to Universe, which will make the whole, um, universe more object oriented.

Main tab has this:

function draw()
    Ratio = DeltaTime/0.0083333
    --displayMode(FULLSCREEN_NO_BUTTONS)
    checkButtons()
    pushStyle()
    background(40, 40, 50)
    U:draw()
    drawButtons()
    drawShip()
    moveShip()
    drawMissiles()
    drawSplats()
    drawScore()
    popStyle()
    U:findCollisions()
end

Let’s do the simple step first, of changing Main to ask Universe to draw the missiles:

  U:drawMissiles()

Now if we move that global function into Universe, we should be good to go:

function Universe:drawMissiles()
    pushStyle()
    pushMatrix()
    fill(255)
    stroke(255)
    for k, missile in pairs(Missiles) do
        missile:draw()
    end
    popMatrix()
    popStyle()
    for k, missile in pairs(Missiles) do
        missile:move()
    end
end

That works just as one would hope. Remember that Universe already contains the asteroids collection?

function Universe:init()
    self.asteroids = {}
end

Let’s remove the global Missiles and use a collection missiles in Universe:

-- Universe
-- RJ 20200523

Universe = class()

function Universe:init()
    self.asteroids = {}
    self.missiles = {}
end

function Universe:draw()
    drawAsteroids(self.asteroids)
end

function Universe:createAsteroids()
    createAsteroids(self.asteroids)
end

function Universe:findCollisions()
    for i,a in pairs(self.asteroids) do
        for k,m in pairs(self.missiles) do -- CHANGED
            if m.pos:dist(a.pos) < killDist(a) then
                scoreAsteroid(a)
                splitAsteroid(a, self.asteroids)
                m:die()
            end
        end
    end
end

function Universe:drawMissiles()
    pushStyle()
    pushMatrix()
    fill(255)
    stroke(255)
    for k, missile in pairs(self.missiles) do -- CHANGED
        missile:draw()
    end
    popMatrix()
    popStyle()
    for k, missile in pairs(self.missiles) do -- CHANGED
        missile:move()
    end
end

Then we’ll have to use that collection in Missiles tab and remove the global one:

-- Missile
-- RJ 20200522

Missile = class()

local MissileVelocity = 2.0

function Missile:init(ship)
    function die()
        self:die()
    end
    self.pos = ship.pos
    self.step = vec2(MissileVelocity,0):rotate(ship.radians) + ship.step
    U.missiles[self] = self -- CHANGED
    tween(3, self, {}, tween.easing.linear, die)
end

function Missile:die()
    U.missiles[self] = nil -- CHANGED
end

Works a treat. One more thing, let’s move MissileVelocity into the Universe as well, and make it a vec2 while we’re at it. It should be anyway, velocity is a vector. Speed is unitary.

local MissileSpeed = 2.0

function Universe:init()
    self.asteroids = {}
    self.missiles = {}
    self.missileVelocity = vec2(MissileSpeed,0)
end

And in Missile:

function Missile:init(ship)
    function die()
        self:die()
    end
    self.pos = ship.pos
    self.step = U.missileVelocity:rotate(ship.radians) + ship.step
    U.missiles[self] = self
    tween(3, self, {}, tween.easing.linear, die)
end

Everything still seems to work. (I emphasize seems. I’m feeling the need for better tests. Maybe next time, maybe later today.)

Anyway, our work here on Missile is done. Commit: “Moved missile globals to Universe, draw from Universe”.

Here’s all of Missile:

-- Missile
-- RJ 20200522

Missile = class()

function Missile:init(ship)
    function die()
        self:die()
    end
    self.pos = ship.pos
    self.step = U.missileVelocity:rotate(ship.radians) + ship.step
    U.missiles[self] = self
    tween(3, self, {}, tween.easing.linear, die)
end

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

function Missile:draw()
    ellipse(self.pos.x, self.pos.y, 6)
end

function Missile:move()
    U:moveObject(self)
end

Very clean class, nothing fancy. There are things not to like, however.

As written, Missile knows that missiles are stored in a collection named U.missiles, and it manipulates those collections directly. That’s a bit invasive: Universe owns those collections, and it would be better if it managed them on its own.

Why? Well, I happen to know that the original Asteroids kept all moving objects in a single table. I don’t like that idea for us, but if we did decide to go that way, we’d have to change all of Missile, Asteroid, Splat, and Ship to accomplish it. That tells us that Missile is probably too closely coupled to Universe.

We could improve that, and perhaps one day soon we will. For now, I think we have bigger, or at least different, fish to fry.

I expect converting Asteroid to be easy and Ship to be less easy. So let’s do Asteroid. Maybe a meteor will strike before we have to do Ship.

Asteroid

Asteroid looks like this:

-- Asteroid
-- RJ 20200520

local DeadAsteroids = {}
local Vel = 1.5

function createAsteroids(asteroids)
    for i = 1,4 do
        local a = createAsteroid()
        asteroids[a] = a
    end
end

function createAsteroid()
    local a = {}
    a.pos = vec2(math.random(WIDTH), math.random(HEIGHT))
    a.shape = Rocks[math.random(1,4)]
    a.scale = 16
    local angle = math.random()*2*math.pi
    a.step = Ratio*vec2(Vel,0):rotate(angle)
    return a
end

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

function killDist(asteroid)
    local s = asteroid.scale
    if s == 16 then return 64 elseif s == 8 then return 32 else return 16 end
end

function killDeadAsteroids(asteroids)
    for k,a in pairs(DeadAsteroids) do
        asteroids[a] = nil
    end
    DeadAsteroids = {}
end

function deathSize()
    local i = 0
    for k, a in pairs(DeadAsteroids) do
        i = i + 1
    end
    return i
end

function scoreAsteroid(asteroid)
    local s = asteroid.scale
    local inc = 0
    if s == 16 then inc = 20
    elseif s == 8 then inc = 50
    else inc = 100
    end
    Score = Score + inc
end

function splitAsteroid(asteroid, asteroids)
    if asteroid.scale == 4 then
        Splat(asteroid.pos)
        DeadAsteroids[asteroid] = asteroid
        return
    end
    asteroid.scale = asteroid.scale//2
    asteroid.angle = math.random()*2*math.pi
    local new = createAsteroid()
    new.pos = asteroid.pos
    new.scale = asteroid.scale
    asteroids[new] = new
    Splat(asteroid.pos)
end

function drawAsteroid(asteroid)
    pushMatrix()
    pushStyle()
    translate(asteroid.pos.x, asteroid.pos.y)
    ellipse(0,0,2*killDist(asteroid))
    scale(asteroid.scale)
    strokeWidth(1/asteroid.scale)
    for i,l in ipairs(asteroid.shape) do
        line(l.x, l.y, l.z, l.w)
    end
    popStyle()
    popMatrix()
end

function moveAsteroid(asteroid)
    U:moveObject(asteroid)
end

The asteroid functionality is entirely procedural at this point. We already had Missile as a class. Here, we get to convert to a class.

We have functions score-, split-, draw-, move- (Asteroid), and those will surely be our object’s methods. This time I’m going to work within this tab, converting as little as possible at a time, while keeping things working.

(Unless that blows up in my hands right away, in which case we’ll devise Plan B.)

A glance at Main tells me that Universe already creates the asteroids table and draws them. Let’s see again what it does that involves asteroids:

-- Universe
-- RJ 20200523

Universe = class()

local MissileSpeed = 2.0

function Universe:init()
    self.asteroids = {}
    self.missiles = {}
    self.missileVelocity = vec2(MissileSpeed,0)
end

function Universe:draw()
    drawAsteroids(self.asteroids)
end

function Universe:createAsteroids()
    createAsteroids(self.asteroids)
end

function Universe:findCollisions()
    for i,a in pairs(self.asteroids) do
        for k,m in pairs(self.missiles) do
            if m.pos:dist(a.pos) < killDist(a) then
                scoreAsteroid(a)
                splitAsteroid(a, self.asteroids)
                m:die()
            end
        end
    end
end

function Universe:drawMissiles()
    pushStyle()
    pushMatrix()
    fill(255)
    stroke(255)
    for k, missile in pairs(self.missiles) do
        missile:draw()
    end
    popMatrix()
    popStyle()
    for k, missile in pairs(self.missiles) do
        missile:move()
    end
end

It creates them on demand, passing each asteroid the collection of all of them. Same issue as with Missiles, but a bit better contained. Then it calls drawAsteroids, which is presently in the Asteroid tab, and then things get even more odd.

At first it seems daunting. But I don’t think it really is. First, we’ll import drawAsteroids into Universe as a method:

function Universe:drawAsteroids()
    pushStyle()
    stroke(255)
    fill(0,0,0, 0)
    strokeWidth(2)
    rectMode(CENTER)
    for i,asteroid in pairs(self.asteroids) do
        drawAsteroid(asteroid)
        moveAsteroid(asteroid)
    end
    popStyle()
    killDeadAsteroids(self.asteroids)
end

Let’s see why that doesn’t work.

function Universe:draw()
    drawAsteroids(self.asteroids)
end

Should be:

function Universe:draw()
    self:drawAsteroids()
end

Asteroids fly and die just fine again.

Note This

Note that we could commit this code right now. Everything is working. We moved one method from a global function into a Universe method. We’re one step closer to object-oriented, and if it were time for supper, we could stop right now.

But we have a few more minutes. Let’s see if we can create theAsteroid class and use it. I’ll try to be as non-invasive as I can.

Inserting the Asteroid = class() line above the global createAsteroid`, I see this:

Asteroid = class()

function createAsteroid()
    local a = {}
    a.pos = vec2(math.random(WIDTH), math.random(HEIGHT))
    a.shape = Rocks[math.random(1,4)]
    a.scale = 16
    local angle = math.random()*2*math.pi
    a.step = Ratio*vec2(Vel,0):rotate(angle)
    return a
end

createAsteroid should be the new init and whoever calls it should instead create an asteroid instance. Here, we have this:

function Asteroid:init()
    self.pos = vec2(math.random(WIDTH), math.random(HEIGHT))
    self.shape = Rocks[math.random(1,4)]
    self.scale = 16
    local angle = math.random()*2*math.pi
    self.step = Ratio*vec2(Vel,0):rotate(angle)
end

You can see what I did. I don’t allocate my local a because instance creation does that for me. And instead of referring to a, I refer to self. Now to fix whoever called createAsteroid. Turns out that’s presently in the Asteroid tab:

function createAsteroids(asteroids)
    for i = 1,4 do
        local a = createAsteroid()
        asteroids[a] = a
    end
end

We change it:

function createAsteroids(asteroids)
    for i = 1,4 do
        local a = Asteroid()
        asteroids[a] = a
    end
end

A quick test or text search shows me:

function splitAsteroid(asteroid, asteroids)
    if asteroid.scale == 4 then
        Splat(asteroid.pos)
        DeadAsteroids[asteroid] = asteroid
        return
    end
    asteroid.scale = asteroid.scale//2
    asteroid.angle = math.random()*2*math.pi
    local new = createAsteroid()
    new.pos = asteroid.pos
    new.scale = asteroid.scale
    asteroids[new] = new
    Splat(asteroid.pos)
end

Which becomes:

function splitAsteroid(asteroid, asteroids)
    if asteroid.scale == 4 then
        Splat(asteroid.pos)
        DeadAsteroids[asteroid] = asteroid
        return
    end
    asteroid.scale = asteroid.scale//2
    asteroid.angle = math.random()*2*math.pi
    local new = Asteroid() -- CHANGED
    new.pos = asteroid.pos
    new.scale = asteroid.scale
    asteroids[new] = new
    Splat(asteroid.pos)
end

Everything works again. We could commit the code and release it to users and it works as well as it ever did. In fact I will commit: “asteroid class created”.

I think now I want to go after draw:


function drawAsteroid(asteroid)
    pushMatrix()
    pushStyle()
    translate(asteroid.pos.x, asteroid.pos.y)
    ellipse(0,0,2*killDist(asteroid))
    scale(asteroid.scale)
    strokeWidth(1/asteroid.scale)
    for i,l in ipairs(asteroid.shape) do
        line(l.x, l.y, l.z, l.w)
    end
    popStyle()
    popMatrix()
end

This converts to a method on Asteroid by renaming and referring to self instead of asteroid:

function Asteroid:draw()
    pushMatrix()
    pushStyle()
    translate(self.pos.x, self.pos.y)
    --ellipse(0,0,2*killDist(self.))
    scale(self.scale)
    strokeWidth(1/self.scale)
    for i,l in ipairs(self.shape) do
        line(l.x, l.y, l.z, l.w)
    end
    popStyle()
    popMatrix()
end

I converted but then commented out the ellipse code because I’m tired of seeing those circles I drew to see how well a circular kill distance works.

Universe has to call our new method:

function Universe:drawAsteroids()
    pushStyle()
    stroke(255)
    fill(0,0,0, 0)
    strokeWidth(2)
    rectMode(CENTER)
    for i,asteroid in pairs(self.asteroids) do
        asteroid:draw()
        moveAsteroid(asteroid)
    end
    popStyle()
    killDeadAsteroids(self.asteroids)
end

Everything works again! Commit: “asteroid draw method”.

Notice what tiny steps we’re able to make. This is nearly always possible and it’s why I like to say that there are no large refactorings (only lots of little tiny ones).

Let’s do move:

function Universe:drawAsteroids()
    pushStyle()
    stroke(255)
    fill(0,0,0, 0)
    strokeWidth(2)
    rectMode(CENTER)
    for i,asteroid in pairs(self.asteroids) do
        asteroid:draw()
        asteroid:move() -- CHANGED
    end
    popStyle()
    killDeadAsteroids(self.asteroids)
end

Easy. This:

function moveAsteroid(asteroid)
    U:moveObject(asteroid)
end

Becomes this:

function Asteroid:move()
    U:moveObject(self)
end

Everything runs. Commit: “asteroid move method”.

Let’s see what else odd is in Universe. This is interesting:

function Universe:createAsteroids()
    createAsteroids(self.asteroids)
end

This calls the create loop in Asteroid:

function createAsteroids(asteroids)
    for i = 1,4 do
        local a = Asteroid()
        asteroids[a] = a
    end
end

That should be moved over into Universe thusly:

function Universe:createAsteroids()
    for i = 1,4 do
        local a = Asteroid()
        self.asteroids[a] = a
    end
end

Everything works. Commit: “move asteroid creation to universe”.

Getting Tired?

Are you getting tired? Are things moving too fast for you? I’m not tired and they’re not moving too fast. And if you were here pairing with me, fully engaged, I don’t think you’d be as tired as you may be right now. How about we do one more quick improvement and then break.

No, two. I found this function that is never called:

function deathSize()
    local i = 0
    for k, a in pairs(DeadAsteroids) do
        i = i + 1
    end
    return i
end

Removed it. That doesn’t count. Commit: “removed unused deathSize”. Moving right along, in Asteroid tab we have this function:

function killDist(asteroid)
    local s = asteroid.scale
    if s == 16 then return 64 elseif s == 8 then return 32 else return 16 end
end

This is clearly a dynamic property of the asteroid and should be a method:

function Asteroid:killDist()
    local s = self.scale
    if s == 16 then return 64 elseif s == 8 then return 32 else return 16 end
end

It’s only called twice and we change those:

function Universe:findCollisions()
    for i,a in pairs(self.asteroids) do
        for k,m in pairs(self.missiles) do
            if m.pos:dist(a.pos) < a:killDist() then -- CHANGED
                scoreAsteroid(a)
                splitAsteroid(a, self.asteroids)
                m:die()
            end
        end
    end
end

The second reference was in that ellipse statement that drew the circle I don’t want. I removed the line from the method:

function Asteroid:draw()
    pushMatrix()
    pushStyle()
    translate(self.pos.x, self.pos.y)
    scale(self.scale)
    strokeWidth(1/self.scale)
    for i,l in ipairs(self.shape) do
        line(l.x, l.y, l.z, l.w)
    end
    popStyle()
    popMatrix()
end

It all still works. Except for summing up, we’re done now. Commit: “added Asteroid:killDist()”.

Summing Up

I was thinking that the Asteroid tab would be the easier to convert to object-oriented, and I may have been mistaken. I had forgotten all the activity around scoring and splitting.

Be that as it may, we have taken a number of good steps toward moving things to Universe that belong there, and to converting various global Asteroid-related functions into nice local member functions on our new class Asteroid. There is more to do, but we’ve done a lot for now.

What’s most important to notice, I suggest, is that we moved from a working version to another working version in very small steps, usually consisting of renaming a function to be a member function of Universe or Asteroid, and then using that function. There were usually nor more than two or three simple changes to make.

At no time were we confused. Even when we missed a change, it took only a moment to see what was needed.

If someone were to tell us that they had a procedural program that needed conversion to objects in order to be cleaner, we might be concerned that it would be a big deal. It’s turning out to be a large number of very small deals.

That’s a much better situation, and it’s almost always the case that we can improve a program in tiny safe steps. And if we can do that … it’s probably the way to go.

See you next time, or address me on Twitter!

Here’s the code:



--# Main
-- Asteroids
-- RJ 20200511

Touches = {}
Ratio = 1.0 -- draw time scaling ratio
Score = 0

function setup()
    U = Universe()
    U:createAsteroids()
    Score = 0
    --displayMode(FULLSCREEN_NO_BUTTONS)
    createButtons()
    createShip()
end

function draw()
    Ratio = DeltaTime/0.0083333
    --displayMode(FULLSCREEN_NO_BUTTONS)
    checkButtons()
    pushStyle()
    background(40, 40, 50)
    U:draw()
    drawButtons()
    drawShip()
    moveShip()
    U:drawMissiles()
    drawSplats()
    drawScore()
    popStyle()
    U:findCollisions()
end

function drawScore()
    local s= "000000"..tostring(Score)
    s = string.sub(s,-5)
    fontSize(100)
    text(s, 200, HEIGHT-60)
end

function touched(touch)
    if touch.state == ENDED or touch.state == CANCELLED then
        Touches[touch.id] = nil
    else
        Touches[touch.id] = touch
    end
end

--# TestAsteroids
-- TestAsteroids
-- RJ 20200511
    
function testAsteroids()
    CodeaUnit.detailed = true

    _:describe("Asteroids First Tests", function()

        _:before(function()
            -- Some setup
        end)

        _:after(function()
            -- Some teardown
        end)
        
        _:test("Hookup", function()
            _:expect( 2+1 ).is(3)
        end)
        
        _:test("Random", function()
            local min = 100
            local max = 0
            for i = 0,1000 do
                local rand = math.random()*2*math.pi
                if rand < min then min = rand end
                if rand > max then max = rand end
            end
            _:expect(min < 0.01).is(true)
            _:expect(max > 6.2).is(true)
        end)
        
        _:test("Rotated Length", function()
            for i = 0, 1000 do
                local rand = math.random()*2*math.pi
                local v = vec2(1.5,0):rotate(rand)
                local d = v:len()
                _:expect(d > 1.495).is(true)
                _:expect(d < 1.505).is(true)
            end
        end)
        
        _:test("Some rotates go down", function()
            local angle = math.rad(-45)
            local v = vec2(1,0):rotate(angle)
            local rvx = v.x*1000//1
            local rvy = v.y*1000//1
            _:expect(rvx).is(707)
            _:expect(rvy).is(-708)
        end)
        
        _:test("Bounds function", function()
            _:expect(U:keepInBounds(100,1000)).is(100)
            _:expect(U:keepInBounds(1000,1000)).is(0)
            _:expect(U:keepInBounds(1001,1000)).is(1)
            _:expect(U:keepInBounds(-1,1000)).is(999)
        end)

    end)
end

--# Shapes
RR1 = {
  vec4(0.000000, 2.000000, 2.000000, 4.000000),
  vec4(2.000000, 4.000000, 4.000000, 2.000000),
  vec4(4.000000, 2.000000, 3.000000, 0.000000),
  vec4(3.000000, 0.000000, 4.000000, -2.000000),
  vec4(4.000000, -2.000000, 1.000000, -4.000000),
  vec4(1.000000, -4.000000, -2.000000, -4.000000),
  vec4(-2.000000, -4.000000, -4.000000, -2.000000),
  vec4(-4.000000, -2.000000, -4.000000, 2.000000),
  vec4(-4.000000, 2.000000, -2.000000, 4.000000),
  vec4(-2.000000, 4.000000, 0.000000, 2.000000)
}

RR2 = {
  vec4(2.000000, 1.000000, 4.000000, 2.000000),
  vec4(4.000000, 2.000000, 2.000000, 4.000000),
  vec4(2.000000, 4.000000, 0.000000, 3.000000),
  vec4(0.000000, 3.000000, -2.000000, 4.000000),
  vec4(-2.000000, 4.000000, -4.000000, 2.000000),
  vec4(-4.000000, 2.000000, -3.000000, 0.000000),
  vec4(-3.000000, 0.000000, -4.000000, -2.000000),
  vec4(-4.000000, -2.000000, -2.000000, -4.000000),
  vec4(-2.000000, -4.000000, -1.000000, -3.000000),
  vec4(-1.000000, -3.000000, 2.000000, -4.000000),
  vec4(2.000000, -4.000000, 4.000000, -1.000000),
  vec4(4.000000, -1.000000, 2.000000, 1.000000)
}

RR3 = {
  vec4(-2.000000, 0.000000, -4.000000, -1.000000),
  vec4(-4.000000, -1.000000, -2.000000, -4.000000),
  vec4(-2.000000, -4.000000, 0.000000, -1.000000),
  vec4(0.000000, -1.000000, 0.000000, -4.000000),
  vec4(0.000000, -4.000000, 2.000000, -4.000000),
  vec4(2.000000, -4.000000, 4.000000, -1.000000),
  vec4(4.000000, -1.000000, 4.000000, 1.000000),
  vec4(4.000000, 1.000000, 2.000000, 4.000000),
  vec4(2.000000, 4.000000, -1.000000, 4.000000),
  vec4(-1.000000, 4.000000, -4.000000, 1.000000),
  vec4(-4.000000, 1.000000, -2.000000, 0.000000)
}

RR4 = {
  vec4(1.000000, 0.000000, 4.000000, 1.000000),
  vec4(4.000000, 1.000000, 4.000000, 2.000000),
  vec4(4.000000, 2.000000, 1.000000, 4.000000),
  vec4(1.000000, 4.000000, -2.000000, 4.000000),
  vec4(-2.000000, 4.000000, -1.000000, 2.000000),
  vec4(-1.000000, 2.000000, -4.000000, 2.000000),
  vec4(-4.000000, 2.000000, -4.000000, -1.000000),
  vec4(-4.000000, -1.000000, -2.000000, -4.000000),
  vec4(-2.000000, -4.000000, 1.000000, -3.000000),
  vec4(1.000000, -3.000000, 2.000000, -4.000000),
  vec4(2.000000, -4.000000, 4.000000, -2.000000),
  vec4(4.000000, -2.000000, 1.000000, 0.000000)
}

Rocks = {RR1,RR2,RR3,RR4}
--# Ship
-- Ship
-- RJ 20200520

local Ship = {}

local rotationStep = math.rad(1) -- one degree in radians

function createShip()
    Ship.pos = vec2(WIDTH, HEIGHT)/2
    Ship.radians = 0
    Ship.step = vec2(0,0)
end

function drawShip()
    local sx = 10
    local sy = 6
    pushStyle()
    pushMatrix()
    translate(Ship.pos.x, Ship.pos.y)
    rotate(math.deg(Ship.radians))
    strokeWidth(2)
    stroke(255)
    line(sx,0, -sx,sy)
    line(-sx,sy, -sx,-sy)
    line(-sx,-sy, sx,0)
    popMatrix()
    popStyle()
end

function moveShip()
    if Button.left then Ship.radians = Ship.radians + rotationStep end
    if Button.right then Ship.radians = Ship.radians - rotationStep end
    if Button.fire then if not Ship.holdFire then fireMissile() end end
    if not Button.fire then Ship.holdFire = false end
    actualShipMove()
end

function actualShipMove()
    if Button.go then
        local accel = vec2(0.015,0):rotate(Ship.radians)
        Ship.step = Ship.step + accel
        Ship.step = maximize(Ship.step, 3)
    end
    finallyMove(Ship)
end

function finallyMove(ship)
    U:moveObject(ship)
end

function maximize(vec, size)
    local s = vec:len()
    if s <= size then
        return vec
    else
        return vec*size/s
    end
end

function fireMissile()
    Ship.holdFire = true
    Missile(Ship)
end

--# Button
-- Button
-- RJ 20200520

Button = {}
local Buttons = {}

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

function checkButtons()
    Button.left = false
    Button.right = false
    Button.go = false
    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)) < 50 then
                Button[button.name]=true
            end
        end
    end
end

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 Button[b.name] then
            fill(128,0,0)
        else
            fill(128,128,128,128)
        end
        ellipse(0,0, 50)
        fill(255)
        fontSize(30)
        text(b.name,0,0)
        popStyle()
        popMatrix()
    end
    popStyle()
end

--# Asteroid
-- Asteroid
-- RJ 20200520

local DeadAsteroids = {}
local Vel = 1.5

Asteroid = class()

function Asteroid:init()
    self.pos = vec2(math.random(WIDTH), math.random(HEIGHT))
    self.shape = Rocks[math.random(1,4)]
    self.scale = 16
    local angle = math.random()*2*math.pi
    self.step = Ratio*vec2(Vel,0):rotate(angle)
end

function Asteroid:killDist()
    local s = self.scale
    if s == 16 then return 64 elseif s == 8 then return 32 else return 16 end
end

function killDeadAsteroids(asteroids)
    for k,a in pairs(DeadAsteroids) do
        asteroids[a] = nil
    end
    DeadAsteroids = {}
end

function scoreAsteroid(asteroid)
    local s = asteroid.scale
    local inc = 0
    if s == 16 then inc = 20
    elseif s == 8 then inc = 50
    else inc = 100
    end
    Score = Score + inc
end

function splitAsteroid(asteroid, asteroids)
    if asteroid.scale == 4 then
        Splat(asteroid.pos)
        DeadAsteroids[asteroid] = asteroid
        return
    end
    asteroid.scale = asteroid.scale//2
    asteroid.angle = math.random()*2*math.pi
    local new = Asteroid()
    new.pos = asteroid.pos
    new.scale = asteroid.scale
    asteroids[new] = new
    Splat(asteroid.pos)
end

function Asteroid:draw()
    pushMatrix()
    pushStyle()
    translate(self.pos.x, self.pos.y)
    scale(self.scale)
    strokeWidth(1/self.scale)
    for i,l in ipairs(self.shape) do
        line(l.x, l.y, l.z, l.w)
    end
    popStyle()
    popMatrix()
end

function Asteroid:move()
    U:moveObject(self)
end


--# Splat
-- Splat
-- RJ 20200521

local Splats = {}

local Vecs = {
vec2(-2,0), vec2(-2,-2), vec2(2,-2), vec2(3,1), vec2(2,-1), vec2(0,2), vec2(1,3), vec2(-1,3), vec2(-4,-1), vec2(-3,1)
}

function drawSplats()
    for k, splat in pairs(Splats) do
        splat:draw()
    end
end

Splat = class()

function Splat:init(pos)
    local die = function()
        Splats[self] = nil
    end
    self.pos = pos
    Splats[self] = self
    self.size = 2
    self.diameter = 6
    self.rot = math.random(0,359)
    tween(4, self, {size=10, diameter=1}, tween.easing.linear, die)
end

function Splat:draw()
    pushStyle()
    pushMatrix()
    translate(self.pos.x, self.pos.y)
    fill(255)
    stroke(255)
    rotate(self.rot)
    local s = self.size
    for i,v in ipairs(Vecs) do
        ellipse(s*v.x, s*v.y, self.diameter)
    end
    popMatrix()
    popStyle()
end

--# Missile
-- Missile
-- RJ 20200522

Missile = class()

function Missile:init(ship)
    function die()
        self:die()
    end
    self.pos = ship.pos
    self.step = U.missileVelocity:rotate(ship.radians) + ship.step
    U.missiles[self] = self
    tween(3, self, {}, tween.easing.linear, die)
end

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

function Missile:draw()
    ellipse(self.pos.x, self.pos.y, 6)
end

function Missile:move()
    U:moveObject(self)
end

--# Universe
-- Universe
-- RJ 20200523

Universe = class()

local MissileSpeed = 2.0

function Universe:init()
    self.asteroids = {}
    self.missiles = {}
    self.missileVelocity = vec2(MissileSpeed,0)
end

function Universe:draw()
    self:drawAsteroids()
end

function Universe:createAsteroids()
    for i = 1,4 do
        local a = Asteroid()
        self.asteroids[a] = a
    end
end

function Universe:findCollisions()
    for i,a in pairs(self.asteroids) do
        for k,m in pairs(self.missiles) do
            if m.pos:dist(a.pos) < a:killDist() then
                scoreAsteroid(a)
                splitAsteroid(a, self.asteroids)
                m:die()
            end
        end
    end
end

function Universe:moveObject(anObject)
    local pos = anObject.pos + Ratio*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

function Universe:drawMissiles()
    pushStyle()
    pushMatrix()
    fill(255)
    stroke(255)
    for k, missile in pairs(self.missiles) do
        missile:draw()
    end
    popMatrix()
    popStyle()
    for k, missile in pairs(self.missiles) do
        missile:move()
    end
end

function Universe:drawAsteroids()
    pushStyle()
    stroke(255)
    fill(0,0,0, 0)
    strokeWidth(2)
    rectMode(CENTER)
    for i,asteroid in pairs(self.asteroids) do
        asteroid:draw()
        asteroid:move()
    end
    popStyle()
    killDeadAsteroids(self.asteroids)
end