GitHub Repo

An imaginary chat leads to a determination to push this design as far as it can go. Along the way I face an apparent fact.

GeePaw asked me a simple question last night on a secret back channel never to be revealed and I saw the message this morning and wrote the following in reply. In essence it was a stream of consciousness, musing on things I’d been thinking about in the shower anyway.

Yes … the fundamental designs, um concern um problem um issue is that all these objects are independent objects in the universe. Kind of like people. It would be “better”, perhaps, maybe, if there were sensors or something that you could use to determine what you’re dealing with, but in the end, as things stand now, it all comes down to type. I think that type doesn’t have to equal class (or superclass). I’m steering perilously close to what I think is called aspect-oriented, but other than some reading, I know nothing about aspect-oriented. I think your little panels of actions are not unlike an aspect approach as I understand each both neither one or the other.

My present growing inclination is to go all in, break out all the objects, have (at top) just one interface, expand that as needed into each object, look for duplication, see how to remove it. Unfortunately, I am now contaminated by some of your ideas, in particular that panel of functions.

Suppose … if you will … maybe it is a bit like the robot wars thing. The job of a space object is to do some thing, all on its own, where basically all that happens is that it updates (that’s really just to provide a uniform state in the universe, quantized time (side effect, drawing)); interactions with all other objects (bracketed by begin /finish), … and that’s it. (I have removed interactWithOther, as have you, and I think finalize can be subsumed. So, in this light, I’d blow apart all the objects under the Universal Interface, which specifies the calls you will receive.

You implement them however you care to. Tools need to exist for interaction. (I am thinking of interacting both ways rather than one way). You’re going to see every other object in the universe. Now what tools do you need to do your job?

Or … this is why one wishes one were on a team … or … could it be that objects only interact with Solid objects, not with each other? Ooo … an idea … WhipWatcher knows the ship and watches for it. WaveWatcher, on the other hand, looks for asteroids and has to check their type somehow. But if he, too, knew the ship, he could count non-ship solids and when the count is zero, time for a new wave. So he doesn’t have to know type.

That leaves Score. I rather like the idea of Score objects floating around in the universe and the ScoreKeeper sweeps them up and totals them. If they were suitably Solid … it’s tempting to go back to everyone knowing a score and we just add up a (REDACTED word meaning “a lot”) of zeros …

This would be more fun collaboratively than whatever the current mode is. But we probably get some better ideas in this somewhat competitive mode. I think the same would happen with robot Wars done well … interesting tactics and such will surely arise. Or would, if the game universe was more rich.

I have to think about whether any of the non-Solids (aside from Score) interact with each other. It might be that there are just two types, physical and nonphysical. Hmm.

Meanwhile I think I should blow out all the objects into separate classes and push everything down to the instances, then see what I’ve got. the inheritance is clearly premature optimization if one allows it at all. I think I feel in love with all the Solids being the same, and truth is, that’ll make it terribly tempting to check class. I want to avoid that.

I’‘m glad we had this little chat. I’m gonna publish it.

And here it is, published as promised. Why? Because I want folx to be able to know, a bit, what other developers think about. We don’t think in clean neatly paragraphed ideas. (At least I don’t, and surely I’m not the only one.) We think this, then that, we flit around, we look at the problem and our solution from many angles and having viewed it from all those angles, we decide where to whack it to make it a bit better.

Tentative Plan

Fact
I think I need to face a fact here, which is that the point of this exercise isn’t to build an Asteroids game, but to learn ways of using Kotlin to build things like Asteroids games. now, all my work is like that: I write example programs to write about programming. But this one, I’m not even pretending to care about the next feature. I’m trying to sort out what this design wants to be, and how to do that nicely in Kotlin.

So let’s face that.

The essence of this game’s design is that the universe is full of objects that interact with each other, and it is because of the interaction that the game happens. We don’t have some master object driving asteroids and ships around, we have essentially independent asteroids and ships driving themselves around, and interacting. This has led to some really interesting ideas, like the ShipWatcher that just keeps an eye on the ship and, if it ever goes away, starts the actions of creating a new one. Or the ScoreKeeper, sweeping up Score objects and accumulating their value. (The ScoreKeeper is going to be a problem for us in the future, I just want to get that prediction down right here.)

So let’s go all in. For each kind of object, let’s get to the point where everything about that object is in that object. We’ll remove all the inheritance, push all the methods down into the individual objects, and see what we get.

I think we should try having two kinds of objects, physical and nonphysical, with the rule that nonphysical objects don’t interact with each other, only with physical objects. (Why? I’m not sure. It seems that it’ll lead to an interesting design. It should make things simpler. If it doesn’t, we’ll do something else. (How will we even know? I don’t know. We’ll see. We’re planning here. Don’t confuse selling with installing.)

OK. This morning we’ll start breaking out the individual classes of space objects. All the non-Solid “special” objects are already separate.

Let’s begin by pushing all the inherited methods or SpaceObject down, and convert to interfaces as a first move in that direction.

Here’s the top level.

abstract class SpaceObject {
    var elapsedTime = 0.0
    open val lifetime
        get() = Double.MAX_VALUE

    fun tick(deltaTime: Double, trans: Transaction) {
        elapsedTime += deltaTime
        update(deltaTime,trans)
    }

    // defaulted, sometimes overridden
    open fun update(deltaTime: Double, trans: Transaction) { }

    open fun beginInteraction() {}
    open fun interactWith(other: SpaceObject): List<SpaceObject> { return emptyList() }
    open fun finishInteraction(trans: Transaction) {}

    open fun draw(drawer: Drawer) {}
    open fun finalize(): List<SpaceObject> { return emptyList() }
}

Let’s put an interface on top of this. Then when we remove one of these open functions with intention of pushing it down, Kotlin will help us find the folx who need to do the thing. IDEA provides this:

interface ISpaceObject {
    var elapsedTime: Double
    open val lifetime: Double
    fun tick(deltaTime: Double, trans: Transaction)

    // defaulted, sometimes overridden
    open fun update(deltaTime: Double, trans: Transaction)
    open fun beginInteraction()
    open fun interactWith(other: SpaceObject): List<SpaceObject>
    open fun finishInteraction(trans: Transaction)
    open fun draw(drawer: Drawer)
    open fun finalize(): List<SpaceObject>
}

And in the SpaceObject class, everything is marked override, as one would expect.

Do these things all have to say open? Isn’t that implied with an interface? Right. IDEA immediately offers to take them all out. Accepted. Why did you put them in?

Now my naive plan is just to remove one of my default implementations in SpaceObject, and fix all the errors by moving that method into every class. This will be tedious but leads me where I want to go, to a scheme with every class implementing every method explicitly. Then we’ll see all the duplication and decide what to do about it.

Let’s try an easy one. How about finalize? I’ll just comment out the implementation and let IDEA tell me where the issues lie. No, wait, it has “safe delete”, let’s try that.

We’re green after adding the interface. Let’s try to emulate Hill and commit on each and every green.

Well, what happens is that of course we can’t compile much of anything, because everyone needs that method now.

I’m going to paste it in everywhere it’s called for. Surely there is a more automated way to do this?

    override fun finalize(): List<SpaceObject> { return emptyList() }

Slightly tedious. There must be a way to push a function down. Done, though. Green. Commit.

IDEA has Refactor/Push Members Down. That sounds promising. Let’s try it.

I push one method down: beginInteraction. We’re green. Commit, then explore, or explore, then commit? Commit. Inspection tells me that, despite some unclear warnings, IDEA does what you’d want … it puts the defaulted method into every object that doesn’t already have an override for it. Let’s continue to do them one at a time. I think we’re going to have trouble with tick or something. One at a time will be easier to deal with.

I push interactWith. Test. Green. Commit. This is fun.

Push finishInteraction. Green, commit.

When you select things from the menu to be pushed down, IDEA highlights things that need to go along. I’m just doing the ones that can be done singly, at least for now.

IDEA prompt showing some red

We see above that if we push tick, we have to push update and elapsedTime as well. That troubles me. Let’s keep doing singles until we run out.

I push draw. Think I’ll test this on screen as well as in the test. Green and game is good. Commit.

I’m left with lifetime that can go down alone. Do it. Green. Commit.

Remaining are elapsedTime, tick, and update, which need to go together. Based on something I did earlier, I think this might break something. Try it. Green. Commit: pushed down elapsedTime, tick, update.

Now the abstract class SpaceObject is empty:

abstract class SpaceObject : ISpaceObject {
}

We can push the interface town, and remove the class. Hm, I thought that would push ISpaceObject down all over, but not quite. We have some issues that IDEA, at least the way I used it, didn’t resolve. First, our classes area all still instantiating SpaceObject:

class ShipMaker(val ship: SolidObject) : SpaceObject(), ISpaceObject {

And, second, many of our lists and such are described as List<SpaceObject> and so we need to change them all. I’ve been reliably informed that there are 80 such cases.

I don’t see a way other than just to do it. Let’s try the safe delete on the class.

After some frenetic replacement of SpaceObject with ISpaceObject, I’m green. Commit.

Repeat some more replacements. Green. Commit. Warning says SpaceObject is not used. Yay. Remove. Test. Commit: SpaceObject defaults all pushed to members. Class removed.

OK, FWIW, I’ve removed all the defaulted methods and values from the SpaceObject. everyone is now required to implement the whole interface, which is:

interface ISpaceObject {
    var elapsedTime: Double
    val lifetime: Double
    fun tick(deltaTime: Double, trans: Transaction)

    fun update(deltaTime: Double, trans: Transaction)
    fun beginInteraction()
    fun interactWith(other: ISpaceObject): List<ISpaceObject>
    fun finishInteraction(trans: Transaction)
    fun draw(drawer: Drawer)
    fun finalize(): List<ISpaceObject>
}

We can simplify this if we wish. Let’s break, though, and look back upon our works.

Reflection.

Update
Hill is raving at me for having suddenly removed all my implementation inheritance, over which we have been banging heads for Lo! these many days. He asserted “I win”, to which my reply must remain private lest children read it.

But I don’t think of it as adopting his viewpoint, which frankly makes me suspect that his mother was frightened by an inheritance while he was in the womb, but instead a commitment to the notion of “all these are independent objects”, and this is a first step in making them truly independent. Will I put inherited behavior back in? Maybe, maybe not. Does Hill “win”? Sure, fine with me. If I like this way better, I’ll keep it. If not, I’ll change it. Win, lose? No such thing. Try things. Learn.1

Anyway, what has happened here? Well, the inherited default behavior is all pushed down into the objects. This has created some duplication … but it is all trivial, or nearly so. I think we’ll get it down to even more trivial quite soon.

What I’d like to do, I think, is come up with a distinction between physical things that can collide and nonphysical ones that cannot. I may be wrong about that, I’m not sure yet. Next steps may include:

  1. Remove the LifetimeClock and make Missiles keep their own time. This may require moving Missile into its own class, but …
  2. Move each of the SolidObjects into its own class.
  3. Try to leave no one in SolidObject

Let’s see if we can simply get rid of LifeTimeClock. I think that to do that, I need to break out Missile so that it can have a local timer.

Let’s see if we can do this in tiny steps. I guess the first thing might be to break Missile out into its own class. We could inherit from SolidObject, but I think we don’t want to do that. Let’s pull an interface out of SolidObject and implement it in things like Missile.

I just put everything in there. We’ll see what that does to us when we try to create Missile class. Commit the interface.

Now let’s create a new implementor of the interface, Missile. I don’t do any tests quite yet.

No, that would be wrong. I’ll start with tests.

class MissileTest {
    @Test
    fun `can be created`() {
        val ship = SolidObject.ship(U.randomPoint())
        val missile = Missile(ship)
    }
}

IDEA already wants to create the class. We’ll let it do that and populate it for us. I think this will be nasty.

Yes, this is the shell we start with:

class Missile(ship: ISolidObject,
              override var position: Point,
              override var velocity: Velocity,
              override var killRadius: Double,
              override val isAsteroid: Boolean,
              override val lifetime: Double,
              override val view: FlyerView,
              override val controls: Controls,
              override val finalizer: IFinalizer,
              override var heading: Double,
              override var elapsedTime: Double
): ISolidObject {
    override fun accelerate(deltaV: Acceleration) {
        TODO("Not yet implemented")
    }

    override fun scale(): Double {
        TODO("Not yet implemented")
    }

    override fun deathDueToCollision(): Boolean {
        TODO("Not yet implemented")
    }

    override fun draw(drawer: Drawer) {
        TODO("Not yet implemented")
    }

    override fun interactWith(other: ISpaceObject): List<ISpaceObject> {
        TODO("Not yet implemented")
    }

    override fun weAreCollidingWith(other: ISpaceObject): Boolean {
        TODO("Not yet implemented")
    }

    override fun weCanCollideWith(other: ISpaceObject): Boolean {
        TODO("Not yet implemented")
    }

    override fun weAreInRange(other: ISpaceObject): Boolean {
        TODO("Not yet implemented")
    }

    override fun finalize(): List<ISpaceObject> {
        TODO("Not yet implemented")
    }

    override fun move(deltaTime: Double) {
        TODO("Not yet implemented")
    }

    override fun toString(): String {
        TODO("Not yet implemented")
    }

    override fun turnBy(degrees: Double) {
        TODO("Not yet implemented")
    }

    override fun update(deltaTime: Double, trans: Transaction) {
        TODO("Not yet implemented")
    }

    override fun beginInteraction() {
        TODO("Not yet implemented")
    }

    override fun finishInteraction(trans: Transaction) {
        TODO("Not yet implemented")
    }

    override fun tick(deltaTime: Double, trans: Transaction) {
        TODO("Not yet implemented")
    }

}

But I think I’ve done the wrong thing. Undo implement members:

class Missile(ship: ISolidObject,
              override var position: Point,
              override var velocity: Velocity,
              override var killRadius: Double,
              override val isAsteroid: Boolean,
              override val lifetime: Double,
              override val view: FlyerView,
              override val controls: Controls,
              override val finalizer: IFinalizer,
              override var heading: Double,
              override var elapsedTime: Double
): ISolidObject {

}

Now push down the members from SolidObject. Oh, wait, hell. We can’t, unless we inherit from him. OK, let’s do that, push, then reconnect.

OK, I’ve done this experiment:

class Missile(
    ship: SolidObject,
    override var elapsedTime: Double = 0.0,
    override val lifetime: Double = 3.0
): ISpaceObject {
    var position: Point
    val velocity: Velocity
    val killRadius = 10.0
    init {
        val missileKillRadius = 10.0
        val missileOwnVelocity = Velocity(U.SPEED_OF_LIGHT / 3.0, 0.0).rotate(ship.heading)
        val standardOffset = Point(2 * (ship.killRadius + missileKillRadius), 0.0)
        val rotatedOffset = standardOffset.rotate(ship.heading)
        position = ship.position + rotatedOffset
        velocity = ship.velocity + missileOwnVelocity
    }

    override fun tick(deltaTime: Double, trans: Transaction) {
        elapsedTime += deltaTime
        update(deltaTime,trans)
    }

    override fun update(deltaTime: Double, trans: Transaction) {
        position = (position + velocity * deltaTime).cap()
    }

    override fun beginInteraction() {
    }

    override fun interactWith(other: ISpaceObject): List<ISpaceObject> {
        return emptyList()
    }

    override fun finishInteraction(trans: Transaction) {
    }

    override fun draw(drawer: Drawer) {
        drawer.fill = ColorRGBa.MEDIUM_SLATE_BLUE
        drawer.translate(position)
        NewMissileView().draw(this, drawer)
    }

    override fun finalize(): List<ISpaceObject> {
        return listOf(SolidObject.splat(this))
    }

}

I also built two other little things, a NewMissileView, and a new way to make a Splat:

class NewMissileView {
    fun draw(missile: Missile, drawer: Drawer) {
        drawer.stroke = ColorRGBa.WHITE
        drawer.fill = ColorRGBa.WHITE
        drawer.circle(Point.ZERO, missile.killRadius*3.0)
    }
}

    fun splat(missile: Missile): SolidObject {
        val lifetime = 2.0
        return SolidObject(
            position = missile.position,
            velocity = Velocity.ZERO,
            lifetime = lifetime,
            view = SplatView(lifetime)
        )
    }

What I have at this point is a new class, Missile, that shoots out from the front of the ship just like the other kind of missile, that interacts with nothing, and that times out after three seconds, generating a Splat. I consider this a spike and might well toss it, but so far, it’s interesting. What can be said about interaction? I know that we’re being called in the interactWith function a lot, because I put a print in it. Can we destroy things?

What if I copy over everything from SolidObject?

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

    private fun weAreCollidingWith(other: ISpaceObject) = weCanCollideWith(other) && weAreInRange(other)

    private fun weCanCollideWith(other: ISpaceObject): Boolean {
        return if ( other !is SolidObject) false
        else ( other.isAsteroid)
    }

    private fun weAreInRange(other: ISpaceObject): Boolean {
        return if ( other !is SolidObject) false
        else position.distanceTo(other.position) < killRadius + other.killRadius
    }

Mysteriously, this actually works in the game. The new missile shoots down asteroids just like the old one did. I think we can simplify the interaction, and because I’m trying to learn how to do this well, let’s do.

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

    private fun weAreCollidingWith(other: ISpaceObject) 
    = weCanCollideWith(other) 
            && weAreInRange(other)

    private fun weCanCollideWith(other: ISpaceObject): Boolean 
    = (other is SolidObject) 
            && other.isAsteroid

    private fun weAreInRange(other: ISpaceObject): Boolean 
    = other is SolidObject 
            && position.distanceTo(other.position) < killRadius + other.killRadius

Not much different, not much better. Let’s have a break and an upsumming. I’m going to commit. I have to change two tests to expect a Missile rather than a SolidObject, because fire is now firing a Missile. Commit: use new Missile.

Summary

The first phase this morning went quite smoothly, pushing all the default methods in SpaceObject down into the classes. It’s not even all that messy.

However, looking at the SolidObject, we see that it has become a sort of garbage dump for every idea in the history of mankind, although to be fair, implementing a new kind of ISpaceObject, a Missile, has only seven required methods, and two of those, tick and finalize, can be eliminated entirely, I believe.

The interaction logic remains a bit complicated but if all else fails I’ll figure out what Hill is doing and copy that. I think his basic path has pulled out some new object, with some kind of “dispatch” table that lets you decide with whom you care to associate.

And the Missile works, is the point, and it’s not even a particularly ugly implementation. I have a concern: is missile only destroying asteroids because it is coming up first in the mix? Let’s look and see how that works now:

Ah, here’s why. We are currently guaranteeing that when the Missile hits anything, it gets dibs on the interaction:

    fun prioritize(p: Pair<ISpaceObject, ISpaceObject>): Pair<ISpaceObject, ISpaceObject> {
        val first = p.first
        val second = p.second
        if (first is Score) return Pair(second,first) // could be ScoreKeeper
        if (second is Score) return Pair(first, second) // could be ScoreKeeper
        if (first is SolidObject) return Pair(second,first) // others want a chance
        return p
    }

Since Missile isn’t SolidObject, an asteroid::missile turns into missile::asteroid, and we’re in Missile’s collision code. Good enough for now. We probably need something better soon.

We surely need some tests for Missile. I’ve put those on the list to do. For now, let’s push the article and find something to eat.

See you next time!



  1. Ever tried. Ever failed. No matter. Try Again. Fail again. Fail better. – Samuel Beckett