Asteroids 61
Our marketing people have a challenging request: modernize the look of this game.
As I understand the request, apparently this old vector graphics look has lost favor in the marketplace, and we’ve been asked to give the product a more modern look, with more realistic asteroids and such.
If we were running on real Asteroids hardware, this would be out of the question. Since we’re running on a modern computer, the challenge isn’t quite so impossible.
Our mission, I guess, is to find some modern-looking asteroid, ship, and saucer pictures and put them into the program. Although I used to hate surprises like that back when I wrote software for a living, they’re not so bad now, in part because I know a bit more about writing software that can be changed, and in part because I only have to take on the requests I want to take on, since I’m requesting things of myself, and I’m very reasonable.
We’ll approach this for the sake of the article by finding and plugging in some credible art. For a real product, we’d probably have to hire a graphical artist to make our asteroids, but that’s really not a very difficult thing. Plugging the art in … well, I hope that’s not very difficult either.
For this article, I’ll use available art, from the assets that come with Codea as examples, where possible.
The Plan
Codea graphics has a feature called “sprite”, which is the ability to paint a graphical image at a point on the screen. I plan to get some graphical images and plug in sprites where now we have line graphics.
I’m not sure if I’ll push this effort all the way to the very end or not: the learning will probably come early on and the details may get tedious. We’ll see.
I plan to put this feature on a switch in the startup parameters, and I’ll call the switch Fancy:
function setup()
parameter.boolean("FullScreen", false)
parameter.integer("NumberOfShips", 2,5,4)
parameter.boolean("InfiniteShips", false)
parameter.boolean("NoAsteroids", false)
parameter.boolean("PerfectSaucerShots", false)
parameter.boolean("Fancy", false)
U = Universe()
runTests()
U:newWave()
end
That was easy.
Ship
Let’s start with the ship, spiffing it up for a more modern look.
Codea has a few assets we can look at, and in there I’m sure there’s a proper space ship.
That “Space Art” folder looks interesting:
Look, there are even a couple of asteroids! And a lot of ship stuff, too.
I think I’ll use that red ship. The marketing guy likes red. Let’s look at the ship drawing code:
function Ship:drawAt(pos,radians)
local sx = 10
local sy = 6
pushStyle()
pushMatrix()
translate(pos.x, pos.y)
rotate(math.deg(radians))
strokeWidth(1)
stroke(255)
scale(self.scale or 2)
line(-3,-2, -3,2)
line(-3,2, -5,4)
line(-5,4, 7,0)
line(7,0, -5,-4)
line(-5,-4,-3,-2)
accel = (accel+1)%3
if U.button.go and accel == 0 then
strokeWidth(1.5)
line(-3,-2, -7,0)
line(-7,0, -3,2)
end
popMatrix()
popStyle()
end
I’ll refactor the line-drawing out into a function:
function Ship:drawAt(pos,radians)
local sx = 10
local sy = 6
pushStyle()
pushMatrix()
translate(pos.x, pos.y)
rotate(math.deg(radians))
strokeWidth(1)
stroke(255)
scale(self.scale or 2)
self:drawLineShip()
popMatrix()
popStyle()
end
function Ship:drawLineShip()
line(-3,-2, -3,2)
line(-3,2, -5,4)
line(-5,4, 7,0)
line(7,0, -5,-4)
line(-5,-4,-3,-2)
accel = (accel+1)%3
if U.button.go and accel == 0 then
strokeWidth(1.5)
line(-3,-2, -7,0)
line(-7,0, -3,2)
end
end
That works just fine, of course. Now to put in what we want the Fancy switch to do:
function Ship:drawAt(pos,radians)
local sx = 10
local sy = 6
pushStyle()
pushMatrix()
translate(pos.x, pos.y)
rotate(math.deg(radians))
strokeWidth(1)
stroke(255)
scale(self.scale or 2)
if Fancy then
self:drawSpriteShip()
else
self:drawLineShip()
end
popMatrix()
popStyle()
end
And to do the drawing, if our luck holds out:
function Ship:drawSpriteShip()
sprite(asset.builtin.Space_Art.Red_Ship)
end
I expect this to work, and to be Not Quite Right. Oh, and I’d better init that Fancy switch to true, we’ll be doing this a while.
Well. There it is. It’s a bit large, and it’s facing north. Our ship’s natural direction is facing east, so we need to rotate this sprite by -90, if I’m not mistaken, and scale it way down. I’ll try 0.25 as a starting guess.
function Ship:drawSpriteShip()
rotate(-90)
scale(0.25)
sprite(asset.builtin.Space_Art.Red_Ship)
end
Well that’s just about right, isn’t it? The spacing of the free ships is too close together. Let’s see about that. I believe those are in Score:
function Score:draw()
local s= "000000"..tostring(self.totalScore)
s = string.sub(s,-5)
pushStyle()
fontSize(100)
text(s, 200, HEIGHT-60)
for i = 1,self.shipCount do
Ship:drawAt(vec2(330-i*20, HEIGHT-120), math.pi/2)
end
if self.gameIsOver then
text("GAME OVER", WIDTH/2, HEIGHT/2)
end
popStyle()
end
Well that’s not as clean as it might be, is it? But it’s pretty straightforward, they’re spaced 20 units apart, from the right. As it is now, the wingtip of the next ship is over the center of the preceding ship, so the ship is … 40 wide? We want, oh, 50 if we’re in Fancy mode:
function Score:draw()
local s= "000000"..tostring(self.totalScore)
s = string.sub(s,-5)
pushStyle()
fontSize(100)
text(s, 200, HEIGHT-60)
local step = Fancy and 50 or 20
for i = 1,self.shipCount do
Ship:drawAt(vec2(330-i*step, HEIGHT-120), math.pi/2)
end
if self.gameIsOver then
text("GAME OVER", WIDTH/2, HEIGHT/2)
end
popStyle()
end
And that looks OK:
Well. Fancy Ship Sprite is done. Let’s commit.
Looking Up
Having done the first bit of a relatively new kind of implementation, we’ll do well to lift up our eyes from the keyboard and screen and think about what we’ve just done.
Frankly, it seems to have gone quite swimmingly. We have a new flag, Fancy, and for anything that is supposed to be fancy to meet this new marketing idea, we can refactor the drawing code if needed, and then put in a new sprite version that is triggered by the switch.
One concern might be that we’ll be checking that flag almost everywhere, and that’s messy. It also adds to our work, in that we’ll be typing If Fancy then
a lot. What could we do?
We could at least change the rules so that when Universe
calls draw, it passes in the Fancy
flag. That would reduce references to the global, but it would still leave us with the if-then-else-end things all over.
We could have Universe
do the only check, and call two different draw methods in the various drawn classes, draw()
and drawFancy()
. That’s a bit messy. Take even our simplest drawing, perhaps Missile
as an example:
function Missile:draw()
pushStyle()
pushMatrix()
fill(255)
stroke(255)
ellipse(self.pos.x, self.pos.y, 6)
popMatrix()
popStyle()
end
The actual drawing is the middle three lines of those seven. Maybe that argues for pulling the push and pop into Universe
, who would do them for everyone, reducing people’s draw
functions down to mere drawing, with little or no bookkeeping. That could actually be an improvement over the existing design.
Then, with rare exceptions perhaps, Universe
could just call draw()
or drawFancy()
and everything would just work.
Wow, I’m glad we had this little talk. I hadn’t thought of this at all. And it really helps to have done one example, so that we have a good sense of what it really takes.
(In fact, I’m sure we’re going to have to do something weird with Score
if we do this, but it seems like the right thing to do.)
Let’s put this in and see what it looks like for Ship
, which is surely one of the hardest if not the hardest, because of its quaint drawAt
feature. That’s the hack that lets Score
draw a ship wherever it wants. I foresee trouble on the horizon. Let’s look first at what we’ll want in Universe
:
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
I could build this in place and then refactor it, but I can see that those o:draw()
calls both need to be replaced with code that calls the plain or fancy version. So I’ll invent a new method:
function Universe:drawEverything()
for k,o in pairs(self.objects) do
self:drawProperly(o)
end
for k,o in pairs(self.indestructibles) do
self:drawProperly(o)
end
end
function Universe:drawProperly(anObject)
anObject:draw()
end
And now I’ll make it deal with Fancy:
function Universe:drawProperly(anObject)
if Fancy then
anObject:drawFancy()
else
anObject:draw()
end
end
But that will break everything. So let’s call drawFancy
only if it exists on anObject
. That will make our pretty ship go away but we can fix that readily, and everything else should be just fine:
function Universe:drawProperly(anObject)
if Fancy and anObject.drawFancy then
anObject:drawFancy()
else
anObject:draw()
end
end
This just says that if the Fancy
switch is on and the object has an element named drawFancy
, call it, otherwise call draw
. The game should now run normally.
Well. I had expected the pretty ship to go away but of course it didn’t, because it has its own check for Fancy
. Now let’s give it the new method.
Oh, but wait, I promised that people could count on Universe for the pushes and pops. I’m glad I remembered that:
function Universe:drawProperly(anObject)
pushMatrix()
pushStyle()
if Fancy and anObject.drawFancy then
anObject:drawFancy()
else
anObject:draw()
end
popStyle()
popMatrix()
end
It’s all still good. Let’s fix up Ship
now. Here’s my first try:
function Ship:draw()
self:drawAt(self.pos, self.radians)
end
function Ship:drawAt(pos,radians)
translate(pos.x, pos.y)
rotate(math.deg(radians))
strokeWidth(1)
stroke(255)
scale(self.scale or 2)
self:drawLineShip()
end
function Ship:drawFancy()
translate(self.pos.x, self.pos.y)
rotate(math.deg(self.radians))
rotate(-90)
scale(0.25)
sprite(asset.builtin.Space_Art.Red_Ship)
end
I’m not going to worry about Score
and if it acts up I’ll turn off its ships. I rather expect this to draw the player’s ship as a sprite, and the score ones as triangles.
It does, but … the ship is now very tiny and all the triangle ships are on top of each other for some reason. I’m going to ignore Score
. The scale issue is clear enough, because the original draw has that self.scale or 2 in it. So we know that drawFancy wants scale to be double what it is now:
function Ship:drawFancy()
translate(self.pos.x, self.pos.y)
rotate(math.deg(self.radians))
rotate(-90)
scale(0.5)
sprite(asset.builtin.Space_Art.Red_Ship)
end
I’m beginning to wonder whether Universe
should handle the translation for us, and maybe even the rotation, but we’ll wait and see on that.
I want to think a bit about how to handle Score
, because I’m still not loving the fact that he calls Ship:drawAt
rather than having a ship of his own to draw with. If he had, I think this would be easier.
This brings up another “big” issue or two, the use of Singleton instances, and self-registration on creation. We’ll talk about that later, after a break.
Commit: Ship uses drawFancy. Score borked.
Singleton and Self-Registration
Each of our objects has the responsibility to register itself with the Universe, typically by saying
U:addObject(self)
And some of our objects are singletons, such that only one is supposed to exist (at least one at a time). Such objects have a local variable in their tab, called Instance
, and a method instance()
to return the current instance from the class. This works pretty well … unless you want one of these objects for your own purposes, and then it’s a bit of a pain. This is always an issue with the Singleton pattern: it seems you always have a legitimate need for one of your own, or you need to reset the singleton one for some reason, and so on.
Still, it was the best idea I had at the time, and it has served us well.
However, right now, Score is broken and the fix would be for it to have its own private ship. If it had its own ship, it could set the ship’s position to wherever it wanted to draw the thing and ask the universe to draw it. I think that’s what we want to do now.
Let’s write the code that uses this new feature first, and then make the Ship
class give us a hand. I think, first cut, I want this:
function Score:init(shipCount)
self.shipCount = shipCount or 1
self.gameIsOver = false
self.totalScore = 0
self.nextFreeShip = U.freeShipPoints
self.myShip = Ship:privateShip() -- < ---
Instance = self
U:addIndestructible(self)
end
function Score:draw()
local s= "000000"..tostring(self.totalScore)
s = string.sub(s,-5)
pushStyle()
fontSize(100)
text(s, 200, HEIGHT-60)
local step = Fancy and 50 or 20
for i = 1,self.shipCount do
self.myShip.pos = vec2(330-i*step, HEIGHT-120) -- <---
self.myShip.radians = math.pi/2 -- <---
self.myShip:draw() -- <---
end
if self.gameIsOver then
text("GAME OVER", WIDTH/2, HEIGHT/2)
end
popStyle()
end
Seems like if I had a private ship, I’d be good to go. In Ship, we have this:
function Ship:init(pos)
self.pos = pos or vec2(WIDTH, HEIGHT)/2
self.radians = 0
self.step = vec2(0,0)
self.scale = 2
Instance = self
U:addObject(self)
end
The init is setting up Instance
and registering. The easiest way to get an unregistered private one, at least the easiest I can think of right now, is this:
function Ship:privateShip()
local instance = Instance
local ship = Ship()
U:deleteObject(ship)
Instance = instance
return ship
end
This feels very familiar to me. Have we done it before? I don’t see anything like it in the code. Maybe I dreamed it. Anyway let’s see what we’ve got now.
Curiously, some odd things happen. First, the Score is now displaying one triangle ship and one fancy one. That’s probably because it’s not calling the universe to do the draw,
function Score:draw()
local s= "000000"..tostring(self.totalScore)
s = string.sub(s,-5)
pushStyle()
fontSize(100)
text(s, 200, HEIGHT-60)
local step = Fancy and 50 or 20
for i = 1,self.shipCount do
self.myShip.pos = vec2(330-i*step, HEIGHT-120)
self.myShip.radians = math.pi/2
U:drawProperly(self.myShip)
end
if self.gameIsOver then
text("GAME OVER", WIDTH/2, HEIGHT/2)
end
popStyle()
end
This makes the score show three ships, but there’s still something else going on. I’ve seen two odd things. First, once in a while, early on in the game, there is a mysterious explosion up near Score, as if there were an invisible ship there. Second, if I let the game run, I sometimes see an asteroid collide with the central ship but it doesn’t disappear, as if there are two of them, one on top of the other.
First, I want to clean up the ship drawing code to fully match our new scheme, just to be sure nothing weird is going on there.
function Ship:draw()
translate(self.pos.x, self.pos.y)
rotate(math.deg(radians))
strokeWidth(1)
stroke(255)
scale(self.scale or 2)
self:drawLineShip()
end
function Ship:drawFancy()
translate(self.pos.x, self.pos.y)
rotate(math.deg(self.radians))
rotate(-90)
scale(0.5)
sprite(asset.builtin.Space_Art.Red_Ship)
end
function Ship:drawLineShip()
line(-3,-2, -3,2)
line(-3,2, -5,4)
line(-5,4, 7,0)
line(7,0, -5,-4)
line(-5,-4,-3,-2)
accel = (accel+1)%3
if U.button.go and accel == 0 then
strokeWidth(1.5)
line(-3,-2, -7,0)
line(-7,0, -3,2)
end
end
It’s tempting to fold drawLineShip
back into draw
but we’re on a mission to understand what’s going on.
I spoke earlier about the kinds of trouble one gets into with Singleton, and we’re still in that. We have tried to finesse our way around Singleton to get a private ship and we’ve run into trouble. Soon, it may become clear that we need to revert. But we’ll give ourselves a few minutes to sort this first, if for no other reason than to understand what’s up.
During the test run, I moved the ship away from center and parked it. An explosion happened in the upper left, and shortly thereafter, we had this:
I conclude that there are two ships in there, somehow. But it sure looks like I deleted it:
function Ship:privateShip()
local instance = Instance
local ship = Ship()
U:deleteObject(ship)
Instance = instance
return ship
end
What could make that delete not work? Let’s look around at how the universe is set up. This looks germane:
function Universe:startGame(currentTime)
self.currentTime = currentTime
self.saucerTime = currentTime
self.attractMode = false
self.objects = {}
self.indestructibles = {}
self.waveSize = nil
self.lastBeatTime = self.currentTime
createButtons()
Score(NumberOfShips):spawnShip()
self:newWave()
end
Better look back at what’s going on in Score …
function Score:init(shipCount)
self.shipCount = shipCount or 1
self.gameIsOver = false
self.totalScore = 0
self.nextFreeShip = U.freeShipPoints
self.myShip = Ship:privateShip()
Instance = self
U:addIndestructible(self)
end
function Score:spawnShip()
if InfiniteShips then self.shipCount = self.shipCount + 1 end
if self.shipCount <= 0 then
self:stopGame()
return false
else
self.shipCount = self.shipCount - 1
Ship()
return true
end
end
So Score creates the first ship, which makes sense, since it creates all the others. That also tells me that there should be no existing instance when Score calls privateShip
. I don’t see, though, why there’d be more than one in the objects collection. Once more:
function Ship:privateShip()
local instance = Instance
local ship = Ship()
U:deleteObject(ship)
Instance = instance
return ship
end
A wild idea has appeared!
Ah. When we add an object, we don’t put it directly into U.objects
, because we can’t add while we’re looping (in the collision finder). Therefore, the private ship isn’t added. Therefore the delete is null and later it gets added. Weird.
So for now …
function Ship:privateShip()
local instance = Instance
local ship = Ship()
U:applyAdditions()
U:deleteObject(ship)
Instance = instance
return ship
end
Now that’s nasty. Maybe it was nasty before, but it’s definitely nasty now.
This is shaping up to be a perfect example of the problems you run into when you use the Singleton pattern and then need something beyond it. We’ll want to figure out a way to improve this but first we must make it work.
I think now it works, but we’ll see. Yes, everything seems to be in order.
However.
See up there when I said “Ah”. That’s me, remembering an important fact about the system that is not obvious, and that is, once in a while, absolutely critical to remember. There are always these things that you “have to know” about the system, and it’s one of the biggest issues with a programmer taking up code that is new to her. And, one of the biggest issues even with an experienced programmer in the code base: you just can’t remember everything all the time.
Now, fact is, creating this instance after saving Instance
and deleting it after it’s added, is inherently a crock. We should create the Instance
explicitly when we want it, and not otherwise. Let’s refactor to make that happen:
function Ship:init(pos)
self.pos = pos or vec2(WIDTH, HEIGHT)/2
self.radians = 0
self.step = vec2(0,0)
self.scale = 2
end
function Ship:makeRegisteredInstance(pos)
local ship = Ship(pos)
Instance = ship
U:addObject(ship)
return ship
end
Now Score
better use that:
function Score:init(shipCount)
self.shipCount = shipCount or 1
self.gameIsOver = false
self.totalScore = 0
self.nextFreeShip = U.freeShipPoints
self.myShip = Ship()
Instance = self
U:addIndestructible(self)
end
function Score:spawnShip()
if InfiniteShips then self.shipCount = self.shipCount + 1 end
if self.shipCount <= 0 then
self:stopGame()
return false
else
self.shipCount = self.shipCount - 1
Ship:makeRegisteredInstance()
return true
end
end
This runs and I think we’re good, but I do have one concern. Shouldn’t we deregister the old instance if there is one? I think so, and it should be harmless:
function Ship:makeRegisteredInstance(pos)
if Instance then U:deleteObject(Instance) end
local ship = Ship(pos)
Instance = ship
U:addObject(ship)
return ship
end
That works. Better commit: Score shows ships properly. Ship creation normalized.
I did notice one thing. When the fancy ship materializes in from hyperspace, it doesn’t drop in large and scale down as does the line-drawing one. Here’s why:
function Ship:draw()
translate(self.pos.x, self.pos.y)
rotate(math.deg(radians))
strokeWidth(1)
stroke(255)
scale(self.scale or 2)
self:drawLineShip()
end
function Ship:drawFancy()
translate(self.pos.x, self.pos.y)
rotate(math.deg(self.radians))
rotate(-90)
scale(0.5)
sprite(asset.builtin.Space_Art.Red_Ship)
end
The plain draw
applies scale if it exists and the fancy one doesn’t. Let’s put that back in:
function Ship:drawFancy()
translate(self.pos.x, self.pos.y)
rotate(math.deg(self.radians))
rotate(-90)
scale(self.scale or 2)
scale(0.25)
sprite(asset.builtin.Space_Art.Red_Ship)
end
Now the fancy ship “drops in” like the triangle one.
Running in plain mode, I discovered this:
function Ship:draw()
translate(self.pos.x, self.pos.y)
rotate(math.deg(self.radians)) -- <--- used to lack 'self'
strokeWidth(1)
stroke(255)
scale(self.scale or 2)
self:drawLineShip()
end
Important to test both ways. Also I wonder if there was some way a CodeaUnit test could have caught this. I’ll have to think on that: it’s not obvious how.
Anyway, all good now. Commit: Fancy ship drops in. Fix line ship radians.
It’s 11:15, been a long morning here. Let’s sum up.
Summing Up
We can do this!
Seems to me that we had a good discovery: we can pretty readily put in fancy graphics replacing our line drawings.
We can improve the code while doing it!
And another good discovery: we can simplify everyone’s drawing code by moving some duplicated entries into Universe, while handling most of the Fancy switching there. (There is a bit in Score, but it probably has to get tarted up a bit anyway.)
Singletons and automatic registration are iffy
We also ran into a wonderfully convenient problem, having to do with our approach to having special instances of things, basically the Singleton pattern. When we had to stretch that pattern a bit, to let the Score draw some ships, we ran into exactly the sorts of problems you encounter with Singleton. We have gotten around that issue with an explicit class method to make the special instance, but we use the pattern in other places and we should look carefully at it.
Tacit knowledge is a concern
And we ran into another delightful bump on the road, the need for a clear tacit understanding of how things work, namely the fact that addObject
doesn’t really add the object, it schedules the object for addition later. (Less than a 60th of a second later, but who’s counting?) Even our most experienced programmer forgot that fact at a key moment, and might still be scratching his head had the key fact not popped back into his head.
We’d like to program so that there are very few special facts like that one, that have to be brought to mind once in a while, and that are thought of infrequently enough to ensure that we’ll forget them.
Maybe there are good ways around these two larger issues, and we’ll see if we can make some progress against them. Meanwhile, we have a demo we can run showing a nice modern ship, and we have an understanding of how to do the other bits.
That’s a good morning’s work. See you next time!