GitHub Repo

When things get bad in space, your ship has a hyperspace capability that will remove you from normal space and, after a delay, insert you back somewhere else. There is a chance that you’ll not survive the attempt. Our mission today: implement this capability. There are issues.

If you can read this, my site is back! Yay, Dundee.net, good job finding the problem.

I am becoming less sure why I'm writing these things, since it appears that my site is down and my ISP seems not to be able to even see that it's broken, much less fix it.

But I have nothing better to do, so I’ll entertain myself by working as though you were going to read this.

So, hyperspace works like it says in the blurb. You can do something — I think hitting space bar — to request a hyperspace jump. If a refractory interval has elapsed since the last jump, you jump, with some chance of exploding when you emerge. You emerge exploding or not, at a random location on the screen. We’ll look up and work out the details as we need them.

We have a convenient object, ShipMonitor, that almost does the job. If your ship is not on screen, then after a while, ShipMonitor checks that screen center is clear, and if it is, your ship emerges there. Hyperspace is just like that except that the location is random and you have some chance of exploding.

So, how might we do this new feature?

ShipMonitor Extension

One possibility that comes right to mind is to enhance the SHipMonitor so that somehow it knows that we are in a hyperspace situation, and instead of rezzing us at screen center, it picks some other location. And it rolls the dice and maybe we get destroyed on the way back.

Issues with this include needing to know whether the ship is in hyperspace rather than dead, and adding complication to an already moderately complicated object. Still, if we could set a little information somewhere, maybe in the ship, this could work.

We might want another option.

Ship Behavior
We could build the behavior right into the ship. When we get the hyperspace command, we could move the ship off screen, so that it is not visible and cannot be collided with. Or we could turn off its draw and have a flag saying that we don’t collide, although that is iffy because we never know if an interaction check goes ship vs asteroid or asteroid vs ship. Moving off screen seems the better hack.

Then after x amount of time, presumably in update, we’d manage returning.

Issues here … gloms up ship, but someone has to do this work. And our code for checking for safe space might have to be duplicated.

HyperspaceMonitor
We could spawn a new object, a HyperspaceMonitor, when we want to go into hyperspace. The ship sets its coordinates somewhere weird, and after a while, the HyperspaceMonitor does its thing, rezzing or destroying the ship.

Issues here? Not too many. It kind of duplicates some ShipMonitor code, but perhaps not much. It really doesn’t care about collisions, just runs a timer and when time runs out, brings back the ship. We could do a very simple test rig by just setting the ship down somewhere without a lot of thought about it. Like hyperspace kind of does.

I think that when the ship wants hyperspace, we’d have it create a HyperspaceMonitor, giving it the ship as we do with the ShipMonitor, and drop the new HyperspaceMonitor into the mix. It would start seeing updates and such and in due time, do its thing.

Among these ideas, I think that the Ship doing it on its own is a decent idea, that enhancing ShipMonitor is not a great idea because it is already complicated, and I think the HyperspaceMonitor, aside from its long name, sounds like it might be quite clean.

Let’s review some code, starting with how the ship is controlled.

class Flyer ...
    override fun update(deltaTime: Double): List<IFlyer> {
        tick(deltaTime)
        val objectsToAdd = controls.control(this, deltaTime)
        move(deltaTime)
        return objectsToAdd
    }

The controls fiddle with the ship’s acceleration and such. Let’s review:

    fun control(obj: Flyer, deltaTime: Double): List<IFlyer> {
        turn(obj, deltaTime)
        accelerate(obj, deltaTime)
        return fire(obj)
    }

    private fun accelerate(obj:Flyer, deltaTime: Double) {
        if (accelerate) {
            val deltaV = acceleration.rotate(obj.heading) * deltaTime
            obj.accelerate(deltaV)
        }
    }

    private fun fire(obj: Flyer): List<IFlyer> {
        return missilesToFire(obj).also { holdFire = fire }
    }

    private fun missilesToFire(obj: Flyer): List<IFlyer> {
        return if (fire && !holdFire) {
            listOf(Flyer.missile(obj))
        } else {
            emptyList()
        }
    }

    private fun turn(obj: Flyer, deltaTime: Double) {
        if (left) obj.turnBy(-rotationSpeed*deltaTime)
        if (right) obj.turnBy(rotationSpeed*deltaTime)
    }
}

Looking at this, I notice something that I hadn’t really thought about: while the ship is in hyperspace it shouldn’t be jetting around, and certainly shouldn’t be shooting off missiles. So we may need a way to know that we’re in hyperspace so that the controls are dead.

This gives me a fourth idea for how we might do this:

Controls Handle Hyperspace
Controls will need to know about hyperspace, unless the ship is fully out of the mix, which would only be the case if we handle hyperspace with ShipMonitor. Otherwise we must keep the ship in the mix lest the monitor plunk it down.

As I think about this, it seems to me that moving the ship off screen to avoid ShipMonitor getting involved is just the wrong thing, almost too much of a hack. It makes me lean a bit more toward doing it in the ShipMonitor. If we do that, the ship won’t be in the mix and won’t try to fly about or fire missiles while it’s gone.

OK, we’d better review ShipMonitor and see how we might use it for Hyperspace.

class ShipMonitor ...
    override fun interactWith(other: IFlyer): List<IFlyer> {
        if (state == LookingForShip) {
            if (other == ship)
                state = HaveSeenShip
        } else if (state == WaitingForSafety){
            if (tooClose(other)) safeToEmerge = false
        }
        return emptyList() // no damage done here
    }

    private fun shipReset(): IFlyer {
        ship.position = Point(5000.0, 5000.0)
        ship.velocity = Velocity.ZERO
        return ship
    }

    private fun startCheckingForSafeEmergence() {
        // assume we're OK. interactWith may tell us otherwise.
        safeToEmerge = true
    }

    private fun tooClose(other:IFlyer): Boolean {
        return (Point(5000.0, 5000.0).distanceTo(other.position) < safeShipDistance)
    }

    override fun update(deltaTime: Double): List<IFlyer> {
        elapsedTime += deltaTime
        var toBeCreated: List<IFlyer> = emptyList()
        state = when (state) {
            HaveSeenShip -> LookingForShip
            LookingForShip -> {
                elapsedTime = 0.0
                WaitingForTime
            }
            WaitingForTime -> {
                if (elapsedTime < 3.0)
                    WaitingForTime
                else {
                    startCheckingForSafeEmergence()
                    WaitingForSafety
                }
            }
            WaitingForSafety -> {
                if (safeToEmerge) {
                    toBeCreated = listOf(shipReset())
                    HaveSeenShip
                } else {
                    startCheckingForSafeEmergence()
                    WaitingForSafety
                }
            }
        }
        return toBeCreated
    }
}

Looking at this, I’m thinking, what if when the ship is “destroyed”, it sets its coordinates at that time either to universe center, or some random location, depending on whether it’s a hyperspace jump or damage. ShipMonitor wouldn’t reset the ship, it would just bring it back wherever its then coordinates suggest. It’s not clear how the small odds of not emerging would be handled, but we’ll defer that to keep our thinking clean.

So in this scheme, when the controls say hyperspace, the ship will set its coordinates randomly. We could do that now and the ship would just jump about. But how would the ship know to disappear? As things stand, we only remove objects when processInteractions returns them as needing to die, after which they are sent finalize and forgotten.

    fun processInteractions() {
        val toBeRemoved = colliders()
        flyers.removeAll(toBeRemoved)
        for (removedObject in toBeRemoved) {
            val addedByFinalize = removedObject.finalize()
            flyers.addAll(addedByFinalize)
        }
    }

    fun colliders() = flyers.collectFromPairs { f1, f2 -> f1.interactWith(f2) }

Hm. If the ship knew to behave as if it had been hit …

    override fun interactWith(other: IFlyer): List<IFlyer> {
        return other.interactWithOther(this)
    }

    override fun interactWithOther(other: IFlyer): List<IFlyer> {
        return when {
            weAreCollidingWith(other) -> listOf(this, other)
            else -> emptyList()
        }
    }

OK, this is a hack, but maybe it’ll give us a good idea. What if when we hit the hyperspace button, Controls calls enterHyperspace on the ship it’s controlling, and the ship sets a hyperspace flag and the next time it is in an interaction, it returns itself as needing to be finalized?

No, that’s no good … the interaction code returns two objects, which would destroy some innocent other object besides the ship. Unless …

In controls, the ship can fire, and if it does, we add a missile to the mix, returning it from controls.control. So if in handling hyperspace, we were to create an object custom made to collide with the ship, like a HyperspaceCapsule or something, that object would collide with the ship and they’d both be finalized, i.e. removed from the mix. If the ship was already primed with a new position (because its controls told it to get ready) the ShipMonitor would just create it.

I see a possible danger, which is that some other object might collide with the ship first. I think that’s not a concern, because the ship isn’t removed until all the collisions have been calculated.

Spike

I’m liking this idea enough that I want to spike it. My plan is this:

I’ll wire up the hyperspace to the space key, and when Controls sees it, it will create a new asteroid right where the ship is. This should immediately destroy the ship. I see this as a test of the concept of dropping something into the mix to start the action.

It turns out that the name of the space bar is “space”, so:

		keyboard.keyDown.listen {
            when (it.name) {
                "d" -> {controls.left = true}
                "f" -> {controls.right = true}
                "j" -> {controls.accelerate = true}
                "k" -> {controls.fire = true}
                "space" -> {controls.hyperspace = true}
            }
        }
        keyboard.keyUp.listen {
            when (it.name) {
                "d" -> {controls.left = false}
                "f" -> {controls.right = false}
                "j" -> {controls.accelerate = false}
                "k" -> {controls.fire = false}
                "space" -> controls.hyperspace = false
            }
        }

Clearly we’re going to need to toggle hyperspace as we do firing or we’ll enter it a lot. Maybe not. We’ll see. Perhaps if we just cleared the fire flag we could do without all that holding stuff. The up and down are events, not states. I think.

So now we need a hyperspace flag in controls, and to deal with it:

    fun control(obj: Flyer, deltaTime: Double): List<IFlyer> {
        if (hyperspace) {
            val vel = Velocity(1000.0, 0.0).rotate(random(0.0,360.0))
            val asteroid = Flyer.asteroid(obj.position, vel)
            return listOf(asteroid)
        }
        turn(obj, deltaTime)
        accelerate(obj, deltaTime)
        return fire(obj)
    }

That actually works as intended. If I hit the space bar during the game, two medium asteroids appear, going in random directions, and the ship is destroyed.

So, if instead, we were to drop a HyperspaceContainmentCapsule, it would collide with the ship and carry it away. Well, actually, they’d mutually destroy each other but it’s much the same. Now let’s see what’s left to cover:

  1. The ship needs to return at screen center when it’s really damaged, and randomly if it’s hyperspace.

  2. The ShipMonitor probably should just rez the ship wherever its coordinates presently are, leaving it to the ship to set itself to zero if it is destroyed. Let’s hack that in, in shipReset:

    private fun shipReset(): IFlyer {
//        ship.position = Point(5000.0, 5000.0)
        ship.velocity = Velocity.ZERO
        return ship
    }

Now when the ship is destroyed, it’ll come back wherever it was. Yes, that works as intended. Except that we really want it to go to zero when it’s real collision damage.

Ah, there’s the rub. Because we do not have a unique Flyer type Ship, we won’t know in finalize whether we are a ship or not. Doesn’t matter: no one else is ever revived. We’ll just set it:

    override fun finalize(): List<IFlyer> {
        // ship returns at universe center ...
        this.position = Point(5000.0, 5000.0)
        return finalizer.finalize(this)
    }

That doesn’t work, for an amusing reason: The asteroids take their starting position from the ship’s death position, so with this line of code in, the new asteroids rez at universe center. We need to do this in the ship’s finalizer.

The ship uses the DefaultFinalizer, I think:

        fun ship(pos:Point, control:Controls= Controls()): Flyer {
            return Flyer(
                position = pos,
                velocity = Velocity.ZERO,
                killRadius = 150.0,
                view = ShipView(),
                controls = control,
            )
        }

He should have a finalizer of his own now:

        fun ship(pos:Point, control:Controls= Controls()): Flyer {
            return Flyer(
                position = pos,
                velocity = Velocity.ZERO,
                killRadius = 150.0,
                view = ShipView(),
                controls = control,
                finalizer = ShipFinalizer()
            )
        }

And …

Well. I do think we need this but there’s something else going on that makes the asteroids appear at zero. That I do not know, and am stumbling a bit more and more, tells me that it’ll be time to roll back this spike and do it right real soon now. Let’s do create the ShipFinalizer, it’s not wrong and might be right.

class ShipFinalizer : IFinalizer {
    override fun finalize(flyer: Flyer): List<IFlyer> {
        flyer.position = Point(5000.0,5000.0)
        return emptyList()
    }
}

I don’t see quite why, but this fixes the asteroids appearing in the middle. And if I comment out that line, the ship rezzes where it was when it died. Therefore, if we were to set a new emergence location, we’d end up there.

So here, we want this:

class ShipFinalizer : IFinalizer {
    override fun finalize(flyer: Flyer): List<IFlyer> {
        if ( flyer.deathDueToCollision())
            flyer.position = Point(5000.0,5000.0)
        else
            flyer.position = Point(random(0.0,10000.0), random(0.0, 10000.0))
        return emptyList()
    }
}

Unfortunately I don’t know how to implement deathDueToCollision, but we’re getting closer.

Unfortunately this will need to be added to the IFlyer interface. I’m starting to wish that I’d broken out my individual flyer classes.

Well, we can at least build the shell.

    fun deathDueToCollision(): Boolean { return true }

Then override in Flyer:

    override fun deathDueToCollision(): Boolean {
        return !controls.hyperspace
    }

This actually works. If I go to hyperspace, two asteroids appear where I was (because the universe rezzes a big one on me .. which gives me an idea …) I appear at a random location. If, on the other hand, I am hit by an asteroid, i respawn at center. This is the behavior we want.

I was thinking that maybe I could rez a small asteroid to kill the ship, avoiding the need to create a new object just for that purpose, but I can’t. However … a missile of my own would kill me.

In the end I create a flyer that I call destroyer:

    fun control(obj: Flyer, deltaTime: Double): List<IFlyer> {
        if (hyperspace) {
            val vel = Velocity(1000.0, 0.0).rotate(random(0.0,360.0))
            val destroyer = Flyer(
                killRadius = 100.0,
                position = obj.position,
                velocity = Velocity.ZERO
            )
            return listOf(destroyer)
        }
        turn(obj, deltaTime)
        accelerate(obj, deltaTime)
        return fire(obj)
    }

That has no score, and destroys the ship. Because the hyperspace flag is still down, since you’d have to lift the key instantly, the ship gives itself a random position.

In short, modulo the occasional explosion on re-entry, this works as intended.

I started this as a spike. Mostly it has been done with sensible code in sensible objects. Let’s sum up and see whether we can keep it.

Summary

We’ve made these changes:

Controls
We added a new flag, hyperspace, triggered by space bar down and up. It has no buffering of any kind. When the key is struck, controls creates and returns a destroyer object with no other action. The destroyer will destroy the ship and itself in the next cycle. Because the ship is gone, it cannot maneuver, fire missiles, or get in any trouble.
ShipFinalizer
This new object asks the ship whether it was deathDueToCollision, which it answers by checking the hyperspace flag in its controls. Only a ship will do this, because only a ship gets a ShipFinalizer. If the destruction was by collision, the ship sets its position to mid-universe, and otherwise sets it randomly. Then it dies, to be revived with luck:
ShipMonitor
The ShipMonitor will become aware that the ship has gone somewhere, and will, after due consideration, bring it back. (Much work needs to be done on ship count, and we need to deal with the dangers of hyperspace. All in good time.)

It may be that the ship is supposed to retain its intrinsic velocity in hyperspace: I’ll have to look that up. For now, the ShipMonitor sets an emerging ship’s velocity to zero.

Flyer
We added deathDueToCollision, defaulting true and replying false if the Controls hyperspace flag is set.
Main
Added handler links for space bar key down and up.

Honestly, this isn’t bad … but I would mention:

  1. The “distributed” character of this architecture is interesting: the objects are closer to autonomous, almost as if each one were running its own little processor. Working out how to do things, or what will happen, seems a bit more complicated than if, somehow, they were all together and we sort of ran the ship, ran the missiles, ran the asteroids, and handled each case separately. I think much of this feeling is an illusion and that a more centralized version would have its own issues.

  2. I think I’ve made more trouble for myself than I’ve avoided by keeping all the actual flyers, asteroid, missile, and ship (and now destroyer) all in one class. All the specials have their own class, why not the flying ones? There might be some code duplication. If so, we know how to deal with it. We might want to schedule a split to happen … one of these days.

I decide this is good enough. Not perfectly clean, but pretty reasonable. We’ll review it with fresh eyes soon. Commit message: Space Bar enters hyperspace. Controls rezzes destroyer, destroyer kills ship, ship checks to see if it’s trying to go to hyperspace, sets random or center position, dies, and is resurrected in due time.

But oh no! I played the game and it is good but I forgot to run the tests! Why do I mention it? because one of them fails. I should withdraw this commit, but let’s see about the test. We did change who controls the ship’s position:

expected: Vector2(x=5000.0, y=5000.0)
 but was: Vector2(x=10.0, y=10.0)

Test is that super long one, and since it isn’t calling finalize on the ship, it doesn’t work. I think I can just call finalize … no, in fact the ship isn’t even put in the mix, this is really just a test of the update function. I’ll delete the check. Commit: fix broken test. sorry.

Why do I even mention this? I could erase my mistake and no one would ever know it happened. As frequent readers know, I prefer to show what really happens, not what would have happened at the hands of some perfect programmer. We have none of those on staff here chez Ron, and I suspect it’s the same at your shop as well.

Anyway, new feature, we decided to accept the spike, and we probably need a bit of a review of the code in an upcoming session. Until then, with hope for one day joy at having a web site again, I remain, yrs etc etc.

See you next time!