Be sure to read and think about the notes on Gross Hackery at the bottom, after you’ve read from here to there.
Our little vehicle has a fixed direction angle of 45 degrees. That’s not very clever. I’d like to see if we can make him move in a circle, by changing his angle of rotation on each move. Seems to me if you change your angle by a constant, you’ll go in a circle. Let’s try it.
First, I add a new member variable, angle:
function BVehicle1:init(x,y, angle) self.x = x self.y = y self.angle = angle or 0 end
We’ll set it to the angle provided in creation, if one is provided, otherwise zero.
Now if we were TDDing right now, we’d have a test in place for our 45 degree guy. We don’t, so I plan just to change the setup and update and so on to get my new effect. First step will be to create our vehicle with 45 and change update and draw to use it:
function setup() print("Braitenberg Vehicle Experiment") vehicle = BVehicle1(100,100, 45) end function BVehicle1:update() local step = vec2(1,0) local move = step:rotate(math.rad(self.angle)) — refer to 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() stroke(255,255,0) fill(255,255,0) pushMatrix() translate(self.x, self.y) rotate(self.angle) — refer to angle rect(0, 0, 20, 10) local s = string.format("%d %d", math.floor(self.x), math.floor(self.y)) text(s,0, 30) popMatrix() end
That works a treat. Commit the code.
Now I’ll change things about. I’ll init the guy closer to center, angle zero, and in update, increment the angle by a degree every time through.
function setup() print("Braitenberg Vehicle Experiment") vehicle = BVehicle1(500,100, 0) end function BVehicle1:update() self.angle = self.angle + 1 local step = vec2(1,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
This works quite well. I’ll save a movie for you. Netting it out, the vehicle starts at the lower middle of the screen, and makes a circle of diameter about 115, starting from there.
If I change the increment of the angle to 0.2 degrees per tick, the diameter of the circle is about 570. Hm. 5*115 = 575. So that’s pretty reasonable. Increment one-fifth as slowly, go in a five times bigger circle. Probably something about circumference = PI times diameter, do you suppose?
Time to commit again.
A bit of musing
OK, what shall we do next? Braitenberg vehicles have sensors, and the sensors are “wired” to the wheels through various mechanisms, and that makes the vehicles wander around. Braitenberg’s first vehicle was a light-seeker, if I recall. Thinking ahead, I have a more ambitious plan.
Suppose our vehicle is hungry and that there are food centers on the land, and that he can see them or somehow sense them. He goes to the food center. If a vehicle (seems more like a bug to me now) is at a food center, the food there declines. As food declines in the area, our bug will be less inclined to stay there, and more inclined to look for other food.
Now, I’m a bit worried. Suppose the bug is facing more or less toward the food. Then he sees it with each of his two eyes (maybe I didn’t mention that he has two eyes but he will) and he turns a bit in the direction of the one that sees the food better, that is, the one closer to the food. OK, that should make him navigate to the food. I’m worried about what will happen if the food is behind him. We’ll burn that bridge when we come to it. First thing: plant the bug somewhere at an angle to the food, and plant some food. Adjust the bug’s behavior to make it travel toward the food. I think I’ll start him going across from 100,100 again, and put the food up toward the top left corner. Here goes:
Food = class() function Food:init(x,y) — you can accept and set parameters here self.x = x self.y = y end function Food:update() end function Food:draw() pushStyle() pushMatrix() stroke(0,0,255) fill(0,0,255) translate(self.x, self.y) ellipse(0,0,20,20) popMatrix() popStyle() end — Br1 function setup() print("Braitenberg Vehicle Experiment") vehicle = BVehicle1(100,100, 0) food = Food(900,900) end — This function gets called once every frame function draw() food:update() vehicle:update() — This sets a dark background color background(40, 40, 50) — Draw the playground strokeWidth(5) stroke(255,0,0) fill(0,0,0,0) rect(0,0, 1091, 1024) stroke(255,255,255) fill(50,50,50) rect(0,0,1000,1000) — draw the world food:draw() vehicle:draw() end BVehicle1 = class() function BVehicle1:init(x,y, angle) self.x = x self.y = y self.angle = angle or 0 end function BVehicle1:update() — self.angle = self.angle + 0.2 local step = vec2(1,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() 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) popMatrix() popStyle() end
I’ve included all the code here, as the changes needed were tiny but spread around. Note especially the push and pop of style and matrix. These are needed around everyone’s draw, to preserve style (stroke and fill colors and the like) and transformations (translate and rotate and the like). They were not in place before.
(We should also note the duplication: every class is going to need to do these. And I already know it’s easy to forget them. Possibly we need to rig a way to push them up to a higher level, no pun intended. But that’s for later. Right now, I want my bug to sense the food and turn toward it.
Right now, only the setup knows where the food is. It’s kind of representing the world, and it holds our bug and food. Probably we need some world object that setup holds, and that is somehow accessible to our vehicles.
Except … the
food variable is actually global. We didn’t declare it to be local, so it’s global. That leaves us the opportunity to hack our bug to just ask where the food is and then figure out what to do. That hack shouldn’t last, but it keeps us on the current learning path rather than shaving the World yak right now.
It seems to sort of work, according to this movie, but I’m honestly not sure it’s the right one. No matter, because we’re going to go right ahead and improve things anyway.
I am thinking that the bug has binocular vision. Let’s imagine that it has eyes at the corners of its front end. It’s a rectangle from 0,0 to 20,10, so its eyes are at 20,0 and 20,10 relative to the bug. And those points have to rotate with the bug. In a TDD frame of mind, I might set up a test for eye position. Instead, I think I’ll draw them as small red dots at the corners. And that works, as you can see in this picture.
Here’s how I did it:
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(20,0) local eyeL = vec2(20,10) stroke(255,0,0) fill(255,0,0) ellipse(eyeR.x, eyeR.y, eyeSize) ellipse(eyeL.x, eyeL.y, eyeSize) popMatrix() popStyle() end
The drawn eyes stay in the right place on the bug as it moves and rotates. We’ll need to calculate their world positions, as we do the world position of the bug. Then, my cunning plan is to compute the distance from each eye to the food and turn toward the closer one. We’ll do that before updating the bug position, in update. Here’s a try. (But first a commit.)
Here’s my first cut at distance from eyes to food:
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 local step = vec2(1,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
Pretty straightforward if not pretty. The tricky bit is that the relative eye positions are rotated according to the current angle, then made global by adding our actual position,
vec2(self.x, self.y). Then we get the two distances and use them to set an angle adjustment. And it works (after I realized I needed to globalize those local values). Here’s a movie of the bug moving toward the food and finally getting there.
Where are we? Well, I think we have a decent little prototype vehicle / bug. It knows how to travel and to adjust its direction toward food. If we had cleverly moved the food in the middle of things, it would have adjusted its direction again. (Fact is, it’s adjusting its direction continually, wandering in very narrow arcs toward the target.
It’s doing everything we set out to do. We could improve what it looks like, or change its behavior around. We know where everything is and it’s in roughly the right place.
However … it’s not very clean. Our next move should be to clean up the code, look for duplication and remove it, move things into separate methods or otherwise push them toward a better design.
And when we do that, I bet I’m going to wish that I had some automated tests to b sure things are turning out right.
I have one more concern. I think the default mode for drawing a rectangle (0,0,20,10) draws it starting at zero. That probably means it’s rotating around its right rear corner. In fact, I just verified that visually. We really ought to make our vehicle behave as if it’s centered on its coordinates, and draw it that way. There is a function
rectMode to set the drawing origin to the center, and you can also change whether the width and height are considered as full, or half size (radius vs diameter, if you will). If we make that change, and I think we should, we’ll also need to adjust where the eyes are. That’ll need to be far better parameterized than it is now. Maybe the eyes (sensors) should be separate little objects that the bug “has”. We’ll see. We’re evolving here and so far evolution is proceeding well.
All that’s for another day.
One more thing … gross hackery?
As I read these first two articles, preparing to publish them, I can imagine someone looking at what’s going on and thinking that it’s nothing but gross hackery and experimentation, writing tomorrow’s legacy code today.
Well, if we continued just like this for very long, you’d be right. This program would get uglier and uglier, and it would get harder and harder to make progress. But the point of my programming articles – and they do have one – is always to show how, as we experiment and learn, the code gets a bit worse, while getting more capable, and then how, with some simple improvements, usually pulling existing code into new structures, objects or methods, the code and design get better.
This enables us to make steady progress, instead of the slower and slower progress we often find in the Legacy Code Mines of Ohio.
As you read these articles, hold me to this prediction. Watch as I discover things needing improvement. Observe how easy or hard they are to improve. Then make your own decision on where to set your own needle on the dial from up-front to as-you-go design. I hope you’ll keep pushing toward as-you-go, but do be careful. No one likes to work in cruddy code. Well, at least I don’t!