Asteroids 13
First, I’m really feeling the need to improve this code. But maybe it’s too soon to do good work? Plus explosions.
Well, zeroth, I feel like doing the little explosion when the rocks break. To do that we’ll want to explore how the original Asteroids did it, and then decide what to do that gives that old program appropriate honor. I think it’ll be fun.
But, even with everything broken out into tabs, things are still a bit messy and, let’s be realistic, we all know that messy code slows us down. Oh, sure, cleaning it up might slow us down even more, but if our code were somehow magically cleaner tomorrow, we’d go somewhat faster. We’d also go with less stress, due to less concern over breaking something we didn’t quite understand because of the mess.
No little elves are likely to come in overnight and clean up this code. If anything, it seems like goblins come in and mess it up worse than before. So if it’s going to happen, we’re going to have to clean things up ourselves.
If we work on the splat when an asteroid breaks, that’ll probably be mostly new code. In splitAsteroid
, we already create a new asteroid, and we’ll “just” have to create a splat too:
function splitAsteroid(asteroid)
if asteroid.scale == 4 then return end
if math.random(1,960) ~= 1 then 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
table.insert(Asteroids, new)
end
Right after we create the new asteroid, we’ll create a splat and insert it … where? We can’t really put it in the Asteroids table, at least not without a lot of messing about in drawAsteroid
, because a splat isn’t an asteroid, doesn’t crash the ship if it hits it, doesn’t really move so much as expand, and quietly expires when it’s done. Not at all like an Asteroid.
So it’s a new kind of thing, isn’t it? So there probably needs to be a new kind of table, a table of splats, and we’ll create a splat and put it in there. Then up in Main, we have this:
function draw()
checkButtons()
--displayMode(FULLSCREEN_NO_BUTTONS)
pushStyle()
background(40, 40, 50)
drawButtons()
drawShip()
moveShip()
drawAsteroids()
popStyle()
end
And we’ll add drawSplats
to that and implement it and of course drawSplat
. We also note a bit of oddness in drawAsteroids
compared to this code:
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)
splitAsteroid(asteroid)
end
popStyle()
end
There, we do the moving (and splitting) inside the draw operation, while for the ship we do it in the Main draw. That’s inconsistent, and the kind of thing that confuses us when there’s more code than we have now.
Now I might argue, or you might, that, well, we don’t have very much code here, and while it might be confusing if we had a million lines of it, it’s not all that confusing here, because there’s not all that much of it.
Yes, well. That million lines of not very good code got there just the way this code is coming into being, bit by bit. And if we allow this code to be only this good, by the time we get to the size of a real program, it will be slowing us down. So even if we’re not slowed down very much now, we’re not programming as cleanly as we might, and in “real life”, that’s going to bite us.
But maybe we don’t know what to do. Fact is, I don’t know what to do right now. But I do know how to think about the subject, and I do know some Codea practices to apply as needed. Interestingly enough, those Codea ideas have corresponding ideas in any language we might use. Yes, any. So let’s think a bit before we dig in.
Thinking
Have you ever heard someone say that there is no up front design in “Agile”? They may have been saying that as a “proof” that “Agile” can’t work. If you are reading this, you’re seeing Agile work, because what I’m doing is building a working Asteroids program in two hour chunks, and it’s working better at the end of every session.
So Agile does work, even if “Agile” doesn’t. But what about there being no up front design? That’s just not the case. Almost every one of these articles discusses my thinking about design. I’m thinking about design all the time, and I try to write down all those thoughts so that you can see that design isn’t something you do enough of six months ago, it’s something you do enough of today and every day.
I do like to start my day’s work with a little reflection on design, and I like to close the day’s work with it as well. That lets me take a somewhat more broad view than I’ll manage during the building part of the day, where my focus will be on the details of the splat or whatever is being built.
So we are designing right now, and it is up front, because we haven’t started coding yet. Let’s design a bit more.
The Design Now
If someone asked me what the essential design of Asteroids is right now, I’d say that there are these tables. Some of them represent a single object like an Asteroid or a Ship or a Button. Some of them represent collections of those tables, like “all the asteroids” or “all the buttons”.
The code kind of splits into two kinds of processing, processing the collections, and processing the individuals. For example, we have code that loops over all the asteroids, moving them or drawing them. Then there is code that moves one asteroid, or draws it.
That latter code pulls values out of the table that represents the asteroid, manipulates it, changes it, and then finally uses it to draw the asteroid.
All the intelligence is in a kind of floating godlike mass of code that knows how to move an Asteroid or split it, knows how to move the Ship (in due time), and so on. The Asteroid doesn’t know how to do anything. It’s just a dumb table of data.
You could say that the table knows how to “be” asteroid, but it doesn’t know how to “do” asteroid.
We’ve seen in our code that other than trying to group it together into related tabs, and giving it names like drawAsteroid
, there’s nothing to associate the doing with the being of an asteroid. We could call all those functions drawAlligator
and moveMarsupial,
and they’d work just as well. This is not a good thing.
What’s better, in my opinion, is classes and objects. Classes, for our purposes of the moment, are just a way of expressing whatever’s necessary to create objects, which are individual instances of the thing in question. An Asteroid
class would create instances of Asteroid
, each one representing one asteroid. And so on.
Both the “being” and the “doing” of asteroid life are included in the class definition, and instead of some outside agency manipulating each instance, we tell each instance to move itself or draw itself or split itself. This, in my opinion, makes for a better design.
We could convert the whole program to that format right now. But that would be a lot of work with no benefit to the product. It’s better to do “large refactorings” like this a bit at a time. Furthermore, you may not see the value of the idea yet. Fortunately we have a perfect opportunity to do a little work and see how it goes.
Splat!
Let’s do our splat as an object instead of following the current more procedural approach. I think we’ll see that it isn’t all that different to build, and that it’s a bit better overall.
Either that or I’m going to be surprised. Which could happen.
Reading through the many fine documents I’ve found about Asteroids, including the 6502 source code, I’ve learned that the splat they use is a messy little array of dots called “Shrapnel Patterns”, found here on Computer Archaeology. The original has that pattern repeated four times, slightly larger each time, so that it looks like it’s expanding. We have the luxury of scaling, so we can make it expand using that facility.
The 6502 code is there to be read and what it does is move to ten different locations and draw a dot. In the spirit of homage, I think we’ll decode those locations using our converter program, but for now, putting the splat object in, we’ll draw something else.
Basic flow …
When a rock splits, it creates a new asteroid, and redirects itself as the other half of the split. After that happens, we’ll create a splat. The spat won’t change position, but it will expand. That’s my current plan, anyway.
Because we’ll have more than one splat running at a time, I figure we’ll need a table of splats and we’ll tell them to move and draw but as we do the asteroids.
Let’s start to give this some shape.
Splat Class
I’ll start with a new class, called Splat. Codea creates the core of this for me:
Splat = class()
function Splat:init(x)
-- you can accept and set parameters here
self.x = x
end
function Splat:draw()
-- Codea does not automatically call this method
end
function Splat:touched(touch)
-- Codea does not automatically call this method
end
We’re not going to need touched
, so I’ll remove that. For the Splat’s information, I’m sure it’ll need a position, and I think it’ll need a timer to count down or something. And probably a size thing … but this is getting pretty speculative. I’m sure about the position, let’s start there.
Splat = class()
function Splat:init(pos)
self.pos = pos
end
function Splat:draw()
-- Codea does not automatically call this method
end
Now I figure we’ll need a table of Splats, at least for now, as we have for Asteroids. And when we create a new Splat, we’ll put it in that table, and when we move and draw things we’ll add in the code to do that to the Splats. Here goes:
Ah. The asteroids keep their own table in an odd sort of way. That’s why they move and draw internally. Let’s copy that for Splats and see what we think:
-- Splat
-- RJ 20200521
local Splats = {}
Splat = class()
function Splat:init(pos)
self.pos = pos
Splats[self] = self
end
Now I’ve done something a bit odd here. Asteroids use table.insert
to tuck themselves into Asteroids, and that creates an array kind of structure, with the indexes going 1, 2, 3, …
Here I’ve decided to use a hash format with the key being the table and the value also the table. self
is the table that contains the Splat’s information. I’m not exactly sure why I’m doing it this way. Partly to see how it goes and partly because I think it’ll make deleting Splats when they time out easier.
Anyway now when a Splat is created, it’ll be put into the draw table “automatically”. Let’s go create one, every time an Asteroid splits:
function splitAsteroid(asteroid)
if asteroid.scale == 4 then return end
if math.random(1,960) ~= 1 then 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
table.insert(Asteroids, new)
Splat(asteroid.pos)
end
There, we’ve created him. Now let’s draw all the splats:
function draw()
checkButtons()
--displayMode(FULLSCREEN_NO_BUTTONS)
pushStyle()
background(40, 40, 50)
drawButtons()
drawShip()
moveShip()
drawAsteroids()
drawSplats()
popStyle()
end
Now we need to build that function and we’ll put it in the Splat tab:
function drawSplats()
for k, splat in pairs(Splats) do
splat:draw()
end
end
This global function just loops over any splats and draws them. Right now, draw doesn’t have any code in it. I’ll run the program to see what mistakes I’ve already made … it runs OK … so far so good.
Now a quick draw for Splats:
function Splat:draw()
pushStyle()
pushMatrix()
translate(self.pos.x, self.pos.y)
fill(255)
scale(2)
text("SPLAT!", 0,0)
popMatrix()
popStyle()
end
And voila, splats:
Here’s all the Splat code. In Main:
drawSplats()
In Asteroid:
Splat(asteroid.pos)
And the Splat tab itself:
-- Splat
-- RJ 20200521
local Splats = {}
function drawSplats()
for k, splat in pairs(Splats) do
splat:draw()
end
end
Splat = class()
function Splat:init(pos)
self.pos = pos
Splats[self] = self
end
function Splat:draw()
pushStyle()
pushMatrix()
translate(self.pos.x, self.pos.y)
fill(255)
scale(2)
text("SPLAT!", 0,0)
popMatrix()
popStyle()
end
Explaining this code
Let me explain a bit of what’s going on here, in case you’re new to Codea and its objects.
We begin our class definition with this:
Splat = class()
This calls the built-in function class
which sets up a class-creating table and stores it in the global variable Splat. This table has magical insides such that when you say Splat(vec2(10,10))
, a new table is created that knows all the functions defined with Splat:functionName
. And that table is passed as self
to Splat:init
, where we store any parameters we’re given, pos
in our case, and do any other initialization needed. We’ll do more in init
shortly.
Thereafter, if you have an instance of Splat
, say ‘s’, you can call functions on it: s:draw()
or s:whatever()
When you say
s:draw(foo, bar)
That is almost exactly as if you had said
s.draw(s, foo, bar)
except that Codea pulls the s
inside and names it self
.
It’s a bit confusing to think about, and I’m sure the internal details are really exciting, but the net effect is that you define member functions in a class using
function ClassName:functionName(args)
And inside the function the word self
refers to the specific instance table that you are … so the things you pull out with self.this
and self.that
are the ones you put in earlier. In short, it just works.
Let’s make a little explosion instead of a text splat.
Explosion
I’ll just sketch some dots that are kind of like the original group and draw them. So I want a list of points, vec2, I guess.
I sketched these values by hand, reading the code for the splat, and guessing a bit. We’ll see how it looks.
-2,0, -2,-2, 2,-2, 3,1, 2,-1, 0,2, 1,3, -1,3, -4,-1, -3,1
I think for now I’ll just draw circles at those points, that’s the best I can do for dots. And I’ll guess at size and tweak when we see what we get:
function Splat:draw()
pushStyle()
pushMatrix()
translate(self.pos.x, self.pos.y)
fill(255)
scale(2)
for i,v in ipairs(Vecs) do
ellipse(v.x, v.y, 2)
end
popMatrix()
popStyle()
end
This gives me unmoving little blobs where the asteroids split:
I think I’d like to scale these up over a couple of seconds. Because of the issue with needing to scale dots down when scale goes up, I think I’ll make them grow using Dave’s trick of multiplying the coordinates by a number that goes from 1 upward to, oh, let’s say 4, over a period of four seconds.
I think I could do this with the Codea tween
function, but I’d have to learn it. Let’s do it the basic way for now:
function Splat:init(pos)
self.pos = pos
Splats[self] = self
self.size = 1
end
function Splat:draw()
pushStyle()
pushMatrix()
translate(self.pos.x, self.pos.y)
fill(255)
scale(2)
local s = self.size
for i,v in ipairs(Vecs) do
ellipse(s*v.x, s*v.y, 2)
end
popMatrix()
popStyle()
self.size = self.size * (1 + DeltaTime)
if self.size > 4 then Splats[self] = nil end
end
We set size to 1 in init
, manually scale the coordinates for our dots to make the blob expand, and increment size proportionately to DeltaTime
until it exceeds 4. Units of DeltaTime
are seconds, so that makes the blob expand for three seconds (4 - 1). I think I’d like four seconds better, so I’ll try that.
I think I like that a bit better. It also seems like the shrapnel should follow the original path of the asteroid. We’ll see about doing that next time.
For now, let’s consider what we have wrought.
Summing Up
We have a neat little capsule of code for our splats:
-- 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)
self.pos = pos
Splats[self] = self
self.size = 1
end
function Splat:draw()
pushStyle()
pushMatrix()
translate(self.pos.x, self.pos.y)
fill(255)
scale(2)
local s = self.size
for i,v in ipairs(Vecs) do
ellipse(s*v.x, s*v.y, 2)
end
popMatrix()
popStyle()
self.size = self.size * (1 + DeltaTime)
if self.size > 5 then Splats[self] = nil end
end
This capsule includes a collection of all the splats, and manages creation and destruction inside the capsule. The individual splats know how to draw themselves in a charming exploding kind of style, and we can enhance that as we see fit without fiddling with anyone else’s code.
The only external references to this capsule are in Main, where we added:
drawSplats()
and in Asteroid, where when we split we added:
Splat(asteroid.pos)
I think that’s nearly good. If everything worked like that, I predict that our code would be a bit more clean, and part of my overall objective for this series is to demonstrate how much cleaner things get when we know a bit more about how to code, and take small steps toward improvement.
Last time, we made some tabs to isolate things that seemed to belong together. This time, we isolated our Splat, and we also implemented it as an object, which reduces how much the rest of the code has to know: it just has to know to draw them, and how to create one where you want it when you want it.
I think that’s good, and I hope you do as well. See you next time!
I’ll include all of Main, Asteroid, and Splat, which are all we’ve changed.
-- 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()
drawSplats()
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
-- Asteroid
-- RJ 20200520
local Asteroids = {}
local Vel = 1.5
function createAsteroids()
for i = 1,4 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)]
a.scale = 16
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)
splitAsteroid(asteroid)
end
popStyle()
end
function splitAsteroid(asteroid)
if asteroid.scale == 4 then return end
if math.random(1,960) ~= 1 then 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
table.insert(Asteroids, new)
Splat(asteroid.pos)
end
function drawAsteroid(asteroid)
pushMatrix()
pushStyle()
translate(asteroid.pos.x, asteroid.pos.y)
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 = 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
-- 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)
self.pos = pos
Splats[self] = self
self.size = 1
end
function Splat:draw()
pushStyle()
pushMatrix()
translate(self.pos.x, self.pos.y)
fill(255)
scale(2)
local s = self.size
for i,v in ipairs(Vecs) do
ellipse(s*v.x, s*v.y, 2)
end
popMatrix()
popStyle()
self.size = self.size * (1 + DeltaTime)
if self.size > 5 then Splats[self] = nil end
end