Last time I managed to make a fairly simple Codea Craft program draw an Orc standing on (or near) the floor. Today I plan to make him move a bit, and then to see what I’ve learned.

I’m starting to get a sense of what I’d say if someone were to ask me about Codea Craft, at least referring to the simplest part, that I’ve been playing with here. Not counting the AR and physics parts, about which about all I can say is what’s in this sentence.

Today, I want to add the necessary code to move our Orc from side to side in the picture, similarly to what it was doing in our first big example. The idea today is to duplicate that capability, first in line, and then to refactor toward a design that’s better (by my standards).

Let’s go for super simple. We’ll move the Orc back and forth in small steps, modifying X only, between -2 and +2, the bounds of the floor. You may recall that the floor is 4 on a side. I suspect that to begin with our variables may need to be global. We’ll see.

The Orc code presently looks like this:

    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)

The variable myEntity is our Orc. I think I’ll rename that, to reflect its Orcness and its global character, to MyOrc. I should then be able to update it more or less directly in the update function.

OK, that went surprisingly well, given who we’re dealing with here. In setup, I did the predicted rename, initialized X directly (it was defaulted in the original), and saved a new global to represent the step size. It turns out that 0.02 results in a reasonable speed.

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

Then in update, I just added this:

    local x = MyOrc.x + MyOrcStep
    if math.abs(x) >= 2 then MyOrcStep = -MyOrcStep end
    MyOrc.x = x

And behold! It moves. No real surprise of course. Unless you’re me.

Now it’s cleanup time

What we have here, in my view, is not unlike the typical code we’re shown when someone is writing about how to use an API like Craft. Everything is in line and rather procedural. Arguably this form is a bit easier for beginners to understand, especially if judiciously sprinkled with explanatory comments like -- move the Orc along X.

The problem is, this is terrible code, by my standards, and because it’s the code we are taught, we tend to copy it and bash it until it does whatever new thing we want to accomplish. This is especially true for relatively inexperienced programmers, who may have never seen code that looks any different from this.

My mission is not just to explain Craft, but to show how at least one experienced practitioner would express ideas in the code.

Digression: Beck’s Four Rules

Kent Beck has described “four rules of simple code”. Code is simple when it:

  1. Runs all the tests (works correctly);
  2. Expresses all the programmer’s design ideas;
  3. Contains no duplication;
  4. Minimizes the number of artifacts.

These four notions alone can drive us from a poor design to a better one. We do, of course, need practice in recognizing the issues and improving things.

Note that I have no tests. As I’ve written elsewhere, I do not as yet know good ways to test code like this, whose purpose is to put things directly on the screen, other than by looking at the screen. Maybe as we go forward here, some ways will present themselves.

For now, my concern is primarily with Rule 2. This code does not express all my design ideas. Let’s look at update and talk about the ideas that are not very well expressed in the code.

function update(dt)
    local x = MyOrc.x + MyOrcStep
    if math.abs(x) >= 2 then MyOrcStep = -MyOrcStep end
    MyOrc.x = x
    
    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
    
--    print("update", dt)
    scene:update(dt)
end

This code could be described this way:

  • Move the Orc
  • Get adjusted Camera Angles
  • Set Camera parameters (redundantly?)
  • Set Camera Angles
  • Set camera position
  • Update the scene.

The redundancy in the camera settings is due to the fact that I removed the parameters that appeared in the Learn Craft example, and just initialized various values to the defaults of those parameters. In the original, those three CameraSettings references included variables that could change. Now they can’t.

Now I will argue that that code should be moved to the setup. Some would say, well, maybe in the future we’ll want to make those items parameters, and we’ll just have to move them back. My practice is to make the code as clean and modular now as I can, with the experienced expectation that that sets me up better for future change than leaving things lying around where they don’t really belong.

Your mileage may vary, of course, but this is what I’ve learned to do, and it works well for me.

So I want to move those camera bits into setup and work from there. I move them to the end of setup(), where everything still works fine. Now we have this in update:

function update(dt)
    local x = MyOrc.x + MyOrcStep
    if math.abs(x) >= 2 then MyOrcStep = -MyOrcStep end
    MyOrc.x = x
    
    if CurrentTouch.state == MOVING then 
        CameraX = CameraX - CurrentTouch.deltaX * 0.25
        CameraY = CameraY - CurrentTouch.deltaY * 0.25
    end
    
    --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
    
--    print("update", dt)
    scene:update(dt)
end

Now, conveniently, all the camera stuff is isolated, as is the Orc moving. These, I would extract as functions. Let’s do the Orc first:

Update becomes:

function update(dt)
    updateOrc(dt)
    
    if CurrentTouch.state == MOVING then 
        CameraX = CameraX - CurrentTouch.deltaX * 0.25
        CameraY = CameraY - CurrentTouch.deltaY * 0.25
    end
    
    --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
    
--    print("update", dt)
    scene:update(dt)
end

With the added function:

function updateOrc(dt)
    local x = MyOrc.x + MyOrcStep
    if math.abs(x) >= 2 then MyOrcStep = -MyOrcStep end
    MyOrc.x = x
end

Note that the update function now says what it’s going to do: update the Orc, and the updateOrc function says how it’s going to be done. By my lights, that’s better.

Now I’ll do the same with the Camera stuff. I should mention that I have questions about how that works, but the process of moving it should be quite mechanical, requiring no deep knowledge:

function update(dt)
    updateOrc(dt)
    updateCamera(dt)
    scene:update(dt)
end

function updateCamera(dt)
    if CurrentTouch.state == MOVING then 
        CameraX = CameraX - CurrentTouch.deltaX * 0.25
        CameraY = CameraY - CurrentTouch.deltaY * 0.25
    end
    --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

end

Now, the update function says exactly what it does: update the orc, update the camera, update the scene. If we care how those things are done, we can read the code for those functions. If we just care about what’s going on, we can read just the top level.

Now, we’re left with a question.

What’s up with the camera?

We “know” that there is a camera, and that it has a position that it’s at, and that it has some direction it points in. We know that from reading the documentation. What confuses me is how that code gets the effect we see, of the camera rotating around the scene (or the scene rotating within the camera’s view. Look at this movie again:

Our view rotates around the Orc and moves up and down a bit, while apparently staying at the same distance away and looking right at the center of the floor. Clearly the code that does this is:

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

That’s all there is. I would expect that setting the angles on the camera would rotate it. I think I’ll check that by commenting out the position part.

Sure enough, now when I drag on the screen, the picture moves off screen, as if the camera is looking away.

So what about that code:

    scene.camera.position = -scene.camera.forward * 5

(Yes, I’m about to read the documentation, but I often prefer to figure things out). I’m guessing that the 5 is the distance of the camera away from the center, because it seems like we’re about 5 away from the middle of the floor. So making that larger would move the camera back. A quick check verifies that for me.

camera.position is going to be a 3-vector, certainly, and so, very likely, camera.forward is also a vector. What is it likely to be?

Based on years of experience, I suspect camera.forward is a unit vector pointing in front of the camera, however it’s currently rotated. If that’s the case, then the negative of that, times five, is a point 5 meters behind where the camera is now.

This is all speculation. Earlier you were telling me to read the documentation. You’ll be happy to hear that neither camera.position nor camera.forward are documented at all. We’re on our own unless we can find the source code somewhere.

My next step is going to be to display some values to see what happens. I think I’d like to display camera position and camera forward.

Hm, something just occurred to me. That update code runs every time. So even if we’re not moving our finger, the camera angles will be set, and then position will be set. Yet the camera doesn’t move. Interesting. Anyway I printed some values. Doing so didn’t help me much.

Most confusing. More study required. I’ll do that and report back.

Camera Explained

After my lunch break I mentioned to a friend in another world that I didn’t understand how the camera worked. No sooner had I said that than I did realize how it works. Here’s a picture I just drew:

In the left frame, the camera is 5 back from the floor center, looking at the wide front of the orc. The view is shown below, some details left to the reader.

Then we move our finger on the screen. As we can “readily see” in this code, that rotates the camera to the right.

    if CurrentTouch.state == MOVING then 
        CameraX = CameraX - CurrentTouch.deltaX * 0.25
        CameraY = CameraY - CurrentTouch.deltaY * 0.25
    end
    --Set the camera rotation and position to look at the center of the scene
    scene.camera.eulerAngles = vec3(CameraY, CameraX, 0)

With the camera rotated exactly 90 degrees right, for convenience, we see that camera.forward will be (1,0). So this code …

    scene.camera.position = -scene.camera.forward * 5

… calculates (-5,0) and moves the camera to that position. That leaves it pointing at the side of the orc, which is what we expect.

This code assumes that the nominal target of the camera is at the zero point. So, in essence, the move really does two things, move to zero and then back off by -5. Since camera.forward is really a vector offset, not a position, our (-5,0) is also an offset. If we calculated target + offset, where target is a position, we’d wind up with a new position. Strictly speaking, we’ve swapped in a new datatype by just moving to the (-5,0) but to Codea a vector is a vector and it “just works”.

Simple trick. A bit obscure, and as written only works if you are essentially aiming at the zero point of your scene. But it does work, and now I understand it. I hope this explanation has worked for you.

If it doesn’t, and you have a better one, let me know and I’ll update this article.

Critiquing our code …

Let’s take a look at this code and see what improvement it might need. Here’s the whole program:

-- Orc-1
-- RJ 20200130

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

function update(dt)
    updateOrc(dt)
    updateCamera(dt)
    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

function updateOrc(dt)
    local x = MyOrc.x + MyOrcStep
    if math.abs(x) >= 2 then MyOrcStep = -MyOrcStep end
    MyOrc.x = x
end

function updateCamera(dt)
    if CurrentTouch.state == MOVING then 
        CameraX = CameraX - CurrentTouch.deltaX * 0.25
        CameraY = CameraY - CurrentTouch.deltaY * 0.25
    end
    --Set the camera rotation and position to look at the center of the scene
    print("pos-in", scene.camera.position)
    scene.camera.eulerAngles = vec3(CameraY, CameraX, 0)
    print("angles", scene.camera.eulerAngles, "forward", scene.camera.forward)
    scene.camera.position = -scene.camera.forward * 5
    print("pos-out", scene.camera.position)
end

Well, I hate that the comments are folding on my screen. If they fold on yours, I apologize. But “comments are the code’s way of asking to be made more clear”, so with luck they’ll go away soon,

LOL, here’s a quick fix in waiting:

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

    -- Draw the scene
    scene:draw()	
end

Love the comment before scene:draw, in case you didn’t know what that meant. I’ll remove that, and the blank lines. I’m inclined to leave the “called automatically” comment, not because I need it, but because some new folks might appreciate the reminder. So:

-- Called automatically by codea 
function draw()
    update(DeltaTime)
    scene:draw()	
end

Ship it, it’s better.

Seriously, many folks would check into trunk with this change. Trunk-based development is great, and works best if we check in frequently. Topic for another day, perhaps, but consider it done.

Truth is, I’ve not put this code under version control, because despite that it’ll fill up four or five articles, it’s essentially a trivial throw-away. I’ll surely regret not putting it under version control anyway, and when that happens I’ll be sure to tell you.

What’s next? Well, obviously, it’s that setup:

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

We need to give setup the same treatment we gave update, using the Composed Method pattern (Composed Function in this case, actually), by extracting some functions:

function setup()
    scene = craft.scene()
    setupSky(scene)
    setupOrc(scene)  
    setupCam(scene)
end

function setupSky(scene)
    scene.sky.active = false
    createGround(-1.125)
end

function setupOrc(scene)
    MyOrc = scene:entity()
    MyOrc.model = craft.model("Blocky Characters:Orc")
    MyOrc.x = 0
    MyOrcStep = 0.02
    MyOrc.y = -1
    MyOrc.z = 0
    MyOrc.scale = vec3(1,1,1) / 8
    MyOrc.eulerAngles = vec3(0, 180, 0)
    --MyOrc:add(RonMover)
end

function setupCam(scene)
    scene.camera.z = -4
    CameraSettings = scene.camera:get(craft.camera)
    CameraX = 0
    CameraY = 0
    FieldOfView = 60
    Ortho = false
    OrthoSize = 5
    CameraSettings.fieldOfView = FieldOfView
    CameraSettings.ortho = Ortho
    CameraSettings.orthoSize = OrthoSize
end

Here again, I prefer this arrangement. I can look at setup and see that it creates a scene and then sets up three items, ground, orc, and camera. (I should rename that setupCamera. Doing it right now.) Then if I want to see how the camera is set up, I can read that bit. And if I don’t (and I really don’t), I can just trust that that’s what goes on in setupCamera.

It appears to me that CameraSettings doesn’t need to be global (though CameraX and CameraY may need to be, as they are referred to in both setup and update). I’ll try making that local, and if that works demote it to lower case, as I’m using upper case for globals (mostly).

function setupCamera(scene)
    scene.camera.z = -4
    local cameraSettings = scene.camera:get(craft.camera)
    CameraX = 0
    CameraY = 0
    local fieldOfView = 60
    local ortho = false
    local orthoSize = 5
    cameraSettings.fieldOfView = fieldOfView
    cameraSettings.ortho = ortho
    cameraSettings.orthoSize = orthoSize
end

I also made fieldOfView, ortho, and orthoSize local. They were global because I copied and pasted from the Learn Craft example, which had parameters looking at them.

This is why copy/paste is so dangerous: this code injected four global variables into our tiny little application, which increases the code complexity by a disproportionate amount, since we have to wonder whenever we see a global, who else is looking at it.

Relatedly, note that I pass the scene to all the setup functions. I prefer that to letting them all fiddle with globals. I should make that practice part of my coding standard.

There’s no way around making the scene a global (at least not right now) but I can at least limit access to it. Let’s do that, and name it upper-case Scene as well, which should help me catch any references to the global.

That went … almost well

I did the scene conversion all in one go. Renaming the scene global to Scene was a good idea, as it meant that all the references I missed were flagged at run time. However, if this program were more complex, so that some objects or actions only happened part of the time, a missed scene reference might not have shown up so readily. Lua defaults variable names to global and initializes them to nil. That’s not always ideal.

In any event, in a few minutes I got down to this version, which now has only a few limited occurrences of the Scene global.

 -- Orc-1
-- RJ 20200130

function setup()
    Scene = craft.scene()
    setupSky(Scene)
    setupOrc(Scene)  
    setupCamera(Scene)
end

function setupSky(scene)
    scene.sky.active = false
    createGround(-1.125, scene)
end

function setupOrc(scene)
    MyOrc = scene:entity()
    MyOrc.model = craft.model("Blocky Characters:Orc")
    MyOrc.x = 0
    MyOrcStep = 0.02
    MyOrc.y = -1
    MyOrc.z = 0
    MyOrc.scale = vec3(1,1,1) / 8
    MyOrc.eulerAngles = vec3(0, 180, 0)
    --MyOrc:add(RonMover)
end

function setupCamera(scene)
    Scene.camera.z = -4
    local cameraSettings = scene.camera:get(craft.camera)
    CameraX = 0
    CameraY = 0
    local fieldOfView = 60
    local ortho = false
    local orthoSize = 5
    cameraSettings.fieldOfView = fieldOfView
    cameraSettings.ortho = ortho
    cameraSettings.orthoSize = orthoSize
end


function update(dt, scene)
    updateOrc(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 updateOrc(dt)
    local x = MyOrc.x + MyOrcStep
    if math.abs(x) >= 2 then MyOrcStep = -MyOrcStep end
    MyOrc.x = x
end

function updateCamera(dt, scene)
    if CurrentTouch.state == MOVING then 
        CameraX = CameraX - CurrentTouch.deltaX * 0.25
        CameraY = CameraY - CurrentTouch.deltaY * 0.25
    end
    --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
end

“One more thing …” – Columbo1

The variables CameraX and CameraY need to be global, I think, but I don’t need to init them to zero in setup if I do this:

function updateCamera(dt, scene)
    CameraX = CameraX or 0
    CameraY = CameraY or 0
    if CurrentTouch.state == MOVING then 
        CameraX = CameraX - CurrentTouch.deltaX * 0.25
        CameraY = CameraY - CurrentTouch.deltaY * 0.25
    end
    --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
end

That’s a Lua idiom for “if CameraX isn’t nil, use it, otherwise use zero. So at a little cost in update, I can limit the references to those two globals to this one function. I’m willing to pay that price. YMMV of course.

I wonder if we can simplify further, by not updating the camera outside the if, and moving the or bits inside. Only one way to be sure: I’ll try it.

Oh yeah, that’s better:

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

If we had a camera object, we could keep those variables as member variables, but for now this localizes the global references to this one small method. One does have to know that Lua idiom, but we’re supposed to know our programming language, so that’s OK in my book.

Summing Up

I think this is about as clean as we can make a basically procedural solution. Next time, I’ll see about moving the Orc’s motion into an object. I see two steps to doing that but I could be wrong. We’ll see.

I kind of suspect that maybe three people in the whole world are actually interested in this. If so, it’s not worth the effort writing these articles: it takes longer to write and edit them than it does to write the code, by a factor of probably five. So if you are enjoying these and find them useful, please tweet me or DM me or drop me an email. Thanks!

Here’s the whole program:

-- Orc-1
-- RJ 20200201
-- upper case variables are global

function setup()
    Scene = craft.scene()
    setupSky(Scene)
    setupOrc(Scene)  
    setupCamera(Scene)
end

function setupSky(scene)
    scene.sky.active = false
    createGround(-1.125, scene)
end

function setupOrc(scene)
    MyOrc = scene:entity()
    MyOrc.model = craft.model("Blocky Characters:Orc")
    MyOrc.x = 0
    MyOrcStep = 0.02
    MyOrc.y = -1
    MyOrc.z = 0
    MyOrc.scale = vec3(1,1,1) / 8
    MyOrc.eulerAngles = vec3(0, 180, 0)
    --MyOrc:add(RonMover)
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)
    updateOrc(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 updateOrc(dt)
    local x = MyOrc.x + MyOrcStep
    if math.abs(x) >= 2 then MyOrcStep = -MyOrcStep end
    MyOrc.x = x
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

  1. Jeffries shows his age.