Kotlin 118
OK, I’ve seen it move. Should I plug in drawing for each element? Or test controls?
Also it is 0725, something woke me up earlier than I would have chosen. In addition, I have failed to reorder my chai concentrate, with which I begin each day, and I’m going to be fresh out tomorrow. Tomorrow, I’m told, is another day, but it has put me off stride before I even hit my stride.
Enough whining, let’s get to it.
Having had three minutes to think, I believe I’ll start by deciding how each type of FlyingObject will know how to draw itself.
- Aside
- FlyingObject is more typing than I enjoy doing all in a bunch. Should we rename the object to Flyer? They are stored in a collection class called Flyers … but I digress. I’ll let you know what I decide.
The rudimentary drawing I’ve done so far uses this method on FlyingObject:
fun cycle(drawer: Drawer, seconds: Double, deltaTime: Double) {
drawer.isolated {
update(deltaTime)
draw(drawer)
}
}
fun drawShip(drawer: Drawer) {
val points = listOf(
Vector2(-3.0, -2.0),
Vector2(-3.0, 2.0),
Vector2(-5.0, 4.0),
Vector2(7.0, 0.0),
Vector2(-5.0, -4.0),
Vector2(-3.0, -2.0)
)
drawer.scale(30.0, 30.0)
drawer.rotate(30.0)
drawer.stroke = ColorRGBa.WHITE
drawer.strokeWeight = 8.0/30.0
drawer.lineStrip(points)
}
And since it’s the only thing we can draw just now:
private fun draw(drawer: Drawer) {
val center = Vector2(drawer.width/2.0, drawer.height/2.0)
drawer.fill = ColorRGBa.MEDIUM_SLATE_BLUE
drawer.translate(position)
drawShip(drawer)
}
All that was just patching so that I could look at the picture. We need to change the cycle
: we should update all the flyers and then draw them. Why? Because we want a consistent set of objects each time through. Is it harmful this way? I frankly don’t know, but it seems wrong.
Anyway, when we do draw a flyer, it has to draw something unique to that kind of flyer, and perhaps even to the individual. It might be that different asteroids have different shapes. In fact, I’m pretty sure that they are supposed to.
What are some ways we could go?
- Implement draw methods in FlyingObject for each kind of object (with an n-way branch for asteroids?) and tell each object what it is so that it can choose the method.
- Implement the methods and plug in a lambda to call the method;
- Make each kind of flyer a separate class knowing how to draw itself;
- Create a FlyerView interface and give each flyer a suitable view implementation;
- Slam a lambda into the object that has the full drawing code in it.
In the olden days, we’d have a bunch of what we used to call subroutines, one for each way of drawing, or possibly just a table of data, like the endpoints of the object’s outline, and there’d be a value associated with the object, like its position and velocity, used to select the subroutine or table.
I note in passing that the ship is currently drawn from a list (table) of vectors.
This is too simple to pass up. Each FlyingObject is to be created with a list of vectors, which it will use to draw itself with lineStrip
, until further notice. I’m glad I took the time to explain this to you, because that wasn’t my original plan. I was either going to plug in a largish lambda, or create a View object, depending on how high-falutin’ I felt.
Time Suddenly Skips Forward
“However, he said, deleting several paragraphs, “not all FlyingObjects are drawn as a line strip. In particular, missiles are just a dot. A small circle, filled.”
OK, not every idea is a winner. I think we’re going to go with a FlyerView interface and object for each. The idea will be that there is a small class for each kind of flyer, with just one method: draw
. When we create the flyer, we’ll give it a view. That view will be called with a reference to the drawer, and to the flyer, so that it can draw appropriately.
What’s a good way to get there from here? Let’s review draw
and drawShip
. Can we extract drawShip
to a new class, ShipView?
There may be a way to induce IDEA to help me with this. I don’t see it. I guess I’ll just create the Interface and class.
I create this class via copy/pasta:
class ShipView {
fun draw(ship: FlyingObject, drawer: Drawer) {
val points = listOf(
Vector2(-3.0, -2.0),
Vector2(-3.0, 2.0),
Vector2(-5.0, 4.0),
Vector2(7.0, 0.0),
Vector2(-5.0, -4.0),
Vector2(-3.0, -2.0)
)
drawer.scale(30.0, 30.0)
drawer.rotate(ship.heading + 30.0)
drawer.stroke = ColorRGBa.WHITE
drawer.strokeWeight = 8.0/30.0
drawer.lineStrip(points)
}
}
I extract an interface from that:
interface FlyerView {
fun draw(ship: FlyingObject, drawer: Drawer)
}
class ShipView : FlyerView {
override fun draw(ship: FlyingObject, drawer: Drawer) {
val points = listOf(
Vector2(-3.0, -2.0),
Vector2(-3.0, 2.0),
Vector2(-5.0, 4.0),
Vector2(7.0, 0.0),
Vector2(-5.0, -4.0),
Vector2(-3.0, -2.0)
)
drawer.scale(30.0, 30.0)
drawer.rotate(ship.heading + 30.0)
drawer.stroke = ColorRGBa.WHITE
drawer.strokeWeight = 8.0/30.0
drawer.lineStrip(points)
}
}
I create a member in FlyingObject:
class FlyingObject(
var position: Vector2,
var velocity: Vector2,
val acceleration: Vector2,
killRad: Double,
splitCt: Int = 0,
private val controls: Controls = Controls()
) {
lateinit var view:FlyerView
I don’t like the lateinit but I need it at least until I get this thing rigged up right.
I update draw
:
private fun draw(drawer: Drawer) {
val center = Vector2(drawer.width/2.0, drawer.height/2.0)
drawer.fill = ColorRGBa.MEDIUM_SLATE_BLUE
drawer.translate(position)
view.draw(this, drawer)
}
I provide the view when I create the ship:
fun ship(pos:Vector2, control:Controls= Controls()): FlyingObject {
return FlyingObject(
position = pos,
velocity = Vector2.ZERO,
acceleration = Vector2(60.0, 0.0),
killRad = 100.0,
controls = control
).also { it.view = ShipView()}
}
And we’re hooked up … for ship.
What’s bugging me here is the huge amount of information that is packed into a FlyingObject, all at the top level. Let’s review it. No, wait, let me commit this: Ship drawn with Shipview
.
Now look at this mess:
class FlyingObject(
var position: Vector2,
var velocity: Vector2,
val acceleration: Vector2,
killRad: Double,
splitCt: Int = 0,
private val controls: Controls = Controls()
) {
lateinit var view:FlyerView
var killRadius = killRad
private set
var heading: Double = 0.0
var rotationSpeed = 360.0
var splitCount = splitCt
var ignoreCollisions = false
We rather seem to need all of these, at least part of the time. Our two constructors are quite mad:
companion object {
fun asteroid(pos:Vector2, vel: Vector2, killRad: Double = 400.0, splitCt: Int = 2): FlyingObject {
return FlyingObject(
position = pos,
velocity = vel,
acceleration = Vector2.ZERO,
killRad = killRad,
splitCt = splitCt
).also { it.ignoreCollisions = true}
}
fun ship(pos:Vector2, control:Controls= Controls()): FlyingObject {
return FlyingObject(
position = pos,
velocity = Vector2.ZERO,
acceleration = Vector2(60.0, 0.0),
killRad = 100.0,
controls = control
).also { it.view = ShipView()}
}
}
Everyone surely needs these:
- position
- velocity
- killRadius
- ignoreCollisions
These are ship only:
- heading
- acceleration
- rotationSpeed (asteroid?)
- controls
This could be asteroid only:
- splitCount
These are probably global constants that could be raised up:
- acceleration (only applies to ship)
- rotationSpeed (ship only? asteroids?)
We could move acceleration and rotationSpeed into the Controls. That might help. Let’s do that.
class Controls {
var accelerate = false
var left = false
var right = false
var fire = false
var holdFire = false
val acceleration = Vector2(60.0, 0.0)
val rotationSpeed = 360.0
I change the references that used to look into the controlled object for those values:
private fun accelerate(obj:FlyingObject, deltaTime: Double) {
if (accelerate) {
val deltaV = acceleration.rotate(obj.heading) * deltaTime
obj.accelerate(deltaV)
}
}
private fun turn(obj: FlyingObject, deltaTime: Double) {
if (left) obj.turnBy(rotationSpeed*deltaTime)
if (right) obj.turnBy(-rotationSpeed*deltaTime)
}
Tests are running. I can remove rotationSpeed and acceleration from a few spots. Some change signature is needed here.
class FlyingObject(
var position: Vector2,
var velocity: Vector2,
killRad: Double,
splitCt: Int = 0,
private val controls: Controls = Controls()
) {
lateinit var view:FlyerView
var killRadius = killRad
private set
var heading: Double = 0.0
var splitCount = splitCt
var ignoreCollisions = false
Slightly simpler. Why didn’t I want killRadius in the parameters? Something about the fact that I halve it, and I didn’t want it public. Let’s just make it be the parameter for now. Same with splitCount.
We’re down to this in FlyingObject:
class FlyingObject(
var position: Vector2,
var velocity: Vector2,
var killRadius: Double,
var splitCount: Int = 0,
private val controls: Controls = Controls()
) {
lateinit var view:FlyerView
var heading: Double = 0.0
var ignoreCollisions = false
Would it be just too weird to maintain heading in the Controls? Yes, I think so. The other two values are constants relating to capability. Heading is really a property of the ship as individual.
Let’s move the view and ignore up into the parameters.
As I get closer to this, I run into testing issues, needing to add parameters to the tests that I’d rather not add. Let me see if I can default more of these members.
Tests are green with this:
class FlyingObject(
var position: Vector2,
var velocity: Vector2,
var killRadius: Double,
var splitCount: Int = 0,
val ignoreCollisions: Boolean = false,
val view: FlyerView = NullView(),
val controls: Controls = Controls()
) {
var heading: Double = 0.0
Oh, and this:
class NullView: FlyerView {
override fun draw(ship: FlyingObject, drawer: Drawer) {
drawer.stroke = ColorRGBa.WHITE
drawer.text("???")
}
}
I had to provide something, so that tests need not provide things they don’t care about.
We could, in principle, separate position, velocity and heading into some kind of Locus object, and killRadius, splitCount, and ignoreCollisions into a DamageHandler.
Seems like a lot. We’re green. Commit: FlyingObject simplified parameters.
- This is irritating.
- I came in here planning to do something useful. I guess I have sort of done that with the addition of the FlyerView, and the refactoring has given us a simpler object. But somehow I’m not satisfied.
You know what would please me? If we had an asteroid on the screen. Let me do an AsteroidView class.
Here’s the Lua definition for one of the asteroids. I think this is copied from the original game.
{
vec4(0.000000, 2.000000, 2.000000, 4.000000),
vec4(2.000000, 4.000000, 4.000000, 2.000000),
vec4(4.000000, 2.000000, 3.000000, 0.000000),
vec4(3.000000, 0.000000, 4.000000, -2.000000),
vec4(4.000000, -2.000000, 1.000000, -4.000000),
vec4(1.000000, -4.000000, -2.000000, -4.000000),
vec4(-2.000000, -4.000000, -4.000000, -2.000000),
vec4(-4.000000, -2.000000, -4.000000, 2.000000),
vec4(-4.000000, 2.000000, -2.000000, 4.000000),
vec4(-2.000000, 4.000000, 0.000000, 2.000000)
}
function Asteroid:draw()
if NoAsteroids then return end
stroke(255)
fill(0,0,0, 0)
strokeWidth(2)
rectMode(CENTER)
translate(self.pos.x, self.pos.y)
scale(self.scale)
strokeWidth(1/self.scale)
for i,l in ipairs(self.shape) do
line(l.x, l.y, l.z, l.w)
end
end
It seems that I have packed a sort of lineStrip
into the table above where we draw each segment since Codea didn’t have a polyline function when I did this. I don’t know if it has one now, for that matter.
I’ll munge that in Sublime to make a suitable input for lineStrip
… and make an AsteroidView:
class AsteroidView: FlyerView {
override fun draw(ship: FlyingObject, drawer: Drawer) {
val points = listOf(
Vector2(4.000000, 2.000000),
Vector2(3.000000, 0.000000),
Vector2(4.000000, -2.000000),
Vector2(1.000000, -4.000000),
Vector2(-2.000000, -4.000000),
Vector2(-4.000000, -2.000000),
Vector2(-4.000000, 2.000000),
Vector2(-2.000000, 4.000000),
Vector2(0.000000, 2.000000),
Vector2(2.000000, 4.000000),
Vector2(4.000000, 2.000000),
)
drawer.scale(30.0, 30.0)
drawer.scale(4.0,4.0)
drawer.rotate(ship.heading + 30.0)
drawer.stroke = ColorRGBa.WHITE
drawer.strokeWeight = 8.0/30.0/4.0
drawer.lineStrip(points)
}
}
The munging was substantial because, for whatever reason, the lines drawn in Lua are not end to end. If you look carefully at the vec4 lines, the end points do not hook up one after the other. So my naive editing of removing the last two values from each vec4 didn’t work. I had to sort them out. I believe that I may have lifted those values from the original game, but I don’t recall.
Note that I added a second call to scale
. The asteroid scales in size and 4 looks about right for a large one. I also have to adjust the stroke width by the scale. I’ll have to check that but for now, when I run the game on screen, it looks OK:
I am feeling frustrated. I think the issue is that with my tests, I could move so rapidly, but when it comes to this graphics stuff, I have to do fiddly things and then wait to look at the screen.
There must be a lesson there … I wonder what it is.
Let’s look at what we have wrought and sum up.
Summary
We have two views now in place, a ship one and an asteroid one. They each implement a simple interface, FlyerView, which embodies the viewing code for that kind of object. We inject a suitable view at the time of creating the object.
Clearly the asteroid view will need to deal with its other rock shapes. That’ll be a simple extension of the view and should be pretty well isolated.
The breadth of FlyingObject is much smaller now, but it is not what I’d call small. I certainly couldn’t remember the sequence of values you have to provide. Fortunately I have my companion methods and IdEA to help me.
I can imagine separating position, velocity, and heading out into a separate object owned by the flyer, and similarly for the destruction-related information, but I’m not sure it would buy us much. Let’s push our brain stack and think about it.
- Push
- The position, velocity, (heading) could implement an interface, Locus or something. The destruction information could be another. The implementations would be moderately-well-known “constants” in the companion object. Might be worth doing, might try that next time.
-
Pop
We should look at the breadth of operations in FlyingObject as well. It implements: accelerate
, asSplit
, asTwin
, collides
, cycle
, draw
, split
, turnBy
, and update
.
We could move accelerate
over to Controls if we’re willing to just set velocity from there, or if we moved velocity
off to a Locus object. Probably similarly with turnBy
. There might be some way to pull out asSplit
, asTwin
, split
, perhaps into Game. It’s going to accumulate the collisions and deal with them anyway.
I think we can conclude from this that while the object may have more methods than it needs, we’ll have opportunities to make it better.
I’m calling the morning done and a success. But it felt a bit slow, probably because of the futzing with the views, then the refactoring of FlyingObject, which wasn’t particularly automatic. I’d like to get back to a place where I can test with microtests. I go faster that way.
See you next time!