//=========================================================================== //FLOCKING IN THE NIGHT SKY - MATT NEWCOMB // // This was initially based on polygon agent, version 4 but I have fairly // thoroughly gutted the code for the agents and the visualization to meet // my needs. // // Algorithmically the code is based on the ideas highlighted by Craig // Reynolds and others in talking about boid simulation. I aimed a lot of // my rule concepts based on the readings of their discussions... // The trig stuff is adapted heavily from // // USER NOTE: I basically ditched having a straight UI. For presentation // purposes I preferred having a gigantic field for the agents to wander // over (it is captivating and I liked to just leave it runnning...) // // TO RUN: Click once in the program area and the program will start running. // TO PAUSE: Click again. // SETTINGS: Available by right-clicking in the program to access a popup menu // //=========================================================================== #include #include #pragma hdrstop #include "Unit1.h" #include "stdlib.h" #include #include #pragma hdrstop #pragma package(smart_init) #pragma resource "*.dfm" TForm1 *Form1; // ====================================================== FUNCTION PROTOTYPES //prototyped to compile function-->used in agent class! TColor colorRamp(int part, int whole); // ====================================================== VARIABLES & CLASSES #define COLORCHANGE 0 #define NEIGHBORCOLOR 1 #define TYPES 2 //VISUALIZATION VARIABLES bool stop = true; const TBrushStyle brStyle[] = { bsSolid, bsBDiagonal, bsCross, bsFDiagonal, bsHorizontal, bsVertical, bsDiagCross }; bool erase = true; //erase old radii, agents or leave on screen? bool drawRadius = false; //draw the visual field of the agent? int brushIndex = 0; //selected brStyle index int counter = 0; bool flip = false; //counting up or down to colorRamp agents bool closeCheck = true; //do I care about checking proximity? int mode = COLORCHANGE; //colorRamp agent coloring by default double width; //I wanted to be able to have a resizeable environment (full screen on my laptop and projector etc...) double height; //These variables track that... //SIMULATION AND FLOCKING VARIABLES int delay = 10; bool bounce = false; //should we bounce off walls or should we wrap the world? int pop = 500; //how many boids on screen? we initialize 1000 but....(300 runs better on my laptop for experiments) int oldPop = 500; int maxNeighbors = 10; //arbitrarily defined in flocking, experimentation (to color by Neighbor count) int neighborLookup[1000][300]; int flockingFactor = 50; //algorithmic interpretations of flocking all require some degree to create realistic behavior w/o clumping, found via trial and error int agentVariance = 10; //if we don't want clumping...this adds a bit of individual behavior that makes things more realistic int flockVariance = 10; //value set by trial and error int scanRange = 180; //180 degree field of vision--can be changed via settings int predatorDirection; class agent { public: double velocity; double direction; //0-360 double directionDesired; //if we want to add collision detection, repulsion int visualField; //controls the visual radius of an agent (made sense at the time...) int tooClose; //number that are impeaching on space...(actual contact) int neighbors; int size; //a size factor, new implementation bases radius off size int type; //for predator / prey model perhaps... float x, y; TPoint points[3]; //the three points that define the polygon TColor color; //color field, different colored agents (not in use, switched to colorRamp) void drawAgent() { if (mode == COLORCHANGE) { //run colorRamp over the entire color cube... Form1->PaintBox1->Canvas->Pen->Color = colorRamp(counter, 1792); Form1->PaintBox1->Canvas->Brush->Color = colorRamp(counter, 1792); } if (mode == NEIGHBORCOLOR) { Form1->PaintBox1->Canvas->Pen->Color = colorRamp(neighbors, maxNeighbors); Form1->PaintBox1->Canvas->Brush->Color = colorRamp(neighbors, maxNeighbors); } if (mode == TYPES) { if (type == 0) { Form1->PaintBox1->Canvas->Pen->Color = clRed; Form1->PaintBox1->Canvas->Brush->Color = clRed; } else { Form1->PaintBox1->Canvas->Pen->Color = clYellow; Form1->PaintBox1->Canvas->Brush->Color = clYellow; } } Form1->PaintBox1->Canvas->Brush->Style = brStyle[brushIndex]; Form1->PaintBox1->Canvas->Polygon(points, 2); } void eraseAgent() { Form1->PaintBox1->Canvas->Pen->Color = Form1->PaintBox1->Color; Form1->PaintBox1->Canvas->Brush->Color = Form1->PaintBox1->Color; Form1->PaintBox1->Canvas->Brush->Style = bsSolid; Form1->PaintBox1->Canvas->Polygon(points, 2); } void drawRadius() { //offset so that the radius and the polygon had contrast Form1->PaintBox1->Canvas->Pen->Color = colorRamp((counter + 500)% 1792, 1792); Form1->PaintBox1->Canvas->Brush->Style = bsClear; Form1->PaintBox1->Canvas->Ellipse(x - visualField, y - visualField, x + visualField, y + visualField); } void eraseRadius() { Form1->PaintBox1->Canvas->Pen->Color = Form1->PaintBox1->Color; Form1->PaintBox1->Canvas->Brush->Style = bsSolid; Form1->PaintBox1->Canvas->Ellipse(x - visualField, y - visualField, x + visualField, y + visualField); } } agent[1000]; // ================================================================ FUNCTIONS //---------------------------------------------------------------- COLOR RAMP TColor colorRamp(int part, int whole) { if (whole == 0) whole++; // prevent divide by zero int pixelDistanceAlongEdges = (part * 1792) / whole; int red, green, blue; // Which edge of the color cube are we on? if (pixelDistanceAlongEdges < 256) { // from BLACK to BLUE red = 0; green = 0; blue = pixelDistanceAlongEdges; } else if (pixelDistanceAlongEdges < 512) { // from BLUE to CYAN red = 0; green = pixelDistanceAlongEdges - 256; blue = 255; } else if (pixelDistanceAlongEdges < 768) { // from CYAN to GREEN red = 0; green = 255; blue = 255 - (pixelDistanceAlongEdges - 512); } else if (pixelDistanceAlongEdges < 1024) { // from GREEN to YELLOW red = (pixelDistanceAlongEdges - 768); green = 255; blue = 0; } else if (pixelDistanceAlongEdges < 1280) { // from YELLOW to RED red = 255; green= 255-(pixelDistanceAlongEdges - 1024); blue = 0; } else if (pixelDistanceAlongEdges < 1536) { // from RED to MAGENTA red = 255; green= 0; blue = pixelDistanceAlongEdges - 1280; } else { // from MAGENTA to WHITE red = 255; green = pixelDistanceAlongEdges - 1537; blue = 255; } return static_cast(RGB(red, green, blue)); } //-----------------------------------------------------------------LOOK AHEAD //This checks the entire radius surrounding an agent... //***VisualField turned out to be a misnomer as the incorporation of the //visualField was actually incorporated in a different function! (flock...) void lookAhead(int i) { int tempN = 0; //count the # of neighbors int closeN = 0; //am I too close? for (int j = 0; j < pop; j++) { //Best way to check if they were in the radius was by creating a square field check if ( agent[j].x > agent[i].x - agent[i].visualField && agent[j].x < agent[i].x + agent[i].visualField) { if (agent[j].y > agent[i].y - agent[i].visualField && agent[j].y < agent[i].y + agent[i].visualField) { neighborLookup[i][tempN]=j; tempN++; } } //This was an experiment that had minimal differences but we are checking to see //if agents are piling on top of each other for lack of a better description... // Can be disabled via settings menu! if (closeCheck == true) { if (agent[j].x > agent[i].x - (agent[i].size/2) && agent[j].x < agent[i].x + (agent[i].size/2)) { if (agent[j].y > agent[i].y - (agent[i].size/2) && agent[j].y < agent[i].y + (agent[i].size/2)) { closeN++; } } } } agent[i].neighbors = tempN; agent[i].tooClose = closeN; } //------------------------------------------------------------FLOCKING BEHAVIOR void flock (int i) { predatorDirection = 0; double directionTotal; double velocityTotal; double neighborsCount = 0; double flockingScaler; double flockNumber; directionTotal = agent[i].direction; //start the total count with the agent's own properties velocityTotal = agent[i].velocity; neighborsCount = agent[i].neighbors; flockingScaler = flockingFactor/double(100); flockNumber = floor(flockingScaler * neighborsCount); for (int j = 0; j < flockNumber; j++) { if (agent[neighborLookup[i][j]].type == 0) {predatorDirection = agent[neighborLookup[i][j]].direction;} //This is how we are able to incorporate a visual field... //scan range is set by default to 180 but can be changed via settings menu //there is some difference in behavior but seeing it is not necessarily obvious... if (abs(agent[i].direction - agent[neighborLookup[i][j]].direction) < scanRange) { directionTotal +=agent[neighborLookup[i][j]].direction; } //visually it is best if we don't care about a visual field for velocity... //if things are swarming around you, you know things are moving fast regardless //of which way they are going! velocityTotal +=agent[neighborLookup[i][j]].velocity; } if (flockNumber != 0) { //averaging the componets.... directionTotal = directionTotal/(flockNumber+1); velocityTotal = velocityTotal/(flockNumber+1); Randomize(); if (Form1->Variance1->Checked == true) { if (random(2) != 0) { if (agent[i].velocity < 4) { velocityTotal += 0.1*random(2); } } else { if (agent[i].velocity > 0.3) { velocityTotal -= 0.1*random(2); } } //adds some random variation to flocks if (random(2) == 0) { directionTotal += double(random(flockVariance)/double(2)); } else { directionTotal -= double(random(flockVariance)/double(2)); } } if (predatorDirection != 0 && mode == TYPES) { agent[i].direction = 360 - agent[i].direction; } else if (abs(agent[i].direction - directionTotal) < 70) { agent[i].direction = directionTotal; } if (abs(agent[i].velocity - velocityTotal) < 0.6) { agent[i].velocity = velocityTotal; } } } //----------------------------------------------------------Neighborhood Check void find_maxNeighbors (void) { int tempMax = 0; for (int i = 0; i < pop; i++) { if (tempMax < agent[i].neighbors) { tempMax = agent[i].neighbors; } } if (tempMax > maxNeighbors) { maxNeighbors = tempMax; } if (tempMax < maxNeighbors && maxNeighbors > 40) { maxNeighbors -= (maxNeighbors-tempMax); } } //----------------------------------------------------------------Initialize void initialize () { width = Form1->PaintBox1->Width; //for resizing capabillities!! height = Form1->PaintBox1->Height; Randomize(); for (int i = 0; i < 1000; i++) { agent[i].x = random(Form1->PaintBox1->Width); agent[i].y = random(Form1->PaintBox1->Height); agent[i].size = random(20) + 3; //3-22 range... agent[i].type = random(2); agent[i].velocity = (random(2)+3); //3 or 4 agent[i].direction = random(360); agent[i].points[0] = Point(agent[i].x, agent[i].y ); agent[i].points[1] = Point(agent[i].x + ((agent[i].size)/2), agent[i].y + (agent[i].size+3)); agent[i].points[2] = Point(agent[i].x -((agent[i].size)/2), agent[i].y + (agent[i].size+3)); agent[i].visualField = agent[i].size*2; agent[i].neighbors = 0; } } //---------------------------------------------------------------------- step void step (void) { double x1, y1, x2, y2; double r, const_degrees, temp_degrees, new_degrees1, new_degrees2, agent_angle; while (stop == false) { Application->ProcessMessages(); for (int i = 0; i < pop; i++) { if (erase == true) //erasing code, clear world... { agent[i].eraseAgent(); if (drawRadius == true) agent[i].eraseRadius(); } if (agent[i].size > 0) //wrapping code around bounds... { if (bounce == false) { if (agent[i].x > width) agent[i].x = 0; if (agent[i].y > height) agent[i].y = 0; if (agent[i].x < 0) agent[i].x = width; if (agent[i].y < 0) agent[i].y = height; } if (bounce == true) { if (agent[i].x > width) {agent[i].x = width; agent[i].direction = 180 + agent[i].direction;} if (agent[i].y > height) {agent[i].y = height; agent[i].direction = 360 - agent[i].direction;} if (agent[i].x < 0) {agent[i].x = 0; agent[i].direction = 180 + agent[i].direction;} if (agent[i].y < 0) {agent[i].y = 0; agent[i].direction = 360 - agent[i].direction;} } } Randomize(); if (random(2) == 0) agent[i].direction += double(random(agentVariance)/double(1.5)); else agent[i].direction -= double(random(agentVariance)/double(1.5)); lookAhead(i); //Avoid flocking at first run... if (i != 0 && agent[i].neighbors > 2) { flock(i); } Randomize(); if (agent[i].tooClose > 4) { if (random(2) == 0) { if (agent[i].velocity < 7) { agent[i].velocity += 1; } } else { if (agent[i].velocity > 1.2) { agent[i].velocity -= 1; } } } agent[i].x+=cos(M_PI/180*agent[i].direction)*double(agent[i].velocity); //conversion to radians to distance seems to be best way--seen in many algorithms... agent[i].y-=sin(M_PI/180*agent[i].direction)*double(agent[i].velocity); x1 = agent[i].x + ((agent[i].size)/2); //point to right y1 = agent[i].y + (agent[i].size+3); x2 = agent[i].x -((agent[i].size)/2); //point to left y2 = agent[i].y + (agent[i].size+3); //shift to a 0,0 frame by subtracting out the agent coordinates x1 -= agent[i].x; y1 -= agent[i].y; x2 -= agent[i].x; y2 -= agent[i].y; //find the magnitude of the hypotenuse r = sqrt(pow((agent[i].size)/2, 2) + pow(agent[i].size+3, 2)); //degrees to be held constant between points and agent[i].x and agent[i].y const_degrees = atan2(((agent[i].size)/2), (agent[i].size +3)); agent_angle = (M_PI/180)*agent[i].direction; //we want the degree measure exactly opposite of where the agent is heading temp_degrees = (agent_angle + M_PI); //we want to add the constant degree to the temporary degree for point 1 new_degrees1 = (temp_degrees + const_degrees); //we want to subtract the constant degree from the temporary degree for point 2 new_degrees2 = (temp_degrees - const_degrees); x1 = r*cos(new_degrees1); y1 = r*-sin(new_degrees1); x2 = r*cos(new_degrees2); y2 = r*-sin(new_degrees2); //shift coordinates back x1 += agent[i].x; y1 += agent[i].y; x2 += agent[i].x; y2 += agent[i].y; //creating the outer points that define the agent based on size agent[i].points[0] = Point(agent[i].x, agent[i].y); agent[i].points[1] = Point(x1, y1); agent[i].points[2] = Point(x2, y2); agent[i].drawAgent(); if (drawRadius == true) { agent[i].drawRadius(); } } Sleep (delay); find_maxNeighbors(); //this got tacked on to the end of the routine for coloration purposes... //counter is counting iterations of the function call and tracks when we //are at the top or bottom of the color cube and changes the flags to move //in the other direction. if (flip == false) { if (counter < 1792) { counter++; } else { flip = true; counter--; } } else if (flip == true) { if (counter > 4) { counter--; } else { flip = false; counter++; } } } } //----------------------------------------------------------------------- RUN void run (void) { Form1->PaintBox1->Refresh(); stop = false; while (stop == false) { step(); Application->ProcessMessages(); } } //=========================================================================== // Event Handlers //=========================================================================== __fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner) { initialize(); } void __fastcall TForm1::Diagonal1Click(TObject *Sender) { brushIndex = 1; } //--------------------------------------------------------------------------- void __fastcall TForm1::Cross1Click(TObject *Sender) { brushIndex = 2; } //--------------------------------------------------------------------------- void __fastcall TForm1::FDiagonal1Click(TObject *Sender) { brushIndex = 3; } //--------------------------------------------------------------------------- void __fastcall TForm1::Horizontal1Click(TObject *Sender) { brushIndex = 4; } //--------------------------------------------------------------------------- void __fastcall TForm1::Vertical1Click(TObject *Sender) { brushIndex = 5; } //--------------------------------------------------------------------------- void __fastcall TForm1::DiagonalCross1Click(TObject *Sender) { brushIndex = 6; } //--------------------------------------------------------------------------- void __fastcall TForm1::Solid1Click(TObject *Sender) { brushIndex = 0; } //--------------------------------------------------------------------------- void __fastcall TForm1::N501Click(TObject *Sender) { pop += 50; oldPop += 50; } //--------------------------------------------------------------------------- void __fastcall TForm1::N502Click(TObject *Sender) { pop -= 50; oldPop -= 50; } //--------------------------------------------------------------------------- void __fastcall TForm1::N1Click(TObject *Sender) { for (int i = 0; i < 1000; i++) { agent[i].size += 5; agent[i].points[0] = Point(agent[i].x, agent[i].y ); agent[i].points[1] = Point(agent[i].x + ((agent[i].size)/2), agent[i].y + (agent[i].size+3)); agent[i].points[2] = Point(agent[i].x -((agent[i].size)/2), agent[i].y + (agent[i].size+3)); agent[i].visualField = agent[i].size*2; } } //--------------------------------------------------------------------------- void __fastcall TForm1::N201Click(TObject *Sender) { for (int i = 0; i < 1000; i++) { agent[i].size += 20; agent[i].points[0] = Point(agent[i].x, agent[i].y ); agent[i].points[1] = Point(agent[i].x + ((agent[i].size)/2), agent[i].y + (agent[i].size+3)); agent[i].points[2] = Point(agent[i].x -((agent[i].size)/2), agent[i].y + (agent[i].size+3)); agent[i].visualField = agent[i].size*2; } } //--------------------------------------------------------------------------- void __fastcall TForm1::N51Click(TObject *Sender) { for (int i = 0; i < 1000; i++) { agent[i].size -= 5; agent[i].points[0] = Point(agent[i].x, agent[i].y ); agent[i].points[1] = Point(agent[i].x + ((agent[i].size)/2), agent[i].y + (agent[i].size+3)); agent[i].points[2] = Point(agent[i].x -((agent[i].size)/2), agent[i].y + (agent[i].size+3)); agent[i].visualField = agent[i].size*2; } } //--------------------------------------------------------------------------- void __fastcall TForm1::N202Click(TObject *Sender) { for (int i = 0; i < 1000; i++) { agent[i].size -= 20; agent[i].points[0] = Point(agent[i].x, agent[i].y ); agent[i].points[1] = Point(agent[i].x + ((agent[i].size)/2), agent[i].y + (agent[i].size+3)); agent[i].points[2] = Point(agent[i].x -((agent[i].size)/2), agent[i].y + (agent[i].size+3)); agent[i].visualField = agent[i].size*2; } } //--------------------------------------------------------------------------- void __fastcall TForm1::CloseCheck1Click(TObject *Sender) { closeCheck = Form1->CloseCheck1->Checked; } //--------------------------------------------------------------------------- void __fastcall TForm1::DrawRadius1Click(TObject *Sender) { drawRadius = Form1->DrawRadius1->Checked; } //--------------------------------------------------------------------------- void __fastcall TForm1::ColorRamp1Click(TObject *Sender) { mode = COLORCHANGE; } //--------------------------------------------------------------------------- void __fastcall TForm1::NeighborColor1Click(TObject *Sender) { mode = NEIGHBORCOLOR; } //--------------------------------------------------------------------------- void __fastcall TForm1::Erase1Click(TObject *Sender) { erase = !erase; } //--------------------------------------------------------------------------- void __fastcall TForm1::PaintBox1MouseDown(TObject *Sender, TMouseButton Button, TShiftState Shift, int X, int Y) { if (Button == 0) { if (stop == true) { stop = false; run(); } else stop = true; } } //--------------------------------------------------------------------------- void __fastcall TForm1::FormResize(TObject *Sender) { height = Form1->Height; Form1->PaintBox1->Height = Form1->Height; width = Form1->Width; Form1->PaintBox1->Width = Form1->Width; } //--------------------------------------------------------------------------- void __fastcall TForm1::Variance1Click(TObject *Sender) { if (Form1->Variance1->Checked == true) { agentVariance = 10; flockVariance = 10; } else { agentVariance = 0; flockVariance = 0; } } //--------------------------------------------------------------------------- void __fastcall TForm1::N180degrees1Click(TObject *Sender) { scanRange = 180; } //--------------------------------------------------------------------------- void __fastcall TForm1::N270degrees1Click(TObject *Sender) { scanRange = 270; } //--------------------------------------------------------------------------- void __fastcall TForm1::N360degrees1Click(TObject *Sender) { scanRange = 360; } //--------------------------------------------------------------------------- void __fastcall TForm1::ResetWorld1Click(TObject *Sender) { Form1->PaintBox1->Refresh(); initialize(); } //--------------------------------------------------------------------------- void __fastcall TForm1::N01Click(TObject *Sender) { flockingFactor = 0; } //--------------------------------------------------------------------------- void __fastcall TForm1::N503Click(TObject *Sender) { flockingFactor = 50; } //--------------------------------------------------------------------------- void __fastcall TForm1::N1001Click(TObject *Sender) { flockingFactor = 100; } //--------------------------------------------------------------------------- void __fastcall TForm1::Allgoingonedirection1Click(TObject *Sender) { for (int i = 0; i < 1000; i++) { agent[i].direction = random(0); } Form1->PaintBox1->Refresh(); } //--------------------------------------------------------------------------- void __fastcall TForm1::Velocity11Click(TObject *Sender) { for (int i = 0; i < 1000; i++) { agent[i].velocity = 1; } Form1->PaintBox1->Refresh(); } //--------------------------------------------------------------------------- void __fastcall TForm1::HighVarianceVelocity1Click(TObject *Sender) { for (int i = 0; i < 1000; i++) { agent[i].velocity = random(10) + 3; } Form1->PaintBox1->Refresh(); } //--------------------------------------------------------------------------- void __fastcall TForm1::Bounce1Click(TObject *Sender) { bounce = Form1->Bounce1->Checked; } //--------------------------------------------------------------------------- void __fastcall TForm1::ypes1Click(TObject *Sender) { mode = TYPES; } //--------------------------------------------------------------------------- void __fastcall TForm1::N101Click(TObject *Sender) { delay += 10; } //--------------------------------------------------------------------------- void __fastcall TForm1::N102Click(TObject *Sender) { delay -= 10; } //---------------------------------------------------------------------------