A couple of new entities lead to a design concern.

Begin today by making a copy of your CoCraTu-002 program. I cleverly named mine CoCraTu-003.

Our plan is to create a new entity, called Wanderer, who circles inside the Adventurer’s path, going the other direction at a different speed. We change setup and update:

function setup()
    scene = craft.scene()
    
    Adventurer = createAdventurer()
    Wanderer = createWanderer()
    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
    Adventurer.x = 2*math.sin(math.rad(angle))
    Adventurer.z = 2*math.cos(math.rad(angle))
    Adventurer.eulerAngles = vec3(0, angle + 90, 0)
    local wRadians = math.rad(-angle/2)
    Wanderer.x = math.sin(wRadians)
    Wanderer.z = math.cos(wRadians)
    scene:update(dt)
end

The above is pretty rote. The main difference is that I converted the angle to radians right rather than call math.rad so many times. We’ll deal with the asymmetry with Adventurer as we continue. I didn’t set Wanderer’s rotation, because I don’t know what it needs to be.

You’ll notice that I’ve not even written createWanderer. I don’t know the details, but I know that I want it. To write it, I of course copy Adventurer’s creation:

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 get 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

That gives us a little square knight going around the other way, always facing us. We adjust his angle:

function update(dt)
    local degreesPerSecond = 360/5
    angle = angle + dt*degreesPerSecond
    Adventurer.x = 2*math.sin(math.rad(angle))
    Adventurer.z = 2*math.cos(math.rad(angle))
    Adventurer.eulerAngles = vec3(0, angle + 90, 0)
    local wRadians = math.rad(-angle/2)
    Wanderer.x = math.sin(wRadians)
    Wanderer.z = math.cos(wRadians)
    Wanderer.eulerAngles = vec3(0, 90-angle/2, 0)
    scene:update(dt)
end

That gives us what we wanted:

two movers

I am irritated by the fact that my clever trick to save calls to math rad forced me to compute - angle/2 again. We’ll improve that later. For now, we have two movers, moving as intended.

Child Entity

An entity can have other entities as their parent. The position and rotation properties of the child are relative to the parent. We’ll give our adventurer a daydream.

function setup()
    scene = craft.scene()
    
    Adventurer = createAdventurer()
    Wanderer = createWanderer()
    Daydream = createDaydream(Adventurer)
    createFloor()
    scene.camera:add(OrbitViewer, vec3(0,0,0), 5, 10, 20)
    angle = 0
end

function createDaydream(parent)
    local 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
    return daydream
end

That gives our Adventurer a dream of a boat a bit above his head:

boat above head

Note the 8*2.25 and 8*0.5 above. Because our Adventurer is scaled down to 1/8, dimensions relative to him are only 1/8 of their “real” values. So to move the boat up above his head, we had to set it, not 2.25 units up, but eight times that.

I think the boat should move a bit as he walks, rather than just stay top left.

function update(dt)
    local degreesPerSecond = 360/5
    angle = angle + dt*degreesPerSecond
    Adventurer.x = 2*math.sin(math.rad(angle))
    Adventurer.z = 2*math.cos(math.rad(angle))
    Adventurer.eulerAngles = vec3(0, angle + 90, 0)
    local wRadians = math.rad(-angle/2)
    Wanderer.x = math.sin(wRadians)
    Wanderer.z = math.cos(wRadians)
    Wanderer.eulerAngles = vec3(0, 90-angle/2, 0)
    Daydream.x = 2*math.sin(math.rad(angle))
    scene:update(dt)
end

We adjust the daydream’s x, relative to the adventurer, between -2 and 2 units, also relative to the adventurer. Now the boat moves back and forth above his head:

back-forth

Done … or Are We?

We’ve accomplished today’s goal, to add a couple of moving objects to our scene, including one that behaves as if it is attached to the adventurer, and one that moves independently.

It should be clear that with some difficulty, we could create a model out of cubes (and spheres, and even other models) and move its parts around. We could, in principle, attach an arm to a robot, and calculate its angles to move it around. It should also be clear that that would be tedious, though not seriously challenging.

We’re not going to go there, at least not soon. To do a real job of creating creatures and animating them is beyond our scope.

Within our scope, however, is to consider the design of this program. Frankly, it’s getting messy and we just have a floor and three moving objects.

function setup()
    scene = craft.scene()
    
    Adventurer = createAdventurer()
    Wanderer = createWanderer()
    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
    Adventurer.x = 2*math.sin(math.rad(angle))
    Adventurer.z = 2*math.cos(math.rad(angle))
    Adventurer.eulerAngles = vec3(0, angle + 90, 0)
    local wRadians = math.rad(-angle/2)
    Wanderer.x = math.sin(wRadians)
    Wanderer.z = math.cos(wRadians)
    Wanderer.eulerAngles = vec3(0, 90-angle/2, 0)
    Daydream.x = 2*math.sin(math.rad(angle))
    scene:update(dt)
end

-- Called automatically by codea 
function draw()
    update(DeltaTime)

    -- Draw the scene
    scene:draw()	
end

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

function createDaydream(parent)
    local 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
    return daydream
end

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

function createFloor()
    local 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)
    return floor
end

We see lots of duplication in the code above, and duplication almost always represents an opportunity for better code. We also see a growing blob of code, and it seems that it would grow more and more as we add entities and behavior:

function update(dt)
    local degreesPerSecond = 360/5
    angle = angle + dt*degreesPerSecond
    Adventurer.x = 2*math.sin(math.rad(angle))
    Adventurer.z = 2*math.cos(math.rad(angle))
    Adventurer.eulerAngles = vec3(0, angle + 90, 0)
    local wRadians = math.rad(-angle/2)
    Wanderer.x = math.sin(wRadians)
    Wanderer.z = math.cos(wRadians)
    Wanderer.eulerAngles = vec3(0, 90-angle/2, 0)
    Daydream.x = 2*math.sin(math.rad(angle))
    scene:update(dt)
end

If you’re like me, you might resort to some spacing to make that more clear:

function update(dt)
    local degreesPerSecond = 360/5
    angle = angle + dt*degreesPerSecond
    
    Adventurer.x = 2*math.sin(math.rad(angle))
    Adventurer.z = 2*math.cos(math.rad(angle))
    Adventurer.eulerAngles = vec3(0, angle + 90, 0)
    
    local wRadians = math.rad(-angle/2)
    Wanderer.x = math.sin(wRadians)
    Wanderer.z = math.cos(wRadians)
    Wanderer.eulerAngles = vec3(0, 90-angle/2, 0)
    
    Daydream.x = 2*math.sin(math.rad(angle))
    
    scene:update(dt)
end

You might notice that the angle handling is done two different ways and decide to do it in just one way, to avoid confusion later:

function update(dt)
    local degreesPerSecond = 360/5
    angle = angle + dt*degreesPerSecond
    
    Adventurer.x = 2*math.sin(math.rad(angle))
    Adventurer.z = 2*math.cos(math.rad(angle))
    Adventurer.eulerAngles = vec3(0, angle + 90, 0)
    
    local wAngle = -angle/2
    Wanderer.x = math.sin(math.rad(wAngle))
    Wanderer.z = math.cos(math.rad(wAngle))
    Wanderer.eulerAngles = vec3(0, wAngle, 0)
    
    Daydream.x = 2*math.sin(math.rad(angle))
    
    scene:update(dt)
end

We can see now that we have three functions here, waiting to be born:

function update(dt)
    local degreesPerSecond = 360/5
    angle = angle + dt*degreesPerSecond
    
    updateAdventurer(angle)
    updateWanderer(-angle/2)
    updateDaydream(angle)
    
    scene:update(dt)
end

function updateAdventurer(angle)
    Adventurer.x = 2*math.sin(math.rad(angle))
    Adventurer.z = 2*math.cos(math.rad(angle))
    Adventurer.eulerAngles = vec3(0, angle + 90, 0)
end

function updateDaydream(angle)
    Daydream.x = 2*math.sin(math.rad(angle))
end

function updateWanderer(angle)
    Wanderer.x = math.sin(math.rad(angle))
    Wanderer.z = math.cos(math.rad(angle))
    Wanderer.eulerAngles = vec3(0, 90+angle, 0)
end

Note that I had to change the eulerAngle for the wanderer, since I negated the angle in the call.

These functions refer to the globals Adventurer, Daydream, and Wanderer. It would be better if they didn’t. We can pass them in as arguments:

    updateAdventurer(Adventurer, angle)
    updateWanderer(Wanderer, -angle/2)
    updateDaydream(Daydream, angle)

function updateAdventurer(adventurer, angle)
    adventurer.x = 2*math.sin(math.rad(angle))
    adventurer.z = 2*math.cos(math.rad(angle))
    adventurer.eulerAngles = vec3(0, angle + 90, 0)
end

function updateDaydream(daydream, angle)
    daydream.x = 2*math.sin(math.rad(angle))
end

function updateWanderer(wanderer, angle)
    wanderer.x = math.sin(math.rad(angle))
    wanderer.z = math.cos(math.rad(angle))
    wanderer.eulerAngles = vec3(0, 90+angle, 0)
end

The updateAdventurer and updateWanderer are almost identical. We could make them identical if we were to pass in the radius to each.

function update(dt)
    local degreesPerSecond = 360/5
    angle = angle + dt*degreesPerSecond
    
    updateAdventurer(Adventurer, 2, angle)
    updateWanderer(Wanderer, 1, -angle/2)
    updateDaydream(Daydream, angle)
    
    scene:update(dt)
end

function updateAdventurer(adventurer, radius, angle)
    adventurer.x = radius*math.sin(math.rad(angle))
    adventurer.z = radius*math.cos(math.rad(angle))
    adventurer.eulerAngles = vec3(0, 90 + angle, 0)
end

function updateWanderer(wanderer, radius, angle)
    wanderer.x = radius*math.sin(math.rad(angle))
    wanderer.z = radius*math.cos(math.rad(angle))
    wanderer.eulerAngles = vec3(0, 90 + angle, 0)
end

Now, since they’re the same, we can remove one of those updates and rename the other:

    updateCircleMover(Adventurer, 2, angle)
    updateCircleMover(Wanderer, 1, -angle/2)
    updateDaydream(Daydream, angle)

function updateCircleMover(entity, radius, angle)
    entity.x = radius*math.sin(math.rad(angle))
    entity.z = radius*math.cos(math.rad(angle))
    entity.eulerAngles = vec3(0, 90 + angle, 0)
end

With a few simple transformations, we’ve eliminated some code, improved clarity, and even provided a potentially useful new function that moves things in circles. It’s already used twice, so it’s a righteous change.

There’s more that we could do, and in a future tutorial we probably will. The creation functions look rather similar as well. There might be some value to be found there.

But this time we’ve added two new objects while adding only a bit of code. We’ve made the program more clear than it was, and therefore better able to be modified to have more capability.

Here, for reference, is the whole program.


-- CoCraTu-003

function setup()
    scene = craft.scene()
    
    Adventurer = createAdventurer()
    Wanderer = createWanderer()
    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)
    updateCircleMover(Wanderer, 1, -angle/2)
    updateDaydream(Daydream, angle)
    
    scene:update(dt)
end

-- Called automatically by codea 
function draw()
    update(DeltaTime)

    -- Draw the scene
    scene:draw()	
end

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

function updateCircleMover(entity, radius, angle)
    entity.x = radius*math.sin(math.rad(angle))
    entity.z = radius*math.cos(math.rad(angle))
    entity.eulerAngles = vec3(0, 90 + angle, 0)
end

function createDaydream(parent)
    local 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
    return daydream
end

function updateDaydream(daydream, angle)
    daydream.x = 2*math.sin(math.rad(angle))
end

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

function createFloor()
    local 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)
    return floor
end