CoCraTu 4
Here’s how you really do that.
It is common, in tutorials like this, to write out the code that’s needed in a “long-hand” form, on the grounds that it’s a good way to show the reader all the procedure that’s needed to accomplish the job. The result, too often is that the tutorial displays code like this as if it were the end point:
Not a great way …
adventurer = scene:entity()
adventurer.model = craft.model(asset.builtin.Blocky_Characters.Adventurer)
adventurer.scale = vec3(1,1,1)/8
adventurer.y = -1
adventurer.eulerAngles = vec3(0,0,0)
wanderer = scene:entity()
wanderer.model = craft.model(asset.builtin.CastleKit.knightRed_obj)
wanderer.scale = vec3(1,1,1)/8
wanderer.y = -1
wanderer.eulerAngles = vec3(0,0,0)
floor = scene:entity()
floor.model = craft.model.cube(vec3(6, 0.1, 6))
floor.y = -1.05
floor.material = craft.material(asset.builtin.Materials.Specular)
floor.material.map = readImage(asset.builtin.Blocks.Dirt)
daydream = scene:entity()
daydream.model = craft.model(asset.builtin.Watercraft.watercraftPack_018_obj)
daydream.y = 8*2.25
daydream.x = 8*0.5
daydream.parent = parent
If we’re lucky, maybe the author tosses in a few blank lines to set off the various bits.
Even up until now, I’ve tried to do better. Once the code is inline, we’ve improved its modularity, and, I think, its clarity, organizing it this way, with separate functions for separate logical bits:
A better way? …
Adventurer = createAdventurer()
Wanderer = createWanderer()
Daydream = createDaydream(Adventurer)
createFloor()
function createAdventurer()
local adventurer = scene:entity()
adventurer.model = craft.model(asset.builtin.Blocky_Characters.Adventurer)
adventurer.scale = vec3(1,1,1)/8
adventurer.y = -1
adventurer.eulerAngles = vec3(0,0,0)
return adventurer
end
...
I believe that the authors who write in the first form are doing what they think is best, focusing on keeping the tutorial focused on the essence of the API that’s being documented, and assuming that developers will organize the code well, because they know it is to their advantage to do so. That’s as may be.
In this tutorial series, I’ve shown the in-line way of doing things, and then, when some bit was “done”, I’ve shown how to make the code more modular, more clear, more habitable. I think that’s part of my job, to mark the times when code improvement is particularly appropriate, and to show how I might do it.
Today is one of those opportunities.
Flat’s Not Great
Codea offers the notion of classes and objects as part of its release, even though, if we look under the hood, they are actually an addition to the basic Lua language. And in particular with Craft, the implementation is particularly friendly to classes.
In our current implementation we have a global creation function for each entity in our little diorama, the adventurer, the wanderer, and the daydream. And we have a global update function for each as well. Our structure is quite flat:
- setup
- draw
- update
- createAdventurer
- updateAdventurer
- createWanderer
- updateWanderer
- createDaydream
- updateDaydream
- createFloor
The only evidence of fine structure that we have here is the names. In this program, as small as it is, maybe this is OK. In a larger program that actually did something, we can do better.
A Better Design
Here, like a rabbit out of a magician’s hat, is what your intrepid author considers a better design for what we have. It’s still imperfect, but it’s better.
First we’ll see it. Then we’ll talk about it. Then we’ll see where it came from, which was neither out of my hat nor any other mysterious place.
The setup and update:
function setup()
scene = craft.scene()
local adv = scene:entity():add(Adventurer)
scene:entity():add(Wanderer)
scene:entity():add(Daydream, adv.entity)
createFloor()
scene.camera:add(OrbitViewer, vec3(0,0,0), 5, 10, 20)
angle = 0
end
function update(dt)
local degreesPerSecond = 360/5
angle = angle + dt*degreesPerSecond
scene:update(dt)
end
We see that our create functions have been replaced with a call to add
, and they’re passed in something new, Adventurer, Wanderer, and Daydream.
And our update no longer has any explicit calls to update the various items.
So that’s nice. How does it work?
The entity():add(Something,more)
call expects a class as its first parameter, and accepts as many more parameters as you may need. It creates an instance of the provided class, calling the init
, and passing the entity in as the first parameter, followed by any others you may have provided.
In addition, when scene:update()
is called, Craft automatically calls the update methods on any instances it has created. That’s why we were able to remove almost all the code from update()
.
The Adventurer Class
Adventurer = class()
function Adventurer:init(entity)
self.entity = entity
entity.model = craft.model(asset.builtin.Blocky_Characters.Adventurer)
entity.scale = vec3(1,1,1)/8
entity.y = -1
entity.eulerAngles = vec3(0,0,0)
end
function Adventurer:update()
updateCircleMover(self.entity, 2, angle)
end
In the Adventurer class, we have essentially the same code we formerly had in createAdventurer
, just better encapsulated in an object. We only create one, but without much more effort we could create more.
Note that the update
method uses the old updateCircleMover
function. This is a bit naff, but I did say “better”, not perfect.
The other two classes are similar:
Daydream Class
Daydream = class()
function Daydream:init(entity, parent)
self.entity = entity
entity.model = craft.model(asset.builtin.Watercraft.watercraftPack_018_obj)
entity.y = 8*2.25
entity.x = 8*0.5
entity.parent = parent
end
function Daydream:update()
self.entity.x = 2*math.sin(math.rad(angle))
end
The only notable difference here is that Daydream accepts a parent
parameter, which is required to be the entity to which the Daydream is attached, the adventurer in our case.
That’s tucked into the Daydream’s entity’s parent member, which is just the same as we did before.
The Wanderer is essentially the same as the Adventurer
Wanderer Class
Wanderer = class()
function Wanderer:init(entity)
self.entity = entity
entity.model = craft.model(asset.builtin.CastleKit.knightRed_obj)
entity.scale = vec3(1,1,1)/8
entity.y = -1
entity.eulerAngles = vec3(0,0,0)
end
function Wanderer:update()
updateCircleMover(self.entity, 1, -angle/2)
end
In the next section, we’ll look at how this seemingly large change was actually made in smaller steps. Each step took only a few minutes and the program worked after each step. A handful of steps later, the program was improved.
If you’re satisfied with magic, skip the next section, but please do look at the Issues that follow, and the Summary.
Moving Toward Better
Here’s how we did what just happened. It actually took place in small steps that let us improve the code bit by bit without breaking it.
Codea Craft includes a method on entity
, called add
. The writeup, here, says this:
Adds a component to this entity. There are several built-in components that can be added, such as craft.renderer, craft.shape.box and others. Lua classes can also be added as a component. Any additional parameters beyond the type will be forwarded to the component itself. For Lua classes the first parameter passed to the init() function will be the entity itself (followed by the rest of the arguments) allowing it to be stored for later use. Some special callback methods can be implemented in Lua classes to provide extra functionality.
The update() method will be called once per frame (useful for animations and game logic). The fixedUpdate() method will be called once per physics update (useful for physics related behaviour).
If the component is successfully added, the component will be returned. Only one of a given component type can be added at a time.
I’m going to create some classes for our entities. I believe it will make for a better design, for this program, and for future larger programs. We’ll start with the wanderer, because he’s the simplest case.
The Wanderer
The plan is to create a new class, Wanderer, and replace our createWanderer
call with an add
. The wanderer init
will do all the setup that is now done in the createWanderer. Once this is done, we can add an update
method to our wanderer and put the motion code in there.
I’m just going to do this. Arguably I should TDD it but honestly I don’t see much to TDD.
The top level will look like this:
function setup()
scene = craft.scene()
Adventurer = createAdventurer()
-- DELETE Wanderer = createWanderer()
scene:entity():add(Wanderer)
Daydream = createDaydream(Adventurer)
createFloor()
scene.camera:add(OrbitViewer, vec3(0,0,0), 5, 10, 20)
angle = 0
end
function update(dt)
local degreesPerSecond = 360/5
angle = angle + dt*degreesPerSecond
updateCircleMover(Adventurer, 2, angle)
-- DELETE updateCircleMover(Wanderer, 1, -angle/2)
updateDaydream(Daydream, angle)
scene:update(dt)
end
And for the class, we convert this:
function createWanderer()
local wanderer = scene:entity()
wanderer.model = craft.model(asset.builtin.CastleKit.knightRed_obj)
wanderer.scale = vec3(1,1,1)/8
wanderer.y = -1
wanderer.eulerAngles = vec3(0,0,0)
return wanderer
end
To this:
Wanderer = class()
function Wanderer:init(entity)
entity.model = craft.model(asset.builtin.CastleKit.knightRed_obj)
entity.scale = vec3(1,1,1)/8
entity.y = -1
entity.eulerAngles = vec3(0,0,0)
end
I expect this to create our wanderer, but I expect him not to wander. And that’s what happens. He just stands there in the middle of the floor:
We can update him now, by calling our updateCircleMover
method.
function Wanderer:update()
updateCircleMover(self.entity, 1, -angle/2)
end
For this to work, we have to cache entity
. This will always be the case, we should get in the habit.
I expect this to work as before. And it does.
Now we can do the Adventurer and Daydream similarly. We can see that this is going to create a lot of nearly duplicate code, but we already have that duplication. We’re just packaging it differently.
Adventurer
Here’s our main code ready for Adventurer class:
function setup()
scene = craft.scene()
-- DELETE Adventurer = createAdventurer()
local adv = scene:entity():add(Adventurer)
scene:entity():add(Wanderer)
Daydream = createDaydream(adv.entity) -- CHANGED
createFloor()
scene.camera:add(OrbitViewer, vec3(0,0,0), 5, 10, 20)
angle = 0
end
function update(dt)
local degreesPerSecond = 360/5
angle = angle + dt*degreesPerSecond
-- DELETE updateCircleMover(Adventurer, 2, angle)
updateDaydream(Daydream, angle)
scene:update(dt)
end
Note in the above that I had to save the adventurer instance, so that I could pull out its entity to pass to createDaydream. We’ll have a bit more to do when we create that class.
And the class:
Adventurer = class()
function Adventurer:init(entity)
self.entity = entity
entity.model = craft.model(asset.builtin.Blocky_Characters.Adventurer)
entity.scale = vec3(1,1,1)/8
entity.y = -1
entity.eulerAngles = vec3(0,0,0)
end
function Adventurer:update()
updateCircleMover(self.entity, 2, angle)
end
We expect all to work as before, and it does. I’ll spare you the movie bandwidth today.
Daydream
Now we’re ready to do the daydream class. It’s different in two regards. It has a parent entity, which the other two have not, and it has a different kind of motion. Fortunately for us, Codea will pass along any arguments from the add
that we happen to provide. So we can write this:
function setup()
scene = craft.scene()
local adv = scene:entity():add(Adventurer)
scene:entity():add(Wanderer)
-- DELETE Daydream = createDaydream(adv.entity)
scene:entity():add(Daydream, adv.entity)
createFloor()
scene.camera:add(OrbitViewer, vec3(0,0,0), 5, 10, 20)
angle = 0
end
function update(dt)
local degreesPerSecond = 360/5
angle = angle + dt*degreesPerSecond
-- DELETE updateDaydream(Daydream, angle)
scene:update(dt)
end
And the class:
Daydream = class()
function Daydream:init(entity, parent)
self.entity = entity
entity.model = craft.model(asset.builtin.Watercraft.watercraftPack_018_obj)
entity.y = 8*2.25
entity.x = 8*0.5
entity.parent = parent
end
function Daydream:update()
self.entity.x = 2*math.sin(math.rad(angle))
end
Better or Simple Different?
Now, I know what you’re thinking. “Is this better, or only different? And did he fire six shots, or only five?”
The answers are, five, and I think it’s better and I’ll tell you why.
This part is shorter and no less communicative:
function setup()
scene = craft.scene()
local adv = scene:entity():add(Adventurer)
scene:entity():add(Wanderer)
scene:entity():add(Daydream, adv.entity)
createFloor()
scene.camera:add(OrbitViewer, vec3(0,0,0), 5, 10, 20)
angle = 0
end
function update(dt)
local degreesPerSecond = 360/5
angle = angle + dt*degreesPerSecond
scene:update(dt)
end
~~
And this part is better organized:
~~~lua
Adventurer = class()
function Adventurer:init(entity)
self.entity = entity
entity.model = craft.model(asset.builtin.Blocky_Characters.Adventurer)
entity.scale = vec3(1,1,1)/8
entity.y = -1
entity.eulerAngles = vec3(0,0,0)
end
function Adventurer:update()
updateCircleMover(self.entity, 2, angle)
end
Daydream = class()
function Daydream:init(entity, parent)
self.entity = entity
entity.model = craft.model(asset.builtin.Watercraft.watercraftPack_018_obj)
entity.y = 8*2.25
entity.x = 8*0.5
entity.parent = parent
end
function Daydream:update()
self.entity.x = 2*math.sin(math.rad(angle))
end
Wanderer = class()
function Wanderer:init(entity)
self.entity = entity
entity.model = craft.model(asset.builtin.CastleKit.knightRed_obj)
entity.scale = vec3(1,1,1)/8
entity.y = -1
entity.eulerAngles = vec3(0,0,0)
end
function Wanderer:update()
updateCircleMover(self.entity, 1, -angle/2)
end
Why better? Because we have three “things” in our design, an adventurer, a wanderer, and the adventurer’s daydream, and now those things are all nicely separated out, and their similarities are at least as visible as before.
Each one has an init, each one has an update. I call that better.
I do not, however, call it “perfect”, and I’m not sure I’d even call it good enough. Let’s look at some of the remaining issues:
Issues
If we were reviewing this code in a code review, and it was someone else’s code, we’d be all over it. Among the issue we might raise are these:
- Duplication
- The three classes are very similar. When changes are made, some of these similarities will all have to change in unison. Others may not. This should be addressed.
- Magic Values
- The classes are full of magic values like
vec3(1,1,1)/8
and-1
and8*0.05
. Some of these are connected magically: it appears that the 8’s have to change in unison, even in separate classes. - Global angle
- The global variable angle is used, variously, by the individual classes. They should probably manage their own angles. Globals are looked upon askance by our coding standards here Chez Ron.
- Global Function
- The
updateCircleMover
function is a global function. If the design is going to revolve around classes, no pun intended, that function sticks up like a weed among the petunias. - The Floor
- If everything else is a class, shouldn’t the floor also be a class?
Perhaps you have other issues. If so and it’s still near November 22nd, 2021, tweet me up and I’ll address them. For now, we’ll sum up:
Summary
We’ve now seen three ways of creating craft entities and making them move and cavort about:
- Write all the code out inline, one after another;
- Break the code out into creation and updating functions, to better identify what’s going on;
- Create individual classes for different entities, putting all the necessary code inside that class.
The order of presentation should make my preference clear. I think it’s just as easy to create little classes, and that it keeps the behavior nicely isolated, and keeps the program from being so flat.
You get to program however you want to, just as I do, but so far, Number 3 is the form I prefer. I’m sure we can improve things further, and I’m hopeful that we will.
For now, my fellow Adventurers, let’s wander to our respective places of wandering and dream our own daydreams.
“All men dream, but not equally. Those who dream by night in the dusty recesses of their minds, wake in the day to find that it was vanity: but the dreamers of the day are dangerous men, for they may act on their dreams with open eyes, to make them possible.”
– T.E.Lawrence “of Arabia”