Asteroids 42
Not really an answer, but just a bit of fun.
I have a bit of fun in mind, but first a few words about some tweets from @Daganev. Daganev said:
I think the inheritance makes sense here. The one thing I would do differently is to only allow player missles to score points, ever.
The @sandimetz rule is inheritance should be wide and shallow. So, if you were to make it so that only “Missle” scores points the current inheritance doesn’t work. But I think you should make a PlayerMissle, that only scores points, and then the inheritance would be right.
And I replied:
only player missiles score points. It occurs to me that if I reversed the inheritance, Missile from Saucer, the “abstract” score defaults might serve for Saucer. It’d still need the check to kill only ships tho …
I meant to say there “SaucerMissile” of course.
Since we’re using inheritance consistently with the notion of “a programmer’s hack to save code”, we should take a look at whether this is the right order at all.
The “abstract” score in Destructible throws an error, because I wanted to be sure that everyone implemented score:
function Destructible:score()
dump("score", self)
assert(false)
end
Smalltalk used a similar trap in superclasses, called shouldImplement:
I believe. So we’d have to override that in both methods anyway.
And SaucerMissile only implements two methods anyway:
SaucerMissile = class(Missile)
function SaucerMissile:collide(anObject)
if anObject:is_a(Ship) then
anObject:mutuallyDestroy(self)
end
end
function SaucerMissile:mutuallyDestroy(anObject)
if anObject:is_a(Ship) then
if self:inRange(anObject) then
self:die()
anObject:die()
end
end
end
We have to test for Ship, as far as I can tell. So … I think this is as good as it gets. We can be reasonably confident that this isn’t going to bite us, because we’re nearing the end of all the interesting Asteroids functionality.
I appreciate the feedback from Dagonev, it’s always helpful to get a different perspective from which to look at things.
Now for some fun.
Ship Explodes
Our ship currently explodes by printing BLAMMO in outer space. This is not what we hope for from an explosion, even when it’s us. The original game had a brief animation of a few ship parts drifting apart. I believe I could eve find enough code and data to replicate those parts, but I have an idea that is much more fun.
But before we get to that (sorry about the clickbait), we might want to consider how Splat and Explosion work.
Splats own their own instances and manage them within the class. The main drawing loop calls drawSplats
, a global in the Splat tab. (It could be changed, easily, to Splat:draw()
if we felt that was better than the additional global.)
Explosion
, on the other hand, is the only “Indestructible` object implemented. It does participate in collision logic, which it implements like this:
-- Indestructible
function Explosion:collide(anything)
-- nope
end
function Explosion:mutuallyDestroy(anything)
--nope
end
My original plan with Destructible
was that there would be a hierarchy of “indestructible” objects as well. In retrospect, that’s a bit silly, because if they aren’t going to be able to be destroyed, there’s no reason to have them in the main objects
collection at all. There could be a separate collection of objects, processed separately in Universe:draw
, probably in here:
function Universe:drawEverything()
for k,o in pairs(self.objects) do
o:draw()
end
end
function Universe:moveEverything()
for k,o in pairs(self.objects) do
o:move()
end
end
We would simply loop over the indestructibles here, giving them their time on screen and the opportunity to move, but then when we do findCollisions
they would be skipped, because they’re not in objects
at all.
function Universe:findCollisions()
for k, o in pairs(self.objects) do
for kk, oo in pairs(self.objects) do
o:collide(oo)
end
end
end
The savings in time wouldn’t be much, but the design might arguably be better. Let’s think about that.
We presently have two schemes for drawing things, Universe.objects
and Splat
. Splat could be duplicated for other objects but why have two entirely different ways, separated so far. And there’s a special call in Universe:draw
to draw the splats anyway.
I think we’ll do it. I don’t see a great value to trying to write microtests for this. I’m just going to go ahead. Watch and see if I get in trouble.
We need a new collection in Universe
, much like objects
. It’ll be initialized and generally handled in the same places. I’ll do that first:
function Universe:drawEverything()
for k,o in pairs(self.objects) do
o:draw()
end
for k,o in pairs(self.indestructibles) do
o:draw()
end
end
function Universe:moveEverything()
for k,o in pairs(self.objects) do
o:move()
end
for k,o in pairs(self.indestructibles) do
o:move()
end
end
And there are two initializers, in init
and startGame
. I’ll spare you those,
Now we want to do the add and delete as we do for objects
:
function Universe:addIndestructible(object)
self.indestructibles[object] = object
end
function Universe:deleteIndestructible(object)
self.indestructibles[object] = nil
end
We don’t have to buffer the indestructibles because we don’t add things while we’re looping over them. (Note that this could be a dangerous decision, but I’m here and I’m sure even if I’m not correct.)
Now at this point we should be able to edit Splat to use these. Let’s do it and then see if it works. Here’s the old version:
-- 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
And here’s the new:
-- Splat
-- RJ 20200521
-- moved to U.indestructibles 20200612
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)
}
Splat = class()
function Splat:init(pos)
local die = function()
U:deleteIndestructible(self)
end
self.pos = pos
U:addIndestructible(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
function Splat:move()
end
I remembered to add the move
. Let’s see what I forgot.
Well, I forgot to remove drawSplats
from draw
. I don’t think a test would have helped with that, fwiw.
Everything works fine, the spats show up just as before.
In removing the call to drawSplats
, I noticed drawScore
and wonder whether the score should become a drawn indestructible. We also have the display that shows how many ships you have to do as well. We may get more use out of this collection than I foresaw.
Now what about the Explosion? We’re here to improve it, remember, and this refactoring is more or less in aid of doing that. Explosion is quite simple, and its init
looks like this:
function Explosion:init(ship)
local f = function()
U:deleteObject(self)
end
self.pos = ship.pos
self.step = vec2(0,0)
U:addObject(self)
tween(4, self, {}, tween.easing.linear, f)
end
It seems clear that we can change those two calls to addObject
and be good to go. And I’ll remove the collision logic from here as well.
-- Explosion
-- RJ modified 20200612 indestructible
Explosion = class()
function Explosion:init(ship)
local f = function()
U:deleteIndestructible(self)
end
self.pos = ship.pos
self.step = vec2(0,0)
U:addIndestructible(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
function Explosion:move()
end
I expect this to work … and it does. the Saucer even got in a lucky shot on me, so I got to see that BLAMMO as well as the other.
Now for the fun, at last. Sorry to keep you waiting. I hope you aren’t too excited, this isn’t quite like a pony for Christmas.
Explosion, finally …
In my Codea spacewar game, I had to build an explosion for the ships. In the spirit of reuse, I’m going to port that code into Asteroids.
In Spacewar, the code looks like this:
Explosion = class(Thing)
function Explosion:init(pos, color)
self.pos = pos
self.color = color
self.life = 10
for i = 1,5 do
local f = Fragment(self.pos, self.color, i==1)
U:addObject(f)
end
end
function Explosion:draw()
self.life = self.life - DeltaTime
if self.life <= 0 then
U.GameOver = true
end
end
Fragment = class(Thing)
function Fragment:init(pos, color, guy)
self.pos = pos
self.color = color
self.frag = not guy
self.base = vec2(0,0)
self.ang= math.random(360)
self.step = vec2(math.random(3),0):rotate(self.ang)
self.spin = math.random(9)
self.ds = 8*math.random()
self.life = 4
end
function Fragment:move()
self.pos = self.pos + self.step
self.pos.x = self:range(0,self.pos.x,WIDTH)
self.pos.y = self:range(0,self.pos.y,HEIGHT)
self.spin = self.spin + self.ds
end
function Fragment:draw()
self.life = self.life - DeltaTime
if self.life < 0 then
U:removeObject(self)
end
stroke(self.color)
strokeWidth(2)
noFill()
translate(self.pos.x, self.pos.y)
rotate(self.spin)
if self.frag then
line(-10,0,10,0)
line(10,0,5,13)
else
scale(4,4)
ellipse(0,5,8)
line(0,3,0,-2)
line(-4,2,4,2)
line(0,-2,-3,-5)
line(0,-2,3,-5)
end
end
Now here, the Explosion has two purposes, it creates the Fragments, and then it times out and calls game over. We may indeed have that concern, but we do things a bit differently around here.
Splat works a bit differently, because although it looks like a bunch of dots moving independently, it’s really all just one pattern. Here, the fragments are independent, and they aren’t all alike. Let’s see how it goes. First, I’ll just import Fragment as it is into Asteroids, then fit it into our scheme.
I think this might do the job:
-- Fragment
-- RJ 20200612 from Spacewar
Fragment = class()
function Fragment:init(pos, guy)
self.pos = pos
self.color = 255
self.frag = not guy
self.base = vec2(0,0)
self.ang= math.random(360)
self.step = vec2(math.random(3),0):rotate(self.ang)
self.spin = math.random(9)
self.ds = 8*math.random()
self.life = 4
U:addIndestructible(self)
end
function Fragment:move()
self.pos = self.pos + self.step
self.pos.x = self.pos.x%WIDTH
self.pos.y = self.pos.y%HEIGHT
self.spin = self.spin + self.ds
end
function Fragment:draw()
self.life = self.life - DeltaTime
if self.life < 0 then
U:deleteIndestructible(self)
end
stroke(self.color)
strokeWidth(2)
noFill()
translate(self.pos.x, self.pos.y)
rotate(self.spin)
if self.frag then
line(-10,0,10,0)
line(10,0,5,13)
else
scale(4,4)
ellipse(0,5,8)
line(0,3,0,-2)
line(-4,2,4,2)
line(0,-2,-3,-5)
line(0,-2,3,-5)
end
end
And now I can change Explosion to create the Fragments and forget about them. It should go like this:
-- Explosion
-- RJ modified 20200612 indestructible
-- RJ modified 20200612 Fragments
Explosion = class()
function Explosion:init(ship)
local pos = ship.pos
for i = 1,5 do
local f = Fragment(pos, i==1)
end
end
After a little tweaking, I settle on this for Fragment:
-- Fragment
-- RJ 20200612 from Spacewar
Fragment = class()
function Fragment:init(pos, guy)
self.pos = pos
self.color = 255
self.frag = not guy
self.base = vec2(0,0)
self.ang= math.random(360)
self.step = vec2(2.0*math.random(),0):rotate(self.ang)
self.spin = math.random(9)
self.ds = 8*math.random()
self.life = 4
U:addIndestructible(self)
end
function Fragment:move()
U:moveObject(self)
self.spin = self.spin + self.ds
end
function Fragment:draw()
self.life = self.life - DeltaTime
if self.life < 0 then
U:deleteIndestructible(self)
end
pushStyle()
pushMatrix()
stroke(self.color)
strokeWidth(2)
noFill()
translate(self.pos.x, self.pos.y)
rotate(self.spin)
scale(1)
if self.frag then
line(-10,0,10,0)
line(10,0,5,13)
else
scale(3)
strokeWidth(1)
ellipse(0,5,8)
line(0,3,0,-2)
line(-4,2,4,2)
line(0,-2,-3,-5)
line(0,-2,3,-5)
end
popMatrix()
popStyle()
end
The Explosion class instances are now just created for long enough to create the fragments. I’m tempted to fold the two together, but for now, I’ll let it be. It might come in handy and it’s doing no harm.
I’ll close with a photo and a video of the explosion, so you can see why I like it well enough to reuse it.
Commit: “new explosion, separate drawing for indestructibles”.