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