Codea Craft 11 -- Trouble
The following are my closing notes from the preceding article, where I managed to make a floor display, after importing a pile of code I neither wrote nor understand:
We have at least these alternatives:
- Live with BasicPlayer and learn him later
- Set up our own camera, which we need to learn about
- Set up another camera, such as the OrbitViewer.
We’re really trying to learn the system here, but there’s no dishonor in using existing utilities. Much as we’d like to, we don’t always start by writing a compiler or an operating system.
My Dilemma
I’ve been struggling with this little project, as may be clear from the recent articles. Let me be more clear about what’s bothering me.
My “fundamental theorem” of software development is that the best thing to do is always
Deliver a running tested product increment frequently, with capabilities chosen by the business people and with the best possible design in place at all times.
I’ve written extensively (and almost exclusively) for years now about how this is the best way I know to succeed and thrive in any development situation. Having working product in hand allows us to balance the conversation between business and technical concerns, and keeps us focused on reality.
Meanwhile, a dedication to maintaining a good design ensures that we can make steady progress, avoiding the horrid slow-down in progress that comes from rushing too much and creating a rats-nest of legacy code.
The biggest objection to this idea is “but what if we can’t”, because many teams have such a mess on their hands that they can’t see how to do anything in a couple of weeks. It is usually – I believe always – possible to start doing things every couple of weeks, by taking on less work and slicing it thinner. This can be painful, but the pain is the pain of coming to grips with the reality of the situation.
Another form of “but what if we can’t” faces me now. When a team takes on a largish product unlike anything they know how to do, they’ll face the choice I’m facing here. Either load in a pile of library and support software that we don’t understand, or build it ourselves.
Loading in the libraries results immediately in “not the best possible design”, since we won’t even understand the whole design, much less have it be right.
Building it ourselves, even based on available libraries and examples, is also a problem. We will almost certainly not be able to build anything that looks like a product in two weeks. We just flat need some time to learn the problem and how to address it.
I’ve just taken that first step on a slippery slope, by importing BasicPlayer, which I certainly don’t understand. That project imports other tabs. Here’s the start of its init:
function BasicPlayer:init(entity, camera, x, y, z)
assert(touches, "Please include Touches project as a dependency")
assert(ui, "Please include UI project as a dependency")
assert(FirstPersonViewer, "Please include Cameras project as a dependency")
assert(blocks, "Please include Block Library project as a dependency")
assert(allBlocks, "Blocks arent loaded, please add 'allBlocks = blocks()' to setup()")
So in one simple move, I’ve exposed myself to four projects and quite a few tabs. (At this moment, I’m not using any of them, and they’re not set as dependencies. But they’re looming over me, I can feel them.)
I want it to be true that we can “always” move quickly to delivering quality working product increments. This is a very fundamental belief of mine after over a half-century of software development. At the same time, it’s clear that there is some irreducible minimum of knowledge to make that happen. We couldn’t do it with a room full of people none of whom could program at all. So there must be an irreducible minimum of learning that must be done before any given team can undertake any given product. That minimum may be quite small, or quite large.
How do we manage that minimum? I’m facing exactly that problem, torn between just wanting to go read and write messy experiments, and showing my “product owner” – and you – some obviously valuable results.
Extreme Programming introduced the notion of the “Spike”, a quick experiment helping us learn how something could and should be done. I clearly need some Spikes here. The trick will be keeping them visible enough to keep everyone on the project happy.
That’s what we’ll be trying to do over the next few sessions. I hope both my readers will stay tuned.
The Overall Play
We’re working toward a simple 3D game made of cubes, with a single player (probably invisible but we see through their eyes) who can wander around finding things, making things, and whatever else comes to mine.
I have a few underlying purposes. I don’t really want the game. I want to:
- Learn about Codea Craft, to the point where I could program a game;
- Share with [both] my readers how I learn a system, hoping it’ll be useful to them to see how imperfection becomes more perfect.
- Support my fundamental belief that incremental delivery is “always” possible and “always” good.
- Prepare myself to write some improved documents for Craft;
- Have fun
So far so good, but I’m feeling iffy about it all.
Present Status
I’ve got a tiny running program that is arguably a small step toward our defined game. To see that it was working, I had to import a big chunk of player code, which set the camera where it needed to be.
That’s not correct. I didn’t have to do that, I chose to do that. It seemed to be the best idea I had at the time.1
I’m facing what seems like an almost impossible choice. I want very much to make discernible progress toward the “game”, in very small steps. I want each of those steps to consist of a program with some new capability that we can see, supported by code that is at least reasonable well-structured, and that we (I) understand well enough to consider it our own.
When I slam in something like the BasicPlayer, I make apparent progress but at the cost of much lower understanding of the code. Now, one can argue that use of a “library” or reuse of some objects is a good thing, and it is, but it comes at a cost when we do that. We lose some control over our system, some ability to change in arbitrary ways, and some sense of ownership. Those costs may be worth paying, but they may not.
Since my fundamental purpose in this exercise is more about understanding than about a specific game, that cost seems too high. But without bringing in those libraries and living with them, I may not be able to progress as I’d like.
So I am torn.
For now, my plan is to bring things in for a quick look, perhaps study them to learn a bit of how they work, and then remove them, replacing with a smaller version that fits our application better. That might be done by trimming things out of the library, or by replicating behavior based on learning how it works, or by creating the desired behavior in some new way. It might even be done by coming to understand the big blob, though that seems unlikely.
The main thing is to try to get rid of the code I don’t understand as quickly as possible.
Today, I’ll try putting writing a simple camera setup, and failing that, bring in a simpler camera from one of my earlier projects in this series. We’ll see how long it takes. If it’s not very long, then maybe we’ve found a viable way to proceed.
Simple Camera
I’ll try to start with a simple fixed camera.
-- Game-1
-- 20200222 initial plan, make a floor
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
scene.voxels:resize(vec3(5,1,5))
scene.voxels.coordinates = vec3(0,0,0)
-- Create ground put of grass
--scene.voxels:fill("Bedrock")
--scene.voxels:box(0,10,0,16*5,10,16*5)
scene.voxels:fill("Dirt")
scene.voxels:box(0,0,0,16*5,9,16*5)
scene:entity():add(craft.camera, 45, -100, 100, false)
end
function update(dt)
scene:update(dt)
end
function draw()
update(DeltaTime)
scene:draw()
end
I just added that craft.camera
line by copying from the craft.camera
example in the documents. The result is a black screen. Better look at one of my older programs, but first to version this code even though it doesn’t work, because I removed the dependency on BasicPlayer.
My second attempt borrows the camera code from the Orc program, looking like this:
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)
updateCamera(dt, scene)
scene:update(dt)
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
(I call setupCamera
from setup
of course.) This works, with a surprise. My first attempt, I copied only the camera setup, not the update, in the interest of a small step. And I got this picture of a floor in the corner of the screen:
So I added the update, to look around, and now I can swing the view around and see more of the world I’ve created. Look:
Remember that my Orc program had things centered around zero. And the camera always aims at zero: I did a little geometry to show why that was in a previous article. Therefore, the world I’ve created must start at zero … and sure enough, it does:
-- Setup voxel terrain
scene.voxels:resize(vec3(5,1,5))
scene.voxels.coordinates = vec3(0,0,0)
scene.voxels:fill("Dirt")
scene.voxels:box(0,0,0, 16*5,9,16*5)
Yep, that creates an array of Dirt from (0,0,0) to (80,9,80). Why those values? Well, I copied them from some other program. Anyway now that I can see the ground, I can improve it.
After a bit of experimentation, I’m here:
function blocks()
scene.voxels.blocks:addAssetPack("Blocks")
local dirt = scene.voxels.blocks:new("Dirt Grass")
dirt.setTexture(ALL, "Blocks:Dirt Grass")
local bedrock = scene.voxels.blocks:new("Bedrock")
bedrock.setTexture(ALL, "Blocks:Greystone")
local allBlocks = scene.voxels.blocks:all()
return allBlocks
end
And this:
-- Setup voxel terrain
scene.voxels:resize(vec3(5,1,5))
scene.voxels.coordinates = vec3(0,0,0)
scene.voxels:fill("Dirt Grass")
local m = 1 -- 5
scene.voxels:box(0,1,0,16*m,1,16*m)
scene.voxels:fill("Bedrock")
scene.voxels:box(0,0,0, 16*m,0,16*m)
Resulting in this:
Notice that I took out the scaling by 5 and the many layers of bedrock (initially dirt) from my original example. And I’m still not clear at all on the relationship between resize
, coordinates
, and box
.
I’m expecting that if I set m
back to 5 I’ll get more land … and I do:
Must I origin my voxels at (0,0,0)? What’s the resize doing? Can I set negative values into the calls to box? I’ll try that first:
This:
scene.voxels:box(-16*m,1,-16*m,16*m,1,16*m)
setting the box to span neg to pos has no visible effect. Either creation is clipped at zero or my view is. I don’t think it’s my view because the Orc camera works, and we’re using it here.
So maybe coordinates. Maybe rereading the docs, too.
After more than an hour of fiddling, I still don’t understand how this works. It’s time for a break, and lunch. I feel demoralized because I’ve made little progress in an hour or so. Well, I did make my own camera work, I guess. Still feel pretty stupid.
Keep in mind, it’s only a bit over an hour. The iteration is far from over. More tomorrow, with luck.
Here’s the code FWIW:
-- Game-1
-- 20200222 initial plan, make a floor
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.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)
updateCamera(dt, scene)
scene:update(dt)
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
function draw()
update(DeltaTime)
scene:draw()
end
-- blocks
-- RJ 20200222
function blocks()
scene.voxels.blocks:addAssetPack("Blocks")
local dirt = scene.voxels.blocks:new("Dirt Grass")
dirt.setTexture(ALL, "Blocks:Dirt Grass")
local bedrock = scene.voxels.blocks:new("Bedrock")
bedrock.setTexture(ALL, "Blocks:Greystone")
local allBlocks = scene.voxels.blocks:all()
return allBlocks
end
Wednesday, Snowpocalypse
Despite impending snow doom, I’ve come to the coffee shop to work a bit more on understanding what I have wrought. My current confusions mostly come down to these two:
- Voxels
- Are they constrained to have non-negative positions? My example seems to indicate that: when I put in negative coordinates, I only see the parts from zero on up. I’ve posted a question on the Codea Forum about that.
- Cameras
- I need to get cameras under control. I don’t understand how one ought to manage them. I’ve got a couple of simple examples that sort of work and today I’m going to instrument something and see what I can figure out.
If I can get control of a simple camera, I feel that I should be able to build on that aspect in a fairly sensible bit-by-bit way. Same with the voxels, probably. So if another hour or so will get this little demo in hand, I’ll consider yesterday and today time well spent.
Oh, and one small thing. I created my grass block using just one texture, the side view of dirt with grass on top. I’ll put all grass on the UP surface and all dirt on the DOWN, when I get around to it. I’ll try to remember to show you a picture of that.
My plan is to start by watching some camera parameters, like position, and controlling its rotation. My best guess is that it’s just facing the wrong way and that’s why I don’t see my construction when I use my own camera. Here goes, hold my chai …
Well …
I forgot that my current camera does see the zero corner of the build. So it’s aimed sort of right. Same plan, watch and adjust camera params.
[sips chai, hands it back]
With a little fiddling, I’ve refreshed my understanding of cameras a bit. The scene has a camera, and it can be at any position and pointed in any direction. The updateCamera
code presently looks like this:
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
end
scene.camera.eulerAngles = vec3(CameraY, CameraX, CameraZ or 0)
scene.camera.position = -scene.camera.forward * 20
end
I fiddled it a bit so that I can drive it from Codea parameter sliders as well as finger dragging. Note that it is positioned at -20 times forward
, which we learned the other day is the forward pointing unit vector, essentially a unit vector rotated by the camera angles set just above. This gives me pictures like this:
You can see in this picture that the grass looks ok from the side but not on top. We’ll fix that later. For now, I’ve got a small floor of grassy dirt blocks with a layer of bedrock underneath spaced off by one cube width. I did that yesterday while trying to understand voxel coordinates. We’re letting that topic mostly slide for now.
Now what I want is to focus the camera somewhere else, not always at zero. But that scene.camera.position
will always do that. Let’s try focusing it in the middle of the floor with a simple offset. I’m building the floor just 16 cubes in x and z, so let’s try to look at (8,0,8). (Remember that z is front to back.)
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
end
scene.camera.eulerAngles = vec3(CameraY, CameraX, CameraZ or 0)
scene.camera.position = -scene.camera.forward * 20 + vec3(8,0,8)
end
I just added the vector target position to the scene camera position, and voila!
That’s just what we want. We’re now 20 units back from the picture, looking at the middle of it. Woot!
The code, of course, is a bit of a hack. Let me commit this and then we’ll see about cleaning it up.
Cleaning up camera
As I look at the clock, I’m not sure I’ll have time to do a decent cleanup on the camera. I have seen two approaches to cameras in Codea so far.
The “Cameras” example attaches an object as a child of the existing scene camera. It then gets updates sent to it, and adjusts the camera parameters as it sees fit. This would work for us here: we could give that object the target and whatever other member information we needed. We might have to lift a pointer to it up somewhere but that remains to be seen.
The other approach is to put an entity into the scene and attach a camera to it. This is what’s done in the BasicPlayer example. The player is represented by a capsule with physical properties, that we can’t see, because its camera is looking out of the capsule’s eyes, if in fact it has eyes. The capsule then manages bumping into things in the world and the camera shows us what “we” see.
I’m not sure which way our game will go. We could have a visible player avatar and be watching it from a distance, or we could go first person. Possibly both, you know how customers are.
For now, pulling the camera management out into an object attached to the scene camera seems like the smaller step. I’ll do that.
First, though, let’s fix the grass. That should be easy. (Never trust the word “easy” or the word “just” when used in programming, by the way.) Anyway …
-- blocks
-- RJ 20200222
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
That gives me grass on top and dirt on the bottom, as planned. An unqualified success! I’m tempted to go out for pizza but the place isn’t open yet.
OK, that took a bit longer than I had hoped. (Remember what I said about trusting “easy”?) But I have an object that moves the camera when I move the parameter sliders. I don’t have touch working quite right but we’ll look at that in a bit. It’s still happening over in Main.
Here’s a photo, and then all the code. I’m off for pizza. See you soon, I hope!
-- Game-1
-- 20200222 initial plan, make a floor
-- 20200226 camera object works w/o touch
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)
parameter.number("CameraX", 0, 360)
parameter.number("CameraY", 0, 360)
parameter.number("CameraZ", 0, 360)
parameter.watch("scene.camera.position")
parameter.watch("scene.camera.forward")
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 = class()
function GameCamera:init(entity)
self.entity = entity
self.camera = entity:get(craft.camera)
Camera = self.camera
--[[Camera.z = -4
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
CameraX = (CameraX or 0) - CurrentTouch.deltaX * 0.25
CameraY = (CameraY or 0) - CurrentTouch.deltaY * 0.25
end
local rotation = quat.eulerAngles(CameraX or 0, CameraY or 0, CameraZ or 0)
self.entity.rotation = rotation
self.entity.position = -self.entity.forward * 20 + vec3(8,0,8)
end
-
Spock asked his father, Sarek, why he had married Spock’s human mother, Amanda. Sarek replied, “At the time, it seemed the logical thing to do.” ↩