Kotlin 285 - Spin
GitHub Decentralized Repo
GitHub Centralized Repo
GitHub Flat Repo
It seems that it might be interesting to make the Asteroids spin. I don’t think they spun in the original game, but hey this is the 21st century or something. But wait! You also get varied Asteroid shapes!
So, um, what do we know? Asteroids have an angle, always zero, because, well, everybody’s got an angle, ain’t they? The trick will be getting them to spin. Naturally, we want each one to have its own unique non-zero amount of spin, positive or negative. I’m guessing a maximum spin of about one revolution per second, but we’ll tune it by eye.
I think that when we activate an asteroid, we’d like to change its rotation speed to a new random speed. As a matter of efficiency, it might be good if we only rotate the ones that are active.
- Aside
- It does occur to me that asteroids have four different shapes in the game and that we’ve only currently assigned one shape to them. You could make a case that we should provide for that before any silly rotation stuff. We do, I think, give them a random angle now, and I think that’s done when we create them. No, it’s here!
fun activateAsteroid(asteroid: SpaceObject, scale: Double, position: Vector2, velocity: Vector2) {
asteroid.position = position
asteroid.scale = scale
asteroid.velocity = velocity
asteroid.angle = randomAngle()
asteroid.active = true
}
How nice! We already have a function for activating an asteroid. We can set up the different shapes there, at random, and we can manage the rotation as well.
Now on a given day, we might set up a Component or something to do the spinning. Rather than that, because we don’t have the space limitations of the 6502 version, why don’t we just add another member to SpaceObject, spinRate
, like this:
data class SpaceObject(
val type: SpaceObjectType,
var x: Double,
var y: Double,
var dx: Double,
var dy: Double,
var angle: Double = 0.0,
var active: Boolean = false,
var spinRate: Double = 0.0
)
Then, where … in update …
private fun updateEverything(
spaceObjects: Array<SpaceObject>,
deltaTime: Double,
width: Int,
height: Int
) {
updateTimers(deltaTime)
for (spaceObject in spaceObjects) {
for (component in spaceObject.components) update(component, deltaTime)
if (spaceObject.type == SpaceObjectType.SHIP) applyControls(spaceObject, deltaTime)
move(spaceObject, width.toDouble(), height.toDouble(), deltaTime)
}
}
Or maybe one level down, in move …
fun move(spaceObject: SpaceObject, width: Double, height: Double, deltaTime: Double) {
with (spaceObject) {
x = wrap(x+dx*deltaTime, width)
y = wrap(y+dy*deltaTime, height)
}
}
Yes, there. Spinning is a kind of moving. We’ll do this:
fun move(spaceObject: SpaceObject, width: Double, height: Double, deltaTime: Double) {
with (spaceObject) {
angle += spinRate*deltaTime
x = wrap(x+dx*deltaTime, width)
y = wrap(y+dy*deltaTime, height)
}
}
And as our first experiment, this:
fun activateAsteroid(asteroid: SpaceObject, scale: Double, position: Vector2, velocity: Vector2) {
asteroid.position = position
asteroid.scale = scale
asteroid.velocity = velocity
asteroid.angle = randomAngle()
asteroid.spinRate = 1.0
asteroid.active = true
}
And play. They’re not spinning. Why not? Because spinRate is an angle and needs to be more like 360, not 1.0. I’ll spare you the dizziness, 360, or one rev per second, is way too fast. I’ll try 90. That doesn’t look bad. I’ll try 10, to get a sense for the minimum. That looks OK.
Now how can we readily get a random number between 10 and 90 or -10 and -90?
fun activateAsteroid(asteroid: SpaceObject, scale: Double, position: Vector2, velocity: Vector2) {
asteroid.position = position
asteroid.scale = scale
asteroid.velocity = velocity
asteroid.angle = randomAngle()
asteroid.spinRate = randomSpinRate()
asteroid.active = true
}
fun randomSpinRate(): Double {
val spin = Random.nextDouble(10.0,90.0, )
if (Random.nextInt(0,1) == 1) return spin else return -spin
}
Try that. No, they all spin the same way. Research indicates that the range is exclusive, not inclusive, therefore:
fun randomSpinRate(): Double {
val spin = Random.nextDouble(10.0,90.0, )
if (Random.nextInt(0,2) == 1) return spin else return -spin
}
That seems satisfactory:
Having accomplished this random feat, let’s go ahead and import the other asteroid shapes and apply them. I can grab them from another version and convert them quickly in Sublime Text, my editor of choice.
private val rocks = listOf(
listOf(
Vector2(4.0, 2.0), Vector2(3.0, 0.0), Vector2(4.0, -2.0),
Vector2(1.0, -4.0), Vector2(-2.0, -4.0), Vector2(-4.0, -2.0),
Vector2(-4.0, 2.0), Vector2(-2.0, 4.0), Vector2(0.0, 2.0),
Vector2(2.0, 4.0), Vector2(4.0, 2.0),
),
listOf(
Vector2(2.0, 1.0), Vector2(4.0, 2.0), Vector2(2.0, 4.0),
Vector2(0.0, 3.0), Vector2(-2.0, 4.0), Vector2(-4.0, 2.0),
Vector2(-3.0, 0.0), Vector2(-4.0, -2.0), Vector2(-2.0, -4.0),
Vector2(-1.0, -3.0), Vector2(2.0, -4.0), Vector2(4.0, -1.0),
Vector2(2.0, 1.0)
),
listOf(
Vector2(-2.0, 0.0), Vector2(-4.0, -1.0), Vector2(-2.0, -4.0),
Vector2(0.0, -1.0), Vector2(0.0, -4.0), Vector2(2.0, -4.0),
Vector2(4.0, -1.0), Vector2(4.0, 1.0), Vector2(2.0, 4.0),
Vector2(-1.0, 4.0), Vector2(-4.0, 1.0), Vector2(-2.0, 0.0)
),
listOf(
Vector2(1.0, 0.0), Vector2(4.0, 1.0), Vector2(4.0, 2.0),
Vector2(1.0, 4.0), Vector2(-2.0, 4.0), Vector2(-1.0, 2.0),
Vector2(-4.0, 2.0), Vector2(-4.0, -1.0), Vector2(-2.0, -4.0),
Vector2(1.0, -3.0), Vector2(2.0, -4.0), Vector2(4.0, -2.0),
Vector2(1.0, 0.0)
)
)
That’s the easy part. The hard part is that we draw like this:
fun draw(spaceObject: SpaceObject, drawer: Drawer, deltaTime: Double) {
drawer.isolated {
val scale = 4.0 *spaceObject.scale
drawer.translate(spaceObject.x, spaceObject.y)
drawer.scale(scale, scale)
drawer.rotate(spaceObject.angle)
drawer.stroke = ColorRGBa.WHITE
drawer.strokeWeight = 1.0/scale
shipSpecialHandling(spaceObject, drawer, deltaTime)
drawer.lineStrip(spaceObject.type.points)
}
}
Note the last line. We assume (making you know what out of me) that points
is a static member, the same for all the objects. That’s true for the ship and saucer and missiles, but we don’t want it to be true for asteroids from this day forward.
So that’s in type, in SpaceObjectTypes:
enum class SpaceObjectType(val points: List<Vector2>, val killRadius: (SpaceObject)->Double) {
ASTEROID(asteroidPoints, asteroidRadius),
SHIP(shipPoints, shipRadius),
SAUCER(saucerPoints, saucerRadius),
MISSILE(missilePoints, missileRadius),
SAUCER_MISSILE(missilePoints, missileRadius)
}
That just won’t hold water any more. There’s probably no water on most asteroids anyway. We really must have a separate set of points for every asteroid. Now we could, in principle, have a subscript from 0 to 3 and look them up, but we might as well have the points themselves.
Rather an extensive change though.
Let’s try a bit of hackery. Let’s do add the subscript to the SpaceObject, and init it to non-zero when we activate an asteroid:
data class SpaceObject(
val type: SpaceObjectType,
var x: Double,
var y: Double,
var dx: Double,
var dy: Double,
var angle: Double = 0.0,
var active: Boolean = false,
var spinRate: Double = 0.0,
var pointsIndex: Int = 0
)
And let’s init it in activateAsteroid
:
fun activateAsteroid(asteroid: SpaceObject, scale: Double, position: Vector2, velocity: Vector2) {
asteroid.position = position
asteroid.scale = scale
asteroid.velocity = velocity
asteroid.angle = randomAngle()
asteroid.spinRate = randomSpinRate()
asteroid.active = true
asteroid.pointsIndex = Random.nextInt(0,4)
}
And one more thing, somewhat nasty:
fun draw(spaceObject: SpaceObject, drawer: Drawer, deltaTime: Double) {
drawer.isolated {
val scale = 4.0 *spaceObject.scale
drawer.translate(spaceObject.x, spaceObject.y)
drawer.scale(scale, scale)
drawer.rotate(spaceObject.angle)
drawer.stroke = ColorRGBa.WHITE
drawer.strokeWeight = 1.0/scale
shipSpecialHandling(spaceObject, drawer, deltaTime)
if ( spaceObject.type == SpaceObjectType.ASTEROID) {
drawer.lineStrip(rocks[spaceObject.pointsIndex])
} else {
drawer.lineStrip(spaceObject.type.points)
}
}
}
And that works. Here are all four shapes:
If we had methods, of course, we’d let our types sort this out, but we don’t have methods, so we need to do something a bit different. This is close to as noninvasive as we can get.
I’ll allow it. Let’s sum up, it’s still Sunday and I have reading to do.
Summary
We began with a whimsical idea, making the asteroids rotate. After finding out, surely for the n-th time, how nextInt
works, we got that working by adding a spinRate variable to every object. We might have added a Component of some kind, but in my judgment that would be far more complicated, all to save an addition operation for a few objects.
So that went well. And it gave me the energy to do a long-awaited feature, the four different asteroid shapes. They imported easily, and after a bit of design thinking, it seemed to me that the best thing to do was to have a check in draw
for drawing an asteroid, and to use a random subscript in the space object to fetch the right shape. That, too, worked as intended.
I would say that the code is a bit less nice now, because of the if statement in draw
, but it’s certainly clear enough, and if you’re into efficiency, it’s probably faster than a method dispatch, if we even had methods.
I could imagine putting a points function in the enum instead of just a list, but that seems more complicated than this.
A Random Idea Has Appeared!
What we could do, I suppose, would be to provide a unique draw function, not a points function, maybe named drawPoints
, in each enum
element, and call that from draw
. In fact, that’s such an interesting idea that I’ll probably try it next time, just to see how it works out. I’ve made a note of it.
For now, back to the fantasy novel I’m reading about the Glass Library, The Medici Manuscript, by C.J.Archer. Light, enjoyable, just the thing to while away some time.
See you next time!