Asteroids 5: A ship on the horizon.
OK, Asteroids doesn’t have a horizon, but today we’ll start on the ship. Shouldn’t be too hard: it’s a triangle.
The old Asteroids game starts with a ship in the center of the screen. The ship is a simple triangle, because Asteroids used a vector scope and with a vector scope you don’t have a lot of time to be messing about. So we’ll do the same.
The triangle, I reckon, will be easy enough to draw, and it’ll give us a chance to explore Codea’s graphics transforms. Just because I’m doing an homage to Asteroids doesn’t mean I can’t use the tools of today.
Controlling the ship will be an issue. I think all we’ll be able to do is make buttons on the screen, because Codea’s keyboard handling is nearly non-existent. It’d be nice to be able to drive it from the actual keyboard but as far as I know, that’s not possible. I’ll double-check along the way.
Anyway, for now we want to draw our triangle ship, and place it in the middle of the screen. Here’s our current program:
-- Asteroids
-- RJ 20200511
local Asteroids = {}
local Vel = 1.5
function setup()
print("Hello Asteroids!")
for i = 1,10 do
table.insert(Asteroids, createAsteroid())
end
end
function createAsteroid()
local a = {}
a.pos = vec2(math.random(WIDTH), math.random(HEIGHT))
a.angle = math.random()*2*math.pi
return a
end
function draw()
pushStyle()
background(40, 40, 50)
drawAsteroids()
popStyle()
end
function drawAsteroids()
pushStyle()
stroke(255)
fill(0,0,0, 0)
strokeWidth(2)
rectMode(CENTER)
for i,asteroid in ipairs(Asteroids) do
drawAsteroid(asteroid)
moveAsteroid(asteroid)
end
popStyle()
end
function drawAsteroid(asteroid)
rect(asteroid.pos.x, asteroid.pos.y, 120)
end
function moveAsteroid(asteroid)
local step = vec2(Vel,0):rotate(asteroid.angle)
local pos = asteroid.pos + step
asteroid.pos = vec2(putInBounds(pos.x, WIDTH), putInBounds(pos.y, HEIGHT))
end
function putInBounds(value, bound)
return (value+bound)%bound
end
Looking at that code, I don’t like the name putInBounds
. I think it should be, maybe, keepInBounds
. Let’s change it. I had to change the microtests as well. Consider it done. Commit; “keepInBounds”.
OK, no more excuses. I think I’ll make the ship a separate table. One could make it the first asteroid, but that seems messy. It’ll be a table much like the asteroids though. I expect it winds up with position, velocity, rotation, and maybe a fuel supply or ammo supply. Remains to be seen. Looking forward to its bullets, I expect to keep those in a separate collection for ease of collision checking. But that’s a ways out.
I’ll try to keep from adding things to the ship before we need them, not because we can’t guess pretty accurately, but because I like to show that deferring these things isn’t harmful. It’s easy to say “well, we’re working on this, and we’re gonna need X Y Z, it’s better to do them now”. In my view, it’s not all that much better, and often we guess wrong. Plus it slows us down to write more code than we need today, and sometimes we even manage to get a defect in there.
You get to decide how you work, but reading these articles you get to see what happens if we defer unneeded work until we do need it.
OK, really, no more excuses. A ship. First, this:
-- Asteroids
-- RJ 20200511
local Asteroids = {}
local Vel = 1.5
local Ship = {}
function setup()
print("Hello Asteroids!")
Ship.pos = vec2(WIDTH, HEIGHT)/2
for i = 1,10 do
table.insert(Asteroids, createAsteroid())
end
end
function draw()
pushStyle()
background(40, 40, 50)
drawShip()
drawAsteroids()
popStyle()
end
function drawShip()
pushStyle()
popStyle()
end
This doesn’t do anything yet, but I wanted to comment. I added the call to drawShip
before I started to code it. I expressed my intention to do it that way. It’d be possible, of course, to write drawShip
first: it is the more interesting problem. But I often find that writing the easy parts of the code helps keep things – sorry – shipshape.
Also note that I put the push and pop of style into the drawShip function first. Isn’t this a case of “you’re not gonna need it”? Well, yes, it is. I also find that I often forget the push and pop, or worse, the pop, so it’s best to put them in as soon as I think of it. Now to draw.
I mentioned graphics transforms. We draw the asteroids by computing where they are, and then drawing our shape (a rectangle for now) at that location:
rect(asteroid.pos.x, asteroid.pos.y, 120)
There is another way that’s often better. Let me write the code so we can talk about it more clearly:
function drawShip()
local sx = 10
local sy = 6
pushStyle()
pushMatrix()
translate(Ship.pos.x, Ship.pos.y)
strokeWidth(2)
stroke(255)
line(sx,0, -sx,sy)
line(-sx,sy, -sx,-sy)
line(-sx,-sy, sx,0)
popMatrix()
popStyle()
end
Note that I’ve pushed and popped “matrix” as well as style. Then note the translate
call. That call changes the drawing origin from wherever it was to the ship’s current position. (Which, right now, is in the center. Later on, of course, it could be anywhere.) All drawing is done relative to that point. The calls to line, expanded, are:
line(10,0, -10,6)
line(-10,6, -10,-6)
line(-10,-6, 6,0)
But they draw, not at the origin in the lower left, but at the center of the screen, because that’s where we’ve translated to. I eyeballed the values. Started with 5 and 3, that was too small, doubled to 10 and 6, and the ship looks like this:
Time to commit, don’t you think? “Ship appears”.
The next real issue is to control the ship, but first I want to see it rotate, just for a sense of whether it looks OK. I’ll just give it an angle and adjust it in drawShip
for now:
function setup()
print("Hello Asteroids!")
Ship.pos = vec2(WIDTH, HEIGHT)/2
Ship.ang = 0
for i = 1,10 do
table.insert(Asteroids, createAsteroid())
end
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()
Ship.ang = Ship.ang + 1
end
I initialized Ship.ang
to zero (degrees), and in drawShip
, added the rotate call to change the angle at which things are drawn to the ship’s current angle. Then I just drew as usual. Then, at the end of the draw, I incremented the angle by 1 (degree). The result is pretty fine:
That’s pretty close to the right rotation speed, I think. Anyway we can tune that as we move forward. Time to commit: “ship rotating”.
Now we need some cleanup. We need a createShip function and a moveShip function similar to the asteroids functions. This is a good time to make those spaces for work to be done:
-- Asteroids
-- RJ 20200511
local Asteroids = {}
local Vel = 1.5
local Ship = {}
function setup()
print("Hello Asteroids!")
for i = 1,10 do
table.insert(Asteroids, createAsteroid())
end
createShip()
end
function createAsteroid()
local a = {}
a.pos = vec2(math.random(WIDTH), math.random(HEIGHT))
a.angle = math.random()*2*math.pi
return a
end
function createShip()
Ship.pos = vec2(WIDTH, HEIGHT)/2
Ship.ang = 0
end
function draw()
pushStyle()
background(40, 40, 50)
drawShip()
moveShip()
drawAsteroids()
popStyle()
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()
local delta = 0
if CurrentTouch.state == BEGAN then
if CurrentTouch.pos.x < WIDTH/2 then
delta = 1
else
delta = -1
end
end
if CurrentTouch.state == ENDED then
delta = 0
end
Ship.ang = Ship.ang + delta
end
I extracted createShip
(and forgot to call it in the first try), then called moveShip
after drawShjp
in draw
. Then I elaborated moveShip
a bit so that if I touch the screen to the left of center, the ship rotates left, and if I touch to the right, it rotates right. That was just for fun, but I discovered something as well. Here’s the effect:
Maybe you noticed some pauses in the video. Some of them were when I switched from pressing on the left to the right. However, sometimes it just stopped turning even though my finger was well on the screen. Possibly that’s because I set the angle to zero on every draw? Possibly there were other touch events. We’ll want to deal more carefully with the actual buttons, of course, and possibly promote the angle change up into Ship. We’ll see.
All that is for another day. We’ve drawn our ship, and even made it rotate. That’ll do for today. See you next time!
Here’s all the Main code, commit: “rotate both ways”.
-- Asteroids
-- RJ 20200511
local Asteroids = {}
local Vel = 1.5
local Ship = {}
function setup()
print("Hello Asteroids!")
for i = 1,10 do
table.insert(Asteroids, createAsteroid())
end
createShip()
end
function createAsteroid()
local a = {}
a.pos = vec2(math.random(WIDTH), math.random(HEIGHT))
a.angle = math.random()*2*math.pi
return a
end
function createShip()
Ship.pos = vec2(WIDTH, HEIGHT)/2
Ship.ang = 0
end
function draw()
pushStyle()
background(40, 40, 50)
drawShip()
moveShip()
drawAsteroids()
popStyle()
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()
local delta = 0
if CurrentTouch.state == BEGAN then
if CurrentTouch.pos.x < WIDTH/2 then
delta = 1
else
delta = -1
end
end
if CurrentTouch.state == ENDED then
delta = 0
end
Ship.ang = Ship.ang + delta
end
function drawAsteroids()
pushStyle()
stroke(255)
fill(0,0,0, 0)
strokeWidth(2)
rectMode(CENTER)
for i,asteroid in ipairs(Asteroids) do
drawAsteroid(asteroid)
moveAsteroid(asteroid)
end
popStyle()
end
function drawAsteroid(asteroid)
rect(asteroid.pos.x, asteroid.pos.y, 120)
end
function moveAsteroid(asteroid)
local step = vec2(Vel,0):rotate(asteroid.angle)
local pos = asteroid.pos + step
asteroid.pos = vec2(keepInBounds(pos.x, WIDTH), keepInBounds(pos.y, HEIGHT))
end
function keepInBounds(value, bound)
return (value+bound)%bound
end