Codea Craft 7 -- Cleaning the bird.
The bird experiment worked pretty well. Now it’s time to clean it up and consolidate what we’ve discovered.
I’m happy with having pretty much figured out child entities, but the overall implementation leaves a bit to be desired. One issue was in the text display that I wanted over the ship. That looked like this, in the main draw
function:
-- Called automatically by codea
function draw()
update(DeltaTime, Scene)
Scene:draw()
-- transform playground
local tvec = Ship:transformPoint(vec3(0,0,0))
local tstr = string.format("tvec is %f %f %f", tvec.x, tvec.y, tvec.z)
text(tstr, 500, 500)
end
I’m deleting that, but I’m sure I’ll want that capability again. To make that work, I had to define a global variable Ship
, via:
function Watercraft:init(entity)
Ship = entity -- darn
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
That’s going away too, right now, but the general issue remains as to how to deal with this sort of thing. The class instances that we attach to entities do get a callback during scene:update()
, but not during the drawing. I’ve suggested that as an improvement to Codea, but meanwhile we need to do something.
The closest to a good idea that I have right now is to create a global collection of objects wanting callbacks, and to process the collection with an addition to the main draw
method. Not great but we’ll see.
That collection might someday “grow up” into some kind of world object or something. I certainly won’t do that now, under the YAGNI1 principle.
I think the main thing to do today is to convert my flying capsule into a real class, which I plan to call Bird
in a spirit of optimism. A smaller task is to add some switches to control entities’ active
flag. I turned off the Orc the better to see the bird, and I want to learn how to do that for everyone.
I’ll start there, with the Orc.
Well, that’s disappointing. I started with this:
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
self.entity.active = OrcActive
end
And:
function setup()
parameter.boolean("OrcActive", true)
...
end
That displays the Orc, and when I toggle the parameter’s switch, the Orc disappears. When I toggle it back … he doesn’t come back. It took me a while to apprehend the reason, which is, of course, that setting the entity to inactive means it won’t get called to update any more. So we never see the flag going true.
Now a parameter can include a callback function, which I could presumably rig to turn the Orc back on. But how can I give that function access to the Orc? Another global? I hate that idea. Maybe my collection idea is going to come into play sooner than I thought.
The callback function needs to work like this sketch:
function orcSwitchChanged(aBoolean)
...
orcEntity.active = aBoolean
end
I’ll start with the new global, but I hate it and will hate it more when I do all three of these guys. I could give up this silly feature, but I think it’ll teach us something we’ll want to know. Other parameters could set the speeds of objects and other useful stuff.
OK, that’s not as nice as one would like. Here …
-- Orc
-- RJ 20200202
Orc = class()
function Orc:init(entity)
Orc = self -- define myself as the official Orc
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:activate(aBoolean)
if not self.entity then return end
self.entity.active = aBoolean
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
We define the Orc global, and we add a new method to set the boolean active
flag. I’ll return to that if statement in a moment. In main we have:
function setup()
parameter.boolean("OrcActive", true, orcCallback)
Scene = craft.scene()
setupSky(Scene)
setupOrc(Scene)
local ship = setupWatercraft(Scene)
setupFollower(Scene, ship)
setupCamera(Scene)
end
function orcCallback(aBoolean)
Orc:activate(aBoolean)
end
Here we define the parameter, and give it the callback, which I wrote out longhand rather than embed its definition directly in the call to parameter. The orcCallback refers to the global Orc variable and passes on the boolean.
Back to the activate:
function Orc:activate(aBoolean)
if not self.entity then return end
self.entity.active = aBoolean
end
It turns out (I conclude) that the callback function is called during initialization of the system, meaning that our Orc isn’t defined yet. So the self.entity
returns a nil and generates an error.
I wonder if we could put the parameter last and avoid this problem. I’ll try it.
Yes, that actually works. So I can remove the entity check and setup becomes:
function setup()
Scene = craft.scene()
setupSky(Scene)
setupOrc(Scene)
local ship = setupWatercraft(Scene)
setupFollower(Scene, ship)
setupCamera(Scene)
parameter.boolean("OrcActive", true, orcCallback)
end
Still not ideal but not too bad. Unfortunately, the parameter callback doesn’t pass the name of the parameter to the callback function, so we can’t reaily write just one function to deal with everyone.
I’ll go ahead and do this for the ship as well. I did it exactly the same way, so I”ll spare you the code. It worked as anticipated, with one surprise that shouldn’t have been a surprise.
Because the “bird” is a child of the ship entity, if we inactivate the ship, it automatically inactivates the bird. Makes some sense, though I’ve seen some folks arguing that it shouldn’t work that way. It seems dead certain that they won’t change that behavior.
Anyway, with the ship not moving maybe the bird wouldn’t be that interesting anyway. And if we wanted an independent one, we could make one that interrogated the ship’s position and computed its position in world coordinates instead of local. It’d be much the same.
Bird Class
OK, not much to see here so far. I think I’ll build a Bird class to better encapsulate our bird. (Yes, it looks like a capsule, but no pun intended.)
Here’s the first cut:
function setup()
Scene = craft.scene()
setupSky(Scene)
setupOrc(Scene)
local ship = setupWatercraft(Scene)
setupBird(Scene, ship)
setupCamera(Scene)
parameter.boolean("OrcActive", true, orcCallback)
parameter.boolean("ShipActive", true, shipCallback)
end
function setupBird(scene, shipEntity)
scene:entity():add(Bird, shipEntity)
end
Bird = class()
function Bird:init(entity, target)
Bird = self
self.entity = entity
entity:add(craft.renderer, craft.model("Primitives:Capsule"))
entity.material = craft.material("Materials:Standard")
entity.parent = target
entity.position = vec3(0,10,0)
end
I’ve not put in the orbiting code for update yet, so the “bird” just settles over the ship and follows along:
I’m sort of wondering why there wasn’t an error due to a missing update
method on Bird. Possibly Codea is smart enough not to call it. I’ll test that quickly by adding one that prints. Sure enough, if I add it, it gets called. Let’s make it work.
function Bird:update(dt)
local angle = ElapsedTime
local xAngle = ElapsedTime*1.2
local xOffset = math.sin(xAngle)*4
local yAngle = ElapsedTime*0.4
local yOffset = math.sin(yAngle)*2
local newVec = vec2(5,0):rotate(angle)
self.entity.x = newVec.x + xOffset
self.entity.y = 10+yOffset
self.entity.z = newVec.y
end
That works as advertised:
I think I can just call position
with those three coordinates, let’s see if we like that better. And it works fine:
function Bird:update(dt)
local angle = ElapsedTime
local xAngle = ElapsedTime*1.2
local xOffset = math.sin(xAngle)*4
local yAngle = ElapsedTime*0.4
local yOffset = math.sin(yAngle)*2
local newVec = vec2(5,0):rotate(angle)
self.entity.position = vec3(newVec.x + xOffset, 10+yOffset, newVec.y)
end
Summing Up
Bit of a short morning today, but we got nearly two hours in. As usual, a fraction of it was programming and the rest was writing. (And waiting on the phone rather a lot.)
Adding the Bird object, I believe, made the code better. It moved the motion into the Bird class, and we didn’t have to search the children of the ship to find the Bird. So that was good. I imagine we could add another bird to follow the Orc, but I’m not going to try that this morning: I’m in a mood for some lunch.
The parameters and activation were a bit disappointing, in that we have to suffer a global to have a parameter at all, and then we have to do something special to re-activate the entity.
Here’s a wild idea. What about having a single change callback that calls a general considerActivating
method on a list of all our objects? Then the objects could check the parameter global that they use. A bit weird but it just might work.
But all that’s for another day. I’ll include all the code, for the record. Then commit Working Copy and head to lunch. See you next time!
-- Orc-1
-- RJ 20200203
-- upper case variables are global
-- 20200203: Orc Class
-- 20200207: Bird Class
function setup()
Scene = craft.scene()
setupSky(Scene)
setupOrc(Scene)
local ship = setupWatercraft(Scene)
setupBird(Scene, ship)
setupCamera(Scene)
parameter.boolean("OrcActive", true, orcCallback)
parameter.boolean("ShipActive", true, shipCallback)
end
function orcCallback(aBoolean)
Orc:activate(aBoolean)
end
function shipCallback(aBoolean)
Ship:activate(aBoolean)
end
function setupBird(scene, shipEntity)
scene:entity():add(Bird, shipEntity)
end
function setupWatercraft(scene)
local entity = scene:entity()
entity:add(Watercraft)
return entity
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
-- Orc
-- RJ 20200202
Orc = class()
function Orc:init(entity)
Orc = self -- define myself as the official Orc
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:activate(aBoolean)
-- if not self.entity then return end
self.entity.active = aBoolean
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
-- Watercraft
-- RJ 20200204
Watercraft = class()
function Watercraft:init(entity)
Ship = self
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:activate(aBoolean)
-- if not self.entity then return end
self.entity.active = aBoolean
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
-- Bird
-- RJ 20200207
Bird = class()
function Bird:init(entity, target)
Bird = self
self.entity = entity
entity:add(craft.renderer, craft.model("Primitives:Capsule"))
entity.material = craft.material("Materials:Standard")
entity.parent = target
entity.position = vec3(0,10,0)
end
function Bird:update(dt)
local angle = ElapsedTime
local xAngle = ElapsedTime*1.2
local xOffset = math.sin(xAngle)*4
local yAngle = ElapsedTime*0.4
local yOffset = math.sin(yAngle)*2
local newVec = vec2(5,0):rotate(angle)
self.entity.position = vec3(newVec.x + xOffset, 10+yOffset, newVec.y)
end
-
“You Aren’t Gonna Need It.” On the first XP project, Kent used to respond to people saying “we’re gonna need X so we might as well do it now”, by saying “You aren’t going to need that.” This got abbreviated to YAGNI, and I try always to build things only when I really need them. I do this mostly to find out what happens, and whether I ever get in deep trouble because I didn’t do it earlier. My general finding is that it works fine for me. YMMV. ↩