diff --git a/examples/drylake/createAvatarDetector.js b/examples/drylake/createAvatarDetector.js new file mode 100644 index 0000000000..51ca55248e --- /dev/null +++ b/examples/drylake/createAvatarDetector.js @@ -0,0 +1,55 @@ +// +// createAvatarDetector.js +// +// Created by James B. Pollack @imgntn on 12/7/2015 +// Copyright 2015 High Fidelity, Inc. +// +// Run this script if you want the rats to run away from 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 avatarDetector = null; + +function createAvatarDetector() { + + var detectorProperties = { + name: 'Hifi-Avatar-Detector', + type: 'Box', + position: MyAvatar.position, + dimensions: { + x: 1, + y: 2, + z: 1 + }, + collisionsWillMove: false, + ignoreForCollisions: true, + visible: false, + color: { + red: 255, + green: 0, + blue: 0 + } + } + + avatarDetector = Entities.addEntity(detectorProperties); +}; + +var updateAvatarDetector = function() { + // print('updating detector position' + JSON.stringify(MyAvatar.position)) + Entities.editEntity(avatarDetector, { + position: MyAvatar.position + }); + +}; + +var cleanup = function() { + Script.update.disconnect(updateAvatarDetector); + Entities.deleteEntity(avatarDetector); +} + +createAvatarDetector(); +Script.scriptEnding.connect(cleanup); +Script.update.connect(updateAvatarDetector); \ No newline at end of file diff --git a/examples/drylake/ratCreator.js b/examples/drylake/ratCreator.js new file mode 100644 index 0000000000..237b8ec32f --- /dev/null +++ b/examples/drylake/ratCreator.js @@ -0,0 +1,471 @@ +// +// ratCreator.js +// +// Created by James B. Pollack @imgntn on 12/7/2015 +// Copyright 2015 High Fidelity, Inc. +// +// This script spawns some rats that have simple steering behaviors applied to them. +// Run it in the 'drylake' environment, or adjust all object locations to match your scene. +// +// Steering bevhaviors from ratSteer.js: +// The rats will move from a spawning point toward their nest. +// They will avoid avoider blocks moving across the alley +// They will avoid avatars running createAvatarDetector.js +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +Script.include('ratSteer.js'); +var steer = loadSteer(); +Script.include('../libraries/tween.js'); +var TWEEN = loadTween(); + +var USE_CONSTANT_SPAWNER = true; + +var RAT_SPAWNER_LOCATION = { + x: 1000.5, + y: 98, + z: 1040 +}; + +var RAT_NEST_LOCATION = { + x: 1003.5, + y: 99, + z: 964.2 +}; + +var RAT_DIMENSIONS = { + x: 0.22, + y: 0.32, + z: 1.14 +}; + +var RAT_MODEL_URL = 'http://hifi-content.s3.amazonaws.com/james/rat/models/rat_model.fbx'; +var RAT_IDLE_ANIMATION_URL = 'http://hifi-content.s3.amazonaws.com/james/rat/animations/idle.fbx'; +var RAT_WALKING_ANIMATION_URL = 'http://hifi-content.s3.amazonaws.com/james/rat/animations/walk.fbx'; +var RAT_RUNNING_ANIMATION_URL = 'http://hifi-content.s3.amazonaws.com/james/rat/animations/run.fbx'; +var RAT_DEATH_ANIMATION_URL = 'http://hifi-content.s3.amazonaws.com/james/rat/animations/death.fbx'; + +var RAT_IN_NEST_DISTANCE = 4; + +//how many milliseconds between rats +var RAT_SPAWN_RATE = 2500; + +var RAT_SOUND_URL = 'http://hifi-public.s3.amazonaws.com/sounds/Rats_Squeaks_Active.wav'; +var ratRunningSound = SoundCache.getSound(RAT_SOUND_URL); + +function playRatRunningAnimation(rat) { + var animationSettings = JSON.stringify({ + running: true + }); + Entities.editEntity(rat, { + animationURL: RAT_RUNNING_ANIMATION_URL, + animation: { + url: RAT_RUNNING_ANIMATION_URL, + running: true, + fps: 30 + }, + }); +} + +function playRatDeathAnimation(rat) { + var animationSettings = JSON.stringify({ + running: true + }); + + Entities.editEntity(rat, { + animationURL: RAT_DEATH_ANIMATION_URL, + animationSettings: animationSettings + }); +} + +var modelRatProperties = { + name: 'rat', + type: 'Model', + modelURL: RAT_MODEL_URL, + dimensions: RAT_DIMENSIONS, + position: RAT_SPAWNER_LOCATION, + shapeType: 'Box', + damping: 0.8, + angularDamping: 0.99, + friction: 0.75, + collisionsWillMove: true, + ignoreForCollisions: false, + gravity: { + x: 0, + y: -9.8, + z: 0 + }, + lifetime: 30, + userData: JSON.stringify({ + grabbableKey: { + grabbable: false + } + }) + +}; + +var targetProperties = { + name: 'Hifi-Rat-Nest', + type: 'Box', + color: { + red: 0, + green: 255, + blue: 0 + }, + dimensions: { + x: 1, + y: 1, + z: 1 + }, + visible: false, + position: RAT_NEST_LOCATION +}; + +var target = Entities.addEntity(targetProperties); + +function addRat() { + var rat = Entities.addEntity(modelRatProperties); + return rat; +} + +//every sixth rat will play a sound +var RAT_SOUND_RATE = 6; + +//spawn rate will be multiplied by this to clear any sounds hanging around +var RAT_SOUND_CLEAR_RATE = 3; + +var rats = []; +var metaRats = []; +var ratCount = 0; + +var AVOIDER_Y_HEIGHT = 99; +var FIRST_AVOIDER_START_POSITION = { + x: 1004, + y: AVOIDER_Y_HEIGHT, + z: 1019 +}; +var FIRST_AVOIDER_FINISH_POSITION = { + x: 997, + y: AVOIDER_Y_HEIGHT, + z: 1019 +}; +var SECOND_AVOIDER_START_POSITION = { + x: 998, + y: AVOIDER_Y_HEIGHT, + z: 998 +}; +var SECOND_AVOIDER_FINISH_POSITION = { + x: 1005, + y: AVOIDER_Y_HEIGHT, + z: 999 +}; +var THIRD_AVOIDER_START_POSITION = { + x: 1001.5, + y: 100, + z: 978 +}; +var THIRD_AVOIDER_FINISH_POSITION = { + x: 1005, + y: 100, + z: 974 +}; + +cleanupLeftoverAvoidersBeforeStart(); + +var avoiders = []; +addAvoiderBlock(FIRST_AVOIDER_START_POSITION); +addAvoiderBlock(SECOND_AVOIDER_START_POSITION); +addAvoiderBlock(THIRD_AVOIDER_START_POSITION); + +function addAvoiderBlock(position) { + var avoiderProperties = { + name: 'Hifi-Rat-Avoider', + type: 'Box', + color: { + red: 255, + green: 0, + blue: 255 + }, + dimensions: { + x: 1, + y: 1, + z: 1 + }, + position: position, + collisionsWillMove: false, + ignoreForCollisions: true, + visible: false + }; + + var avoider = Entities.addEntity(avoiderProperties); + avoiders.push(avoider); +} + + +tweenAvoider(avoiders[0], FIRST_AVOIDER_START_POSITION, FIRST_AVOIDER_FINISH_POSITION); +tweenAvoider(avoiders[1], SECOND_AVOIDER_START_POSITION, SECOND_AVOIDER_FINISH_POSITION); +tweenAvoider(avoiders[2], THIRD_AVOIDER_START_POSITION, THIRD_AVOIDER_FINISH_POSITION); + +function tweenAvoider(entityID, startPosition, endPosition) { + var ANIMATION_DURATION = 4200; + + var begin = { + x: startPosition.x, + y: startPosition.y, + z: startPosition.z + }; + + var end = endPosition; + + var original = startPosition; + + var tweenHead = new TWEEN.Tween(begin).to(end, ANIMATION_DURATION); + + function updateTo() { + Entities.editEntity(entityID, { + position: { + x: begin.x, + y: begin.y, + z: begin.z + } + }); + } + + function updateBack() { + Entities.editEntity(entityID, { + position: { + x: begin.x, + y: begin.y, + z: begin.z + } + }) + } + + var tweenBack = new TWEEN.Tween(begin).to(original, ANIMATION_DURATION).onUpdate(updateBack); + + tweenHead.onUpdate(function() { + updateTo() + }); + + tweenHead.chain(tweenBack); + tweenBack.chain(tweenHead); + tweenHead.start(); + +} + +function updateTweens() { + TWEEN.update(); +} + +function createRatSoundInjector() { + var audioOptions = { + volume: 0.05, + loop: false + }; + + var injector = Audio.playSound(ratRunningSound, audioOptions); + + return injector; +} + +function moveRats() { + rats.forEach(function(rat) { + + //remove the rat if its near the nest + checkDistanceFromNest(rat); + + //see if there are avatars to run from + var avatarFlightVectors = steer.fleeAllAvatars(rat); + var averageAvatarFlight; + var i; + for (i = 0; i < avatarFlightVectors.length; i++) { + if (i === 0) { + averageAvatarFlight = avatarFlightVectors[0]; + } else { + averageAvatarFlight = Vec3.sum(avatarFlightVectors[i - 1], avatarFlightVectors[i]); + } + } + averageAvatarFlight = Vec3.multiply(averageAvatarFlight, 1 / avatarFlightVectors.length); + + //see if there are avoiders to flee + var avoidBlockVectors = steer.fleeAvoiderBlocks(rat); + var averageAvoiderFlight; + var j; + for (j = 0; j < avoidBlockVectors.length; j++) { + if (j === 0) { + averageAvoiderFlight = avoidBlockVectors[0]; + } else { + averageAvoiderFlight = Vec3.sum(avoidBlockVectors[j - 1], avoidBlockVectors[j]); + } + }; + averageAvoiderFlight = Vec3.multiply(averageAvoiderFlight, 1 / avoidBlockVectors.length); + + //add all of the vectors and divide them by total to get average vector + //start by trying to go toward the nest + var seek = steer.arrive(rat, target); + var averageVector = seek; + var divisorCount = 1; + + //if there are avatars to run away from + if (avatarFlightVectors.length > 0) { + divisorCount++; + averageVector = Vec3.sum(averageVector, averageAvatarFlight); + } + + //or if there are avoider blocks to run away from + if (avoidBlockVectors.length > 0) { + divisorCount++; + averageVector = Vec3.sum(averageVector, averageAvoiderFlight); + } + + averageVector = Vec3.multiply(averageVector, 1 / divisorCount); + + var thisRatProps = Entities.getEntityProperties(rat, ["position", "rotation"]); + var ratPosition = thisRatProps.position; + var ratToNest = Vec3.subtract(RAT_NEST_LOCATION, ratPosition); + var ratRotation = Quat.rotationBetween(Vec3.UNIT_Z, ratToNest); + var eulerAngle = Quat.safeEulerAngles(ratRotation); + eulerAngle.x = 0; + eulerAngle.z = 0; + var constrainedRotation = Quat.fromVec3Degrees(eulerAngle); + + Entities.editEntity(rat, { + velocity: averageVector, + rotation: constrainedRotation, + }); + + //have to make a 'meta' rat object to keep track of rats for updating sound injector locations. parenting sounds would make this easy. + var metaRat = getMetaRatByRat(rat); + if (metaRat !== undefined) { + if (metaRat.injector !== undefined) { + if (metaRat.injector.isPlaying === true) { + metaRat.injector.options = { + loop: true, + position: ratPosition + }; + } + } + + } + }) +} + +Script.update.connect(moveRats) +Script.update.connect(updateTweens); + +function checkDistanceFromNest(rat) { + var ratProps = Entities.getEntityProperties(rat, "position"); + var distance = Vec3.distance(ratProps.position, RAT_NEST_LOCATION); + if (distance < RAT_IN_NEST_DISTANCE) { + //at nest + removeRatFromScene(rat); + } +} + +function removeRatFromScene(rat) { + + var index = rats.indexOf(rat); + if (index > -1) { + rats.splice(index, 1); + Entities.deleteEntity(rat); + + } + + var metaRatIndex = findWithAttr(metaRats, 'rat', rat); + if (metaRatIndex > -1) { + metaRats[index].injector.stop(); + metaRats.splice(index, 1); + } + +} + +function popRatFromStack(entityID) { + + var index = rats.indexOf(entityID); + if (index > -1) { + rats.splice(index, 1); + } + + var metaRatIndex = findWithAttr(metaRats, 'rat', entityID); + if (metaRatIndex > -1) { + metaRats[index].injector.stop(); + metaRats.splice(index, 1); + } + +} + +function findWithAttr(array, attr, value) { + for (var i = 0; i < array.length; i += 1) { + if (array[i][attr] === value) { + return i; + } + } +} + +function getMetaRatByRat(rat) { + var result = metaRats.filter(function(metaRat) { + return rat === metaRat.rat; + }); + return result[0]; +} + +Entities.deletingEntity.connect(popRatFromStack); + + +function cleanupLeftoverAvoidersBeforeStart() { + //sometimes if we crash or something there could be extra avoider blocks around. clear them out. + var nearbyEntities = Entities.findEntities(RAT_SPAWNER_LOCATION, 100); + var entityIndex; + for (entityIndex = 0; entityIndex < nearbyEntities.length; entityIndex++) { + var entityID = nearbyEntities[entityIndex]; + var entityProps = Entities.getEntityProperties(entityID); + if (entityProps.name === 'Hifi-Rat-Avoider') { + Entities.deleteEntity(entityID); + } + } +} + +function cleanup() { + while (rats.length > 0) { + Entities.deleteEntity(rats.pop()); + } + + while (avoiders.length > 0) { + Entities.deleteEntity(avoiders.pop()); + } + + Entities.deleteEntity(target); + Script.update.disconnect(moveRats); + Script.update.disconnect(updateTweens); + Entities.deletingEntity.disconnect(popRatFromStack); + Script.clearInterval(ratSpawnerInterval); +} + +Script.scriptEnding.connect(cleanup); + +var ratSpawnerInterval; + +if (USE_CONSTANT_SPAWNER === true) { + ratSpawnerInterval = Script.setInterval(function() { + var rat = addRat(); + playRatRunningAnimation(rat); + rats.push(rat); + ratCount++; + if (ratCount % RAT_SOUND_RATE === 0) { + var metaRat = { + rat: rat, + injector: createRatSoundInjector() + } + metaRats.push(metaRat); + + Script.setTimeout(function() { + //if we have too many injectors hanging around there are problems + metaRat.injector.stop(); + delete metaRat.injector; + }, RAT_SPAWN_RATE * RAT_SOUND_CLEAR_RATE; + } + + }, RAT_SPAWN_RATE); + } \ No newline at end of file diff --git a/examples/drylake/ratSteer.js b/examples/drylake/ratSteer.js new file mode 100644 index 0000000000..e9a2647ff5 --- /dev/null +++ b/examples/drylake/ratSteer.js @@ -0,0 +1,183 @@ +// +// ratSteer.js +// +// Created by James B. Pollack @imgntn on 12/7/2015 +// Copyright 2015 High Fidelity, Inc. +// +// This is an example of steering behaviors that can be applied entities. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +function flee(thisEntity, target) { + var targetPosition = Entities.getEntityProperties(target, "position").position; + var properties = Entities.getEntityProperties(thisEntity, ["position", "velocity"]); + var location = properties.position; + var velocity = properties.velocity; + var MAX_SPEED = 1; + var MAX_FORCE = 1; + var FLEE_RANGE = 2; + var desired = Vec3.subtract(location, targetPosition); + var d = Vec3.length(desired); + desired = Vec3.normalize(desired); + desired = Vec3.multiply(MAX_SPEED, desired); + if (d < FLEE_RANGE) { + var steer = Vec3.subtract(desired, velocity); + + var steerVector = new V3(desired.x, 0, desired.z); + steer = steerVector.limit(MAX_FORCE); + + return steer; + } else { + //target too far away to flee + return + } +} + +function fleeAllAvatars(thisEntity) { + //print('FLEE AVATARS'); + var properties = Entities.getEntityProperties(thisEntity, ["position", "velocity"]); + var location = properties.position; + var velocity = properties.velocity; + + var nearbyEntities = Entities.findEntities(location, 3); + var flightVectors = []; + for (var entityIndex = 0; entityIndex < nearbyEntities.length; entityIndex++) { + var entityID = nearbyEntities[entityIndex]; + var entityProps = Entities.getEntityProperties(entityID); + if (entityProps.name === 'Hifi-Avatar-Detector') { + //found an avatar to flee + + var MAX_SPEED = 8; + var MAX_FORCE = 8; + var FLEE_AVATAR_RANGE = 3; + var desired = Vec3.subtract(location, entityProps.position); + var d = Vec3.length(desired); + desired = Vec3.normalize(desired); + desired = Vec3.multiply(MAX_SPEED, desired); + if (d < FLEE_AVATAR_RANGE) { + var steer = Vec3.subtract(desired, velocity); + var steerVector = new V3(desired.x, 0, desired.z); + steer = steerVector.limit(MAX_FORCE); + flightVectors.push(steer); + } else { + // target too far away from this avatar to flee + } + } + + } + + return flightVectors; +} + +function fleeAvoiderBlocks(thisEntity) { + // print('FLEE AVOIDER BLOCKS'); + var properties = Entities.getEntityProperties(thisEntity, ["position", "velocity"]); + var location = properties.position; + var velocity = properties.velocity; + + var nearbyEntities = Entities.findEntities(location, 2); + var flightVectors = []; + for (var entityIndex = 0; entityIndex < nearbyEntities.length; entityIndex++) { + var entityID = nearbyEntities[entityIndex]; + var entityProps = Entities.getEntityProperties(entityID); + if (entityProps.name === 'Hifi-Rat-Avoider') { + //found an avoiderblock to flee + + var MAX_SPEED = 11; + var MAX_FORCE = 6; + var FLEE_AVOIDER_RANGE = 5; + + var desired = Vec3.subtract(location, entityProps.position); + var d = Vec3.length(desired); + desired = Vec3.normalize(desired); + desired = Vec3.multiply(MAX_SPEED, desired); + + if (d < FLEE_AVOIDER_RANGE) { + var steer = Vec3.subtract(desired, velocity); + var steerVector = new V3(desired.x, 0, desired.z); + steer = steerVector.limit(MAX_FORCE); + flightVectors.push(steer); + } else { + //target too far away from this avoider to flee + } + + } + + } + + return flightVectors; +} + +function arrive(thisEntity, target) { + + var targetPosition = Entities.getEntityProperties(target, "position").position; + var properties = Entities.getEntityProperties(thisEntity, ["position", "velocity"]); + var location = properties.position; + var velocity = properties.velocity; + var MAX_SPEED = 10; + var MAX_FORCE = 6; + var ARRIVAL_DISTANCE = 2; + + var desired = Vec3.subtract(targetPosition, location); + var d = Vec3.length(desired); + desired = Vec3.normalize(desired); + + if (d < ARRIVAL_DISTANCE) { + var m = scale(d, 0, ARRIVAL_DISTANCE, 0, MAX_SPEED); + } else { + desired = Vec3.multiply(MAX_SPEED, desired); + + } + + var steer = Vec3.subtract(desired, velocity); + var steerVector = new V3(desired.x, 0, desired.z); + steer = steerVector.limit(MAX_FORCE); + + return steer; +} + + +function V3(x, y, z) { + this.x = x; + this.y = y; + this.z = z; + return +} + +V3.prototype.length = function() { + return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z); +}; + +V3.prototype.limit = function(s) { + var len = this.length(); + + if (len > s && len > 0) { + this.scale(s / len); + } + + return this; +}; + +V3.prototype.scale = function(f) { + this.x *= f; + this.y *= f; + this.z *= f; + return this; +}; + +var v3 = new V3(); + +var scale = function(value, min1, max1, min2, max2) { + return min2 + (max2 - min2) * ((value - min1) / (max1 - min1)); +} + +loadSteer = function() { + return { + flee: flee, + fleeAllAvatars: fleeAllAvatars, + fleeAvoiderBlocks: fleeAvoiderBlocks, + arrive: arrive + }; +} \ No newline at end of file