@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
189 lines (145 loc) • 6.23 kB
JavaScript
import Quaternion from "../../../../core/geom/Quaternion.js";
import Vector3 from "../../../../core/geom/Vector3.js";
import { min2 } from "../../../../core/math/min2.js";
import { ResourceAccessKind } from "../../../../core/model/ResourceAccessKind.js";
import { ResourceAccessSpecification } from "../../../../core/model/ResourceAccessSpecification.js";
import { System } from '../../../ecs/System.js';
import { Transform } from '../../../ecs/transform/Transform.js';
import Path from '../components/Path.js';
import PathFollower from './PathFollower.js';
import { PathFollowerEventType } from "./PathFollowerEventType.js";
import { PathFollowerFlags } from "./PathFollowerFlags.js";
const v3_forward = new Vector3();
const v3_temp1 = new Vector3();
const v3_temp2 = new Vector3();
const scratch_quaternion = new Quaternion();
/**
*
* @param {PathFollower} pathFollower
* @param {Path} path
* @param {Transform} transform
* @param {number} timeDelta
*/
function performStep(pathFollower, path, transform, timeDelta) {
let remainingDistance = pathFollower.speed.getValue() * timeDelta;
const path_length = path.length;
while (remainingDistance > 0) {
let stepLimit = pathFollower.maxMoveDistance;
if (stepLimit <= 0) {
console.warn('pathFollower.maxMoveDistance <= 0, defaulting to 1');
stepLimit = 1;
}
const stepDistance = min2(remainingDistance, stepLimit);
remainingDistance -= stepDistance;
/**
*
* @type {Vector3}
*/
const nextPosition = v3_temp1;
const next_offset = pathFollower.position + stepDistance;
const next_offset_truncated = next_offset % path_length;
if (next_offset > path_length && !pathFollower.getFlag(PathFollowerFlags.Loop)) {
// we're not looping
remainingDistance = 0;
path.sample(nextPosition, path_length);
pathFollower.setFlag(PathFollowerFlags.Finished);
pathFollower.position = path_length;
} else {
path.sample(nextPosition, next_offset_truncated);
pathFollower.position = next_offset_truncated;
}
const position = transform.position;
if (!pathFollower.getFlag(PathFollowerFlags.WritePositionX)) {
nextPosition.setX(position.x);
}
if (!pathFollower.getFlag(PathFollowerFlags.WritePositionY)) {
nextPosition.setY(position.y);
}
if (!pathFollower.getFlag(PathFollowerFlags.WritePositionZ)) {
nextPosition.setZ(position.z);
}
if (!nextPosition.equals(position)) {
const oldPosition = position;
const alignment_x = pathFollower.getFlag(PathFollowerFlags.WriteRotationX);
const alignment_y = pathFollower.getFlag(PathFollowerFlags.WriteRotationY);
const alignment_z = pathFollower.getFlag(PathFollowerFlags.WriteRotationZ);
if (alignment_x || alignment_y || alignment_z) {
// TODO we can extract tangent and normal from the path itself, no need to do euler approximation
//compute old facing direction vector
v3_forward.copy(Vector3.forward);
v3_forward.applyQuaternion(transform.rotation);
const positionDelta = v3_temp2;
positionDelta.subVectors(nextPosition, oldPosition);
if (!alignment_x) {
positionDelta.x = v3_forward.x;
}
if (!alignment_y) {
positionDelta.y = v3_forward.y;
}
if (!alignment_z) {
positionDelta.z = v3_forward.z;
}
positionDelta.normalize();
const angularLimit = pathFollower.rotationSpeed.getValue() * timeDelta;
// console.log("Angular limit:", angularLimit, positionDelta.toJSON());
scratch_quaternion.lookRotation(positionDelta);
transform.rotation.rotateTowards(scratch_quaternion,angularLimit);
}
position.copy(nextPosition);
}
}
}
class PathFollowingSystem extends System {
constructor() {
super();
this.dependencies = [PathFollower];
this.components_used = [
ResourceAccessSpecification.from(PathFollower, ResourceAccessKind.Read | ResourceAccessKind.Write),
ResourceAccessSpecification.from(Transform, ResourceAccessKind.Read | ResourceAccessKind.Write),
ResourceAccessSpecification.from(Path, ResourceAccessKind.Read),
];
/**
*
* @type {number}
* @private
*/
this.__time_delta = 0;
}
/**
*
* @param {PathFollower} pathFollower
* @param {Transform} transform
* @param {Path} path
* @param {number} entity Entity ID
*/
__visit_entity(pathFollower, transform, path, entity) {
if (!pathFollower.getFlag(PathFollowerFlags.Active)) {
//follower is not active, skip
return;
}
if (path.isEmpty()) {
//path is empty, do nothing
return;
}
performStep(pathFollower, path, transform, this.__time_delta);
if (pathFollower.getFlag(PathFollowerFlags.Finished)) {
//deactivate
pathFollower.clearFlag(PathFollowerFlags.Active);
const dataset = this.entityManager.dataset
dataset.sendEvent(entity, PathFollowerEventType.EndReached);
}
}
/**
*
* @param {number} timeDelta Time in seconds
*/
update(timeDelta) {
this.__time_delta = timeDelta;
const entityManager = this.entityManager;
const dataset = entityManager.dataset;
if (dataset !== null) {
dataset.traverseEntities([PathFollower, Transform, Path], this.__visit_entity, this);
}
}
}
export default PathFollowingSystem;