Boids

I love ‘boids and the emergent behaviour you get just be adding simple rules to a swarm of objects. In this simulation each fish has simple rules

  • If you can see the big fish: swim away from it, otherwise
  • look around and establish the centre of mass (CoM) of fish around you
  • Also make sure your not too close to your nearest neighbour
  • check the direction your shoal colleagues are swimming in
  • THEN (using a weighted value system) apply steering towards the CoM, but away from anyone too close, and turn (a little) to match the shoal.

Code

// MaxCanvas Project
// 08-Boids.JS File (April 2019)
//
// Required to drag MC.js into Intellisense
// thanks to https://visualstudiomagazine.com/blogs/tool-tracker/2018/07/use-javascript-code.aspx (Dec 2018)
/// <reference path="MC.js" />

window.onload = (function() {

// AQUIRE HTML DOM CANVAS REFERENCE
var c=document.getElementById("canvas");

//////////////////////////
// INITIALISE MC Engine //
//////////////////////////
MC.init(c);

// Configure .game
MC.game.physicsUpdates = 1;     
MC.canvas.setSize(900, 650)
MC.game.setBounds(0, 60 / 65, 0, 1);

///////////////////////////
// END OF Initialisation //
///////////////////////////
//-----------------------------------------------------------------------------
//////////////////////////
// VARIABLE DECLARATION //
//////////////////////////

var boidfont = new MC.Typeface("Impact", 30, "White");

var Boids = {
    BGColor: new MC.Color(157, 226, 228, 1), // Back ground Color
    // cache required images, so only loaded once each
    little: new MC.Picture("Images/littlefish.png"),
    big: new MC.Picture("Images/bigfish.png"),
    Report: false,
    Fish: new MC.SpriteBin(),
    Centre: new MC.Point(MC.canvas.midX, MC.canvas.midY),
    sSpeed: 55,
    fSpeed: 60,
    fCount: 200,
    fVision: 150,
    fAvoidRatio : 0.1,
    fComMod: 50,
    fCloseMod: 350,
    fRotMod: 0.5,
    fSharkAvoidMod: 250 
};

    Boids.FishFactory = function (ID) {
    var f = new MC.Sprite({ type: "anim" });
    f.setValues ({  wrapAround: true,
                    picture: Boids.little,
                    followNose: true,
                    frontAngle: -90,
                    pos: new MC.Point(MC.maths.randBetween(MC.canvas.left,MC.canvas.right),MC.maths.randBetween(MC.canvas.top,MC.canvas.bottom)),
                    vel: new MC.Point(Boids.fSpeed,0).rotate(MC.maths.randBetween(0,360)),
                    });
    f.setAnimation(3, 4, 0, 0, 2, 0, 5);
    f.animTimer = MC.maths.randBetween(0, 0.2);
    f.dev.ID = ID;
    f.dev.closest = new MC.Point(); // closest Fish
    f.dev.CoM = new MC.Point();     // Centre Of Mass
    f.dev.Rot = 0;                  // Average Rotation of fish
    f.dev.applySteering = function (steering) {
        if (!MC.game.paused) {
            f.vel.add(steering.times(MC.game.deltaTime));
            f.vel.setLength(Boids.fSpeed);
        }
    };
    f.dev.update = function () {
        var CoM = new MC.Point(0, 0);
        var CoMFound = 0;
        var Avoid = new MC.Point(10000, 10000);
        var AvoidFound = 0;
        var AngleMod = 0;
        f.dev.CoM.set(0, 0);

        for (var i = 0; i < Boids.Fish.bin.length; i++) {
            if (f.dev.ID != i)
            {
                var t = Boids.Fish.bin[i];
                var range = MC.maths.range(f.pos, t.pos);
                if (range <= Boids.fVision) {
                    // populate CoM
                    CoM.add(t.pos);
                    CoMFound++;
                    // check if closest
                    if (range < MC.maths.range(f.pos,Avoid) && range <= Boids.fVision * Boids.fAvoidRatio) {
                        Avoid.set(t.pos);
                        AvoidFound++;
                    }
                    // Add desired angle
                    AngleMod += Boids.getAngle(f.vel.getAngle(), t.vel.getAngle());
                }
            }
        }
        if (CoMFound > 0) {
            CoM.div(CoMFound);
            f.dev.CoM.set(CoM);
        }
        else { f.dev.CoM.set(f.pos); }
        if (AvoidFound > 0) {
            f.dev.closest.set(Avoid);
        }
        else { f.dev.closest.set(f.pos); }

        var steering = new MC.Point(0, 0);

        if (MC.maths.range(f.pos, Boids.Shark.pos) > Boids.fVision) {
            // CoM Steering & Rotation
            if (CoMFound > 0) {
                var ComSteer = CoM.clone().minus(f.pos);
                ComSteer.normalise().times(Boids.fComMod);
                steering.add(ComSteer);
            }

            if (AvoidFound > 0) {
                var AvoidSteer = Avoid.clone().minus(f.pos);
                AvoidSteer.normalise().times(Boids.fCloseMod);
                steering.minus(AvoidSteer);
            }

            // Apply Steering
            f.dev.applySteering(steering);

            // rotate as required
            if (!MC.game.paused) {
                if (AngleMod < 0) f.vel.rotate(MC.maths.maxOf(AngleMod, -Boids.fRotMod));
                if (AngleMod > 0) f.vel.rotate(MC.maths.minOf(AngleMod, Boids.fRotMod));
            }
        }
        else { // Swim away from shark !!
            steering = f.pos.clone().minus(Boids.Shark.pos).normalise().times(Boids.fSharkAvoidMod);
            f.dev.applySteering(steering);
        }

    };
    return f;
    }


Boids.Shark = Boids.FishFactory(0);
Boids.Shark.setValues({ picture: Boids.big });
Boids.Shark.dev.timer = 0;
Boids.Shark.dev.angleMod = 0.5;
Boids.Shark.dev.maxAngleMod = 0.5;
Boids.Shark.dev.maxTimer = 2;
Boids.Shark.dev.update = function () {
    if (!MC.game.paused) {
        var m = Boids.Shark;
        m.dev.timer -= MC.game.deltaTime;
        if (m.dev.timer <= 0) {
            m.dev.timer = MC.maths.randBetween(0.25, m.dev.maxTimer);
            m.dev.angleMod = MC.maths.randBetween(-m.dev.maxAngleMod, m.dev.maxAngleMod);
        }
        m.vel.rotate(m.dev.angleMod);
    }
};
Boids.Shark.setAnimation(12, 8, 3, 0, 5, 0, 5);
Boids.Shark.vel.setLength(Boids.sSpeed);

// utility function, establishes and returns closest angle to rotate to target
Boids.getAngle = function (myAngle, tAngle) {

    //      270
    //       |
    // 180 - + -  0
    //       |
    //       90
    var left = 0;
    var right = 0;
    if (myAngle == tAngle) return 0;
    if (myAngle < tAngle) {
        right = tAngle - myAngle;
        left = right - 360;
    }
    else {
        left = tAngle - myAngle;
        right = left + 360;
    }
    if (-left < right) return left;
    else { return right; }
};



MC.keys.p.onUp = function () { MC.game.paused = !MC.game.paused; };
MC.keys.r.onUp = function () { Boids.Report = !Boids.Report; };

for (var i = 0; i < Boids.fCount; i++) Boids.Fish.push(Boids.FishFactory(i));




//////////////////////////
// END OF declarations  //
//////////////////////////
//-----------------------------------------------------------------------------
//////////////////////////
//      GAME LOOP       //
//////////////////////////

// first call, request subsequently made within
gameLoop();

function gameLoop() {
    // ESSENTIAL MAINTAINANCE
    requestAnimationFrame(gameLoop);
    MC.game.update();                   
    
    // PHYSICS UPDATE LOOP
    for (var i = 0; i < MC.game.physicsUpdates; i++) {
        Boids.Fish.update();
        Boids.Shark.update();
    }
    
    // NON-PHYSICS UPDATES


    // RENDER
    // Stage 1.  Clear the canvas
    MC.draw.clearCanvas("Black");
    MC.draw.clearInBounds(Boids.BGColor);

    // Stage 2.  Render Sprites
    if(!Boids.Report) Boids.Fish.render();
    Boids.Shark.render();
    if (Boids.Report) {
        for (var i = 0; i < Boids.Fish.bin.length; i++) {
            var me = Boids.Fish.bin[i];
            MC.draw.line(me.pos, me.dev.closest, "red", 1);
            MC.draw.line(me.pos, me.dev.CoM, "green", 1);
        }
    }
    
    // Stage 3.  Render GUI
    MC.draw.clearOutBounds("Black");
    MC.draw.text("BOIDS            Pause [ p ]      Report [ r ]", new MC.Point(13, MC.canvas.height - 13), boidfont);
    if (Boids.Report) MC.game.fpsLog();

}

}); // EoF