Codea Craft 4 - do we need any objects here?
Today I’ll review the code, and think about whether to build in some object thinking. I want to … but should I?
I haven’t looked yet this morning, but my recollection is that the current code for my moving Orc is pretty clean. But I have questions.
In the first sample that I copied, there was a small object, a “RonMover” associated with the entity
that represented the Orc. The comment in the source code where I stole that idea was this:
-- Create an entity and attach a Mover to make it move automatically
The Mover class only contained an init
method and an update
method, the former recording the entity and speed, and the latter moving the entity.
To me, that seems odd. Here are two ways one might imagine doing it, first the Mover way, then what I’d have expected.
- Mover
- There’s an entity for every object in the scene. Entities with behavior have an attached object, such as a “Mover”, that give them behavior.
- Object
- There are objects in the scene. Some have behavior, some do not.
The problem with the second is that Codea Craft just doesn’t work that way. Codea manages a collection of entity
objects, not a collection of Orcs and Castles and the like. An entity can have (only) one associated component (of a given type). The word “type” here isn’t defined very well. Just “the type of component to add to this entity”. We may have to do an experiment to figure out just what this means. For now, I’m supposing it means you can only add one Lua class, though you can also add a renderer
or a shape;box
or the like.
As I think about it, it seems that I’ll need to think of any classes I write as being attached to the entity, sort of in the style of the Decorator pattern. However, that still leaves a design question open, I think.
Who constructs the entity?
In our present code, the Orc is built this way:
function setupOrc(scene)
MyOrc = scene:entity()
MyOrc.model = craft.model("Blocky Characters:Orc")
MyOrc.x = 0
MyOrcStep = 0.02
MyOrc.y = -1
MyOrc.z = 0
MyOrc.scale = vec3(1,1,1) / 8
MyOrc.eulerAngles = vec3(0, 180, 0)
--MyOrc:add(RonMover)
end
The comment at the bottom shows where we’d add in our Mover. But what about all that other stuff? Right now, that’s done at a higher level than our mover, a higher level than “the Orc”, in my mind.
We could continue to do that, but suppose we create some newer kind of faster-moving Orc? Its creation would look just like the above, except for something like
MyOrcStep = 0.04
Furthermore, as written, we need to create a global, like MyOrc
for every Orc, and add code to our update
method for each one. That’ll never do.
We could build a mover class to deal with that global, but we’d still have all the creation logic written in line and at least one level above where it should be.
So I think a better design is to have a new class, Orc
, that does all the entity creation and hooking up. We’ll work in that direction this morning.
This is a refactoring. When we’re done, we will have exactly the same scene and motion as before. No external behavior change. However, it’s a big refactoring.
What? You call that big??{: .pull-quote}
Yes, big. You probably think a big refactoring is one that takes a week or a month. I think that all refactorings can be made small, and certainly whenever I can see how to make them small, I do so. In this case, I see a split around moving, and setting up … in that order.
New Orc Class
My plan is to build a class named Orc
, not Creature
or MovableObject
or anything more general. Of course I see the future need for that. Of course I’m tempted to do it. But I’ve learned that my vision for a generalized class is pretty weak when I’ve never even built a simple concrete class. And I’ve almost successfully disciplined myself to always start with the simple concrete implementation.
Of course the argument is that we’re gonna need the general stuff and we might as well write it now. I’ve made that argument, and I think it’s generally mistaken. Since I’m writing all this up, you’ll get to see what happens.
My first step is to add an Orc instance to the entity. (I’ll move entity creation into Orc as a later step. My mission now is to get this thing working quickly and cleanly.
function setupOrc(scene)
MyOrc = scene:entity()
MyOrc.model = craft.model("Blocky Characters:Orc")
MyOrc.x = 0
MyOrcStep = 0.02
MyOrc.y = -1
MyOrc.z = 0
MyOrc.scale = vec3(1,1,1) / 8
MyOrc.eulerAngles = vec3(0, 180, 0)
MyOrc:add(Orc,MyOrc)
end
This requires me to build an Orc class:
Orc = class()
function Orc:init(entity)
self.entity = entity
self.step = 0.02
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
I also commented out the code in updateOrc
:
function updateOrc(dt)
-- local x = MyOrc.x + MyOrcStep
-- if math.abs(x) >= 2 then MyOrcStep = -MyOrcStep end
-- MyOrc.x = x
end
I saved it because I was afraid I’d have to refer to it again. The Orc moves fine, and I can delete that code, and the call to it, entirely.
function update(dt, scene)
updateCamera(dt, scene)
scene:update(dt)
end
I checked to be sure it was working by changing step
to be much larger, and sure enough the Orc moved much more rapidly.
Now let’s move creation and population of the entity
into the Orc class. We could do this in a few steps, but I’m feeling strong, so I’ll try to do it in one go.
Arrgh!
That doesn’t work. After a bit of confusion I realize that when we call entity:add
, we pass a class name, not an instance. The add then instantiates our class, passing … [reads docs] … the entity
plus whatever other parameters we include on the call to add
.
OK. So to build an object our minimum call in setup will be, roughly:
scene:entity():add(Orc)
At least that’s what I’ll try. Then we can init the rest of the entity as planned in the Orc:init()
. Trying again:
function setupOrc(scene)
scene:entity():add(Orc)
end
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
A minor bug was that even though I just said to use scene:entity():add(Orc)
, I forgot to edit setupOrc
.
Anyway, now the Orc moves again. It’s 1040 here, and I’d like to leave by 1100, so we’ll stop for now, right after lessons learned, and the code.
First the code in Main. (Orc is as above.)
-- Orc-1
-- RJ 20200203
-- upper case variables are global
-- 20200203: Orc Class
function setup()
Scene = craft.scene()
setupSky(Scene)
setupOrc(Scene)
setupCamera(Scene)
end
function setupSky(scene)
scene.sky.active = false
createGround(-1.125, scene)
end
function setupOrc(scene)
scene:entity():add(Orc)
end
function setupCamera(scene)
Scene.camera.z = -4
local cameraSettings = scene.camera:get(craft.camera)
local fieldOfView = 60
local ortho = false
local orthoSize = 5
cameraSettings.fieldOfView = fieldOfView
cameraSettings.ortho = ortho
cameraSettings.orthoSize = orthoSize
end
function update(dt, scene)
updateCamera(dt, scene)
scene:update(dt)
end
-- Called automatically by codea
function draw()
update(DeltaTime, Scene)
Scene:draw()
end
-- Creates the ground using a box model and applies a simple textured material
function createGround(y, scene)
local ground = scene:entity()
ground.model = craft.model.cube(vec3(4,0.125,4))
ground.material = craft.material("Materials:Specular")
ground.material.map = readImage("Blocks:Dirt")
ground.material.specular = color(0, 0, 0, 255)
ground.material.offsetRepeat = vec4(0,0,5,5)
ground.y = y
return ground
end
function updateCamera(dt, scene)
if CurrentTouch.state == MOVING then
CameraX = (CameraX or 0) - CurrentTouch.deltaX * 0.25
CameraY = (CameraY or 0) - CurrentTouch.deltaY * 0.25
scene.camera.eulerAngles = vec3(CameraY, CameraX, 0)
scene.camera.position = -scene.camera.forward * 5
end
end
Lessons Learned
Even though I broke Orc up into two steps, first attaching and moving, and then putting the Orc creation parameters inside the class, the second step still got me in trouble.
Now in the old days, I’d bang my forehead and tell myself to think better or work harder or something. These days, I try to think of a smaller step.
However, I had a fundamental misconception about creating the Orc, even though I “knew” that you pass a class name, not an instance, to entity:add
. The desire to create the object and then add it, which to me is a more conventional structure, was too strong, and I forgot.
It’s possible that I need some micro-tests here, as described in my earlier TDD article. But I didn’t see then what those tests would be like, and at this moment, I still don’t.
Was there a smaller step than moving all the Orc setup into init
in one go? Surely there was. We had this code:
function setupOrc(scene)
MyOrc = scene:entity()
MyOrc.model = craft.model("Blocky Characters:Orc")
MyOrc.x = 0
MyOrcStep = 0.02
MyOrc.y = -1
MyOrc.z = 0
MyOrc.scale = vec3(1,1,1) / 8
MyOrc.eulerAngles = vec3(0, 180, 0)
MyOrc:add(Orc,MyOrc)
end
I note that MyOrc
is an entity, not an Orc. Possibly that confused me. As for a smaller step, what if I had just moved one line out of setupOrc
into Orc:init()
? The model line would have been ideal, but almost any line would have worked.
It would still require me to begin accessing the entity
inside Orc, and it would have just as readily discovered any mistakes like expecting scene and getting entity, which I think happened at one point.
So my big lesson here is that smaller steps, even smaller than I can at first imagine, and still a good idea.
Other lessons are just hammering Craft concepts into my head, like the fact that you add a class name, not an instance, and that you can’t create the entity inside the Orc class because of that. So the start, to me, is a bit odd, but it should always be the same.
One more thing: “Ron, but WHY???”
I want to address (again) why I think it’s worth-while to spend this much effort on a trivial throw-away program.
The more obvious reason is that in these articles, I’m trying to give the reader insight into what I think about, my reasons for doing things, and a sense of when things go well and when they don’t. This is in contrast to articles that just go BEHOLD! and there’s the code. Maybe some programmer somewhere is good enough to do the behold trick, but I’ve never met them.
Perhaps less obvious is that I’m trying to work out, on a small stage, good ways of building things. I’ve mentioned before that the programming part of these articles is a small fraction of the time used. Most of it is in the writing of the articles. The time spent doing and learning is really quite small, and the payoff is large.
Whatever my “real” Codea Craft application, or whatever real application I might be working on, I need its design to be very good, because poor design slows me down. So working in a small example, and growing it in directions that are next steps for a “real” program, gives me a solid and visible sense of how to do things.
I expect that we’ll see a bit of that happening when I move to more complex motion, and motion of more than one kind of creature.
Finally, I have a fantasy of writing up at least some of Codea Craft with examples that are useful, not just for exercising its capabilities, but that show a learner what a good design is like.
OK, I also admit that I really enjoy making code nicer. That’s in there too.
See you next time!