UNPKG

babylon-steering

Version:
559 lines (490 loc) 23.3 kB
class SteeringVehicle { constructor(mesh, engine, options={}) { this.engine = engine; this.mesh = mesh; this.mesh.checkCollisions = true; this.steeringForce = new BABYLON.Vector3(0, 0, 0); this.velocity = new BABYLON.Vector3(0, 0, 0); this.maxSpeed = options.maxSpeed || 0.1; this.mass = options.mass || 1; this.velocitySamples = []; this.numSamplesForSmoothing = 20; // ARRIVAL this.arrivalThreshold = 150; // AVOID this.avoidDistance = 100; this.radius = 50; // FOLLOWPATH this.waypoints = []; this.pathIndex = 0; // WANDER this.wanderDistance = .05; this.wanderAngle = 0 this.wanderRadius = .05; this.wanderRange = 100; // QUEUE this.inSightDistance = 200 this.tooCloseDistance = 60 } get dt () { return this.engine.getDeltaTime(); } set dt (what) { this.dt = what; } vecToLocal (vector, actor) { var m = actor.mesh.getWorldMatrix(); var v = BABYLON.Vector3.TransformCoordinates(vector, m); return v; } idle (target) { this.velocity.scaleInPlace(0); this.steeringForce.setAll(0, 0, 0); // this.lookAt(target) } // not used anymore lookAt (target) { if (!target.mesh) return; let targetPos = target.mesh.position.clone(); let actorPos = this.mesh.position.clone(); let direction = targetPos.subtract(actorPos).negateInPlace(); let angle = -Math.atan2(direction.z, direction.x) - Math.PI / 2; // console.log(angle * 180 / Math.PI); this.mesh.rotation.y = angle; } lookTarget (target) { this.mesh.lookAt(target.mesh.position); // funzione nativa // this.lookAt(target) } lookWhereGoing (smoothing) { var direction = this.mesh.position.clone().add(this.velocity); // si punta alla somma della velocità e della posizione direction.y = this.mesh.position.y; if (smoothing) { if (this.velocitySamples.length == this.numSamplesForSmoothing) { this.velocitySamples.shift(); } let c = this.velocity.clone(); c.y = this.mesh.position.y this.velocitySamples.push(c); direction.setAll(0, 0, 0); for (var v = 0; v < this.velocitySamples.length; v++) { direction.addInPlace(this.velocitySamples[v]) } direction.scaleInPlace(1 / this.velocitySamples.length) direction = this.mesh.position.clone().add(direction) direction.y = this.mesh.position.y; } this.mesh.lookAt(direction); } // se è sullo stesso lato ma non che è visibile... // TEST: vedere se new BABYLON.Vector3(0, 0, 1) funziona meglio inSight (target) { if (BABYLON.Vector3.Distance(this.mesh.position, target.mesh.position) > this.inSightDistance) { return false; } var heading = new BABYLON.Vector3(0, 0, 1); // target.velocity.clone().normalize(); var difference = target.mesh.position.clone().subtract(this.mesh.position.clone()); var dot = BABYLON.Vector3.Dot(difference, heading); console.log(`Dot:${dot}`) return dot < 0 ? false : true; } // https://forum.babylonjs.com/t/rotation-angle-of-camera-to-object/2603/21 isInConeOfViewOf (target) { let targetPos = target.mesh.position.clone(); let actorPos = this.mesh.position.clone(); let distance = BABYLON.Vector3.Distance(targetPos, actorPos); /* var v0 = new BABYLON.Vector3(0, 0, 1); v0 = this.vecToLocal(v0, target); v0.normalize(); */ let v0 = target.velocity.clone().normalize(); // new BABYLON.Vector3(0, 0, 1); // var ray = new BABYLON.Ray(target.mesh.position, v0, 100); // let rayHelper = new BABYLON.RayHelper(ray); // rayHelper.show(this.mesh.getScene()); let v1 = this.mesh.position.clone().subtract(target.mesh.position.clone()); // var ray2 = new BABYLON.Ray(target.mesh.position, v1, 100); // let rayHelper2 = new BABYLON.RayHelper(ray2); // rayHelper2.show(this.mesh.getScene()); v1.normalize(); let dot = BABYLON.Vector3.Dot(v0, v1) let angle = Math.acos(dot); let angleInDegree = BABYLON.Tools.ToDegrees(angle); // console.log(`Distance: ${distance}`, `Degree: ${angleInDegree}, Dot:${dot}`); if (distance < 170 && (angleInDegree < 60)) { this.mesh.material.emissiveColor = new BABYLON.Color3(1, 0.5, 0); return true; } this.mesh.material.emissiveColor = new BABYLON.Color3(0, 0, 1); return false; } randomRotate () { // TODO: // this.mesh.rotation.y = angle; } // si aggiorna in base a sistema di riferimento GLOBALE update () { let max = this.maxSpeed * this.dt; // if (!isNaN(max) && this.steeringForce.length() > 0) { this.steeringForce = this.steeringForce.minimizeInPlaceFromFloats(max, max, max); // TODO: convert Three.js clampLength method to BABYLON // this.steeringForce.multiplyByFloats(1 / this.mass,1 / this.mass,1 / this.mass); // TODO: convert Three.js divedeScalar method to BABYLON /* this.steeringForce = */this.steeringForce/* .clone().normalize() */.scaleInPlace(1 / this.mass); // FIXME: mass > 1 not working this.velocity.addInPlace(this.steeringForce); this.velocity.y = 0; this.steeringForce.setAll(0, 0, 0); this.mesh.moveWithCollisions(this.velocity); // } } // forza generica... applyForce (force) { this.steeringForce = this.steeringForce.add(force); } // seek with a specific strength attract (target, threshold = 0, strenght = 1) { let distance = BABYLON.Vector3.Distance(this.mesh.position, target.mesh.position); if (distance < threshold && distance > 20) { var desiredVelocity = (this.mesh.position.clone().subtract(target.mesh.position.clone())).normalize().scaleInPlace(this.maxSpeed * this.dt * strenght); target.steeringForce = target.steeringForce.add(desiredVelocity.subtractInPlace(target.velocity)); } else { // senza di questo oscilla... target.flee(this); } } followActor (target, threshold = 50) { let targetPos = target.mesh.position.clone(); let actorPos = this.mesh.position.clone(); let distance = BABYLON.Vector3.Distance(targetPos, actorPos); let direction = targetPos.subtract(actorPos); let directionNormalized = BABYLON.Vector3.Normalize(direction); if (distance > threshold) { //this.mesh.translate(directionNormalized, this.maxSpeed, BABYLON.Space.WORLD); //this.mesh.moveWithCollisions(directionNormalized.scaleInPlace(this.maxSpeed)); this.steeringForce = this.steeringForce.add(directionNormalized.scaleInPlace(this.maxSpeed * this.dt).subtractInPlace(this.velocity)); } else { this.idle(); } } fleeActor (target, threshold = 50) { let targetPos = target.mesh.position.clone(); let actorPos = this.mesh.position.clone(); let distance = BABYLON.Vector3.Distance(targetPos, actorPos); let direction = targetPos.subtract(actorPos); let directionNormalized = BABYLON.Vector3.Normalize(direction).negateInPlace(); if (distance < threshold) { //this.mesh.translate(directionNormalized, this.maxSpeed, BABYLON.Space.WORLD); //this.mesh.moveWithCollisions(directionNormalized.scaleInPlace(this.maxSpeed)); this.steeringForce = this.steeringForce.add(directionNormalized.scaleInPlace(this.maxSpeed * this.dt).subtractInPlace(this.velocity)); } else { this.idle(); } } seek (target, threshold = 0) { let distance = BABYLON.Vector3.Distance(this.mesh.position, target.mesh.position); if (distance > threshold) { var desiredVelocity = (target.mesh.position.clone().subtract(this.mesh.position.clone())).normalize().scaleInPlace(this.maxSpeed * this.dt); this.steeringForce = this.steeringForce.add(desiredVelocity.subtractInPlace(this.velocity)); } else { this.idle(target); } } flee (target, threshold = 0) { let distance = BABYLON.Vector3.Distance(this.mesh.position, target.mesh.position); if (distance < threshold) { var desiredVelocity = (this.mesh.position.clone().subtract(target.mesh.position.clone())).normalize().scaleInPlace(this.maxSpeed * this.dt); this.steeringForce = this.steeringForce.add(desiredVelocity.subtractInPlace(this.velocity)); } else { this.idle(target); } } arrive (target) { var desiredVelocity = target.mesh.position.clone().subtract(this.mesh.position.clone()); desiredVelocity.normalize() var distance = BABYLON.Vector3.Distance(target.mesh.position, this.mesh.position) if (distance > this.arrivalThreshold) { desiredVelocity.scaleInPlace(this.maxSpeed * this.dt); } else { desiredVelocity.scaleInPlace(this.maxSpeed * this.dt * distance / this.arrivalThreshold) } desiredVelocity.subtractInPlace(this.velocity); this.steeringForce = this.steeringForce.add(desiredVelocity); } pursue (target, threshold = 0) { var lookAheadTime = BABYLON.Vector3.Distance(this.mesh.position, target.mesh.position) / (this.maxSpeed * this.dt); var predictedTarget = target.mesh.position.clone().add(target.velocity.clone().scaleInPlace(lookAheadTime)); this.seek({ mesh: { position: predictedTarget } }, threshold); } evade (target, threshold = 0) { var lookAheadTime = BABYLON.Vector3.Distance(this.mesh.position, target.mesh.position) / (this.maxSpeed * this.dt); var predictedTarget = target.mesh.position.clone().subtract(target.velocity.clone().scaleInPlace(lookAheadTime)); this.flee({ mesh: { position: predictedTarget } }, threshold); } // tramite un ray casting si vede se il target è visibile (non ci stanno ostacoli che lo nascondono) // FIXME: non funziona + !!!! canSee (target) { var forward = target.mesh.position.clone() // this.vecToLocal(new BABYLON.Vector3(0, 0, 1), target); var direction = forward.subtract(this.mesh.position); direction = BABYLON.Vector3.Normalize(direction); var length = 250; var ray = new BABYLON.Ray(this.mesh.position, direction, length); let rayHelper = new BABYLON.RayHelper(ray); rayHelper.show(this.mesh.getScene()); var hit = target.mesh.getScene().pickWithRay(ray); return hit.pickedMesh && hit.pickedMesh.uniqueId === target.mesh.uniqueId ? true : false; } hide (target, obstacles, threshold = 300) { // si sceglie l'ostacolo più vicino all'elemento da nascondere let closestObstacle = new BABYLON.Vector3(0, 0, 0); let closestDistance = 10000; for (let i = 0; i < obstacles.length; i++) { const obstacle = obstacles[i]; let distance = BABYLON.Vector3.Distance(this.mesh.position.clone(), obstacle.mesh.position); if (distance < closestDistance) { closestObstacle = obstacle.mesh.position.clone(); closestDistance = distance; } } // si calcola il punto dove si andrà a nasconderci let distanceWithTarget = BABYLON.Vector3.Distance(this.mesh.position, target.mesh.position); var pointToReach = BABYLON.Vector3.Lerp(target.mesh.position.clone(), closestObstacle.clone(), 2); pointToReach.y = this.mesh.position.y; // ci si nasconde solo se il target non è troppo lontano if (distanceWithTarget < threshold) { this.seek({ mesh: { position: pointToReach } }, 10); } else { // this.idle(); this.flee(target) } } // FIXME: NOT WORKING PROPERLY ??? wander () { var center = this.velocity.clone().normalize().scaleInPlace(this.wanderDistance); var offset = new BABYLON.Vector3(0, 0, 0); offset.x = (center.x + Math.sin(this.wanderAngle)) * this.wanderRadius; offset.z = (center.z + Math.cos(this.wanderAngle)) * this.wanderRadius; offset.y = 0; this.wanderAngle = Math.random() * this.wanderRange - this.wanderRange * .5; this.steeringForce = this.steeringForce.add(offset); // TEST: offset.normalize().scaleInPlace(this.maxSpeed * this.dt) } // FIXME: NOT WORKING PROPERLY separation (entities, separationRadius = 50, maxSeparation = 40) { var force = new BABYLON.Vector3(0, 0, 0); var neighborCount = 0 for (var i = 0; i < entities.length; i++) { if (entities[i] != this && BABYLON.Vector3.Distance(this.mesh.position, entities[i].mesh.position) <= separationRadius) { force.addInPlace(entities[i].mesh.position.clone().subtractInPlace(this.mesh.position)); neighborCount++; } } if (neighborCount != 0) { force.scaleInPlace(1 / neighborCount) force.negateInPlace(); } force.normalize().scaleInPlace(maxSeparation); this.steeringForce = this.steeringForce.add(force); } interpose (targetA, targetB) { var midPoint = targetA.mesh.position.clone().addInPlace(targetB.mesh.position.clone()).scaleInPlace(.5); var timeToMidPoint = BABYLON.Vector3.Distance(this.mesh.position, midPoint) / (this.maxSpeed * this.dt); var pointA = targetA.mesh.position.clone().addInPlace(targetA.velocity.clone().scaleInPlace(timeToMidPoint)) var pointB = targetB.mesh.position.clone().addInPlace(targetB.velocity.clone().scaleInPlace(timeToMidPoint)) midPoint = pointA.addInPlace(pointB).scaleInPlace(.5); this.seek({ mesh: { position: midPoint } }, 10); } avoid (obstacles) { var dynamic_length = this.velocity.length() / (this.maxSpeed * this.dt); var ahead = this.mesh.position.clone().addInPlace(this.velocity.clone().normalize().scaleInPlace(dynamic_length)) var ahead2 = this.mesh.position.clone().addInPlace(this.velocity.clone().normalize().scaleInPlace(this.avoidDistance * .5)); var mostThreatening = null; for (var i = 0; i < obstacles.length; i++) { if (obstacles[i] === this) continue; var collision = BABYLON.Vector3.Distance(obstacles[i].mesh.position, ahead) <= this.radius || BABYLON.Vector3.Distance(obstacles[i].mesh.position, ahead2) <= this.radius; if (collision && (mostThreatening == null || BABYLON.Vector3.Distance(this.mesh.position, obstacles[i].mesh.position) < BABYLON.Vector3.Distance(this.mesh.position, mostThreatening.mesh.position))) { mostThreatening = obstacles[i]; } } var avoidance = new BABYLON.Vector3(0, 0, 0); if (mostThreatening != null) { avoidance = ahead.clone().subtractInPlace(mostThreatening.mesh.position.clone()).normalize().scaleInPlace(this.maxSpeed * this.dt * .75); // MAX AVOID FORCE (<= maxSpeed) } else { avoidance.scaleInPlace(0); // nullify the avoidance force } this.steeringForce = this.steeringForce.add(avoidance); } followPath (path, loop, thresholdRadius = 10) { var wayPoint = path[this.pathIndex] if (wayPoint == null) return; if (BABYLON.Vector3.Distance(this.mesh.position, wayPoint) < thresholdRadius) { if (this.pathIndex >= path.length - 1) { if (loop) this.pathIndex = 0; } else { this.pathIndex++ } } if (this.pathIndex >= path.length - 1 && !loop) { this.arrive({ mesh: { position: wayPoint } }); } else { this.seek({ mesh: { position: wayPoint } }); } } isOnLeaderSight (leader, ahead, leaderSightRadius) { return (BABYLON.Vector3.Distance(ahead, this.mesh.position) <= leaderSightRadius || BABYLON.Vector3.Distance(leader.mesh.position, this.mesh.position) <= leaderSightRadius); } followLeader (leader, entities, distance = 20, separationRadius = 40, maxSeparation = 10, leaderSightRadius = 50, arrivalThreshold = 100) { var tv = leader.velocity.clone(); tv.normalize().scaleInPlace(distance) var ahead = leader.mesh.position.clone().add(tv); tv.negateInPlace() var behind = leader.mesh.position.clone().add(tv); if (this.isOnLeaderSight(leader, ahead, leaderSightRadius)) { this.flee(leader); } this.arrivalThreshold = arrivalThreshold; this.arrive({ mesh: { position: behind } }); this.separation(entities, separationRadius, maxSeparation); } getNeighborAhead (entities) { var maxQueueAhead = 100; var maxQueueRadius = 100; var res; var qa = this.velocity.clone().normalize().scaleInPlace(maxQueueAhead); var ahead = this.mesh.position.clone().add(qa); for (var i = 0; i < entities.length; i++) { var distance = BABYLON.Vector3.Distance(ahead, entities[i].mesh.position); if (entities[i] != this && distance <= maxQueueRadius) { res = entities[i] break; } } return res; } queue (entities, maxQueueRadius = 50) { var neighbor = this.getNeighborAhead(entities); var brake = new BABYLON.Vector3(0, 0, 0); var v = this.velocity.clone() if (neighbor != null) { brake = this.steeringForce.clone().negateInPlace().scaleInPlace(0.8); v.negateInPlace().normalize(); brake.add(v) if (BABYLON.Vector3.Distance(this.mesh.position, neighbor.mesh.position) <= maxQueueRadius) { this.velocity.scaleInPlace(0.3) } } this.steeringForce = this.steeringForce.add(brake); } // NOT WORKING !!! flock (entities) { var averageVelocity = this.velocity.clone(); var averagePosition = new BABYLON.Vector3(0, 0, 0); var inSightCount = 0; for (var i = 0; i < entities.length; i++) { if (entities[i] != this && this.inSight(entities[i])) { averageVelocity.add(entities[i].velocity) averagePosition.add(entities[i].mesh.position) if (BABYLON.Vector3.Distance(this.mesh.position, entities[i].mesh.position) < this.tooCloseDistance) { this.flee(entities[i] /* .mesh.position */); } inSightCount++; } } if (inSightCount > 0) { averageVelocity.scaleInPlace(1 / inSightCount); averagePosition.scaleInPlace(1 / inSightCount); this.seek({ mesh: { position: averagePosition } }); this.steeringForce = this.steeringForce.add(averageVelocity.subtractInPlace(this.velocity)); } } } /* var A = new BABYLON.Vector3(2, 0, 2); var B = new BABYLON.Vector3(1, 0, 1); // somma i vettori mettendolo in un nuovo vettore let position3 = A.add(B); console.log('add: ', A, B, position3); // somma i vettori mettendolo in quello di partenza A.addInPlace(B); console.log('addInPlace: ', A); // moltiplica tutto per uno scalare mettendolo in un nuovo vettore let position4 = A.scale(2) console.log('scale :', position4, A); // motiplica tutto per uno scalare mettendo nel vettore di partenza A.scaleInPlace(.5); console.log('scaleInPlace: ', A); // sottrae un vettore mettendolo in un nuovo let position5 = A.subtract(B); console.log('subtract: ', position5, A); // sottrae un vettore mettendolo nel vettore di partenza A.subtractInPlace(B); console.log('subtractInPlace: ', A); // DISTANZA let distance1 = BABYLON.Vector3.Distance(A, B); let distance2 = A.subtract(B).length(); console.log('Distance', distance1, distance2); console.log('A: ', A, 'B: ', B) let C = A.add(B).scale(0.5); // PUNTO CENTRALE let D = BABYLON.Vector3.Lerp(A, C, 0.5); // INTERPOLAZIONE console.log('Centrale: ', C); console.log('Interpolato con Lerp: ', D); var E = new BABYLON.Vector3(0.5, 0, -0.5); // la z è < !!! let F = new BABYLON.Vector3(2, 0, 2).add(E); console.log('F: ', F) let forward = BABYLON.Vector3.Forward(); let backword = BABYLON.Vector3.Backward(); let right = BABYLON.Vector3.Right(); let left = BABYLON.Vector3.Left(); COMBINAZIONI DI FORZE (behaviours) 1) Priority arbitration: si sceglie la steering force >0 a priorità + alta 2) Weighted Blending: si applica tutte le forse miscelandole in base a pesi 3) Prioritised Dithering: si assegna una priorità ed una priorità se ad ogni giro si estrae un numero a caso se questo è > della prob della priorità più alta si applica solo quello altrimenti si scende la scala delle priorità 4) Weighted Prioritised Truncated Sum: si considerano le forse per priorità si moltiplica la 1° forza per il proprio peso: se questo è > della forza totale consentita ci si ferma altrimenti si considera la 2° forza in base alla priorità e si somma la forza e si ricontrolla che sia inferiore a quella consentita, etc Esempio di API: entity .seek(target1, 50) .flee(target2,100) .avoid(target3) .combine() ogni metodo ritorna una forza che è messa dentro una mappa per poi essere usata singolarmente o miscelata secondo i 4 metodi sopra Priorità http://www.lagers.org.uk/ai4g/libguides/lg15-force-calc.html Icone per game development gratis: https://game-icons.net/ */