Yesterday I managed to move an Orc, by hacking a sample program. Today, let’s try to do a clean version to consolidate our learning and ask ourselves some questions. Maybe even begin to answer some ..

Yesterday’s Orc mover was a small class patched into a large Codea Craft example called Learning Craft. That sample contains five or six tabs of code, almost all of which is irrelevant to our wandering Orc. My purpose here is to learn, and to represent my learning in examples that, to me, represent good coding practices. I freely grant that my idea of good and yours may be different. If yours are quite different, I’d love to see your equivalent examples.

There is one part of the example that I plan to re-use in toto1. That is the “OrbitViewer”, a very nice camera control written by John Millard, who has contributed many of the Codea Craft examples. At some future time, if I continue digging into Codea and Craft, I’ll want to learn about cameras. Just now, I’m content to use this library one as I study other areas.

Our Basic Plan

What I hope to do today is to build a minimal stand-alone Codea Craft program that displays and moves our Orc. Along the way, I expect that I’ll tart up what the program does, by way of learning things that come to mind, but the main thing will be to have a small program that’s organized “my way”.

I expect that to take more than one pass. I expect that we’ll begin with a fairly procedural version, then clean it up into something resembling what I’d hope to do with a larger well-organized system.

For the small example,this may seem like overkill. However, I believe that our small starting examples in fact should be kept very clean. The more we build on something messy, the more messy we make it. So I find two kinds of value in building small clean things:

  1. I learn what clean code looks like. This helps me improve the code when it’s getting complicated.
  2. Mess seems to accumulate. Clean doesn’t quite accumulate in the same way, but if our existing code is clean, the messy bits stand out and there’s at least some incentive to keep clean areas clean.
  3. (I lied about the two.) I frankly enjoy taking small steps each of which makes the program a bit nicer.

I’ll proceed by reading the existing example and extracting what I hope will be a small working subset. At this point I will be copying ideas from existing code, and often copying bits of actual code.

Exemplum docet, they say, and I agree.

I’ll mention where I’m looking, and occasionally paste bits that tell me something (or confuse me), but for a full look at the existing code, please get a copy of Codea and browse on your own.

The Story

The story we’re working on is:

Write a small clear program that displays a floor and an Orc, with the Orc moving about on the floor. Use OrbitViewer as is to observe the program running.

First Try

After a couple of quick copy-pastes, I’ve got this:

-- Orc-1
-- RJ 20200130

function setup()
    -- Create a new craft scene
    scene = craft.scene()
    scene.sky.active = false
    createGround(-1.125)
  
    myEntity = scene:entity()
    myEntity.model = craft.model("Blocky Characters:Orc")
    myEntity.y = -1
    myEntity.z = 0
    myEntity.scale = vec3(1,1,1) / 8
    myEntity.eulerAngles = vec3(0, 180, 0)
    --myEntity:add(RonMover)
    
    scene.camera.z = -4
end

function update(dt)
    -- Update the scene (physics, transforms etc)
    scene:update(dt)
end

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

    -- Draw the scene
    scene:draw()	
end

-- Creates the ground using a box model and applies a simple textured material
function createGround(y)
    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

Note that I commented out the add of RonMover. My purpose was to get this hummer working as quickly and simply as possible. To “get to green” as soon as I can. Putting in moving would delay my gratification and increase my chance of making a hard-to-find mistake.

This program displays the Orc, on the “ground”:

The OrbitViewer camera is not set up. I’ve just got a vanilla static camera: I can’t spin and zoom and all the OrbitCamera stuff. Back to the Learn Craft example to see how to get that going.

OrthoCam

The update function in the Orc’s part of Learning Craft looks like this:

function update(dt)
    if CurrentTouch.state == MOVING then 
        CameraX = CameraX - CurrentTouch.deltaX * 0.25
        CameraY = CameraY - CurrentTouch.deltaY * 0.25
    end
    
    cameraSettings.fieldOfView = FieldOfView
    -- Orthographic mode
    cameraSettings.ortho = Ortho
    cameraSettings.orthoSize = OrthoSize
    
    --Set the camera rotation to look at the center of the scene
    scene.camera.eulerAngles = vec3(CameraY, CameraX, 0)
    scene.camera.position = -scene.camera.forward * 5
    
    -- Update the scene (physics, transforms etc)
    scene:update(dt)
end

There are also some parameter definitions that this code refers to, in the setup():

    parameter.number("CameraX", 0, 360, 0)   
    parameter.number("CameraY", 0, 360, 0)   
    parameter.number("FieldOfView", 45, 90, 60)
    parameter.boolean("Ortho", false)
    parameter.number("OrthoSize", 1,10,5)

I don’t really think I need these parameters, but they’re kind of interesting. Let’s paste them back into our setup and add the above code to our update.

The camera began to work after I added the following line, which I had noticed but forgot to include in setup.

    cameraSettings = scene.camera:get(craft.camera)

That line sets up a global that’s referenced in update. I hate the it’s a global with a lower-case name, so I’ll fix that and then show you what we have:

 -- Orc-1
-- RJ 20200130

function setup()
    -- Create a new craft scene
    scene = craft.scene()
    scene.sky.active = false
    createGround(-1.125)
  
    myEntity = scene:entity()
    myEntity.model = craft.model("Blocky Characters:Orc")
    myEntity.y = -1
    myEntity.z = 0
    myEntity.scale = vec3(1,1,1) / 8
    myEntity.eulerAngles = vec3(0, 180, 0)
    --myEntity:add(RonMover)
    
    scene.camera.z = -4
    CameraSettings = scene.camera:get(craft.camera)
    
    parameter.number("CameraX", 0, 360, 0)   
    parameter.number("CameraY", 0, 360, 0)   
    parameter.number("FieldOfView", 45, 90, 60)
    parameter.boolean("Ortho", false)
    parameter.number("OrthoSize", 1,10,5)
end

function update(dt)
    if CurrentTouch.state == MOVING then 
        CameraX = CameraX - CurrentTouch.deltaX * 0.25
        CameraY = CameraY - CurrentTouch.deltaY * 0.25
    end
    
    CameraSettings.fieldOfView = FieldOfView
    -- Orthographic mode
    CameraSettings.ortho = Ortho
    CameraSettings.orthoSize = OrthoSize
    
    --Set the camera rotation to look at the center of the scene
    scene.camera.eulerAngles = vec3(CameraY, CameraX, 0)
    scene.camera.position = -scene.camera.forward * 5
    
    -- Update the scene (physics, transforms etc)
    scene:update(dt)
end

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

    -- Draw the scene
    scene:draw()	
end

-- Creates the ground using a box model and applies a simple textured material
function createGround(y)
    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

Camera Confusion

I thought that I’d next review the OrbitViewer class from the Cameras example. In fiddling with that, I realized that this code doesn’t use the orbit viewer at all. Everything in our code is using nothing but the regular camera plus that touch code that adjusts the angles when you touch the screen:

    if CurrentTouch.state == MOVING then 
        CameraX = CameraX - CurrentTouch.deltaX * 0.25
        CameraY = CameraY - CurrentTouch.deltaY * 0.25
    end
-- ...
    scene.camera.eulerAngles = vec3(CameraY, CameraX, 0)

That’s good. It means I can remove my dependency on OrbitViewer for now, resulting in a simpler starting example.

I am curious about one thing, which is why X and Y are reversed in the setting of camera.eulerAngles. Ah. I see. When I drag my finger from side to side, the picture rotates to track it. Up and down, same thing. However, for a rotation in the plane of the floor, we have to rotate around Y, and to tilt forward and back, we have to rotate around X. (Remember, in Codea 3D, X is across and Y is up (and Z is depth). So that little mystery becomes clear.

I’d like to have the code become clear enough so that the next reader (me, tomorrow, probably) isn’t confused. We’ll think about that.

For now, I’m going to remove the dependency on Cameras, and take out those parameters. We may put them in later but we’ll be doing it to our own purpose, not that of whoever wrote the example.

Here’s the current version:

-- Orc-1
-- RJ 20200130

function setup()
    scene = craft.scene()
    scene.sky.active = false
    createGround(-1.125)
  
    myEntity = scene:entity()
    myEntity.model = craft.model("Blocky Characters:Orc")
    myEntity.y = -1
    myEntity.z = 0
    myEntity.scale = vec3(1,1,1) / 8
    myEntity.eulerAngles = vec3(0, 180, 0)
    --myEntity:add(RonMover)
    
    scene.camera.z = -4
    CameraSettings = scene.camera:get(craft.camera)
    CameraX = 0
    CameraY = 0
    FieldOfView = 60
    Ortho = false
    OrthoSize = 5
end

function update(dt)
    if CurrentTouch.state == MOVING then 
        CameraX = CameraX - CurrentTouch.deltaX * 0.25
        CameraY = CameraY - CurrentTouch.deltaY * 0.25
    end
    
    CameraSettings.fieldOfView = FieldOfView
    CameraSettings.ortho = Ortho
    CameraSettings.orthoSize = OrthoSize
    
    --Set the camera rotation and position to look at the center of the scene
    scene.camera.eulerAngles = vec3(CameraY, CameraX, 0)
    scene.camera.position = -scene.camera.forward * 5
    
    scene:update(dt)
end

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

    -- Draw the scene
    scene:draw()	
end

-- Creates the ground using a box model and applies a simple textured material
function createGround(y)
    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

I’ve removed most of the comments, which I didn’t find helpful. Commenting the creation of the scene with “create the scene” didn’t do much for me. I left in the commands around camera euler angles and position because I don’t feel that I understand that code yet and the comment reminds me to make it more clear2.

Closing for the day

We’ve reached a fairly simple single-tab program that displays an orc and lets us cam around a bit. In the interest of a timely lunch, I’m calling this done and shipping it.

Looking to the next steps, I might package up parts of this, similar to how createGround is packaged. Then I’ll make the Orc move, first in a very simple and terrible way, then refactor it a time or two to see what makes a better design.

My tentative plan is to put the Orc’s positioning right into the current update function, then “discover” that it needs to be broken out. Maybe break it out as a function, then note that if we add a component to the entity we can have it automatically called during update, and see where that thinking leads us.

For now … I’m off to lunch!


  1. Not to be confused with Dorothy’s little dog Toto.

  2. Kent Beck taught us that a comment is the code’s way of asking to be made more clear. When we need a comment, we try to improve the code until we don’t need the comment any more. That’s not always possible but you’d perhaps be surprised how often it is possible and how often it makes the code better.