Asteroids 11: Plug in the new shapes.
Having converted the 6502 asteroid shapes to Codea style, let’s plug them in. This design is starting to bug me, and we’ll think about that too.
Plugging in the new asteroids shapes is as simple as creating a new tab in Codea and pasting in the code generated in the conversion process. Right now, the shapes are defined separately, RR1, RR2, RR3, and RR4. RR stands for RROCK, by the way.
But this code is bugging me
As I tried to bring my uncut hair under some vestige of control this morning, I was thinking about the morning’s work. (It’s about 09:20, by the way, in case we want to consider how long things took this morning.) But I digress. I was thinking, first of all, that I’ll surely want the rock shapes in a table, so that I can easily select one randomly. Right now they are in those four globals, which isn’t very harmful, but it got me thinking about the overall design we have here.
Here is a list of the functions defined in our app so far:
- setup
- createAsteroids
- createAsteroid
- createButtons
- createShip
- draw
- checkButtons
- drawButtons
- drawNewAsteroid
- drawShip
- moveShip
- drawAsteroids
- drawAsteroid
- moveAsteroid
- keepInBounds
- touch
These functions are all in one tab, Main. They are all global functions, visible to each other and to anything else we ever do.
This is the kind of design you get when your thinking is primarily procedural, and when you just sort of begin at one end and program until you’re done or time runs out. Everything all at the top level, and, if you’re a bit careful with the naming, the names reflect something about the design – the design in your head, that’s not very well reflected in the code.
There are six functions with “Asteroid” in their name. Three with “Ship”. Three with “Buttons”. Those names say that there are those kinds of things in our mind, but they’re reflected only very weakly in the code.
If you’re anywhere near typical as a programmer, you’ve seen code like this in real life: some huge file, full of code that does all kinds of random stuff. If you’re like me, you’ve written some code like that. Most of us learn programming in a way that invites us to write code without much structure, because we don’t learn structuring approaches early on. And when we encounter examples, they look like this. And when we encounter working code that we have to maintain, it looks like this.
Further, the more code looks like this, the more defects we insert, just because of the difficulty of grasping it all at once. This kind of code literally bugs us.
This is not the way.1
The Way
We can sometimes find things to do that give us a better sense of organization. It would be easy to put all the Asteroids code in a tab called Asteroids, the Buttons in a Buttons tab, and so on. In Codea, this is the same as separating things out into separate files.
Even here there is an objection from some of us.
With everything in one file, if I want to know what some function or variable is, I just search and there it is. No problem. If we start putting things in separate files, then I hardly know where to look and things are harder to find even if I guess right.
I’m not going to handle that objection, I’m going to ignore it and write code as I think it should be written, explaining why as I go. If you like what you see, maybe give it a try. If you don’t, please continue to program in the best way you know. That’ll be fine with me.
So today’s plan is first to plug in our new rock shapes, and then to begin to push this code into a shape that isn’t quite so monolithic as it is right now.
The Rocks
We have our rock shapes, RR1 and so on. We have a table of asteroids, and each asteroid is itself a table, created like this:
function createAsteroid()
local a = {}
a.pos = vec2(math.random(WIDTH), math.random(HEIGHT))
a.angle = math.random()*2*math.pi
return a
end
For now, let’s just add in a random shape and then edit our drawAsteroid
function to use that shape. I’ll start with a table of rocks:
local Rocks = {RR1,RR2,RR3,RR4}
function createAsteroid()
local a = {}
a.pos = vec2(math.random(WIDTH), math.random(HEIGHT))
a.angle = math.random()*2*math.pi
a.shape = Rocks[math.random(1,4)]
return a
end
Now each asteroid table has a new element, shape
, which is one of the RROCK tables. At this point the program runs the same as before: the shape isn’t being used.
Here’s the draw as it stands now:
function drawAsteroid(asteroid)
rect(asteroid.pos.x, asteroid.pos.y, 120)
end
If we replace this with our drawing code from the conversion, we should be pretty close to good.
First I just plug in the drawTable
from the conversion program:
function drawAsteroid(asteroid)
drawTable(asteroid.pos.x, asteroid.pos.y, asteroid.shape)
end
function drawTable(x,y, tab)
pushMatrix()
pushStyle()
translate(x,y)
scale(10)
strokeWidth(1/10)
for i,l in ipairs(tab) do
line(l.x, l.y, l.z, l.w)
end
popStyle()
popMatrix()
end
Why? Because I want to change as little as possible to get back to working as soon as possible. And with one minor glitch, that works as expected. Here’s a screen shot:
The glitch was something I worried about but tried anyway. In Main, I just wrote that statement:
local Rocks = {RR1,RR2,RR3,RR4}
This didn’t work, the table Rocks was empty. That’s because Codea compiles its tabs in left to right order. As one does, I suppose. Since I have Main first, the table was {nil,nil,nil,nil} and that’s the same as {}.
I moved the Rocks definition into the Shapes tab and left it global and things run fine. It’s another odd design glitch that is part of what’s bugging me.
Anyway, commit: “New Shapes for Rocks”.
Now I’ll fold that drawTable
into drawAsteroid
, remove the code that draws the one in the middle, and then we’ll look for larger prey.
function drawAsteroid(asteroid)
pushMatrix()
pushStyle()
translate(asteroid.pos.x, asteroid.pos.y)
scale(10)
strokeWidth(1/10)
for i,l in ipairs(asteroid.shape) do
line(l.x, l.y, l.z, l.w)
end
popStyle()
popMatrix()
end
So that’s reasonable. It kind of bugs me that Codea isn’t consistent with the use of free-standing x and y coordinates and vector coordinates. Arguably our asteroid table should contain x
and y
rather than pos
. Small issue and perhaps we’ll return to it.
For now …
How can we clean this baby up?
We could begin by doing nothing more than moving separate bits that belong together into separate tabs. I’ve never done that with Codea before: I generally create classes earlier. This Asteroids exercise has been intended to avoid that for a while, letting the code get more procedural, more in-line than I’d usually do.
My thinking is that folks new to Codea often start out with this kind of solution that just grows and gets more and more entangled. I know this because I see on the forum what they’re doing. Often, my more experienced eye (the left one) sees that part of their trouble is just in the organization of the code, not in a failure to know how to write code.
So here, the idea is to get into somewhat bad shape and then move to get out.
Let’s try the tab trick. I’ll move each type of code to its own tab. Then we’ll see what we see. I’ll do the ship first, for no particular reason.
First, though, commit. “Remove Center Rock, improve draw”
Ship
-- Ship
-- RJ 20200520
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
To make this work, I had to make two variables global, Ship
and Button
. Ship is referred to above, as is Button. I think I can move the Ship variable into this file and leave it local. Let’s see.
Yes, that works. Buttons are tricky, because the Button
variable is a shared structure between the buttons and the ship. The buttons set Button.left=true
and so on, and the ship checks them.
We could finesse that in a number of ways. One idea is that the Buttons don’t care what kind of a table they set their values into, so they could slam left
and right
directly into the Ship if we wanted.
But right now, we’re just here to get things in separate tabs. We’ll worry about connections later.
I think I’ll move the buttons next:
-- Button
-- RJ 20200520
Button = {}
local Buttons = {}
function createButtons()
local dx=50
local dy=200
table.insert(Buttons, {x=dx, y=dy, name="left"})
table.insert(Buttons, {x=dy, y=dx, name="right"})
table.insert(Buttons, {x=WIDTH-dx, y=dy, name="fire"})
table.insert(Buttons, {x=WIDTH-dy, y=dx, name = "go"})
end
function checkButtons()
Button.left = false
Button.right = false
Button.go = false
Button.fire = false
for id,touch in pairs(Touches) do
for i,button in ipairs(Buttons) do
if touch.pos:dist(vec2(button.x,button.y)) < 50 then
Button[button.name]=true
end
end
end
end
function drawButtons()
pushStyle()
ellipseMode(RADIUS)
textMode(CENTER)
stroke(255)
strokeWidth(1)
for i,b in ipairs(Buttons) do
pushMatrix()
pushStyle()
translate(b.x,b.y)
if Button[b.name] then
fill(128,0,0)
else
fill(128,128,128,128)
end
ellipse(0,0, 50)
fill(255)
fontSize(30)
text(b.name,0,0)
popStyle()
popMatrix()
end
popStyle()
end
I had to make Touches
global for this to work. Main currently looks like this:
-- Asteroids
-- RJ 20200511
local Asteroids = {}
local Vel = 1.5
Touches = {}
function setup()
print("Hello Asteroids!")
displayMode(FULLSCREEN_NO_BUTTONS)
createButtons()
createAsteroids()
createShip()
end
function createAsteroids()
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
a.shape = Rocks[math.random(1,4)]
return a
end
function draw()
checkButtons()
displayMode(FULLSCREEN_NO_BUTTONS)
pushStyle()
background(40, 40, 50)
drawButtons()
drawShip()
moveShip()
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)
pushMatrix()
pushStyle()
translate(asteroid.pos.x, asteroid.pos.y)
scale(10)
strokeWidth(1/10)
for i,l in ipairs(asteroid.shape) do
line(l.x, l.y, l.z, l.w)
end
popStyle()
popMatrix()
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
function touched(touch)
if touch.state == ENDED or touch.state == CANCELLED then
Touches[touch.id] = nil
else
Touches[touch.id] = touch
end
end
Clearly the next thing is to move the asteroid code to an Asteroid tab:
-- Asteroid
-- RJ 20200520
local Asteroids = {}
local Vel = 1.5
function createAsteroids()
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
a.shape = Rocks[math.random(1,4)]
return a
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)
pushMatrix()
pushStyle()
translate(asteroid.pos.x, asteroid.pos.y)
scale(10)
strokeWidth(1/10)
for i,l in ipairs(asteroid.shape) do
line(l.x, l.y, l.z, l.w)
end
popStyle()
popMatrix()
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
We moved Asteroids
and Vel
into this tab, both local. We now have two global variables, Button, and Touches. Main now looks like this:
-- Asteroids
-- RJ 20200511
Touches = {}
function setup()
print("Hello Asteroids!")
displayMode(FULLSCREEN_NO_BUTTONS)
createButtons()
createAsteroids()
createShip()
end
function draw()
checkButtons()
displayMode(FULLSCREEN_NO_BUTTONS)
pushStyle()
background(40, 40, 50)
drawButtons()
drawShip()
moveShip()
drawAsteroids()
popStyle()
end
function touched(touch)
if touch.state == ENDED or touch.state == CANCELLED then
Touches[touch.id] = nil
else
Touches[touch.id] = touch
end
end
That’s nearly good. I probably should have been committing on each tab’s completion, but anyway let’s do it now: Commit: all separate tabs.
Summing up
We have lovely new shapes for our asteroids, and we have the code all nicely broken out by the kind of thing being addressed, Asteroids, Buttons, Ships, Tests, and Shapes.
I think this is enough for today. The next changes that I presently have in mind will require a fresh mind for me and for you. I’ll include all the code here and call it a day.
--# Main
-- Asteroids
-- RJ 20200511
Touches = {}
function setup()
print("Hello Asteroids!")
displayMode(FULLSCREEN_NO_BUTTONS)
createButtons()
createAsteroids()
createShip()
end
function draw()
checkButtons()
displayMode(FULLSCREEN_NO_BUTTONS)
pushStyle()
background(40, 40, 50)
drawButtons()
drawShip()
moveShip()
drawAsteroids()
popStyle()
end
function touched(touch)
if touch.state == ENDED or touch.state == CANCELLED then
Touches[touch.id] = nil
else
Touches[touch.id] = touch
end
end
--# TestAsteroids
-- TestAsteroids
-- RJ 20200511
function testAsteroids()
CodeaUnit.detailed = true
_:describe("Asteroids First Tests", function()
_:before(function()
-- Some setup
end)
_:after(function()
-- Some teardown
end)
_:test("Hookup", function()
_:expect( 2+1 ).is(3)
end)
_:test("Random", function()
local min = 100
local max = 0
for i = 0,1000 do
local rand = math.random()*2*math.pi
if rand < min then min = rand end
if rand > max then max = rand end
end
_:expect(min < 0.01).is(true)
_:expect(max > 6.2).is(true)
end)
_:test("Rotated Length", function()
for i = 0, 1000 do
local rand = math.random()*2*math.pi
local v = vec2(1.5,0):rotate(rand)
local d = v:len()
_:expect(d > 1.495).is(true)
_:expect(d < 1.505).is(true)
end
end)
_:test("Some rotates go down", function()
local angle = math.rad(-45)
local v = vec2(1,0):rotate(angle)
local rvx = v.x*1000//1
local rvy = v.y*1000//1
_:expect(rvx).is(707)
_:expect(rvy).is(-708)
end)
_:test("Bounds function", function()
_:expect(keepInBounds(100,1000)).is(100)
_:expect(keepInBounds(1000,1000)).is(0)
_:expect(keepInBounds(1001,1000)).is(1)
_:expect(keepInBounds(-1,1000)).is(999)
end)
end)
end
--# Shapes
RR1 = {
vec4(0.000000, 2.000000, 2.000000, 4.000000),
vec4(2.000000, 4.000000, 4.000000, 2.000000),
vec4(4.000000, 2.000000, 3.000000, 0.000000),
vec4(3.000000, 0.000000, 4.000000, -2.000000),
vec4(4.000000, -2.000000, 1.000000, -4.000000),
vec4(1.000000, -4.000000, -2.000000, -4.000000),
vec4(-2.000000, -4.000000, -4.000000, -2.000000),
vec4(-4.000000, -2.000000, -4.000000, 2.000000),
vec4(-4.000000, 2.000000, -2.000000, 4.000000),
vec4(-2.000000, 4.000000, 0.000000, 2.000000)
}
RR2 = {
vec4(2.000000, 1.000000, 4.000000, 2.000000),
vec4(4.000000, 2.000000, 2.000000, 4.000000),
vec4(2.000000, 4.000000, 0.000000, 3.000000),
vec4(0.000000, 3.000000, -2.000000, 4.000000),
vec4(-2.000000, 4.000000, -4.000000, 2.000000),
vec4(-4.000000, 2.000000, -3.000000, 0.000000),
vec4(-3.000000, 0.000000, -4.000000, -2.000000),
vec4(-4.000000, -2.000000, -2.000000, -4.000000),
vec4(-2.000000, -4.000000, -1.000000, -3.000000),
vec4(-1.000000, -3.000000, 2.000000, -4.000000),
vec4(2.000000, -4.000000, 4.000000, -1.000000),
vec4(4.000000, -1.000000, 2.000000, 1.000000)
}
RR3 = {
vec4(-2.000000, 0.000000, -4.000000, -1.000000),
vec4(-4.000000, -1.000000, -2.000000, -4.000000),
vec4(-2.000000, -4.000000, 0.000000, -1.000000),
vec4(0.000000, -1.000000, 0.000000, -4.000000),
vec4(0.000000, -4.000000, 2.000000, -4.000000),
vec4(2.000000, -4.000000, 4.000000, -1.000000),
vec4(4.000000, -1.000000, 4.000000, 1.000000),
vec4(4.000000, 1.000000, 2.000000, 4.000000),
vec4(2.000000, 4.000000, -1.000000, 4.000000),
vec4(-1.000000, 4.000000, -4.000000, 1.000000),
vec4(-4.000000, 1.000000, -2.000000, 0.000000)
}
RR4 = {
vec4(1.000000, 0.000000, 4.000000, 1.000000),
vec4(4.000000, 1.000000, 4.000000, 2.000000),
vec4(4.000000, 2.000000, 1.000000, 4.000000),
vec4(1.000000, 4.000000, -2.000000, 4.000000),
vec4(-2.000000, 4.000000, -1.000000, 2.000000),
vec4(-1.000000, 2.000000, -4.000000, 2.000000),
vec4(-4.000000, 2.000000, -4.000000, -1.000000),
vec4(-4.000000, -1.000000, -2.000000, -4.000000),
vec4(-2.000000, -4.000000, 1.000000, -3.000000),
vec4(1.000000, -3.000000, 2.000000, -4.000000),
vec4(2.000000, -4.000000, 4.000000, -2.000000),
vec4(4.000000, -2.000000, 1.000000, 0.000000)
}
Rocks = {RR1,RR2,RR3,RR4}
--# Ship
-- 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
--# Button
-- Button
-- RJ 20200520
Button = {}
local Buttons = {}
function createButtons()
local dx=50
local dy=200
table.insert(Buttons, {x=dx, y=dy, name="left"})
table.insert(Buttons, {x=dy, y=dx, name="right"})
table.insert(Buttons, {x=WIDTH-dx, y=dy, name="fire"})
table.insert(Buttons, {x=WIDTH-dy, y=dx, name = "go"})
end
function checkButtons()
Button.left = false
Button.right = false
Button.go = false
Button.fire = false
for id,touch in pairs(Touches) do
for i,button in ipairs(Buttons) do
if touch.pos:dist(vec2(button.x,button.y)) < 50 then
Button[button.name]=true
end
end
end
end
function drawButtons()
pushStyle()
ellipseMode(RADIUS)
textMode(CENTER)
stroke(255)
strokeWidth(1)
for i,b in ipairs(Buttons) do
pushMatrix()
pushStyle()
translate(b.x,b.y)
if Button[b.name] then
fill(128,0,0)
else
fill(128,128,128,128)
end
ellipse(0,0, 50)
fill(255)
fontSize(30)
text(b.name,0,0)
popStyle()
popMatrix()
end
popStyle()
end
--# Asteroid
-- Asteroid
-- RJ 20200520
local Asteroids = {}
local Vel = 1.5
function createAsteroids()
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
a.shape = Rocks[math.random(1,4)]
return a
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)
pushMatrix()
pushStyle()
translate(asteroid.pos.x, asteroid.pos.y)
scale(10)
strokeWidth(1/10)
for i,l in ipairs(asteroid.shape) do
line(l.x, l.y, l.z, l.w)
end
popStyle()
popMatrix()
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
-
Mando, private communication. ↩