Drive Wheels

I’ve been reading Braitenberg’s book, and his thought-experiments all drive the wheels of the vehicles at differing speeds to bring about turning. As I talked about above, it’s “clear” that just incrementing the vehicle’s rotation is equivalent, but as I think about how I’d like my vehicles to evolve, using the variable wheel rotation seems that it would be better. So I decided to switch out the process. I thought about it for most of Friday morning at the coffee shop, while chatting about this and other things with Bill and Chet, and then in the last 20 minutes, changed it over. I did it by commenting out the food search entirely, and then changing the driving code in place to drive my little test vehicle with varying wheel speeds.

I also did a bit of trig to determine how differential wheel speed affects the incremental angle of rotation. Here’s an ugly picture of that. And here’s the code, as committed last time:

function BVehicle1:init(x,y, angle)
    self.x = x
    self.y = y
    self.angle = angle or 0
    self.rWheel = 0
    self.lWheel = 0
    self.width = 10
end

function BVehicle1:calcAngle(d1, d2)
    return math.atan(d2-d1, self.width)
end

function BVehicle1:update()
// food code commented out here
    self.lWheel = 10
    self.rWheel = 10.5
    self.angle = self.angle + math.deg(self:calcAngle(self.lWheel, self.rWheel))
    local step = vec2((self.lWheel+self.rWheel)/2,0)
    local move = step:rotate(math.rad(self.angle))
    self.x = self.x + move.x
    self.y = self.y + move.y
    if self.x > 1000 then self.x = 100; self.y = 100 end
end

Not much to see here. I decided the width of the vehicle was 10 (world coordinates), mostly because that’s how wide the rectangle is, and had observed that if one wheel moves d2 and the other d1, the angle of rotation gone thru is arctangent((d2-d1)/width). And the distance moved, by the center of the vehicle, is (of course) the average of d2 and d1.

I see now that d2 and d1 should be named something better, distanceLeft and distanceRight or something like that. I had drawn d2 and d1 on the diagram and those names stuck.

I’m irritated by swapping back and forth between vec2 and separate parameters but that’s caused by Codea having decided that some things work one way and others work another. I’ll be watching for ways to do fewer conversions as we go alone.

Where we stand now is that with speed 10 and 10.5 to the wheels, our little guy runs in a circle, as one would expect. Speed 10 is very fast: the nominal top speed of these guys should probably be around 1 or 2, not 10. The limit will be on what looks good on the screen.

What else?

Well, the rectangle mode we’re using has the origin of our vehicle at its lower left corner. I think first thing I’ll do is change it to the center.

That was easy but with a surprise. Here’s the code for drawing the vehicle:


function BVehicle1:draw()
    pushStyle()
    pushMatrix()
    stroke(255,255,0)
    fill(255,255,0)
    translate(self.x, self.y)
    rotate(self.angle)
    rect(0, 0, 20, 10)
     local s = string.format("%d %d", math.floor(self.x), math.floor(self.y))
     text(s,0, 30)
    local eyeSize = 8
    local eyeR = vec2(10,-5)  changed
    local eyeL = vec2(10,5)   changed
    stroke(255,0,0)
    fill(255,0,0)
    ellipse(eyeR.x, eyeR.y, eyeSize)
    ellipse(eyeL.x, eyeL.y, eyeSize)
    popMatrix()
    popStyle()
end

What we changed here was just the eye position, since the rectangle is now drawn centered, so its corner offsets need to be halved. I changed the mode in setup, which also changed where the framing rectangles were drawn. Perhaps I should have just changed the mode for the vehicle and left the mode left-bottom-centered for the rest. There’s some art to having the world coordinates zeroed on the lower left. I think I’ll do that:

function BVehicle1:draw()
    pushStyle()
    pushMatrix()
    rectMode(CENTER)  changed
    stroke(255,255,0)
    fill(255,255,0)
    translate(self.x, self.y)
    rotate(self.angle)
    rect(0, 0, 20, 10)
     local s = string.format("%d %d", math.floor(self.x), math.floor(self.y))
     text(s,0, 30)
    local eyeSize = 8
    local eyeR = vec2(10,-5)  changed
    local eyeL = vec2(10,5)   changed
    stroke(255,0,0)
    fill(255,0,0)
    ellipse(eyeR.x, eyeR.y, eyeSize)
    ellipse(eyeL.x, eyeL.y, eyeSize)
    popMatrix()
    popStyle()
end

OK, that’s better. I moved the rectMode inside, so everything else draws as before and my vehicle is drawn about its geometric center. Time to commit.

What’s next? Behavior?

I was thinking on the way home that these little guys have lots of different behaviors. They are attracted to light, or to food, they avoid things, and so on. All these behaviors are implemented by sensing some aspect of the world, and then adjusting the speed of the driving wheels. As one reads the book, there get to be more and more complicated behaviors, and then finally, more and more complicated interconnections of behaviors. I’m not sure if I’ll get to that last part. But I do plan to build a few different behaviors, starting with the food-seeking one that I built the other day.

Here’s my cunning plan. A “behavior” will be a function returning the x and y values to be added to the wheel’s current velocities. At this moment, I think behaviors will not be executed in any particular order, but we’ll see what happens in the future. I do suspect that a vehicle will have a collection of behaviors, and I think I’ll have that collection be a table with key of the behavior name, and value the function. Each behavior will have the same form. I’m torn about whether I should make them pure functions, with a known parameter list, or make them be methods on the BVehicle1 class. Just because it might be interesting, I think I’ll make them functions, taking the vehicle and the world as parameters. (We haven’t really defined world yet, but we can see that coming.)

I’ll begin by creating my food-seeking behavior in this fashion. I’ll do that after I make that work by uncommenting it and adjusting it to the new wheel-driving style.

The starting code looks like this:

function BVehicle1:update()
    [[
    local food = vec2(food.x, food.y)
    local eyeR = vec2(20,0)
    local eyeL = vec2(20,10)
    eyeR = eyeR:rotate(math.rad(self.angle)) + vec2(self.x, self.y)
    eyeL = eyeL:rotate(math.rad(self.angle)) + vec2(self.x, self.y)
    local distL = food:dist(eyeL)
    local distR = food:dist(eyeR)
    local adj
    if distL < distR then 
        adj = 0.2
    else
        adj = -0.2
    end
    self.angle = self.angle + adj
      ]]
    self.lWheel = 2.0
    self.rWheel = 2.1
    self.angle = self.angle + math.deg(self:calcAngle(self.lWheel, self.rWheel))
    local step = vec2((self.lWheel+self.rWheel)/2,0)
    local move = step:rotate(math.rad(self.angle))
    self.x = self.x + move.x
    self.y = self.y + move.y
    if self.x > 1000 then self.x = 100; self.y = 100 end
end

All that commented out bit is the old form of the food seeker. I’ll extract it now as a method, even though I think I’m heading in a different direction, because I think it’ll be a smaller step.

Already there’s a problem. I’m storing the wheel speeds in two separate scalars, lWheel and ‘rWheel. A Behavior needs to return two values, one for each wheel, so either I can return two temps and use them, or return a vec2`. Let’s just return two temps for now. Here’s what I came up with:


function BVehicle1:seekFood()
    local lw = 0
    local rw = 0
    local food = vec2(food.x, food.y)
    local eyeR = vec2(20,0)
    local eyeL = vec2(20,10)
    eyeR = eyeR:rotate(math.rad(self.angle)) + vec2(self.x, self.y)
    eyeL = eyeL:rotate(math.rad(self.angle)) + vec2(self.x, self.y)
    local distL = food:dist(eyeL)
    local distR = food:dist(eyeR)
    local adj
    if distR > distL then 
        rw = 1
    else
        lw = 1
    end
    return lw, rw
end

function BVehicle1:update()
    local l
    local r
    self.lWheel = math.random()*4 - 2
    self.rWheel = math.random()*4 - 2
    l, r = self:seekFood()
    self.lWheel = self.lWheel + l
    self.rWheel = self.rWheel + r
    self.angle = self.angle + math.deg(self:calcAngle(self.lWheel, self.rWheel))
    local step = vec2((self.lWheel+self.rWheel)/2,0)
    local move = step:rotate(math.rad(self.angle))
    self.x = self.x + move.x
    self.y = self.y + move.y
    if self.x > 1000 then self.x = 100; self.y = 100 end
end

So what’s going on here? I initialize the wheels to a random float between -2 and 2. (Lua’s math.random returns integer values only if you give it max and min. Odd.) Then I call the seekFood method, which returns one for the speed to add to the wheel furthest from the food, and zero for the other. That causes us to turn a bit toward the food. Now our basic motion is biased a bit toward the food, we calculate the angle we’ll turn to, and reposition the vehicle by setting our x and y appropriately. (We’re just updating now, the drawing takes place later, in the draw method.)

The result of this is that the vehicle wanders in a haphazard fashion, but tends toward the food. Here’s a video.

Vehicle Circle Movie

What’s next?

I’m tempted to play with the food game a bit, maybe to make the food get consumed and disappear, maybe to reappear somewhere else, so our little bug wanders around to a new location. We could play with the basic speed, add more randomness or more consistency. Right now, it changes direction randomly on every tick. If we changed it to update every few clicks instead, the bug might appear more purposeful. That might be fun. It would also be another behavior to extract, so there’d be some learning.

There’s also some cleanup that we might do. This code bugs me, for example, no pun intended:

function BVehicle1:update()
    local l = 0
    local r = 0
    self.lWheel = 0.5 + math.random()*4 - 2
    self.rWheel = 0.5 + math.random()*4 - 2
    l, r = self:seekFood()
    self.lWheel = self.lWheel + l
    self.rWheel = self.rWheel + r
    self.angle = self.angle + math.deg(self:calcAngle(self.lWheel, self.rWheel))
    local step = vec2((self.lWheel+self.rWheel)/2,0)
    local move = step:rotate(math.rad(self.angle))
    self.x = self.x + move.x
    self.y = self.y + move.y
    if self.x > 1000 then self.x = 100; self.y = 100 end
end

Notice all this “do it to x, then do it to y” stuff. That’s a clear call for some kind of two-element “coordinate” kind of object. And Codea does have a vector type, vec2, just right for this sort of thing. So I’m inclined to clean that up.

We’re often faced with this kind of dilemma. We want more features, and the code is a bit crufty, so we have to decide between cleaning up the kitchen and getting lunch made. I believe that most of the time, in our business, the features win. The business people get to say what we work on, and their focus is on tangible new features. Because of that, my usual advice is to clean things up as one goes, rather than either of the alternatives of stopping everything to clean, or just grinding through a dirtier and dirtier program. Naturally, even then, it’s best, from moment to moment, to focus on one aspect. Right now, we’re cleaning; now we’re adding capability; now we’re cleaning again; and so on.

Here, these little episodes are about 20 minutes long. Frankly, if I were working on something much larger, I’d shoot for this same cycle: features for a few minutes, clean for a few, rinse, repeat. I think it always makes sense to work that way, and I offer the idea to you to try if you feel like it. Let me know what you find out, if you wish.

But do keep in mind: these clean up the code digressions take ages to write up and a while to read, but each cleanup is only a few minutes in duration, and the code gets easier to improve each time.

Anyway, I’m going to convert to vec2 and then press on. Here goes:

function BVehicle1:update()
    local wheel = vec2(0.5 + math.random()*4 - 2, 0.5 + math.random()*4 - 2)
    wheel = wheel + self:seekFood()
    self.angle = self.angle + math.deg(self:calcAngle(wheel.x, wheel.y))
    local step = vec2((wheel.x+wheel.y)/2, 0)
    local move = step:rotate(math.rad(self.angle))
    self.position = self.position + move
     if self.x > 1000 then self.x = 100; self.y = 100 end
end

This is kind of a big bite. I’ve decided to use a local vec2 for the current motion of the wheels, and then i also decided to remove the x and y of the vehicle and replace with a vec2 position. It’s not a good idea to do both at once, and the latter requires changes in our object init and in draw, so I’m likely to mess this up. And there’s another issue: arguably, the wheel variable isn’t a vector at all. It’s the step, respectively taken by the left wheel and the right wheel. The step variable has a bit more claim to really being a vector. So we may wind up wishing we had a separate class for the wheel rotations but right now I don’t see it. Feel free to call me on it when we get in trouble later.

Anyway, now I must forge ahead, removing x and y from the vehicle.

I should have said “Watch this”, or “Hold my beer”. It compiles, and runs, and doesn’t work. Here’s the whole vehicle for the record:

BVehicle1 = class()

function BVehicle1:init(x,y, angle)
    self.position = vec2(x,y)
    self.angle = angle or 0
    self.width = 10
end

function BVehicle1:calcAngle(d1, d2)
    return math.atan(d2-d1, self.width)
end

function BVehicle1:seekFood()
    local lw = 0
    local rw = 0
    local food = vec2(food.x, food.y)
    local eyeR = vec2(20,0)
    local eyeL = vec2(20,10)
    eyeR = eyeR:rotate(math.rad(self.angle)) + vec2(self.x, self.y)
    eyeL = eyeL:rotate(math.rad(self.angle)) + vec2(self.x, self.y)
    local distL = food:dist(eyeL)
    local distR = food:dist(eyeR)
    local adj
    if distR > distL then 
        rw = 1
    else
        lw = 1
    end
    return vec2(lw, rw)
end

function BVehicle1:update()
    local wheel = vec2(0.5 + math.random()*4 - 2, 0.5 + math.random()*4 - 2)
    wheel = wheel + self:seekFood()
    self.angle = self.angle + math.deg(self:calcAngle(wheel.x, wheel.y))
    local step = vec2((wheel.x+wheel.y)/2, 0)
    local move = step:rotate(math.rad(self.angle))
    self.position = self.position + move
     if self.x > 1000 then self.x = 100; self.y = 100 end
end

function BVehicle1:draw()
    pushStyle()
    pushMatrix()
    rectMode(CENTER)
    stroke(255,255,0)
    fill(255,255,0)
    translate(self.position:unpack())  changed
    rotate(self.angle)
    rect(0, 0, 20, 10)
     local s = string.format("%d %d", math.floor(self.x), math.floor(self.y))
     text(s,0, 30)
    local eyeSize = 8
    local eyeR = vec2(10,-5)  changed
    local eyeL = vec2(10,5)   changed
    stroke(255,0,0)
    fill(255,0,0)
    ellipse(eyeR.x, eyeR.y, eyeSize)
    ellipse(eyeL.x, eyeL.y, eyeSize)
    popMatrix()
    popStyle()
end

What happens is that our guy wanders as before, but he isn’t heading toward the food. He is, however, heading in a consistent direction, just not the right one. I’m tempted to debug it. What I should do, since it was just a couple of minutes’ work, is revert and do it again. Summoning great will power, I revert. Now I have to do it again, in smaller steps. In particular, let’s just make incremental changes inside update, then move outside. Here goes. Hold onto my beer this time.

Failed again! I won’t trouble you with the code but I just changed a few lines. Still too many, I guess. Revert again, then back in.

OK. First thing, this time, I just changed the member variables self.lWheel and self.rWheel to be local. That works and there should be no references to the member variables now, so I’ll check that and remove them. That’s still good. Here’s the code, which I’ll commit forthwith.

BVehicle1 = class()

function BVehicle1:init(x,y, angle)
    self.x = x
    self.y = y
    self.angle = angle or 0
    self.width = 10
end

function BVehicle1:calcAngle(d1, d2)
    return math.atan(d2-d1, self.width)
end

function BVehicle1:seekFood()
    local lw = 0
    local rw = 0
    local food = vec2(food.x, food.y)
    local eyeR = vec2(20,0)
    local eyeL = vec2(20,10)
    eyeR = eyeR:rotate(math.rad(self.angle)) + vec2(self.x, self.y)
    eyeL = eyeL:rotate(math.rad(self.angle)) + vec2(self.x, self.y)
    local distL = food:dist(eyeL)
    local distR = food:dist(eyeR)
    local adj
    if distR > distL then 
        rw = 1
    else
        lw = 1
    end
    return lw, rw
end

function BVehicle1:update()
    local l = 0
    local r = 0
    local lWheel = 0.5 + math.random()*4 - 2
    local rWheel = 0.5 + math.random()*4 - 2
    l, r = self:seekFood()
    lWheel = lWheel + l
    rWheel = rWheel + r
    self.angle = self.angle + math.deg(self:calcAngle(lWheel, rWheel))
    local step = vec2((lWheel+rWheel)/2,0)
    local move = step:rotate(math.rad(self.angle))
    self.x = self.x + move.x
    self.y = self.y + move.y
    if self.x > 1000 then self.x = 100; self.y = 100 end
end

function BVehicle1:draw()
    pushStyle()
    pushMatrix()
    rectMode(CENTER)
    stroke(255,255,0)
    fill(255,255,0)
    translate(self.x, self.y)
    rotate(self.angle)
    rect(0, 0, 20, 10)
     local s = string.format("%d %d", math.floor(self.x), math.floor(self.y))
     text(s,0, 30)
    local eyeSize = 8
    local eyeR = vec2(10,-5)  changed
    local eyeL = vec2(10,5)   changed
    stroke(255,0,0)
    fill(255,0,0)
    ellipse(eyeR.x, eyeR.y, eyeSize)
    ellipse(eyeL.x, eyeL.y, eyeSize)
    popMatrix()
    popStyle()
end

Nothing much to see here, just that there’s no reference to the wheel member variables and they are local in the update method. Next step, I think, will be to find a minimal place to insert a vector usage. But first, a break. Well, an underlining, then a break.

Underlining

Ever tried. Ever failed. No matter. Try Again. Fail again. Fail better.
– Samuel Beckett

When you’ve just worked for a few minutes, and done a bit more than your brain was up for, and messed it up, it’s tempting to debug. I’d like to suggest that you try another approach. Just revert your code, checking out your current master, and try again.

This is easy to do when it’s just a few minutes’ work. It’s harder when you’ve worked for an hour, harder still if you’ve worked for a day. Start small: do a few minutes and when you fail, as sooner or later you surely will, just revert and do again.

And think about doing it at a larger time scale. Try it once in a while: I think you’ll be glad you did.

Brand new day

Ok, I’m going back to the vehicle and pushing it more toward using vectors. Remember, it took me two reverts to get a start that worked. Each time I took a smaller step, until things didn’t break. Let’s see what’s next.

function BVehicle1:update()
    local wheels = vec2(0.5, 0.5)
    wheels = wheels + vec2(math.random()*4 - 2, math.random()*4 -2)
    local lr = vec2(self:seekFood())
    wheels = wheels + lr
    self.angle = self.angle + math.deg(self:calcAngle(wheels.x, wheels.y))
    local step = vec2((wheels.x+wheels.y)/2,0)
    local move = step:rotate(math.rad(self.angle))
    self.x = self.x + move.x
    self.y = self.y + move.y
    if self.x > 1000 then self.x = 100; self.y = 100 end
end

I turned rWheel, lWheel into wheels, and the locals l and r into lr. This works, yay! Now there are those places where we pull wheels apart. I’ll commit and try that.

function BVehicle1:update()
    local wheels = vec2(0.5, 0.5)
    wheels = wheels + vec2(math.random()*4 - 2, math.random()*4 -2)
    local lr = vec2(self:seekFood())
    wheels = wheels + lr
    self.angle = self.angle + math.deg(self:calcAngle(wheels)) -- changed
    local step = vec2((wheels.x+wheels.y)/2,0)
    local move = step:rotate(math.rad(self.angle))
    self.x = self.x + move.x
    self.y = self.y + move.y
    if self.x > 1000 then self.x = 100; self.y = 100 end
end


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

Here, I just changed calcAngle to expect a vector. Note that this is a bit odd, because what we have is what we think of as wheels settings. However, I argue that the numbers we have are distances, so, it is a vector, formally speaking. Still, I find it awkward and the argument unconvincing. I think there’s a tiny object waiting to be born here. Still, it’s nicely encapsulated. We’ll press on, after committing.


BVehicle1 = class()

function BVehicle1:init(x,y, angle)
    self.position = vec2(x,y) -- changed
    self.angle = angle or 0
    self.width = 10
end

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

function BVehicle1:update()
    local wheels = vec2(0.5, 0.5)
    wheels = wheels + vec2(math.random()*4 - 2, math.random()*4 -2)
    local lr = vec2(self:seekFood())
    wheels = wheels + lr
    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 -- changed
    if self.position.x > 1000 then self.x = 100; self.y = 100 end
end

function BVehicle1:draw()
    pushStyle()
    pushMatrix()
    rectMode(CENTER)
    stroke(255,255,0)
    fill(255,255,0)
    translate(self.position:unpack()) -- changed
    rotate(self.angle)
    rect(0, 0, 20, 10)
    local eyeSize = 8
    local eyeR = vec2(10,-5)
    local eyeL = vec2(10,5) 
    stroke(255,0,0)
    fill(255,0,0)
    ellipse(eyeR.x, eyeR.y, eyeSize)
    ellipse(eyeL.x, eyeL.y, eyeSize)
    popMatrix()
    popStyle(d)
end

This change required us to remove all references to x and y and replace them with references to position. It didn’t go as smoothly as I’d like, but I just reverted and did it more orderly and it’s good now.

Everything works again, as before. Time to close out this article. Remember what we said about reverting when things go wrong: it’s a useful trick to have in the bag, and frankly, I think I should use it more often myself.

Let’s commit and I’ll see you in article number 4.