From ee95f7906b50c4fb6c66eccd7379d57508d5bc6c Mon Sep 17 00:00:00 2001 From: Philip Rosedale Date: Fri, 29 Jan 2016 00:59:06 -0800 Subject: [PATCH] Fish flocking example --- examples/FlockOfFish.js | 161 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 examples/FlockOfFish.js diff --git a/examples/FlockOfFish.js b/examples/FlockOfFish.js new file mode 100644 index 0000000000..a6b724ab9d --- /dev/null +++ b/examples/FlockOfFish.js @@ -0,0 +1,161 @@ +// +// flockOfFish.js +// examples +// +// Philip Rosedale +// Copyright 2016 High Fidelity, Inc. +// Fish smimming around in a space in front of you +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + + +var LIFETIME = 60; +var NUM_FISH = 20; +var TANK_WIDTH = 3.0; +var TANK_HEIGHT = 1.0; +var FISH_WIDTH = 0.03; +var FISH_LENGTH = 0.15; +var MAX_SIGHT_DISTANCE = 0.4; +var MIN_SEPARATION = 0.15; +var AVOIDANCE_FORCE = 0.2; +var COHESION_FORCE = 0.05; +var ALIGNMENT_FORCE = 0.05; +var SWIMMING_FORCE = 0.05; +var SWIMMING_SPEED = 2.0; + +var fishLoaded = false; +var fish = []; + +var lowerCorner = { x: 0, y: 0, z: 0 }; +var upperCorner = { x: 0, y: 0, z: 0 }; + +function randomVector(scale) { + return { x: Math.random() * scale - scale / 2.0, y: Math.random() * scale - scale / 2.0, z: Math.random() * scale - scale / 2.0 }; +} + +function updateFish(deltaTime) { + if (!Entities.serversExist() || !Entities.canRez()) { + return; + } + if (!fishLoaded) { + loadFish(NUM_FISH); + fishLoaded = true; + return; + } + + var averageVelocity = { x: 0, y: 0, z: 0 }; + var averagePosition = { x: 0, y: 0, z: 0 }; + var birdPositionsCounted = 0; + var birdVelocitiesCounted = 0; + for (var i = 0; i < fish.length; i++) { + if (fish[i].entityId) { + var properties = Entities.getEntityProperties(fish[i].entityId); + // If Bird has been deleted, bail + if (properties.id != fish[i].entityId) { + fish[i].entityId = false; + return; + } + + var velocity = properties.velocity; + var position = properties.position; + averageVelocity = { x: 0, y: 0, z: 0 }; + averagePosition = { x: 0, y: 0, z: 0 }; + + var othersCounted = 0; + for (var j = 0; j < fish.length; j++) { + if (i != j) { + var otherProps = Entities.getEntityProperties(fish[j].entityId); + var separation = Vec3.distance(properties.position, otherProps.position); + if (separation < MAX_SIGHT_DISTANCE) { + averageVelocity = Vec3.sum(averageVelocity, otherProps.velocity); + averagePosition = Vec3.sum(averagePosition, otherProps.position); + othersCounted++; + } + if (separation < MIN_SEPARATION) { + var pushAway = Vec3.multiply(Vec3.normalize(Vec3.subtract(properties.position, otherProps.position)), AVOIDANCE_FORCE); + velocity = Vec3.sum(velocity, pushAway); + } + } + } + + if (othersCounted > 0) { + averageVelocity = Vec3.multiply(averageVelocity, 1.0 / othersCounted); + averagePosition = Vec3.multiply(averagePosition, 1.0 / othersCounted); + // Alignment: Follow group's direction and speed + velocity = Vec3.mix(velocity, Vec3.multiply(Vec3.normalize(averageVelocity), Vec3.length(velocity)), ALIGNMENT_FORCE); + // Cohesion: Steer towards center of flock + var towardCenter = Vec3.subtract(averagePosition, position); + velocity = Vec3.mix(velocity, Vec3.multiply(Vec3.normalize(towardCenter), Vec3.length(velocity)), COHESION_FORCE); + // Try to swim at a constant speed + velocity = Vec3.mix(velocity, Vec3.multiply(Vec3.normalize(velocity), SWIMMING_SPEED), SWIMMING_FORCE); + } + + // Keep fish in their 'tank' + if (position.x < lowerCorner.x) { + position.x = lowerCorner.x; + velocity.x *= -1.0; + } else if (position.x > upperCorner.x) { + position.x = upperCorner.x; + velocity.x *= -1.0; + } + if (position.y < lowerCorner.y) { + position.y = lowerCorner.y; + velocity.y *= -1.0; + } else if (position.y > upperCorner.y) { + position.y = upperCorner.y; + velocity.y *= -1.0; + } + if (position.z < lowerCorner.z) { + position.z = lowerCorner.z; + velocity.z *= -1.0; + } else if (position.z > upperCorner.z) { + position.z = upperCorner.z; + velocity.z *= -1.0; + } + + // Orient in direction of velocity + var rotation = Quat.rotationBetween(Vec3.UNIT_NEG_Z, velocity); + Entities.editEntity(fish[i].entityId, { position: position, velocity: velocity, rotation: Quat.mix(properties.rotation, rotation, 0.33) }); + } + } +} + +// Connect a call back that happens every frame +Script.update.connect(updateFish); + +// Delete our little friends if script is stopped +Script.scriptEnding.connect(function() { + for (var i = 0; i < fish.length; i++) { + Entities.deleteEntity(fish[i].entityId); + } +}); + +var STARTING_FRACTION = 0.25; +function loadFish(howMany) { + var center = Vec3.sum(MyAvatar.position, Vec3.multiply(Quat.getFront(MyAvatar.orientation), 3 * TANK_WIDTH)); + lowerCorner = { x: center.x - TANK_WIDTH / 2, y: center.y, z: center.z - TANK_WIDTH / 2 }; + upperCorner = { x: center.x + TANK_WIDTH / 2, y: center.y + TANK_HEIGHT, z: center.z + TANK_WIDTH / 2 }; + + for (var i = 0; i < howMany; i++) { + var position = { + x: lowerCorner.x + (upperCorner.x - lowerCorner.x) / 2.0 + (Math.random() - 0.5) * (upperCorner.x - lowerCorner.x) * STARTING_FRACTION, + y: lowerCorner.y + (upperCorner.y - lowerCorner.y) / 2.0 + (Math.random() - 0.5) * (upperCorner.y - lowerCorner.y) * STARTING_FRACTION, + z: lowerCorner.z + (upperCorner.z - lowerCorner.x) / 2.0 + (Math.random() - 0.5) * (upperCorner.z - lowerCorner.z) * STARTING_FRACTION + }; + + fish.push({ + entityId: Entities.addEntity({ + type: "Box", + position: position, + rotation: { x: 0, y: 0, z: 0, w: 1 }, + dimensions: { x: FISH_WIDTH, y: FISH_WIDTH, z: FISH_LENGTH }, + velocity: { x: SWIMMING_SPEED, y: SWIMMING_SPEED, z: SWIMMING_SPEED }, + damping: 0.0, + dynamic: false, + lifetime: LIFETIME, + color: { red: 0, green: 255, blue: 255 } + }) + }); + } +}