Asteroids 18: Let's get some objects up in this.
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