Asteroids 32 - Saucer?
I’ve decoded how to draw the saucers. Let’s see about making them fly. That will require some research.
I’m getting nearly good at decoding the DVG code that the old Asteroids used for drawing things. It’s unfortunate, perhaps, that I’ve probably decoded the last thing I need to, the saucer. Here’s the work I did in Paper™ to work out the coordinates:
I coded the draw method up so as to check my work, and we’ll see that soon enough.
Today, I plan to at least make the big saucer enter the scene and fly around. I hope, as opposed to plan, to make it fire missiles. My understanding of the game is that the big saucer fires randomly, and the small saucer fires once randomly and thereafter shoots to kill. I’m not sure how good its firing solution was. I can imagine how to do a near perfect job if the ship is moving at a constant rate.
I’d like for the game to behave as much like the original as possible. So this morning will be spent watching some game-play videos, and reading the 6502 code to see what I can figure out. Fortunately for you, though I’ll provide some links, I won’t drag you through my every confused thought.
My favorite video of the game is here, a nine-minute video that shows things pretty clearly. I see some of the ship behavior. If a ship hits an asteroid, it is destroyed. They seem to start at one side of the screen and move toward the other, changing direction a bit, but always heading that way. Changes of direction look like they are of constant angle, perhaps 45 degrees.
I need a better take than that to be sure, but I’m more inclined to program than I am to read 6502 code, so let’s spike in a simple saucer.
Initial Saucer
Here’s the simple class I created last night:
Saucer = class()
function Saucer:init()
end
function Saucer:draw()
pushMatrix()
pushStyle()
translate(500,700)
scale(4)
stroke(255)
strokeWidth(1)
line(-2,1, 2,1)
line(2,1, 5,-1)
line(5,-1, -5,-1)
line(-5,-1, -2,-3)
line(-2,-3, 2,-3)
line(2,-3, 5,-1)
line(5,-1, 2,1)
line(2,1, 1,3)
line(1,3, -1,3)
line(-1,3, -2,1)
line(-2,1, -5,-1)
popStyle()
popMatrix()
end
I’ll just patch in a bit more code to register with U
:
function Saucer:init()
U.saucer = self
end
Now let’s create one at game start time and just let it sit somewhere:
function Universe:startGame(currentTime)
self.currentTime = currentTime
self.attractMode = false
createButtons()
self.ship = Ship()
self.asteroids = {}
self.waveSize = nil
self.lastBeatTime = self.currentTime
Saucer()
self:newWave()
end
function Universe:draw(currentTime)
self.currentTime = currentTime
if self.timeOfNextWave > 0 and self.currentTime >= self.timeOfNextWave then
self:newWave()
end
self.frame64 = (self.frame64+1)%64
self:checkBeat()
--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
if self.saucer then self.saucer:draw() end
self:drawMissiles()
drawSplats()
self:drawScore()
popStyle()
self:findCollisions()
end
The result is a lovely fixed saucer:
I’ll commit this, since there is a whole new class created. “First big saucer”.
Semi-useful Saucer
Now that we know we can draw it, we need to make the saucer somewhat useful. I think the plan should be that it is pretty autonomous. When it’s created, it appears at one edge and flies to the other. First time out, we’ll just go straight across.
When should we create a Saucer? It certainly shouldn’t be very often. How about this? If the number of asteroids is less than three, a saucer appears My guess is that with just this much logic, once a saucer appears we’ll keep getting new ones as soon as the old ones go away, but that’ll be fine for now. Baby steps.
(Yay! My new iPad keyboard just arrived. Brief pause for unboxing.)
Well. As always, the packaging is lovely. I’m now typing on the new keyboard. It’s more like a real keyboard than the old one, and it has a touchpad down below the space bar that works, well, like a touchpad.
I’ll try to learn more later. For now, let’s just carry on as if nothing has happened. Where was I, oh yes, trigger a saucer when there isn’t a saucer and the number of asteroids drops to three or lower.
The hardest part of that will be knowing how many asteroids there are. We’ll have to count them, I guess.
This is all just spiking, so here we go:
I added this near the top of Universe:draw
:
if not self.saucer and self:asteroidCount() <= 3 then
Saucer()
end
And this little function:
function Universe:asteroidCount()
local c = 0
for i,a in self.asteroids do
c = c + 1
end
return c
end
It’s a shame to have to do this but our current scheme for asteroids requires it.
If I’m right, what should now happen is that there should be no Saucer on the screen until I get down to three asteroids and then it should appear and just sit where it was before. I did remember to implement move
, since I foolishly inserted it into the U:draw
. YAGNI. :)
Here goes.
Of course I forgot to call pairs:
function Universe:asteroidCount()
local c = 0
for i,a in pairs(self.asteroids) do
c = c + 1
end
return c
end
I really need some trick to help me remember that. Anyway … with that done, it works like a charm. Takes forever to get down to three asteroids, we need a better scheme for triggering. How about every N seconds after the game starts. We can randomize that as we may need, in due time.
startGame
and draw
already know currentTime
, so let’s have a new value, saucerTime
, init it at startGame
and check and overwrite it in draw
:
if not self.saucer and self.currentTime - self.saucerTime > 30 then
self.saucerTime = currentTime
Saucer()
end
Meh. We need to be in game mode:
if not self.attractMode and not self.saucer and self.currentTime - self.saucerTime > 30 then
self.saucerTime = currentTime
Saucer()
end
That’s a bit nasty but we’re just learning here. Now I expect to get a Saucer after 30 seconds. That works. Now what?
Create the saucer on an edge and fly. I think the rule is that it dies at the other edge. We’ll see about that. Oh, and it occurs to me that the next saucer should appear N seconds after the previous one leaves, not after it starts. We’ll have to see about that as well.
Ah. Our new convention is that objects ask the Universe to destroy them. so that’ll be a good thing here.
With just a bit of fiddling, we wind up as shown below. I set the timer down to ten seconds just so I didn’t have to wait so long to see if it worked.
At this point, the saucer moves too fast, and lasts too long. I like running it on a timer, though, instead of edge checking. But do we really want it wrapping around? Perhaps not.
Anyway, our somewhat messy implementation looks like this:
Saucer = class()
function Saucer:init()
function die()
self:die()
end
U.saucer = self
self.pos = vec2(0, math.random(HEIGHT))
self.vel = vec2(3,0)
if math.random(2) == 1 then self.vel = -self.vel end
tween(15, self, {}, tween.easing.linear, die)
end
function Saucer:draw()
pushMatrix()
pushStyle()
translate(self.pos.x%WIDTH, self.pos.y%HEIGHT)
scale(4)
stroke(255)
strokeWidth(1)
line(-2,1, 2,1)
line(2,1, 5,-1)
line(5,-1, -5,-1)
line(-5,-1, -2,-3)
line(-2,-3, 2,-3)
line(2,-3, 5,-1)
line(5,-1, 2,1)
line(2,1, 1,3)
line(1,3, -1,3)
line(-1,3, -2,1)
line(-2,1, -5,-1)
popStyle()
popMatrix()
end
function Saucer:move()
self.pos = self.pos + self.vel
end
function Saucer:die()
U:deleteSaucer()
end
function Universe:startGame(currentTime)
self.currentTime = currentTime
self.saucerTime = currentTime
self.attractMode = false
createButtons()
self.ship = Ship()
self.asteroids = {}
self.waveSize = nil
self.lastBeatTime = self.currentTime
self:newWave()
end
function Universe:draw(currentTime)
self.currentTime = currentTime
if self.timeOfNextWave > 0 and self.currentTime >= self.timeOfNextWave then
self:newWave()
end
if not self.attractMode and not self.saucer and self.currentTime - self.saucerTime > 10 then
self.saucerTime = currentTime
Saucer()
end
...
if self.ship then self.ship:draw() end
if self.ship then self.ship:move() end
if self.saucer then self.saucer:draw() end
if self.saucer then self.saucer:move() end
self:drawMissiles()
drawSplats()
self:drawScore()
popStyle()
self:findCollisions()
end
function Universe:deleteSaucer(saucer)
self.saucer = nil
self.saucerTime = self.currentTime
end
And it flies across the screen from time to time:
I tweaked the speed and delay time before that video, so it’s a bit better. And we may want to keep it away from the top and bottom edges. All in due time.
Enough for today, let’s sum up.
Summing Up
No big surprises today, just the way we like it. The new rule that objects die by reporting back to the Universe was a good one for us, as the Saucer accounting needs to be done. Right now the saucer is adding itself, I noted, so that should be fixed.
The checking for saucer seems a bit overly complex, and that for the ship isn’t far behind. Makes me think we should have an entirely different drawing function for attract mode.
I think the design held up well for this addition, though with a few more kinds of objects I could imagine we’d want some kind of consolidation of logic. For now, it’s just fine, I think.
The additional saucer logic will wait until I read the code a bit, but it should be straightforward. Its motion and firing will be local. We have to decide whether we want a new kind of missile that can kill the ship and cannot kill asteroids, or whether it should be allowed to kill asteroids.
And the keyboard? I think I like it better, which was the plan. And it’s lighted, which helps.
Here’s the code: see you next time!