Asteroids 15
A report on last night, and a ‘major’ refactoring. P.S. There is no such thing as a major refactoring.
Report
Again last night, I did a bit of programming without concurrent writing. Frankly, it’s much more relaxing. I accomplished three things:
- Adjusted motion speed to draw-cycle time;
- Implemented simple scoring;
- Adjusted asteroid hit radii based on size.
We’ll briefly review how those were done: they’re all rather simple.
Adjusted motion speed
It turns out that Codea’s draw cycle time isn’t always one sixtieth of a second as I thought. On fast enough iPads, like this one, it runs at 1/120 if it can. If drawing lags behind, not finishing in time for the next cycle, it steps down to 1/60. It can even step down to 1/30 if it needs to.
I discovered this when I thought I was observing a variance in speed of things on the screen. I did some tests and it became clear that my iPad was running at 1/120. An inquiry to the Codea forum confirmed the information above.
The speed at which things move in the version we last looked at is based on Vel
, which I set to 1.5 by inspection. That means that a moving object moves a distance of 1.5 in screen coordinates every draw cycle. The screen is 1366 wide by 1024 high. Pixels are “square”, so that a 45 degree angle looks like 45 degrees.
So an object going straight up would need 1024/1.5 cycles, or 683 cycles, to move from bottom to top. Dividing by 120 gives us about 5.7 seconds to cross the screen vertically. Eyeballing the screen tells me that’s about right.
But if we run this code on an iPad that cycles at 1/60, everything slows down by a factor of two. We’d like to do something to speed it back up, and what we can do is move twice as far in each cycle.
Codea maintains a global DeltaTime
, which is the actual time between the previous draw cycle and the current one. The 1/120 cycle time is about 0.0083333 seconds. Suppose we set:
Ratio = DeltaTime/0.0083333
Then if we’re on a slow iPad, Ratio
will be about 0.01666/0.0083333
, or about 2. If we multiply our increments to position by Ratio
, we should get a constant speed for things.
And that’s what I did. At the top of the main draw
, I calculate Ratio:
function draw()
Ratio = DeltaTime/0.0083333
--displayMode(FULLSCREEN_NO_BUTTONS)
checkButtons()
pushStyle()
background(40, 40, 50)
drawButtons()
drawShip()
moveShip()
drawMissiles()
drawAsteroids()
drawSplats()
drawScore()
popStyle()
findCollisions()
end
Only asteroids and missiles move so far. Splats do not move, they just expand. Splats use a tween, which is already enumerated in seconds, so they should be OK. So in the moving code, we have these changes:
function moveAsteroid(asteroid)
local step = Ratio*vec2(Vel,0):rotate(asteroid.angle)
local pos = asteroid.pos + step
asteroid.pos = vec2(keepInBounds(pos.x, WIDTH), keepInBounds(pos.y, HEIGHT))
end
function Missile:move()
self.pos = self.pos + Ratio*self.vel
self.pos = vec2(keepInBounds(self.pos.x, WIDTH), keepInBounds(self.pos.y, HEIGHT))
end
So that’s speed correction, for now. The fact that we have to remember to put it into everything that moves is troubling. Surely I’ll forget something, so it would be good to find a way to centralize that facility.
I also note that the main draw
is getting pretty messy, and it irritates me that moving an asteroid and moving a missile are done differently. I’d like to clean that up.
Simple Scoring and Hit Radius
Asteroids scores 20 for breaking a large asteroid, 50 for a medium, and 100 for a small. I did a quick and dirty scoring feature, like this:
In setup
, I set Score
to zero. Then in findCollisions
, I call score(asteroid)
:
function findCollisions()
local KillDist = 50
for i,a in pairs(Asteroids) do
for k,m in pairs(Missiles) do
if m.pos:dist(a.pos) < killDist(a) then
scoreAsteroid(a)
splitAsteroid(a)
m:die()
end
end
end
end
Then in the Asteroid tab:
function scoreAsteroid(asteroid)
local s = asteroid.scale
local inc = 0
if s == 16 then inc = 20
elseif s == 8 then inc = 50
else inc = 100
end
Score = Score + inc
end
We compute score based on asteroid size and add it into Score. I thought at first of looking the score up in a table but decided to go with the if
for now.
Finally, I display the score near the end of draw
, calling drawScore
, which looks like this:
function drawScore()
local s= "000000"..tostring(Score)
s = string.sub(s,-5)
fontSize(100)
text(s, 200, HEIGHT-60)
end
Convert score to string, slam on leading zeros, take five digits, display. Codea does have a format
function but this seemed more in the spirit of 1979. Here’s a pic:
Notice the circles in each of the asteroids? I popped those in to see whether just checking the distance between missile and asteroid center would suffice for collisions. What we see in that picture is that the implementor of the original Asteroids was very careful to shape them so that a simple distance check would work well.
I did that in three new commits, between 9:30 and 10:00PM last night.
I’ll spare you the code for now and make sure there’s a copy at the end.
And Now What?
Let’s think about what features we need, or may need, and what code improvements are indicated.
Features include ship movement, ship destruction when hit by asteroids, multiple ships per “game”, perhaps a two-player mode, limiting shots to four at a time, hyperspace, and the two saucers that come out and try to kill you. And maybe some sounds. I suppose one could even have an idling mode like the real game. And probably there are more features I’ve forgotten. Ha! Game restarting after all your ships are gone, for one almost forgotten example.
Code improvements include, hmm …
- Normalize Style
- I prefer the object-oriented style of
Splat
andMissile
, and would like to convertAsteroid
andShip
to be compatible with that. - Drawing, Moving, Colliding
- The current approach to drawing and moving and finding collisions is tangled at best. The “right thing” in a video game is to draw everything, move everything, check for collisions and other events, and then loop back to draw everything. We’re working OK now but it’s confusing and likely to lead to mistakes, especially as we add in saucers and such.
- Object Collections
- There’s no uniform handling of the various objects, and there are random global variables around, holding groups of things, or just providing shared access like
Button
does. It would be nice to centralize management of all the “stuff” somehow. That way we’d know where to look for things when we need to change them. - Game Creation
- Everything is set up in, well,
setup
, but there are tables that are initialized only in their declaration, spread among tabs, and other oddities that I’ve surely forgotten but that will turn up when we start looking around - Magic Numbers
- There are magic numbers all around, speeds and ratios and scores and angles. These ought to be brought together in as few places as possible, and perhaps even given better names. If there are required relationships among them, those should be made explicit.
And surely there’s more.
And this, mind you, is the mess we’ve created in less than 500 lines of code, counting blank lines. We may have another 500 to go, at a random guess, and the going won’t get smoother.
So I’d like to spend a bit of time today smoothing out some of these bumps.
Where shall we start?
The Universe
I think I’d like to start at the top, with the universe. We have a number of nearly global structures, mostly tables and some constants. Things are fairly well isolated into tabs now, but there are still the collections at the top of most of the tabs, and some code, like the findCollisions
that knows where to look for things.
Now there are some fancy patterns we could use to pass collections around when needed, but the Codea draw
cycle limits us in that nothing is passed into that function, which means that anything we did pass around would have to be discovered there and passed in. That might still be desirable: we’ll see. Right now the structure isn’t clear enough in my mind for me to imagine a perfect better one.
I propose a new class, Universe,
with one instance, U
, that will contain all the collections, magic constants, and global behaviors that we need. We’ll move things to it slowly, to get a feeling for whether it’s working out.
I think the first thing to move into it will be the Asteroids
collection. I’m thinking it’s big enough to be interesting and hoping that it’s small enough not to be a problem. If it is, we’ll try something else.
So here goes. With the Codea-provided touch
function and the comments removed, and my header comment added, it looks like this:
-- Universe
-- RJ 20200523
Universe = class()
function Universe:init()
end
function Universe:draw()
end
So far so good. My tentative plan is that Universe
has member variables that are the various global tables and values we use throughout the program. We might bury values another level down in something like U.constants.speed
or the like. We’ll see.
My plan is that Main will create the universe and the universe will create everything else. We’ll do this incrementally of course.
In Main:
function setup()
U = Universe()
U:createAsteroids()
Score = 0
--displayMode(FULLSCREEN_NO_BUTTONS)
createButtons()
createShip()
end
I just created a Universe and told it to create the asteroids. If I run now, nothing good happens, because there will be no asteroids. Now to fix that:
function Universe:createAsteroids()
createAsteroids()
end
That works, trivially, because there’s already a function over in Asteroids to do that. I want the Asteroids table inside the Universe. Right now, it’s a local in Asteroid tab, as is DeadAsteroids
, the table that stores moribund asteroids until it is safe to edit the table.
-- Asteroid
-- RJ 20200520
Asteroids = {}
local DeadAsteroids = {}
local Vel = 1.5
function createAsteroids()
for i = 1,4 do
local a = createAsteroid()
Asteroids[a] = a
end
end
My plan is to remove the global Asteroids
here, create a member variable named asteroids
in Universe
, and pass that collection to the creator and whoever else needs it. This will break stuff and Codea doesn’t have much refactoring support, by which I mean none.
Universe = class()
function Universe:init()
self.asteroids = {}
end
function Universe:draw()
end
function Universe:createAsteroids()
createAsteroids(self.asteroids)
end
And in Asteroid tab:
function createAsteroids(asteroids)
for i = 1,4 do
local a = createAsteroid()
asteroids[a] = a
end
end
Now a quick search for who else needs that global. Inside the tab we find a few references. I think I’ll just take them one by one:
function drawAsteroids()
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
This loops over all the asteroids. if we pass it the asteroids collection, it’ll do fine, though it will need to pass it on to the kill function, I reckon. (I’m starting to wish this Asteroid thing was an object but no matter.)
Let’s make Main call U:draw() and begin to move things like moving and drawing the asteroids into U.
function draw()
Ratio = DeltaTime/0.0083333
--displayMode(FULLSCREEN_NO_BUTTONS)
checkButtons()
pushStyle()
background(40, 40, 50)
U:draw()
drawButtons()
drawShip()
moveShip()
drawMissiles()
drawSplats()
drawScore()
popStyle()
findCollisions()
end
In Universe:
function Universe:draw()
drawAsteroids(self.asteroids)
end
And in Asteroid:
function drawAsteroids(asteroids)
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(asteroids)
end
I took a flyer and passed the collection into the kill operation. Better look at it and clean it up:
function killDeadAsteroids(asteroids)
for k,a in pairs(DeadAsteroids) do
asteroids[a] = nil
end
DeadAsteroids = {}
end
There’s one more reference to the old global left in the Asteroid tab (assuming I’ve spotted them all):
function splitAsteroid(asteroid)
if asteroid.scale == 4 then
Splat(asteroid.pos)
DeadAsteroids[asteroid] = asteroid
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
Asteroids[new] = new
Splat(asteroid.pos)
end
The splitAsteroid
function is called from … Main tab:
function findCollisions()
local KillDist = 50
for i,a in pairs(Asteroids) do
for k,m in pairs(Missiles) do
if m.pos:dist(a.pos) < killDist(a) then
scoreAsteroid(a)
splitAsteroid(a)
m:die()
end
end
end
end
Let’s move that to U and have Main just call it. Then we have legit access to the asteroids collection.
function Universe:findCollisions()
for i,a in pairs(self.asteroids) do
for k,m in pairs(Missiles) do
if m.pos:dist(a.pos) < killDist(a) then
scoreAsteroid(a)
splitAsteroid(a, asteroids)
m:die()
end
end
end
end
Note that I’m using the asteroids collection in U twice there, the second time passing it to the splitter.
function splitAsteroid(asteroid, asteroids)
if asteroid.scale == 4 then
Splat(asteroid.pos)
DeadAsteroids[asteroid] = asteroid
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
asteroids[new] = new
Splat(asteroid.pos)
end
At this point I want to run the program and see what explodes. I’m hoping it’s just asteroids.
And it runs. By Jove, I think we’ve done it. One Codea search to see if there is a capital-A “Asteroids” left but I don’t expect to find one … and there is not.
We have successfully created the Universe and have moved some key functions into it, creation of the asteroids, plus their drawing, plus collision detection. We’ve hidden the Asteroids
global, although we’ve replaced it with another global, U
, a much more capable object and a rather nice place for things to live.
That’s enough for today. I’ll sum up, and paste another copy of the code down below the starting version.
In due time, I plan to keep the current version live in a standard place, as suggested by Dave1707, but for today, we’ll keep it here.
Summing Up
We’ve observed some systematic structural concerns in our code. It’s the sort of thing that might have tempted us to do some kind of major rewriting of parts of it. We resisted that bravely, and instead created a very simple object with only four methods, init
, draw
, createAsteroids
, and findCollisions
.
Three of those methods were trivial and the fourth was just moved, with a light edit, from another location. It went smoothly and we were never deeply confused, and things work.
I’m a bit less happy than I’d be if I had lots of micro-tests, but I still see no good way to test the things I’m worried about. Maybe we’ll talk about that next time. In any case the game still plays as before.
So we’ve done an hour’s work or less, and taken a decent first step toward an improved structure. I’ve noticed that we’d have done well to have imported createAsteroids
as we did the find, but we can do that another time. And there’s a lesson there, which is that there’s a perfectly good first step to consolidating things in place, and another step to take if and when we want to.
To me, there is a big lesson here, which is that we can always (OK, nearly always) improve the code in small safe steps, without rewriting or stalling the project while we clean things up. And that’s a much better way to go, because the Powers That Be really don’t like it when projects stall.
I like to say that there is no such thing as a “major refactoring”, and this little example shows us why I say that. There are always small safe steps to be taken. We just have to find them.
See you next time!
The Starting Code
As of starting new work Saturday:
--# Main
-- Asteroids
-- RJ 20200511
Touches = {}
Ratio = 1.0 -- draw time scaling ratio
Score = 0
function setup()
print("Hello Asteroids!")
print(WIDTH, HEIGHT)
Score = 0
--displayMode(FULLSCREEN_NO_BUTTONS)
createButtons()
createAsteroids()
createShip()
end
function draw()
Ratio = DeltaTime/0.0083333
--displayMode(FULLSCREEN_NO_BUTTONS)
checkButtons()
pushStyle()
background(40, 40, 50)
drawButtons()
drawShip()
moveShip()
drawMissiles()
drawAsteroids()
drawSplats()
drawScore()
popStyle()
findCollisions()
end
function drawScore()
local s= "000000"..tostring(Score)
s = string.sub(s,-5)
fontSize(100)
text(s, 200, HEIGHT-60)
end
function findCollisions()
local KillDist = 50
for i,a in pairs(Asteroids) do
for k,m in pairs(Missiles) do
if m.pos:dist(a.pos) < killDist(a) then
scoreAsteroid(a)
splitAsteroid(a)
m:die()
end
end
end
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
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
Missile(Ship)
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
Asteroids = {}
local DeadAsteroids = {}
local Vel = 1.5
function createAsteroids()
for i = 1,4 do
local a = createAsteroid()
Asteroids[a] = a
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)]
a.scale = 16
return a
end
function drawAsteroids()
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 killDist(asteroid)
local s = asteroid.scale
if s == 16 then return 64 elseif s == 8 then return 32 else return 16 end
end
function killDeadAsteroids()
for k,a in pairs(DeadAsteroids) do
Asteroids[a] = nil
end
DeadAsteroids = {}
end
function deathSize()
local i = 0
for k, a in pairs(DeadAsteroids) do
i = i + 1
end
return i
end
function scoreAsteroid(asteroid)
local s = asteroid.scale
local inc = 0
if s == 16 then inc = 20
elseif s == 8 then inc = 50
else inc = 100
end
Score = Score + inc
end
function splitAsteroid(asteroid)
if asteroid.scale == 4 then
Splat(asteroid.pos)
DeadAsteroids[asteroid] = asteroid
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
Asteroids[new] = new
Splat(asteroid.pos)
end
function drawAsteroid(asteroid)
pushMatrix()
pushStyle()
translate(asteroid.pos.x, asteroid.pos.y)
ellipse(0,0,2*killDist(asteroid))
scale(asteroid.scale)
strokeWidth(1/asteroid.scale)
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 = Ratio*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
--# Splat
-- Splat
-- RJ 20200521
local Splats = {}
local Vecs = {
vec2(-2,0), vec2(-2,-2), vec2(2,-2), vec2(3,1), vec2(2,-1), vec2(0,2), vec2(1,3), vec2(-1,3), vec2(-4,-1), vec2(-3,1)
}
function drawSplats()
for k, splat in pairs(Splats) do
splat:draw()
end
end
Splat = class()
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
function Splat:draw()
pushStyle()
pushMatrix()
translate(self.pos.x, self.pos.y)
fill(255)
stroke(255)
rotate(self.rot)
local s = self.size
for i,v in ipairs(Vecs) do
ellipse(s*v.x, s*v.y, self.diameter)
end
popMatrix()
popStyle()
end
--# Missile
-- Missile
-- RJ 20200522
Missiles = {}
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
Missile = class()
local MissileVelocity = 2.0
function Missile:init(ship)
function die()
self:die()
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:die()
Missiles[self] = nil
end
function Missile:draw()
ellipse(self.pos.x, self.pos.y, 6)
end
function Missile:move()
self.pos = self.pos + Ratio*self.vel
self.pos = vec2(keepInBounds(self.pos.x, WIDTH), keepInBounds(self.pos.y, HEIGHT))
end
The Ending Code
Here’s what we finished with, commit: Created Universe:
--# Main
-- Asteroids
-- RJ 20200511
Touches = {}
Ratio = 1.0 -- draw time scaling ratio
Score = 0
function setup()
U = Universe()
U:createAsteroids()
Score = 0
--displayMode(FULLSCREEN_NO_BUTTONS)
createButtons()
createShip()
end
function draw()
Ratio = DeltaTime/0.0083333
--displayMode(FULLSCREEN_NO_BUTTONS)
checkButtons()
pushStyle()
background(40, 40, 50)
U:draw()
drawButtons()
drawShip()
moveShip()
drawMissiles()
drawSplats()
drawScore()
popStyle()
U:findCollisions()
end
function drawScore()
local s= "000000"..tostring(Score)
s = string.sub(s,-5)
fontSize(100)
text(s, 200, HEIGHT-60)
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
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
Missile(Ship)
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 DeadAsteroids = {}
local Vel = 1.5
function createAsteroids(asteroids)
for i = 1,4 do
local a = createAsteroid()
asteroids[a] = a
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)]
a.scale = 16
return a
end
function drawAsteroids(asteroids)
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(asteroids)
end
function killDist(asteroid)
local s = asteroid.scale
if s == 16 then return 64 elseif s == 8 then return 32 else return 16 end
end
function killDeadAsteroids(asteroids)
for k,a in pairs(DeadAsteroids) do
asteroids[a] = nil
end
DeadAsteroids = {}
end
function deathSize()
local i = 0
for k, a in pairs(DeadAsteroids) do
i = i + 1
end
return i
end
function scoreAsteroid(asteroid)
local s = asteroid.scale
local inc = 0
if s == 16 then inc = 20
elseif s == 8 then inc = 50
else inc = 100
end
Score = Score + inc
end
function splitAsteroid(asteroid, asteroids)
if asteroid.scale == 4 then
Splat(asteroid.pos)
DeadAsteroids[asteroid] = asteroid
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
asteroids[new] = new
Splat(asteroid.pos)
end
function drawAsteroid(asteroid)
pushMatrix()
pushStyle()
translate(asteroid.pos.x, asteroid.pos.y)
ellipse(0,0,2*killDist(asteroid))
scale(asteroid.scale)
strokeWidth(1/asteroid.scale)
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 = Ratio*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
--# Splat
-- Splat
-- RJ 20200521
local Splats = {}
local Vecs = {
vec2(-2,0), vec2(-2,-2), vec2(2,-2), vec2(3,1), vec2(2,-1), vec2(0,2), vec2(1,3), vec2(-1,3), vec2(-4,-1), vec2(-3,1)
}
function drawSplats()
for k, splat in pairs(Splats) do
splat:draw()
end
end
Splat = class()
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
function Splat:draw()
pushStyle()
pushMatrix()
translate(self.pos.x, self.pos.y)
fill(255)
stroke(255)
rotate(self.rot)
local s = self.size
for i,v in ipairs(Vecs) do
ellipse(s*v.x, s*v.y, self.diameter)
end
popMatrix()
popStyle()
end
--# Missile
-- Missile
-- RJ 20200522
Missiles = {}
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
Missile = class()
local MissileVelocity = 2.0
function Missile:init(ship)
function die()
self:die()
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:die()
Missiles[self] = nil
end
function Missile:draw()
ellipse(self.pos.x, self.pos.y, 6)
end
function Missile:move()
self.pos = self.pos + Ratio*self.vel
self.pos = vec2(keepInBounds(self.pos.x, WIDTH), keepInBounds(self.pos.y, HEIGHT))
end
--# Universe
-- Universe
-- RJ 20200523
Universe = class()
function Universe:init()
self.asteroids = {}
end
function Universe:draw()
drawAsteroids(self.asteroids)
end
function Universe:createAsteroids()
createAsteroids(self.asteroids)
end
function Universe:findCollisions()
for i,a in pairs(self.asteroids) do
for k,m in pairs(Missiles) do
if m.pos:dist(a.pos) < killDist(a) then
scoreAsteroid(a)
splitAsteroid(a, self.asteroids)
m:die()
end
end
end
end