Asteroids 21: Kill the Ship
I reckon it’s time to kill the ship when it’s hit by an asteroid. I have a clever way to do it and a direct way. Which is best?
We have the ship colliding with asteroids and saying “BLAMMO”. (Perhaps it should have said “OUCH!”) But the ship merrily carries on after the collision. What is supposed to happen is that it’s “destroyed” by the collision, disappears except for some debris (BLAMMO) and then, if you have lives left, a new ship spawns and you carry on.
I don’t remember whether a new wave of asteroids starts, or whether you spawn in the middle of an existing onslaught. I’ll watch a video about that, and/or read the code, when I need to know. For now, I’d like the ship to “go away” and after a while, come back.
I can think of at least two ways to accomplish the basic go-away/come-back behavior. One would be to put a flag or timer in the ship and while it is counting down, the ship is invisible and unconsidered by the buttons and asteroids and whatever cares about the ship. Another way would be to destroy the ship and have a timer elsewhere, presumably in Universe, that spawns a ship when the time is up. In that form, some ship-related code just wouldn’t happen, but the findCollision
code needs to know not to look, and other
But I have a clever idea lurking in my head as well. Remember tween
, that function that we use to make the Splat expand? It takes a table as a parameter and tweaks values in that table, and can optionally call a callback function when the tween ends. So when we kill the ship, whether we really destroy it or just hide it, we could start up a tween that would call back when the time is up.
I really love clever ideas, and I’ve tried to train myself never to put them into the code. But here, I think I’ll give it a go. It should be straightforward enough. We’ll decide after we do it.
So here goes. It’s 0955, by the way, and I’m typing on Slack a bit as well as doing this. That may be a bad idea.
Kill the Ship
We have an empty function killShip
, I believe, which is called when the ship is hit by an asteroid:
function Universe:checkShipCollision(asteroid)
if self.ship.pos:dist(asteroid.pos) < asteroid:killDist() then
scoreAsteroid(asteroid)
splitAsteroid(asteroid, self.asteroids)
killShip()
end
end
I think I’ll try actually destroying the ship object. That will require us to deal with ship being nil … unless …
If there are no asteroids, findCollision
works just fine: it loops over all the asteroids, of which there are none, and nothing happens. If we put the ship in a collection of ships and looped over it, when the ship is gone, nothing will happen. Let’s explore that approach.
In Universe:
function Universe:init()
self.score = 0
self.missileVelocity = vec2(MissileSpeed,0)
self.button = {}
createButtons()
self.ship = createShip()
self.processorRatio = 1.0
self.asteroids = {}
self.missiles = {}
self.explosions = {}
end
If we make a collection self.ships
instead, and put the ship into it in the way we do asteroids, that might be a good thing.
Now here is a place where if our Ship were an object, things might go better. As it is, we have Universe calling drawShip
, which is a function (not method) in the Ship tab, and it goes ahead and draws:
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
Note that that code refers to the variable Ship
, which is local to Ship tab, not to U.ship
as it arguably should. This is the kind of thing that happens when you have a procedural design and when you let it get a bit out of hand.
And, fact is, we almost always do let it get a bit out of hand. We don’t even realize at the time we do something like that that it’s not the best way, and when we do find a better way, we may or may not remember to go back and fix the not so better ways. This, by the way, is the original meaning of “technical debt” as Ward Cunningham originally spoke of it. It’s the inevitable difference in the code between what we knew then and what we know now.
We reduce that debt, sometimes in big collapses as we apply some new understanding most everywhere, and sometimes in small improvements as we run across them.
Remind me to talk about cleaning code we’re not working on, in Summing Up.
Anyway, I don’t like that global, and there’s no better time than now to get rid of it.
Looking at Ship tab, I think we can rather quickly convert Ship to an object, and I think we’ll be glad we did. Let’s see if I’m right about either of those ideas. Here’s what it looks like now:
-- 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)
return Ship
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 U.button.left then Ship.radians = Ship.radians + rotationStep end
if U.button.right then Ship.radians = Ship.radians - rotationStep end
if U.button.fire then if not Ship.holdFire then fireMissile() end end
if not U.button.fire then Ship.holdFire = false end
actualShipMove()
end
function actualShipMove()
if U.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
I’m just going to tick right down through those functions and convert them, and then see what needs changing. The additional changes should all be in Universe if we’re at all lucky.
Ship = class()
local rotationStep = math.rad(1) -- one degree in radians
function Ship:init()
self.pos = vec2(WIDTH, HEIGHT)/2
self.radians = 0
self.step = vec2(0,0)
end
That’ll create the ship and automatically return it. Universe has one call to createShip
and the tests have three. I think I’ll fix all of those right now:
function Universe:init()
self.score = 0
self.missileVelocity = vec2(MissileSpeed,0)
self.button = {}
createButtons()
self.ship = Ship() -- CHANGED
self.processorRatio = 1.0
self.asteroids = {}
self.missiles = {}
self.explosions = {}
end
The tests go like this:
_:test("Missile fired at rest", function()
local ship = Ship()
local missile = Missile(ship)
_:expect(missile.step).is(U.missileVelocity)
end)
_:test("Missile fired north", function()
local ship = Ship()
ship.radians = math.pi/2
local missile = Missile(ship)
local mx = missile.step.x
local my = missile.step.y
_:expect(mx).is(0, 0.001)
_:expect(my).is(U.missileVelocity.x, 0.001)
end)
_:test("Missile fired from moving ship", function()
local ship = Ship()
ship.step = vec2(1,2)
local missile = Missile(ship)
local mx = missile.step.x
local my = missile.step.y
_:expect(mx).is(U.missileVelocity.x + 1, 0.001)
_:expect(my).is(U.missileVelocity.y + 2, 0.001)
end)
All the references to ship
used to be U.ship
. Because the ship is nearly an object, we can use a local one. This is actually better than it was before. The tests run. The game does not, which is no surprise. It’s useful to look at why, since it will point to things that need to be changed.
The error is in drawShip
, which is accessing capital-S Ship
, which is now the class rather than the former ship. drawShip
was next on our list to convert anyway:
function Ship:draw()
local sx = 10
local sy = 6
pushStyle()
pushMatrix()
translate(self.pos.x, self.pos.y)
rotate(math.deg(self.radians))
strokeWidth(2)
stroke(255)
line(sx,0, -sx,sy)
line(-sx,sy, -sx,-sy)
line(-sx,-sy, sx,0)
popMatrix()
popStyle()
end
I renamed it draw
, which is our convention, and changed the ship references to self
. Now to change the call to drawShip
in Universe:
self.ship:draw()
Right after that in Universe is:
moveShip()
I’ll change that too:
self.ship:move()
And convert moveShip
:
function Ship:move()
if U.button.left then self.radians = self.radians + rotationStep end
if U.button.right then self.radians = self.radians - rotationStep end
if U.button.fire then if not self.holdFire then fireMissile() end end
if not U.button.fire then self.holdFire = false end
actualShipMove()
end
I’m just going to go right ahead and change all these calls to methods. I could leave them in place and pass self as an argument but in for a penny:
-- Ship
-- RJ 20200520
Ship = class()
local rotationStep = math.rad(1) -- one degree in radians
function Ship:init()
self.pos = vec2(WIDTH, HEIGHT)/2
self.radians = 0
self.step = vec2(0,0)
end
function Ship:draw()
local sx = 10
local sy = 6
pushStyle()
pushMatrix()
translate(self.pos.x, self.pos.y)
rotate(math.deg(self.radians))
strokeWidth(2)
stroke(255)
line(sx,0, -sx,sy)
line(-sx,sy, -sx,-sy)
line(-sx,-sy, sx,0)
popMatrix()
popStyle()
end
function Ship:move()
if U.button.left then self.radians = self.radians + rotationStep end
if U.button.right then self.radians = self.radians - rotationStep end
if U.button.fire then if not self.holdFire then self:fireMissile() end end
if not U.button.fire then self.holdFire = false end
self:actualShipMove()
end
function Ship:actualShipMove()
if U.button.go then
local accel = vec2(0.015,0):rotate(Ship.radians)
self.step = self.step + accel
self.step = maximize(self.step, 3)
end
self:finallyMove()
end
function Ship:finallyMove()
U:moveObject(self)
end
function maximize(vec, size)
local s = vec:len()
if s <= size then
return vec
else
return vec*size/s
end
end
function Ship:fireMissile()
self.holdFire = true
Missile(self)
end
Note that I left maximize
as a function. It’s a utility and not properly part of the ship at all.
The game runs properly. Commit: “ship is an object”. It’s 1033. That took 38 minutes start to finish, and now we have a nice clean ship object to work with.
Killing, Remember?
We are here to kill the ship. All this – thewhole 33 minutes of it – was cleaning things up so that we can deal with the ship being gone.
There are now three places that I can think of where we’ll have to be sensitive to the ship being gone.
We can’t call draw
or move
if we don’t have a ship. And wee can’t check collisions if we don’t have a ship. Let’s first just do the obvious check:
function Universe:draw()
--displayMode(FULLSCREEN_NO_BUTTONS)
pushStyle()
background(40, 40, 50)
self.processorRatio = DeltaTime/0.0083333
self:drawAsteroids()
self:drawExplosions()
checkButtons()
drawButtons()
if self.ship then self.ship:draw() end
if self.ship then self.ship:move() end
self:drawMissiles()
drawSplats()
U:drawScore()
popStyle()
U:findCollisions()
end
function Universe:findCollisions()
for i,a in pairs(self.asteroids) do
self:checkMissileCollisions(a)
if self.ship then self:checkShipCollision(a) end
end
end
This should run just fine. And it does. Now, at last, we can do something about killShip
, which looks like this:
function killShip()
Explosion(U.ship)
end
It's not a member function on Universe, because I wasn't comfortable with that. In any case, we'll now just nil out the ship:
~~~lua
function killShip()
Explosion(U.ship)
U.ship = nil
end
That should make the ship disappear at the time of BLAMMO. And it does:
Now to make it come back, I’ll try the tween trick, and I think I’ll put it up in the checkShipCollision
function, though I am not entirely happy with that.
No. On second thought, I’ll promote killShip
to a method and put it there. Here’s my first try:
function Universe:killShip()
local f = function()
self.ship = Ship()
end)
Explosion(U.ship)
U.ship = nil
self.dummy = 0
tween(6, self, {dummy=10}, tween.easing.linear, f)
end
The tween will count dummy
from 0 to 10, then call f, which will create a new ship. The delay is 6 seconds, which should be enough to be scary. I put the dummy
in there because I believe the tween won’t run if there isn’t anything to change. We’ll see.
Now to test. And it works. The ship goes away and comes back about six seconds later. I’ll try it with an empty change set just to be sure. I’d rather it worked that way.
Ah, it does work. My experiment the other day must have been wrong. Here we are now:
function Universe:killShip()
local f = function()
self.ship = Ship()
end
Explosion(U.ship)
U.ship = nil
tween(6, self, {}, tween.easing.linear, f)
end
The BLAMMO stays on the screen. Let’s make it disappear also, while we’re here. No, first let’s commit: “respawn”. Now then:
function Explosion:init(ship)
self.pos = ship.pos
self.step = vec2(0,0)
U.explosions[self] = self
end
We can put a tween in here too, I’d think:
function Explosion:init(ship)
local f = function()
U.explosions[self] = nil
end
self.pos = ship.pos
self.step = vec2(0,0)
U.explosions[self] = self
tween(4, self, {}, tween.easing.linear, f)
end
That ought to make the BLAMMO go away in four seconds.
WHOA! Guess what! In my superficial testing, I never moved the ship, I just watched asteroids go by and shot them. When I go to accelerate, it breaks:
Bad argument to rotate
in actualShipMove
. Let’s see:
function Ship:actualShipMove()
if U.button.go then
local accel = vec2(0.015,0):rotate(Ship.radians)
self.step = self.step + accel
self.step = maximize(self.step, 3)
end
self:finallyMove()
end
Sure enough, a reference to Ship
that should be self
. That fixes that little problem. Better think about that in Summing Up as well.
Anyway things are working well now. Here’s a video of me killing asteroids and them killing me:
Wow, that was a good run. Every time the ship respawned an asteroid hit it before I could get near a button. Good test and things seem to be working as intended.
Let’s sum up.
Summing Up
There are a couple of promised topics. Let me get those out of the way first:
Cleaning code we’re not working on
Throughout this project, I have often made improvements to the code based on what I saw in the design and what I thought would be better. The point of this was to demonstrate that incremental design improvements are always possible and that we can do them in small enough bites that it doesn’t slow us down. When the design is better, subsequent work goes faster.
However, on a real project, I wouldn’t recommend just going in willy-nilly and improving code. The reason is that our management looks to us for new and improved capability in the product, not nicer and nicer code. So on a real project, what I would do would be to improve code only when we were going to work on it anyway, to add a new capability or change an old one per our product leadership.
The reasoning is pretty simple. Code that we are not working in doesn’t hurt us, no matter how ugly it is. We don’t see the ugliness and can ignore it until we have to go into that area. So time spent working on that code is wasted, unless and until we actually have to change it for product reasons rather than reasons of code clarity.
Naturally, ugly code is more likely to have defects in it, so we may be called upon to fix those defects, and then we have a good reason to improve that code. I’ve written a separate article about this: Refactoring - Not On the Backlog.
Of course you get to make your own decisions about what to work on, but in general I think we do best to let the product definition process point us into the code and then wherever we are pointed, we improve things incrementally as needed.
Missed converting Ship to self.
Then there was today’s surprise defect. For a “long time”, perhaps as long as a half an hour, there was a defect in the program such that the ship could not accelerate, because in converting the Ship
to a class, I had missed a reference to the former variable that pointed to the Ship instance. That reference needed to say self
.
Codea does have a sort of global replace function, and had I trusted it, I could have bulk-converted all the “Ship” references at once. But Codea will convert “Shipshape” to “selfshape” if you’re not careful, and it doesn’t show very well where it’s going to make the next change, nor is there a way to skip a change as far as I know.
So I don’t use that capability and I make mistakes.
What’s to do? It’s sure to happen again. It might be possible to write a microtest to move the ship, now that it’s an object. That might be a fun exercise for the next article. Maybe. Or I could “remember” always to fly the ship around more extensively during testing.
Yeah, remember. I remember when I used to actually remember stuff. Even then I never remembered to do the good things.
At this moment, I don’t really see a good way to better ensure that I don’t make mistakes like these. And that’s troubling, because it means they’ll continue. All I can do is take a moment to reflect, like we’re doing now, and sometimes I get an idea for an improvement to my practice that will actually work. When I get an idea like that, I try it. When I don’t, I forgive myself and move on.
Today, it’s forgive and move on, unless a reader comes up with a good idea.
What else?
Well, we set out to implement the ship killing and respawning, and we did just that. We used a moderately clever thing, the tween, to implement our timing. It turned out rather well. Here’s why I like it:
If we did timing in the ordinary way, we’d set a timer somewhere, probably in Universe, and then we’d put in code to tick and check the timer, and code to do things when the timer expired. And there would be more and more timers. We already have three tweens doing timing, so there would be at least three timers.
And things that happened when the timers expired would be all over the universe, in explosions, in splats, in Universe itself, and so on.
The use of tween to do our timing lets us put the timer where the action is.
One improvement we will surely want to make is to put the various timing constants into Universe for safe keeping. But overall, I like how the tween handled our timing concerns.
And I like how the morning went. Today I’ll remember to provide the code, whose final commit comment is “blammo disappears, ship comes back”.
Bye for now!
--# Main
-- Asteroids
-- RJ 20200511
Touches = {}
function setup()
U = Universe()
U:createAsteroids()
Score = 0
end
function draw()
U:draw()
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)
_:test("Missile fired at rest", function()
local ship = Ship()
local missile = Missile(ship)
_:expect(missile.step).is(U.missileVelocity)
end)
_:test("Missile fired north", function()
local ship = Ship()
ship.radians = math.pi/2
local missile = Missile(ship)
local mx = missile.step.x
local my = missile.step.y
_:expect(mx).is(0, 0.001)
_:expect(my).is(U.missileVelocity.x, 0.001)
end)
_:test("Missile fired from moving ship", function()
local ship = Ship()
ship.step = vec2(1,2)
local missile = Missile(ship)
local mx = missile.step.x
local my = missile.step.y
_:expect(mx).is(U.missileVelocity.x + 1, 0.001)
_:expect(my).is(U.missileVelocity.y + 2, 0.001)
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
Ship = class()
local rotationStep = math.rad(1) -- one degree in radians
function Ship:init()
self.pos = vec2(WIDTH, HEIGHT)/2
self.radians = 0
self.step = vec2(0,0)
end
function Ship:draw()
local sx = 10
local sy = 6
pushStyle()
pushMatrix()
translate(self.pos.x, self.pos.y)
rotate(math.deg(self.radians))
strokeWidth(2)
stroke(255)
line(sx,0, -sx,sy)
line(-sx,sy, -sx,-sy)
line(-sx,-sy, sx,0)
popMatrix()
popStyle()
end
function Ship:move()
if U.button.left then self.radians = self.radians + rotationStep end
if U.button.right then self.radians = self.radians - rotationStep end
if U.button.fire then if not self.holdFire then self:fireMissile() end end
if not U.button.fire then self.holdFire = false end
self:actualShipMove()
end
function Ship:actualShipMove()
if U.button.go then
local accel = vec2(0.015,0):rotate(self.radians)
self.step = self.step + accel
self.step = maximize(self.step, 3)
end
self:finallyMove()
end
function Ship:finallyMove()
U:moveObject(self)
end
function maximize(vec, size)
local s = vec:len()
if s <= size then
return vec
else
return vec*size/s
end
end
function Ship:fireMissile()
self.holdFire = true
Missile(self)
end
--# Button
-- Button
-- RJ 20200520
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()
U.button.left = false
U.button.right = false
U.button.go = false
U.button.fire = false
for id,touch in pairs(Touches) do
for i,button in ipairs(Buttons) do
if touch.pos:dist(vec2(button.x,button.y)) < 50 then
U.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 U.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 = 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.score = 0
self.missileVelocity = vec2(MissileSpeed,0)
self.button = {}
createButtons()
self.ship = Ship()
self.processorRatio = 1.0
self.asteroids = {}
self.missiles = {}
self.explosions = {}
end
function Universe:draw()
--displayMode(FULLSCREEN_NO_BUTTONS)
pushStyle()
background(40, 40, 50)
self.processorRatio = DeltaTime/0.0083333
self:drawAsteroids()
self:drawExplosions()
checkButtons()
drawButtons()
if self.ship then self.ship:draw() end
if self.ship then self.ship:move() end
self:drawMissiles()
drawSplats()
U:drawScore()
popStyle()
U:findCollisions()
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
self:checkMissileCollisions(a)
if self.ship then self:checkShipCollision(a) end
end
end
function Universe:checkShipCollision(asteroid)
if self.ship.pos:dist(asteroid.pos) < asteroid:killDist() then
scoreAsteroid(asteroid)
splitAsteroid(asteroid, self.asteroids)
self:killShip()
end
end
function Universe:checkMissileCollisions(asteroid)
for k,m in pairs(self.missiles) do
if m.pos:dist(asteroid.pos) < asteroid:killDist() then
scoreAsteroid(asteroid)
splitAsteroid(asteroid, self.asteroids)
m:die()
end
end
end
function Universe:killShip()
local f = function()
self.ship = Ship()
end
Explosion(U.ship)
U.ship = nil
tween(6, self, {}, tween.easing.linear, f)
end
function Universe:moveObject(anObject)
local pos = anObject.pos + self.processorRatio*anObject.step
anObject.pos = vec2(self:keepInBounds(pos.x, WIDTH), self:keepInBounds(pos.y, HEIGHT))
end
function Universe:keepInBounds(value, bound)
return (value+bound)%bound
end
function Universe:drawExplosions()
for k,e in pairs(self.explosions) do
e:draw()
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
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
function Universe:drawScore()
local s= "000000"..tostring(self.score)
s = string.sub(s,-5)
fontSize(100)
text(s, 200, HEIGHT-60)
end
--# Explosion
Explosion = class()
function Explosion:init(ship)
local f = function()
U.explosions[self] = nil
end
self.pos = ship.pos
self.step = vec2(0,0)
U.explosions[self] = self
tween(4, self, {}, tween.easing.linear, f)
end
function Explosion:draw()
pushStyle()
pushMatrix()
translate(self.pos.x, self.pos.y)
fontSize(30)
text("BLAMMO", 0, 0)
popMatrix()
popStyle()
end