Codea Craft 5: Another creature.
Today I’ll build a different kind of creature, who moves differently from the Orc. The idea is to build up an understanding of where the design needs improvement in terms of flexibility or clarity.
My rough plan today is to build another moving creature into our little program. I intend for this next one to move in a circle. I also intend that it shouldn’t look like an Orc. So we’ll have at least two bits of variability, and then we’ll look for common elements and how to deal with the similarities and differences in our creatures.
I’ve looked through Codea’s provided asset packs and decided, just for fun, to use one of the Watercraft boats. That should be fun. I’ll create a new class, borrowing the code from Orc.
lua Watercraft = class()
function Watercraft:init(entity)
self.entity = entity
self.step = 0.02
entity.model = craft.model("Watercraft:watercraftPack_003")
entity.position = vec3(0,-1,0)
entity.scale = vec3(1,1,1) / 8
entity.eulerAngles = vec3(0, 180, 0)
end
function Watercraft:update(dt)
end
That should position the ship and not move it. We’ll see what we get. Now to set one up:
function setup()
Scene = craft.scene()
setupSky(Scene)
setupOrc(Scene)
setupWatercraft(Scene)
setupCamera(Scene)
end
function setupWatercraft(scene)
scene:entity():add(Watercraft)
end
Sure enough, that nearly works as intended. It looks like this:
Full disclosure, I had in mind putting the ship in front of the Orc, but I forgot (again!) that front to back in Codea Craft is the z axis, not Y. But that’s OK, because we’re going to move the ship anyway.
How is that to be done? The easiest way I know to draw a circle is to vary an angle, theta, from 0 to two pi, and position at r times the cosine and sine of theta. Except that I know a better way to do that as well. We saw it in that tricky camera bit, in a slightly different form. If we take a vector, say <1,0,0> and rotate it around Y by, oh, 90 degrees, we’ll get <0,0,1>. So it turns out that we can use Codea’s ability to rotate a vector to give us a circle.
That’s my theory, anyway. Now we’ll have to aim the ship as well, but for now I’m going to skip that part in the interest of tiny steps. Let’s just do the motion:
Hey! Did you notice that when I copied and pasted the Orc code, I kept the step variable? Our watercraft doesn’t need that. That’s an issue with copy/paste: we wind up with vestigial tails and stuff that we don’t need, and in a few days or a month we’ll be wondering why that’s there, when it shouldn’t be there at all. Anyway … that’ll be gone as soon as I do this:
-- Watercraft
-- RJ 20200204
Watercraft = class()
function Watercraft:init(entity)
self.entity = entity
self.radiusVector = vec2(1.5,0)
self.theta = 0
self.deltaTheta = 0.01 -- random guess
entity.model = craft.model("Watercraft:watercraftPack_003")
entity.position = vec3(0,-1,0)
entity.scale = vec3(1,1,1) / 8
entity.eulerAngles = vec3(0, 180, 0)
end
function Watercraft:update(dt)
local pos2d = self.radiusVector:rotate(self.theta)
self.theta = self.theta + self.deltaTheta
self.entity.x = pos2d.x
self.entity.z = pos2d.y
end
Codea doesn’t have a rotate for the vec3 type (because we have a built in type matrix
that does the whole 4x4 quaternion thing). We’ll explore that in due time, I suppose. For now, since I have a flat ocean, I just rotate the 2-vector and then plug its x and y into x and z. I cleverly remembered that z is forward and back this time. And it works:
Now the ship needs to set its own rotation. Entities have “eulerAngles”, as we’ve seen but let’s see if we can do better. I’ll take a quick look at the docs for entity.
Ok, yeah, well, no. The entity does accept a rotation, in the form of a quaternion. I’m not ready to figure out quaternions yet, so we’ll go with the euler angles. I noticed that we’ve set the ship’s euler angles (in degrees) to 0, 180,0, and that’s dead backward. I think there’s a good chance that his euler angle, which should be just an angle around y axis, is equal to theta. Except that euler angles are in degrees. “A foolish consistency” etc etc. Anyway …
That was amusingly wrong. I’ll spare you the code and show you the video
The following works:
-- Watercraft
-- RJ 20200204
Watercraft = class()
function Watercraft:init(entity)
self.entity = entity
self.radiusVector = vec2(1.5,0)
self.theta = 0
self.deltaTheta = 0.01 -- random guess
entity.model = craft.model("Watercraft:watercraftPack_003")
entity.position = vec3(0,-1,0)
entity.scale = vec3(1,1,1) / 8
entity.eulerAngles = vec3(0, 180, 0)
end
function Watercraft:update(dt)
local pos2d = self.radiusVector:rotate(self.theta)
self.theta = self.theta + self.deltaTheta
self.entity.x = pos2d.x
self.entity.z = pos2d.y
self.entity.eulerAngles = vec3(0, math.deg(-self.theta), 0)
end
We rotate the ship by - theta
, converted to degrees, rather than theta
. Probably I could have figured that out, but honestly it’s easier to try it and look.
Here’s the code for Orc, to compare with Watercraft:
-- Orc
-- RJ 20200202
Orc = class()
function Orc:init(entity)
self.entity = entity
self.step = 0.02
entity.model = craft.model("Blocky Characters:Orc")
entity.x = 0
entity.y = -1
entity.z = 0
entity.scale = vec3(1,1,1) / 8
entity.eulerAngles = vec3(0, 180, 0)
end
function Orc:update(dt)
local x = self.entity.x + self.step
if math.abs(x) >= 2 then self.step = -self.step end
self.entity.x = x
end
Comparing and Contrasting
Well, of course so far, our two objects only have an init
and an update
. The update
is entirely different, and while they are similar in style, I would argue that the init
is almost entirely different as well.
Given that our updates completely control the motion of our creatures, the only initialization that really matters is the setting of the model (Orc vs Watercraft) and the setting of the member variables. The latter are really entirely different. The Orc has a position (single x but it could be a vector) which is updated linearly, while the Watercraft has a single angle, theta, which is updated (also linearly). I could have obfuscated this difference by calling deltaTheta
step
instead but that would truly be obfuscating, since Orc’s step
is in meters and Watercraft’s is in radians.
Are these classes so entirely different that we should ignore similarities, at least for now? Or should we try now to pull out commonalities and separate on differences?
Don’t talk to me about subclassing!
Someone around here is thinking we need an abstract superclass of MovingObject and some concrete subclasses. No. Kent Beck used to tell us “Inheritance is a card you can only play once. Keep it in your hand as long as possible.” This statement is, like so many good ideas, not formally true, but informally very true.
These days, I’d almost never use class inheritance. It tends to make some dimension of the problem special, and almost always that dimension doesn’t deserve being made special. In addition, making it special often makes the variability harder rather than easier.
Someone else said “inheritance is a programmer hack for saving code”. I’m not sure who it was, but I think they had it right. We do want to write as little code as possible, and reuse as much as possible, at least if it makes our work easier.
For now, I see no substantial code to be reused, no big savings to be had, via inheritance or any other means.
Therefore, we’ll do nothing. We’ll let these two classes stand as they are, and see what the future brings.
I’m a bit surprised, and a bit sad. I’d hoped to find some duplication that we could remove, but it just hasn’t happened. That’s probably really a good thing, in that it means we can build separate moving creatures without a lot of rigmarole, but good or bad, it seems to be the current case.
We’ll see about trying something else next time … thanks for tuning in.