Asteroids 17
I thought I knew what we had to do today but I was a bit mistaken. We need more, and better thinking … and code.
There was an embarrassing defect in the last release. Attempts to fire a missile accessed an out of date notion, Ship.ang
, resulting in an error and no missiles. So one mission this morning is to think about how that happened and what we can do about it.
It’s easy – and true – to say “it was just an oversight”. Certainly it was. But how do we fix our tendency to miss things from time to time? “Think better” isn’t very good advice: we’re thinking as best we can. So we’ll see if there are ways to program that allow for fewer oversights.
I’m sure some of you are thinking about strict typing right now. In a more strict language, the fact that ang
wasn’t defined would have given us an error. We’ll talk about that when we get to the topic.
What I plan to do this morning is a further improvement to the Missile. When a missile is fired, its velocity is presently speed 2.0 in the direction the ship is pointing. That’s not quite right: the missile should get a velocity equal to its own firing velocity *plus the velocity of the ship”. So let’s take a look at that:
function Missile:init(ship)
function die()
self:die()
end
self.pos = ship.pos
self.vel = vec2(MissileVelocity,0):rotate(ship.radians)
Missiles[self] = self
tween(3, self, {}, tween.easing.linear, die)
end
The self.vel
line there is the one I had to fix last night, by changing the use of ang
, which no longer exists, to radians
. We need to add the velocity of the ship to that value. Conveniently, the ship knows its velocity, so we can do this:
self.vel = vec2(MissileVelocity,0):rotate(ship.radians) + ship.velocity
That works as intended.
Now that that little task is done, let’s commit “missiles track” and look upon our works and despair (just a little).
That Darn Oversight
When we added acceleration to the Ship, I made a mistake between degrees and radians. Codea doesn’t help much with this, not least because sometimes it uses degrees and sometimes radians. So we changed the member variable from ang
to radians
, hoping that would help us remember the data type.
The firing problem was that Missile, which looks to the Ship during creation, was referring to ang
and I didn’t catch it. My question now is whether there is something we can do beyond “be smarter”.
I have two ideas. First, we could have some tests that check whether things are being computed correctly. Certainly if there had been a test for missile velocity, it would have failed. It’s a bit late to close that barn door but we might want to write a test or two to improve our testing habits. As I’ve said before, in a visual app like this game, I have difficulty seeing what tests to write, and it’s so easy just to run the program and see if a change works.
Unless you check acceleration but don’t ever fire a missile after adding acceleration. It was breakfast time, so I didn’t do even a bit of game play.
Since you can’t expect me to skip a meal, it might be fair to ask me to improve the automated tests.
My second idea is to find ways to improve the code to make oversights less likely. The best way to do that is to make the code simpler, and the second best way is to make it more consistent.
(I don’t really know if those are the best ways in the world. They’re the best ones I can think of right now. See previous remarks about being smarter.)
We have three moving objects just now, asteroids, the ship, and the missiles. Their moving code looks like this:
function moveAsteroid(asteroid)
local step = Ratio*vec2(Vel,0):rotate(asteroid.angle)
local pos = asteroid.pos + step
asteroid.pos = vec2(keepInBounds(pos.x, WIDTH), keepInBounds(pos.y, HEIGHT))
end
function actualShipMove()
if Button.go then
local accel = vec2(0.015,0):rotate(Ship.radians)
Ship.velocity = Ship.velocity + accel
Ship.velocity = maximize(Ship.velocity, 3)
end
Ship.pos = Ship.pos + Ship.velocity
Ship.pos = vec2(keepInBounds(Ship.pos.x, WIDTH), keepInBounds(Ship.pos.y, HEIGHT))
end
function Missile:move()
self.pos = self.pos + Ratio*self.vel
self.pos = vec2(keepInBounds(self.pos.x, WIDTH), keepInBounds(self.pos.y, HEIGHT))
end
These are doing roughly the same thing, but they don’t look as similar as they might. Furthermore … where is Ratio
being used in the ship at all? Remember that Ratio
compensates for processor speed, to keep game speed the same on different processors. I don’t see that happening for the ship.
Another oversight,you say, Jeffries? Been a lot of oversights lately, wouldn’t you say? What’s going to be done about these oversights, hmm?
What we have here, lords and ladies, is duplication. Not your simple these two lines are the same kind of duplication, but the more pernicious these lines are doing the same things in different ways kind of duplication.
What do we do with duplication? We remove it. We do that “same thing” in one and only one place. How do we get there? One really good way is to make the duplication more visible, so that the parts that are the same look the same, and the parts that are different are set off from the parts that are the same.
Common Elements
What do all our moving objects have in common?
- They all have a position in the world. It’s even named
pos
in all cases. - They all clip their position to remain within screen boundaries.
- They all add a small amount, the step, to their position, on every move.
- The step should be adjusted by Ratio, to give them all the same speed across processors. (The ship seems not to do this at present.)
- Probably, all velocities should be limited, not necessarily to the same limit.
Let’s look at what they don’t have in common:
- The asteroids and missiles never change speed or direction: they just plod along at whatever pace they’re given.
- The ship does change speed: it can accelerate in any direction.
One way of restating the difference is that the acceleration of asteroids and missiles is zero, and that of the ship may not be.
This is suggesting to me that everyone’s motion might look like this:
adjustVelocityByAcceleration() -- ship only
adjustVelocityByLimit() -- ship and missile
adjustVelocityByRatio() -- everyone
addVelocityToPosition() -- everyone
keepPositionInBounds() -- everyone
Now that I look at that I think we can do better. I’ll leave that there because I did think it, and my job here is to show you how things really happen, not how some god of programming would do it.
Let’s go back to the things in common and work with them.
- Everyone has a step, an increment to position;
- Everyone adjusts that step by Ratio
- Everyone adds the step to position
- Everyone keeps the position in bounds.
There are other operations that are not shared, at least this: some people change the step, for some it is constant. If it’s constant, we should just use it, adjusting it not at all, for efficiency.
So, in principle, asteroids and missiles should know their step
, which would be a vector embodying their velocity and direction, and including the application of Ratio. They just do steps 3 and 4, given step.
And the ship computes its step, adding in any acceleration, and limiting by maximum velocity, adjusting for Ratio … and then does steps 3 and 4.
And the missiles need to be sure to apply Ratio and maximum velocity (if they have it) when they set up their step.
And we need a shared name for the step: and it should be velocity.
We have a plan
Well, we have enough of a plan. I’m moving toward each of these three objects having a move method that adjusts by velocity and clamps values to the screen, and to have them all be identical.
Let’s start to make that happen. First, Asteroid, which looks like this:
function createAsteroid()
local a = {}
a.pos = vec2(math.random(WIDTH), math.random(HEIGHT))
a.angle = math.random()*2*math.pi
a.shape = Rocks[math.random(1,4)]
a.scale = 16
return a
end
function moveAsteroid(asteroid)
local step = Ratio*vec2(Vel,0):rotate(asteroid.angle)
local pos = asteroid.pos + step
asteroid.pos = vec2(keepInBounds(pos.x, WIDTH), keepInBounds(pos.y, HEIGHT))
end
function keepInBounds(value, bound)
return (value+bound)%bound
end
We want to compute step
during creation. But wait, we can’t apply Ratio at creation time, it can vary during the run. So we don’t do just steps 3 and 4 from above, we do 2, 3, and 4. I’ll refactor move and create thus:
function moveAsteroid(asteroid)
local pos = asteroid.pos + Ratio*asteroid.step
asteroid.pos = vec2(keepInBounds(pos.x, WIDTH), keepInBounds(pos.y, HEIGHT))
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
That works, by actual test. Commit “canonical asteroid motion”.
Now Missile, which looks like this:
function Missile:init(ship)
function die()
self:die()
end
self.pos = ship.pos
self.vel = vec2(MissileVelocity,0):rotate(ship.radians) + ship.velocity
Missiles[self] = self
tween(3, self, {}, tween.easing.linear, die)
end
function Missile:move()
self.pos = self.pos + Ratio*self.vel
self.pos = vec2(keepInBounds(self.pos.x, WIDTH), keepInBounds(self.pos.y, HEIGHT))
end
This seems problematical, since missiles are objects and asteroids are not, but I’m going to let that slide for now. I think it’ll sort out.
I’ll rewrite the move to look more like the asteroids one:
function Missile:move()
local pos = self.pos + Ratio*self.vel
self.pos = vec2(keepInBounds(self.pos.x, WIDTH), keepInBounds(self.pos.y, HEIGHT))
end
Now in Missile we called it vel
and in Asteroid it’s step
. Let’s stick with step
. I was thinking velocity but the idea here is that all the velocity calculations have been done and we just have a step to take. So I’ll modify Missile to have step
instead of vel
.
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.velocity
Missiles[self] = self
tween(3, self, {}, tween.easing.linear, die)
end
Oops:
function Missile:move()
local pos = self.pos + Ratio*self.step
self.pos = vec2(keepInBounds(pos.x, WIDTH), keepInBounds(pos.y, HEIGHT))
end
I need a refactoring browser. Forgot to remove the self.
inside the keepInBounds
.
These two functions are the same. They’re so much the same that I think I could use one of them to do the work of the other
Just for fun:
function Missile:move()
--local pos = self.pos + Ratio*self.step
--self.pos = vec2(keepInBounds(pos.x, WIDTH), keepInBounds(pos.y, HEIGHT))
moveAsteroid(self)
end
That actually works. The moveAsteroid function doesn’t know it wasn’t given an asteroid, it just looks for and uses step
and pos
.
That’s evil, of course, and I’ll back that change out for now. Then commit: “canonical missile motion”.
Now let’s do the ship. We’re doing to have to be careful, because the missile is referring to ship.velocity
and we plan to change that to step
. Don’t let me forget that.
Here’s the ship now:
function createShip()
Ship.pos = vec2(WIDTH, HEIGHT)/2
Ship.radians = 0
Ship.velocity = vec2(0,0)
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.velocity = Ship.velocity + accel
Ship.velocity = maximize(Ship.velocity, 3)
end
Ship.pos = Ship.pos + Ship.velocity
Ship.pos = vec2(keepInBounds(Ship.pos.x, WIDTH), keepInBounds(Ship.pos.y, HEIGHT))
end
I rather hate that I allowed that moveShip
function to keep its name. Anyway we’re here for a different purpose. I’ll name my new little mover, oh, finallyMove
I guess, and we’ll do like this:
function createShip()
Ship.pos = vec2(WIDTH, HEIGHT)/2
Ship.radians = 0
Ship.step = vec2(0,0)
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)
local pos = ship.pos + Ratio*ship.step
Ship.pos = vec2(keepInBounds(pos.x, WIDTH), keepInBounds(pos.y, HEIGHT))
end
That works, and I remembered to fix the missile also. But did I remember to put Ratio
into all three functions?
function finallyMove(ship)
local pos = ship.pos + Ratio*ship.step
Ship.pos = vec2(keepInBounds(pos.x, WIDTH), keepInBounds(pos.y, HEIGHT))
end
function moveAsteroid(asteroid)
local pos = asteroid.pos + Ratio*asteroid.step
asteroid.pos = vec2(keepInBounds(pos.x, WIDTH), keepInBounds(pos.y, HEIGHT))
end
function Missile:move()
local pos = self.pos + Ratio*self.step
self.pos = vec2(keepInBounds(pos.x, WIDTH), keepInBounds(pos.y, HEIGHT))
end
I did in fact remember. Yay, me. Maybe I can be smarter after all.
Now all three of these moving objects use the same code to move. They all point to a different object / table when the function runs, but that’s just fine.
Now we can consolidate that function if we care to. We could put a new copy of that function in Main, move the keep there as well, and use the new function for all three cases.
I don’t like that. It’s semi-OK for the asteroids and ship to consolidate that function, but it doesn’t consolidate well for the object-oriented missile to call a global function. I’m not even really happy about calling the keepInBounds
that way.
What’s another way of making three different things use the same function?
This could get tricky. First, let’s commit “canonical ship motion”.
Consolidating movement
I was thinking of providing a general motion function, since now we’ve written it three times, and plugging it into each of our tables. But in talking about this on one of my Slacks just now, I got an idea I like better: a method on Universe that moves things.
We can think of it as moving things according to the laws of the universe. It could even apply speed limits if we wish. So I’ll add a method to Universe and use it.
Maybe we’ll like this, maybe we won’t. Either way is OK. Here it is for missiles:
function Universe:moveObject(anObject)
local pos = anObject.pos + Ratio*anObject.step
anObject.pos = vec2(keepInBounds(pos.x, WIDTH), keepInBounds(pos.y, HEIGHT))
end
function Missile:move()
U:moveObject(self)
end
Now we can clearly do ship and asteroid the same way, replacing their calls to their own moves with calls to this one:
function moveAsteroid(asteroid)
U:moveObject(asteroid)
end
function finallyMove(ship)
U:moveObject(ship)
end
This works just fine. The question is whether we like it.
The good news is we have completely removed that duplication, hopefully preventing any further mistakes like leaving out Ratio or referring to non-existent variables.
The bad news is, now the universe moves all the things, instead of the things moving themselves. If I could have my druthers, I druther have objects moving themselves … all in the same way.
Is there a way to avoid the duplication but not cause the Universe to move all the objects … and do we even mind that it does move them?
For now, I think we’ll leave it this way. However, I think we should move the keepInBounds
function to the Universe as well, since it should only be used right here.
function Universe:keepInBounds(value, bound)
return (value+bound)%bound
end
That breaks a unit test, which was counting on keepInBounds
being global. Fixed:
_: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)
Commit “universe moves things”.
Wider View
Let’s think about this change. Taken overall, it’s pretty global, ultimately affecting five tabs, Test, Ship, Asteroid, Missile, and Universe. But we did it in small steps and rarely broke anything.
The errors I made were, I believe, mostly failures to catch all the occurrences of something that needed to change because its name or access had changed, Failing to add in a “self” where needed, or referencing an old variable name.
Oversights, in other words. I hope that this centralization will prevent further such oversights in this one area, but there are plenty of other areas to be concerned about, like the approximately 500 lines of code that are not moving the object.
I think this is a bit better, a bit more like it “should” be, but I’m not certain. I’m not entirely comfortable having motion done “to” an object rather than “by” the object. It makes a kind of sense, but it seems that it reduces their autonomy.
I wouldn’t do this trick to save memory by removing the duplication. I did it because it ensures that everyone who should do the same thing does in fact do the same thing. It’s a bit odd but for now, I’ll pay the price of oddness for the improvement in consistency.
I look forward to your comments (via Twitter or other means) and hope to see you next time!
--# 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()
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
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
--# 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
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
--# Universe
-- Universe
-- RJ 20200523
Universe = class()
function Universe:init()
self.asteroids = {}
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(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: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