UNPKG

noa-engine

Version:

Experimental voxel game engine

453 lines (374 loc) 13.3 kB
import ECS from 'ent-comp' import vec3 from 'gl-vec3' import { updatePositionExtents } from '../components/position' import { setPhysicsFromPosition } from '../components/physics' // Component definitions import collideEntitiesComp from "../components/collideEntities.js" import collideTerrainComp from "../components/collideTerrain.js" import fadeOnZoomComp from "../components/fadeOnZoom.js" import followsEntityComp from "../components/followsEntity.js" import meshComp from "../components/mesh.js" import movementComp from "../components/movement.js" import physicsComp from "../components/physics.js" import positionComp from "../components/position.js" import receivesInputsComp from "../components/receivesInputs.js" import shadowComp from "../components/shadow.js" import smoothCameraComp from "../components/smoothCamera.js" var defaultOptions = { shadowDistance: 10, } /** * `noa.entities` - manages entities and components. * * This class extends [ent-comp](https://github.com/fenomas/ent-comp), * a general-purpose ECS. It's also decorated with noa-specific helpers and * accessor functions for querying entity positions, etc. * * Expects entity definitions in a specific format - see source `components` * folder for examples. * * This module uses the following default options (from the options * object passed to the {@link Engine}): * * ```js * var defaults = { * shadowDistance: 10, * } * ``` */ export class Entities extends ECS { /** @internal */ constructor(noa, opts) { super() opts = Object.assign({}, defaultOptions, opts) // optional arguments to supply to component creation functions var componentArgs = { 'shadow': opts.shadowDistance, } /** * @internal * @type {import('../index').Engine} */ this.noa = noa /** Hash containing the component names of built-in components. * @type {{ [key:string]: string }} */ this.names = {} // call `createComponent` on all component definitions, and // store their names in ents.names var compDefs = { collideEntities: collideEntitiesComp, collideTerrain: collideTerrainComp, fadeOnZoom: fadeOnZoomComp, followsEntity: followsEntityComp, mesh: meshComp, movement: movementComp, physics: physicsComp, position: positionComp, receivesInputs: receivesInputsComp, shadow: shadowComp, smoothCamera: smoothCameraComp, } Object.keys(compDefs).forEach(bareName => { var arg = componentArgs[bareName] || undefined var compFn = compDefs[bareName] var compDef = compFn(noa, arg) this.names[bareName] = this.createComponent(compDef) }) /* * * * * ENTITY ACCESSORS * * A whole bunch of getters and such for accessing component state. * These are moderately faster than `ents.getState(whatever)`. * * * */ /** @internal */ this.cameraSmoothed = this.getComponentAccessor(this.names.smoothCamera) /** * Returns whether the entity has a physics body * @type {(id:number) => boolean} */ this.hasPhysics = this.getComponentAccessor(this.names.physics) /** * Returns whether the entity has a position * @type {(id:number) => boolean} */ this.hasPosition = this.getComponentAccessor(this.names.position) /** * Returns the entity's position component state * @type {(id:number) => null | import("../components/position").PositionState} */ this.getPositionData = this.getStateAccessor(this.names.position) /** * Returns the entity's position vector. * @type {(id:number) => number[]} */ this.getPosition = (id) => { var state = this.getPositionData(id) return (state) ? state.position : null } /** * Get the entity's `physics` component state. * @type {(id:number) => null | import("../components/physics").PhysicsState} */ this.getPhysics = this.getStateAccessor(this.names.physics) /** * Returns the entity's physics body * Note, will throw if the entity doesn't have the position component! * @type {(id:number) => null | import("voxel-physics-engine").RigidBody} */ this.getPhysicsBody = (id) => { var state = this.getPhysics(id) return (state) ? state.body : null } /** * Returns whether the entity has a mesh * @type {(id:number) => boolean} */ this.hasMesh = this.getComponentAccessor(this.names.mesh) /** * Returns the entity's `mesh` component state * @type {(id:number) => {mesh:any, offset:number[]}} */ this.getMeshData = this.getStateAccessor(this.names.mesh) /** * Returns the entity's `movement` component state * @type {(id:number) => import('../components/movement').MovementState} */ this.getMovement = this.getStateAccessor(this.names.movement) /** * Returns the entity's `collideTerrain` component state * @type {(id:number) => {callback: function}} */ this.getCollideTerrain = this.getStateAccessor(this.names.collideTerrain) /** * Returns the entity's `collideEntities` component state * @type {(id:number) => { * cylinder:boolean, collideBits:number, * collideMask:number, callback: function}} */ this.getCollideEntities = this.getStateAccessor(this.names.collideEntities) /** * Pairwise collideEntities event - assign your own function to this * property if you want to handle entity-entity overlap events. * @type {(id1:number, id2:number) => void} */ this.onPairwiseEntityCollision = function (id1, id2) { } } /* * * * PUBLIC ENTITY STATE ACCESSORS * * */ /** Set an entity's position, and update all derived state. * * In general, always use this to set an entity's position unless * you're familiar with engine internals. * * ```js * noa.ents.setPosition(playerEntity, [5, 6, 7]) * noa.ents.setPosition(playerEntity, 5, 6, 7) // also works * ``` * * @param {number} id */ setPosition(id, pos, y = 0, z = 0) { if (typeof pos === 'number') pos = [pos, y, z] // convert to local and defer impl var loc = this.noa.globalToLocal(pos, null, []) this._localSetPosition(id, loc) } /** Set an entity's size * @param {number} xs * @param {number} ys * @param {number} zs */ setEntitySize(id, xs, ys, zs) { var posDat = this.getPositionData(id) posDat.width = (xs + zs) / 2 posDat.height = ys this._updateDerivedPositionData(id, posDat) } /** * called when engine rebases its local coords * @internal */ _rebaseOrigin(delta) { for (var state of this.getStatesList(this.names.position)) { var locPos = state._localPosition var hw = state.width / 2 nudgePosition(locPos, 0, -hw, hw, state.__id) nudgePosition(locPos, 1, 0, state.height, state.__id) nudgePosition(locPos, 2, -hw, hw, state.__id) vec3.subtract(locPos, locPos, delta) this._updateDerivedPositionData(state.__id, state) } } /** @internal */ _localGetPosition(id) { return this.getPositionData(id)._localPosition } /** @internal */ _localSetPosition(id, pos) { var posDat = this.getPositionData(id) vec3.copy(posDat._localPosition, pos) this._updateDerivedPositionData(id, posDat) } /** * helper to update everything derived from `_localPosition` * @internal */ _updateDerivedPositionData(id, posDat) { vec3.copy(posDat._renderPosition, posDat._localPosition) var offset = this.noa.worldOriginOffset vec3.add(posDat.position, posDat._localPosition, offset) updatePositionExtents(posDat) var physDat = this.getPhysics(id) if (physDat) setPhysicsFromPosition(physDat, posDat) } /* * * * OTHER ENTITY MANAGEMENT APIs * * note most APIs are on the original ECS module (ent-comp) * these are some overlaid extras for noa * * */ /** * Safely add a component - if the entity already had the * component, this will remove and re-add it. */ addComponentAgain(id, name, state) { // removes component first if necessary if (this.hasComponent(id, name)) this.removeComponent(id, name) this.addComponent(id, name, state) } /** * Checks whether a voxel is obstructed by any entity (with the * `collidesTerrain` component) */ isTerrainBlocked(x, y, z) { // checks if terrain location is blocked by entities var off = this.noa.worldOriginOffset var xlocal = Math.floor(x - off[0]) var ylocal = Math.floor(y - off[1]) var zlocal = Math.floor(z - off[2]) var blockExt = [ xlocal + 0.001, ylocal + 0.001, zlocal + 0.001, xlocal + 0.999, ylocal + 0.999, zlocal + 0.999, ] var list = this.getStatesList(this.names.collideTerrain) for (var i = 0; i < list.length; i++) { var id = list[i].__id var ext = this.getPositionData(id)._extents if (extentsOverlap(blockExt, ext)) return true } return false } /** * Gets an array of all entities overlapping the given AABB */ getEntitiesInAABB(box, withComponent) { // extents to test against var off = this.noa.worldOriginOffset var testExtents = [ box.base[0] - off[0], box.base[1] - off[1], box.base[2] - off[2], box.max[0] - off[0], box.max[1] - off[1], box.max[2] - off[2], ] // entity position state list var entStates if (withComponent) { entStates = [] for (var compState of this.getStatesList(withComponent)) { var pdat = this.getPositionData(compState.__id) if (pdat) entStates.push(pdat) } } else { entStates = this.getStatesList(this.names.position) } // run each test var hits = [] for (var i = 0; i < entStates.length; i++) { var state = entStates[i] if (extentsOverlap(testExtents, state._extents)) { hits.push(state.__id) } } return hits } /** * Helper to set up a general entity, and populate with some common components depending on arguments. */ add(position = null, width = 1, height = 1, mesh = null, meshOffset = null, doPhysics = false, shadow = false) { var self = this // new entity var eid = this.createEntity() // position component this.addComponent(eid, this.names.position, { position: position || vec3.create(), width: width, height: height, }) // rigid body in physics simulator if (doPhysics) { // body = this.noa.physics.addBody(box) this.addComponent(eid, this.names.physics) var body = this.getPhysics(eid).body // handler for physics engine to call on auto-step var smoothName = this.names.smoothCamera body.onStep = function () { self.addComponentAgain(eid, smoothName) } } // mesh for the entity if (mesh) { if (!meshOffset) meshOffset = vec3.create() this.addComponent(eid, this.names.mesh, { mesh: mesh, offset: meshOffset }) } // add shadow-drawing component if (shadow) { this.addComponent(eid, this.names.shadow, { size: width }) } return eid } } /* * * * * HELPERS * * * */ // safety helper - when rebasing, nudge extent away from // voxel boudaries, so floating point error doesn't carry us accross function nudgePosition(pos, index, dmin, dmax, id) { var min = pos[index] + dmin var max = pos[index] + dmax if (Math.abs(min - Math.round(min)) < 0.002) pos[index] += 0.002 if (Math.abs(max - Math.round(max)) < 0.001) pos[index] -= 0.001 } // compare extent arrays function extentsOverlap(extA, extB) { if (extA[0] > extB[3]) return false if (extA[1] > extB[4]) return false if (extA[2] > extB[5]) return false if (extA[3] < extB[0]) return false if (extA[4] < extB[1]) return false if (extA[5] < extB[2]) return false return true }