Today I am without internet, and running on the home generator for electricity, for the past 8 1/2 hours. We live in a cellular dead zone, so we’re kind of living in the 20th century. Perfect kind of day to work on a 1980’s program.

Reading the 6502 code tells me that the original game would not bring you back from hyperspace in danger, that is, with other objects right on top of you. I say “tells me”. What I mean is that that’s my interpretation of the code.

It appears to me that the game decides when you enter hyperspace where it is going to bring you back, and then when the spawn time is up, checks that area to bring you in. I’m not sure whether it does that on regular spawning, but if it did, it would be nice. I’ve been hit many times by respawning right in front of an asteroid.

For now we’ll concern ourselves with returns from hyperspace, not regular spawning. I believe we’ll have to copy the collision code rather than use it directly, because our present setup handles all the decisions down inside the objects, and destroys the ones who need it. Here, we’re just wondering if anyone would destroy us were we to pop into view. We may find some duplication to remove once we’ve done this.

Let’s start by reviewing how hyperspace works. That was yesterday: I have no idea.

function Ship:enterHyperspace()
    local appear = function()
        self.pos = vec2(math.random(WIDTH), math.random(HEIGHT))
        self.realSpace = true
        self.scale = 10
        tween(1, self, {scale=2})     
    end
    self.realSpace = false
    self.pos = vec2(10000,10000)
    tween.delay(6,appear)
end

Hm. Our ship manages to avoid trouble by being way out beyond the screen. That finesses all the checks for collisions. We do have the realSpace flag as well. But as things stand, the ship is in the objects collection and is being checked for collisions. What if we were to remove it from the objects collection upon entry to hyperspace, and put it back upon return?

Let’s try that: it should be “easy”. (I don’t have a beer for you to hold, sorry.)

This might do the job:

function Ship:enterHyperspace()
    local appear = function()
        U:addObject(self)
        self.realSpace = true
        self.scale = 10
        tween(1, self, {scale=2})     
    end
    self.realSpace = false
    U.objects[self] = nil
    self.pos = vec2(math.random(WIDTH), math.random(HEIGHT))
    tween.delay(6,appear)
end

Perhaps unsurprisingly, that works just fine. I’m starting to think there should be a sound for returning from hyperspace, but we’ll deal with that another time.

Now what we’d like to do, roughly, is to change the notion of our appear function to more like checkWhetherToAppear, and if the coast isn’t clear, restart the clock. However … I’m not sure it’s a good idea to put a tween in appear that sets up a callback to appear. It seems recursive or something. Still, it ought to work, it’s really a separately running thing, so the version of the function we’re running will have returned. Let’s try it. I’ll try to think of something, maybe a sound, to signal when we’ve deferred arrival. And I think I’ll shorten the delay as well.

I wrote this, plus a couple of other functions we’ll look at in a moment.

function Ship:enterHyperspace()
    local appear = function()
        if self:safeToAppear() then
            U:addObject(self)
            self.realSpace = true
            self.scale = 10
            tween(1, self, {scale=2})
        else
            self:signalUnsafe()
            tween.delay(2, appear)
        end
    end
    self.realSpace = false
    U.objects[self] = nil
    self.pos = vec2(math.random(WIDTH), math.random(HEIGHT))
    tween.delay(3,appear)
end

I’m not sure if the reference to appear will work inside appear. I suspect it’ll be nil and cause trouble. The two new functions are these:

function Ship:safeToAppear()
    if not self.safe then
        self.safe = true
        return false
    else
        return true
    end
end

function Ship:signalUnsafe()
    sound(self.sounds.extraShip)
end

This should just signal unsafe the first time and safe the second, and make the extra ship sound, which of course will surprise me since I rarely hear it.

I guess I’ll try it and see what happens.

The sound occurs, telling me I got to the unsafe, but the reappear doesn’t happen. I reckon appear is nil inside. I’ll check that to be sure. And yes, it is. However, maybe we can do this:

function Ship:enterHyperspace()
    local appear = function()
        if self:safeToAppear() then
            U:addObject(self)
            self.realSpace = true
            self.scale = 10
            tween(1, self, {scale=2})
        else
            self:signalUnsafe()
            tween.reset(self.hyperDelay)
            tween.play(self.hyperDelay)
        end
    end
    self.realSpace = false
    U.objects[self] = nil
    self.pos = vec2(math.random(WIDTH), math.random(HEIGHT))
    self.hyperDelay = tween.delay(3,appear)
end

I save the tween in self.hyperDelay and reset it and play it again (Sam) inside the appear function. I do expect this to work.

My expectations are dashed. It beeps but it doesn’t come back. I’ll start with an assert to make sure it is trying to come back. Maybe I just can’t trust that tween. The assert doesn’t fire, telling me that the tween didn’t call back a second time. I’ll try it another way:

Ship:85: attempt to index a function value (upvalue 'self')
stack traceback:
	Ship:85: in field 'callback'
	...in pairs(tweens) do
    c = c + 1
  end
  return c
end

:158: in upvalue 'finishTween'
	...in pairs(tweens) do
    c = c + 1
  end
  return c
end

:589: in function <...in pairs(tweens) do
    c = c + 1
  end
  return c
end

:582>

OK, that’s obscure enough for most purposes. Line 85 is here:

function Ship:enterHyperspace()
    local appear = function()
        if self:safeToAppear() then -- <--- 85
            assert(false, "tried to appear")
            U:addObject(self)
            self.realSpace = true
            self.scale = 10
            tween(1, self, {scale=2})
        else
            self:signalUnsafe()
            tween.delay(3, self.hyperReturn)
        end
    end
    self.realSpace = false
    U.objects[self] = nil
    self.pos = vec2(math.random(WIDTH), math.random(HEIGHT))
    self,hyperReturn = appear -- <--- COMMA!
    tween.delay(3,appear)
end

My eyes manage to detect the comma on the other line with the arrow. Fixing that takes me successfully to the assert, so it is probably working.

And yes, it is. I’m not sure why restarting the tween didn’t work but this will do. Now we have “merely” to improve the safeToAppear function (and probably the sound signal, if we choose to have one at all).

First, a commit: “hyperspace skips first attempt to return”.

Checking Collisions

I recall that we have a function that checks for collision distance, somewhere in Destructible, I imagine.

function Destructible:inRange(anObject)
    local triggerDistance = self:killDist() + anObject:killDist()
    local trueDistance = self.pos:dist(anObject.pos)
    return trueDistance < triggerDistance
end

So, can’t we just loop over all the objects here, and return false if we find one in range, and true if not? That won’t protect us from people near us but at least we won’t come in right under something deadly.

function Ship:safeToAppear()
    for k,o in pairs(U.objects) do
        if self:inRange(o) then return false end
    end
    return true
end

I think this does the job. It’s a bit invasive, ripping the objects collection untimely from the womb of Universe, but we can deal with that if it works.

How will we know? This really calls for a test, doesn’t it? But I’m just going to run it. I learn two things. First, it does seem to work, in that a couple of times I got the beep saying it wasn’t coming out. Second, the check only detects things literally on top of us, but not when they are close and bearing down like the boulder bearing down on Indy. We might want to give ourselves more of a break. Third (I lied about the two), When we reappear under the buttons, there’s little hope of noticing. Let’s change the appear points to be a bit inboard of the edges:

function Ship:enterHyperspace()
    local appear = function()
        if self:safeToAppear() then
            U:addObject(self)
            self.realSpace = true
            self.scale = 10
            tween(1, self, {scale=2})
        else
            self:signalUnsafe()
            tween.delay(3, self.hyperReturn)
        end
    end
    self.realSpace = false
    U.objects[self] = nil
    local w = math.random(WIDTH-200)
    local h = math.random(HEIGHT-300)
    self.pos = vec2(w,h) + vec2(100,200)
    self.hyperReturn = appear
    tween.delay(3,appear)
end

That’s nearly good, I think. Good enough for now. Let’s commit and sum up. Commit: hyperspace semi-safe, spawn in central area.

Summing Up

This has gone rather smoothly, hasn’t it? We adjusted the hyperspace entry to remove our ship from the objects list, freeing it from concern over destruction. That allowed us to decide at entry time where the reappearance would be scheduled. That allowed us to check the area before reappearing. We did that with a simple loop over the other objects.

We had to try a few things with the tween, but saving the appear function in a member variable allowed us to refer to the closure from inside itself.

We have a bit of an invasion with pulling the objects collection out of Universe, but we can refactor that to a better location at our leisure.

All that said, as smoothly as it went, testing would have made it smoother and better. I’ll see about addressing that next time.

The power and internet are back on here, so I will enjoy the rest of my Saturday in the 21st century. See you next time!

Asteroids.zip