Spacewar! 30 - An expanding universe.
An expanding universe
Today I am broadcasting from BMW of Ann Arbor, where they are checking my car’s fluid balance or something. I’m stuck here for a while and I plan to expand the Universe class started yesterday. Started twice, you’ll recall, once that didn’t work and again, with things going better.
Let’s review what worked yesterday, as a hint for what might work today. The process was:
- Forward Main’s
touched
to U.universe, as a method call, colon not dot. - In Universe
init
, add tableTouched
- Copy over Main’s functions
touched
andaddTouched
Today’s mission is a bit more tricky. The Drawn
table is supported by two other tables, Adds
and Removes
, which are used to add things, and remove things, outside the main drawing loop. The draw code does a lot. It looks like this:
function Universe:draw()
U.Tick = U.Tick + 1
remove()
add()
moveAll()
interactAll()
drawAll()
end
However, it is already moved to Universe
. Let’s try this: the U
table has a function add
that adds new objects to the Drawn
table. Let’s move the Adds
to Universe class, and make everyone talk to it. While we’re at it, let’s change the name of that function to addObject
. First that refactoring:
-- Main
-- The Universe
U = {}
U.universe = Universe()
U.Tick = 0
U.Adds ={}
U.Drawn = {}
U.Removes = {}
U.Touched = {}
U.Running = true
U.MissileKillDistance = 20
U.kill = function(objToRemove)
table.insert(U.Removes, objToRemove)
end
U.addObject = function(objToAdd)
table.insert(U.Adds, objToAdd)
end
And the corresponding changes in the objects:
Button = class()
function Button:init(x, y)
self.name = string.format("Button %f %f", x, y)
self.pos = vec2(x,y)
self.radius = 50
self.pressed = false
self.capturedID = nil
self.cannotCollide = true
addTouched(self)
U.addObject(self)
end
Explosion = class()
function Explosion:init(ship)
U.addObject(self)
self.timeOut = 100
self.pos = ship.pos
self.cannotCollide = true
end
Missile = class()
Missile.count = 0
function Missile:init(ship)
self.name = "missile" .. Missile.count
Missile.count = Missile.count + 1
self.pos = ship.pos + self:aWaysOutInFront(ship)
local velocity = vec2(0,1):rotate(math.rad(ship.heading))
self.vel = ship.vel + velocity
self.maxDistance = WIDTH*1.4
self.distance = 0
U.addObject(self)
end
Ship = class()
Ship.MaxMissiles = 50
Ship.TicksBetweenMissiles = U.MissileKillDistance + 1
Ship.ThrustAmount = vec2(0,0.02)
Ship.TurnAmount = 1
Ship.MaxSpeed = 3
-- do more of this
function Ship:init(shipNumber)
self.name = string.format("Ship %d", shipNumber)
self.missileLoad = Ship.MaxMissiles
self.timeLastFired = -Ship.TicksBetweenMissiles
self.shipNumber = shipNumber
self.pos = self:initialPosition()
self.heading = self:initialHeading()
self.vel = vec2(0,0)
self.controls = {
left = self:controlButton(HEIGHT/5),
right = self:controlButton(2*HEIGHT/5),
thrust = self:controlButton(3*HEIGHT/5),
fire = self:controlButton(4*HEIGHT/5)
}
U.addObject(self)
end
Does this run? Let me try it. Yes, in fact it does. A simple renaming should work, unless I had missed one. Now I’ll extend Universe to have the Adds
table and to implement Universe:addObject
. Then I’ll connect the U.addObject
function to call that, and change the references in Universe to its own table.
Ah, no, that’s not quite it. Universe is still calling the global functions add
and remove
. So first I’ll change those to reference the Universe’s Add table:
Universe = class()
function Universe:init(x)
self.Touched = {}
self.Adds = {}
end
function Universe:addObject(anObject)
table.insert(self.Adds, anObject)
end
-- Main
function add()
for _, obj in ipairs(U.universe.Adds) do
U.Drawn[obj] = obj
end
U.universe.Adds = {}
end
This works. I forgot to change the second reference to Adds
to refer to U.universe.Adds
, which broke things for a moment. When I do the removes in a moment, I’ll try to do better. Now I can implement the add
function on Universe and do it all internally:
function Universe:draw()
U.Tick = U.Tick + 1
remove()
self:addInNewObjects()
moveAll()
interactAll()
drawAll()
end
function Universe:addInNewObjects()
for _, obj in ipairs(self.Adds) do
U.Drawn[obj] = obj
end
self.Adds = {}
end
I renamed that method while I was at it. This time my problem was forgetting the self:
on the call. Anyway this is working. Now we can remove the add function from Main … which works … and then redirect all the U.addObject
calls to the universe.
However, this is a bit irritating. We’re going to have to say U.universe:addObject, because at the moment the universe is only known to U. My original plan was to wait till the end and then set U
to be the universe instance instead of the current U table. What would be a smoother refactoring?
It’s time to think of some options and then pick one:
- Proceed as planned. Convert
U.addObject
to U.universe:addObject` and then convert them back. - Or, a bit ugly, we could have the Universe instance, to be named
U
real soon now, contain a pointer to self, so that references to U.universe would just work, then remove them at leisure. - Replace U the table with U the universe now, and give U the universe the U table, which it would use to implement whatever it needs until it sucks the juices out. Unfortunately, there’s really only one more function in U the table. Most of the other references to U are accessing U’s various values, like Tick.
- Just go for it. Add the same initializers to U the Universe as U the table has, and then just swap the object right now, so that U becomes the Universe. See what breaks, and how hard it is to fix.
I’ve decided to try that last thing, more as an information-gathering Spike than as an actual plan. Most of the work has to be done anyway.
Universe = class()
function Universe:init(x)
self.Touched = {}
self.Adds = {}
self.Tick = 0
self.Adds ={}
self.Drawn = {}
self.Removes = {}
self.Touched = {}
self.Running = true
self.MissileKillDistance = 20
end
-- The Universe
U = {}
U.universe = Universe()
U.Tick = 0
U.Adds ={}
U.Drawn = {}
U.Removes = {}
U.Touched = {}
U.Running = true
U.MissileKillDistance = 20
U.kill = function(objToRemove)
table.insert(U.Removes, objToRemove)
end
U.addObject = function(objToAdd)
U.universe:addObject(objToAdd)
end
U.universe.oldU = U
U = U.universe
I’ve saved the old U in the universe, and then just slammed in the universe instance into U. I’ll run and see what happens. The main thing that blows up is that people like addTouched are now referring to U.universe and the U universe does not know itself. Try a one line addition:
U.universe.oldU = U
U = U.universe
U.universe = U
OK, good experiment but maybe the change is premature. Gives us something to think about though. Objects like Button add themselves like this:
function Button:init(x, y)
self.name = string.format("Button %f %f", x, y)
self.pos = vec2(x,y)
self.radius = 50
self.pressed = false
self.capturedID = nil
self.cannotCollide = true
addTouched(self)
U.addObject(self)
end
They’re calling that function on U, with a dot not a colon. What if we just changed it though? Well, then Ship would do it, and then, I imagine, Missile and Explosion. Let’s change those as they turn up. After changing Ship and Missile, the game almost plays, until a Ship tries to die. In the picture below, notice that there is a bullet about to strike from behind, and we’ve blown up trying to do the kill:
Ship:105: attempt to call a nil value (field 'kill')
stack traceback:
Ship:105: in method 'die'
Main:57: in function 'interactAll'
Universe:24: in method 'draw'
Main:37: in function 'draw'
The code in question is this:
function Ship:die()
U.kill(self)
Explosion(self)
end
There are two problems here. First, that will have to say U:kill
instead of U.kill
, and the Universe doesn’t implement kill
anyway. Let’s change the call, catch the error, and add the capability:
function Universe:kill(anObject)
self.oldU.kill(anObject)
end
OK, all I did here was to refer the kill
back to the old U table. That blows up on inserting the Explosion, which has not yet been converted to colon instead of dot. With that done … hmm. We get an odd message:
Universe:46: attempt to index a nil value (field 'oldU')
stack traceback:
Universe:46: in field 'kill'
Missile:46: in method 'die'
Main:57: in function 'interactAll'
Universe:24: in method 'draw'
Main:37: in function 'draw'
I think that’s telling me that the Universe U doesn’t know table U in itsoldU
field. But I thought it was in there. But maybe not. The explosion kills itself using U.kill
. Let me change that to U:kill
’ and see what happens … same thing. OK, where is oldU
? A quick print assures me that it’s really nil. Let’s look at that code again:
-- The Universe
U = {}
U.universe = Universe()
U.Tick = 0
U.Adds ={}
U.Drawn = {}
U.Removes = {}
U.Touched = {}
U.Running = true
U.MissileKillDistance = 20
U.kill = function(objToRemove)
table.insert(U.Removes, objToRemove)
end
U.addObject = function(objToAdd)
U.universe:addObject(objToAdd)
end
U.universe.oldU = U
U = U.universe
U.universe = U
That sure looks good to me. Ah! Read the message, Ron: it’s a Missile trying to die and it is still saying U.kill
. I changed the wrong U.kill
. Fix that and the game is running. Time to commit, and to talk about something that may be bothering you.
What’s going on here? Aren’t you just fixing bugs? Isn’t this grotesque hackery? How can this be reasonable?
Back in the olden days, in Smalltalk, we used to do something like this: We’d make some base change, like swapping a Universe in for the U table, then run the code. It would break, giving us what’s called a “walkback” in Smalltalk, basically opening a debugger on the line that broke. Usually, as here, the indicated change is clear, and we’d just make it.
And so far, the changes are clear. Mostly we’ve been converting dots to colons. I’d not have been confused at all had I changed them all at once, back when I mentioned the idea. Instead, I decided to go one at a time and although the message pointed right at Missile, I missed it.
So, I argue that this isn’t a big change that we are debugging. Instead it is a sensible change (swap U table and U universe), followed by letting the computer tell us where the necessary changes are. And we’re now in a very good place: the game is running fine, and there is only one reference to U-table’s kill
function, inside Universe. Universe wants to move the Removes table over, and then use it in its draw cycle:
Universe = class()
function Universe:init(x)
self.Touched = {}
self.Adds = {}
self.Tick = 0
self.Adds ={}
self.Drawn = {}
self.Removes = {}
self.Touched = {}
self.Running = true
self.MissileKillDistance = 20
end
function Universe:addObject(anObject)
table.insert(self.Adds, anObject)
end
function Universe:draw()
U.Tick = U.Tick + 1
self:removeDeadObjects()
self:addInNewObjects()
moveAll()
interactAll()
drawAll()
end
function Universe:addInNewObjects()
for _, obj in ipairs(self.Adds) do
self.Drawn[obj] = obj
end
self.Adds = {}
end
function Universe:removeDeadObjects()
for _, obj in ipairs(self.Removes) do
self.Drawn[obj] = nil
end
self.Removes = {}
end
function Universe:touched(touch)
for _,obj in ipairs(self.Touched) do
obj:touched(touch)
end
end
function Universe:addTouched(anObject)
table.insert(self.Touched, anObject)
end
function Universe:kill(anObject)
table.insert(self.Removes, anObject)
end
This seems to be working fine. As I’ve gone along, I’ve removed a lot of the U-table functionality. Let’s see if we can remove it all and see what happens. What I expect is that something will explode, because we still have those functions in Main that should be moved to Universe. But we might get lucky – and we do! It all runs, with Main like this:
-- HAVE YOU PUSHED TO GITHUB TODAY?
-- S3 Spacewar
-- The Universe
U = Universe()
U.universe = U
function setup()
Ship(1)
Ship(2)
U.Running = true
end
function touched(touchid)
U.universe:touched(touchid)
end
function draw()
U.universe:draw()
end
function moveAll()
for _, obj in pairs(U.Drawn) do
obj:move()
end
end
function interactAll()
for _, a in pairs(U.universe.Drawn) do
for _, b in pairs(U.universe.Drawn) do
if colliding(a,b) then
a:die()
b:die()
end
end
end
end
function drawAll()
background(40, 40, 50)
for _, obj in pairs(U.universe.Drawn) do
obj:draw()
end
end
function addTouched(anObject)
U.universe:addTouched(anObject)
end
function clip_to_screen(vec)
return vec2(vec.x%WIDTH, vec.y%HEIGHT)
end
function colliding(obj1, obj2)
if obj1 == nil then return false end
if obj2 == nil then return false end
if obj1.cannotCollide then return false end
if obj2.cannotCollide then return false end
if obj1 == obj2 then return false end
local d = obj1.pos:dist(obj2.pos)
local result = d < U.MissileKillDistance
return result
end
Time to commit again, then pull out the redundant references to U.universe
, and move those functions over. We can do those one at a time and if we’re wise, we’ll commit after each one. See you next time!