Kotlin 226 - Assessing
GitHub Decentralized Repo
GitHub Centralized Repo
I’ve gone back and forth on how much type information to preserve. Let’s try to compare the two.
To summarize what we’re comparing, there are two versions of SpaceObjectCollection and Transaction that we should look at. One version preserves all the individual types, asteroid, missile, and so on, in real collections. The other version discards most of the type information, though SpaceObjectCollection can product a list of any type, by simply filtering the objects by type.
I built the type-preserving version to learn how to do it, and because the then-current code for calculating collisions wanted to look at specific types, avoiding checks that aren’t necessary, such as checking asteroids to see if they collide with each other. (They never do: them’s the rules.) I subsequently decided that the prior collision-checking code was preferable, because it is simpler although less efficient. Once that was in place, the need for type-checking the space objects was no longer so strong, so I went ahead and removed most of the type checking. That’s where we are now.
There are a couple of “costs” to the current type-hiding scheme:
-
Collision checking now compares all pairs of objects. Assume we have 10 missiles in flight, plus saucer, ship, and 12 asteroids. The careful scheme would do about 164 comparisons and the all-pairs version would do 253. I prefer the simpler version, but it is more costly. It has no discernible effect on game performance.
-
Collision-checking now does one more message send than in the type-checked version. The pair-wise one sends
interactWith
to one member of the pair, and each object implements that as a type specific call tointeract
, providing the necessary type information. The type-aware version calls theinteract
method directly, since it already knows both types.
Let’s compare some of the code. We’ll look at SpaceObjectCollection: I’ll leave out all the code that remains the same between the two versions.
TYPE-HIDING
class SpaceObjectCollection {
private val colliders = mutableListOf<Collider>()
private val deferredActions = mutableListOf<DeferredAction>()
private val spaceObjects = mutableListOf<SpaceObject>()
fun add (spaceObject: SpaceObject) {
spaceObjects.add(spaceObject)
if (spaceObject is Collider) colliders.add(spaceObject)
}
fun asteroids() = spaceObjects().filterIsInstance<Asteroid>()
fun colliders() = colliders
fun deferredActions() = deferredActions
fun missiles() = spaceObjects.filterIsInstance<Missile>()
fun saucers() = spaceObjects.filterIsInstance<Saucer>()
fun ships() = spaceObjects.filterIsInstance<Ship>()
fun spaceObjects():List<SpaceObject> = spaceObjects
fun splats() = spaceObjects.filterIsInstance<Splat>()
}
TYPE-PRESERVING
class SpaceObjectCollection {
val asteroids = mutableListOf<Asteroid>()
val deferredActions = mutableListOf<DeferredAction>()
val missiles = mutableListOf<Missile>()
val saucers = mutableListOf<Saucer>()
val ships = mutableListOf<Ship>()
val splats = mutableListOf<Splat>()
// update function below if you add to these
fun allCollections(): List<MutableList<out SpaceObject>> {
return listOf (asteroids, deferredActions, missiles, saucers, ships, splats)
}
fun spaceObjects():List<SpaceObject> = asteroids + missiles + saucers + ships + splats
fun add(asteroid: Asteroid) {
asteroids.add(asteroid)
}
fun add(missile: Missile) {
missiles.add(missile)
}
fun add(saucer: Saucer) {
saucers.add(saucer)
}
fun add(ship: Ship) {
ships.add(ship)
}
fun add(splat: Splat) {
splats.add(splat)
}
}
I think that’s a pretty fair comparison. There are certainly more methods on each side, but they are essentially the same, with minor tweaks.
So on the one side, we have lots of add
methods, one for each type, and on the other, we just have one add
method. There is the addition of the code to save colliders
, which the old scheme didn’t need. Providing the colliders
collection lets us avoid checking the splats for collisions, since they don’t collide with anything.
It’s worth noting that the game actually does look at most of the virtual collections provided. The ships
and splats
collections are only referenced in tests, but the others are used. For example, the game checks the missile count when deciding when to recreate a destroyed ship, and it checks to be sure the saucer is missing as well.
Which is Better?
Which of these two implementations is “better”?
I think you could argue from principle that preserving type information is generally good. We do have more collections in the type-preserving format, but no more elements. And we do have a bit more code but space isn’t tight these days.
In the specific case we have, I think we have a different preference depending on which collision detection we are using. If we use the type-sensitive one, then type preservation is better because the individual typed collections can be provided at low cost. If we use the pairwise version, we would have to construct the combined collection, so the type-hiding version seems better.
However, it would be easy enough in the type-preserving version to save the colliders into a separate collection, at low additional cost, and then there’d be little performance difference between them. The type-preserving version would have a slight advantage, because it can provide the detailed collections of asteroids and missiles, which the game does like to think about.
I think I’d argue that even using the pair-wise collision checking, the type-preserving version of SpaceObjectCollection, if we add in saving colliders, will be at least as performant as the type-hiding version.
I think that looking at the code comparison above there’s no reason to think that the type-hiding version is more clear. It does have fewer add methods, but the add methods are quite clear and obvious.
Today’s Conclusion
I think the difference is not large, but that
- the type-preserving approach is preferable on principle, that
- the two are about equal on clarity of the code, and that
- the type-preserving approach is probably slightly more performant.
Today, I think type preservation wins. It’s a near thing, so I might think differently tomorrow.
Shall We Switch Back?
I don’t think we’ll do that. The difference isn’t great, and we have no need for the small performance improvement that we would probably get. to get back to the type-preserving version we could either pull in some old code and debug it, or refactor yet again to the type-preserving solution.
What we could do would be to preserve types incrementally, along the way of doing other work. We could, for example, preserve asteroids in a separate collection, because the game likes to know how many there are. We’d have to modify Transaction and SpaceObjectCollection at the same time (or SpaceObjectCollection after Transaction), unless we wanted to actually check the types in SpaceObjectCollection instead of letting Kotlin do the work. That might be good for a hack, but it wouldn’t really be good code.
We’ll do better to focus on any remaining features for the game, or to seek out areas that need improvement from which we can learn … or to work on something completely different. I have no idea what something completely different might be.
The only remaining official feature is the small saucer, which appears in place of the large saucer when your score gets high enough. (Mine never does.) The small saucer is smaller, has a higher associated score if you shoot it down, and I believe all its missiles are targeted instead of only a few as with the large saucer.
I think we could do that in a morning. Maybe we will. Otherwise, I may need to think of a new project. Feel free to offer ideas, but I don’t promise to do them. I only work on things I want to work on. The joy of retirement!
See you next time!