Kotlin 115: Ideas
I may not get much coding in today, but I have a plan and will write it up in the few minutes before I have to head out.
I rather like the new scheme with Controls, where that object sends simple messages to the FlyingObject, after making some complex determinations about what’s going on:
class Controls {
var accelerate = false
var left = false
var right = false
var fire = false
var holdFire = false
fun control(obj:FlyingObject, deltaTime: Double): List<FlyingObject> {
turn(obj,deltaTime)
accelerate(obj,deltaTime)
return fire(obj)
}
private fun accelerate(obj:FlyingObject, deltaTime: Double) {
if (accelerate) {
val deltaV = obj.acceleration.rotate(obj.heading) * deltaTime
obj.accelerate(deltaV)
}
}
private fun fire(obj: FlyingObject): List<FlyingObject> {
// too tricky? deponent denieth accusation.
val result: MutableList<FlyingObject> = mutableListOf()
if (fire && !holdFire ) result.add(createMissile(obj))
holdFire = fire
return result
}
private fun createMissile(obj: FlyingObject): FlyingObject {
val missileKillRadius = 10.0
val missileOwnVelocity = Vector2(SPEED_OF_LIGHT / 100.0, 0.0)
val missilePos = obj.position + Vector2(2.0 * missileKillRadius, 0.0).rotate(obj.heading)
val missileVel = obj.velocity + missileOwnVelocity
return FlyingObject(missilePos, missileVel, Vector2.ZERO, missileKillRadius)
}
private fun turn(obj: FlyingObject, deltaTime: Double) {
if (left) obj.turnBy(obj.rotationSpeed*deltaTime)
if (right) obj.turnBy(-obj.rotationSpeed*deltaTime)
}
}
Above is a controller that has enough capability to fly. the ship, if we were to connect someone to the boolean buttons. We actually do that in the tests, but have no user controls as yet, because we have no game. Right now, the same controller can control simpler objects like the asteroid and missiles, because they just move at constant velocity, so never have occasion to run all that nice code. That’s probably potentially confusing to a reader of the code who might wonder why those objects have all that capability.
A similar concern would arise because all the FlyingObjects presently include acceleration variables, headings, and so on, which they do not use.
Another concern is that the Controls object asks the FlyingObject a number of questions, about its available acceleration, its heading, velocity, position, and so on. So I am toying with the idea of having some of that information be stored in the Controls rather than the FlyingObject. (I am not sure this is a good idea. I’m toying with it.)
Only the ship really has a heading or acceleration. They do all have position and velocity. Only the ship really needs a Controls: the others just keep on going until they are destroyed or the game is over. But …
I’m finding it interesting, if not useful, to be thinking of all the flying objects as being of a single kind. It seems to be leading to a design that, while perhaps not as expressive as one with individual classes for each kind, seems to me to be simpler and a bit more like the designs used in programs like this in the olden days. I could be talking through my hat but I’m going to continue in this direction for now.
October 21st
I couldn’t code yesterday, so I did the next best thing and napped a lot did some paper design.
I drew a matrix of what collision had what effect, and, after deciding that splitting was a form of dying, came to see that there are only two situations: when a ship or missile collides with anything, the two are mutually destroyed. When anything else collides, nothing happens: they miss each other.
I arranged that table a couple of different ways, simplifying it a bit, until I came up with this notion: A FlyingObject has a val killer:Boolean
which, if true, says that any pair of colliding objects where one or both of them is a killer, destroys both objects. If both are false, nothing happens.
That seems to me to be going to lead to some very simple code.
This morning I was thinking about another part of the collision issue: processing the list of objects in some ideal way. We could check all pairs with something like this:
for (first in objects) for (second in objects) {}
That would process each pair twice, which is twice as much as we need. I’ve come up with this:
for (i in 1..objects.size) for (j in i..objects.size) {}
- Aside
- The following paragraph may have the right idea, but definitely has the wrong details. We’ll see the upshot of this, but Jeffries does finally get it right.
That will generate all index pairs with i < j, which isn’t entirely unreasonable, assuming that our list is represented in some reasonable way. Is it an array list? If it is, adding and deleting items may be an issue. Deleting, anyway. But that happens infrequently.
At this writing, I’m not seeing anything very clever to be done. But I do see that I can do what I need in a number of ways.
Let’s do a collision test.
Arrgh, as I write this, I realize that while I’ve hand-created a missile in the test for the fire
button, I have no real missile creation method yet. If I were reasonable, I’d go backfill a decent missile creation. I am not reasonable. I’ll do this test by hand, because it’s in my mind, then I’ll deal with cleaning up missile creation. That whole area needs improvement anyway.
This may be a mistaken way to proceed. We’ll see. I think this is what I want:
@Test
fun `collision test`() {
val p1 = Vector2(100.0,100.0)
val p2 = Vector2(200.0, 200.0)
val v = Vector2.ZERO
val a1 = FlyingObject.asteroid(p1,v)
val m1 = FlyingObject(p1, v, Vector2.ZERO, 10.0)
val s1 = FlyingObject.ship(p1)
val a2 = FlyingObject.asteroid(p2,v)
val a3 = FlyingObject.asteroid(p2,v)
val objects = mutableListOf<FlyingObject>(a1,m1,s1, a2,a3)
val shouldDie = mutableListOf<FlyingObject>()
for (i in 0..objects.size) {
for (j in i..objects.size) {
if (objects[i].killer || objects[j].killer) {
shouldDie.add(objects[i])
shouldDie.add(objects[j])
}
}
}
assertThat(shouldDie.size).isEqualTo(4)
}
- Aside
- This test and the next one that I do are pretty long, but I need to check collection handling, so I need multiple items in the collection. No way around it that I can see.
As it happens, we don’t have a killer var in FlyingObject. For now, I’ll patch it in. I just want to get a sense of whether this test works.
Oh, just had a decent idea. An object is a killer if its kill radius is non zero.
Arrgh. First of all the looping is wrong, should be this:
for (i in 0 until objects.size-1) {
for (j in i until objects.size) {
if (objects[i].isKiller || objects[j].isKiller) {
shouldDie.add(objects[i])
shouldDie.add(objects[j])
}
}
}
Ah, and the radius trick won’t work. Ships and missiles are killers. Asteroids are not. Gotta make it be that way.
I must be off my game, but finally I get it right:
@Test
fun `collision test`() {
val p1 = Vector2(100.0,100.0)
val p2 = Vector2(200.0, 200.0)
val v = Vector2.ZERO
val a0 = FlyingObject.asteroid(p1,v) // yes
val m1 = FlyingObject(p1, v, Vector2.ZERO, 10.0) // yes
val s2 = FlyingObject.ship(p1) // yes
val a3 = FlyingObject.asteroid(p2,v) // no
val a4 = FlyingObject.asteroid(p2,v) // no
val objects = mutableListOf<FlyingObject>(a0,m1,s2, a3,a4)
val shouldDie = mutableSetOf<FlyingObject>()
for (i in 0 until objects.size-1) {
for (j in i+1 until objects.size) {
val oi = objects[i]
val oj = objects[j]
if (oi.position == oj.position && (objects[i].isKiller || objects[j].isKiller)) {
shouldDie.add(objects[i])
shouldDie.add(objects[j])
}
}
}
assertThat(shouldDie.size).isEqualTo(3)
}
Wow, that was ragged!
What happened here? Well, one way of looking at it is that I was spiking a way of doing collisions. That’s about the best face I can put on it. Over the course of getting this to run, I had both the starting and ending indices of both for loops wrong. I forgot to check the positions, which stands in for checking that the objects are within kill distance. I forgot that possibly (very unlikely in real life) the same object might be killed by two others. (Suppose two asteroids enter the ship’s kill distance at the same cycle. Each would report the death of the ship.) So the output has to be a set, not a list.
All those mistakes aside, I think the test above does a decent job of finding all the collisions. I think I’ll try recasting that test with better syntax until we have decent looking code.
The key bits of my new test:
for (oi in objects) {
for (oj in objects) {
if (collides(oi,oj)) {
shouldDie.add(oi)
shouldDie.add(oj)
}
}
}
assertThat(shouldDie.size).isEqualTo(3)
}
fun collides(oi:FlyingObject, oj:FlyingObject): Boolean {
if (oi == oj ) return false
if (oi.isKiller || oj.isKiller) {
return oi.position == oj.position
}
return false
}
Let’s put a collides method on FlyingObject for an even better test.
I update the collide method that already existed:
fun collides(other: FlyingObject):Boolean {
if ( this === other) return false
if ( !this.isKiller && !other.isKiller) return false
val dist = position.distanceTo(other.position)
val allowed = killRadius + other.killRadius
return dist < allowed
}
Note that I used the ===
, not ==
. I suppose two of these objects might be considered equal, and I am only trying to exclude an object colliding with itself.
Having to not
isKiller irritates me. Let’s replace with a better term. ignoreCollisions? Let’s try that. That looks good. Now I’ll improve that first collision test a bit, just to show the indexing form.
Here’s what we’ve got now:
fun collides(other: FlyingObject):Boolean {
if ( this === other) return false
if ( this.ignoreCollisions && other.ignoreCollisions) return false
val dist = position.distanceTo(other.position)
val allowed = killRadius + other.killRadius
return dist < allowed
}
@Test
fun `collision INDEXING test`() {
val p1 = Vector2(100.0,100.0)
val p2 = Vector2(500.0, 500.0)
val v = Vector2.ZERO
val a0 = FlyingObject.asteroid(p1,v) // yes
val m1 = FlyingObject(p1, v, Vector2.ZERO, 10.0) // yes
val s2 = FlyingObject.ship(p1) // yes
val a3 = FlyingObject.asteroid(p2,v) // no
val a4 = FlyingObject.asteroid(p2,v) // no
val objects = mutableListOf<FlyingObject>(a0,m1,s2, a3,a4)
val shouldDie = mutableSetOf<FlyingObject>()
for (i in 0 until objects.size-1) {
for (j in i+1 until objects.size) {
val oi = objects[i]
val oj = objects[j]
if (oi.collides(oj)) {
shouldDie.add(objects[i])
shouldDie.add(objects[j])
}
}
}
assertThat(shouldDie.size).isEqualTo(3)
}
@Test
fun `collision PLAIN LOOP test better`() {
val p1 = Vector2(100.0,100.0)
val p2 = Vector2(500.0, 500.0)
val v = Vector2.ZERO
val a0 = FlyingObject.asteroid(p1,v) // yes
val m1 = FlyingObject(p1, v, Vector2.ZERO, 10.0) // yes
val s2 = FlyingObject.ship(p1) // yes
val a3 = FlyingObject.asteroid(p2,v) // no
val a4 = FlyingObject.asteroid(p2,v) // no
val objects = mutableListOf<FlyingObject>(a0,m1,s2, a3,a4)
val shouldDie = mutableSetOf<FlyingObject>()
for (oi in objects) {
for (oj in objects) {
if (oi.collides(oj)) {
shouldDie.add(oi)
shouldDie.add(oj)
}
}
}
assertThat(shouldDie.size).isEqualTo(3)
}
Let’s …
Sum Up
The second form is easier to understand, and executes n squared comparisons, while the indexed form hits n*(n-1)/2). Both are O(n squared) but the indexed form does fewer (and produces no duplicated a:b b:a pairs).
Anyway … what we have here is some progress on collisions. We have what I think is a correct collides
method in FlyingObject, taking into account that we only consider collisions involving ships or missiles to be of interest. And we’ve done that without needing to know the object type. There are just two cases, basically interesting or uninteresting.
We have need of a better creation method for missile
, and should review the asteroid and ship as well, though I think they are OK. In making the tests run, I found that the current values for collision radius seemed too large, but I’ll want to look again at the Lua version to see what I had there. I certainly had to move things what seemed like a long way apart.
Of course, I’m assuming a universe of 10,000 x 10,000. We could just as well have a universe of 1x1, since our vectors are floats.
We still need to have a standard place for all our named constants, and we should of course have no unnamed constants other than 0 and 1, which we could call Zero and One for that matter, but won’t.
I think the next few sessions should work on these things:
- Normalize creation of missiles;
- Converge creation for clarity as needed;
- Create a Game object to contain all the objects.
- Write the game loop to cycle all the objects.
And soon, we should do
N. Come up with a form of the game that draws on the screen.
I’m proud of the fact that, aside from watching a square go form one side of the screen to the other, so far the game has been created entirely with tests, rather than by watching it play on the screen. I suspect that when we do put it on screen, we’ll want to tune some constants, but I’ll be disappointed if looking at the screen turns up any real problems.
And I am pretty confident that it’ll work just fine. Of course, I’ve been over-confident in the past.
We’ll see. Stop by next time and see what I do. I’m curious to find out, myself.