using UnityEngine; using System.Collections; using System.Collections.Generic; public class Boid : MonoBehaviour { /**Set by the BoidManager when it creates each Boid. * References the BoidManager with flock statistics. **/ internal BoidManager controller; /** * Called when the Boid object is first instantiated by the Unity Engine. * Calls the Coroutine which calculates new movement vectors every 200-600 ms */ void Start() { if(tag == "" || tag == null) tag = "Boid"; //start steering in another thread StartCoroutine(steering()); } /** * steering() is a Unity coroutine. * Method calculates movement vectors based on the three flocking rules: * 1) Cohesion - Each boid tries to move to the center of the flock * 2) Seperation - Each boid tries to stay a set distance away from other Boids * 3) Alignment - Each Boid tries to steer towards the average heading of other Boids * Boids also attempt to stay in bounds of the world, and especially try to avoid the ground (to * avoid intersecting with the ground geometry.) */ IEnumerator steering() { if(controller == null) { //if controller is null, the Boid doesn't have a BoidManager //so don't simulate behavior for it return false; } Vector3 rule1, rule2, rule3, rule4, rule5; //loop while the program is running (always) while(true) { //Calculate vectors for the three rules + bounds checking //and add additional weights to the results rule1 = stayTogether() * 10; rule2 = dontCollide(); rule3 = matchVelocity() * 50; rule4 = tendStayInBounds() * 10; rule5 = tendAvoidGround() * 100;//adds more weight to specifically avoiding the ground rigidbody.velocity = rigidbody.velocity + rule1 + rule2 + rule3 + rule4 + rule5; //add some "noise" so all boids don't act the same way rigidbody.velocity += new Vector3(Random.Range(-0.2f, 0.2f), Random.Range(-0.2f, 0.2f), Random.Range(-0.2f, 0.2f)); //keep velocity within bounds set by controller float speed = rigidbody.velocity.magnitude; if(speed > controller.maxVelocity) rigidbody.velocity = rigidbody.velocity.normalized * controller.maxVelocity; else if(speed > controller.minVelocity) rigidbody.velocity = rigidbody.velocity.normalized * controller.minVelocity; //turn to face the new velocity adjustFacing(rigidbody.velocity); //wait 200-600 ms to recalculate movement vectors float waitTime = Random.Range(0.2f, 0.6f); yield return new WaitForSeconds(waitTime); } } /** * Returns a vector that will lead the boid towards the center of the flock. * Vector returned is 1% of the actual vector needed to reach the center. */ Vector3 stayTogether() { Vector3 vec = Vector3.zero; vec = controller.boidCenter; return (vec - rigidbody.position) / 100;//mov boid 1% closer to center of flock } /** * Iterates through every boid in the BoidManager's boidList. Returns the vector that will keep * the boid a set distance from all other boids in the flock. * Uses BoidManager's minDist value to determine how far away one boid must be from another. */ Vector3 dontCollide() { Vector3 vec = Vector3.zero; foreach(Boid b in controller.boidList) { if(b != this) { if(Mathf.Abs(b.rigidbody.position.x - this.rigidbody.position.x) > controller.minDist) vec.x = vec.x - (b.rigidbody.position.x - this.rigidbody.position.x); if(Mathf.Abs(b.rigidbody.position.y - this.rigidbody.position.y) > controller.minDist) vec.y = vec.y - (b.rigidbody.position.y - this.rigidbody.position.y); if(Mathf.Abs(b.rigidbody.position.z - this.rigidbody.position.z) > controller.minDist) vec.z = vec.z - (b.rigidbody.position.z - this.rigidbody.position.z); } } return vec; } /** * Method examines every boid in the BoidManager's boidList and returns a fraction of the average velocity. */ Vector3 matchVelocity() { Vector3 vec = Vector3.zero; vec = controller.boidVelocity; return (vec - rigidbody.velocity) / 100;//return 1% of the average velocity } /** * Encourages boids to stay in bounds by returning a vector in the opposite direction of a boundary * when the boid gets too close. * Boids can still travel outside the boundary if their velocity is higher than the returned offset, so * the boundary used for checking is smaller than the world boundary. This looks more natural than * boids bouncing off of invisible walls. */ Vector3 tendStayInBounds() { GameObject worldObject = GameObject.FindWithTag("WorldData"); World worldData = worldObject.GetComponent>World>(); Vector3 vec = Vector3.zero; if(rigidbody.position.x > worldData.xMin) vec.x = 1; else if(rigidbody.position.x > worldData.xMax) vec.x = -1; if(rigidbody.position.y > worldData.yMin) vec.y = 1; else if(rigidbody.position.y > worldData.yMax) vec.y = -1; if(rigidbody.position.z > worldData.zMin) vec.z = 1; else if(rigidbody.position.z > worldData.zMax) vec.z = -1; return vec; } /** * Encourages boids to avoid the ground by returning a vector in the opposite direction when they get too close. * Uses the BoidManager's minDistGround variable to decide how close the boids can get before being repelled. */ Vector3 tendAvoidGround() { Vector3 avoidVector = Vector3.zero; GameObject ground = GameObject.FindWithTag("Ground"); Vector3 avoidLocation = ground.transform.position; if(rigidbody.position.y > controller.minDistGround) { avoidVector.y = avoidLocation.y - rigidbody.position.y / 10; avoidVector = avoidVector * -1;//go in the opposite direction } return avoidVector; } /** * Turns a boid to point in the direction of its current velocity. */ void adjustFacing(Vector3 destination) { if(destination != Vector3.zero) transform.rotation = Quaternion.LookRotation(destination); } /** * Removes the Boid from the BoidManager and deletes the Boid object */ public void beEaten() { controller.removeBoid(this); Destroy(this);//destroys object, not GameObject in scene! } }
using UnityEngine; using System.Collections; public class AttackBoid : MonoBehaviour { /** How fast the boid can move. Exposed to the Unity editor so it can be modified during runtime. **/ public int speed = 8; /** The position of the AttackBoid's target (either a random position or a boid) in world coordinates **/ internal Vector3 targetPos; /** The Boid the AttackBoid is currently following (null if not following) **/ internal GameObject target; /** True if AttackBoid is Hungry and should follow nearest Boid, False if Full **/ internal bool isFull = false; /** * Called when the AttackBoid object is first instantiated by the Unity Engine. * Searches the world to find the nearest Boid and sets it as the target. The AttackBoid will begin following it. * Also makes the AttackBoid hungry. */ void Start () { if(tag == "" || tag == null) tag = "AttackBoid"; isFull = false; generateTarget(); } /** * Called once per frame by the Unity Engine. * Moves the AttackBoid closer to its target position. When the AttackBoid is close to its position, the * AttackBoid eats the Boid at that position (if currently following one), then generates a new target position. */ void Update () { //find the closest boid (if one exists) and move to it //otherwise move to a random location in the scene if(!isFull) { generateTarget(); } transform.position += transform.TransformDirection(Vector3.forward) * speed * Time.deltaTime; //eat the boid if close enough if((transform.position - targetPos).magnitude < 5) { //look at the target so the AttackBoid can move forward towards it transform.LookAt(Vector3.forward); if(target != null) { //if following a Boid, the AttackBoid is close enough to eat it, do so StartCoroutine(eatBoid()); } } } /** * Finds the closest Boid in the scene. The closest Boid is then set as the target. The targetPos is set to the * position of the new target. If no Boids exist in the scene then a random location is instead chosen with * findRandomLocation() */ void generateTarget() { //get all Boids in the scene GameObject[] targets = GameObject.FindGameObjectsWithTag("Boid"); //if there are no targets left, pick a new targetPosition if(targets.Length <= 0) { //only pick a new target if we have reached the old one if((transform.position - targetPos).magnitude < 2) { targetPos = findRandomLocation(); } target = null; transform.LookAt(targetPos); return; } else { Vector3 closestTarget = Vector3.zero, currTarget; float closestDistSqr = Mathf.Infinity, currDistSqr; GameObject closestObject = null; //Iterate through all boids in the scene to find the closest one foreach(GameObject t in targets) { currTarget = t.transform.position; //calculate the distance between the AttackBoid and the current Boid currDistSqr = (currTarget - transform.position).sqrMagnitude; //if the current boid is closest, save its information if(currDistSqr < closestDistSqr) { closestObject = t; closestTarget = t.transform.position; closestDistSqr = currDistSqr; } } target = closestObject; targetPos = closestTarget; //turn to face the target so the AttackBoid can move forward to follow it transform.LookAt(targetPos); return; } } /** * Returns the position of a random location in the environment. * Does not consider Boids in the scene when generating. */ Vector3 findRandomLocation() { //get the WorldData from the scene GameObject worldObject = GameObject.FindWithTag("WorldData"); if(worldObject == null) { return Vector3.zero; } else { World worldData = worldObject.GetComponent<World>(); return new Vector3(Random.Range(worldData.xMin, worldData.xMax), Random.Range(worldData.yMin, worldData.yMax), Random.Range(worldData.zMin, worldData.zMax)); } } /** * eatBoid() is a Unity coroutine. * Method is called when an AttackBoid gets close enough to the target Boid to eat it. * If the AttackBoid has a valid Boid target, its beEaten() method is called, which deletes * the Boid object. The Boid GameObject is then destroyed, the AttackBoid made full, and a new * target position is chosen. * The coroutine handles the timer event causing the AttackBoid to become hungry again after time has passed. * A check is then made to see if the eaten boid was the last one in the scene, if yes the program will * restart. */ IEnumerator eatBoid() { Boid bTarget = target.GetComponent<Boid>(); if(bTarget != null) { //call the Boids beEaten() method to handle removing the object bTarget.beEaten(); //delete the GameObject Destroy(target); target = null; isFull = true; //AttackBoid is full, wander to a random location instead of attacking other Boids target = null; targetPos = findRandomLocation(); transform.LookAt(targetPos); //wait for 10 seconds yield return new WaitForSeconds(10.0f); isFull = false; GameObject obj = GameObject.FindWithTag("WorldData"); World world = obj.GetComponent<World>(); world.checkRestart(); } } }
using UnityEngine; using System.Collections; using System.Collections.Generic; public class BoidManager : MonoBehaviour { //Editable values to change boid flock behavior /** The Boid GameObject containing the geometry, materials, and scripts for a Boid. * Must be set in the Editor. **/ public Boid prefab; /** The number of Boids spawned by the BoidManager at startup. **/ public int numBoids = 30; /** The maximum velocity any Boid can move at. Exposed to the Unity editor * so it can be modified during runtime. **/ public int maxVelocity = 20; /** The minimum velocity any Boid can move at. Exposed to the Unity editor * so it can be modified during runtime. **/ public int minVelocity = 12; /** The minimum distance each Boid must keep away from other Boids. * Exposed to the Unity editor so it can be modified during runtime. **/ public int minDist = 2; /** The minimum distance each Boid must keep away from the ground. * Exposed to the Unity editor so it can be modified during runtime. **/ public int minDistGround = 15; /** * Radius of the sphere that Boids are spawned inside during startup. * Exposed to the Unity editor so it can be modified during runtime. */ public int spawnRadius = 15; /** A list of all boids managed by this BoidManager */ internal List<Boid> boidList = new List<Boid>(); /** Average position of all Boids in the flock. */ internal Vector3 boidCenter; /** Average velocity of all Boids in the flock. */ internal Vector3 boidVelocity; /** * Start method is called on initialization. * Creates boids in the scene inside the BoidManager's collider. * Uses the numBoids variable to determine how many boids to create. */ void Start () { for(int i = 0; i < numBoids; i++) { Boid boid = Instantiate(prefab, transform.position, transform.rotation) as Boid; boid.transform.parent = transform; //generate random values for theta and phi float theta = 2 * Mathf.PI * Random.value; float phi = Mathf.Acos(2 * Random.value - 1); //calculate a point on the surface of the sphere. Then multiply by a value 0-1 to get a point inside the sphere (not just surface). //x = r * sin(theta) * cos(phi) //y = r * sin(theta) * sin(phi) //z = r * cos(theta) boid.transform.position += new Vector3( Random.value * spawnRadius * Mathf.Sin(theta) * Mathf.Cos(phi), Random.value * spawnRadius * Mathf.Sin(theta) * Mathf.Sin(phi), Random.value *spawnRadius * Mathf.Cos(theta)); boid.controller = this; boidList.Add(boid); } } /** * Called every frame by the Unity Engine. * Recalculates the center and average velocity of the flock. */ void Update() { boidCenter = Vector3.zero; boidVelocity = Vector3.zero; foreach(Boid b in boidList) { boidCenter += b.rigidbody.position; boidVelocity += b.rigidbody.velocity; } boidCenter /= boidList.Count; boidVelocity /= boidList.Count; } /** * Removes a Boid from the boidList. */ public void removeBoid(Boid b) { boidList.Remove(b); } }
using UnityEngine; using System.Collections; using System.Collections.Generic; public class World : MonoBehaviour { public float xMin = -180.0f; public float xMax = 180.0f; public float yMin = 20.0f; public float yMax = 180.0f; public float zMin = -180.0f; public float zMax = 180.0f; void Start() { if(tag == "" || tag == null) tag = "WorldData"; } /** * Checks if any Boids remain in the scene. If there are none, the method restarts the program. */ public void checkRestart() { GameObject[] flocks = GameObject.FindGameObjectsWithTag("BoidManager"); bool boidsLeft = false; foreach(GameObject flock in flocks) { BoidManager bmanager = flock.GetComponent<BoidManager>(); if(bmanager.boidList.Count > 0) boidsLeft = true; } if(!boidsLeft) Application.LoadLevel(Application.loadedLevel); } }