Asteroids 33: More saucer!
I think this afternoon I’ll just push a little further on the saucer. Maybe make it dangerous.
I took my break this morning a bit earlier than usual, with the saucer moving but doing little else. Let’s do a special afternoon session to deliver a bit more.
What I have in mind is:
- Shoot some missiles, using the existing ones
- Allow missiles to kill asteroids
- Allow missiles to kill the ship
- Play the ship sound
- See what needs cleaning up
That seems like a lot but I think we can do it. Let’s first see what makes Missiles go.
function Missile:init(ship)
function die()
self:die()
end
self.pos = ship.pos
self.step = U.missileVelocity:rotate(ship.radians) + ship.step
U:addMissile(self)
tween(3, self, {}, tween.easing.linear, die)
end
That almost makes me want to change my mind, the Missile seems too tightly bound to the ship that fires it. For the Saucer, missiles aren’t dependent on the angle of the saucer, though they probably should track its velocity. Their angle should be random.
But not so fast. Could we make some sort of factory methods for ships and for saucers making missiles? It’ll mean changing a few tests but let’s try something.
Could we say Missile:fromShip(aShip)
to create the one, and `Missile:fromSaucer(aSaucer) to create the other? Like this:
function Missile:init(pos, step)
function die()
self:die()
end
self.pos = pos
self.step = step
U:addMissile(self)
tween(3, self, {}, tween.easing.linear, die)
end
function Missile:fromShip(ship)
return Missile(ship.pos, U.missileVelocity:rotate(ship.radians) + ship.step)
end
I’ll plug that in and see it work before moving on.
function Ship:fireMissile()
U:playStereo(U.sounds.fire, self)
self.holdFire = true
Missile:fromShip(self)
end
I expect this to work as before … and it does. I’ll fix the tests and commit: “Missile factory method fromShip”.
That took a bit longer than it should have. I’ll discuss it later. Anyway, now we should be able to fire missiles from the saucer similarly to the ship’s approach.
The ship requires the player to touch the “fire” button once per missile: you can’t hold it down. In the saucer, let’s try firing one every second and see how that is:
function Saucer:move()
self.pos = self.pos + self.vel
if U.currentTime >= self.fireTime then
self.fireTime = U.currentTime + 1
Missile:fromSaucer(self)
end
end
with fireTime
initialized in init
:
function Saucer:init()
function die()
self:die()
end
U:addSaucer(self)
self.pos = vec2(0, math.random(HEIGHT))
self.vel = vec2(2,0)
self.fireTime = U.currentTime + 1 -- one second from now
if math.random(2) == 1 then self.vel = -self.vel end
tween(7, self, {}, tween.easing.linear, die)
end
And in Missile – I’m starting not to like this but we’ll go for now:
That’s odd, I get this message:
Missile:21: bad argument #1 to 'random' (number has no integer representation)
stack traceback:
[C]: in function 'math.random'
Missile:21: in method 'fromSaucer'
Saucer:41: in method 'move'
Universe:73: in method 'draw'
Main:12: in function 'draw'
The code is:
function Missile:fromSaucer(saucer)
return Missile(saucer.pos, U.missileVelocity:rotate(math.random(2*math.pi)) + saucer.step)
end
What do I not understand about math.random? Ah. It expects a integer parameter. This will be better:
function Missile:fromSaucer(saucer)
return Missile(saucer.pos, U.missileVelocity:rotate(math.random()*2*math.pi) + saucer.step)
end
Ah. I called the saucer’s step vel
not step
. We’ve seen that mistake before. NOW will it go?
And it does. It kills asteroids nicely, thank you very much. So far, it can’t kill the ship because missiles aren’t checked against the ship. First commit: “ship fires random missiles”.
Now in findCollisions
:
function Universe:findCollisions()
-- we clone the asteroids collection to allow live editing
local needNewWave = true
for i,a in pairs(clone(self.asteroids)) do
needNewWave = false
self:checkMissileCollisions(a)
if self.ship then self:checkShipCollision(a) end
end
if needNewWave == true then
if self.timeOfNextWave == 0 then
self.timeOfNextWave = self.currentTime + self.timeBetweenWaves
end
end
end
function Universe:checkShipCollision(asteroid)
if self.ship.pos:dist(asteroid.pos) < asteroid:killDist() then
asteroid:score()
asteroid:split()
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
asteroid:score()
asteroid:split()
m:die()
end
end
end
That’s rather intricate, isn’t it? But we should just be able to check the ship (and the saucer) in checkMissileCollisions
:
function Universe:checkMissileCollisions(asteroid)
for k,m in pairs(self.missiles) do
if m.pos:dist(self.ship.pos) < self.ship:killDist() then
self:killShip()
m:die()
end
if m.pos:dist(asteroid.pos) < asteroid:killDist() then
asteroid:score()
asteroid:split()
m:die()
end
end
end
Ship doesn’t have a killDist
so we’ll give it one. The ship length is about 12, at a scale of 2, so:
function Ship:killDist()
return 24
end
Now to run it forever and see if the saucer ever kills the ship. Since it’s firing randomly this could take time. I’ll speed up the frequency.
Saucer’s first shot hit the ship. Lucky shot. Ship exploded, then:
Universe:178: attempt to index a nil value (field 'ship')
stack traceback:
Universe:178: in method 'checkMissileCollisions'
Universe:158: in method 'findCollisions'
Universe:79: in method 'draw'
Main:12: in function 'draw'
I think what’s happening is that we’re still checking and the ship isn’t there to be checked. Must look a bit more deeply. Yes, this fixes it, checking whether the ship exists, which we’ve had to do in other places.
function Universe:checkMissileCollisions(asteroid)
for k,m in pairs(self.missiles) do
if self.ship and m.pos:dist(self.ship.pos) < self.ship:killDist() then
self:killShip()
m:die()
end
if m.pos:dist(asteroid.pos) < asteroid:killDist() then
asteroid:score()
asteroid:split()
m:die()
end
end
end
That test run took forever, but the saucer is nice enough to score some points for me while I wait. It is correctly killing the ship when its random firing finally hits the target. It looks pretty good. Here’s a short video with the saucer interval set very low:
And I found a bug. I knew this would happen but forgot. Early in that video, the ship explodes for no visible reason, unless you notice that I pressed fire. The reason is that the missiles start out at the position of the firing agency. Since missiles are now lethal to the ship, it explodes with the missile still in the barrel. I’ll fix that quickly and then wrap up.
function Missile:fromShip(ship)
local pos = ship.pos + vec2(ship:killDist() + 1,0):rotate(ship.radians)
local step = U.missileVelocity:rotate(ship.radians) + ship.step
return Missile(pos, step)
end
We compute a vector equal to the ship’s kill distance + 1, rotate it to be in front of the ship, and start the missile there. Works well.
Commit: “saucer shots kill ship”.
Summing Up
This went pretty smoothly, all things considered. But we did hit some soft spots.
Microtests
First issue was that after I changed the tests to use the new fromShip
method, I ran them and discovered that the new wave tests were not working. I fixed one and ignored the other.
This is a clear message that I’m not relying very strongly on those tests, since I’m obviously not running them often enough to even notice when one broke.
I’m not surprised at this, because I have trouble with microtests for this kind of program, but I am disappointed. I wish I were smarter than I am. Not the first time for that wish.
Maybe I can think of a way to make the tests more valuable. I suppose it would be possible to run them automatically at the start of every run. That would at least get my attention.
Reusing Missile
I was nearly pleased by how easy it was to get missiles to work for the saucer as well as the ship. The little factory methods do the job. They do have a bit too much knowledge of the ship and saucer now, so they might be better moved over to the corresponding classes. It’s test to keep all the intimate knowledge of an object with the object, and these from
methods know things they probably shouldn’t.
Checking for existence
There are a number of places in the code that check for existence of things, mostly the ship. Certainly we need to deal with the possibility that it isn’t there, but it crops up too often, in about five places.
That suggests to me that we might find something better to do.
Bottom line …
Bottom line, it went pretty well. Lots of tuning needed, and I will want to read the 6502 code but we have a nice little saucer running.
See you next time!