(function() { Script.include('../../../../examples/libraries/virtualBaton.js?' + Math.random()); Script.include('../../../../examples/libraries/utils.js?' + Math.random()); //only one person should simulate the tank at a time -- we pass around a virtual baton var baton; var iOwn = false; var _entityID; var _this; var connected = false; var TANK_SEARCH_RADIUS = 5; var WANT_LOOK_DEBUG_LINE = false; var WANT_LOOK_DEBUG_SPHERE = true; var INTERSECT_COLOR = { red: 255, green: 0, blue: 255 } function FishTank() { _this = this; } function startUpdate() { //when the baton is claimed; iOwn = true; connected = true; Script.update.connect(_this.update); } function stopUpdateAndReclaim() { //when the baton is released; print('i released the object ' + _entityID) iOwn = false; if (connected === true) { connected = false; Script.update.disconnect(_this.update); _this.clearLookAttractor(); } //hook up callbacks to the baton baton.claim(startUpdate, stopUpdateAndReclaim); } FishTank.prototype = { fish: null, tankLocked: false, hasLookAttractor: false, lookAttractor: null, overlayLine: null, overlayLineDistance: 3, debugSphere: null, findFishInTank: function() { print('looking for a fish in the tank') var results = Entities.findEntities(_this.currentProperties.position, TANK_SEARCH_RADIUS); var fishList = []; results.forEach(function(fish) { var properties = Entities.getEntityProperties(fish, 'name'); if (properties.name.indexOf('hifi-fishtank-fish' + _this.entityID) > -1) { fishList.push(fish); } }) print('fish? ' + fishList.length) return fishList; }, initialize: function(entityID) { print('JBP fishtank initialize' + entityID) var properties = Entities.getEntityProperties(entityID); if (properties.userData.length === 0 || properties.hasOwnProperty('userData') === false) { _this.initTimeout = Script.setTimeout(function() { print('JBP no user data yet, try again in one second') _this.initialize(entityID); }, 1000) } else { print('JBP userdata before parse attempt' + properties.userData) _this.userData = null; try { _this.userData = JSON.parse(properties.userData); } catch (err) { print('JBP error parsing json'); print('JBP properties are:' + properties.userData); return; } print('after parse') _this.currentProperties = Entities.getEntityProperties(entityID); baton = virtualBaton({ batonName: 'io.highfidelity.fishtank:' + entityID, // One winner for each entity }); stopUpdateAndReclaim(); } }, preload: function(entityID) { print("preload"); this.entityID = entityID; _entityID = entityID; this.initialize(entityID); this.initTimeout = null; }, unload: function() { print(' UNLOAD') Script.update.disconnect(_this.update); if (WANT_LOOK_DEBUG_LINE === true) { _this.overlayLineOff(); } if (baton) { print('BATON RELEASE ') baton.release(function() {}); } }, update: function() { if (iOwn === false) { //exit if we're not supposed to be simulating the fish return } // print('i am the owner!') //do stuff updateFish(); _this.seeIfOwnerIsLookingAtTheTank(); }, debugSphereOn: function(position) { if (_this.debugSphere !== null) { Entities.editEntity(_this.debugSphere, { visible: true }) return; } var sphereProperties = { type: 'Sphere', parentID: _this.entityID, dimensions: { x: 0.1, y: 0.1, z: 0.1, }, color: INTERSECT_COLOR, position: position, collisionless: true } _this.debugSphere = Entities.addEntity(sphereProperties); }, updateDebugSphere: function(position) { Entities.editEntity(_this.debugSphere, { visible: true, position: position }) }, debugSphereOff: function() { // Entities.deleteEntity(_this.debugSphere); Entities.editEntity(_this.debugSphere, { visible: false }) //_this.debugSphere = null; }, overlayLineOn: function(closePoint, farPoint, color) { if (_this.overlayLine === null) { var lineProperties = { lineWidth: 5, start: closePoint, end: farPoint, color: color, ignoreRayIntersection: true, // always ignore this visible: true, alpha: 1 }; this.overlayLine = Overlays.addOverlay("line3d", lineProperties); } else { var success = Overlays.editOverlay(_this.overlayLine, { lineWidth: 5, start: closePoint, end: farPoint, color: color, visible: true, ignoreRayIntersection: true, // always ignore this alpha: 1 }); } }, overlayLineOff: function() { if (_this.overlayLine !== null) { Overlays.deleteOverlay(this.overlayLine); } _this.overlayLine = null; }, seeIfOwnerIsLookingAtTheTank: function() { var cameraPosition = Camera.getPosition(); var cameraOrientation = Camera.getOrientation(); var front = Quat.getFront(cameraOrientation); var pickRay = { origin: cameraPosition, direction: front }; if (WANT_LOOK_DEBUG_LINE === true) { _this.overlayLineOn(pickRay.origin, Vec3.sum(pickRay.origin, Vec3.multiply(front, _this.overlayLineDistance)), INTERSECT_COLOR); }; var intersection = Entities.findRayIntersection(pickRay, true, [_this.entityID]); if (intersection.intersects && intersection.entityID === _this.entityID) { //print('intersecting a tank') if (WANT_LOOK_DEBUG_SPHERE === true) { if (_this.debugSphere === null) { _this.debugSphereOn(intersection.intersection); } else { _this.updateDebugSphere(intersection.intersection); } } if (intersection.distance > 1.5) { if (WANT_LOOK_DEBUG_SPHERE === true) { _this.debugSphereOff(); } return } // print('intersection:: ' + JSON.stringify(intersection)); if (_this.hasLookAttractor === false) { _this.createLookAttractor(intersection.intersection, intersection.distance); } else if (_this.hasLookAttractor === true) { _this.updateLookAttractor(intersection.intersection, intersection.distance); } } else { if (_this.hasLookAttractor === true) { _this.clearLookAttractor(); } if (WANT_LOOK_DEBUG_SPHERE === true) { _this.debugSphereOff(); } } }, //look attractors could be private to the tank... createLookAttractor: function(position, distance) { _this.lookAttractor = { position: position, distance: distance }; _this.hasLookAttractor = true; }, updateLookAttractor: function(position, distance) { _this.lookAttractor = { position: position, distance: distance }; }, clearLookAttractor: function() { _this.hasLookAttractor = false; _this.lookAttractor = null; }, createLookAttractorEntity: function() { }, findLookAttractorEntities: function() { }, seeIfAnyoneIsLookingAtTheTank: function() { // get avatars // get their positions // get their gazes // check for intersection // add attractor for closest person (?) var intersection = Entities.findRayIntersection(pickRay, true, [_this.entityID]); if (intersection.intersects && intersection.entityID === _this.entityID) { print('intersecting a tank') print('intersection:: ' + JSON.stringify(intersection)); } } }; // // 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 FISHTANK_USERDATA_KEY = 'hifi-home-fishtank' var LIFETIME = 300; // Fish live for 5 minutes var NUM_FISH = 8; var TANK_DIMENSIONS = { x: 1.3393, y: 1.3515, z: 3.5914 }; var TANK_WIDTH = TANK_DIMENSIONS.z / 2; var TANK_HEIGHT = TANK_DIMENSIONS.y / 2; var FISH_WIDTH = 0.03; var FISH_LENGTH = 0.15; var MAX_SIGHT_DISTANCE = 1.5; var MIN_SEPARATION = 0.15; var AVOIDANCE_FORCE = 0.32; var COHESION_FORCE = 0.025; var ALIGNMENT_FORCE = 0.025; var LOOK_ATTRACTOR_FORCE = 0.029; var SWIMMING_FORCE = 0.05; var SWIMMING_SPEED = 0.5; var FISH_DAMPING = 0.25; var THROTTLE = false; var THROTTLE_RATE = 100; var sinceLastUpdate = 0; // var FISH_MODEL_URL = "http://hifi-content.s3.amazonaws.com/DomainContent/Home/fishTank/Fish-1.fbx"; // var FISH_MODEL_TWO_URL = "http://hifi-content.s3.amazonaws.com/DomainContent/Home/fishTank/Fish-2.fbx"; var FISH_MODEL_URL = "http://hifi-content.s3.amazonaws.com/DomainContent/Home/fishTank/goodfish5.fbx"; var FISH_MODEL_TWO_URL = "http://hifi-content.s3.amazonaws.com/DomainContent/Home/fishTank/goodfish5.fbx"; var fishLoaded = false; 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 createEntitiesAtCorners(lower, upper) { var lowerProps = { type: "box", dimensions: { x: 0.2, y: 0.2, z: 0.2 }, color: { red: 255, green: 0, blue: 0 }, collisionless: true, position: lower } var upperProps = { type: "box", dimensions: { x: 0.2, y: 0.2, z: 0.2 }, color: { red: 0, green: 255, blue: 0 }, collisionless: true, position: upper } _this.lowerCorner = Entities.addEntity(lowerProps); _this.upperCorner = Entities.addEntity(upperProps); } function updateFish(deltaTime) { if (_this.tankLocked === true) { return; } if (!Entities.serversExist() || !Entities.canRez()) { return; } if (THROTTLE === true) { sinceLastUpdate = sinceLastUpdate + deltaTime * 100; if (sinceLastUpdate > THROTTLE_RATE) { sinceLastUpdate = 0; } else { return; } } // print('has userdata fish??' + _this.userData['hifi-home-fishtank'].fishLoaded) if (_this.userData['hifi-home-fishtank'].fishLoaded === false) { //no fish in the user data _this.tankLocked = true; loadFish(NUM_FISH); setEntityCustomData(FISHTANK_USERDATA_KEY, _this.entityID, { fishLoaded: true }); _this.userData['hifi-home-fishtank'].fishLoaded = true; Script.setTimeout(function() { _this.fish = _this.findFishInTank(); }, 2000) return; } else { //fish in userdata already if (_this.fish === null) { _this.fish = _this.findFishInTank(); } } var fish = _this.fish; // print('how many fish do i find?' + fish.length) if (fish.length === 0) { // print('no fish...') return }; var averageVelocity = { x: 0, y: 0, z: 0 }; var averagePosition = { x: 0, y: 0, z: 0 }; //var center = _this.currentProperties.position; var bounds = Entities.getEntityProperties(_this.entityID, "boundingBox").boundingBox; lowerCorner = bounds.brn; upperCorner = bounds.tfl; //createEntitiesAtCorners(lowerCorner,upperCorner); // First pre-load an array with properties on all the other fish so our per-fish loop // isn't doing it. var flockProperties = []; for (var i = 0; i < fish.length; i++) { var otherProps = Entities.getEntityProperties(fish[i], ["position", "velocity", "rotation"]); flockProperties.push(otherProps); } for (var i = 0; i < fish.length; i++) { if (fish[i]) { // Get only the properties we need, because that is faster var properties = flockProperties[i]; // If fish has been deleted, bail if (properties.id != fish[i]) { fish[i] = false; return; } // Store old values so we can check if they have changed enough to update var velocity = { x: properties.velocity.x, y: properties.velocity.y, z: properties.velocity.z }; var position = { x: properties.position.x, y: properties.position.y, z: properties.position.z }; 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) { // Get only the properties we need, because that is faster var otherProps = flockProperties[j]; 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); //attractors //[position, radius, force] } if (_this.hasLookAttractor === true) { //print('has a look attractor, so use it') var attractorPosition = _this.lookAttractor.position; var towardAttractor = Vec3.subtract(attractorPosition, position); velocity = Vec3.mix(velocity, Vec3.multiply(Vec3.normalize(towardAttractor), Vec3.length(velocity)), LOOK_ATTRACTOR_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); var VELOCITY_FOLLOW_RATE = 0.30; var safeEuler = Quat.safeEulerAngles(rotation); // Only update properties if they have changed, to save bandwidth var MIN_POSITION_CHANGE_FOR_UPDATE = 0.001; if (Vec3.distance(properties.position, position) < MIN_POSITION_CHANGE_FOR_UPDATE) { Entities.editEntity(fish[i], { velocity: velocity, rotation: Quat.mix(properties.rotation, rotation, VELOCITY_FOLLOW_RATE) }); } else { Entities.editEntity(fish[i], { position: position, velocity: velocity, rotation: Quat.slerp(properties.rotation, rotation, VELOCITY_FOLLOW_RATE) }); } } } } var STARTING_FRACTION = 0.25; function loadFish(howMany) { print('LOADING FISH: ' + howMany) var center = _this.currentProperties.position; lowerCorner = { x: center.x - (_this.currentProperties.dimensions.z / 2), y: center.y, z: center.z - (_this.currentProperties.dimensions.z / 2) }; upperCorner = { x: center.x + (_this.currentProperties.dimensions.z / 2), y: center.y + _this.currentProperties.dimensions.y, z: center.z + (_this.currentProperties.dimensions.z / 2) }; var fish = []; 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.z) / 2.0 + (Math.random() - 0.5) * (upperCorner.z - lowerCorner.z) * STARTING_FRACTION }; fish.push( Entities.addEntity({ name: 'hifi-fishtank-fish' + _this.entityID, type: "Model", modelURL: fish.length % 2 === 0 ? FISH_MODEL_URL : FISH_MODEL_TWO_URL, position: position, parentID: _this.entityID, // rotation: { // x: 0, // y: 0, // z: 0, // w: 1 // }, // type: "Box", // dimensions: { // x: FISH_WIDTH, // y: FISH_WIDTH, // z: FISH_LENGTH // }, // velocity: { // x: SWIMMING_SPEED, // y: SWIMMING_SPEED, // z: SWIMMING_SPEED // }, damping: FISH_DAMPING, dynamic: false, lifetime: LIFETIME, color: { red: 0, green: 255, blue: 255 } }) ); } print('initial fish::' + fish.length) _this.tankLocked = false; } Script.scriptEnding.connect(function() { Script.update.disconnect(_this.update); }) return new FishTank(); });