CoCraTu 3
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:
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:
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:
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