Denizen Herding Behavior
This article is by Russell Ackerman. I thought I would share my success at getting my deer to herd in my roguelike, I did it by doing this: This article relates to the LUA programming language Sound propogates out from a square, dropping informatino about itself. The function is as follows:
function funcs.makesound(intensity,activator,x,y,z) -- print("Making a sound .."..intensity.." from "..activator["name"].." X "..x.." Y "..y.." Z "..z) local objectloc = data.objectloc local working, future = {}, Template:X,y,z local soundscape = data.soundscape local finished = {} local insert = table.insert local tiles = data.tiles local checkforedgeofmap = funcs.checkforedgeofmap finished[x] = {} finished[x][y] = {} finished[x][y][z] = 1 soundscape[x][y][z][activator] = soundscape[x][y][z][activator] or {} --for moveenemytowards insert(soundscape[x][y][z][activator],{["strength"] = intensity}) local counter = intensity while(counter <= intensity and counter >= 1) do counter = counter - 1 working,future = future, {} local x,y,z for k,v in pairs(working) do for x = v[1]- 1,v[1] + 1 do finished[x] = finished[x] or {} for y = v[2] -1,v[2] + 1 do finished[x][y] = finished[x][y] or {} for z = v[3] - 1,v[3] + 1 do if z < data.amountofzlevels and z >= 1 and (not checkforedgeofmap(x,y)) and (not finished[x][y][z]) and (not tiles[x][y][z]["filled"]) and (not tiles[x][y][z]["open"]) then finished[x][y][z] = 1 insert(future,{x,y,z}) soundscape[x][y][z][activator] = soundscape[x][y][z][activator] or {} --for moveenemytowards insert(soundscape[x][y][z][activator],{["strength"] = counter}) --the following visualizes the sound data. -- slang.gotorc(y+data.mapoffsety,x+data.mapoffsetx) -- slang.setcolor(counter) -- slang.writechar("*") -- slang.refresh() end end end end end
end end
Data about the "soundscape" is stored, for now, just with the data of who made the sound and how intensly it was heard at the square. soundscape[x][y][z][the object that activated it][irreleveant key][strengthofsound] and any other pertinent variables about the noise would also be stored there.
To use this data in pathfinding to get your denizens to herd, simply do the following:
1/x times that the creature activates pathfinding to choose a square with the shortest path, simply weight the RELATIVE VALUE of those squares. Squares with more noise generated by creatures who are the same species as me should be weighted with an amount relative to the total noise on that square from those animals except myself. Its a simple matter of using a function like "total_sound_at_square_except_me(me,x,y,z).
My code for moving the denizens is at follows. the table "Distances" is a table of data in the form distances[x][y][z] = amount, where distances[x][y][z] is the distance of a particular square to the destination square. It's a little hokey but it's pretty simple too:
function funcs.totalsoundfromob(ob,x,y,z)
--print("Totaling sound")
local soundscape = data.soundscape
local totalstr = 0
for k2,v2 in pairs(soundscape[x][y][z][ob]) do
totalstr = totalstr + v2["strength"]
end
return totalstr
end
function funcs.totalsoundatspot(ob,x,y,z) --totals sounds at spot from "my species" local soundscape = data.soundscape local sound local totalstrength = 0 local numfriends = 0 for k,v in pairs(soundscape[x][y][z]) do if ob["species"] == k["species"] and k ~= ob and (not k["isplayer"]) then --not k isplayer for sanity purposes. sound = funcs.totalsoundfromob(k,x,y,z) totalstrength = totalstrength + sound numfriends = numfriends + 1 end end -- print("total strength "..totalstrength) return totalstrength, numfriends end
--ob contains ob.x and ob.y and ob.z, targ contains targ.x,y,z etc. function funcs.moveenemytowards(ob,targ) -- print("Moving "..ob.name) -- if targ then print("Towards"..targ.x.." "..targ.y.." "..targ.z.." from "..ob.x.." "..ob.y.." "..ob.z) end local numfriends local soundonhomesquare, numfriends = funcs.totalsoundatspot(ob,ob.x,ob.y,ob.z) local distances -- if not targ and ob.activateherding and soundonhomesquare < 0 then --if i dont hear anything and im supposed to be moving randomly if targ and targ.name then print("Targ name "..targ.name) end -- print("Starting pathfinding") if targ then funcs.pathfindfromto(ob,targ) distances = ob.distances end --gets a path... checks for a new path every so often. -- if not targ then distances = nil end -- print("Finished pathfinding") local soundscape = data.soundscape local totalsound local oldx,oldy,oldz = ob.x,ob.y,ob.z --print("MOVING A MONSTER!!") local x local y local z local lowestdistance = {distance = 1000000 ,x=oldx,y=oldy,z=oldz} --HARDCODED LIMIT local iterations = 0
local movementintelligence if ob.movementintelligence then movementintelligence = ob.movementintelligence else movementintelligence = 20 end
while(iterations < movementintelligence) do iterations = iterations + 1 x = math.random(ob["x"]-1,ob["x"]+1) y = math.random(ob["y"]-1,ob["y"]+1) z = math.random(ob["z"]-1,ob["z"]+1) -- for x = ob["x"] -1,ob["x"] + 1 do iterating in this pattern makes us select the upper left corner... -- for y = ob["y"] -1,ob["y"] + 1 do -- for z = ob["z"] -1,ob["z"] + 1 do if ((not targ) and (not funcs.checkforblockpassageofpath(x,y,z))) or (targ) then local relativevalue = 0 if ob.activateherding then totalsound = funcs.totalsoundatspot(ob,x,y,z) end if targ and not distances then return end --at destination already if ((not targ) or (distances and distances[x] and distances[x][y] and distances[x][y][z])) and not funcs.checkforedgeofmap(x,y) then --was and distances[x][y][z] if targ then relativevalue = distances[x][y][z] + relativevalue end --print("SOUND "..soundonhomesquare) local soundvariable local usesoundvariable = true if ob.tightgroups then soundvariable = ob.prefersoundlevel * numfriends end if ob.loosegroups then soundvariable = ob.prefersoundlevel end if ob.prefersoundlevel == 0 then usesoundvariable = false end
if (not targ) and ob.activateherding and soundonhomesquare == 0 then --if i dont hear anything and have no target, choose a random square. relativevalue = relativevalue - (math.random(1,1000) * 10) elseif ob.activateherding and (usesoundvariable and (soundonhomesquare < (soundvariable)) or not usesoundvariable) and soundonhomesquare > 0 then --if sound is lower than threshhold, herd HALF the time.. if math.random(1,ob.herdingtendency) == 1 then relativevalue = relativevalue - (totalsound * 10) end --herd according to a fraction of times ob.herdingtendency should be 10 or 20 or 5 or whatever. If im beyond the sound range by not herding, i wont herd from that point until i hear more sound.. elseif (not targ) then --if i dont care about sound, move randomly. relativevalue = relativevalue - (math.random(1,1000) * 10) --was + end if relativevalue < lowestdistance["distance"] then --go towards high strength sound lowestdistance = {["distance"] = relativevalue,["x"]=x,["y"]=y,["z"]=z} end end end --ends if not targ... -- end -- end -- end end --ends iterations over intelligence... funcs.moveobject(ob,lowestdistance["x"],lowestdistance["y"],lowestdistance["z"]) -- funcs.drawspotsimple(oldx,oldy,oldz,nil,ob) end
--so as you can see the "relative value" of the square gets changed depending on how much sound is detected nearby. Hope this helps someone! Check out my game Ascii Wilderness, which is open source LUA. http://asciiwilderness.blogspot.com/p/ascii-wilderness.html In this game, the deer properly herd together based on their internal variables. This code also depends on some internal herding variables from the participants such as "activateherding = 1" and "tightgroups" or "loosegroups" = 1 and "prefersoundlevel" = amount and also "herdingtendency" = 3- 10
Using this code, my ghouls will wait up for other nearby ghouls before closing in for the attack. Sweet!