Well, I whomped on the original swarm script, and here's what I came up with as a workaround for the nerfed llVolumeDetect feature. The comments are pretty raw and there's some stuff that doesn't have much to do with swarming behavior, but if I wait till I feel like prettying it up I'll never post it, so here it is hot off the griddle.
I had my swarm follow a leader to make it look like a school, since I couldn't seem to get convincing schooling behavior out of the original. The swarm objects are set up to look for a leader, and if they don't find one to rez a new one.
I made the objects swim upright (Z axis up) so they look more fishy. The parameters here are adjusted for a school of rays, as I write this they're swimming around at Klaw 243, 88, 5.
This script goes in the swarming objects:
// Swarm script
// by Bathsheba Dorn
// Modified from the script by Apotheus Silverman
//AS's comments:
// This script is my implementation of the well-known swarm algorithm
// which can be found in numerous open-source programs.
// Due to the specifics of the SL environment, I have strayed from some
// of the traditional rules slightly. Regardless, the end effect is
// indistiguishable from the original algorithm.
//BD: I started rewriting this because the llVolumeDetect function has been nerfed and
//does not work for this purpose. I ended up changing many things about it for my own purpose.
//It still uses the swarm code, but the school follows a leader rather than trying to move on its own.
//When a swarm object is rezzed or in any way finds itself without a leader, it will try to detect
//a leader (it expects a leader with the same name as itself but a funny orientation) and if it can't find one
//it will rez one.
//I also changed it so the objects will die if they leave the land of whosever land they were rezzed on,
//and so that they swim upright (z axis up).
// Configurable parameters
// Determines whether or not to enable STATUS_SANDBOX.
integer sandbox = FALSE;
// Timer length
float timer_length = 0.5;
// Die after this many seconds
integer kill_time = 0;
//Lower altitude limit to keep object from intersecting an above-ground floor
//if 0, use ground.
float lower_limit = 0;
//Objects are presumed to have collided when they're this close.
float collide_distance = 4;
//Object swims in X direction. How fast?
float swim_speed = 1.2;
// How much force to apply with each impulse
float force_modifier = 0.7;
// How much force to apply when repulsed by another like me
float repulse_force_modifier = 0.5;//.86
// How much friction to use on a scale from 0 to 1.
// Note that friction takes effect each timer cycle, so the lower the timer length,
// the more the friction you specify here will take effect, thereby increasing actual
// friction applied.
float friction = 0.45;//0.45;
// How much to modify the rotation strength. Higher numbers produce greater strength
// Note that if the modifier is too small, the object may not rotate at all.
float rotation_strength_modifier = .1; //2.8;
// How much to modify rotation damping. Higher numbers produce slower rotation.
float rotation_damping_modifier = 1.0; //5000000.0;
// Does this object "swim" in air or water?
// 2 = air
// 1 = water
// 0 = both
integer flight_mode = 0;
// Maximum distance from spawn point
float max_distance = 12.0;
// How far away to scan for others like me. 2 * max_distance + a bit is a good value.
float sensor_distance = 26.0;
// *** Don't change anything below unless you *really* know what you're doing ***
// Debug switch: if true object will report collisions
integer talk = FALSE;
//Remember whose land I started on, and die if I leave it.
key my_landowner;
// Key of swarm leader object
string leader_key = NULL_KEY;
//Flag: I just rezzed a leader but haven't detected it yet.
integer leader_rezzed_but_not_found = FALSE;
//how long I've been without a leader
integer no_leader = 0;
float mass;
vector spawn_location;
//Rez a new leader.
RezLeader()
{
llOwnerSay("Rezzing a new leader.");
llRezObject(llGetObjectName(), llGetPos(),ZERO_VECTOR, ZERO_ROTATION, 0);
no_leader = 0;
leader_rezzed_but_not_found = TRUE;
}
//Stay upright while turning.
//I got this from the scripty-tips forum, it points x axis at target while keeping z axis up
rotation upright_point_at(vector target)
{
vector fwd = llVecNorm(target - llGetPos());
vector left = llVecNorm(<0,0,1> % fwd);
vector up = fwd % left;
return llAxes2Rot(fwd, left, up);
}
do_rotation(vector mypos, vector myvel) {
llRotLookAt(upright_point_at(mypos + myvel),
mass * rotation_strength_modifier, mass * rotation_damping_modifier);
}
// Collision function
collide(vector loc) {
vector mypos = llGetPos();
// Apply repulse force
vector impulse = llVecNorm(mypos - loc);
llApplyImpulse(impulse * repulse_force_modifier * mass, FALSE);
if (talk) llOwnerSay("collide() - impulse " + (string)impulse + " applied.");
}
//Do collisions: this function is called whether or not the sensor finds anything
sensor_any() {
// Die after reaching kill_time
if (kill_time != 0 && llGetTime() >= kill_time) {
llDie();
}
// Get my velocity
vector myvel = llGetVel();
// Apply friction
llApplyImpulse(-(myvel * friction * mass), FALSE);
// Get my position
vector mypos = llGetPos();
//Die if off my land (this can happen if somebody shot or pushed me)
if (llGetLandOwnerAt(mypos) != my_landowner) {
llOwnerSay("Went off my parcel, I'll now die.");
llDie();
}
// Check for air/water breach
if (flight_mode == 1) {
// water
if (mypos.z >= llWater(mypos) - llVecMag(llGetScale())) {
if (talk) llOwnerSay( "collide() called due to air/water breach.");
collide(<mypos.x, mypos.y, mypos.z + 0.3>);
}
} else if (flight_mode == 2) {
// air
if (mypos.z <= llWater(mypos) + llVecMag(llGetScale())) {
if (talk) llOwnerSay( "collide() called due to air/water breach.");
collide(<mypos.x, mypos.y, mypos.z - 0.3>);
}
}
// Stay near spawn location
if (llVecDist(mypos, spawn_location) > max_distance) {
// Compensate for being near sim border
if (spawn_location.x - mypos.x > 100) {
mypos.x += 255;
}
if (talk) llOwnerSay("collide() called due to too much distance from my spawn point.");
// mypos=" + (string)mypos + ", spawn_location = " + (string)spawn_location);
collide(mypos - llVecNorm(spawn_location - mypos));
}
// Stay above ground level
if (lower_limit == 0) {
if (mypos.z <= llGround(ZERO_VECTOR)) {
if (talk) llOwnerSay("Collide called due to ground intersection.");
collide(<mypos.x,mypos.y,mypos.z - .3>);
}
} else
if (mypos.z <= lower_limit) {
if (talk) llOwnerSay("Collide called due to lower limit intersection.");
collide(<mypos.x,mypos.y,mypos.z - .3>);
}
}
default {
state_entry() {
llResetTime();
llOwnerSay("Started.");
// Sandbox
llSetStatus(STATUS_SANDBOX, sandbox);
llSetStatus(STATUS_BLOCK_GRAB, FALSE);
// spawn_location = <240,77,10>;
spawn_location = llGetPos();
//Find out what land I'm on
my_landowner = llGetLandOwnerAt(llGetPos());
// Initialize physics behavior
mass = llGetMass();
llSetStatus(STATUS_PHYSICS, TRUE);
llSetStatus(STATUS_PHANTOM, TRUE); //Note that if the script is turned off, the object will fall off-world.
llSetBuoyancy(1.0);
//BD turned off because doesn't work llVolumeDetect(TRUE);
// Initialize sensor
llSensorRepeat(llGetObjectName(), NULL_KEY, ACTIVE|SCRIPTED, sensor_distance, PI, timer_length);
}
touch_start(integer detected) {
if (llDetectedKey(0) == llGetOwner())
state stopped;
// llResetScript();
}
//If I've rezzed a new leader, capture its key.
object_rez(key id) {
leader_key = id;
llOwnerSay("Found new leader!");
leader_rezzed_but_not_found = FALSE;
}
//This is only called if leader isn't found and I'm the only swarmer.
//Therefore, if no new leader has been rezzed already, I need to rez one.
no_sensor() {
llOwnerSay("Lost leader, nothing sensed.");
if (!leader_rezzed_but_not_found) RezLeader();
sensor_any();
}
sensor(integer total_number) {
sensor_any();
// Populate neighbors with the positions of the two nearest neighbors.
vector mypos = llGetPos();
list neighbors = [];
integer i;
vector leaderpos;
integer leaderfound = FALSE;
for (i = 0; i < total_number; i++) {
vector current_pos = llDetectedPos(i);
if (llDetectedKey(i) == leader_key) {
if (talk) llOwnerSay("Found leader");
leaderpos = llDetectedPos(i);
leaderfound = TRUE;
i++;
jump Next;
}
if(llDetectedName(i) != llGetObjectName()) {
i++;
jump Next;
}
if (llGetListLength(neighbors) < 2) {
// Add to list
neighbors = llListInsertList(neighbors, [current_pos], llGetListLength(neighbors));
} else {
// Check to see if the current vector is closer than the list
// vector which is furthest away.
if (llVecDist(mypos, llList2Vector(neighbors, 0)) >
llVecDist(mypos, llList2Vector(neighbors, 1))) {
// check against first list item
if (llVecDist(mypos, llList2Vector(neighbors, 0)) >
llVecDist(mypos, current_pos)) {
llListInsertList(neighbors, [current_pos], 0);
}
} else {
// check against second list item
if (llVecDist(mypos, llList2Vector(neighbors, 1)) >
llVecDist(mypos, current_pos)) {
llListInsertList(neighbors, [current_pos], 1);
}
}
}
@Next;
}
// Process movement
//If there is a closest neighbor and it is too close, push away
//This is a proxy for the lamentable fact that llVolumeDetect no longer
//works in this context: two objects cannot mutually detect each other using it, because
//each sees the other as phantom.
if (llGetListLength(neighbors) > 1) {
vector neighbor1 = llList2Vector(neighbors,0);
if (llVecDist(mypos,neighbor1) < collide_distance) {
if (talk) llOwnerSay( "collide() called due to collision.");
collide(neighbor1);
//return;
}
}
// Apply force
vector impulse = <0,0,0>;
if (llGetListLength(neighbors) == 2) {
vector neighbor1 = llList2Vector(neighbors, 0);
vector neighbor2 = llList2Vector(neighbors, 1);
vector target = neighbor2 + ((neighbor1 - neighbor2) * 0.5);
impulse = llVecNorm(target - mypos);
}
// Follow leader
if (leaderfound) {
vector leaderTarget = llVecNorm(leaderpos - mypos);
impulse += swim_speed * leaderTarget;
}
//This keeps 3 swarmers from getting into a horizontal formation and staying there forever.
if (llFrand(1.0) > .8) {
// llOwnerSay("Vertical push");
impulse += <0,0,llFrand(4.0) - 2.0>;
}
//llOwnerSay("setforce " + (string)(impulse * force_modifier * mass));
llSetForce(impulse * force_modifier * mass, FALSE);
// Update rotation
do_rotation(llGetPos(), leaderpos - llGetPos());
//If leader wasn't found and none is pending,
//look for a swarm member with rotation 90 degrees off from normal...
if (!leaderfound && !leader_rezzed_but_not_found) {
llOwnerSay("Lost leader.");
for (i = 0; i < total_number; i++) {
vector left = llRot2Left(llDetectedRot(i));
if (left.z * RAD_TO_DEG < -10) {
leader_key = llDetectedKey(i);
no_leader = 0;
llOwnerSay("Found leader by rotation.");
}
}
no_leader++;
//if no leader could be found after looking 20 times, rez a new one.
//Do this randomly so that if a whole swarm loses its leader at once, they won't all rez newleaders
//at the same moment
if (no_leader > 20 && llFrand(1.0) > .9) RezLeader();
}
}
on_rez(integer start_param) {
llResetScript();
}
}
state stopped {
state_entry() {
llOwnerSay("Stopped.");
llSetForce(<0,0,0>,TRUE);
}
touch_start(integer detected) {
if (llDetectedKey(0) == llGetOwner())
state default;
}
}
The swarmers expect to find a leader object in their inventory, with the same name as themselves and containing a script to makes it move around so they can follow it. The leader has to exist, move, have the same name as the swarm objects, and swim on its side (this last is how the objects know which one is the leader.) I used a transparent sphere as the leader, since I don't especially want to see it.
This is the leader script of my rays:
// Swarm leader script
// by Bathsheba Dorn
// Modified from the script by Apotheus Silverman
//************
//This object is the swarm leader, all it does is swim around for other swarm objects to follow.
// Configurable parameters
// Determines whether or not to enable STATUS_SANDBOX.
integer sandbox = FALSE;
//remember whose land I started on, and die if I leave it.
key my_landowner;
// Timer length
float timer_length = 0.5;
// Die after this many seconds
integer kill_time = 0;
//Lower altitude limit to keep object from intersecting an above-ground floor
//0 = use ground
float lower_limit = 0;
//Object swims in X direction. How fast?
float swim_speed = 1.3;
// How much force to apply with each impulse
float force_modifier =1.0;
// How much force to apply when repulsed by a collision condition
float repulse_force_modifier = 4.0; //.86
// How much friction to use on a scale from 0 to 1.
// Note that friction takes effect each timer cycle, so the lower the timer length,
// the more the friction you specify here will take effect, thereby increasing actual
// friction applied.
float friction = 0.45;
// How much to modify the rotation strength. Higher numbers produce greater strength
// Note that if the modifier is too small, the object may not rotate at all.
float rotation_strength_modifier = .2; //2.8
// How much to modify rotation damping. Higher numbers produce slower rotation.
float rotation_damping_modifier = .10; //5000000.0;
// Does this object "swim" in air or water?
// 2 = air
// 1 = water
// 0 = both
integer flight_mode = 0;
// Maximum distance from spawn point
float max_distance = 12.0;
// *** Don't change anything below unless you *really* know what you're doing ***
//debug switch: if true object will report collisions
integer talk = FALSE;
float mass;
vector spawn_location;
// Update rotation function
//Swim in a particular orientation. I got this from the forum, it points x axis at target while keeping y axis down.
//Note that that's the reverse of the usual: this is so the other swarm members can recognize this object
//as their leader.
rotation upright_point_at(vector target)
{
vector fwd = llVecNorm(target - llGetPos());
vector left = llVecNorm(<0,0,1> % fwd);
vector up = fwd % left;
return llAxes2Rot(fwd, -up, left);
}
do_rotation(vector mypos, vector myvel) {
llRotLookAt(upright_point_at(mypos + myvel),
mass * rotation_strength_modifier, mass * rotation_damping_modifier);
}
// Collision function
collide(vector loc) {
vector mypos = llGetPos();
// Apply repulse force
vector impulse = llVecNorm(mypos - loc);
llApplyImpulse(impulse * repulse_force_modifier * mass, FALSE);
if (talk) llOwnerSay("collide() - impulse " + (string)impulse + " applied.");
}
default {
state_entry() {
llResetTime();
llOwnerSay("Leader started.");
// Sandbox
llSetStatus(STATUS_SANDBOX, sandbox);
llSetStatus(STATUS_BLOCK_GRAB, FALSE);
spawn_location = llGetPos();
//Find out what land I'm on
my_landowner = llGetLandOwnerAt(llGetPos());
// Initialize physics behavior
mass = llGetMass();
llSetStatus(STATUS_PHYSICS, TRUE);
llSetStatus(STATUS_PHANTOM, TRUE);
llSetBuoyancy(1.0);
// Swim forward
llSetForce(<swim_speed,0,0> * force_modifier * mass, TRUE);
// Set timer
llSetTimerEvent(timer_length);
}
touch_start(integer detected) {
if (llDetectedKey(0) == llGetOwner())
state stopped;
// llResetScript();
}
timer() {
// Die after reaching kill_time
if (kill_time != 0 && llGetTime() >= kill_time) {
llDie();
}
// Get my velocity
vector myvel = llGetVel();
// Apply friction
llApplyImpulse(-(myvel * friction * mass), FALSE);
// Get my position
vector mypos = llGetPos();
//Die if knocked off my land
if (llGetLandOwnerAt(mypos) != my_landowner) {
llOwnerSay("Swarm leader went off parcel, will now die.");
llDie();
}
// Check for air/water breach
if (flight_mode == 1) {
// water
if (mypos.z >= llWater(mypos) - llVecMag(llGetScale())) {
if (talk) llOwnerSay( "collide() called due to air/water breach.");
collide(<mypos.x, mypos.y, mypos.z + 0.3>);
}
} else if (flight_mode == 2) {
// air
if (mypos.z <= llWater(mypos) + llVecMag(llGetScale())) {
if (talk) llOwnerSay( "collide() called due to air/water breach.");
collide(<mypos.x, mypos.y, mypos.z - 0.3>);
}
}
// Stay near spawn location
if (llVecDist(mypos, spawn_location) > max_distance) {
// Compensate for being near sim border
if (spawn_location.x - mypos.x > 100)
mypos.x += 255;
if (talk) llOwnerSay("collide() called due to too much distance from my spawn point.");
// mypos=" + (string)mypos + ", spawn_location = " + (string)spawn_location);
collide(mypos - llVecNorm(spawn_location - mypos));
}
// Stay above ground level
if (lower_limit == 0) {
if (mypos.z <= llGround(ZERO_VECTOR)) {
if (talk) llOwnerSay("Collide called due to ground intersection.");
collide(<mypos.x,mypos.y,mypos.z - .3>);
}
} else
if (mypos.z <= lower_limit) {
if (talk) llOwnerSay("Collide called due to lower limit intersection.");
collide(<mypos.x,mypos.y,mypos.z - .3>);
}
//point in the direction you're swimming
do_rotation(llGetPos(), llGetVel());
}
on_rez(integer start_param) {
llResetScript();
spawn_location = llGetPos();
// llResetTime();
}
}
state stopped {
state_entry() {
llOwnerSay("Stopped.");
llSetForce(<0,0,0>,TRUE);
}
touch_start(integer detected) {
if (llDetectedKey(0) == llGetOwner())
state default;
}
}