Codea Craft 12 - A place to stand.
I think I nearly have a solid place to stand: a tiny application that I understand pretty well. Today well clean it up and see what’s next.
Yesterday I got to a fairly decent point, with pretty good understanding of all the Codea Craft features Im using. Today we need to do some finish work. Then we’ll look to what’s next.
The present little program draws a floor (with a gap in it because I wanted to se what was underneath the grass[^grass]) and includes a camera class that lets me orbit around and view things. It doesn’t support zooming in, just rotations.
The touch movement is still not integrated into my GameCamera class, so that’ll be the first order of business. I’ll remove the parameter switches that I was using to move the camera at this time as well. Here goes …
First I did this:
-- Game-1
-- RJ 20200222 initial plan, make a floor
-- RJ 20200226 camera object works w/o touch
-- RJ 20200227 cleanup
function setup()
scene = craft.scene()
-- Setup camera and lighting
scene.sun.rotation = quat.eulerAngles(25, 125, 0)
-- Set the scenes ambient lighting
scene.ambientColor = color(127, 127, 127, 255)
allBlocks = blocks()
-- Setup voxel terrain
local m = 1 -- 5
scene.voxels:resize(vec3(5,1,5))
scene.voxels.coordinates = vec3(-16*m,0,-16*m)
scene.voxels:fill("Dirt Grass")
scene.voxels:box(0,2,0,16*m,2,16*m)
scene.voxels:fill("Bedrock")
scene.voxels:box(0,0,0, 16*m,0,16*m)
setupCamera(scene)
end
function setupCamera(scene)
scene.camera:add(GameCamera)
end
function update(dt)
scene:update(dt)
end
function draw()
update(DeltaTime)
scene:draw()
end
-- GameCamera
-- RJ 20200226
-- RJ 20200227 move camera rotation inside; cleanup
GameCamera = class()
function GameCamera:init(entity)
self.entity = entity
self.camera = entity:get(craft.camera)
Camera = self.camera
self.cameraX = 0
self.cameraY = 0
self.cameraZ = 0
self.cameraDistance = 20
self.target = vec3(8,0,8)
--[[local fieldOfView = 60
local ortho = false
local orthoSize = 5
Camera.fieldOfView = fieldOfView
Camera.ortho = ortho
Camera.orthoSize = orthoSize ]]--
end
function GameCamera:update(dt)
if CurrentTouch.state == MOVING then
self.cameraX = self.cameraX - CurrentTouch.deltaX * 0.25
self.cameraY = self.cameraY - CurrentTouch.deltaY * 0.25
end
local rotation = quat.eulerAngles(self.cameraX, self.cameraY, self.cameraZ)
self.entity.rotation = rotation
self.entity.position = -self.entity.forward * self.cameraDistance + self.target
end
It was at this point that I noticed that moving my finger side to side was tilting the picture up and down, and moving my finger up and down was turning the picture around the Y axis.
A moment’s thought and a heel of the hand strike to the forehead tells us why. The parameters to the rotation calculation are in the wrong order. Or the names are wrong. Or both.
I’m a bit confused, however, because I thought I had copied this code pretty carefully and it wasn’t doing that before. So I’ll look back and see what I missed, just for reassurance.
Sure enough, for example, in the Cameras example, I find code like this:
self.mx = -touch.deltaY / DeltaTime * self.sensitivity
self.my = -touch.deltaX / DeltaTime * self.sensitivity
Note the use of deltaY for X and deltaX for Y. We’ll rip off that idea.
I noticed reading my GameCamera class that it calls its variables “camera” this and that. Since it is a camera, that’s redundant. After this functional fix (and commit) I think I’ll do some name improvement. First the fix.
function GameCamera:update(dt)
if CurrentTouch.state == MOVING then
self.cameraX = self.cameraX - CurrentTouch.deltaY * 0.25 -- note XY inversion
self.cameraY = self.cameraY - CurrentTouch.deltaX * 0.25
end
local rotation = quat.eulerAngles(self.cameraX, self.cameraY, self.cameraZ)
self.entity.rotation = rotation
self.entity.position = -self.entity.forward * self.cameraDistance + self.target
end
Those two lines at the top do the trick. Now some name changes. After a commit.
-- GameCamera
-- RJ 20200226
-- RJ 20200227 move camera rotation inside; cleanup
GameCamera = class()
function GameCamera:init(entity)
self.entity = entity
self.camera = entity:get(craft.camera)
Camera = self.camera
self.rx = 0
self.ry = 0
self.rz = 0
self.distance = 20
self.target = vec3(8,0,8)
--[[local fieldOfView = 60
local ortho = false
local orthoSize = 5
Camera.fieldOfView = fieldOfView
Camera.ortho = ortho
Camera.orthoSize = orthoSize ]]--
end
function GameCamera:update(dt)
if CurrentTouch.state == MOVING then
self.rx = self.rx - CurrentTouch.deltaY * 0.25 -- note XY inversion
self.ry = self.ry - CurrentTouch.deltaX * 0.25
end
local rotation = quat.eulerAngles(self.rx, self.ry, self.rz)
self.entity.rotation = rotation
self.entity.position = -self.entity.forward * self.distance + self.target
end
The rx
etc variables stand for rotation. Perhaps they are now too terse.
Did you notice the commented out bit in the init
? That’s leftover cruft from wherever I stole that code from. Copy-paste cruft. However, I left it there on purpose, because I want to learn what those variables do to the view. I’m going to hook parameters to them and experiment. Might as well do that now.
-- GameCamera
-- RJ 20200226
-- RJ 20200227 move camera rotation inside; cleanup
GameCamera = class()
function GameCamera:init(entity)
self.entity = entity
self.camera = entity:get(craft.camera)
Camera = self.camera
self.rx = 0
self.ry = 0
self.rz = 0
self.distance = 20
self.target = vec3(8,0,8)
parameter.number("FOV", 0, 360, 60)
parameter.boolean("ORTHO", false)
parameter.number("OrthoSize", 1, 20, 5)
end
function GameCamera:updateParams()
self.camera.fieldOfView = FOV
self.camera.ortho = ORTHO
self.camera.orthoSize = OrthoSize
end
function GameCamera:update(dt)
self:updateParams()
if CurrentTouch.state == MOVING then
self.rx = self.rx - CurrentTouch.deltaY * 0.25 -- note XY inversion
self.ry = self.ry - CurrentTouch.deltaX * 0.25
end
local rotation = quat.eulerAngles(self.rx, self.ry, self.rz)
self.entity.rotation = rotation
self.entity.position = -self.entity.forward * self.distance + self.target
end
Here are a few pics. The ortho
flag basically turns off perspective. We’ll probably never use that again. As you can see, the resulting picture looks worse than orthographic, it looks larger in the back than in the front. The orthoSize
value controls how big the picture looks in ortho mode and the FOV seems to have no effect. In non-ortho (perspective) view, FOV is the angular field that the camera sees. Wider FOV makes the picture draw back.
Normal Perspective
Ortho View
Perspective wide FOV
For now, I’ll peg those to the values stolen from my example code and remove the parameters. That’s a neat feature, by the way, as you can probably tell from the pictures. Just call parameter
and you get a switch plus view of a value which you can then play with.
Now to commit and back out those parameters.
-- GameCamera
-- RJ 20200226
-- RJ 20200227 move camera rotation inside; cleanup
GameCamera = class()
function GameCamera:init(entity)
self.entity = entity
self.camera = entity:get(craft.camera)
Camera = self.camera
self.rx = 0
self.ry = 0
self.rz = 0
self.distance = 20
self.target = vec3(8,0,8)
self.camera.ortho = false
self.camera.orthoSize = 5
self.camera.fieldOfView = 60
end
function GameCamera:update(dt)
if CurrentTouch.state == MOVING then
self.rx = self.rx - CurrentTouch.deltaY * 0.25 -- note XY inversion
self.ry = self.ry - CurrentTouch.deltaX * 0.25
end
local rotation = quat.eulerAngles(self.rx, self.ry, self.rz)
self.entity.rotation = rotation
self.entity.position = -self.entity.forward * self.distance + self.target
end
All good. Commit again.
Frequent Commits
You’ll notice that I’m trying to commit the code every time something works as intended, with just a few lines changed. That may seem strange to you, especially if you’re on a team with long-standing branches and commits daily or less frequently.
Well, think about these notions.
First, for just me, if I make a mistake and just can’t recover it, there’s a version just a few lines back to revert to. So mistakes don’t cost me much: what costs me is time spent banging my head on a mistake when I could just revert a couple of lines and do over.
If I were on a team, well, merges would be problematical. If you work for a day and I work for a day on the same code base, we’ll each build up tests and changes that the other doesn’t have. The git merge is pretty good, but if we do anything incompatible, things get messy. And the longer we wait to merge, the worse it gets.
So if I change, commit, pull, continue, I’m current with you basically all the time. If something conflicts, I know it right away, and since you just committed it, it’s fresh in your mind. We connect, resolve the concern, and move on. The more frequently we commit, the fewer conflicts we have.
Especially if we are using extensive microtesting: then I’ll have added tests to go with my code, committed the code, pulled and run all the tests. If any test fails, my code broke it, because when you committed everything worked. (If you commit without running the tests to green, I am allowed to kill you, so that doesn’t become a problem for long.)
Since I just changed a few lines, I probably don’t break any tests, but if I do, it’s just a few lines that could have one it. Usually I see the mistake and if not, you’re just a tap away.
Frequent commits and integration, many times per day, really seems to be the way to go. There seems to be almost no limit to how small a commit can usefully be, so long as it is working code.
However
Now, for me, there’s a problem, because I have no microtests at this point. I honestly don’t know how to write tests for this kind of thing. Probably when we start having some real game play, I’ll be able to write some tests and if I don’t you are authorized to tweet rude things at me.
For now, I’m trying to develop the habit of frequent commits, using WorkingCopy, the lovely iOS git app.
I’m even thinking of attaching a remote repo so I can use my older iPad from the tv room.
Let’s review a bit now.
Review
We’re an hour into the morning and the code is far better than it was, and I think I pretty much understand every line of it. I notice a few magic constants, but most of them are OK.
The target
value, however, troubles me. It’s the coordinates of the middle of my current very small very flat terrain. It should probably be an init
parameter. Even as it stands, we can just say something like myCam.target =
and change it. Not a great interface: you’d probably want a setter. But in Lua,stabbing values into public members is more the thing to do,since it’s just a tiny scripting language. We need to consider how far to go toward big-system thinking. Anyway, that target
is pinned more than it should be, in my opinion.
Then there’s the setup:
function setup()
scene = craft.scene()
-- Setup camera and lighting
scene.sun.rotation = quat.eulerAngles(25, 125, 0)
-- Set the scenes ambient lighting
scene.ambientColor = color(127, 127, 127, 255)
allBlocks = blocks()
-- Setup voxel terrain
local m = 1 -- 5
scene.voxels:resize(vec3(5,1,5))
scene.voxels.coordinates = vec3(-16*m,0,-16*m)
scene.voxels:fill("Dirt Grass")
scene.voxels:box(0,2,0,16*m,2,16*m)
scene.voxels:fill("Bedrock")
scene.voxels:box(0,0,0, 16*m,0,16*m)
setupCamera(scene)
end
function setupCamera(scene)
scene.camera:add(GameCamera)
end
That’s not ideal. There’s procedural stuff in there, plus a function call (to a single-line setup!). We should move that all the way to separate named functions for the various semantic bits. That’s our local programming style, and remember, we’re thinking of writing something that actually shows how to write decent code.
That’ll be for tomorrow. Today, we’ve reached a decent point, understanding is at a local maximum, and I’m thinking about some lunch.
See you next time: here’s the code:
-- Game-1
-- RJ 20200222 initial plan, make a floor
-- RJ 20200226 camera object works w/o touch
-- RJ 20200227 cleanup
function setup()
scene = craft.scene()
-- Setup camera and lighting
scene.sun.rotation = quat.eulerAngles(25, 125, 0)
-- Set the scenes ambient lighting
scene.ambientColor = color(127, 127, 127, 255)
allBlocks = blocks()
-- Setup voxel terrain
local m = 1 -- 5
scene.voxels:resize(vec3(5,1,5))
scene.voxels.coordinates = vec3(-16*m,0,-16*m)
scene.voxels:fill("Dirt Grass")
scene.voxels:box(0,2,0,16*m,2,16*m)
scene.voxels:fill("Bedrock")
scene.voxels:box(0,0,0, 16*m,0,16*m)
setupCamera(scene)
end
function setupCamera(scene)
scene.camera:add(GameCamera)
end
function update(dt)
scene:update(dt)
end
function draw()
update(DeltaTime)
scene:draw()
end
-- blocks
-- RJ 20200222
-- ERJ 20200226 fixed dirt grass
function blocks()
scene.voxels.blocks:addAssetPack("Blocks")
local dirt = scene.voxels.blocks:new("Dirt Grass")
dirt.setTexture(ALL, "Blocks:Dirt Grass")
dirt.setTexture(UP, "Blocks:Grass Top")
dirt.setTexture(DOWN, "Blocks:Dirt")
local bedrock = scene.voxels.blocks:new("Bedrock")
bedrock.setTexture(ALL, "Blocks:Greystone")
local allBlocks = scene.voxels.blocks:all()
return allBlocks
end
-- GameCamera
-- RJ 20200226
-- RJ 20200227 move camera rotation inside; cleanup
GameCamera = class()
function GameCamera:init(entity)
self.entity = entity
self.camera = entity:get(craft.camera)
Camera = self.camera
self.rx = 0
self.ry = 0
self.rz = 0
self.distance = 20
self.target = vec3(8,0,8)
self.camera.ortho = false
self.camera.orthoSize = 5
self.camera.fieldOfView = 60
end
function GameCamera:update(dt)
if CurrentTouch.state == MOVING then
self.rx = self.rx - CurrentTouch.deltaY * 0.25 -- note XY inversion
self.ry = self.ry - CurrentTouch.deltaX * 0.25
end
local rotation = quat.eulerAngles(self.rx, self.ry, self.rz)
self.entity.rotation = rotation
self.entity.position = -self.entity.forward * self.distance + self.target
end