Asteroids 14
A quick report on a short project last night, then let’s try to get something nearly playable.
Rirst I need to tell you what I did last night while hiding from a Hallmark thing on the TV. I read a note from Dave1707 of the Codea forum, who tried scaling the Splat dots down from large to small as the Splat expanded. I tried it, liked the look, and added the effect to my tween:
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
I also added a random angle, self.rot
to the Splat, so that each one is rotated differently on the screen, making them look different at low cost. The effect is very nice:
So that’s my report on my secret work. As you can imagine, since there were only about 4 lines of code involved, it went rather smoothly.
What’s Next?
I’d actually like to be able to play this game sometime soon. It might actually still be fun. To make it playable, we need to be able to shoot and break up asteroids. Ideally (!) we’d also be able to fly around avoiding them. And of course asteroids have to kill the ship; if they hit it.
There are lots of other details to make it a decent game, including things like scoring, the large and small saucers, limiting the top speed of ship and missiles, some sound effects, free additional ships for scoring enough points, and so on. But shooting the asteroids seems to be the most important to me right now.
The original game allowed only four missiles on screen at a time, and required that you press the fire button once per missile. No just holding it down and hosing missiles about like a hoonigan.
Well, just musing here, I suppose a missile will be a little circle dot, and it’ll move in a straight line, because we have no gravity here. The code for timing how long shots live looks like this on the 6502:
LDA #$12
That’s 18 decimal. The units are “frames”, which somewhere I believe I read were about 0.16 seconds. So a missile will live about 3 seconds. That seems short to me but I don’t have a real arcade version here to check.
So, let’s see. We’ll make a Missile object, if you don’t mind, since we had good luck with the Splat. We’ll give them some velocity, in the direction the ship is pointing, maybe time them out with a tween.
I feel a bit unsure of this but I think we can find our way. I want to start with the firing button, which needs to fire only single shots. Normally I’d defer that but I’m going to want to be able to watch a missile fly, so firing just one will help with that.
Fire One
Right now, Button.fire
is true if the fire button is down, and false if it’s not. Since firing is a ship thing, we’ll check it there. I think I’ll have a ship state variable called holdFire
, set true when we fire a round, and set false whenever Button.fire
is false. If we do this in the right order, we should be OK.
Note: timing is tricky, we’ll have to be careful here.
So there’s not much to Ship right now:
-- Ship
-- RJ 20200520
local Ship = {}
function createShip()
Ship.pos = vec2(WIDTH, HEIGHT)/2
Ship.ang = 0
end
function drawShip()
local sx = 10
local sy = 6
pushStyle()
pushMatrix()
translate(Ship.pos.x, Ship.pos.y)
rotate(Ship.ang)
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.ang = Ship.ang + 1 end
if Button.right then Ship.ang = Ship.ang - 1 end
end
I think I’ll start by expressing my intentions inside moveShip
, which now has kind of an odd name. We’ll worry about that later.
function moveShip()
if Button.left then Ship.ang = Ship.ang + 1 end
if Button.right then Ship.ang = Ship.ang - 1 end
if Button.fire then if ~Ship.holdFire then fireMissile() end end
if ~Button.fire then Ship.holdFire = false end
end
That’s a bit odd but I think it’s right. Now to fire something in fireMissile. I propose to trigger a Splat, which will tell me whether it’s working. I’m also going to dial down the number of asteroids to 1, just to keep the screen uncluttered.
function fireMissile()
Splat(Ship.pos)
end
Yeah right, well, ~
doesn’t apply to booleans, we have to say not
:
function moveShip()
if Button.left then Ship.ang = Ship.ang + 1 end
if Button.right then Ship.ang = Ship.ang - 1 end
if Button.fire then if not Ship.holdFire then fireMissile() end end
if not Button.fire then Ship.holdFire = false end
end
Try again. OK, that was nearly good, but I forgot to set holdFire
. Here’s the “final” code:
function moveShip()
if Button.left then Ship.ang = Ship.ang + 1 end
if Button.right then Ship.ang = Ship.ang - 1 end
if Button.fire then if not Ship.holdFire then fireMissile() end end
if not Button.fire then Ship.holdFire = false end
end
function fireMissile()
Ship.holdFire = true
Splat(Ship.pos)
end
And it works as advertised:
Commit this: “Ship fires splats”. Sweet.
“Real” Missiles
Let me comment a bit on what we just did. We took a small slice of the missile-firing problem, managing the toggle of the fire
button and triggering some action, and built that part. We left aside the problem of creating a new kind of flying object, giving it a speed and direction and lifetime and so on.
And by building that slice, we know rather exactly where the logic for creating the missile goes: right there where it says “Splat” now.
To me, picking small slices of the problem and building them one at a time is one of the key skills in good development, especially the kind of development we call “Agile”. An Agile team has a running tested version of the program always ready to ship, long before it has enough features to be fully useful. The key to doing that is to slice off small bits of functionality that can add up to larger and more complete bits, and to build those slices robustly enough to serve as foundation for the next slices.
That’s what we did here, and developing slicing skill is, in my ancient if not wise opinion, one of the most important things a developer can learn.
OK, homily over. Open your missals to “Missile”. (haha) Here’s my plan for firing the Missile:
function fireMissile()
Ship.holdFire = true
Missile(Ship)
end
Note that I’m passing in the entire Ship to the creation of the Missile. I’ve decided that the Missile is clearly part of the ship and so it can look at anything it wants in order to fire itself properly. We’ll see how that works out.
I’m going to make Missile a class, as with Splat. I like how Splat turned out and I expect to like Missiles as well. We create a new class and get the standard template:
Missile = class()
function Missile:init(x)
-- you can accept and set parameters here
self.x = x
end
function Missile:draw()
-- Codea does not automatically call this method
end
function Missile:touched(touch)
-- Codea does not automatically call this method
end
I’ll edit that to my liking:
-- Missile
-- RJ 20200522
Missile = class()
function Missile:init(ship)
end
function Missile:draw()
end
The missile has a starting position, and a velocity. Strictly it should start out in front of the ship a bit and of course its velocity should be whatever standard missile velocity is, plus the ship’s velocity. Just now, ships don’t have velocity, so we’ll have to deal with that later.
(The alternative might be to guess at how the ship will work, or to divert to solving ship velocity. I’m not going to do that, because I am a bear of very little brain and don’t need any distractions.)
Asteroid velocity is defined as 1.5 (somethings per something) and it seems like a missile should be able to shoot an asteroid in the back, so let’s try something like half again as large as that. Maybe 2.0.
We now have a new problem that we didn’t have before, we have a magic number that is even more magic than the 1.5 for Vel. We’ll certainly want to come up with some way to consolidate all our key constants someday. That day is not today.
The missile velocity will therefore be vec2(2,0) times the ship rotation, or I miss my guess. (Which is entirely possible.) So, Missile …
function Missile:init(ship)
self.pos = ship.pos
self.vel = vec2(MissileVelocity,0):rotate(math.rad(ship.ang))
end
Now to put this into a missile collection and fly it around. For now I’ll let it live forever, and not worry about the four at a time rule. It’ll be enough to get it drawn. OK, I took a pretty big flyer here, so I’d better admit it so you can mock me when it explodes, or high-five me when it works.
-- Missile
-- RJ 20200522
local Missiles = {}
function drawMissiles()
pushStyle()
pushMatrix()
fill(255)
stroke(255)
for i,missile in iPairs(Missiles) do
ellipse(missile.pos.x, missile.pos.y, 2)
end
popMatrix()
popStyle()
for i, missile in iPairs(Missiles) do
missile.pos = missile.pos + missile.vel
end
end
Missile = class()
local MissileVelocity
function Missile:init(ship)
self.pos = ship.pos
self.vel = vec2(MissileVelocity,0):rotate(math.rad(ship.ang))
table.insert(Missiles, self)
end
function Missile:draw()
end
I’m doing the drawing in the top level drawMissiles
function, and the moving as well. We need to normalize the drawing and moving logic overall but for now this might just do it. Here goes.
Oh, yeah. I didn’t call drawMissiles from anywhere. Not much use creating them. In Main:
function draw()
checkButtons()
--displayMode(FULLSCREEN_NO_BUTTONS)
pushStyle()
background(40, 40, 50)
drawButtons()
drawShip()
moveShip()
drawMissiles()
drawAsteroids()
drawSplats()
popStyle()
end
Dammit! It’s ipairs
, not iPairs
. Dammit!
This sort of works. At first I thought it didn’t, but then I looked more closely. When I press “fire”, a small dot appears in the middle of the ship, and doesn’t move. Something has gone wrong with the velocity calc or the motion.
OK um, well, I forgot to define MissileVelocity as 2.0. Now then:
Whee, that fires a round right across the screen. The round doesn’t wrap around, because we didn’t tell it to. And I think I’d like it to be a bit larger, if only so it’ll show up in the movies I’m posting.
And that works and looks nearly good:
Time to commit: “Unlimited missiles”
OK, now I’d like the missiles to die. I had in mind using tween to set a timer and tell the missile to die, which would cause it to remove itself from the Missiles
. That’s nearly a good idea.
I built Missiles as an array, using table.insert
. I thought I’d “just” remove the individual Missile at the time of its death, and that I’d use the array size directly to control how many Missiles could fly at once. Trouble is, you dare not remove an array element while you’re iterating over the array. Removal scootches everything up a slot, so you wind up missing elements and probably processing a nil at some point.
So I’ll go to the form I used with Splat, treating Missiles as a keyed hash, and deal with missile count directly. Meh.
Here goes the death part:
-- Missile
-- RJ 20200522
local Missiles = {}
function drawMissiles()
pushStyle()
pushMatrix()
fill(255)
stroke(255)
for k, missile in pairs(Missiles) do
ellipse(missile.pos.x, missile.pos.y, 6)
end
popMatrix()
popStyle()
for k, missile in pairs(Missiles) do
missile.pos = missile.pos + missile.vel
missile.pos = vec2(keepInBounds(missile.pos.x, WIDTH), keepInBounds(missile.pos.y, HEIGHT))
end
end
Missile = class()
local MissileVelocity = 2.0
function Missile:init(ship)
function die()
Missiles.self = nil
end
self.pos = ship.pos
self.vel = vec2(MissileVelocity,0):rotate(math.rad(ship.ang))
Missiles.self = self
tween(3, self, {}, tween.easing.linear, die)
end
function Missile:draw()
end
What is good here is that if I fire a Missile, it dies in 3 seconds. What is not so good is that when I … oh darn, I see it. Can’t say
Missiles.self = self
Must say
Missiles[self] = self
The former sets the key to “self”, so all Missiles have the same key and so there is only one missile. The change shown fixes it, and now we can spray missiles like mad:
I think this is worth a commit: “Spray missiles”
Now Let’s Think
We could clean up this code a bit, making draw and move into methods on Missile, like this:
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
function Missile:draw()
ellipse(self.pos.x, self.pos.y, 6)
end
function Missile:move()
self.pos = self.pos + self.vel
self.pos = vec2(keepInBounds(self.pos.x, WIDTH), keepInBounds(self.pos.y, HEIGHT))
end
It was as easy to do it as to explain it, so I cleaned it that far right now. Commit: Cleaner missile.
We could go further with that but I’m not inclined to it. We could implement the missile limit but I’m inclined to leave them unlimited until the game is somewhat playable.
So what we need now is to split asteroids when they are “hit” by missiles. (The missile should die as well, help me remember that.) When a small asteroid is hit, it has to splat and die. Right now, with auto-splatting, the little ones live forever.
How do we do collisions? Well there may be some incredibly clever way that I don’t know, maybe something with quadtrees or the like, but the good old-fashioned approach of looping over all the stuff and seeing if it has been hit sounds good enough to me. I’m a bit nervous about destroying things in the middle of a double loop, so maybe I’ll do something semi-clever there. Main thing is to find a place to splitAsteroids and plug it in.
I think it needs to happen after moving, and/or before drawing. Let’s do after moving:
Meh: I hate this:
function draw()
checkButtons()
--displayMode(FULLSCREEN_NO_BUTTONS)
pushStyle()
background(40, 40, 50)
drawButtons()
drawShip()
moveShip()
drawMissiles()
drawAsteroids()
drawSplats()
popStyle()
end
As it stands, drawing and moving are interleaved. We should really draw everything, then move everything. For now, I’ll just do collisions at the end of draw.
function draw()
checkButtons()
--displayMode(FULLSCREEN_NO_BUTTONS)
pushStyle()
background(40, 40, 50)
drawButtons()
drawShip()
moveShip()
drawMissiles()
drawAsteroids()
drawSplats()
popStyle()
findCollisions()
end
Now the first issue is that no one knows both where the missiles are and where the Asteroids are: they each have their own collections. We need either to make those global (which is what I’m going to do right now), or we need to create some kind of singleton universe that holds all our stuff. The latter is better, the former is closer to done.
The Universe would be a nice place to put all these collection-oriented functions that are lying about. But I’m on a roll here and I’m going to put collisions in right now. I’ll make Missiles
and Asteroids
both public and write findCollisions
in Main.
Here’s my first cut:
function findCollisions()
for i,a in ipairs(Asteroids) do
for k,m in pairs(Missiles) do
if m.pos:dist(a.pos) < KillDist then
a:Split()
m:Die()
end
end
end
end
I need to define KillDist, I’ll make it a local, I don’t know, 50 for now. And while asteroids know how to split, missiles don’t know how to die. Easy enough. And I need to fiddle the asteroids so they don’t die on their own.
Meh. Asteroids aren’t objects yet, so I need to say this:
function findCollisions()
local KillDist = 50
for i,a in ipairs(Asteroids) do
for k,m in pairs(Missiles) do
if m.pos:dist(a.pos) < KillDist then
splitAsteroid(a)
m:Die()
end
end
end
end
I need to stop calling splitAsteroid
from drawAsteroids
, and definitely have a problem destroying the little ones. For now, I think I’ll try letting them live:
function splitAsteroid(asteroid)
if asteroid.scale == 4 then 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
table.insert(Asteroids, new)
Splat(asteroid.pos)
end
Wow, first shot nearly worked. The hit was detected, the asteroid split, the splat started running, and the method is die
not Die
.
Amazing! This actually works. It’s really hard operating with the iPad on an angle for typing, and with the Codea buttons in the way, but asteroids get hit and they split. We’re left with the question of how to kill the little ones. I can’t remove them while they’re being looped over, because they’re an array, not a hash. I guess I can just convert to a hash, can’t I? Let’s commit: Large and medium can die.
With Asteroids
a hash, we can try this:
function splitAsteroid(asteroid)
if asteroid.scale == 4 then
Splat(asteroid.pos)
Asteroids[asteroid] = nil
end
asteroid.scale = asteroid.scale//2
asteroid.angle = math.random()*2*math.pi
local new = createAsteroid()
new.pos = asteroid.pos
new.scale = asteroid.scale
Asteroides[new] = new
Splat(asteroid.pos)
end
I should be able to kill the little ones now. This may take a while, I’m a terrible shot.
Ahem. I forgot to change the loops to use pairs
. Trying again:
Ahem again. Spelling Asteroids correctly, instead of “Asteroides” might help. Again:
That nearly works but I got “invalid key in ‘next’” from inside findCollision, when killing a small one. That tells me that calling die inside the loop was probably the problem.
I think the real solution is that removing things from the collections should be done after all the action is over, as a separate phase. I’ll try creating a death collection for Asteroids, and clearing them at the end of things:
-- Asteroid
-- RJ 20200520
Asteroids = {}
local DeadAsteroids = {}
local Vel = 1.5
function drawAsteroids()
DeadAsteroids = {}
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()
end
function killDeadAsteroids()
for k,a in pairs(DeadAsteroids) do
Asteroids[a] = nil
end
DeadAsteroids = {}
end
function splitAsteroid(asteroid)
if asteroid.scale == 4 then
Splat(asteroid.pos)
DeadAsteroids[asteroid] = asteroid
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
I have high hopes for this. Let me try.
That nearly works. Then I noticed that when I hit a small asteroid, I got a couple of tiny ones. Apparently one ought not drop into the splitting code in splitAsteroid
. I’ll start by tossing in a return, but we’re going to need to improve that code, and a lot more than that code.
Wow. Now they don’t split, but they also don’t die. I’m guessing that DeadAsteroids is cleared too soon or something.
I’m in a bit of trouble here and turning to debugging. Remind me to reflect on how this could have been better tested. Maybe there was something that would have made this better. But I’m sure we’re just moments away from good.
OK. As a wild guess, I removed the redundant clearing of DeadAsteroids
from the beginning of drawAsteroids
. Now it works. I just cleared the whole screen and it only took me about an hour because I’m a terrible player.
I think the bug was that the collision finding is done up in Main, split is called from there, split saves the dead guys in DeadAsteroids, but then they don’t get killed because the draw cycle is over and then the next draw cycle emptied the collection. Another fix would have been to call kill at the beginning of the draw loop.
For now, the game runs and it is time to commit: “Kill all asteroids”. Here’s a veiw of how terrible I am. I blame the controls. Or Chet.
Summing Up
I’m tired, let’s sum up. We’re at a decent working point, with code that’s a bit iffy. The main concern I have right now is about the strange intermixing of drawing, moving, and finding collisions. I think we’ll want to split out those functions and decide what order to run them in. I’m assuming that batching them will be OK: I see no reason why not, and it makes sense to me that we’d want a consistent state for everyone’s positions to do our checking.
Beyond that, the code is a bit messy now. I’d clean it up now, but honestly I’m tired and hungry, so it’s unwise to refactor in my experienced and hungry opinion.
I’m noticing the asymmetry in the code between our newer object-style Splats and Missiles, and the older procedural Asteroids and Ship.
I foresee one change for sure, and one probable.
I feel sure we should convert the individual Ship and Asteroid code to object classes. I think probably we should consolidate all the collection oriented code into a Universe object, but I’m less sure about that. We have multiple collections now, and to a degree we need them, since we want to compare asteroids with missiles, not with each other. But it would be nice, wouldn’t it, if all the things we had to draw were all in one collection and could just be told to move, draw and such? We’ll have to see how that might be done.
There’s plenty to do, but for now, we have a somewhat playable game.
Fun for me and I hope interesting for you. If not, you should have stopped reading sooner.
See you next time!