Kotlin 184: Targeted Missiles
The Saucer can fire targeted missiles. Let’s explore what we might do about that.
From time to time, the Saucer fires a missile that is aimed directly at the Ship. I believe that the original game actually projected the position of the ship forward in time, so as to aim correctly even when the ship was moving. (I could be wrong about that, I’ve only read the comments: understanding the code details would require me to study more than I care to.)
At present, in our game, the missiles have an intrinsic speed of their own, and their velocity on screen is the sum of the missile’s intrinsic velocity and the velocity of the Saucer. Missiles fired forward fly very fast; missiles fired to the rear drift along slowly. That summing of velocities need not have been the case: we could have chosen to set their direction and speed and have them all fly at the missile’s own speed, disregarding that of the Saucer. The current way seemed better. I don’t know whether the original game adds the velocities or not. For perfect emulation, we should know. Perfect is for somebody else, but I’ll try to figure out the assembly code when I get a chance.
What are some options for aiming a missile? Options include:
- Fixed Target Position
- We could aim, however we do it, at the present location of the Ship. Naturally, if the Ship is moving, we’ll probably miss if we do that, but the shots will be close and often good enough.
- Predicted Target Position
- Could we aim at where the Ship will be, if it continues on its present course? Well, sure, but it’s a bit tricky. We can calculate where the Ship will be at any time t in the future, assuming constant velocity: it’s just Pt = P0 + v*(t - t0), where P0 is the position of the ship at time t0 (now), and v is its velocity. So if we knew how long it would take the missile to get there, we’d know t-t0, so we’d know Pt, and we could use whatever aiming mechanism we use to set the missile in that direction.
-
Unfortunately, it’s not quite that easy, because the ship is probably moving toward or away relative to the Saucer, so the missile flight time to Pt will not be the same as the flight time to P0, so we have some tricky algebra to do to get the answer to be correct.
-
P.S. I’m not going to do that subscripting stuff any more. What a pain!
- Override Saucer Velocity
- It would be entirely possible to drop a missile with any velocity we want, setting a new velocity that makes it travel in any desired direction. In essence, once we’ve selected the desired speed and direction, we could just skip over the addition of the ship’s velocity.
- Compute Firing Direction
- Given that the velocity of a missile is its intrinsic velocity plus the ship’s velocity, we can get it to move in any desired direction by aiming with “windage”, that is, picking the angle of aim so that the direction of travel is whatever we want after adding. (Is there always a solution? I suspect not. Interesting question.)
- Mix and Match
- There are certainly a number of ways to put this all together. One key question is whether we want to adhere to the physics of the situation, adding the missile intrinsic velocity to the ship velocity, or whether we just want to drop a missile with a suitable direction and speed.
How might we proceed from right here, where we are now?
One approach would be to do a little vector math to decide how hard it would be to compute the best possible firing angle to hit a moving target. I note two things: a) it seems clear that there is not always a solution, and b) I happen to know that the Saucer was clever enough to fire a wrap-around shot that would take advantage of space wrapping around. If the saucer is far to the right of the ship, it’ll fire across the boundary, and so on.
Another approach would be to just code up a simple solution like get the direction to the Ship and send a missile in that direction at some speed, even without regard to the Saucer velocity. There’s a good chance that that would be quite sufficient. I think I recall a comment in the original code that suggests that the Saucer introduces a bit of error into its firing by not getting the angle quite right.
And a final next thing to do would be to study the existing Atari code, and, I suppose, my existing Lua code, to see what others have done.
What if we did this:
- Make a targeted missile be a different color, just to help us observe how things play. I’ve noticed that the missiles can actually hit me even now, at random. Irritating.
- With some settable probability, drop a simple targeted missile that has been given some speed, and the correct direction to the Ship, ignoring Saucer velocity.
That should be pretty simple. Let’s spike that. (I do not promise to throw this code away. I am not a good person. If I like the code, I keep it.)
The Saucer fires every half second, running this code:
private fun fire(trans: Transaction) {
timeSinceLastMissileFired = 0.0
trans.add(Missile(this))
}
The relevant Missile code is this:
constructor(saucer: Saucer): this(saucer.position, Random.nextDouble(360.0), saucer.killRadius, saucer.velocity, ColorRGBa.GREEN)
class Missile(
shipPosition: Point,
shipHeading: Double = 0.0,
shipKillRadius: Double = U.KILL_SHIP,
shipVelocity: Velocity = Velocity.ZERO,
val color: ColorRGBa = ColorRGBa.WHITE,
val missileIsFromShip: Boolean = false
): ISpaceObject, InteractingSpaceObject, Collider {
init {
val missileOwnVelocity = Velocity(U.SPEED_OF_LIGHT / 3.0, 0.0).rotate(shipHeading)
val standardOffset = Point(2 * (shipKillRadius + killRadius), 0.0)
val rotatedOffset = standardOffset.rotate(shipHeading)
position = shipPosition + rotatedOffset
velocity = shipVelocity + missileOwnVelocity
}
One issue here is that missiles can kill the one who fired them, so they are started a bit away from the source, using that rotatedOffset
that is basically double the kill distance out from the source.
Another question pops into my mind: should aiming the missile be the job of the saucer, or should we have another Missile constructor to fire an aimed missile, putting the brains in the missile rather than the saucer?
Let’s think how we might do this. Let’s assume that we’ll start the missile as if we have aimed at the target, so the “heading” will be in that direction. (With saucers it is presently random.) Given that point, and given that we are targeting, we can just set the missile’s velocity in that direction.
Another question is: how will the Saucer even know where the ship is? Well, it does interact with Ship in case of collision, which means it already sees the ship every cycle. Ha, good, no problem there.
I think I’d like to rename the variables in Missile that refer to ship, such as shipPosition
and shipVelocity
. They are really either the ship’s value or the saucers, depending who is firing. Let me rename them to shooter
.
class Missile(
shooterPosition: Point,
shooterHeading: Double = 0.0,
shooterKillRadius: Double = U.KILL_SHIP,
shooterVelocity: Velocity = Velocity.ZERO,
init {
val missileOwnVelocity = Velocity(U.SPEED_OF_LIGHT / 3.0, 0.0).rotate(shooterHeading)
val standardOffset = Point(2 * (shooterKillRadius + killRadius), 0.0)
val rotatedOffset = standardOffset.rotate(shooterHeading)
position = shooterPosition + rotatedOffset
velocity = shooterVelocity + missileOwnVelocity
}
Test, commit: rename shipXXX variables to shooterXXX.
What if, for our spike at least, we provide another parameter to Missile, the desired velocity, which is usually …
I was going to say zero. It seems to me that this sequence will fire at the ship.
- Determine, in saucer, the direction to the ship (ship position - our position, normalized).
- Convert that to a heading, as if we were going to point in that direction. (arc tangent?)
- Ask for a missile, providing that heading and a shooterVelocity of zero.
I think that just does it. We can fudge all the numbers for the Missile constructor.
I think I’ll spike that. I’ll just make all the missiles good shots for my test. I first spike this much:
private fun fire(trans: Transaction) {
timeSinceLastMissileFired = 0.0
val heading = 0.0
val missile = Missile(position, heading, killRadius, Velocity.ZERO, ColorRGBa.RED)
trans.add(missile)
}
If I’ve got the calling sequence right, this will fire a red missile, directly forward of the Saucer, every half second. And it does. Actually it always fires east, not in the direction it’s moving. That’s what I meant.
So now we just need one more value, the bearing to the Ship. So we save the shipPosition and use it in the fire
function:
private var shipPosition = Point.ZERO
override val subscriptions = Subscriptions(
draw = this::draw,
interactWithAsteroid = { asteroid, trans -> checkCollision(asteroid, trans) },
interactWithShip = { ship, trans ->
shipPosition = ship.position // save it
checkCollision(ship, trans) },
interactWithMissile = { missile, trans -> checkCollision(missile, trans) },
finalize = this::finalize
)
private fun fire(trans: Transaction) {
timeSinceLastMissileFired = 0.0
val directionToShip = (shipPosition - position)
val heading = atan2(y = directionToShip.y, x = directionToShip.x).asDegrees
val missile = Missile(position, heading, killRadius, Velocity.ZERO, ColorRGBa.RED)
trans.add(missile)
}
This works mostly as I expected, firing red missiles, every one of which will hit the ship. I had to put in the asDegrees
, because of course we can’t be consistent between degrees and radians. That confused me for a while.
Now I think I would like to make the firing intermittent, just to see how it looks.
private fun fire(trans: Transaction) {
if (Random.nextInt(4) == 0 ) fireTargetted(trans)
else fireRandom(trans)
}
private fun fireRandom(trans: Transaction) {
timeSinceLastMissileFired = 0.0
trans.add(Missile(this))
}
private fun fireTargeted(trans: Transaction) {
timeSinceLastMissileFired = 0.0
val directionToShip = (shipPosition - position)
val heading = atan2(y = directionToShip.y, x = directionToShip.x).asDegrees
val missile = Missile(position, heading, killRadius, Velocity.ZERO, ColorRGBa.RED)
trans.add(missile)
}
This looks pretty good: the targeted missiles seem to be moving about the same speed as the non-targeted, though of course the regular ones vary in speed depending on which direction they’re fired in. It looks pretty natural to me and if the missiles weren’t red to tip me off, I wouldn’t know which ones to dodge.
My tests are green and the feature is pretty clean. It adds a bit to the game. I’m going to commit it: Saucer now fires red targeted missile with probability 1/4.
The evil of the morning is sufficient thereto. Let’s sum up.
Summary
We did a bit of thinking / planning for ways to approach the idea of a targeted missile. Then we implemented the simplest one we could think of, a red missile that emits from the saucer, at standard missile speed, in the direction of the ship at the instant of firing. If the ship isn’t moving, isn’t moving very fast, or is moving more or less at the same angle as the angle between the ship and the saucer, the targeted missile will hit the Ship.
If the ship is moving rapidly, or is moving at a wide angle from the saucer-ship angle,the targeted missile will almost certainly miss. Still, I’d say that this is a good first version, and I can imagine the decision being made to make it even more accurate. If that decision is made, we have the place where it all happens isolated, in fireTargeted
, and we have demonstrated with this spike that we can configure a missile to proceed in any direction we want at standard missile velocity. (In fact, since when we fire targeted we tell the Missile that the saucer velocity is zero, I’d bet we can adjust the velocity of the missile by lying about the velocity of the saucer. If we were to say that the saucer was flying directly at the ship, the missile would come out faster. And so on.)
We can be pretty confident that we have the basic capability we need, and, again, it has gone into the system quite nicely. That’s a good sign, in my view. When the code doesn’t resist change, it’s a sign that that part of our design is pretty decent.
I am pleased, and ready for a bit of relaxation. I hope you are as well, and I hope you’ll look in next time to find out what we do. I’m rather curious myself.