It’s probably time to work on behavior, so I want to take a bit of time to look forward at what we might do, might not do, and probably won’t do.

My purpose in this Codea Craft series is to have a bit of fun, learn a bit about Codea, and to explore with all four readers the kinds of issues we encounter and to share my experience and ideas as things happen. I’m focusing on a game, not with intention to complete a game, but because it will probably give enough structure to provide lots of opportunities for learning.

Why not complete a real game? Well, never say never, but if you look at the work that goes into a modern game, it’s immense, with graphics and sound and special effects and levels and who knows what all. There’s just one of me, and I’m old and tired. If it continues to be fun, I’ll push on. When it gets not to be fun, I’m outa here.

Take one example. For a real game, our creatures would need interesting 3D bodies and probably visible behavior like walking, running, jumping, flying. There would probably be several different kinds of creatures, and even within a creature type, we’d want to be able to identify individuals. There are hours and hours of 3D graphical design work tied up in those two sentences. That’s no fun for me, and even less fun for readers to read about.

Therefore, I’m going to use available shapes for creatures, or create some really simple ones out of spheres and boxes. Our learning will be about the same, but the effort will be much lower. However, the quality of the game as a game will be much less than if we put in those many hours.

Of course, if a cadre of graphical designers and programmers wanted to join me in this effort, it might be a different story. But I don’t expect that to happen, and neither should you, unless you happen to be a cadre of graphical designers and programmers.

Anyway, looking forward …

Behavior

I envision our Creatures, whatever they look like, as having a kind of user-selectable behavior. The game consists, at least in part, of setting up Creatures, setting them loose in the world, and observing what happens.

At base, behavior will consist of some nearly trivial senses and actions. In the 2D Braitenberg articles, the creatures could sense food at a distance, and could sense the edges of the world. The connection between their senses and their actions was primarily the notion of “wheels”, because Braitenberg’s bugs worked that way. It goes like this:

A bug’s wheels turn independently, according to rules like this:

  1. Some default constant amount added to each wheel;
  2. Some small “jitter” randomly added to each wheel;
  3. If something dangerous is seen, add a bit more to the wheel nearer to the danger than to the one further away;
  4. If something desirable is seen, add a bit more to the further wheel than to the nearer.

These rules, with some tuning, will keep the creature from falling off the edge of the world, and make it move toward food.

Braitenberg’s idea, elaborated for many pages, was that simple rules like these could engender very complex behavior that looks “intelligent”. And it’s a pretty compelling argument, though it’s hard to come up with the simple rules that generate automobile construction or portrait painting.

So my 2D game was driven that way. I had actually started by having the senses rotate the bug directly: if danger, turn away, and went back to the wheels approach the better to follow Braitenberg. I’m not sure which way we should go here, but the two approaches are probably equivalent.

The notion of adding a bit of motion to the wheels led to a simple design, in that each behavior returned a simple 2D vector saying how much to spin the left wheel and right wheel, respectively. So all the behaviors basically added into the wheels, which then moved the bug. Pretty simple.

That may not hold water forever, and we could certainly do something more elaborate. I trust that if I keep the design clean, changes for other behaviors will be pretty straightforward. And if they aren’t, we’ll learn something, won’t we?

Of course, we’ll be on the lookout for ideas that may break our current ones. It would be imprudent to hold off on some idea that rather clearly doesn’t fit into the wheels notion, and do 100 that do fit into the wheels, only then to discover that wheels won’t do. We’ll try to be judicious about picking stories that “test” our current design.

But the main defense against surprise requirements, in my view, is clean design. My view could be wrong: we’ll see how it goes.

Thinking of details

In the 2D version, the “vehicles”, as Braitenberg called them, are instantiated with a collection of behaviors. For example, the behavior “seek food” consists of:

  • default motion
  • jitter
  • seek food (if hungry)
  • eat food (if food present)
  • avoid walls

The update operation on a vehicle executes those behaviors, one after another, unconditionally. The behavior tables are created in the main program, then vehicles are instantiated with the behavior tables. The behaviors are stored as partial functions, so that upon definition of a behavior table, you can plug in values. For example, the vanilla vehicle jitters by 0.1, while the food seeker jitters by 0.25. Why? Just to see what would happen.

Each behavior in a behavior table is a function, with some arguments bound, that calls a method on the vehicle, using those saved arguments. This may not be ideal.

Another approach, possibly better if we imagine some kind of behavior-building GUI, might be to provide a table of arguments, so that a behavior item would consist of an operation, like creature:seekFood, and an associated table, perhaps {dist=100}, representing the maximum distance this creature can sense food. Then all the creature methods would be invoked the same, like

function Creature:seekFood(argTable)
  local dist = argTable.dist or 50
  blah blah
end

The setup when coding might be a bit more tricky, but it’s likely that the setup from some kind of creature GUI would be easier, coming down always to populating a table and passing it in.

This is looking forward a ways but I don’t mind thinking about the future, I just try not to build elaborate things that I don’t need yet.

For now, I think I”ll just try to replicate some simple motion on my Orc creature and see where it leads. I’ll try to be informed by what I’ve done before, but not limited by it.

Instant Learning

I started by thinking about some kind of table format that the main could use to tell the Orc not to fall off the edge of the world. (He wouldn’t, really, he’d just walk into space.) It was immediately clear that I didn’t know enough to define the behavior from the outside: I need to decide inside how to do it. So instead, I’ll start inside the Creature class and set some wandering and simple limits.

Even this may get tricky and I’ve only got 20 or 30 minutes left to play. We’ll see …

And it’s Tuesday.

I did get a bit of wandering finished before I went to lunch but not the wall avoidance. There was more to it than a few minutes could accommodate. The behavior looks like this:

And here’s the code, of which I do not approve:

Creature = class()

local CreatureMaxTime = 300

function Creature:init(entity, x, z, behaviors)
    self.entity = entity
    self.behaviors = behaviors or {self.walk}
    self.angle = 0
    self.time = 0
    self.move = vec2(0,1)
    entity.x = x
    entity.z = z
    entity.y = 2 -- just above the ground
    entity.model = craft.model("Blocky Characters:Orc")
    entity.scale = vec3(1,1,1)/8
end

function Creature:update(dt)
    local dt = dt or DeltaTime
    for _,b in pairs(self.behaviors) do
        b(self)
    end
end

function Creature:walk()
    self.time = self.time + 1
    local speed = 100
    if self.time == 200 then
        self.time = 0
        if math.random() < 0.5 then
            self.angle = 45
        else
            self.angle = -45
        end
        local rotor = quat.eulerAngles(0,-self.angle,0)
        self.entity.rotation = self.entity.rotation*rotor
        self.move = self.move:rotate(math.rad(self.angle))
    end
    self.entity.x = self.entity.x + self.move.x/speed
    self.entity.z = self.entity.z + self.move.y/speed
end

I defaulted the creation method to include the behavior self.walk. Then update does all the behaviors (just walk for now), and well, there’s the walk itself. This is a bit weird.

We change our direction of motion by 45 degrees every 200 steps. I used 45 so the change would be obvious. I’d tried a random value and it was often small enough that I couldn’t be sure what was happening.

Then we have to maintain two things: the entity rotation, to make the Orc face forward, and the direction of motion. You’ll note that in the init we start with move as (0,1), one step in the Y direction. Then, when we turn by some angle, we update move by rotating it by the angle. (Oddly, the Orc has to rotate by the negative of the angle. At this moment I can’t explain that.)

Then we update the entity’s x and z position by the x and y values of the move vector, dividing out by speed. (Speed is clearly a bad name, since the larger it is the slower we go.)

So yes, this is a hack, or as we professionals call it, a “Spike”. It’s an experiment to learn how to do something (and how not to do it). Now that we have a way to do it, we need to figure out a good way.

What’s not to like? Well, we don’t like representing the situation in so many places. We have an incremental angle of rotation in our object, and a cumulative equivalent quaternion in the entity. We have a direction of motion in our move variable, which is clearly implied by the entity’s quaternion and instead gets modified by our intended angle of change.

Furthermore … I had intended originally to have walk just return “wheel speed”, a different value for the left side and right side of the Creature, because that can neatly embed both urgency (by being large) and motivation (by turning one way or the other).

So we’re definitely not there yet.

Now in the Braitenberg experiment, which does just sum up wheel speeds, the movement is calculated like this:

function BVehicle1:calcAngle(wheels)
    return math.atan(wheels.y-wheels.x, self.width)
end

function BVehicle1:determinePosition(wheels)
    self.angle = self.angle + math.deg(self:calcAngle(wheels))
    local step = vec2((wheels.x+wheels.y)/2,0)
    local move = step:rotate(math.rad(self.angle))
    self.position = self.position + move
end

Here we’re remembering our cumulative angle and position, and we calculate our new angle as the atan of the difference between the wheels’ proposed movement. We rotate as needed, and then move forward to the average position indicated by the wheels. That’s at least semi-clean.

So the first thing will be to try to clean this up a bit. I’ll browse some documentation and see whether the entity and its quaternion can help us.

Somewhat cleaner

Well! That took longer than I expected but turns out OK. It turns out that there is a value entity.forward that returns a normalized 3D vector pointing in the forward direction of the entity. Just what we needed. Now, if we adjust the rotation of the entity, and read out its forward, we have what we need to move our Orc properly:


function Creature:walk()
    local angle
    local move
    self.time = self.time + 1
    local speed = 200
    if self.time == 100 then
        self.time = 0
        if math.random() < 0.5 then
            angle = 45
        else
            angle = -45
        end
        local rotor = quat.eulerAngles(0,-angle,0)
        self.entity.rotation = self.entity.rotation*rotor
    end
    move = self.entity.forward
    self.entity.position = self.entity.position + move/speed
end

Now all the information we need is in the entity, not in the Orc. However, we’re still not using the notion of “wheels”, which is, I think, a better model than speed plus rotation would be. Maybe.

We could have each behavior return a table. At the beginning, it might be one like {speed = 3, turn = 45}. We could allow either or both value to be missing and default them to zero. In future, if we need more returns, we could add them to the table. That has a certain something that I like. We’ll try it.

The thing here will be to change walk to return a table like that, and to cumulate the values for turn and speed in the main update, and do the final update there. Should be “easy”. Hold my water bottle.

Ha! Not bad at all.

[takes water bottle back]

That worked quite nicely, and now that the turn and direction are properly embedded in the entity, that irritating minus sign can come out. Here’s Creature:

Creature = class()

local CreatureMaxTime = 300

function Creature:init(entity, x, z, behaviors)
    self.entity = entity
    self.behaviors = behaviors or {self.walk}
    self.time = 0
    entity.x = x
    entity.z = z
    entity.y = 2 -- just above the ground
    entity.model = craft.model("Blocky Characters:Orc")
    entity.scale = vec3(1,1,1)/8
end

function Creature:update(dt)
    local divisor = 100
    local speed = 0
    local turn = 0
    for _,b in pairs(self.behaviors) do
        local tab = b(self)
        speed = speed + (tab.speed or 0)
        turn = turn + (tab.turn or 0)
    end
    local rotor = quat.eulerAngles(0,turn,0)
    self.entity.rotation = self.entity.rotation*rotor
    local move = self.entity.forward
    self.entity.position = self.entity.position + move*speed/divisor
end

function Creature:walk()
    local tab = {speed=1}
    self.time = self.time + 1
    local speed = 200
    if self.time == 100 then
        self.time = 0
        if math.random() < 0.5 then
            tab.turn = 45
        else
            tab.turn = -45
        end
    end
    return tab
end

Note that the behavior (walk) now returns a table that will always say speed=1 and may include a value for turn. The loop that calls behaviors defaults the speed and turn to zero, so that behaviors can return either, both, or no values at all. (A food-seeking behavior might have no effect if we weren’t hungry. (Which I am, and I’ll be leaving shortly.))

The rest of the code is (essentially) the same as you’ve seen before:


-- Game-1
-- RJ 20200222 initial plan, make a floor
-- RJ 20200226 camera object works w/o touch
-- RJ 20200227 cleanup
-- RJ 20200307 clean a bit for bugs

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,1,0,16*m,1,16*m)
    scene.voxels:fill("Bedrock")
    scene.voxels:box(0,0,0, 16*m,0,16*m)
    
    setupCreature(scene)
    
    setupCamera(scene)
end

function setupCamera(scene)
    scene.camera:add(GameCamera)
end

function setupCreature(scene)
    scene:entity():add(Creature, 7, 7, actions)
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
-- RJ 20200307 change starting rx ry for fun

GameCamera = class()

function GameCamera:init(entity)
    self.entity = entity
    self.camera = entity:get(craft.camera)
    self.rx = 45
    self.ry = 45
    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

That’ll do it for today. I think we’ve got a structure we can live with, and the Orc walks:

See you next time!