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
  1. “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.