//============================================================================ // //Entity Class - Definition for Entity // //============================================================================ var Entity = function(params){ //Setup the entity params = params || {}; this.position = params.position || new Vector( Math.random() * 200 | 0, Math.random() * 200 | 0 ); this.velocity = params.velocity || new Vector(0,0); this.canBirth = true; this.acceleration = params.acceleration || new Vector(0,0); this.color = params.color || 'rgba(' + (Math.random() * 255 | 0) + ',' + (Math.random() * 255 | 0) + ',' + (Math.random() * 255 | 0) + ',' + '1)'; this.health = params.health || 80; this.mass = params.mass || (Math.random() * 20 | 0) + 5; this.maxSpeed = params.maxSpeed || 8; this.maxForce = params.maxForce || .5; this.ruleAlign = Math.random() * 2; this.ruleCohesion = Math.random() * 2; this.ruleSeparate = Math.random() * 2; return this; }; Entity.prototype.update = function(){ //Adds the velocity to the location this.velocity.add(this.acceleration); this.velocity.limit(this.maxSpeed); this.position.add(this.velocity); this.checkEdges(); //reset acceleration this.acceleration.multiply(0); }; //Utility functions Entity.prototype.draw = function(){ context.save(); context.fillStyle = this.color; context.fillRect( this.position.x - (this.mass / 2), this.position.y - (this.mass / 2), this.mass, this.mass ); //context.beginPath(); //context.arc( //(this.position.x - (this.mass / 4)) | 0, //(this.position.y - (this.mass / 4)) | 0, //this.mass / 2, //0, //Math.PI * 2 //) //context.closePath(); //context.fill(); context.restore(); } Entity.prototype.applyForce = function(force){ //Add the passed in force to the acceleration this.acceleration.add( force.copy()//.divide(this.mass) ); }; Entity.prototype.checkEdges = function(){ //Wrap around if(this.position.x >= width){ this.position.x = this.position.x % (width); } else if (this.position.x < 0){ this.position.x = width - 1; } if(this.position.y >= height){ this.position.y = this.position.y % (height); } else if (this.position.y < 0){ this.position.y = height - 1; } //Bounce off walls /* if(this.position.x > width){ this.position.x = width; this.velocity.x *= -1; } else if(this.position.x < 0){ this.velocity.x *= -1; this.position.x = 0; } if (this.position.y > height){ this.velocity.y *= -1; this.position.y = height; } if (this.position.y < 0){ this.velocity.y *= -1; this.position.y = 0; } */ }; //--------------------------------------- //Behaviors - Desired forces //--------------------------------------- //Calculate steering force towards a target Entity.prototype.seekForce = function(target, flee, maxForceDistance){ //How far to check for neighbors in var maxDistance = 100; //check if the passed in object has a position property try{ if(target.position){ target = target.position; } }catch(err){} //seek a target var desiredVelocity = Vector.prototype.subtract( target, this.position); ////Simple - no arriving behavior //desiredVelocity.normalize(); //----------------------------------- //get distance threshold //----------------------------------- if(maxForceDistance){ curDistance = this.position.distance(target) //Make sure entity is within range of other entities if(curDistance <= 0 || curDistance > maxForceDistance){ return new Vector(0,0); } } //----------------------------------- //Arriving behavior - slow down on approach if within radius //----------------------------------- var distance = desiredVelocity.magnitude(); var magnitude = 0; var scale = d3.scaleLinear() .domain([0, maxDistance]) .range([0, this.maxSpeed]) //val to map, current range min and max, then output range if(distance < maxDistance){ var magnitude = scale(distance); desiredVelocity.multiply(magnitude); }else{ //outside of radius, so go max speed towards it desiredVelocity.multiply(this.maxSpeed); } //steer force var steer = Vector.prototype.subtract( desiredVelocity, this.velocity); //draw the line (optional) //----------------------------------- var steerLine = Vector.prototype.add(this.position, steer); //GAME.util.drawLine(this.position, steerLine); //GAME.util.drawLine( //this.position, //Vector.prototype.add(this.position, this.velocity) //); //limit steer amount //----------------------------------- steer.limit(this.maxForce); if(flee){ steer.multiply(-1); } return steer; }; Entity.prototype.cosLookup = {}; Entity.prototype.sinLookup = {}; Entity.prototype.walkForce = function(futureDistance, radius){ //Pick a spot based on current velocity, then randomly // pick a spot at radius r at a random angle. This is // the new target futureDistance = futureDistance || 40; radius = radius || 30; var futurePosition = this.velocity.copy(); futurePosition.normalize(); //If entity is NOT already moving, make it move if(futurePosition.magnitude() < 0.1){ //Random position futurePosition.add( new Vector( (Math.random() * 3 | 0) - 1 || 1, (Math.random() * 3 | 0) - 1 || 1 ) ); } //set the length of the vector futurePosition.multiply(futureDistance); //get a random position var scale = d3.scaleLinear() .domain([0,1]) .range([0,360]) var randomAngle = Math.random() * 361 | 0; //Lookup table (nasty - todo make this better); var cosLookup = Entity.prototype.cosLookup; if( !cosLookup[randomAngle] ){ cosLookup[randomAngle] = Math.cos(randomAngle); } var cos = cosLookup[randomAngle]; var sinLookup = Entity.prototype.sinLookup; if( !sinLookup[randomAngle] ){ sinLookup[randomAngle] = Math.sin(randomAngle); } var sin = sinLookup[randomAngle]; var x = radius * cos; var y = radius * sin; //now we got the target var target = new Vector(x,y); //Add the target location to the entity's position target.add(this.position); //we have target now, so seek it var force = this.seekForce(target); return force; }; //Follow flow field Entity.prototype.flowForce = function(){ var desired = field.lookup(this.position.x, this.position.y); desired.multiply(this.maxSpeed); var steer = Vector.prototype.subtract( desired, this.velocity ); steer.limit(this.maxForce); this.applyForce(steer); }; //--------------------------------------- //Behaviors - Desired forces //--------------------------------------- Entity.prototype.separate = function(entities){ //Apply a separation force between this entity and a list of // passed in entities. If the distance is greater than some // value, don't apply the force var separationDistance = this.mass; var targetEntity = null; var curDistance = 0; var diffVector = null; var sumVector = new Vector(0,0); var count = 0; var steer = new Vector(0,0); for(entity in entities){ targetEntity = entities[entity]; curDistance = this.position.distance(targetEntity.position) //Make sure entity is within range of other entities if(curDistance > 0 && curDistance < separationDistance && targetEntity !== this){ //get vector which points away (get a new vector) diffVector = Vector.prototype.subtract(this.position, targetEntity.position); diffVector.normalize(); diffVector.divide(curDistance); //closer it is, further it should flee sumVector.add(diffVector); count += 1; } } //divide to get average if(count > 0){ sumVector.divide(count); sumVector.normalize(); sumVector.multiply(this.maxSpeed); steer = Vector.prototype.subtract(sumVector, this.velocity); steer.limit(this.maxSpeed); //lower the force even more //steer.divide(4); } return steer; }; Entity.prototype.align = function(entities){ var distance = 40; var sum = new Vector(0,0); var i=0, entity=null; var curDistance = 0; var count = 0; for(i in entities){ entity = entities[i]; curDistance = this.position.distance(entity.position); if(curDistance <= distance){ sum.add(entity.velocity); count += 1; } } var steer = new Vector(0,0); if(count > 0){ sum.divide(count); sum.normalize(); sum.multiply(this.maxSpeed); steer = Vector.prototype.subtract(sum, this.velocity); steer.limit(this.maxForce); } return steer; }; Entity.prototype.cohesion = function(entities){ var sum = new Vector(0,0); var i=0, entity=null; var distance = 40; var curDistance = 0; var count = 0; for(i in entities){ entity = entities[i]; curDistance = this.position.distance(entity.position); if(curDistance <= distance){ sum.add(entity.position); count += 1; } } var steer = new Vector(0,0); if(count > 0){ sum.divide(count); steer = this.seekForce(sum); } return steer; }; Entity.prototype.flock = function(entities){ var sep = this.separate(entities); var align = this.align(entities); var cohesion = this.cohesion(entities); sep.multiply(this.ruleSeparate); align.multiply(this.ruleAlign); cohesion.multiply(this.ruleCohesion); this.applyForce(sep); this.applyForce(align); this.applyForce(cohesion); }; //============================================================================ // //Entities - Collection of Entities // //============================================================================ var Entities = function(){ this.entities = []; }; Entities.prototype.add = function(params){ params = params || {}; this.entities.push(new Entity(params)); } Entities.prototype.remove = function(i){ this.entities.splice(i,1); } Entities.prototype.applyForce = function(force){ for(entity in this.entities){ this.entities[entity].applyForce(force); } } //============================================================================ //vector class //============================================================================ var Vector = function(x,y){ x = x || 0; y = y || 0; this.x = x; this.y = y; }; Vector.prototype.add = function(vector, vector2){ //If one vector passed in, add to this instance vector if(!vector2){ //If a scalar was passed in, add it if(typeof(vector) === 'number'){ this.x += vector; this.y += vector; }else { //It's a vector this.x += vector.x; this.y += vector.y; } return this; }else { return new Vector( vector.x + vector2.x, vector.y + vector2.y ); } }; Vector.prototype.subtract = function(vector, vector2){ if(!vector2){ this.x -= vector.x; this.y -= vector.y; return this; }else{ return new Vector( vector.x - vector2.x, vector.y - vector2.y ); } }; Vector.prototype.multiply = function(scalar){ this.x *= scalar; this.y *= scalar; return this; }; Vector.prototype.divide = function(scalar){ if(this.x != 0){ this.x /= scalar; } if(this.y != 0){ this.y /= scalar; } return this; }; Vector.prototype.copy = function(){ //returns a copy of the passed in vector return new Vector( this.x, this.y ); }; Vector.prototype.magnitude = function(){ return Math.sqrt( (this.x * this.x) + (this.y * this.y) ); }; Vector.prototype.magnitudeSquared = function(){ return (this.x * this.x) + (this.y * this.y); }; Vector.prototype.limit = function(max){ var magnitude = this.magnitude(); if(Math.abs(magnitude) > max){ //normalize this.divide(magnitude); //multiply this.multiply(max); } return this; }; Vector.prototype.normalize = function(){ var magnitude = this.magnitude(); if(magnitude !== 0){ this.divide(magnitude); } return this; }; Vector.prototype.dotProduct = function(vector1, vector2){ //Can pass in either one or two vectors. If one vector is passed, // assume we multiply it by ourself var dot; if(!vector2){ dot = (this.x * vector1.x) + (this.y * vector1.y); } else { //two vectors passed in dot = (vector1.x * vector2.x) + (vector1.y * vector2.y); } return dot; } Vector.prototype.angle = function(vector1, vector2){ if(!vector2){ //use vector 1 and 2, set vector2 to vector1 vector2 = vector1; vector1 = this; } //Find angle between two vectors var dot = vector1.dotProduct(vector1, vector2); var angle = Math.acos( dot / ( vector1.magnitude() * vector2.magnitude()) ); //convert to degrees angle = angle * (180 / Math.PI); return angle; } Vector.prototype.distance = function(vector1, vector2){ if(!vector2){ //use vector 1 and 2, set vector2 to vector1 vector2 = vector1; vector1 = this; } var dist = Math.sqrt( Math.pow((vector1.x - vector2.x), 2) + Math.pow((vector1.y - vector2.y) , 2) ); return dist; }