Braitenberg Vehicles - 10
Some personal setbacks have, well, set me back. I think today I’ll play with food and eating. Back to 3D later.
The current behaviors include food seeking:
function BVehicle1:behaveSeekFood()
local food = vec2(food.x, food.y)
local distL = food:dist(self.eyeL)
local distR = food:dist(self.eyeR)
if distR > distL then
return vec2(0,1)
else
return vec2(1,0)
end
end
Here’s how food works now:
function setup()
...
food = Food(300,900)
...
end
function Food:update()
self.count = self.count - 1
if self.count <= 0 then
self.count = 1000
if self.y > 500 then self.y = self.y - 500 else self.y = self.y + 500 end
end
end
So after 500 ticks of being wherever it started, the food moves its y coordinate up or down by 500. This leads to the food-seeking bug running up to the food, circling around (supposedly eating) and then when the food vanishes, running to its new location.
There’s a lot not to like about that, not least that there is but one food, and it’s a global and its name doesn’t even start with a capital letter. What kind of coding standard is this, anyway? I’m only semi-serious. The whole idea here is to move as gracefully as we can, and to recognize bits of less graceful code and improve them. So we’ll put that on the list.
What can we say about food?
- There should probably be, in principle at least, multiple food sources.
- The bugs should probably only seek food when they are hungry, and otherwise behave otherwise.
- When bugs find food, they should consume it: the food should go away.
- Food should spring up, or perhaps grow up, more or less randomly.
This gives me these quick design thoughts:
- There should be a list of food locations;
- Maybe the bugs can only sense the nearest food;
- When the bugs are near enough to the food, its quantity goes down until it disappears;
- When the bugs aren’t near food, their energy goes down. When it’s low enough, they seek food;
- When the bugs are near the food, they tank up.
I think I’ll start with bug energy. I’ll give them a random amount to start with, and tick it down with each update. I’ll give it some minimum above which the food behavior has no effect. That should change the visible behavior, with the hungry bug wandering a while before he hunts for food.
The code I’ll show you in a moment worked first time. But I noticed that since the food seeker is the same color as the others, I couldn’t tell which one he was until he got hungry. So I’ll change his color. Two changes at once? Risky. I think I’ll commit first.
We have a fair number of changes now. Let’s look at some diffs:
It’s a fairly common Lua trick to put optional arguments at the end of parameter lists, and I’ve done that here with hue
. But the list for creating the vehicles (bugs) is getting complicated, so that needs to go on the list of things to improve. Nonetheless, now the hungry guy is blue with a yellow border, and he wanders freely until he gets hungry, and then chases food. Watch:
He never gets satiated once he gets hungry. Let’s fix that. We’ll have him add energy when he’s “close enough” to food. That should probably be a new behavior, maybe called something like “eat”.
bSeekFood = {}
table.insert(bSeekFood, BVehicle1:partial(BVehicle1.behaveDefaultMotion, 1, 1.1))
table.insert(bSeekFood, BVehicle1:partial(BVehicle1.behaveJitter, 0.25))
table.insert(bSeekFood, BVehicle1:partial(BVehicle1.behaveSeekFood))
table.insert(bEat, BVehicle1:partial(BVehicle1.behaveEat))
table.insert(bAvoidWalls, BVehicle1:partial(BVehicle1.behaveAvoidWall))
That’s my intention. Notice that I also added wall avoidance, since this new mode means he could in principle walk through the walls and we don’t want that. Now I have to fulfill the intention by writing the behavior:
Arrgh, this has not gone well. First, I discovered that my setup was referring to some mistaken tables and should be:
bSeekFood = {}
table.insert(bSeekFood, BVehicle1:partial(BVehicle1.behaveDefaultMotion, 1, 1.1))
table.insert(bSeekFood, BVehicle1:partial(BVehicle1.behaveJitter, 0.25))
table.insert(bSeekFood, BVehicle1:partial(BVehicle1.behaveSeekFood))
--table.insert(bSeekFood, BVehicle1:partial(BVehicle1.behaveEat))
table.insert(bSeekFood, BVehicle1:partial(BVehicle1.behaveAvoidWall))
Then my implementation of behaveEat
has caused an error.
function BVehicle1:behaveEat()
local food = vec2(food.x, food.y)
if food:dist(self.position) < 50 then self.energy = self.energy + 50 end
end
I see the problem, I think. (If I don’t, I may be honor-bound to revert. But I think the convention for all behaviors is that they return an adjustment to the wheels, so first let’s return a zero vector and see what happens.
function BVehicle1:behaveEat()
local food = vec2(food.x, food.y)
if food:dist(self.position) < 50 then self.energy = self.energy + 50 end
return vec2(0,0)
end
Well, that gets things running again but the bug seems to stay hungry all the time. And that kind of makes sense. His hunger threshold is 100, and he adds 50 energy when he’s close enough. Meanwhile he’s ticking down toward zero. If I’m not completely off base, he’ll get energy up to 150 and then start to wander. But 50 ticks is only a second, so he won’t say full long at all.
I’ll check that by adding 500 but that won’t do for a final answer. No, he still stays hungry. I think I’ve got to debug. I’ll display his energy level on draw.
This is distressing. It’s working now, and I’m not sure why it wasn’t … or even that it wasn’t. I had commented out the behaveEat
and debugged fruitlessly for a while without it. Anyway, it’s working now. I’ll talk about my learning after we view the video and the code. Notice in this new video that the blue bug wanders freely, then gets hungry and runs for food, then quickly fills up again and wanders freely:
So we see that he’s toggling between seeking food and wandering. It seems not to take long enough for him to tank up, so the values at least need tweaking. We’re not depleting the food, but clearly in the behaveEat
is the place to do it: if we get food, food should be depleted. “Rang bell, dog ate food”. So that can wait for the next behavior improvement. Here are the important code bits:
-- setup
bSeekFood = {}
table.insert(bSeekFood, BVehicle1:partial(BVehicle1.behaveDefaultMotion, 1, 1.1))
table.insert(bSeekFood, BVehicle1:partial(BVehicle1.behaveJitter, 0.25))
table.insert(bSeekFood, BVehicle1:partial(BVehicle1.behaveSeekFood))
table.insert(bSeekFood, BVehicle1:partial(BVehicle1.behaveEat))
table.insert(bSeekFood, BVehicle1:partial(BVehicle1.behaveAvoidWall))
-- BVehicle1
function BVehicle1:behaveEat()
local food = vec2(food.x, food.y)
if food:dist(self.position) < 50 then self.energy = self.energy + 10 end
self.energy = math.min(self.energy, 1000)
return vec2(0,0)
end
The good news is that’s all it took. The bad news is that I confused myself getting that little bit of code to work.
I’ve noticed some other things. First, the draw code is getting messy:
function BVehicle1:draw()
pushStyle()
pushMatrix()
rectMode(CENTER)
translate(self.position:unpack() )
strokeWidth(2)
stroke(255,255,0)
fill(self.hue)
self:keepOnPage()
rotate(self.angle)
rect(0, 0, 20, 10)
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
There’s a lot going on there, and it needs to be broken out, maybe into drawing body, drawing eyes, inside drawing the whole bug, and so on. Colors are all very much literals, so those probably need to be moved into some kind of more global kind of structure, maybe class-based in the BVehicle1, or maybe even a behavior. behaveColor
perhaps?
But to me, the concerning thing is that it didn’t go well. Now I know a lot of people, and I don’t know any for whom programming goes well every day. So I’ll not beat myself up over it. But I’m here to learn in public, in the hopes that you can find some nuggets of ideas that are useful to you. So, what have we learned on the show today?