├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── .vscode └── settings.json ├── LICENSE.txt ├── README.md ├── dist └── src │ ├── components │ ├── collideEntities.d.ts │ ├── collideTerrain.d.ts │ ├── fadeOnZoom.d.ts │ ├── followsEntity.d.ts │ ├── mesh.d.ts │ ├── movement.d.ts │ ├── physics.d.ts │ ├── position.d.ts │ ├── receivesInputs.d.ts │ ├── shadow.d.ts │ └── smoothCamera.d.ts │ ├── index.d.ts │ └── lib │ ├── camera.d.ts │ ├── chunk.d.ts │ ├── container.d.ts │ ├── entities.d.ts │ ├── inputs.d.ts │ ├── objectMesher.d.ts │ ├── physics.d.ts │ ├── registry.d.ts │ ├── rendering.d.ts │ ├── sceneOctreeManager.d.ts │ ├── shims.d.ts │ ├── terrainMaterials.d.ts │ ├── terrainMesher.d.ts │ ├── util.d.ts │ └── world.d.ts ├── docs ├── .nojekyll ├── API │ ├── .nojekyll │ ├── assets │ │ ├── highlight.css │ │ ├── main.js │ │ ├── search.js │ │ └── style.css │ ├── classes │ │ ├── Engine.html │ │ ├── _internal_.BlockOptions.html │ │ ├── _internal_.Camera.html │ │ ├── _internal_.CameraDefaults.html │ │ ├── _internal_.Chunk.html │ │ ├── _internal_.Container.html │ │ ├── _internal_.DefaultOptions.html │ │ ├── _internal_.ECS.html │ │ ├── _internal_.Entities.html │ │ ├── _internal_.EventEmitter-1.html │ │ ├── _internal_.GameInputs.html │ │ ├── _internal_.Inputs.html │ │ ├── _internal_.MaterialOptions.html │ │ ├── _internal_.MovementState.html │ │ ├── _internal_.ObjectMesher.html │ │ ├── _internal_.Physics-1.html │ │ ├── _internal_.Physics.html │ │ ├── _internal_.PhysicsState.html │ │ ├── _internal_.PositionState.html │ │ ├── _internal_.Registry.html │ │ ├── _internal_.Rendering.html │ │ ├── _internal_.RigidBody.html │ │ ├── _internal_.TerrainMesher.html │ │ └── _internal_.World.html │ ├── functions │ │ ├── _internal_.EventEmitter.init.html │ │ ├── _internal_.EventEmitter.once.html │ │ ├── _internal_._gl_vec3_.add.html │ │ ├── _internal_._gl_vec3_.angle.html │ │ ├── _internal_._gl_vec3_.ceil.html │ │ ├── _internal_._gl_vec3_.clone.html │ │ ├── _internal_._gl_vec3_.copy.html │ │ ├── _internal_._gl_vec3_.create.html │ │ ├── _internal_._gl_vec3_.cross.html │ │ ├── _internal_._gl_vec3_.dist.html │ │ ├── _internal_._gl_vec3_.distance.html │ │ ├── _internal_._gl_vec3_.div.html │ │ ├── _internal_._gl_vec3_.divide.html │ │ ├── _internal_._gl_vec3_.dot.html │ │ ├── _internal_._gl_vec3_.equals.html │ │ ├── _internal_._gl_vec3_.exactEquals.html │ │ ├── _internal_._gl_vec3_.floor.html │ │ ├── _internal_._gl_vec3_.forEach.html │ │ ├── _internal_._gl_vec3_.fromValues.html │ │ ├── _internal_._gl_vec3_.inverse.html │ │ ├── _internal_._gl_vec3_.len.html │ │ ├── _internal_._gl_vec3_.length.html │ │ ├── _internal_._gl_vec3_.lerp.html │ │ ├── _internal_._gl_vec3_.max.html │ │ ├── _internal_._gl_vec3_.min.html │ │ ├── _internal_._gl_vec3_.mul.html │ │ ├── _internal_._gl_vec3_.multiply.html │ │ ├── _internal_._gl_vec3_.negate.html │ │ ├── _internal_._gl_vec3_.normalize.html │ │ ├── _internal_._gl_vec3_.random.html │ │ ├── _internal_._gl_vec3_.rotateX.html │ │ ├── _internal_._gl_vec3_.rotateY.html │ │ ├── _internal_._gl_vec3_.rotateZ.html │ │ ├── _internal_._gl_vec3_.round.html │ │ ├── _internal_._gl_vec3_.scale.html │ │ ├── _internal_._gl_vec3_.scaleAndAdd.html │ │ ├── _internal_._gl_vec3_.set.html │ │ ├── _internal_._gl_vec3_.sqrDist.html │ │ ├── _internal_._gl_vec3_.sqrLen.html │ │ ├── _internal_._gl_vec3_.squaredDistance.html │ │ ├── _internal_._gl_vec3_.squaredLength.html │ │ ├── _internal_._gl_vec3_.sub.html │ │ ├── _internal_._gl_vec3_.subtract.html │ │ ├── _internal_._gl_vec3_.transformMat3.html │ │ ├── _internal_._gl_vec3_.transformMat4.html │ │ └── _internal_._gl_vec3_.transformQuat.html │ ├── index.html │ ├── interfaces │ │ └── _internal_.MatDef.html │ ├── modules.html │ ├── modules │ │ ├── _internal_.EventEmitter.html │ │ ├── _internal_._gl_vec3_.html │ │ └── _internal_.html │ └── variables │ │ ├── _internal_.EventEmitter.defaultMaxListeners.html │ │ └── _internal_._gl_vec3_.EPSILON.html ├── api-header.md ├── components.md ├── history.md └── positions.md ├── package-lock.json ├── package.json ├── src ├── components │ ├── collideEntities.js │ ├── collideTerrain.js │ ├── fadeOnZoom.js │ ├── followsEntity.js │ ├── mesh.js │ ├── movement.js │ ├── physics.js │ ├── position.js │ ├── receivesInputs.js │ ├── shadow.js │ └── smoothCamera.js ├── index.js └── lib │ ├── camera.js │ ├── chunk.js │ ├── container.js │ ├── entities.js │ ├── inputs.js │ ├── objectMesher.js │ ├── physics.js │ ├── registry.js │ ├── rendering.js │ ├── sceneOctreeManager.js │ ├── shims.js │ ├── terrainMaterials.js │ ├── terrainMesher.js │ ├── util.js │ └── world.js ├── tsconfig.json └── types ├── aabb-3d └── index.d.ts ├── ent-comp └── index.d.ts ├── events └── index.d.ts └── gl-vec3 └── index.d.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_size = 4 7 | indent_style = space 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "root": true, 3 | "extends": [ 4 | "eslint:recommended", 5 | ], 6 | plugins: [], 7 | "env": { 8 | "node": true, 9 | "browser": true, 10 | "es6": true, 11 | }, 12 | "parserOptions": { 13 | "ecmaVersion": 13, 14 | "sourceType": "module", 15 | }, 16 | "rules": { 17 | "strict": ["error", "global"], 18 | 19 | "no-unused-vars": ["warn", { "args": "none" }], 20 | "no-empty": "off", 21 | "no-console": "off", 22 | "no-return-await": "error", 23 | 24 | "semi": ["error", "never"], 25 | "no-unexpected-multiline": "error", 26 | 27 | }, 28 | } 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | node_modules 3 | *-error.log 4 | ignore/ 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "search.exclude": { 3 | "docs": true, 4 | "dist": true, 5 | "node_modules": true, 6 | "types": true, 7 | "ignore": true, 8 | }, 9 | "editor.detectIndentation": true, 10 | "[json]": { 11 | "editor.defaultFormatter": "vscode.json-language-features" 12 | }, 13 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2022 Andy Hall (andy@fenomas.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # noa-engine 3 | 4 | An experimental voxel game engine. 5 | 6 | Some projects using `noa`: 7 | * [bloxd.io](https://bloxd.io/) - multiplayer voxel games with editable worlds, by [Arthur](https://github.com/MCArth) 8 | * [Minecraft Classic](https://classic.minecraft.net/) - from Mojang (I'm as surprised as you are) 9 | * [VoxelSrv](https://github.com/Patbox/voxelsrv) - a voxel game inspired by Minecraft, by [patbox](https://github.com/Patbox) 10 | * [CityCraft.io](https://citycraft.io/) - multiplayer voxel cities, by [raoneel](https://github.com/raoneel) 11 | * [OPCraft](https://github.com/latticexyz/opcraft) - a voxel game running on Ethereum smart contracts, by [Lattice](https://github.com/latticexyz) 12 | * [noa-examples](https://github.com/fenomas/noa-examples) - starter repo with minimal hello-world and testbed games 13 | 14 | 15 | ---- 16 | 17 | ## Usage 18 | 19 | The easiest way to start building a game with `noa` is to clone the 20 | [examples](https://github.com/fenomas/noa-examples) repo and start hacking 21 | on the code there. The comments in the `hello-world` example source walk 22 | through how to instantiate the engine, define world geometry, and so forth. 23 | The example repo also shows the intended way to import noa's 24 | peer dependencies, test a world, build for production, etc. 25 | 26 | 27 | ## Docs 28 | 29 | See the [API reference](https://fenomas.github.io/noa/API/) 30 | for engine classes and methods. 31 | 32 | Documentation PRs are welcome! See the source for details, API docs 33 | are generated automatically via `npm run docs`. 34 | 35 | 36 | ## Status, contributing, etc. 37 | 38 | This engine is under active development and contributions are welcome. 39 | Please open a discussion issue before submitting large changes. 40 | **PRs should be sent against the `develop` branch!** 41 | 42 | Code style/formatting are set up with config files and dev dependencies, 43 | if you use VSCode most of it should work automatically. If you send PRs, 44 | please try to be sorta-kinda consistent with what's already there. 45 | 46 | 47 | 48 | ## Change logs 49 | 50 | See [history.md](docs/history.md) for full changes and migration for each version. 51 | 52 | Recent changes: 53 | 54 | * `v0.33`: 55 | * Much improved [API docs](https://fenomas.github.io/noa/API/) 56 | * Terrain now supports texture atlases! See `registry.registerMaterial`. 57 | * Added a fast way to specify that a worldgen chunk is entirely air/dirt/etc. 58 | * Modernized keybinds to use [KeyboardEvent.code](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code) strings, and changed several binding state properties 59 | * Bunch of internal improvements to support shadows - see [examples](https://github.com/fenomas/noa-examples) 60 | 61 | * `v0.32`: Fixes npm versioning issue - no code changes. 62 | * `v0.31`: 63 | * Change the speed of the world! See `noa.timeScale` 64 | * Now possible to control chunk processing order: `noa.world.chunkSortingDistFn` 65 | * Much improved type exports and [API docs](https://fenomas.github.io/noa/API/) 66 | * `v0.30`: 67 | * Engine now a named export, use `import {Engine} from 'noa-engine'` 68 | * many performance and size optimizations 69 | * now generates proper type declarations and API references! 70 | * can now configure separate vert/horiz values for chunk load distance 71 | * core option `tickRate` is now in **ticks per second**, not ms per tick 72 | * adds several init options, e.g. `maxRenderRate`, `stickyFullscreen` 73 | * `v0.29`: 74 | * maximum voxel ID is now `65535` 75 | * adds option `worldGenWhilePaused` 76 | * adds option `manuallyControlChunkLoading` and related APIs 77 | * performance and bug fixes 78 | 79 | 80 | ---- 81 | 82 | ## Credits 83 | 84 | Made with 🍺 by [@fenomas](https://fenomas.com), license is [MIT](LICENSE.txt). 85 | 86 | Uses [Babylon.js](https://www.babylonjs.com/) for 3D rendering. 87 | -------------------------------------------------------------------------------- /dist/src/components/collideEntities.d.ts: -------------------------------------------------------------------------------- 1 | export default function _default(noa: any): { 2 | name: string; 3 | order: number; 4 | state: { 5 | cylinder: boolean; 6 | collideBits: number; 7 | collideMask: number; 8 | callback: any; 9 | }; 10 | onAdd: any; 11 | onRemove: any; 12 | system: (dt: any, states: any) => void; 13 | }; 14 | -------------------------------------------------------------------------------- /dist/src/components/collideTerrain.d.ts: -------------------------------------------------------------------------------- 1 | export default function _default(noa: any): { 2 | name: string; 3 | order: number; 4 | state: { 5 | callback: any; 6 | }; 7 | onAdd: (eid: any, state: any) => void; 8 | onRemove: (eid: any, state: any) => void; 9 | }; 10 | -------------------------------------------------------------------------------- /dist/src/components/fadeOnZoom.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Component for the player entity, when active hides the player's mesh 3 | * when camera zoom is less than a certain amount 4 | */ 5 | export default function _default(noa: any): { 6 | name: string; 7 | order: number; 8 | state: { 9 | cutoff: number; 10 | }; 11 | onAdd: any; 12 | onRemove: any; 13 | system: (dt: any, states: any) => void; 14 | }; 15 | -------------------------------------------------------------------------------- /dist/src/components/followsEntity.d.ts: -------------------------------------------------------------------------------- 1 | export default function _default(noa: any): { 2 | name: string; 3 | order: number; 4 | state: { 5 | entity: number; 6 | offset: any; 7 | onTargetMissing: any; 8 | }; 9 | onAdd: (eid: any, state: any) => void; 10 | onRemove: any; 11 | system: (dt: any, states: any) => void; 12 | renderSystem: (dt: any, states: any) => void; 13 | }; 14 | -------------------------------------------------------------------------------- /dist/src/components/mesh.d.ts: -------------------------------------------------------------------------------- 1 | export default function _default(noa: any): { 2 | name: string; 3 | order: number; 4 | state: { 5 | mesh: any; 6 | offset: any; 7 | }; 8 | onAdd: (eid: any, state: any) => void; 9 | onRemove: (eid: any, state: any) => void; 10 | renderSystem: (dt: any, states: any) => void; 11 | }; 12 | -------------------------------------------------------------------------------- /dist/src/components/movement.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * State object of the `movement` component 4 | * 5 | */ 6 | export function MovementState(): void; 7 | export class MovementState { 8 | heading: number; 9 | running: boolean; 10 | jumping: boolean; 11 | maxSpeed: number; 12 | moveForce: number; 13 | responsiveness: number; 14 | runningFriction: number; 15 | standingFriction: number; 16 | airMoveMult: number; 17 | jumpImpulse: number; 18 | jumpForce: number; 19 | jumpTime: number; 20 | airJumps: number; 21 | _jumpCount: number; 22 | _currjumptime: number; 23 | _isJumping: boolean; 24 | } 25 | /** 26 | * Movement component. State stores settings like jump height, etc., 27 | * as well as current state (running, jumping, heading angle). 28 | * Processor checks state and applies movement/friction/jump forces 29 | * to the entity's physics body. 30 | * @param {import('..').Engine} noa 31 | */ 32 | export default function _default(noa: import('..').Engine): { 33 | name: string; 34 | order: number; 35 | state: MovementState; 36 | onAdd: any; 37 | onRemove: any; 38 | system: (dt: any, states: any) => void; 39 | }; 40 | -------------------------------------------------------------------------------- /dist/src/components/physics.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Physics component, stores an entity's physics engbody. 3 | * @param {import('..').Engine} noa 4 | */ 5 | export default function _default(noa: import('..').Engine): { 6 | name: string; 7 | order: number; 8 | state: PhysicsState; 9 | onAdd: (entID: any, state: any) => void; 10 | onRemove: (entID: any, state: any) => void; 11 | system: (dt: any, states: any) => void; 12 | renderSystem: (dt: any, states: any) => void; 13 | }; 14 | export function setPhysicsFromPosition(physState: any, posState: any): void; 15 | export class PhysicsState { 16 | /** @type {import('voxel-physics-engine').RigidBody} */ 17 | body: import('voxel-physics-engine').RigidBody; 18 | } 19 | -------------------------------------------------------------------------------- /dist/src/components/position.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Component holding entity's position, width, and height. 3 | * By convention, entity's "position" is the bottom center of its AABB 4 | * 5 | * Of the various properties, _localPosition is the "real", 6 | * single-source-of-truth position. Others are derived. 7 | * Local coords are relative to `noa.worldOriginOffset`. 8 | * @param {import('..').Engine} noa 9 | */ 10 | export default function _default(noa: import('..').Engine): { 11 | name: string; 12 | order: number; 13 | state: PositionState; 14 | onAdd: (eid: any, state: any) => void; 15 | onRemove: any; 16 | system: (dt: any, states: any) => void; 17 | }; 18 | export function updatePositionExtents(state: any): void; 19 | export class PositionState { 20 | /** Position in global coords (may be low precision) 21 | * @type {null | number[]} */ 22 | position: null | number[]; 23 | width: number; 24 | height: number; 25 | /** Precise position in local coords 26 | * @type {null | number[]} */ 27 | _localPosition: null | number[]; 28 | /** [x,y,z] in LOCAL COORDS 29 | * @type {null | number[]} */ 30 | _renderPosition: null | number[]; 31 | /** [lo,lo,lo, hi,hi,hi] in LOCAL COORDS 32 | * @type {null | number[]} */ 33 | _extents: null | number[]; 34 | } 35 | -------------------------------------------------------------------------------- /dist/src/components/receivesInputs.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Input processing component - gets (key) input state and 4 | * applies it to receiving entities by updating their movement 5 | * component state (heading, movespeed, jumping, etc.) 6 | * 7 | */ 8 | export default function _default(noa: any): { 9 | name: string; 10 | order: number; 11 | state: {}; 12 | onAdd: any; 13 | onRemove: any; 14 | system: (dt: any, states: any) => void; 15 | }; 16 | -------------------------------------------------------------------------------- /dist/src/components/shadow.d.ts: -------------------------------------------------------------------------------- 1 | /** @param {import('../index').Engine} noa */ 2 | export default function _default(noa: import('../index').Engine, distance?: number): { 3 | name: string; 4 | order: number; 5 | state: { 6 | size: number; 7 | _mesh: any; 8 | }; 9 | onAdd: (eid: any, state: any) => void; 10 | onRemove: (eid: any, state: any) => void; 11 | system: (dt: any, states: any) => void; 12 | renderSystem: (dt: any, states: any) => void; 13 | }; 14 | -------------------------------------------------------------------------------- /dist/src/components/smoothCamera.d.ts: -------------------------------------------------------------------------------- 1 | export default function _default(noa: any): { 2 | name: string; 3 | order: number; 4 | state: { 5 | time: number; 6 | }; 7 | onAdd: any; 8 | onRemove: any; 9 | system: (dt: any, states: any) => void; 10 | }; 11 | -------------------------------------------------------------------------------- /dist/src/index.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Main engine class. 3 | * Takes an object full of optional settings as a parameter. 4 | * 5 | * ```js 6 | * import { Engine } from 'noa-engine' 7 | * var noa = new Engine({ 8 | * debug: false, 9 | * }) 10 | * ``` 11 | * 12 | * Note that the options object is also passed to noa's 13 | * child modules ({@link Rendering}, {@link Container}, etc). 14 | * See docs for each module for their options. 15 | * 16 | */ 17 | export class Engine extends EventEmitter { 18 | /** 19 | * The core Engine constructor uses the following options: 20 | * 21 | * ```js 22 | * var defaultOptions = { 23 | * debug: false, 24 | * silent: false, 25 | * playerHeight: 1.8, 26 | * playerWidth: 0.6, 27 | * playerStart: [0, 10, 0], 28 | * playerAutoStep: false, 29 | * playerShadowComponent: true, 30 | * tickRate: 30, // ticks per second 31 | * maxRenderRate: 0, // max FPS, 0 for uncapped 32 | * blockTestDistance: 10, 33 | * stickyPointerLock: true, 34 | * dragCameraOutsidePointerLock: true, 35 | * stickyFullscreen: false, 36 | * skipDefaultHighlighting: false, 37 | * originRebaseDistance: 25, 38 | * } 39 | * ``` 40 | * 41 | * **Events:** 42 | * + `tick => (dt)` 43 | * Tick update, `dt` is (fixed) tick duration in ms 44 | * + `beforeRender => (dt)` 45 | * `dt` is the time (in ms) since the most recent tick 46 | * + `afterRender => (dt)` 47 | * `dt` is the time (in ms) since the most recent tick 48 | * + `targetBlockChanged => (blockInfo)` 49 | * Emitted each time the user's targeted world block changes 50 | * + `addingTerrainMesh => (mesh)` 51 | * Alerts client about a terrain mesh being added to the scene 52 | * + `removingTerrainMesh => (mesh)` 53 | * Alerts client before a terrain mesh is removed. 54 | */ 55 | constructor(opts?: {}); 56 | /** Version string, e.g. `"0.25.4"` */ 57 | version: string; 58 | /** @internal */ 59 | _paused: boolean; 60 | /** @internal */ 61 | _originRebaseDistance: any; 62 | /** @internal */ 63 | worldOriginOffset: number[]; 64 | /** @internal */ 65 | positionInCurrentTick: number; 66 | /** 67 | * String identifier for the current world. 68 | * It's safe to ignore this if your game has only one level/world. 69 | */ 70 | worldName: string; 71 | /** 72 | * Multiplier for how fast time moves. Setting this to a value other than 73 | * `1` will make the game speed up or slow down. This can significantly 74 | * affect how core systems behave (particularly physics!). 75 | */ 76 | timeScale: number; 77 | /** Child module for managing the game's container, canvas, etc. */ 78 | container: Container; 79 | /** The game's tick rate (number of ticks per second) 80 | * @type {number} 81 | * @readonly 82 | */ 83 | readonly tickRate: number; 84 | /** The game's max framerate (use `0` for uncapped) 85 | * @type {number} 86 | */ 87 | maxRenderRate: number; 88 | /** Manages key and mouse input bindings */ 89 | inputs: Inputs; 90 | /** A registry where voxel/material properties are managed */ 91 | registry: Registry; 92 | /** Manages the world, chunks, and all voxel data */ 93 | world: World; 94 | /** Rendering manager */ 95 | rendering: Rendering; 96 | /** Physics engine - solves collisions, properties, etc. */ 97 | physics: Physics; 98 | /** Entity manager / Entity Component System (ECS) */ 99 | entities: Entities; 100 | /** Alias to `noa.entities` */ 101 | ents: Entities; 102 | /** Entity id for the player entity */ 103 | playerEntity: number; 104 | /** Manages the game's camera, view angle, sensitivity, etc. */ 105 | camera: Camera; 106 | /** How far to check for a solid voxel the player is currently looking at 107 | * @type {number} 108 | */ 109 | blockTestDistance: number; 110 | /** 111 | * Callback to determine which voxels can be targeted. 112 | * Defaults to a solidity check, but can be overridden with arbitrary logic. 113 | * @type {(blockID: number) => boolean} 114 | */ 115 | blockTargetIdCheck: (blockID: number) => boolean; 116 | /** 117 | * Dynamically updated object describing the currently targeted block. 118 | * @type {null | { 119 | * blockID:number, 120 | * position: number[], 121 | * normal: number[], 122 | * adjacent: number[], 123 | * }} 124 | */ 125 | targetedBlock: { 126 | blockID: number; 127 | position: number[]; 128 | normal: number[]; 129 | adjacent: number[]; 130 | }; 131 | defaultBlockHighlightFunction: (tgt: any) => void; 132 | /** @internal */ 133 | _terrainMesher: TerrainMesher; 134 | /** @internal */ 135 | _objectMesher: ObjectMesher; 136 | /** @internal */ 137 | _targetedBlockDat: { 138 | blockID: number; 139 | position: any; 140 | normal: any; 141 | adjacent: any; 142 | }; 143 | /** @internal */ 144 | _prevTargetHash: number; 145 | /** @internal */ 146 | _pickPos: any; 147 | /** @internal */ 148 | _pickResult: { 149 | _localPosition: any; 150 | position: number[]; 151 | normal: number[]; 152 | }; 153 | /** @internal */ 154 | vec3: typeof vec3; 155 | /** @internal */ 156 | ndarray: any; 157 | /** 158 | * Tick function, called by container module at a fixed timestep. 159 | * Clients should not normally need to call this manually. 160 | * @internal 161 | */ 162 | tick(dt: any): void; 163 | /** 164 | * Render function, called every animation frame. Emits #beforeRender(dt), #afterRender(dt) 165 | * where dt is the time in ms *since the last tick*. 166 | * Clients should not normally need to call this manually. 167 | * @internal 168 | */ 169 | render(dt: any, framePart: any): void; 170 | /** Pausing the engine will also stop render/tick events, etc. */ 171 | setPaused(paused?: boolean): void; 172 | /** 173 | * Get the voxel ID at the specified position 174 | */ 175 | getBlock(x: any, y?: number, z?: number): any; 176 | /** 177 | * Sets the voxel ID at the specified position. 178 | * Does not check whether any entities are in the way! 179 | */ 180 | setBlock(id: any, x: any, y?: number, z?: number): void; 181 | /** 182 | * Adds a block, unless there's an entity in the way. 183 | */ 184 | addBlock(id: any, x: any, y?: number, z?: number): any; 185 | /** 186 | * Precisely converts a world position to the current internal 187 | * local frame of reference. 188 | * 189 | * See `/docs/positions.md` for more info. 190 | * 191 | * Params: 192 | * * `global`: input position in global coords 193 | * * `globalPrecise`: (optional) sub-voxel offset to the global position 194 | * * `local`: output array which will receive the result 195 | */ 196 | globalToLocal(global: any, globalPrecise: any, local: any): any; 197 | /** 198 | * Precisely converts a world position to the current internal 199 | * local frame of reference. 200 | * 201 | * See `/docs/positions.md` for more info. 202 | * 203 | * Params: 204 | * * `local`: input array of local coords 205 | * * `global`: output array which receives the result 206 | * * `globalPrecise`: (optional) sub-voxel offset to the output global position 207 | * 208 | * If both output arrays are passed in, `global` will get int values and 209 | * `globalPrecise` will get fractional parts. If only one array is passed in, 210 | * `global` will get the whole output position. 211 | */ 212 | localToGlobal(local: any, global: any, globalPrecise?: any): any; 213 | /** 214 | * Raycast through the world, returning a result object for any non-air block 215 | * 216 | * See `/docs/positions.md` for info on working with precise positions. 217 | * 218 | * @param {number[]} pos where to pick from (default: player's eye pos) 219 | * @param {number[]} dir direction to pick along (default: camera vector) 220 | * @param {number} dist pick distance (default: `noa.blockTestDistance`) 221 | * @param {(id:number) => boolean} blockTestFunction which voxel IDs can be picked (default: any solid voxel) 222 | */ 223 | pick(pos?: number[], dir?: number[], dist?: number, blockTestFunction?: (id: number) => boolean): { 224 | position: number[]; 225 | normal: number[]; 226 | _localPosition: number[]; 227 | }; 228 | /** 229 | * @internal 230 | * Do a raycast in local coords. 231 | * See `/docs/positions.md` for more info. 232 | * @param {number[]} pos where to pick from (default: player's eye pos) 233 | * @param {number[]} dir direction to pick along (default: camera vector) 234 | * @param {number} dist pick distance (default: `noa.blockTestDistance`) 235 | * @param {(id:number) => boolean} blockTestFunction which voxel IDs can be picked (default: any solid voxel) 236 | * @returns { null | { 237 | * position: number[], 238 | * normal: number[], 239 | * _localPosition: number[], 240 | * }} 241 | */ 242 | _localPick(pos?: number[], dir?: number[], dist?: number, blockTestFunction?: (id: number) => boolean): null | { 243 | position: number[]; 244 | normal: number[]; 245 | _localPosition: number[]; 246 | }; 247 | } 248 | import { EventEmitter } from 'events'; 249 | import { Container } from './lib/container'; 250 | import { Inputs } from './lib/inputs'; 251 | import { Registry } from './lib/registry'; 252 | import { World } from './lib/world'; 253 | import { Rendering } from './lib/rendering'; 254 | import { Physics } from './lib/physics'; 255 | import { Entities } from './lib/entities'; 256 | import { Camera } from './lib/camera'; 257 | import { TerrainMesher } from './lib/terrainMesher'; 258 | import { ObjectMesher } from './lib/objectMesher'; 259 | import vec3 from 'gl-vec3'; 260 | -------------------------------------------------------------------------------- /dist/src/lib/camera.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * `noa.camera` - manages the camera, its position and direction, 3 | * mouse sensitivity, and so on. 4 | * 5 | * This module uses the following default options (from the options 6 | * object passed to the {@link Engine}): 7 | * ```js 8 | * var defaults = { 9 | * inverseX: false, 10 | * inverseY: false, 11 | * sensitivityX: 10, 12 | * sensitivityY: 10, 13 | * initialZoom: 0, 14 | * zoomSpeed: 0.2, 15 | * } 16 | * ``` 17 | */ 18 | export class Camera { 19 | /** 20 | * @internal 21 | * @param {import('../index').Engine} noa 22 | * @param {Partial.} opts 23 | */ 24 | constructor(noa: import('../index').Engine, opts: Partial); 25 | noa: import("../index").Engine; 26 | /** Horizontal mouse sensitivity. Same scale as Overwatch (typical values around `5..10`) */ 27 | sensitivityX: number; 28 | /** Vertical mouse sensitivity. Same scale as Overwatch (typical values around `5..10`) */ 29 | sensitivityY: number; 30 | /** Mouse look inverse (horizontal) */ 31 | inverseX: boolean; 32 | /** Mouse look inverse (vertical) */ 33 | inverseY: boolean; 34 | /** 35 | * Multiplier for temporarily altering mouse sensitivity. 36 | * Set this to `0` to temporarily disable camera controls. 37 | */ 38 | sensitivityMult: number; 39 | /** 40 | * Multiplier for altering mouse sensitivity when pointerlock 41 | * is not active - default of `0` means no camera movement. 42 | * Note this setting is ignored if pointerLock isn't supported. 43 | */ 44 | sensitivityMultOutsidePointerlock: number; 45 | /** 46 | * Camera yaw angle. 47 | * Returns the camera's rotation angle around the vertical axis. 48 | * Range: `0..2π` 49 | * This value is writeable, but it's managed by the engine and 50 | * will be overwritten each frame. 51 | */ 52 | heading: number; 53 | /** Camera pitch angle. 54 | * Returns the camera's up/down rotation angle. The pitch angle is 55 | * clamped by a small epsilon, such that the camera never quite 56 | * points perfectly up or down. 57 | * Range: `-π/2..π/2`. 58 | * This value is writeable, but it's managed by the engine and 59 | * will be overwritten each frame. 60 | */ 61 | pitch: number; 62 | /** 63 | * Entity ID of a special entity that exists for the camera to point at. 64 | * 65 | * By default this entity follows the player entity, so you can 66 | * change the player's eye height by changing the `follow` component's offset: 67 | * ```js 68 | * var followState = noa.ents.getState(noa.camera.cameraTarget, 'followsEntity') 69 | * followState.offset[1] = 0.9 * myPlayerHeight 70 | * ``` 71 | * 72 | * For customized camera controls you can change the follow 73 | * target to some other entity, or override the behavior entirely: 74 | * ```js 75 | * // make cameraTarget stop following the player 76 | * noa.ents.removeComponent(noa.camera.cameraTarget, 'followsEntity') 77 | * // control cameraTarget position directly (or whatever..) 78 | * noa.ents.setPosition(noa.camera.cameraTarget, [x,y,z]) 79 | * ``` 80 | */ 81 | cameraTarget: number; 82 | /** How far back the camera should be from the player's eye position */ 83 | zoomDistance: number; 84 | /** How quickly the camera moves to its `zoomDistance` (0..1) */ 85 | zoomSpeed: number; 86 | /** Current actual zoom distance. This differs from `zoomDistance` when 87 | * the camera is in the process of moving towards the desired distance, 88 | * or when it's obstructed by solid terrain behind the player. 89 | * This value will get overwritten each tick, but you may want to write to it 90 | * when overriding the camera zoom speed. 91 | */ 92 | currentZoom: number; 93 | /** @internal */ 94 | _dirVector: any; 95 | /** @internal */ 96 | _localGetTargetPosition(): any; 97 | /** @internal */ 98 | _localGetPosition(): any; 99 | /** 100 | * Returns the camera's current target position - i.e. the player's 101 | * eye position. When the camera is zoomed all the way in, 102 | * this returns the same location as `camera.getPosition()`. 103 | */ 104 | getTargetPosition(): any; 105 | /** 106 | * Returns the current camera position (read only) 107 | */ 108 | getPosition(): any; 109 | /** 110 | * Returns the camera direction vector (read only) 111 | */ 112 | getDirection(): any; 113 | /** 114 | * Called before render, if mouseLock etc. is applicable. 115 | * Applies current mouse x/y inputs to the camera angle and zoom 116 | * @internal 117 | */ 118 | applyInputsToCamera(): void; 119 | /** 120 | * Called before all renders, pre- and post- entity render systems 121 | * @internal 122 | */ 123 | updateBeforeEntityRenderSystems(): void; 124 | /** @internal */ 125 | updateAfterEntityRenderSystems(): void; 126 | } 127 | declare function CameraDefaults(): void; 128 | declare class CameraDefaults { 129 | inverseX: boolean; 130 | inverseY: boolean; 131 | sensitivityMult: number; 132 | sensitivityMultOutsidePointerlock: number; 133 | sensitivityX: number; 134 | sensitivityY: number; 135 | initialZoom: number; 136 | zoomSpeed: number; 137 | } 138 | export {}; 139 | -------------------------------------------------------------------------------- /dist/src/lib/chunk.d.ts: -------------------------------------------------------------------------------- 1 | /** @param {import('../index').Engine} noa */ 2 | export function Chunk(noa: import('../index').Engine, requestID: any, ci: any, cj: any, ck: any, size: any, dataArray: any, fillVoxelID?: number): void; 3 | export class Chunk { 4 | /** @param {import('../index').Engine} noa */ 5 | constructor(noa: import('../index').Engine, requestID: any, ci: any, cj: any, ck: any, size: any, dataArray: any, fillVoxelID?: number); 6 | noa: import("../index").Engine; 7 | isDisposed: boolean; 8 | userData: any; 9 | requestID: any; 10 | voxels: any; 11 | i: any; 12 | j: any; 13 | k: any; 14 | size: any; 15 | x: number; 16 | y: number; 17 | z: number; 18 | pos: number[]; 19 | _terrainDirty: boolean; 20 | _objectsDirty: boolean; 21 | _terrainMeshes: any[]; 22 | _isFull: boolean; 23 | _isEmpty: boolean; 24 | _wholeLayerVoxel: any[]; 25 | _neighbors: any; 26 | _neighborCount: number; 27 | _timesMeshed: number; 28 | /** @internal */ 29 | _blockHandlerLocs: LocationQueue; 30 | _updateVoxelArray(dataArray: any, fillVoxelID?: number): void; 31 | get(i: any, j: any, k: any): any; 32 | getSolidityAt(i: any, j: any, k: any): boolean; 33 | set(i: any, j: any, k: any, newID: any): void; 34 | updateMeshes(): void; 35 | dispose(): void; 36 | } 37 | export namespace Chunk { 38 | function _createVoxelArray(size: any): any; 39 | } 40 | import { LocationQueue } from './util'; 41 | -------------------------------------------------------------------------------- /dist/src/lib/container.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * `noa.container` - manages the game's HTML container element, canvas, 3 | * fullscreen, pointerLock, and so on. 4 | * 5 | * This module wraps `micro-game-shell`, which does most of the implementation. 6 | * 7 | * **Events** 8 | * + `DOMready => ()` 9 | * Relays the browser DOMready event, after noa does some initialization 10 | * + `gainedPointerLock => ()` 11 | * Fires when the game container gains pointerlock. 12 | * + `lostPointerLock => ()` 13 | * Fires when the game container loses pointerlock. 14 | */ 15 | export class Container extends EventEmitter { 16 | /** @internal */ 17 | constructor(noa: any, opts: any); 18 | /** 19 | * @internal 20 | * @type {import('../index').Engine} 21 | */ 22 | noa: import('../index').Engine; 23 | element: any; 24 | /** The `canvas` element that the game will draw into */ 25 | canvas: any; 26 | /** Whether the browser supports pointerLock. @readonly */ 27 | supportsPointerLock: boolean; 28 | /** Whether the user's pointer is within the game area. @readonly */ 29 | pointerInGame: boolean; 30 | /** Whether the game is focused. @readonly */ 31 | isFocused: boolean; 32 | /** Gets the current state of pointerLock. @readonly */ 33 | hasPointerLock: boolean; 34 | /** @internal */ 35 | _shell: MicroGameShell; 36 | /** @internal */ 37 | appendTo(htmlElement: any): void; 38 | /** 39 | * Sets whether `noa` should try to acquire or release pointerLock 40 | */ 41 | setPointerLock(lock?: boolean): void; 42 | } 43 | import { EventEmitter } from 'events'; 44 | import { MicroGameShell } from 'micro-game-shell'; 45 | -------------------------------------------------------------------------------- /dist/src/lib/entities.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * `noa.entities` - manages entities and components. 3 | * 4 | * This class extends [ent-comp](https://github.com/fenomas/ent-comp), 5 | * a general-purpose ECS. It's also decorated with noa-specific helpers and 6 | * accessor functions for querying entity positions, etc. 7 | * 8 | * Expects entity definitions in a specific format - see source `components` 9 | * folder for examples. 10 | * 11 | * This module uses the following default options (from the options 12 | * object passed to the {@link Engine}): 13 | * 14 | * ```js 15 | * var defaults = { 16 | * shadowDistance: 10, 17 | * } 18 | * ``` 19 | */ 20 | export class Entities extends ECS { 21 | /** @internal */ 22 | constructor(noa: any, opts: any); 23 | /** 24 | * @internal 25 | * @type {import('../index').Engine} 26 | */ 27 | noa: import('../index').Engine; 28 | /** Hash containing the component names of built-in components. 29 | * @type {{ [key:string]: string }} 30 | */ 31 | names: { 32 | [key: string]: string; 33 | }; 34 | /** @internal */ 35 | cameraSmoothed: (id: any) => boolean; 36 | /** 37 | * Returns whether the entity has a physics body 38 | * @type {(id:number) => boolean} 39 | */ 40 | hasPhysics: (id: number) => boolean; 41 | /** 42 | * Returns whether the entity has a position 43 | * @type {(id:number) => boolean} 44 | */ 45 | hasPosition: (id: number) => boolean; 46 | /** 47 | * Returns the entity's position component state 48 | * @type {(id:number) => null | import("../components/position").PositionState} 49 | */ 50 | getPositionData: (id: number) => null | import("../components/position").PositionState; 51 | /** 52 | * Returns the entity's position vector. 53 | * @type {(id:number) => number[]} 54 | */ 55 | getPosition: (id: number) => number[]; 56 | /** 57 | * Get the entity's `physics` component state. 58 | * @type {(id:number) => null | import("../components/physics").PhysicsState} 59 | */ 60 | getPhysics: (id: number) => null | import("../components/physics").PhysicsState; 61 | /** 62 | * Returns the entity's physics body 63 | * Note, will throw if the entity doesn't have the position component! 64 | * @type {(id:number) => null | import("voxel-physics-engine").RigidBody} 65 | */ 66 | getPhysicsBody: (id: number) => null | import("voxel-physics-engine").RigidBody; 67 | /** 68 | * Returns whether the entity has a mesh 69 | * @type {(id:number) => boolean} 70 | */ 71 | hasMesh: (id: number) => boolean; 72 | /** 73 | * Returns the entity's `mesh` component state 74 | * @type {(id:number) => {mesh:any, offset:number[]}} 75 | */ 76 | getMeshData: (id: number) => { 77 | mesh: any; 78 | offset: number[]; 79 | }; 80 | /** 81 | * Returns the entity's `movement` component state 82 | * @type {(id:number) => import('../components/movement').MovementState} 83 | */ 84 | getMovement: (id: number) => import('../components/movement').MovementState; 85 | /** 86 | * Returns the entity's `collideTerrain` component state 87 | * @type {(id:number) => {callback: function}} 88 | */ 89 | getCollideTerrain: (id: number) => { 90 | callback: Function; 91 | }; 92 | /** 93 | * Returns the entity's `collideEntities` component state 94 | * @type {(id:number) => { 95 | * cylinder:boolean, collideBits:number, 96 | * collideMask:number, callback: function}} 97 | */ 98 | getCollideEntities: (id: number) => { 99 | cylinder: boolean; 100 | collideBits: number; 101 | collideMask: number; 102 | callback: Function; 103 | }; 104 | /** 105 | * Pairwise collideEntities event - assign your own function to this 106 | * property if you want to handle entity-entity overlap events. 107 | * @type {(id1:number, id2:number) => void} 108 | */ 109 | onPairwiseEntityCollision: (id1: number, id2: number) => void; 110 | /** Set an entity's position, and update all derived state. 111 | * 112 | * In general, always use this to set an entity's position unless 113 | * you're familiar with engine internals. 114 | * 115 | * ```js 116 | * noa.ents.setPosition(playerEntity, [5, 6, 7]) 117 | * noa.ents.setPosition(playerEntity, 5, 6, 7) // also works 118 | * ``` 119 | * 120 | * @param {number} id 121 | */ 122 | setPosition(id: number, pos: any, y?: number, z?: number): void; 123 | /** Set an entity's size 124 | * @param {number} xs 125 | * @param {number} ys 126 | * @param {number} zs 127 | */ 128 | setEntitySize(id: any, xs: number, ys: number, zs: number): void; 129 | /** 130 | * called when engine rebases its local coords 131 | * @internal 132 | */ 133 | _rebaseOrigin(delta: any): void; 134 | /** @internal */ 135 | _localGetPosition(id: any): number[]; 136 | /** @internal */ 137 | _localSetPosition(id: any, pos: any): void; 138 | /** 139 | * helper to update everything derived from `_localPosition` 140 | * @internal 141 | */ 142 | _updateDerivedPositionData(id: any, posDat: any): void; 143 | /** 144 | * Safely add a component - if the entity already had the 145 | * component, this will remove and re-add it. 146 | */ 147 | addComponentAgain(id: any, name: any, state: any): void; 148 | /** 149 | * Checks whether a voxel is obstructed by any entity (with the 150 | * `collidesTerrain` component) 151 | */ 152 | isTerrainBlocked(x: any, y: any, z: any): boolean; 153 | /** 154 | * Gets an array of all entities overlapping the given AABB 155 | */ 156 | getEntitiesInAABB(box: any, withComponent: any): any[]; 157 | /** 158 | * Helper to set up a general entity, and populate with some common components depending on arguments. 159 | */ 160 | add(position?: any, width?: number, height?: number, mesh?: any, meshOffset?: any, doPhysics?: boolean, shadow?: boolean): number; 161 | } 162 | import ECS from 'ent-comp'; 163 | -------------------------------------------------------------------------------- /dist/src/lib/inputs.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * `noa.inputs` - Handles key and mouse input bindings. 3 | * 4 | * This module extends 5 | * [game-inputs](https://github.com/fenomas/game-inputs), 6 | * so turn on "Inherited" to see its APIs here, or view the base module 7 | * for full docs. 8 | * 9 | * This module uses the following default options (from the options 10 | * object passed to the {@link Engine}): 11 | * 12 | * ```js 13 | * defaultBindings: { 14 | * "forward": ["KeyW", "ArrowUp"], 15 | * "backward": ["KeyS", "ArrowDown"], 16 | * "left": ["KeyA", "ArrowLeft"], 17 | * "right": ["KeyD", "ArrowRight"], 18 | * "fire": "Mouse1", 19 | * "mid-fire": ["Mouse2", "KeyQ"], 20 | * "alt-fire": ["Mouse3", "KeyE"], 21 | * "jump": "Space", 22 | * } 23 | * ``` 24 | */ 25 | export class Inputs extends GameInputs { 26 | /** @internal */ 27 | constructor(noa: any, opts: any, element: any); 28 | } 29 | import { GameInputs } from 'game-inputs'; 30 | -------------------------------------------------------------------------------- /dist/src/lib/objectMesher.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @internal 3 | * @param {import('../index').Engine} noa 4 | */ 5 | export function ObjectMesher(noa: import('../index').Engine): void; 6 | export class ObjectMesher { 7 | /** 8 | * @internal 9 | * @param {import('../index').Engine} noa 10 | */ 11 | constructor(noa: import('../index').Engine); 12 | rootNode: TransformNode; 13 | allBaseMeshes: any[]; 14 | initChunk: (chunk: any) => void; 15 | setObjectBlock: (chunk: any, blockID: any, i: any, j: any, k: any) => void; 16 | buildObjectMeshes: () => void; 17 | disposeChunk: (chunk: any) => void; 18 | tick: () => void; 19 | _rebaseOrigin: (delta: any) => void; 20 | } 21 | import { TransformNode } from '@babylonjs/core/Meshes/transformNode'; 22 | -------------------------------------------------------------------------------- /dist/src/lib/physics.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * `noa.physics` - Wrapper module for the physics engine. 3 | * 4 | * This module extends 5 | * [voxel-physics-engine](https://github.com/fenomas/voxel-physics-engine), 6 | * so turn on "Inherited" to see its APIs here, or view the base module 7 | * for full docs. 8 | * 9 | * This module uses the following default options (from the options 10 | * object passed to the {@link Engine}): 11 | * 12 | * ```js 13 | * { 14 | * gravity: [0, -10, 0], 15 | * airDrag: 0.1, 16 | * fluidDrag: 0.4, 17 | * fluidDensity: 2.0, 18 | * minBounceImpulse: .5, // cutoff for a bounce to occur 19 | * } 20 | * ``` 21 | */ 22 | export class Physics extends VoxelPhysics { 23 | /** 24 | * @internal 25 | * @param {import('../index').Engine} noa 26 | */ 27 | constructor(noa: import('../index').Engine, opts: any); 28 | } 29 | import { Physics as VoxelPhysics } from 'voxel-physics-engine'; 30 | -------------------------------------------------------------------------------- /dist/src/lib/registry.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * `noa.registry` - Where you register your voxel types, 3 | * materials, properties, and events. 4 | * 5 | * This module uses the following default options (from the options 6 | * object passed to the {@link Engine}): 7 | * 8 | * ```js 9 | * var defaults = { 10 | * texturePath: '' 11 | * } 12 | * ``` 13 | */ 14 | export class Registry { 15 | /** 16 | * @internal 17 | * @param {import('../index').Engine} noa 18 | */ 19 | constructor(noa: import('../index').Engine, opts: any); 20 | /** @internal */ 21 | noa: import("../index").Engine; 22 | /** @internal */ 23 | _texturePath: any; 24 | /** 25 | * Register (by integer ID) a block type and its parameters. 26 | * `id` param: integer, currently 1..65535. Generally you should 27 | * specify sequential values for blocks, without gaps, but this 28 | * isn't technically necessary. 29 | * 30 | * @param {number} id - sequential integer ID (from 1) 31 | * @param {Partial} [options] 32 | * @returns the `id` value specified 33 | */ 34 | registerBlock: (id?: number, options?: Partial) => number; 35 | /** 36 | * Register (by name) a material and its parameters. 37 | * 38 | * @param {string} name of this material 39 | * @param {Partial} [options] 40 | */ 41 | registerMaterial: (name?: string, options?: Partial) => number; 42 | /** 43 | * block solidity (as in physics) 44 | * @param id 45 | */ 46 | getBlockSolidity: (id: any) => boolean; 47 | /** 48 | * block opacity - whether it obscures the whole voxel (dirt) or 49 | * can be partially seen through (like a fencepost, etc) 50 | * @param id 51 | */ 52 | getBlockOpacity: (id: any) => boolean; 53 | /** 54 | * block is fluid or not 55 | * @param id 56 | */ 57 | getBlockFluidity: (id: any) => boolean; 58 | /** 59 | * Get block property object passed in at registration 60 | * @param id 61 | */ 62 | getBlockProps: (id: any) => any; 63 | getBlockFaceMaterial: (blockId: any, dir: any) => number; 64 | /** 65 | * General lookup for all properties of a block material 66 | * @param {number} matID 67 | * @returns {MatDef} 68 | */ 69 | getMaterialData: (matID: number) => { 70 | color: number[]; 71 | alpha: number; 72 | texture: string; 73 | texHasAlpha: boolean; 74 | atlasIndex: number; 75 | renderMat: any; 76 | }; 77 | /** 78 | * Given a texture URL, does any material using that 79 | * texture need alpha? 80 | * @internal 81 | * @returns {boolean} 82 | */ 83 | _textureNeedsAlpha: (tex?: string) => boolean; 84 | /** @internal */ 85 | _solidityLookup: boolean[]; 86 | /** @internal */ 87 | _opacityLookup: boolean[]; 88 | /** @internal */ 89 | _fluidityLookup: boolean[]; 90 | /** @internal */ 91 | _objectLookup: boolean[]; 92 | /** @internal */ 93 | _blockMeshLookup: any[]; 94 | /** @internal */ 95 | _blockHandlerLookup: any[]; 96 | /** @internal */ 97 | _blockIsPlainLookup: boolean[]; 98 | /** @internal */ 99 | _materialColorLookup: any[]; 100 | /** @internal */ 101 | _matAtlasIndexLookup: number[]; 102 | } 103 | export type TransformNode = import('@babylonjs/core/Meshes').TransformNode; 104 | /** 105 | * Default options when registering a block type 106 | */ 107 | declare function BlockOptions(isFluid?: boolean): void; 108 | declare class BlockOptions { 109 | /** 110 | * Default options when registering a block type 111 | */ 112 | constructor(isFluid?: boolean); 113 | /** Solidity for physics purposes */ 114 | solid: boolean; 115 | /** Whether the block fully obscures neighboring blocks */ 116 | opaque: boolean; 117 | /** whether a nonsolid block is a fluid (buoyant, viscous..) */ 118 | fluid: boolean; 119 | /** The block material(s) for this voxel's faces. May be: 120 | * * one (String) material name 121 | * * array of 2 names: [top/bottom, sides] 122 | * * array of 3 names: [top, bottom, sides] 123 | * * array of 6 names: [-x, +x, -y, +y, -z, +z] 124 | * @type {string|string[]} 125 | */ 126 | material: string | string[]; 127 | /** Specifies a custom mesh for this voxel, instead of terrain */ 128 | blockMesh: any; 129 | /** Fluid parameter for fluid blocks */ 130 | fluidDensity: number; 131 | /** Fluid parameter for fluid blocks */ 132 | viscosity: number; 133 | /** @type {(x:number, y:number, z:number) => void} */ 134 | onLoad: (x: number, y: number, z: number) => void; 135 | /** @type {(x:number, y:number, z:number) => void} */ 136 | onUnload: (x: number, y: number, z: number) => void; 137 | /** @type {(x:number, y:number, z:number) => void} */ 138 | onSet: (x: number, y: number, z: number) => void; 139 | /** @type {(x:number, y:number, z:number) => void} */ 140 | onUnset: (x: number, y: number, z: number) => void; 141 | /** @type {(mesh:TransformNode, x:number, y:number, z:number) => void} */ 142 | onCustomMeshCreate: (mesh: TransformNode, x: number, y: number, z: number) => void; 143 | } 144 | /** @typedef {import('@babylonjs/core/Meshes').TransformNode} TransformNode */ 145 | /** 146 | * Default options when registering a Block Material 147 | */ 148 | declare function MaterialOptions(): void; 149 | declare class MaterialOptions { 150 | /** An array of 0..1 floats, either [R,G,B] or [R,G,B,A] 151 | * @type {number[]} 152 | */ 153 | color: number[]; 154 | /** Filename of texture image, if any 155 | * @type {string} 156 | */ 157 | textureURL: string; 158 | /** Whether the texture image has alpha */ 159 | texHasAlpha: boolean; 160 | /** Index into a (vertical strip) texture atlas, if applicable */ 161 | atlasIndex: number; 162 | /** 163 | * An optional Babylon.js `Material`. If specified, terrain for this voxel 164 | * will be rendered with the supplied material (this can impact performance). 165 | */ 166 | renderMaterial: any; 167 | } 168 | export {}; 169 | -------------------------------------------------------------------------------- /dist/src/lib/rendering.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * `noa.rendering` - 3 | * Manages all rendering, and the BABYLON scene, materials, etc. 4 | * 5 | * This module uses the following default options (from the options 6 | * object passed to the {@link Engine}): 7 | * ```js 8 | * { 9 | * showFPS: false, 10 | * antiAlias: true, 11 | * clearColor: [0.8, 0.9, 1], 12 | * ambientColor: [0.5, 0.5, 0.5], 13 | * lightDiffuse: [1, 1, 1], 14 | * lightSpecular: [1, 1, 1], 15 | * lightVector: [1, -1, 0.5], 16 | * useAO: true, 17 | * AOmultipliers: [0.93, 0.8, 0.5], 18 | * reverseAOmultiplier: 1.0, 19 | * preserveDrawingBuffer: true, 20 | * octreeBlockSize: 2, 21 | * renderOnResize: true, 22 | * } 23 | * ``` 24 | */ 25 | export class Rendering { 26 | /** 27 | * @internal 28 | * @param {import('../index').Engine} noa 29 | */ 30 | constructor(noa: import('../index').Engine, opts: any, canvas: any); 31 | /** @internal */ 32 | noa: import("../index").Engine; 33 | /** Whether to redraw the screen when the game is resized while paused */ 34 | renderOnResize: boolean; 35 | /** @internal */ 36 | useAO: boolean; 37 | /** @internal */ 38 | aoVals: any; 39 | /** @internal */ 40 | revAoVal: any; 41 | /** @internal */ 42 | meshingCutoffTime: number; 43 | /** the Babylon.js Engine object for the scene */ 44 | engine: Engine; 45 | /** the Babylon.js Scene object for the world */ 46 | scene: Scene; 47 | /** a Babylon.js DirectionalLight that is added to the scene */ 48 | light: DirectionalLight; 49 | /** the Babylon.js FreeCamera that renders the scene */ 50 | camera: FreeCamera; 51 | /** 52 | * Constructor helper - set up the Babylon.js scene and basic components 53 | * @internal 54 | */ 55 | _initScene(canvas: any, opts: any): void; 56 | /** @internal */ 57 | _octreeManager: SceneOctreeManager; 58 | /** @internal */ 59 | _cameraHolder: TransformNode; 60 | /** @internal */ 61 | _camScreen: import("@babylonjs/core/Meshes").Mesh; 62 | /** @internal */ 63 | _camScreenMat: StandardMaterial; 64 | /** @internal */ 65 | _camLocBlock: number; 66 | /** The Babylon `scene` object representing the game world. */ 67 | getScene(): Scene; 68 | /** @internal */ 69 | tick(dt: any): void; 70 | /** @internal */ 71 | render(): void; 72 | /** @internal */ 73 | postRender(): void; 74 | /** @internal */ 75 | resize(): void; 76 | /** @internal */ 77 | highlightBlockFace(show: any, posArr: any, normArr: any): void; 78 | /** 79 | * Adds a mesh to the engine's selection/octree logic so that it renders. 80 | * 81 | * @param mesh the mesh to add to the scene 82 | * @param isStatic pass in true if mesh never moves (i.e. never changes chunks) 83 | * @param pos (optional) global position where the mesh should be 84 | * @param containingChunk (optional) chunk to which the mesh is statically bound 85 | */ 86 | addMeshToScene(mesh: any, isStatic?: boolean, pos?: any, containingChunk?: any): void; 87 | /** 88 | * Use this to toggle the visibility of a mesh without disposing it or 89 | * removing it from the scene. 90 | * 91 | * @param {import('@babylonjs/core/Meshes').Mesh} mesh 92 | * @param {boolean} visible 93 | */ 94 | setMeshVisibility(mesh: import("@babylonjs/core/Meshes").Mesh, visible?: boolean): void; 95 | /** 96 | * Create a default standardMaterial: 97 | * flat, nonspecular, fully reflects diffuse and ambient light 98 | * @returns {StandardMaterial} 99 | */ 100 | makeStandardMaterial(name: any): StandardMaterial; 101 | /** @internal */ 102 | prepareChunkForRendering(chunk: any): void; 103 | /** @internal */ 104 | disposeChunkForRendering(chunk: any): void; 105 | /** @internal */ 106 | _rebaseOrigin(delta: any): void; 107 | /** @internal */ 108 | debug_SceneCheck(): string; 109 | /** @internal */ 110 | debug_MeshCount(): void; 111 | } 112 | import { Engine as Engine_1 } from '@babylonjs/core/Engines/engine'; 113 | import { Scene } from '@babylonjs/core/scene'; 114 | import { DirectionalLight } from '@babylonjs/core/Lights/directionalLight'; 115 | import { FreeCamera } from '@babylonjs/core/Cameras/freeCamera'; 116 | import { SceneOctreeManager } from './sceneOctreeManager'; 117 | import { TransformNode } from '@babylonjs/core/Meshes/transformNode'; 118 | import { StandardMaterial } from '@babylonjs/core/Materials/standardMaterial'; 119 | -------------------------------------------------------------------------------- /dist/src/lib/sceneOctreeManager.d.ts: -------------------------------------------------------------------------------- 1 | /** @internal */ 2 | export class SceneOctreeManager { 3 | /** @internal */ 4 | constructor(rendering: any, blockSize: any); 5 | rebase: (offset: any) => void; 6 | addMesh: (mesh: any, isStatic: any, pos: any, chunk: any) => void; 7 | removeMesh: (mesh: any) => void; 8 | setMeshVisibility: (mesh: any, visible?: boolean) => void; 9 | } 10 | -------------------------------------------------------------------------------- /dist/src/lib/shims.d.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenomas/noa/bd74cd8add3abf216b53a995139276af665b1d52/dist/src/lib/shims.d.ts -------------------------------------------------------------------------------- /dist/src/lib/terrainMaterials.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * 4 | * This module creates and manages Materials for terrain meshes. 5 | * It tells the terrain mesher which block face materials can share 6 | * the same material (and should thus be joined into a single mesh), 7 | * and also creates the materials when needed. 8 | * 9 | * @internal 10 | */ 11 | export class TerrainMatManager { 12 | /** @param {import('../index').Engine} noa */ 13 | constructor(noa: import('../index').Engine); 14 | _defaultMat: import("@babylonjs/core/Materials/standardMaterial").StandardMaterial; 15 | allMaterials: import("@babylonjs/core/Materials/standardMaterial").StandardMaterial[]; 16 | noa: import("../index").Engine; 17 | _idCounter: number; 18 | _blockMatIDtoTerrainID: {}; 19 | _terrainIDtoMatObject: {}; 20 | _texURLtoTerrainID: {}; 21 | _renderMatToTerrainID: Map; 22 | /** 23 | * Maps a given `matID` (from noa.registry) to a unique ID of which 24 | * terrain material can be used for that block material. 25 | * This lets the terrain mesher map which blocks can be merged into 26 | * the same meshes. 27 | * Internally, this accessor also creates the material for each 28 | * terrainMatID as they are first encountered. 29 | */ 30 | getTerrainMatId(blockMatID: any): any; 31 | /** 32 | * Get a Babylon Material object, given a terrainMatID (gotten from this module) 33 | */ 34 | getMaterial(terrainMatID?: number): any; 35 | } 36 | -------------------------------------------------------------------------------- /dist/src/lib/terrainMesher.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @internal 3 | * @param {import('../index').Engine} noa 4 | */ 5 | export function TerrainMesher(noa: import('../index').Engine): void; 6 | export class TerrainMesher { 7 | /** 8 | * @internal 9 | * @param {import('../index').Engine} noa 10 | */ 11 | constructor(noa: import('../index').Engine); 12 | allTerrainMaterials: import("@babylonjs/core/Materials/standardMaterial").StandardMaterial[]; 13 | _defaultMaterial: import("@babylonjs/core/Materials/standardMaterial").StandardMaterial; 14 | initChunk: (chunk: any) => void; 15 | disposeChunk: (chunk: any) => void; 16 | /** 17 | * meshing entry point and high-level flow 18 | * @param {import('./chunk').Chunk} chunk 19 | */ 20 | meshChunk: (chunk: import('./chunk').Chunk, ignoreMaterials?: boolean) => void; 21 | } 22 | -------------------------------------------------------------------------------- /dist/src/lib/util.d.ts: -------------------------------------------------------------------------------- 1 | export function removeUnorderedListItem(list: any, item: any): void; 2 | export function numberOfVoxelsInSphere(rad: any): number; 3 | export function copyNdarrayContents(src: any, tgt: any, pos: any, size: any, tgtPos: any): void; 4 | export function iterateOverShellAtDistance(d: any, xmax: any, ymax: any, cb: any): any; 5 | export function locationHasher(i: any, j: any, k: any): number; 6 | export function makeProfileHook(every: any, title: string, filter: any): (state: any) => void; 7 | export function makeThroughputHook(_every: any, _title: any, filter: any): (state: any) => void; 8 | /** @internal */ 9 | export class ChunkStorage { 10 | hash: {}; 11 | /** @returns {import('./chunk').Chunk} */ 12 | getChunkByIndexes(i?: number, j?: number, k?: number): import('./chunk').Chunk; 13 | /** @param {import('./chunk').Chunk} chunk */ 14 | storeChunkByIndexes(i: number, j: number, k: number, chunk: import('./chunk').Chunk): void; 15 | removeChunkByIndexes(i?: number, j?: number, k?: number): void; 16 | } 17 | /** @internal */ 18 | export class LocationQueue { 19 | arr: any[]; 20 | hash: {}; 21 | forEach(cb: any, thisArg: any): void; 22 | includes(i: any, j: any, k: any): boolean; 23 | add(i: any, j: any, k: any, toFront?: boolean): void; 24 | removeByIndex(ix: any): void; 25 | remove(i: any, j: any, k: any): void; 26 | count(): number; 27 | isEmpty(): boolean; 28 | empty(): void; 29 | pop(): any; 30 | copyFrom(queue: any): void; 31 | sortByDistance(locToDist: any, reverse?: boolean): void; 32 | } 33 | -------------------------------------------------------------------------------- /dist/src/lib/world.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * `noa.world` - manages world data, chunks, voxels. 3 | * 4 | * This module uses the following default options (from the options 5 | * object passed to the {@link Engine}): 6 | * ```js 7 | * var defaultOptions = { 8 | * chunkSize: 24, 9 | * chunkAddDistance: [2, 2], // [horizontal, vertical] 10 | * chunkRemoveDistance: [3, 3], // [horizontal, vertical] 11 | * worldGenWhilePaused: false, 12 | * manuallyControlChunkLoading: false, 13 | * } 14 | * ``` 15 | * 16 | * **Events:** 17 | * + `worldDataNeeded = (requestID, dataArr, x, y, z, worldName)` 18 | * Alerts client that a new chunk of world data is needed. 19 | * + `playerEnteredChunk => (i, j, k)` 20 | * Fires when player enters a new chunk 21 | * + `chunkAdded => (chunk)` 22 | * Fires after a new chunk object is added to the world 23 | * + `chunkBeingRemoved = (requestID, dataArr, userData)` 24 | * Fires before a chunk is removed from world 25 | */ 26 | export class World extends EventEmitter { 27 | /** @internal */ 28 | constructor(noa: any, opts: any); 29 | /** @internal */ 30 | noa: any; 31 | /** @internal */ 32 | playerChunkLoaded: boolean; 33 | /** @internal */ 34 | Chunk: typeof Chunk; 35 | /** 36 | * Game clients should set this if they need to manually control 37 | * which chunks to load and unload. When set, client should call 38 | * `noa.world.manuallyLoadChunk` / `manuallyUnloadChunk` as needed. 39 | */ 40 | manuallyControlChunkLoading: boolean; 41 | /** 42 | * Defining this function sets a custom order in which to create chunks. 43 | * The function should look like: 44 | * ```js 45 | * (i, j, k) => 1 // return a smaller number for chunks to process first 46 | * ``` 47 | */ 48 | chunkSortingDistFn: (i: any, j: any, k: any) => number; 49 | /** 50 | * Set this higher to cause chunks not to mesh until they have some neighbors. 51 | * Max legal value is 26 (each chunk will mesh only when all neighbors are present) 52 | */ 53 | minNeighborsToMesh: number; 54 | /** When true, worldgen queues will keep running if engine is paused. */ 55 | worldGenWhilePaused: boolean; 56 | /** Limit the size of internal chunk processing queues 57 | * @type {number} 58 | */ 59 | maxChunksPendingCreation: number; 60 | /** Limit the size of internal chunk processing queues 61 | * @type {number} 62 | */ 63 | maxChunksPendingMeshing: number; 64 | /** Cutoff (in ms) of time spent each **tick** 65 | * @type {number} 66 | */ 67 | maxProcessingPerTick: number; 68 | /** Cutoff (in ms) of time spent each **render** 69 | * @type {number} 70 | */ 71 | maxProcessingPerRender: number; 72 | /** @internal */ 73 | _chunkSize: any; 74 | /** @internal */ 75 | _chunkAddDistance: number[]; 76 | /** @internal */ 77 | _chunkRemoveDistance: number[]; 78 | /** @internal */ 79 | _addDistanceFn: (i: any, j: any, k: any) => boolean; 80 | /** @internal */ 81 | _remDistanceFn: (i: any, j: any, k: any) => boolean; 82 | /** @internal */ 83 | _prevWorldName: string; 84 | /** @internal */ 85 | _prevPlayerChunkHash: number; 86 | /** @internal */ 87 | _chunkAddSearchFrom: number; 88 | /** @internal */ 89 | _prevSortingFn: any; 90 | /** @internal */ 91 | _sortMeshQueueEvery: number; 92 | /** @internal All chunks existing in any queue */ 93 | _chunksKnown: LocationQueue; 94 | /** @internal in range but not yet requested from client */ 95 | _chunksToRequest: LocationQueue; 96 | /** @internal known to have invalid data (wrong world, eg) */ 97 | _chunksInvalidated: LocationQueue; 98 | /** @internal out of range, and waiting to be removed */ 99 | _chunksToRemove: LocationQueue; 100 | /** @internal requested, awaiting data event from client */ 101 | _chunksPending: LocationQueue; 102 | /** @internal has data, waiting to be (re-)meshed */ 103 | _chunksToMesh: LocationQueue; 104 | /** @internal priority queue for chunks to re-mesh */ 105 | _chunksToMeshFirst: LocationQueue; 106 | /** 107 | * @internal A queue of chunk locations, rather than chunk references. 108 | * Has only the positive 1/16 quadrant, sorted (reverse order!) */ 109 | _chunksSortedLocs: LocationQueue; 110 | /** @internal */ 111 | _storage: ChunkStorage; 112 | /** @internal */ 113 | _coordsToChunkIndexes: typeof chunkCoordsToIndexesGeneral; 114 | /** @internal */ 115 | _coordsToChunkLocals: typeof chunkCoordsToLocalsPowerOfTwo; 116 | /** @internal */ 117 | _coordShiftBits: number; 118 | /** @internal */ 119 | _coordMask: number; 120 | getBlockID(x?: number, y?: number, z?: number): any; 121 | getBlockSolidity(x?: number, y?: number, z?: number): boolean; 122 | getBlockOpacity(x?: number, y?: number, z?: number): any; 123 | getBlockFluidity(x?: number, y?: number, z?: number): any; 124 | getBlockProperties(x?: number, y?: number, z?: number): any; 125 | setBlockID(id?: number, x?: number, y?: number, z?: number): void; 126 | /** @param box */ 127 | isBoxUnobstructed(box: any): boolean; 128 | /** 129 | * Clients should call this after creating a chunk's worth of data (as an ndarray) 130 | * If userData is passed in it will be attached to the chunk 131 | * @param {string} id - the string specified when the chunk was requested 132 | * @param {*} array - an ndarray of voxel data 133 | * @param {*} userData - an arbitrary value for game client use 134 | * @param {number} fillVoxelID - specify a voxel ID here if you want to signify that 135 | * the entire chunk should be solidly filled with that voxel (e.g. `0` for air). 136 | * If you do this, the voxel array data will be overwritten and the engine will 137 | * take a fast path through some initialization steps. 138 | */ 139 | setChunkData(id: string, array: any, userData?: any, fillVoxelID?: number): void; 140 | /** 141 | * Sets the distances within which to load new chunks, and beyond which 142 | * to unload them. Generally you want the remove distance to be somewhat 143 | * farther, so that moving back and forth across the same chunk border doesn't 144 | * keep loading/unloading the same distant chunks. 145 | * 146 | * Both arguments can be numbers (number of voxels), or arrays like: 147 | * `[horiz, vert]` specifying different horizontal and vertical distances. 148 | * @param {number | number[]} addDist 149 | * @param {number | number[]} remDist 150 | */ 151 | setAddRemoveDistance(addDist?: number | number[], remDist?: number | number[]): void; 152 | /** 153 | * Tells noa to discard voxel data within a given `AABB` (e.g. because 154 | * the game client received updated data from a server). 155 | * The engine will mark all affected chunks for removal, and will later emit 156 | * new `worldDataNeeded` events (if the chunk is still in draw range). 157 | */ 158 | invalidateVoxelsInAABB(box: any): void; 159 | /** When manually controlling chunk loading, tells the engine that the 160 | * chunk containing the specified (x,y,z) needs to be created and loaded. 161 | * > Note: throws unless `noa.world.manuallyControlChunkLoading` is set. 162 | * @param x, y, z 163 | */ 164 | manuallyLoadChunk(x?: number, y?: number, z?: number): void; 165 | /** When manually controlling chunk loading, tells the engine that the 166 | * chunk containing the specified (x,y,z) needs to be unloaded and disposed. 167 | * > Note: throws unless `noa.world.manuallyControlChunkLoading` is set. 168 | * @param x, y, z 169 | */ 170 | manuallyUnloadChunk(x?: number, y?: number, z?: number): void; 171 | /** @internal */ 172 | tick(): void; 173 | /** @internal */ 174 | render(): void; 175 | /** @internal */ 176 | _getChunkByCoords(x?: number, y?: number, z?: number): Chunk; 177 | _queueChunkForRemesh(chunk: any): void; 178 | /** @internal */ 179 | report(): void; 180 | } 181 | import EventEmitter from 'events'; 182 | import { Chunk } from './chunk'; 183 | import { LocationQueue } from './util'; 184 | import { ChunkStorage } from './util'; 185 | declare function chunkCoordsToIndexesGeneral(x: any, y: any, z: any): number[]; 186 | declare function chunkCoordsToLocalsPowerOfTwo(x: any, y: any, z: any): number[]; 187 | export {}; 188 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. -------------------------------------------------------------------------------- /docs/API/.nojekyll: -------------------------------------------------------------------------------- 1 | TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. -------------------------------------------------------------------------------- /docs/API/assets/highlight.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --light-hl-0: #0000FF; 3 | --dark-hl-0: #569CD6; 4 | --light-hl-1: #000000; 5 | --dark-hl-1: #D4D4D4; 6 | --light-hl-2: #001080; 7 | --dark-hl-2: #9CDCFE; 8 | --light-hl-3: #098658; 9 | --dark-hl-3: #B5CEA8; 10 | --light-hl-4: #795E26; 11 | --dark-hl-4: #DCDCAA; 12 | --light-hl-5: #A31515; 13 | --dark-hl-5: #CE9178; 14 | --light-hl-6: #008000; 15 | --dark-hl-6: #6A9955; 16 | --light-hl-7: #000000; 17 | --dark-hl-7: #C8C8C8; 18 | --light-hl-8: #AF00DB; 19 | --dark-hl-8: #C586C0; 20 | --light-code-background: #FFFFFF; 21 | --dark-code-background: #1E1E1E; 22 | } 23 | 24 | @media (prefers-color-scheme: light) { :root { 25 | --hl-0: var(--light-hl-0); 26 | --hl-1: var(--light-hl-1); 27 | --hl-2: var(--light-hl-2); 28 | --hl-3: var(--light-hl-3); 29 | --hl-4: var(--light-hl-4); 30 | --hl-5: var(--light-hl-5); 31 | --hl-6: var(--light-hl-6); 32 | --hl-7: var(--light-hl-7); 33 | --hl-8: var(--light-hl-8); 34 | --code-background: var(--light-code-background); 35 | } } 36 | 37 | @media (prefers-color-scheme: dark) { :root { 38 | --hl-0: var(--dark-hl-0); 39 | --hl-1: var(--dark-hl-1); 40 | --hl-2: var(--dark-hl-2); 41 | --hl-3: var(--dark-hl-3); 42 | --hl-4: var(--dark-hl-4); 43 | --hl-5: var(--dark-hl-5); 44 | --hl-6: var(--dark-hl-6); 45 | --hl-7: var(--dark-hl-7); 46 | --hl-8: var(--dark-hl-8); 47 | --code-background: var(--dark-code-background); 48 | } } 49 | 50 | :root[data-theme='light'] { 51 | --hl-0: var(--light-hl-0); 52 | --hl-1: var(--light-hl-1); 53 | --hl-2: var(--light-hl-2); 54 | --hl-3: var(--light-hl-3); 55 | --hl-4: var(--light-hl-4); 56 | --hl-5: var(--light-hl-5); 57 | --hl-6: var(--light-hl-6); 58 | --hl-7: var(--light-hl-7); 59 | --hl-8: var(--light-hl-8); 60 | --code-background: var(--light-code-background); 61 | } 62 | 63 | :root[data-theme='dark'] { 64 | --hl-0: var(--dark-hl-0); 65 | --hl-1: var(--dark-hl-1); 66 | --hl-2: var(--dark-hl-2); 67 | --hl-3: var(--dark-hl-3); 68 | --hl-4: var(--dark-hl-4); 69 | --hl-5: var(--dark-hl-5); 70 | --hl-6: var(--dark-hl-6); 71 | --hl-7: var(--dark-hl-7); 72 | --hl-8: var(--dark-hl-8); 73 | --code-background: var(--dark-code-background); 74 | } 75 | 76 | .hl-0 { color: var(--hl-0); } 77 | .hl-1 { color: var(--hl-1); } 78 | .hl-2 { color: var(--hl-2); } 79 | .hl-3 { color: var(--hl-3); } 80 | .hl-4 { color: var(--hl-4); } 81 | .hl-5 { color: var(--hl-5); } 82 | .hl-6 { color: var(--hl-6); } 83 | .hl-7 { color: var(--hl-7); } 84 | .hl-8 { color: var(--hl-8); } 85 | pre, code { background: var(--code-background); } 86 | -------------------------------------------------------------------------------- /docs/api-header.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | This is the API documentation for noa, a voxel game engine. 4 | 5 | You probably want to start with the core engine class: 6 | 7 | > {@link Engine} 8 | 9 | Or return to the source repo: 10 | 11 | > [github.com/fenomas/noa](https://github.com/fenomas/noa) 12 | -------------------------------------------------------------------------------- /docs/components.md: -------------------------------------------------------------------------------- 1 | 2 | # Component notes 3 | 4 | Reference list of components, what they do, and order in which they're called. 5 | 6 | Order for systems on custom components can be specified with an `order` property 7 | in the component definition. For example, if you define a component with a 8 | `renderSystem` that depends on the current camera target position, it will want an 9 | `order` greater than `50` (when `followsEntity` moves the camera target before each render). 10 | 11 | ---- 12 | 13 | ## All components 14 | 15 | | name | order | desc 16 | | ---- | ----- | ---- 17 | | `collideEntities` | `70` | marks entities that collide with other entities 18 | | `collideTerrain` | | marks entities that collide with terrain 19 | | `fadeOnZoom` | `99` | marks an entity to hide when camera is zoomed in 20 | | `followsEntity` | `50` | locks location of an entity to another one 21 | | `mesh` | `100`| stores a mesh associated with an entity 22 | | `movement` | `30` | stores movement state (move direction, jumping, etc.) 23 | | `physics` | `40` | stores an entity's physics body 24 | | `position` | | stores entity's position, height/weight, and related data 25 | | `receivesInputs` | `20` | marks an entity as being controlled by keyboard/mouse 26 | | `shadow` | `80` | stores and manages a shadow mesh for an entity 27 | | `smoothCamera` | `99` | marks a mesh to move smoothly to its physics position, rather than abruptly 28 | 29 | ---- 30 | 31 | ## Component `system` handlers and order 32 | 33 | | name | order | system 34 | | ---- | ----- | ------ 35 | | `receivesInputs` | `20` | update `movement` state based on key/mouse input 36 | | `movement` | `30` | applies physics forces based on `movement` state 37 | | `physics` | `40` | update entity `_localPosition` from physics body 38 | | `followsEntity` | `50` | move own `_localPosition` to match target 39 | | `position` | `60` | update `position` and `extents` properties 40 | | `collideEntities` | `70` | runs collision test, fires onCollide events 41 | | `shadow` | `80` | update shadow's `y` position 42 | | `fadeOnZoom` | `99` | checks camera zoom, hides or reveals entity 43 | | `smoothCamera` | `99` | removes itself after time limit 44 | 45 | ---- 46 | 47 | ## Component `renderSystem` handlers and order 48 | 49 | | name | order | render system 50 | | ---- | ----- | ------ 51 | | `physics` | `40` | backtrack entity `renderPosition` towards physics position 52 | | `followsEntity` | `50` | moves entity's `renderPosition` to match its follow target 53 | | `shadow` | `80` | update shadow's `x/z` position 54 | | `mesh` | `100`| moves rendering mesh to entity `renderPosition` 55 | 56 | 57 | -------------------------------------------------------------------------------- /docs/history.md: -------------------------------------------------------------------------------- 1 | 2 | ## Version history 3 | 4 | This is a summary of new features and breaking changes in recent `noa` versions. 5 | 6 | * [0.33.0](#0330) 7 | * [0.32.0](#0320) 8 | * [0.31.0](#0310) 9 | * [0.30.0](#0300) 10 | * [0.29.0](#0290) 11 | * [0.28.0](#0280) 12 | * [0.27.0](#0270) 13 | * [0.26.0](#0260) 14 | * [0.25.0](#0250) 15 | * [0.24.0](#0240) 16 | * [0.23.0](#0230) 17 | * [0.22.0](#0220) 18 | * [0.21.0](#0210) 19 | * [0.20.0](#0200) 20 | * [0.16.0](#0160) 21 | 22 | 23 | ---- 24 | 25 | ### 0.33.0 26 | 27 | * Signature of `noa.registry.registerMaterial` changed to take an options object 28 | * Terrain now supports texture atlases! Merge your textures into a vertical strip atlas, then call `noa.registry.registerMaterial` with that texture and specify an `atlasIndex` options property. 29 | * When passing world data to `setChunkData`, client may now pass in a `fillVoxelID` to signify that entire chunk should be filled with that voxel (e.g. `0` for air) 30 | * Babylon version updated 31 | * Added options/properties to `noa.camera` for temprary changes to camera control sensitivity. 32 | * `sensitivityMult` (default `1`) 33 | * `sensitivityMultOutsidePointerlock` (default `0`) 34 | * Added `noa.camera.inputsDisabled` for temporarily disabling camera controls 35 | * Modernization updates to `noa.inputs`. Breaking changes: 36 | * Key bindings should now use [KeyboardEvent.code](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code) strings, like `KeyA`, `Shift`, etc. 37 | * Mouse button bindings should use `Mouse1`, `Mouse2`.. 38 | * Mouse move/scroll values (`dx,dy,scrollx,scrolly`) are moved from 39 | `noa.inputs.state` to `noa.inputs.pointerState` 40 | * Changes default light to Directional, and updates related engine options: 41 | * removes option `groundLightColor`, adds `lightVector`, and changes`ambientColor` 42 | * Removes `noa.rendering.postMaterialCreationHook` - use mesh hooks instead 43 | * Adds `rendering.setMeshVisibility` for toggling the display of meshes that are added to the scene with `addMeshToScene` 44 | * Engine now emits events when adding/removing terrain meshes that it magnages (static chunk terrain, or custom block meshes). Clients can listen to these to implement shadows. 45 | * `noa#addingTerrainMesh` 46 | * `noa#removingTerrainMesh` 47 | * Adds `playerShadowComponent` option, defaulting to `true` 48 | * Renames some internals to be public - e.g. `rendering._scene` to `scene` 49 | 50 | ### 0.32.0 51 | * Fixes npm versioning issue - no code changes. 52 | 53 | ### 0.31.0 54 | 55 | * Change the speed of the world with `noa.timeScale` 56 | * Now possible to control chunk processing order: `noa.world.chunkSortingDistFn` 57 | * Chunk processing will happen more reliably, particularly after switching worlds. 58 | * Changed how the docs work, and how code comments are arranged for this purpose. See [API docs](https://fenomas.github.io/noa/API/). 59 | * Moves to Babylonjs version 5 (alpha). 60 | * Adds more exported types and code hints. 61 | 62 | 63 | ### 0.30.0 64 | 65 | * Engine now a named export, use `import {Engine} from 'noa-engine'` 66 | * many performance and size optimizations 67 | * now generates proper type declarations and API references! 68 | * Adds separate horizontal/vertical add/remove chunk distances in `world` 69 | * Scene octree now can put multiple chunks in each octree block 70 | * Adds option `noa.rendering.renderOnResize` 71 | * Changed game-shell dependency, which affects several properties: 72 | * init option `tickRate` is now in **ticks per second**, not ms per tick 73 | * init option `maxRenderRate` added (leave at `0` for no cap) 74 | * init option `stickyFullscreen` added 75 | * adds `noa.tickRate`. Read only; if you really need to change it use `noa.container._shell.tickRate`. 76 | * adds `noa.maxRenderRate` - this is safe to change dynamically. Set to `0` for no limit. 77 | * Removed the `id` property on `Chunk` objects. Shouldn't realistically affect any game clients, but if you were using it for some reason, use `chunk.requestID` instead. 78 | * Made several of the core `babylon` imports more specific, which could cause errors if your client code is using Babylon modules without importing them. If you're using any mesh builders (e.g. `Mesh.CreateBox()`), make sure to import the necessary module (`import '@babylonjs/core/Meshes/Builders/boxBuilder'`). 79 | 80 | ### 0.29.0 81 | 82 | * Maximum allowed voxel ID is now `65535` 83 | * New option `worldGenWhilePaused` added to `noa.world`. When true, the engine will keep doing world generation (requesting new chunks, disposing old ones, meshing, etc) even while paused. 84 | * New option `manuallyControlChunkLoading` added to `noa.world`. When set, the engine will not automatically add or remove chunks near the player. Instead, call `noa.world.manuallyLoadChunk` and `manuallyUnloadChunk` on the coordinates you need. 85 | * Voxel IDs are now stored internally as plain `Uint16Array` elements, rather packing IDs and bit flags together. Any clients that were accessing internal data arrays will probably need to be updated. 86 | * Fixed the `dt` parameter to `noa#render(dt)` events. Previously it could occasionally be wrong in such a way as to cause temporal aliasing when used for animations. 87 | 88 | 89 | ### 0.28.0 90 | 91 | * Voxel data is no longer internally duplicated at chunk borders. This means: 92 | * World generation no longer necessarily needs to be deterministic. Previously, voxels on chunk borders would get tracked in several chunks, and if the data didn't match up it would cause rendering artifacts. This is no longer the case, each voxel is stored in only one place. 93 | * When each chunk gets meshed for the first time, it will have have some artifacts at edges where the neighboring chunk doesn't exist yet. Such chunks will later get re-meshed once all their neighbor chunks exist. 94 | * Set `noa.world.minNeighborsToMesh` (default `6`) to control how aggressively chunks first get meshed. 95 | * Can now swap between world data sets 96 | * Set `noa.worldName` to manage 97 | * Current worldName is now sent with `worldDataNeeded` events, so that 98 | game client knows which worldgen data to provide 99 | * Removed leading `_` from several property names, since they're meant to be set by the client: 100 | * `noa.world.maxChunksPendingCreation` (max # of chunks to queue) 101 | * `noa.world.maxChunksPendingMeshing` (max # of chunks to queue) 102 | * `noa.world.maxProcessingPerTick` (time in ms) 103 | * `noa.world.maxProcessingPerRender` (time in ms) 104 | * Mostly rewrites `noa.world` internals (chunk create/update/dispose flow) 105 | 106 | 107 | ### 0.27.0 108 | 109 | * Engine now does *world origin rebasing*, to avoid precision bugs in large worlds 110 | * Entity positions are now handled internally relative to a local coordinate system, which is periodically rebased around the player entity 111 | * The following systems now internally use local coordinates: 112 | * rendering 113 | * physics 114 | * entity/entity collisions 115 | * raycasting 116 | * Pre-existing position properties and related APIs still work, but may be imprecise. Each such API now has a `_local` alternate 117 | * Engine option `originRebaseDistance` controls how often rebasing occurs 118 | * See [/doc/positions.md](positions.md) for more details 119 | 120 | 121 | ### 0.26.0 122 | 123 | * Engine now imports Babylon as a **peer dependency** 124 | * Noa games must now declare their own dependency on `@babylon/core` 125 | * See [examples](https://github.com/fenomas/noa-examples) for sample code, weback config, etc. 126 | * Noa now exports Engine as an ES6 module. 127 | * Clients using `require` will need to do `require('noa-engine').default` 128 | * Example worlds (`test` and `hello-world`) moved to a [separate repo](https://github.com/fenomas/noa-examples) 129 | * Internal modules all migrated to es6 import/export syntax 130 | * Moves several camera-related APIs from rendering to `noa.camera` 131 | * Removes several redundant properties/APIs (they throw depreceation messages when accessed) 132 | * Component systems now fire in a fixed order, see [components.md](components.md) 133 | * Changes order of various render logic - fixes temporal aliasing bugs 134 | * `noa#render` events now pass correct `dt` argument - see issue #53 135 | 136 | ### 0.25.0 137 | 138 | * Adds `debug` option: populates `window` with useful references, binds `Z` to BJS inspector 139 | * Now current with Babylon.js 4.0 140 | * Updates many dependencies, many small bug fixes. 141 | 142 | ### 0.24.0 143 | 144 | * Terrain materials can specify a renderMaterial (see `registry.registerMaterial()`) 145 | * Targeting and `noa.pick` can take a function for which block IDs to target - #36 146 | * `every` component is removed (client apps using this, please define it separately) 147 | 148 | ### 0.23.0 149 | 150 | * Now uses octrees for scene selection for all meshes, even moving ones 151 | * Option `useOctreesForDynamicMeshes` (default `true`) to disable previous 152 | * `noa.rendering.addDynamicMesh` changed to `addMeshToScene(mesh, isStatic)` 153 | * Entities can now be cylindrical w.r.t. `collideEntities` component 154 | * Adds pairwise entity collision handler `noa.entities.onPairwiseEntityCollision` 155 | 156 | ### 0.22.0 157 | 158 | * Large/complicated scenes should mesh and render much faster 159 | * Chunk terrain/object meshing now merges results. Block object meshes must be static! 160 | * Removed redundant `player` component - use `noa.playerEntity` property 161 | * Added `showFPS` option 162 | * Many internal changes that hopefully don't break compatibility 163 | 164 | ### 0.21.0 165 | 166 | * Support unloading/reloading new world data. 167 | Sample implementation in the `docs/test` app (hit "O" to swap world data) 168 | * changes `noa.world#setChunkData` params: `id, array, userData` 169 | * changes `noa.world#chunkBeingRemoved` event params: `id, array, userData` 170 | 171 | ### 0.20.0 172 | 173 | * Near chunks get loaded and distant ones get unloaded faster and more sensibly 174 | * Greatly speeds up chunk init, meshing, and disposal (and fixes some new Chrome deopts) 175 | 176 | ### 0.19.0 177 | 178 | * Revise per-block callbacks: 179 | * `onLoad` when a block is created as part of a newly-loaded chunk 180 | * `onUnload` - when the block goes away because its chunk was unloaded 181 | * `onSet` - when a block gets set to that particular id 182 | * `onUnset` - when a block that had that id gets set to something else 183 | * `onCustomMeshCreate` - when that block's custom mesh is instantiated (either due to load or set) 184 | 185 | ### 0.18.0 186 | 187 | * Simplifies block targeting. Instead of several accessor methods, now there's a persistent `noa.targetedBlock` with details on whatever block is currently targeted. 188 | * `noa` now emits `targetBlockChanged` 189 | * Built-in block highlighting can now be overridden or turned off with option `skipDefaultHighlighting` 190 | 191 | ### 0.17.0 192 | 193 | * Adds per-block callbacks: `onCreate`, `onDestroy`, `onCustomMeshCreate` 194 | 195 | ### 0.16.0 196 | 197 | * Simplifies block registration - now takes an options argument, and the same API is used for custom mesh blocks 198 | * Removes the idea of registration for meshes 199 | -------------------------------------------------------------------------------- /docs/positions.md: -------------------------------------------------------------------------------- 1 | 2 | # Notes on positions 3 | 4 | 5 | Like all entity data, the position of an entity in `noa` lives in a 6 | component state object - specifically, the state of the 7 | [position](../src/components/position.js) component. 8 | There are several accessors on `noa.entities` for working with positions: 9 | 10 | ```js 11 | var id = noa.entities.createEntity() 12 | noa.ents.addComponent(id, noa.ents.names.position, { 13 | position: [1, 2, 3], 14 | width: 1, 15 | height: 2, 16 | }) 17 | 18 | noa.ents.hasPosition(id) // true 19 | noa.ents.getPosition(id) // [1, 2, 3] 20 | noa.ents.setPosition(id, [4.5, 5, 6]) 21 | ``` 22 | 23 | And such. There is also a state accessor: 24 | 25 | ```js 26 | var posState = noa.ents.getPositionData(id) 27 | posState.position // [4.5, 5, 6] 28 | posState.width // 1 29 | posState.height // 2 30 | ``` 31 | 32 | If you then add the `physics` and `mesh` components, the related positions 33 | will get managed automatically. That is, the physics engine will manage 34 | the entity's position, which will in turn manage the attached mesh's position 35 | in the 3D scene. 36 | 37 | ---- 38 | 39 | ## Advanced 40 | 41 | Behind the scenes things are a little more complicated. 42 | Game worlds tend to be large, so to avoid precision bugs `noa` 43 | runs all its logic in a *local frame of reference*, offset from global coordinates. 44 | This affects physics, rendering, raycasts, and entity/entity collision tests. 45 | 46 | This affects you the game programmer in two ways: 47 | 48 | * If you directly manipulate mesh or physics body positions, 49 | you'll need to do so in local coords 50 | * When entities are very far away from the world origin, 51 | regular position APIs may be imprecise 52 | 53 | If you run afoul of such issues, here's all you need to know: 54 | 55 | ## Converting between local and global coords 56 | 57 | ```js 58 | // noa has two functions to precisely convert between global and local coords. 59 | // In both cases the "global" argument is treated as integer values, 60 | // and the "precisePos" is fractional offsets to the global position 61 | noa.localToGlobal(local, global, precisePos) 62 | noa.globalToLocal(global, precisePos, local) 63 | 64 | // e.g. 65 | var local = [] 66 | noa.globalToLocal([1, 2, 3], [0.1, 0.1, 0.1], local) 67 | console.log(local) // [1.1, 2.1, 3.1], converted to local coords 68 | 69 | var pos = [] 70 | var frac = [] 71 | noa.localToGlobal(local, pos, frac) 72 | console.log(pos) // [1, 2, 3] 73 | console.log(frac) // [0.1, 0.1, 0.1] 74 | 75 | // In both cases the "precise" argument can be omitted, 76 | // so the "global" array will be treated as full (int+fraction) values. 77 | // This may cause precision issues in very large game worlds. 78 | var pos = [1.1, 2.1, 3.1] 79 | noa.globalToLocal(pos, null, local) 80 | noa.localToGlobal(local, pos) 81 | console.log(pos) // approx. [1.1, 2.1, 3.1] 82 | ``` 83 | 84 | ## Using local coord APIs 85 | 86 | In general, all position related APIs in `noa` have a counterpart that 87 | works in local coordinates, prefixed with `_local`. 88 | These are generally for internal use, but it's safe to use them 89 | whenever you need high precision, or you need to 90 | manually mess with positions in the rendering or physics engine. 91 | 92 | ```js 93 | // do a raycast from an entity's position, the normal way 94 | var dir = noa.camera.getDirection() 95 | var pos = noa.ents.getPosition(id) 96 | var res = noa.pick(pos, dir) 97 | 98 | // same thing but higher precision 99 | var pos = noa.ents._localGetPosition(id) 100 | var res = noa._localPick(pos, dir) 101 | 102 | // full list of _local APIs 103 | noa._localPick 104 | noa.ents._localGetPosition 105 | noa.ents._localSetPosition 106 | noa.camera._localGetPosition 107 | noa.camera._localGetTargetPosition 108 | ``` 109 | 110 | ---- 111 | 112 | ## Extra gory details 113 | 114 | Hopefully you won't need to know anything below here, 115 | but here are internal details for anyone hacking on the engine: 116 | 117 | * `noa.worldOriginOffset` is the offset between local and global coords 118 | 119 | The position component keeps the following internal properties (all in *local coordinates*). 120 | 121 | * `_localPosition` - the "game logic" position 122 | * `_renderPosition` - the "render" position in the 3D scene 123 | * `_extents` - an array like: `[lox, loy, loz, hix, hiy, hiz]` 124 | 125 | `_localPosition` is the entity's "real" single-source-of-truth position. 126 | All other properties are derived from it. The reason for `_renderPosition` 127 | being a separate value is that it can change every render, 128 | while `_localPosition` only changes once per tick. 129 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "noa-engine", 3 | "version": "0.33.0", 4 | "description": "Experimental voxel game engine", 5 | "main": "src/index.js", 6 | "typings": "dist/src/index.d.ts", 7 | "files": [ 8 | "/src", 9 | "/dist" 10 | ], 11 | "scripts": { 12 | "build": "npm run types; npm run docs", 13 | "types": "tsc", 14 | "docs": "typedoc" 15 | }, 16 | "author": "Andy Hall (https://fenomas.com)", 17 | "license": "MIT", 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/fenomas/noa.git" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/fenomas/noa/issues" 24 | }, 25 | "dependencies": { 26 | "aabb-3d": "fenomas/aabb-3d", 27 | "box-intersect": "fenomas/box-intersect", 28 | "ent-comp": "^0.11.0", 29 | "events": "^3.3.0", 30 | "fast-voxel-raycast": "^0.1.1", 31 | "game-inputs": "^0.8.0", 32 | "gl-vec3": "^1.1.3", 33 | "micro-game-shell": "^0.9.0", 34 | "ndarray": "^1.0.19", 35 | "voxel-aabb-sweep": "^0.5.0", 36 | "voxel-physics-engine": "^0.13.0" 37 | }, 38 | "peerDependencies": { 39 | "@babylonjs/core": "^6.1.0" 40 | }, 41 | "devDependencies": { 42 | "eslint": "^8.3.0", 43 | "js-beautify": "^1.14.0", 44 | "typedoc": "^0.24.6", 45 | "typedoc-plugin-missing-exports": "^2.0.0", 46 | "typescript": "^5.0.4" 47 | }, 48 | "keywords": [ 49 | "voxel", 50 | "voxels", 51 | "game", 52 | "engine", 53 | "game-engine" 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /src/components/collideEntities.js: -------------------------------------------------------------------------------- 1 | 2 | import boxIntersect from 'box-intersect' 3 | 4 | 5 | 6 | /* 7 | * Every frame, entities with this component will get mutually checked for colliions 8 | * 9 | * * cylinder: flag for checking collisions as a vertical cylindar (rather than AABB) 10 | * * collideBits: category for this entity 11 | * * collideMask: categories this entity collides with 12 | * * callback: function(other_id) - called when `own.collideBits & other.collideMask` is true 13 | * 14 | * 15 | * Notes: 16 | * Set collideBits=0 for entities like bullets, which can collide with things 17 | * but are never the target of a collision. 18 | * Set collideMask=0 for things with no callback - things that get collided with, 19 | * but don't themselves instigate collisions. 20 | * 21 | */ 22 | 23 | 24 | 25 | export default function (noa) { 26 | 27 | var intervals = [] 28 | 29 | return { 30 | 31 | name: 'collideEntities', 32 | 33 | order: 70, 34 | 35 | state: { 36 | cylinder: false, 37 | collideBits: 1 | 0, 38 | collideMask: 1 | 0, 39 | callback: null, 40 | }, 41 | 42 | onAdd: null, 43 | 44 | onRemove: null, 45 | 46 | 47 | system: function entityCollider(dt, states) { 48 | var ents = noa.ents 49 | 50 | // data struct that boxIntersect looks for 51 | // - array of [lo, lo, lo, hi, hi, hi] extents 52 | for (var i = 0; i < states.length; i++) { 53 | var id = states[i].__id 54 | var dat = ents.getPositionData(id) 55 | intervals[i] = dat._extents 56 | } 57 | intervals.length = states.length 58 | 59 | // run the intersect library 60 | boxIntersect(intervals, function (a, b) { 61 | var stateA = states[a] 62 | var stateB = states[b] 63 | if (!stateA || !stateB) return 64 | var intervalA = intervals[a] 65 | var intervalB = intervals[b] 66 | if (cylindricalHitTest(stateA, stateB, intervalA, intervalB)) { 67 | handleCollision(noa, stateA, stateB) 68 | } 69 | }) 70 | 71 | } 72 | } 73 | 74 | 75 | 76 | /* 77 | * 78 | * IMPLEMENTATION 79 | * 80 | */ 81 | 82 | 83 | function handleCollision(noa, stateA, stateB) { 84 | var idA = stateA.__id 85 | var idB = stateB.__id 86 | 87 | // entities really do overlap, so check masks and call event handlers 88 | if (stateA.collideMask & stateB.collideBits) { 89 | if (stateA.callback) stateA.callback(idB) 90 | } 91 | if (stateB.collideMask & stateA.collideBits) { 92 | if (stateB.callback) stateB.callback(idA) 93 | } 94 | 95 | // general pairwise handler 96 | noa.ents.onPairwiseEntityCollision(idA, idB) 97 | } 98 | 99 | 100 | 101 | // For entities whose extents overlap, 102 | // test if collision still happens when taking cylinder flags into account 103 | 104 | function cylindricalHitTest(stateA, stateB, intervalA, intervalB) { 105 | if (stateA.cylinder) { 106 | if (stateB.cylinder) { 107 | return cylinderCylinderTest(intervalA, intervalB) 108 | } else { 109 | return cylinderBoxTest(intervalA, intervalB) 110 | } 111 | } else if (stateB.cylinder) { 112 | return cylinderBoxTest(intervalB, intervalA) 113 | } 114 | return true 115 | } 116 | 117 | 118 | 119 | 120 | // Cylinder-cylinder hit test (AABBs are known to overlap) 121 | // given their extent arrays [lo, lo, lo, hi, hi, hi] 122 | 123 | function cylinderCylinderTest(a, b) { 124 | // distance between cylinder centers 125 | var rada = (a[3] - a[0]) / 2 126 | var radb = (b[3] - b[0]) / 2 127 | var dx = a[0] + rada - (b[0] + radb) 128 | var dz = a[2] + rada - (b[2] + radb) 129 | // collide if dist <= sum of radii 130 | var distsq = dx * dx + dz * dz 131 | var radsum = rada + radb 132 | return (distsq <= radsum * radsum) 133 | } 134 | 135 | 136 | 137 | 138 | // Cylinder-Box hit test (AABBs are known to overlap) 139 | // given their extent arrays [lo, lo, lo, hi, hi, hi] 140 | 141 | function cylinderBoxTest(cyl, cube) { 142 | // X-z center of cylinder 143 | var rad = (cyl[3] - cyl[0]) / 2 144 | var cx = cyl[0] + rad 145 | var cz = cyl[2] + rad 146 | // point in X-Z square closest to cylinder 147 | var px = clamp(cx, cube[0], cube[3]) 148 | var pz = clamp(cz, cube[2], cube[5]) 149 | // collision if distance from that point to circle <= cylinder radius 150 | var dx = px - cx 151 | var dz = pz - cz 152 | var distsq = dx * dx + dz * dz 153 | return (distsq <= rad * rad) 154 | } 155 | 156 | function clamp(val, lo, hi) { 157 | return (val < lo) ? lo : (val > hi) ? hi : val 158 | } 159 | 160 | 161 | 162 | 163 | } 164 | -------------------------------------------------------------------------------- /src/components/collideTerrain.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | export default function (noa) { 4 | return { 5 | 6 | name: 'collideTerrain', 7 | 8 | order: 0, 9 | 10 | state: { 11 | callback: null 12 | }, 13 | 14 | onAdd: function (eid, state) { 15 | // add collide handler for physics engine to call 16 | var ents = noa.entities 17 | if (ents.hasPhysics(eid)) { 18 | var body = ents.getPhysics(eid).body 19 | body.onCollide = function bodyOnCollide(impulse) { 20 | var cb = noa.ents.getCollideTerrain(eid).callback 21 | if (cb) cb(impulse, eid) 22 | } 23 | } 24 | }, 25 | 26 | onRemove: function (eid, state) { 27 | var ents = noa.entities 28 | if (ents.hasPhysics(eid)) { 29 | ents.getPhysics(eid).body.onCollide = null 30 | } 31 | }, 32 | 33 | 34 | 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/components/fadeOnZoom.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Component for the player entity, when active hides the player's mesh 4 | * when camera zoom is less than a certain amount 5 | */ 6 | 7 | export default function (noa) { 8 | return { 9 | 10 | name: 'fadeOnZoom', 11 | 12 | order: 99, 13 | 14 | state: { 15 | cutoff: 1.5, 16 | }, 17 | 18 | onAdd: null, 19 | 20 | onRemove: null, 21 | 22 | system: function fadeOnZoomProc(dt, states) { 23 | var zoom = noa.camera.currentZoom 24 | for (var i = 0; i < states.length; i++) { 25 | checkZoom(states[i], zoom, noa) 26 | } 27 | } 28 | } 29 | } 30 | 31 | 32 | function checkZoom(state, zoom, noa) { 33 | if (!noa.ents.hasMesh(state.__id)) return 34 | var mesh = noa.ents.getMeshData(state.__id).mesh 35 | if (!mesh.metadata) return 36 | var shouldHide = (zoom < state.cutoff) 37 | noa.rendering.setMeshVisibility(mesh, !shouldHide) 38 | } 39 | -------------------------------------------------------------------------------- /src/components/followsEntity.js: -------------------------------------------------------------------------------- 1 | 2 | import vec3 from 'gl-vec3' 3 | 4 | 5 | /* 6 | * Indicates that an entity should be moved to another entity's position each tick, 7 | * possibly by a fixed offset, and the same for renderPositions each render 8 | */ 9 | 10 | export default function (noa) { 11 | 12 | return { 13 | 14 | name: 'followsEntity', 15 | 16 | order: 50, 17 | 18 | state: { 19 | entity: 0 | 0, 20 | offset: null, 21 | onTargetMissing: null, 22 | }, 23 | 24 | onAdd: function (eid, state) { 25 | var off = vec3.create() 26 | state.offset = (state.offset) ? vec3.copy(off, state.offset) : off 27 | updatePosition(state) 28 | updateRenderPosition(state) 29 | }, 30 | 31 | onRemove: null, 32 | 33 | 34 | // on tick, copy over regular positions 35 | system: function followEntity(dt, states) { 36 | for (var i = 0; i < states.length; i++) { 37 | updatePosition(states[i]) 38 | } 39 | }, 40 | 41 | 42 | // on render, copy over render positions 43 | renderSystem: function followEntityMesh(dt, states) { 44 | for (var i = 0; i < states.length; i++) { 45 | updateRenderPosition(states[i]) 46 | } 47 | } 48 | } 49 | 50 | 51 | 52 | function updatePosition(state) { 53 | var id = state.__id 54 | var self = noa.ents.getPositionData(id) 55 | var other = noa.ents.getPositionData(state.entity) 56 | if (!other) { 57 | if (state.onTargetMissing) state.onTargetMissing(id) 58 | noa.ents.removeComponent(id, noa.ents.names.followsEntity) 59 | } else { 60 | vec3.add(self._localPosition, other._localPosition, state.offset) 61 | } 62 | } 63 | 64 | function updateRenderPosition(state) { 65 | var id = state.__id 66 | var self = noa.ents.getPositionData(id) 67 | var other = noa.ents.getPositionData(state.entity) 68 | if (other) { 69 | vec3.add(self._renderPosition, other._renderPosition, state.offset) 70 | } 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/components/mesh.js: -------------------------------------------------------------------------------- 1 | 2 | import vec3 from 'gl-vec3' 3 | 4 | 5 | export default function (noa) { 6 | return { 7 | 8 | name: 'mesh', 9 | 10 | order: 100, 11 | 12 | state: { 13 | mesh: null, 14 | offset: null 15 | }, 16 | 17 | 18 | onAdd: function (eid, state) { 19 | // implicitly assume there's already a position component 20 | var posDat = noa.ents.getPositionData(eid) 21 | if (state.mesh) { 22 | noa.rendering.addMeshToScene(state.mesh, false, posDat.position) 23 | } else { 24 | throw new Error('Mesh component added without a mesh - probably a bug!') 25 | } 26 | if (!state.offset) state.offset = vec3.create() 27 | 28 | // set mesh to correct position 29 | var rpos = posDat._renderPosition 30 | state.mesh.position.copyFromFloats( 31 | rpos[0] + state.offset[0], 32 | rpos[1] + state.offset[1], 33 | rpos[2] + state.offset[2]) 34 | }, 35 | 36 | 37 | onRemove: function (eid, state) { 38 | state.mesh.dispose() 39 | }, 40 | 41 | 42 | 43 | renderSystem: function (dt, states) { 44 | // before render move each mesh to its render position, 45 | // set by the physics engine or driving logic 46 | for (var i = 0; i < states.length; i++) { 47 | var state = states[i] 48 | var id = state.__id 49 | 50 | var rpos = noa.ents.getPositionData(id)._renderPosition 51 | state.mesh.position.copyFromFloats( 52 | rpos[0] + state.offset[0], 53 | rpos[1] + state.offset[1], 54 | rpos[2] + state.offset[2]) 55 | } 56 | } 57 | 58 | 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/components/movement.js: -------------------------------------------------------------------------------- 1 | 2 | import vec3 from 'gl-vec3' 3 | 4 | 5 | 6 | 7 | 8 | /** 9 | * 10 | * State object of the `movement` component 11 | * 12 | */ 13 | export function MovementState() { 14 | this.heading = 0 // radians 15 | this.running = false 16 | this.jumping = false 17 | 18 | // options 19 | this.maxSpeed = 10 20 | this.moveForce = 30 21 | this.responsiveness = 15 22 | this.runningFriction = 0 23 | this.standingFriction = 2 24 | 25 | // jumps 26 | this.airMoveMult = 0.5 27 | this.jumpImpulse = 10 28 | this.jumpForce = 12 29 | this.jumpTime = 500 // ms 30 | this.airJumps = 1 31 | 32 | // internal state 33 | this._jumpCount = 0 34 | this._currjumptime = 0 35 | this._isJumping = false 36 | } 37 | 38 | 39 | 40 | 41 | 42 | /** 43 | * Movement component. State stores settings like jump height, etc., 44 | * as well as current state (running, jumping, heading angle). 45 | * Processor checks state and applies movement/friction/jump forces 46 | * to the entity's physics body. 47 | * @param {import('..').Engine} noa 48 | */ 49 | 50 | export default function (noa) { 51 | return { 52 | 53 | name: 'movement', 54 | 55 | order: 30, 56 | 57 | state: new MovementState(), 58 | 59 | onAdd: null, 60 | 61 | onRemove: null, 62 | 63 | 64 | system: function movementProcessor(dt, states) { 65 | var ents = noa.entities 66 | for (var i = 0; i < states.length; i++) { 67 | var state = states[i] 68 | var phys = ents.getPhysics(state.__id) 69 | if (phys) applyMovementPhysics(dt, state, phys.body) 70 | } 71 | } 72 | 73 | 74 | } 75 | } 76 | 77 | 78 | var tempvec = vec3.create() 79 | var tempvec2 = vec3.create() 80 | var zeroVec = vec3.create() 81 | 82 | 83 | /** 84 | * @param {number} dt 85 | * @param {MovementState} state 86 | * @param {*} body 87 | */ 88 | 89 | function applyMovementPhysics(dt, state, body) { 90 | // move implementation originally written as external module 91 | // see https://github.com/fenomas/voxel-fps-controller 92 | // for original code 93 | 94 | // jumping 95 | var onGround = (body.atRestY() < 0) 96 | var canjump = (onGround || state._jumpCount < state.airJumps) 97 | if (onGround) { 98 | state._isJumping = false 99 | state._jumpCount = 0 100 | } 101 | 102 | // process jump input 103 | if (state.jumping) { 104 | if (state._isJumping) { // continue previous jump 105 | if (state._currjumptime > 0) { 106 | var jf = state.jumpForce 107 | if (state._currjumptime < dt) jf *= state._currjumptime / dt 108 | body.applyForce([0, jf, 0]) 109 | state._currjumptime -= dt 110 | } 111 | } else if (canjump) { // start new jump 112 | state._isJumping = true 113 | if (!onGround) state._jumpCount++ 114 | state._currjumptime = state.jumpTime 115 | body.applyImpulse([0, state.jumpImpulse, 0]) 116 | // clear downward velocity on airjump 117 | if (!onGround && body.velocity[1] < 0) body.velocity[1] = 0 118 | } 119 | } else { 120 | state._isJumping = false 121 | } 122 | 123 | // apply movement forces if entity is moving, otherwise just friction 124 | var m = tempvec 125 | var push = tempvec2 126 | if (state.running) { 127 | 128 | var speed = state.maxSpeed 129 | // todo: add crouch/sprint modifiers if needed 130 | // if (state.sprint) speed *= state.sprintMoveMult 131 | // if (state.crouch) speed *= state.crouchMoveMult 132 | vec3.set(m, 0, 0, speed) 133 | 134 | // rotate move vector to entity's heading 135 | vec3.rotateY(m, m, zeroVec, state.heading) 136 | 137 | // push vector to achieve desired speed & dir 138 | // following code to adjust 2D velocity to desired amount is patterned on Quake: 139 | // https://github.com/id-Software/Quake-III-Arena/blob/master/code/game/bg_pmove.c#L275 140 | vec3.subtract(push, m, body.velocity) 141 | push[1] = 0 142 | var pushLen = vec3.length(push) 143 | vec3.normalize(push, push) 144 | 145 | if (pushLen > 0) { 146 | // pushing force vector 147 | var canPush = state.moveForce 148 | if (!onGround) canPush *= state.airMoveMult 149 | 150 | // apply final force 151 | var pushAmt = state.responsiveness * pushLen 152 | if (canPush > pushAmt) canPush = pushAmt 153 | 154 | vec3.scale(push, push, canPush) 155 | body.applyForce(push) 156 | } 157 | 158 | // different friction when not moving 159 | // idea from Sonic: http://info.sonicretro.org/SPG:Running 160 | body.friction = state.runningFriction 161 | } else { 162 | body.friction = state.standingFriction 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/components/physics.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * @internal 4 | */ 5 | 6 | import vec3 from 'gl-vec3' 7 | 8 | 9 | export class PhysicsState { 10 | constructor() { 11 | /** @type {import('voxel-physics-engine').RigidBody} */ 12 | this.body = null 13 | } 14 | } 15 | 16 | 17 | /** 18 | * Physics component, stores an entity's physics engbody. 19 | * @param {import('..').Engine} noa 20 | */ 21 | 22 | export default function (noa) { 23 | 24 | return { 25 | 26 | name: 'physics', 27 | 28 | order: 40, 29 | 30 | state: new PhysicsState, 31 | 32 | onAdd: function (entID, state) { 33 | state.body = noa.physics.addBody() 34 | // implicitly assume body has a position component, to get size 35 | var posDat = noa.ents.getPositionData(state.__id) 36 | setPhysicsFromPosition(state, posDat) 37 | }, 38 | 39 | 40 | onRemove: function (entID, state) { 41 | // update position before removing 42 | // this lets entity wind up at e.g. the result of a collision 43 | // even if physics component is removed in collision handler 44 | if (noa.ents.hasPosition(state.__id)) { 45 | var pdat = noa.ents.getPositionData(state.__id) 46 | setPositionFromPhysics(state, pdat) 47 | backtrackRenderPos(state, pdat, 0, false) 48 | } 49 | noa.physics.removeBody(state.body) 50 | }, 51 | 52 | 53 | system: function (dt, states) { 54 | for (var i = 0; i < states.length; i++) { 55 | var state = states[i] 56 | var pdat = noa.ents.getPositionData(state.__id) 57 | setPositionFromPhysics(state, pdat) 58 | } 59 | }, 60 | 61 | 62 | renderSystem: function (dt, states) { 63 | 64 | var tickPos = noa.positionInCurrentTick 65 | var tickTime = 1000 / noa.container._shell.tickRate 66 | tickTime *= noa.timeScale 67 | var tickMS = tickPos * tickTime 68 | 69 | // tickMS is time since last physics engine tick 70 | // to avoid temporal aliasing, render the state as if lerping between 71 | // the last position and the next one 72 | // since the entity data is the "next" position this amounts to 73 | // offsetting each entity into the past by tickRate - dt 74 | // http://gafferongames.com/game-physics/fix-your-timestep/ 75 | 76 | var backtrackAmt = (tickMS - tickTime) / 1000 77 | for (var i = 0; i < states.length; i++) { 78 | var state = states[i] 79 | var id = state.__id 80 | var pdat = noa.ents.getPositionData(id) 81 | var smoothed = noa.ents.cameraSmoothed(id) 82 | backtrackRenderPos(state, pdat, backtrackAmt, smoothed) 83 | } 84 | } 85 | 86 | } 87 | 88 | } 89 | 90 | 91 | 92 | // var offset = vec3.create() 93 | var local = vec3.create() 94 | 95 | export function setPhysicsFromPosition(physState, posState) { 96 | var box = physState.body.aabb 97 | var ext = posState._extents 98 | vec3.copy(box.base, ext) 99 | vec3.set(box.vec, posState.width, posState.height, posState.width) 100 | vec3.add(box.max, box.base, box.vec) 101 | } 102 | 103 | 104 | function setPositionFromPhysics(physState, posState) { 105 | var base = physState.body.aabb.base 106 | var hw = posState.width / 2 107 | vec3.set(posState._localPosition, base[0] + hw, base[1], base[2] + hw) 108 | } 109 | 110 | 111 | function backtrackRenderPos(physState, posState, backtrackAmt, smoothed) { 112 | // pos = pos + backtrack * body.velocity 113 | var vel = physState.body.velocity 114 | vec3.scaleAndAdd(local, posState._localPosition, vel, backtrackAmt) 115 | 116 | // smooth out update if component is present 117 | // (this is set after sudden movements like auto-stepping) 118 | if (smoothed) vec3.lerp(local, posState._renderPosition, local, 0.3) 119 | 120 | // copy values over to renderPosition, 121 | vec3.copy(posState._renderPosition, local) 122 | } 123 | -------------------------------------------------------------------------------- /src/components/position.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 3 | * @internal 4 | */ 5 | 6 | import vec3 from 'gl-vec3' 7 | 8 | 9 | 10 | // definition for this component's state object 11 | export class PositionState { 12 | constructor() { 13 | /** Position in global coords (may be low precision) 14 | * @type {null | number[]} */ 15 | this.position = null 16 | this.width = 0.8 17 | this.height = 0.8 18 | 19 | /** Precise position in local coords 20 | * @type {null | number[]} */ 21 | this._localPosition = null 22 | 23 | /** [x,y,z] in LOCAL COORDS 24 | * @type {null | number[]} */ 25 | this._renderPosition = null 26 | 27 | /** [lo,lo,lo, hi,hi,hi] in LOCAL COORDS 28 | * @type {null | number[]} */ 29 | this._extents = null 30 | } 31 | } 32 | 33 | 34 | 35 | 36 | /** 37 | * Component holding entity's position, width, and height. 38 | * By convention, entity's "position" is the bottom center of its AABB 39 | * 40 | * Of the various properties, _localPosition is the "real", 41 | * single-source-of-truth position. Others are derived. 42 | * Local coords are relative to `noa.worldOriginOffset`. 43 | * @param {import('..').Engine} noa 44 | */ 45 | 46 | export default function (noa) { 47 | 48 | return { 49 | 50 | name: 'position', 51 | 52 | order: 60, 53 | 54 | state: new PositionState, 55 | 56 | onAdd: function (eid, state) { 57 | // copy position into a plain array 58 | var pos = [0, 0, 0] 59 | if (state.position) vec3.copy(pos, state.position) 60 | state.position = pos 61 | 62 | state._localPosition = vec3.create() 63 | state._renderPosition = vec3.create() 64 | state._extents = new Float32Array(6) 65 | 66 | // on init only, set local from global 67 | noa.globalToLocal(state.position, null, state._localPosition) 68 | vec3.copy(state._renderPosition, state._localPosition) 69 | updatePositionExtents(state) 70 | }, 71 | 72 | onRemove: null, 73 | 74 | 75 | 76 | system: function (dt, states) { 77 | var off = noa.worldOriginOffset 78 | for (var i = 0; i < states.length; i++) { 79 | var state = states[i] 80 | vec3.add(state.position, state._localPosition, off) 81 | updatePositionExtents(state) 82 | } 83 | }, 84 | 85 | 86 | } 87 | } 88 | 89 | 90 | 91 | // update an entity's position state `_extents` 92 | export function updatePositionExtents(state) { 93 | var hw = state.width / 2 94 | var lpos = state._localPosition 95 | var ext = state._extents 96 | ext[0] = lpos[0] - hw 97 | ext[1] = lpos[1] 98 | ext[2] = lpos[2] - hw 99 | ext[3] = lpos[0] + hw 100 | ext[4] = lpos[1] + state.height 101 | ext[5] = lpos[2] + hw 102 | } 103 | -------------------------------------------------------------------------------- /src/components/receivesInputs.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * 4 | * Input processing component - gets (key) input state and 5 | * applies it to receiving entities by updating their movement 6 | * component state (heading, movespeed, jumping, etc.) 7 | * 8 | */ 9 | 10 | export default function (noa) { 11 | return { 12 | 13 | name: 'receivesInputs', 14 | 15 | order: 20, 16 | 17 | state: {}, 18 | 19 | onAdd: null, 20 | 21 | onRemove: null, 22 | 23 | system: function inputProcessor(dt, states) { 24 | var ents = noa.entities 25 | var inputState = noa.inputs.state 26 | var camHeading = noa.camera.heading 27 | 28 | for (var i = 0; i < states.length; i++) { 29 | var state = states[i] 30 | var moveState = ents.getMovement(state.__id) 31 | setMovementState(moveState, inputState, camHeading) 32 | } 33 | } 34 | 35 | } 36 | } 37 | 38 | 39 | 40 | /** 41 | * @param {import('../components/movement').MovementState} state 42 | * @param {Object} inputs 43 | * @param {number} camHeading 44 | */ 45 | 46 | function setMovementState(state, inputs, camHeading) { 47 | state.jumping = !!inputs.jump 48 | 49 | var fb = inputs.forward ? (inputs.backward ? 0 : 1) : (inputs.backward ? -1 : 0) 50 | var rl = inputs.right ? (inputs.left ? 0 : 1) : (inputs.left ? -1 : 0) 51 | 52 | if ((fb | rl) === 0) { 53 | state.running = false 54 | } else { 55 | state.running = true 56 | if (fb) { 57 | if (fb == -1) camHeading += Math.PI 58 | if (rl) { 59 | camHeading += Math.PI / 4 * fb * rl // didn't plan this but it works! 60 | } 61 | } else { 62 | camHeading += rl * Math.PI / 2 63 | } 64 | state.heading = camHeading 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/components/shadow.js: -------------------------------------------------------------------------------- 1 | 2 | import vec3 from 'gl-vec3' 3 | 4 | import { Color3 } from '@babylonjs/core/Maths/math.color' 5 | import { CreateDisc } from '@babylonjs/core/Meshes/Builders/discBuilder' 6 | import '@babylonjs/core/Meshes/instancedMesh' 7 | 8 | 9 | /** @param {import('../index').Engine} noa */ 10 | export default function (noa, distance = 10) { 11 | 12 | var shadowDist = distance 13 | 14 | // create a mesh to re-use for shadows 15 | var scene = noa.rendering.getScene() 16 | var disc = CreateDisc('shadow', { radius: 0.75, tessellation: 30 }, scene) 17 | disc.rotation.x = Math.PI / 2 18 | var mat = noa.rendering.makeStandardMaterial('shadow_component_mat') 19 | mat.diffuseColor.set(0, 0, 0) 20 | mat.ambientColor.set(0, 0, 0) 21 | mat.alpha = 0.5 22 | disc.material = mat 23 | mat.freeze() 24 | 25 | // source mesh needn't be in the scene graph 26 | noa.rendering.setMeshVisibility(disc, false) 27 | 28 | 29 | return { 30 | 31 | name: 'shadow', 32 | 33 | order: 80, 34 | 35 | state: { 36 | size: 0.5, 37 | _mesh: null, 38 | }, 39 | 40 | 41 | onAdd: function (eid, state) { 42 | var mesh = disc.createInstance('shadow_instance') 43 | noa.rendering.addMeshToScene(mesh) 44 | mesh.setEnabled(false) 45 | state._mesh = mesh 46 | }, 47 | 48 | 49 | onRemove: function (eid, state) { 50 | state._mesh.dispose() 51 | state._mesh = null 52 | }, 53 | 54 | 55 | system: function shadowSystem(dt, states) { 56 | var cpos = noa.camera._localGetPosition() 57 | var dist = shadowDist 58 | for (var i = 0; i < states.length; i++) { 59 | var state = states[i] 60 | var posState = noa.ents.getPositionData(state.__id) 61 | var physState = noa.ents.getPhysics(state.__id) 62 | updateShadowHeight(noa, posState, physState, state._mesh, state.size, dist, cpos) 63 | } 64 | }, 65 | 66 | 67 | renderSystem: function (dt, states) { 68 | // before render adjust shadow x/z to render positions 69 | for (var i = 0; i < states.length; i++) { 70 | var state = states[i] 71 | var rpos = noa.ents.getPositionData(state.__id)._renderPosition 72 | var spos = state._mesh.position 73 | spos.x = rpos[0] 74 | spos.z = rpos[2] 75 | } 76 | } 77 | 78 | 79 | 80 | 81 | } 82 | } 83 | 84 | var shadowPos = vec3.fromValues(0, 0, 0) 85 | var down = vec3.fromValues(0, -1, 0) 86 | 87 | function updateShadowHeight(noa, posDat, physDat, mesh, size, shadowDist, camPos) { 88 | 89 | // local Y ground position - from physics or raycast 90 | var localY 91 | if (physDat && physDat.body.resting[1] < 0) { 92 | localY = posDat._localPosition[1] 93 | } else { 94 | var res = noa._localPick(posDat._localPosition, down, shadowDist) 95 | if (!res) { 96 | mesh.setEnabled(false) 97 | return 98 | } 99 | localY = res.position[1] - noa.worldOriginOffset[1] 100 | } 101 | 102 | // round Y pos and offset upwards slightly to avoid z-fighting 103 | localY = Math.round(localY) 104 | vec3.copy(shadowPos, posDat._localPosition) 105 | shadowPos[1] = localY 106 | var sqdist = vec3.squaredDistance(camPos, shadowPos) 107 | // offset ~ 0.01 for nearby shadows, up to 0.1 at distance of ~40 108 | var offset = 0.01 + 0.1 * (sqdist / 1600) 109 | if (offset > 0.1) offset = 0.1 110 | mesh.position.y = localY + offset 111 | // set shadow scale 112 | var dist = posDat._localPosition[1] - localY 113 | var scale = size * 0.7 * (1 - dist / shadowDist) 114 | mesh.scaling.copyFromFloats(scale, scale, scale) 115 | mesh.setEnabled(true) 116 | } 117 | -------------------------------------------------------------------------------- /src/components/smoothCamera.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | export default function (noa) { 4 | 5 | var compName = 'smoothCamera' 6 | 7 | return { 8 | 9 | name: compName, 10 | 11 | order: 99, 12 | 13 | state: { 14 | time: 100.1 15 | }, 16 | 17 | onAdd: null, 18 | 19 | onRemove: null, 20 | 21 | system: function (dt, states) { 22 | // remove self after time elapses 23 | for (var i = 0; i < states.length; i++) { 24 | var state = states[i] 25 | state.time -= dt 26 | if (state.time < 0) noa.ents.removeComponent(state.__id, compName) 27 | } 28 | }, 29 | 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/lib/camera.js: -------------------------------------------------------------------------------- 1 | 2 | import vec3 from 'gl-vec3' 3 | import aabb from 'aabb-3d' 4 | import sweep from 'voxel-aabb-sweep' 5 | 6 | 7 | 8 | // default options 9 | function CameraDefaults() { 10 | this.inverseX = false 11 | this.inverseY = false 12 | this.sensitivityMult = 1 13 | this.sensitivityMultOutsidePointerlock = 0 14 | this.sensitivityX = 10 15 | this.sensitivityY = 10 16 | this.initialZoom = 0 17 | this.zoomSpeed = 0.2 18 | } 19 | 20 | 21 | // locals 22 | var tempVectors = [ 23 | vec3.create(), 24 | vec3.create(), 25 | vec3.create(), 26 | ] 27 | var originVector = vec3.create() 28 | 29 | 30 | /** 31 | * `noa.camera` - manages the camera, its position and direction, 32 | * mouse sensitivity, and so on. 33 | * 34 | * This module uses the following default options (from the options 35 | * object passed to the {@link Engine}): 36 | * ```js 37 | * var defaults = { 38 | * inverseX: false, 39 | * inverseY: false, 40 | * sensitivityX: 10, 41 | * sensitivityY: 10, 42 | * initialZoom: 0, 43 | * zoomSpeed: 0.2, 44 | * } 45 | * ``` 46 | */ 47 | 48 | export class Camera { 49 | 50 | /** 51 | * @internal 52 | * @param {import('../index').Engine} noa 53 | * @param {Partial.} opts 54 | */ 55 | constructor(noa, opts) { 56 | opts = Object.assign({}, new CameraDefaults, opts) 57 | this.noa = noa 58 | 59 | /** Horizontal mouse sensitivity. Same scale as Overwatch (typical values around `5..10`) */ 60 | this.sensitivityX = +opts.sensitivityX 61 | 62 | /** Vertical mouse sensitivity. Same scale as Overwatch (typical values around `5..10`) */ 63 | this.sensitivityY = +opts.sensitivityY 64 | 65 | /** Mouse look inverse (horizontal) */ 66 | this.inverseX = !!opts.inverseX 67 | 68 | /** Mouse look inverse (vertical) */ 69 | this.inverseY = !!opts.inverseY 70 | 71 | /** 72 | * Multiplier for temporarily altering mouse sensitivity. 73 | * Set this to `0` to temporarily disable camera controls. 74 | */ 75 | this.sensitivityMult = opts.sensitivityMult 76 | 77 | /** 78 | * Multiplier for altering mouse sensitivity when pointerlock 79 | * is not active - default of `0` means no camera movement. 80 | * Note this setting is ignored if pointerLock isn't supported. 81 | */ 82 | this.sensitivityMultOutsidePointerlock = opts.sensitivityMultOutsidePointerlock 83 | 84 | /** 85 | * Camera yaw angle. 86 | * Returns the camera's rotation angle around the vertical axis. 87 | * Range: `0..2π` 88 | * This value is writeable, but it's managed by the engine and 89 | * will be overwritten each frame. 90 | */ 91 | this.heading = 0 92 | 93 | /** Camera pitch angle. 94 | * Returns the camera's up/down rotation angle. The pitch angle is 95 | * clamped by a small epsilon, such that the camera never quite 96 | * points perfectly up or down. 97 | * Range: `-π/2..π/2`. 98 | * This value is writeable, but it's managed by the engine and 99 | * will be overwritten each frame. 100 | */ 101 | this.pitch = 0 102 | 103 | /** 104 | * Entity ID of a special entity that exists for the camera to point at. 105 | * 106 | * By default this entity follows the player entity, so you can 107 | * change the player's eye height by changing the `follow` component's offset: 108 | * ```js 109 | * var followState = noa.ents.getState(noa.camera.cameraTarget, 'followsEntity') 110 | * followState.offset[1] = 0.9 * myPlayerHeight 111 | * ``` 112 | * 113 | * For customized camera controls you can change the follow 114 | * target to some other entity, or override the behavior entirely: 115 | * ```js 116 | * // make cameraTarget stop following the player 117 | * noa.ents.removeComponent(noa.camera.cameraTarget, 'followsEntity') 118 | * // control cameraTarget position directly (or whatever..) 119 | * noa.ents.setPosition(noa.camera.cameraTarget, [x,y,z]) 120 | * ``` 121 | */ 122 | this.cameraTarget = this.noa.ents.createEntity(['position']) 123 | 124 | // make the camera follow the cameraTarget entity 125 | var eyeOffset = 0.9 * noa.ents.getPositionData(noa.playerEntity).height 126 | noa.ents.addComponent(this.cameraTarget, 'followsEntity', { 127 | entity: noa.playerEntity, 128 | offset: [0, eyeOffset, 0], 129 | }) 130 | 131 | /** How far back the camera should be from the player's eye position */ 132 | this.zoomDistance = opts.initialZoom 133 | 134 | /** How quickly the camera moves to its `zoomDistance` (0..1) */ 135 | this.zoomSpeed = opts.zoomSpeed 136 | 137 | /** Current actual zoom distance. This differs from `zoomDistance` when 138 | * the camera is in the process of moving towards the desired distance, 139 | * or when it's obstructed by solid terrain behind the player. 140 | * This value will get overwritten each tick, but you may want to write to it 141 | * when overriding the camera zoom speed. 142 | */ 143 | this.currentZoom = opts.initialZoom 144 | 145 | /** @internal */ 146 | this._dirVector = vec3.fromValues(0, 0, 1) 147 | } 148 | 149 | 150 | 151 | 152 | /* 153 | * 154 | * 155 | * API 156 | * 157 | * 158 | */ 159 | 160 | 161 | /* 162 | * Local position functions for high precision 163 | */ 164 | /** @internal */ 165 | _localGetTargetPosition() { 166 | var pdat = this.noa.ents.getPositionData(this.cameraTarget) 167 | var pos = tempVectors[0] 168 | return vec3.copy(pos, pdat._renderPosition) 169 | } 170 | /** @internal */ 171 | _localGetPosition() { 172 | var loc = this._localGetTargetPosition() 173 | if (this.currentZoom === 0) return loc 174 | return vec3.scaleAndAdd(loc, loc, this._dirVector, -this.currentZoom) 175 | } 176 | 177 | 178 | 179 | /** 180 | * Returns the camera's current target position - i.e. the player's 181 | * eye position. When the camera is zoomed all the way in, 182 | * this returns the same location as `camera.getPosition()`. 183 | */ 184 | getTargetPosition() { 185 | var loc = this._localGetTargetPosition() 186 | var globalCamPos = tempVectors[1] 187 | return this.noa.localToGlobal(loc, globalCamPos) 188 | } 189 | 190 | 191 | /** 192 | * Returns the current camera position (read only) 193 | */ 194 | getPosition() { 195 | var loc = this._localGetPosition() 196 | var globalCamPos = tempVectors[2] 197 | return this.noa.localToGlobal(loc, globalCamPos) 198 | } 199 | 200 | 201 | /** 202 | * Returns the camera direction vector (read only) 203 | */ 204 | getDirection() { 205 | return this._dirVector 206 | } 207 | 208 | 209 | 210 | 211 | /* 212 | * 213 | * 214 | * 215 | * internals below 216 | * 217 | * 218 | * 219 | */ 220 | 221 | 222 | 223 | /** 224 | * Called before render, if mouseLock etc. is applicable. 225 | * Applies current mouse x/y inputs to the camera angle and zoom 226 | * @internal 227 | */ 228 | 229 | applyInputsToCamera() { 230 | 231 | // conditional changes to mouse sensitivity 232 | var senseMult = this.sensitivityMult 233 | if (this.noa.container.supportsPointerLock) { 234 | if (!this.noa.container.hasPointerLock) { 235 | senseMult *= this.sensitivityMultOutsidePointerlock 236 | } 237 | } 238 | if (senseMult === 0) return 239 | 240 | // dx/dy from input state 241 | var pointerState = this.noa.inputs.pointerState 242 | bugFix(pointerState) // TODO: REMOVE EVENTUALLY 243 | 244 | // convert to rads, using (sens * 0.0066 deg/pixel), like Overwatch 245 | var conv = 0.0066 * Math.PI / 180 246 | var dx = pointerState.dx * this.sensitivityX * senseMult * conv 247 | var dy = pointerState.dy * this.sensitivityY * senseMult * conv 248 | if (this.inverseX) dx = -dx 249 | if (this.inverseY) dy = -dy 250 | 251 | // normalize/clamp angles, update direction vector 252 | var twopi = 2 * Math.PI 253 | this.heading += (dx < 0) ? dx + twopi : dx 254 | if (this.heading > twopi) this.heading -= twopi 255 | var maxPitch = Math.PI / 2 - 0.001 256 | this.pitch = Math.max(-maxPitch, Math.min(maxPitch, this.pitch + dy)) 257 | 258 | vec3.set(this._dirVector, 0, 0, 1) 259 | var dir = this._dirVector 260 | var origin = originVector 261 | vec3.rotateX(dir, dir, origin, this.pitch) 262 | vec3.rotateY(dir, dir, origin, this.heading) 263 | } 264 | 265 | 266 | 267 | /** 268 | * Called before all renders, pre- and post- entity render systems 269 | * @internal 270 | */ 271 | updateBeforeEntityRenderSystems() { 272 | // zoom update 273 | this.currentZoom += (this.zoomDistance - this.currentZoom) * this.zoomSpeed 274 | } 275 | 276 | /** @internal */ 277 | updateAfterEntityRenderSystems() { 278 | // clamp camera zoom not to clip into solid terrain 279 | var maxZoom = cameraObstructionDistance(this) 280 | if (this.currentZoom > maxZoom) this.currentZoom = maxZoom 281 | } 282 | 283 | } 284 | 285 | 286 | 287 | 288 | /* 289 | * check for obstructions behind camera by sweeping back an AABB 290 | */ 291 | 292 | function cameraObstructionDistance(self) { 293 | if (!self._sweepBox) { 294 | self._sweepBox = new aabb([0, 0, 0], [0.2, 0.2, 0.2]) 295 | self._sweepGetVoxel = self.noa.world.getBlockSolidity.bind(self.noa.world) 296 | self._sweepVec = vec3.create() 297 | self._sweepHit = () => true 298 | } 299 | var pos = vec3.copy(self._sweepVec, self._localGetTargetPosition()) 300 | vec3.add(pos, pos, self.noa.worldOriginOffset) 301 | for (var i = 0; i < 3; i++) pos[i] -= 0.1 302 | self._sweepBox.setPosition(pos) 303 | var dist = Math.max(self.zoomDistance, self.currentZoom) + 0.1 304 | vec3.scale(self._sweepVec, self.getDirection(), -dist) 305 | return sweep(self._sweepGetVoxel, self._sweepBox, self._sweepVec, self._sweepHit, true) 306 | } 307 | 308 | 309 | 310 | 311 | 312 | 313 | // workaround for this Chrome 63 + Win10 bug 314 | // https://bugs.chromium.org/p/chromium/issues/detail?id=781182 315 | // later updated to also address: https://github.com/fenomas/noa/issues/153 316 | function bugFix(pointerState) { 317 | var dx = pointerState.dx 318 | var dy = pointerState.dy 319 | var badx = (Math.abs(dx) > 400 && Math.abs(dx / lastx) > 4) 320 | var bady = (Math.abs(dy) > 400 && Math.abs(dy / lasty) > 4) 321 | if (badx || bady) { 322 | pointerState.dx = lastx 323 | pointerState.dy = lasty 324 | lastx = (lastx + dx) / 2 325 | lasty = (lasty + dy) / 2 326 | } else { 327 | lastx = dx || 1 328 | lasty = dy || 1 329 | } 330 | } 331 | 332 | var lastx = 0 333 | var lasty = 0 334 | -------------------------------------------------------------------------------- /src/lib/chunk.js: -------------------------------------------------------------------------------- 1 | 2 | import { LocationQueue } from './util' 3 | import ndarray from 'ndarray' 4 | 5 | 6 | 7 | 8 | /* 9 | * 10 | * Chunk 11 | * 12 | * Stores and manages voxel ids and flags for each voxel within chunk 13 | * 14 | */ 15 | 16 | 17 | 18 | 19 | 20 | /* 21 | * 22 | * Chunk constructor 23 | * 24 | */ 25 | 26 | /** @param {import('../index').Engine} noa */ 27 | export function Chunk(noa, requestID, ci, cj, ck, size, dataArray, fillVoxelID = -1) { 28 | this.noa = noa 29 | this.isDisposed = false 30 | 31 | // arbitrary data passed in by client when generating world 32 | this.userData = null 33 | 34 | // voxel data and properties 35 | this.requestID = requestID // id sent to game client 36 | this.voxels = dataArray 37 | this.i = ci 38 | this.j = cj 39 | this.k = ck 40 | this.size = size 41 | this.x = ci * size 42 | this.y = cj * size 43 | this.z = ck * size 44 | this.pos = [this.x, this.y, this.z] 45 | 46 | // flags to track if things need re-meshing 47 | this._terrainDirty = false 48 | this._objectsDirty = false 49 | 50 | // inits state of terrain / object meshing 51 | this._terrainMeshes = [] 52 | noa._terrainMesher.initChunk(this) 53 | noa._objectMesher.initChunk(this) 54 | 55 | this._isFull = false 56 | this._isEmpty = false 57 | 58 | this._wholeLayerVoxel = Array(size).fill(-1) 59 | if (fillVoxelID >= 0) { 60 | this.voxels.data.fill(fillVoxelID, 0, this.voxels.size) 61 | this._wholeLayerVoxel.fill(fillVoxelID) 62 | } 63 | 64 | // references to neighboring chunks, if they exist (filled in by `world`) 65 | var narr = Array.from(Array(27), () => null) 66 | this._neighbors = ndarray(narr, [3, 3, 3]).lo(1, 1, 1) 67 | this._neighbors.set(0, 0, 0, this) 68 | this._neighborCount = 0 69 | this._timesMeshed = 0 70 | 71 | // location queue of voxels in this chunk with block handlers (assume it's rare) 72 | /** @internal */ 73 | this._blockHandlerLocs = new LocationQueue() 74 | 75 | // passes through voxel contents, calling block handlers etc. 76 | scanVoxelData(this) 77 | } 78 | 79 | 80 | // expose logic internally to create and update the voxel data array 81 | Chunk._createVoxelArray = function (size) { 82 | var arr = new Uint16Array(size * size * size) 83 | return ndarray(arr, [size, size, size]) 84 | } 85 | 86 | Chunk.prototype._updateVoxelArray = function (dataArray, fillVoxelID = -1) { 87 | // dispose current object blocks 88 | callAllBlockHandlers(this, 'onUnload') 89 | this.noa._objectMesher.disposeChunk(this) 90 | this.noa._terrainMesher.disposeChunk(this) 91 | this.voxels = dataArray 92 | this._terrainDirty = false 93 | this._objectsDirty = false 94 | this._blockHandlerLocs.empty() 95 | this.noa._objectMesher.initChunk(this) 96 | this.noa._terrainMesher.initChunk(this) 97 | 98 | if (fillVoxelID >= 0) { 99 | this._wholeLayerVoxel.fill(fillVoxelID) 100 | } else { 101 | this._wholeLayerVoxel.fill(-1) 102 | } 103 | 104 | scanVoxelData(this) 105 | } 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | /* 115 | * 116 | * Chunk API 117 | * 118 | */ 119 | 120 | // get/set deal with block IDs, so that this class acts like an ndarray 121 | 122 | Chunk.prototype.get = function (i, j, k) { 123 | return this.voxels.get(i, j, k) 124 | } 125 | 126 | Chunk.prototype.getSolidityAt = function (i, j, k) { 127 | var solidLookup = this.noa.registry._solidityLookup 128 | return solidLookup[this.voxels.get(i, j, k)] 129 | } 130 | 131 | Chunk.prototype.set = function (i, j, k, newID) { 132 | var oldID = this.voxels.get(i, j, k) 133 | if (newID === oldID) return 134 | 135 | // update voxel data 136 | this.voxels.set(i, j, k, newID) 137 | 138 | // lookup tables from registry, etc 139 | var solidLookup = this.noa.registry._solidityLookup 140 | var objectLookup = this.noa.registry._objectLookup 141 | var opaqueLookup = this.noa.registry._opacityLookup 142 | var handlerLookup = this.noa.registry._blockHandlerLookup 143 | 144 | // track invariants about chunk data 145 | if (!opaqueLookup[newID]) this._isFull = false 146 | if (newID !== 0) this._isEmpty = false 147 | if (this._wholeLayerVoxel[j] !== newID) this._wholeLayerVoxel[j] = -1 148 | 149 | // voxel lifecycle handling 150 | var hold = handlerLookup[oldID] 151 | var hnew = handlerLookup[newID] 152 | if (hold) callBlockHandler(this, hold, 'onUnset', i, j, k) 153 | if (hnew) { 154 | callBlockHandler(this, hnew, 'onSet', i, j, k) 155 | this._blockHandlerLocs.add(i, j, k) 156 | } else { 157 | this._blockHandlerLocs.remove(i, j, k) 158 | } 159 | 160 | // track object block states 161 | var objMesher = this.noa._objectMesher 162 | var objOld = objectLookup[oldID] 163 | var objNew = objectLookup[newID] 164 | if (objOld) objMesher.setObjectBlock(this, 0, i, j, k) 165 | if (objNew) objMesher.setObjectBlock(this, newID, i, j, k) 166 | 167 | // decide dirtiness states 168 | var solidityChanged = (solidLookup[oldID] !== solidLookup[newID]) 169 | var opacityChanged = (opaqueLookup[oldID] !== opaqueLookup[newID]) 170 | var wasTerrain = !objOld && (oldID !== 0) 171 | var nowTerrain = !objNew && (newID !== 0) 172 | 173 | if (objOld || objNew) this._objectsDirty = true 174 | if (solidityChanged || opacityChanged || wasTerrain || nowTerrain) { 175 | this._terrainDirty = true 176 | } 177 | 178 | if (this._terrainDirty || this._objectsDirty) { 179 | this.noa.world._queueChunkForRemesh(this) 180 | } 181 | 182 | // neighbors only affected if solidity or opacity changed on an edge 183 | if (solidityChanged || opacityChanged) { 184 | var edge = this.size - 1 185 | var imin = (i === 0) ? -1 : 0 186 | var jmin = (j === 0) ? -1 : 0 187 | var kmin = (k === 0) ? -1 : 0 188 | var imax = (i === edge) ? 1 : 0 189 | var jmax = (j === edge) ? 1 : 0 190 | var kmax = (k === edge) ? 1 : 0 191 | for (var ni = imin; ni <= imax; ni++) { 192 | for (var nj = jmin; nj <= jmax; nj++) { 193 | for (var nk = kmin; nk <= kmax; nk++) { 194 | if ((ni | nj | nk) === 0) continue 195 | var nab = this._neighbors.get(ni, nj, nk) 196 | if (!nab) continue 197 | nab._terrainDirty = true 198 | this.noa.world._queueChunkForRemesh(nab) 199 | } 200 | } 201 | } 202 | } 203 | } 204 | 205 | 206 | 207 | // helper to call handler of a given type at a particular xyz 208 | function callBlockHandler(chunk, handlers, type, i, j, k) { 209 | var handler = handlers[type] 210 | if (!handler) return 211 | handler(chunk.x + i, chunk.y + j, chunk.z + k) 212 | } 213 | 214 | 215 | // gets called by World when this chunk has been queued for remeshing 216 | Chunk.prototype.updateMeshes = function () { 217 | if (this._terrainDirty) { 218 | this.noa._terrainMesher.meshChunk(this) 219 | this._timesMeshed++ 220 | this._terrainDirty = false 221 | } 222 | if (this._objectsDirty) { 223 | this.noa._objectMesher.buildObjectMeshes() 224 | this._objectsDirty = false 225 | } 226 | } 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | /* 240 | * 241 | * Init 242 | * 243 | * Scans voxel data, processing object blocks and setting chunk flags 244 | * 245 | */ 246 | 247 | function scanVoxelData(chunk) { 248 | var voxels = chunk.voxels 249 | var data = voxels.data 250 | var len = voxels.shape[0] 251 | var opaqueLookup = chunk.noa.registry._opacityLookup 252 | var handlerLookup = chunk.noa.registry._blockHandlerLookup 253 | var objectLookup = chunk.noa.registry._objectLookup 254 | var plainLookup = chunk.noa.registry._blockIsPlainLookup 255 | var objMesher = chunk.noa._objectMesher 256 | 257 | // flags for tracking if chunk is entirely opaque or transparent 258 | var fullyOpaque = true 259 | var fullyAir = true 260 | 261 | // scan vertically.. 262 | for (var j = 0; j < len; ++j) { 263 | 264 | // fastest case where whole layer is air/dirt/etc 265 | var layerID = chunk._wholeLayerVoxel[j] 266 | if (layerID >= 0 && !objMesher[layerID] && !handlerLookup[layerID]) { 267 | if (!opaqueLookup[layerID]) fullyOpaque = false 268 | if (layerID !== 0) fullyAir = false 269 | continue 270 | } 271 | 272 | var constantID = voxels.get(0, j, 0) 273 | 274 | for (var i = 0; i < len; ++i) { 275 | var index = voxels.index(i, j, 0) 276 | for (var k = 0; k < len; ++k, ++index) { 277 | var id = data[index] 278 | 279 | // detect constant layer ID if there is one 280 | if (constantID >= 0 && id !== constantID) constantID = -1 281 | 282 | // most common cases: air block... 283 | if (id === 0) { 284 | fullyOpaque = false 285 | continue 286 | } 287 | // ...or plain boring block (no mesh, handlers, etc) 288 | if (plainLookup[id]) { 289 | fullyAir = false 290 | continue 291 | } 292 | // otherwise check opacity, object mesh, and handlers 293 | fullyOpaque = fullyOpaque && opaqueLookup[id] 294 | fullyAir = false 295 | if (objectLookup[id]) { 296 | objMesher.setObjectBlock(chunk, id, i, j, k) 297 | chunk._objectsDirty = true 298 | } 299 | var handlers = handlerLookup[id] 300 | if (handlers) { 301 | chunk._blockHandlerLocs.add(i, j, k) 302 | callBlockHandler(chunk, handlers, 'onLoad', i, j, k) 303 | } 304 | } 305 | } 306 | 307 | if (constantID >= 0) chunk._wholeLayerVoxel[j] = constantID 308 | } 309 | 310 | chunk._isFull = fullyOpaque 311 | chunk._isEmpty = fullyAir 312 | chunk._terrainDirty = !chunk._isEmpty 313 | } 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | // dispose function - just clears properties and references 325 | 326 | Chunk.prototype.dispose = function () { 327 | // look through the data for onUnload handlers 328 | callAllBlockHandlers(this, 'onUnload') 329 | this._blockHandlerLocs.empty() 330 | 331 | // let meshers dispose their stuff 332 | this.noa._objectMesher.disposeChunk(this) 333 | this.noa._terrainMesher.disposeChunk(this) 334 | 335 | // apparently there's no way to dispose typed arrays, so just null everything 336 | this.voxels.data = null 337 | this.voxels = null 338 | this._neighbors.data = null 339 | this._neighbors = null 340 | 341 | this.isDisposed = true 342 | } 343 | 344 | 345 | 346 | // helper to call a given handler for all blocks in the chunk 347 | function callAllBlockHandlers(chunk, type) { 348 | var voxels = chunk.voxels 349 | var handlerLookup = chunk.noa.registry._blockHandlerLookup 350 | chunk._blockHandlerLocs.arr.forEach(([i, j, k]) => { 351 | var id = voxels.get(i, j, k) 352 | callBlockHandler(chunk, handlerLookup[id], type, i, j, k) 353 | }) 354 | } 355 | -------------------------------------------------------------------------------- /src/lib/container.js: -------------------------------------------------------------------------------- 1 | 2 | import { EventEmitter } from 'events' 3 | import { MicroGameShell } from 'micro-game-shell' 4 | 5 | 6 | 7 | 8 | 9 | /** 10 | * `noa.container` - manages the game's HTML container element, canvas, 11 | * fullscreen, pointerLock, and so on. 12 | * 13 | * This module wraps `micro-game-shell`, which does most of the implementation. 14 | * 15 | * **Events** 16 | * + `DOMready => ()` 17 | * Relays the browser DOMready event, after noa does some initialization 18 | * + `gainedPointerLock => ()` 19 | * Fires when the game container gains pointerlock. 20 | * + `lostPointerLock => ()` 21 | * Fires when the game container loses pointerlock. 22 | */ 23 | 24 | export class Container extends EventEmitter { 25 | 26 | /** @internal */ 27 | constructor(noa, opts) { 28 | super() 29 | opts = opts || {} 30 | 31 | /** 32 | * @internal 33 | * @type {import('../index').Engine} 34 | */ 35 | this.noa = noa 36 | 37 | /** The game's DOM element container */ 38 | var domEl = opts.domElement || null 39 | if (typeof domEl === 'string') { 40 | domEl = document.querySelector(domEl) 41 | } 42 | this.element = domEl || createContainerDiv() 43 | 44 | /** The `canvas` element that the game will draw into */ 45 | this.canvas = getOrCreateCanvas(this.element) 46 | doCanvasBugfix(noa, this.canvas) // grumble... 47 | 48 | 49 | /** Whether the browser supports pointerLock. @readonly */ 50 | this.supportsPointerLock = false 51 | 52 | /** Whether the user's pointer is within the game area. @readonly */ 53 | this.pointerInGame = false 54 | 55 | /** Whether the game is focused. @readonly */ 56 | this.isFocused = !!document.hasFocus() 57 | 58 | /** Gets the current state of pointerLock. @readonly */ 59 | this.hasPointerLock = false 60 | 61 | 62 | 63 | // shell manages tick/render rates, and pointerlock/fullscreen 64 | var pollTime = 10 65 | /** @internal */ 66 | this._shell = new MicroGameShell(this.element, pollTime) 67 | this._shell.tickRate = opts.tickRate 68 | this._shell.maxRenderRate = opts.maxRenderRate 69 | this._shell.stickyPointerLock = opts.stickyPointerLock 70 | this._shell.stickyFullscreen = opts.stickyFullscreen 71 | this._shell.maxTickTime = 50 72 | 73 | 74 | 75 | // core timing events 76 | this._shell.onTick = noa.tick.bind(noa) 77 | this._shell.onRender = noa.render.bind(noa) 78 | 79 | // shell listeners 80 | this._shell.onPointerLockChanged = (hasPL) => { 81 | this.hasPointerLock = hasPL 82 | this.emit((hasPL) ? 'gainedPointerLock' : 'lostPointerLock') 83 | // this works around a Firefox bug where no mouse-in event 84 | // gets issued after starting pointerlock 85 | if (hasPL) this.pointerInGame = true 86 | } 87 | 88 | // catch and relay domReady event 89 | this._shell.onInit = () => { 90 | this._shell.onResize = noa.rendering.resize.bind(noa.rendering) 91 | // listeners to track when game has focus / pointer 92 | detectPointerLock(this) 93 | this.element.addEventListener('mouseenter', () => { this.pointerInGame = true }) 94 | this.element.addEventListener('mouseleave', () => { this.pointerInGame = false }) 95 | window.addEventListener('focus', () => { this.isFocused = true }) 96 | window.addEventListener('blur', () => { this.isFocused = false }) 97 | // catch edge cases for initial states 98 | var onFirstMousedown = () => { 99 | this.pointerInGame = true 100 | this.isFocused = true 101 | this.element.removeEventListener('mousedown', onFirstMousedown) 102 | } 103 | this.element.addEventListener('mousedown', onFirstMousedown) 104 | // emit for engine core 105 | this.emit('DOMready') 106 | // done and remove listener 107 | this._shell.onInit = null 108 | } 109 | } 110 | 111 | 112 | /* 113 | * 114 | * 115 | * PUBLIC API 116 | * 117 | * 118 | */ 119 | 120 | /** @internal */ 121 | appendTo(htmlElement) { 122 | this.element.appendChild(htmlElement) 123 | } 124 | 125 | /** 126 | * Sets whether `noa` should try to acquire or release pointerLock 127 | */ 128 | setPointerLock(lock = false) { 129 | // not sure if this will work robustly 130 | this._shell.pointerLock = !!lock 131 | } 132 | } 133 | 134 | 135 | 136 | /* 137 | * 138 | * 139 | * INTERNALS 140 | * 141 | * 142 | */ 143 | 144 | 145 | function createContainerDiv() { 146 | // based on github.com/mikolalysenko/game-shell - makeDefaultContainer() 147 | var container = document.createElement("div") 148 | container.tabIndex = 1 149 | container.style.position = "fixed" 150 | container.style.left = "0px" 151 | container.style.right = "0px" 152 | container.style.top = "0px" 153 | container.style.bottom = "0px" 154 | container.style.height = "100%" 155 | container.style.overflow = "hidden" 156 | document.body.appendChild(container) 157 | document.body.style.overflow = "hidden" //Prevent bounce 158 | document.body.style.height = "100%" 159 | container.id = 'noa-container' 160 | return container 161 | } 162 | 163 | 164 | function getOrCreateCanvas(el) { 165 | // based on github.com/stackgl/gl-now - default canvas 166 | var canvas = el.querySelector('canvas') 167 | if (!canvas) { 168 | canvas = document.createElement('canvas') 169 | canvas.style.position = "absolute" 170 | canvas.style.left = "0px" 171 | canvas.style.top = "0px" 172 | canvas.style.height = "100%" 173 | canvas.style.width = "100%" 174 | canvas.id = 'noa-canvas' 175 | el.insertBefore(canvas, el.firstChild) 176 | } 177 | return canvas 178 | } 179 | 180 | 181 | // set up stuff to detect pointer lock support. 182 | // Needlessly complex because Chrome/Android claims to support but doesn't. 183 | // For now, just feature detect, but assume no support if a touch event occurs 184 | // TODO: see if this makes sense on hybrid touch/mouse devices 185 | function detectPointerLock(self) { 186 | var lockElementExists = 187 | ('pointerLockElement' in document) || 188 | ('mozPointerLockElement' in document) || 189 | ('webkitPointerLockElement' in document) 190 | if (lockElementExists) { 191 | self.supportsPointerLock = true 192 | var listener = function (e) { 193 | self.supportsPointerLock = false 194 | document.removeEventListener(e.type, listener) 195 | } 196 | document.addEventListener('touchmove', listener) 197 | } 198 | } 199 | 200 | 201 | /** 202 | * This works around a weird bug that seems to be chrome/mac only? 203 | * Without this, the page sometimes initializes with the canva 204 | * zoomed into its lower left quadrant. 205 | * Resizing the canvas fixes the issue (also: resizing page, changing zoom...) 206 | */ 207 | function doCanvasBugfix(noa, canvas) { 208 | var ct = 0 209 | var fixCanvas = () => { 210 | var w = canvas.width 211 | canvas.width = w + 1 212 | canvas.width = w 213 | if (ct++ > 10) noa.off('beforeRender', fixCanvas) 214 | } 215 | noa.on('beforeRender', fixCanvas) 216 | } 217 | -------------------------------------------------------------------------------- /src/lib/inputs.js: -------------------------------------------------------------------------------- 1 | 2 | import { GameInputs } from 'game-inputs' 3 | 4 | var defaultOptions = { 5 | preventDefaults: false, 6 | stopPropagation: false, 7 | allowContextMenu: false, 8 | } 9 | 10 | var defaultBindings = { 11 | "forward": ["KeyW", "ArrowUp"], 12 | "backward": ["KeyS", "ArrowDown"], 13 | "left": ["KeyA", "ArrowLeft"], 14 | "right": ["KeyD", "ArrowRight"], 15 | "fire": "Mouse1", 16 | "mid-fire": ["Mouse2", "KeyQ"], 17 | "alt-fire": ["Mouse3", "KeyE"], 18 | "jump": "Space", 19 | } 20 | 21 | /** 22 | * `noa.inputs` - Handles key and mouse input bindings. 23 | * 24 | * This module extends 25 | * [game-inputs](https://github.com/fenomas/game-inputs), 26 | * so turn on "Inherited" to see its APIs here, or view the base module 27 | * for full docs. 28 | * 29 | * This module uses the following default options (from the options 30 | * object passed to the {@link Engine}): 31 | * 32 | * ```js 33 | * defaultBindings: { 34 | * "forward": ["KeyW", "ArrowUp"], 35 | * "backward": ["KeyS", "ArrowDown"], 36 | * "left": ["KeyA", "ArrowLeft"], 37 | * "right": ["KeyD", "ArrowRight"], 38 | * "fire": "Mouse1", 39 | * "mid-fire": ["Mouse2", "KeyQ"], 40 | * "alt-fire": ["Mouse3", "KeyE"], 41 | * "jump": "Space", 42 | * } 43 | * ``` 44 | */ 45 | 46 | export class Inputs extends GameInputs { 47 | 48 | /** @internal */ 49 | constructor(noa, opts, element) { 50 | opts = Object.assign({}, defaultOptions, opts) 51 | super(element, opts) 52 | 53 | var b = opts.bindings || defaultBindings 54 | for (var name in b) { 55 | var keys = Array.isArray(b[name]) ? b[name] : [b[name]] 56 | this.bind(name, ...keys) 57 | } 58 | } 59 | } 60 | 61 | -------------------------------------------------------------------------------- /src/lib/objectMesher.js: -------------------------------------------------------------------------------- 1 | 2 | import { TransformNode } from '@babylonjs/core/Meshes/transformNode' 3 | import { makeProfileHook } from './util' 4 | import '@babylonjs/core/Meshes/thinInstanceMesh' 5 | 6 | 7 | var PROFILE = 0 8 | 9 | 10 | 11 | 12 | 13 | /* 14 | * 15 | * Object meshing 16 | * 17 | * Per-chunk handling of the creation/disposal of static meshes 18 | * associated with particular voxel IDs 19 | * 20 | * 21 | */ 22 | 23 | 24 | /** 25 | * @internal 26 | * @param {import('../index').Engine} noa 27 | */ 28 | export function ObjectMesher(noa) { 29 | 30 | // transform node for all instance meshes to be parented to 31 | this.rootNode = new TransformNode('objectMeshRoot', noa.rendering.scene) 32 | 33 | // tracking rebase amount inside matrix data 34 | var rebaseOffset = [0, 0, 0] 35 | 36 | // flag to trigger a rebuild after a chunk is disposed 37 | var rebuildNextTick = false 38 | 39 | // mock object to pass to customMesh handler, to get transforms 40 | var transformObj = new TransformNode('') 41 | 42 | // list of known base meshes 43 | this.allBaseMeshes = [] 44 | 45 | // internal storage of instance managers, keyed by ID 46 | // has check to dedupe by mesh, since babylon chokes on 47 | // separate sets of instances for the same mesh/clone/geometry 48 | var managers = {} 49 | var getManager = (id) => { 50 | if (managers[id]) return managers[id] 51 | var mesh = noa.registry._blockMeshLookup[id] 52 | for (var id2 in managers) { 53 | var prev = managers[id2].mesh 54 | if (prev === mesh || (prev.geometry === mesh.geometry)) { 55 | return managers[id] = managers[id2] 56 | } 57 | } 58 | this.allBaseMeshes.push(mesh) 59 | if (!mesh.metadata) mesh.metadata = {} 60 | mesh.metadata[objectMeshFlag] = true 61 | return managers[id] = new InstanceManager(noa, mesh) 62 | } 63 | var objectMeshFlag = 'noa_object_base_mesh' 64 | 65 | 66 | 67 | /* 68 | * 69 | * public API 70 | * 71 | */ 72 | 73 | 74 | // add any properties that will get used for meshing 75 | this.initChunk = function (chunk) { 76 | chunk._objectBlocks = {} 77 | } 78 | 79 | 80 | // called by world when an object block is set or cleared 81 | this.setObjectBlock = function (chunk, blockID, i, j, k) { 82 | var x = chunk.x + i 83 | var y = chunk.y + j 84 | var z = chunk.z + k 85 | var key = `${x}:${y}:${z}` 86 | 87 | var oldID = chunk._objectBlocks[key] || 0 88 | if (oldID === blockID) return // should be impossible 89 | if (oldID > 0) { 90 | var oldMgr = getManager(oldID) 91 | oldMgr.removeInstance(chunk, key) 92 | } 93 | 94 | if (blockID > 0) { 95 | // if there's a block event handler, call it with 96 | // a mock object so client can add transforms 97 | var handlers = noa.registry._blockHandlerLookup[blockID] 98 | var onCreate = handlers && handlers.onCustomMeshCreate 99 | if (onCreate) { 100 | transformObj.position.copyFromFloats(0.5, 0, 0.5) 101 | transformObj.scaling.setAll(1) 102 | transformObj.rotation.setAll(0) 103 | onCreate(transformObj, x, y, z) 104 | } 105 | var mgr = getManager(blockID) 106 | var xform = (onCreate) ? transformObj : null 107 | mgr.addInstance(chunk, key, i, j, k, xform, rebaseOffset) 108 | } 109 | 110 | if (oldID > 0 && !blockID) delete chunk._objectBlocks[key] 111 | if (blockID > 0) chunk._objectBlocks[key] = blockID 112 | } 113 | 114 | 115 | 116 | // called by world when it knows that objects have been updated 117 | this.buildObjectMeshes = function () { 118 | profile_hook('start') 119 | 120 | for (var id in managers) { 121 | var mgr = managers[id] 122 | mgr.updateMatrix() 123 | if (mgr.count === 0) mgr.dispose() 124 | if (mgr.disposed) delete managers[id] 125 | } 126 | 127 | profile_hook('rebuilt') 128 | profile_hook('end') 129 | } 130 | 131 | 132 | 133 | // called by world at end of chunk lifecycle 134 | this.disposeChunk = function (chunk) { 135 | for (var key in chunk._objectBlocks) { 136 | var id = chunk._objectBlocks[key] 137 | if (id > 0) { 138 | var mgr = getManager(id) 139 | mgr.removeInstance(chunk, key) 140 | } 141 | } 142 | chunk._objectBlocks = null 143 | 144 | // since some instance managers will have been updated 145 | rebuildNextTick = true 146 | } 147 | 148 | 149 | 150 | // tick handler catches case where objects are dirty due to disposal 151 | this.tick = function () { 152 | if (rebuildNextTick) { 153 | this.buildObjectMeshes() 154 | rebuildNextTick = false 155 | } 156 | } 157 | 158 | 159 | 160 | // world rebase handler 161 | this._rebaseOrigin = function (delta) { 162 | rebaseOffset[0] += delta[0] 163 | rebaseOffset[1] += delta[1] 164 | rebaseOffset[2] += delta[2] 165 | 166 | for (var id1 in managers) managers[id1].rebased = false 167 | for (var id2 in managers) { 168 | var mgr = managers[id2] 169 | if (mgr.rebased) continue 170 | for (var i = 0; i < mgr.count; i++) { 171 | var ix = i << 4 172 | mgr.buffer[ix + 12] -= delta[0] 173 | mgr.buffer[ix + 13] -= delta[1] 174 | mgr.buffer[ix + 14] -= delta[2] 175 | } 176 | mgr.rebased = true 177 | mgr.dirty = true 178 | } 179 | rebuildNextTick = true 180 | } 181 | 182 | } 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | /* 199 | * 200 | * 201 | * manager class for thin instances of a given object block ID 202 | * 203 | * 204 | */ 205 | 206 | /** @param {import('../index').Engine} noa*/ 207 | function InstanceManager(noa, mesh) { 208 | this.noa = noa 209 | this.mesh = mesh 210 | this.buffer = null 211 | this.capacity = 0 212 | this.count = 0 213 | this.dirty = false 214 | this.rebased = true 215 | this.disposed = false 216 | // dual struct to map keys (locations) to buffer locations, and back 217 | this.keyToIndex = {} 218 | this.locToKey = [] 219 | // prepare mesh for rendering 220 | this.mesh.position.setAll(0) 221 | this.mesh.parent = noa._objectMesher.rootNode 222 | this.noa.rendering.addMeshToScene(this.mesh, false) 223 | this.noa.emit('addingTerrainMesh', this.mesh) 224 | this.mesh.isPickable = false 225 | this.mesh.doNotSyncBoundingInfo = true 226 | this.mesh.alwaysSelectAsActiveMesh = true 227 | } 228 | 229 | 230 | 231 | InstanceManager.prototype.dispose = function () { 232 | if (this.disposed) return 233 | this.mesh.thinInstanceCount = 0 234 | this.setCapacity(0) 235 | this.noa.emit('removingTerrainMesh', this.mesh) 236 | this.noa.rendering.setMeshVisibility(this.mesh, false) 237 | this.mesh = null 238 | this.keyToIndex = null 239 | this.locToKey = null 240 | this.disposed = true 241 | } 242 | 243 | 244 | InstanceManager.prototype.addInstance = function (chunk, key, i, j, k, transform, rebaseVec) { 245 | maybeExpandBuffer(this) 246 | var ix = this.count << 4 247 | this.locToKey[this.count] = key 248 | this.keyToIndex[key] = ix 249 | if (transform) { 250 | transform.position.x += (chunk.x - rebaseVec[0]) + i 251 | transform.position.y += (chunk.y - rebaseVec[1]) + j 252 | transform.position.z += (chunk.z - rebaseVec[2]) + k 253 | transform.computeWorldMatrix(true) 254 | var xformArr = transform._localMatrix._m 255 | copyMatrixData(xformArr, 0, this.buffer, ix) 256 | } else { 257 | var matArray = tempMatrixArray 258 | matArray[12] = (chunk.x - rebaseVec[0]) + i + 0.5 259 | matArray[13] = (chunk.y - rebaseVec[1]) + j 260 | matArray[14] = (chunk.z - rebaseVec[2]) + k + 0.5 261 | copyMatrixData(matArray, 0, this.buffer, ix) 262 | } 263 | this.count++ 264 | this.dirty = true 265 | } 266 | 267 | 268 | InstanceManager.prototype.removeInstance = function (chunk, key) { 269 | var remIndex = this.keyToIndex[key] 270 | if (!(remIndex >= 0)) throw 'tried to remove object instance not in storage' 271 | delete this.keyToIndex[key] 272 | var remLoc = remIndex >> 4 273 | // copy tail instance's data to location of one we're removing 274 | var tailLoc = this.count - 1 275 | if (remLoc !== tailLoc) { 276 | var tailIndex = tailLoc << 4 277 | copyMatrixData(this.buffer, tailIndex, this.buffer, remIndex) 278 | // update key/location structs 279 | var tailKey = this.locToKey[tailLoc] 280 | this.keyToIndex[tailKey] = remIndex 281 | this.locToKey[remLoc] = tailKey 282 | } 283 | this.count-- 284 | this.dirty = true 285 | maybeContractBuffer(this) 286 | } 287 | 288 | 289 | InstanceManager.prototype.updateMatrix = function () { 290 | if (!this.dirty) return 291 | this.mesh.thinInstanceCount = this.count 292 | this.mesh.thinInstanceBufferUpdated('matrix') 293 | this.mesh.isVisible = (this.count > 0) 294 | this.dirty = false 295 | } 296 | 297 | 298 | 299 | InstanceManager.prototype.setCapacity = function (size = 4) { 300 | this.capacity = size 301 | if (size === 0) { 302 | this.buffer = null 303 | } else { 304 | var newBuff = new Float32Array(this.capacity * 16) 305 | if (this.buffer) { 306 | var len = Math.min(this.buffer.length, newBuff.length) 307 | for (var i = 0; i < len; i++) newBuff[i] = this.buffer[i] 308 | } 309 | this.buffer = newBuff 310 | } 311 | this.mesh.thinInstanceSetBuffer('matrix', this.buffer) 312 | this.updateMatrix() 313 | } 314 | 315 | 316 | function maybeExpandBuffer(mgr) { 317 | if (mgr.count < mgr.capacity) return 318 | var size = Math.max(8, mgr.capacity * 2) 319 | mgr.setCapacity(size) 320 | } 321 | 322 | function maybeContractBuffer(mgr) { 323 | if (mgr.count > mgr.capacity * 0.4) return 324 | if (mgr.capacity < 100) return 325 | mgr.setCapacity(Math.round(mgr.capacity / 2)) 326 | mgr.locToKey.length = Math.min(mgr.locToKey.length, mgr.capacity) 327 | } 328 | 329 | 330 | 331 | // helpers 332 | 333 | var tempMatrixArray = [ 334 | 1.0, 0.0, 0.0, 0.0, 335 | 0.0, 1.0, 0.0, 0.0, 336 | 0.0, 0.0, 1.0, 0.0, 337 | 0.0, 0.0, 0.0, 1.0, 338 | ] 339 | 340 | function copyMatrixData(src, srcOff, dest, destOff) { 341 | for (var i = 0; i < 16; i++) dest[destOff + i] = src[srcOff + i] 342 | } 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | var profile_hook = (PROFILE) ? 356 | makeProfileHook(PROFILE, 'Object meshing') : () => { } 357 | -------------------------------------------------------------------------------- /src/lib/physics.js: -------------------------------------------------------------------------------- 1 | 2 | import { Physics as VoxelPhysics } from 'voxel-physics-engine' 3 | 4 | 5 | 6 | 7 | var defaultOptions = { 8 | gravity: [0, -10, 0], 9 | airDrag: 0.1, 10 | } 11 | 12 | /** 13 | * `noa.physics` - Wrapper module for the physics engine. 14 | * 15 | * This module extends 16 | * [voxel-physics-engine](https://github.com/fenomas/voxel-physics-engine), 17 | * so turn on "Inherited" to see its APIs here, or view the base module 18 | * for full docs. 19 | * 20 | * This module uses the following default options (from the options 21 | * object passed to the {@link Engine}): 22 | * 23 | * ```js 24 | * { 25 | * gravity: [0, -10, 0], 26 | * airDrag: 0.1, 27 | * fluidDrag: 0.4, 28 | * fluidDensity: 2.0, 29 | * minBounceImpulse: .5, // cutoff for a bounce to occur 30 | * } 31 | * ``` 32 | */ 33 | 34 | export class Physics extends VoxelPhysics { 35 | 36 | /** 37 | * @internal 38 | * @param {import('../index').Engine} noa 39 | */ 40 | constructor(noa, opts) { 41 | opts = Object.assign({}, defaultOptions, opts) 42 | var world = noa.world 43 | var solidLookup = noa.registry._solidityLookup 44 | var fluidLookup = noa.registry._fluidityLookup 45 | 46 | // physics engine runs in offset coords, so voxel getters need to match 47 | var offset = noa.worldOriginOffset 48 | 49 | var blockGetter = (x, y, z) => { 50 | var id = world.getBlockID(x + offset[0], y + offset[1], z + offset[2]) 51 | return solidLookup[id] 52 | } 53 | var isFluidGetter = (x, y, z) => { 54 | var id = world.getBlockID(x + offset[0], y + offset[1], z + offset[2]) 55 | return fluidLookup[id] 56 | } 57 | 58 | super(opts, blockGetter, isFluidGetter) 59 | } 60 | 61 | } 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /src/lib/registry.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | var defaults = { 4 | texturePath: '' 5 | } 6 | 7 | // voxel ID now uses the whole Uint16Array element 8 | var MAX_BLOCK_ID = (1 << 16) - 1 9 | 10 | 11 | 12 | 13 | 14 | /** 15 | * `noa.registry` - Where you register your voxel types, 16 | * materials, properties, and events. 17 | * 18 | * This module uses the following default options (from the options 19 | * object passed to the {@link Engine}): 20 | * 21 | * ```js 22 | * var defaults = { 23 | * texturePath: '' 24 | * } 25 | * ``` 26 | */ 27 | 28 | export class Registry { 29 | 30 | 31 | /** 32 | * @internal 33 | * @param {import('../index').Engine} noa 34 | */ 35 | constructor(noa, opts) { 36 | opts = Object.assign({}, defaults, opts) 37 | /** @internal */ 38 | this.noa = noa 39 | 40 | /** @internal */ 41 | this._texturePath = opts.texturePath 42 | 43 | /** Maps block face material names to matIDs 44 | * @type {Object.} */ 45 | var matIDs = {} 46 | 47 | // lookup arrays for block props and flags - all keyed by blockID 48 | // fill in first value for the air block with id=0 49 | var blockSolidity = [false] 50 | var blockOpacity = [false] 51 | var blockIsFluid = [false] 52 | var blockIsObject = [false] 53 | var blockProps = [null] // less-often accessed properties 54 | var blockMeshes = [null] // custom mesh objects 55 | var blockHandlers = [null] // block event handlers 56 | var blockIsPlain = [false] // true if voxel is "boring" - solid/opaque, no special props 57 | 58 | // this one is keyed by `blockID*6 + faceNumber` 59 | var blockMats = [0, 0, 0, 0, 0, 0] 60 | 61 | // and these are keyed by material id 62 | var matColorLookup = [null] 63 | var matAtlasIndexLookup = [-1] 64 | 65 | /** 66 | * Lookup array of block face material properties - keyed by matID (not blockID) 67 | * @typedef MatDef 68 | * @prop {number[]} color 69 | * @prop {number} alpha 70 | * @prop {string} texture 71 | * @prop {boolean} texHasAlpha 72 | * @prop {number} atlasIndex 73 | * @prop {*} renderMat 74 | */ 75 | /** @type {MatDef[]} */ 76 | var matDefs = [] 77 | 78 | 79 | /* 80 | * 81 | * Block registration methods 82 | * 83 | */ 84 | 85 | 86 | 87 | /** 88 | * Register (by integer ID) a block type and its parameters. 89 | * `id` param: integer, currently 1..65535. Generally you should 90 | * specify sequential values for blocks, without gaps, but this 91 | * isn't technically necessary. 92 | * 93 | * @param {number} id - sequential integer ID (from 1) 94 | * @param {Partial} [options] 95 | * @returns the `id` value specified 96 | */ 97 | this.registerBlock = function (id = 1, options = null) { 98 | var defaults = new BlockOptions(options && options.fluid) 99 | var opts = Object.assign({}, defaults, options || {}) 100 | 101 | // console.log('register block: ', id, opts) 102 | if (id < 1 || id > MAX_BLOCK_ID) throw 'Block id out of range: ' + id 103 | 104 | // if block ID is greater than current highest ID, 105 | // register fake blocks to avoid holes in lookup arrays 106 | while (id > blockSolidity.length) { 107 | this.registerBlock(blockSolidity.length, {}) 108 | } 109 | 110 | // flags default to solid, opaque, nonfluid 111 | blockSolidity[id] = !!opts.solid 112 | blockOpacity[id] = !!opts.opaque 113 | blockIsFluid[id] = !!opts.fluid 114 | 115 | // store any custom mesh 116 | blockIsObject[id] = !!opts.blockMesh 117 | blockMeshes[id] = opts.blockMesh || null 118 | 119 | // parse out material parameter 120 | // always store 6 material IDs per blockID, so material lookup is monomorphic 121 | var mat = opts.material || null 122 | var mats 123 | if (!mat) { 124 | mats = [null, null, null, null, null, null] 125 | } else if (typeof mat == 'string') { 126 | mats = [mat, mat, mat, mat, mat, mat] 127 | } else if (mat.length && mat.length == 2) { 128 | // interpret as [top/bottom, sides] 129 | mats = [mat[1], mat[1], mat[0], mat[0], mat[1], mat[1]] 130 | } else if (mat.length && mat.length == 3) { 131 | // interpret as [top, bottom, sides] 132 | mats = [mat[2], mat[2], mat[0], mat[1], mat[2], mat[2]] 133 | } else if (mat.length && mat.length == 6) { 134 | // interpret as [-x, +x, -y, +y, -z, +z] 135 | mats = mat 136 | } else throw 'Invalid material parameter: ' + mat 137 | 138 | // argument is material name, but store as material id, allocating one if needed 139 | for (var i = 0; i < 6; ++i) { 140 | blockMats[id * 6 + i] = getMaterialId(this, matIDs, mats[i], true) 141 | } 142 | 143 | // props data object - currently only used for fluid properties 144 | blockProps[id] = {} 145 | 146 | // if block is fluid, initialize properties if needed 147 | if (blockIsFluid[id]) { 148 | blockProps[id].fluidDensity = opts.fluidDensity 149 | blockProps[id].viscosity = opts.viscosity 150 | } 151 | 152 | // event callbacks 153 | var hasHandler = opts.onLoad || opts.onUnload || opts.onSet || opts.onUnset || opts.onCustomMeshCreate 154 | blockHandlers[id] = (hasHandler) ? new BlockCallbackHolder(opts) : null 155 | 156 | // special lookup for "plain"-ness 157 | // plain means solid, opaque, not fluid, no mesh or events 158 | var isPlain = blockSolidity[id] && blockOpacity[id] 159 | && !hasHandler && !blockIsFluid[id] && !blockIsObject[id] 160 | blockIsPlain[id] = isPlain 161 | 162 | return id 163 | } 164 | 165 | 166 | 167 | 168 | /** 169 | * Register (by name) a material and its parameters. 170 | * 171 | * @param {string} name of this material 172 | * @param {Partial} [options] 173 | */ 174 | 175 | this.registerMaterial = function (name = '?', options = null) { 176 | // catch calls to earlier signature 177 | if (Array.isArray(options)) { 178 | throw 'This API changed signatures in v0.33, please use: `noa.registry.registerMaterial("name", optionsObj)`' 179 | } 180 | 181 | var opts = Object.assign(new MaterialOptions(), options || {}) 182 | var matID = matIDs[name] || matDefs.length 183 | matIDs[name] = matID 184 | 185 | var texURL = opts.textureURL ? this._texturePath + opts.textureURL : '' 186 | var alpha = 1.0 187 | var color = opts.color || [1.0, 1.0, 1.0] 188 | if (color.length === 4) alpha = color.pop() 189 | if (texURL) color = null 190 | 191 | // populate lookup arrays for terrain meshing 192 | matColorLookup[matID] = color 193 | matAtlasIndexLookup[matID] = opts.atlasIndex 194 | 195 | matDefs[matID] = { 196 | color, 197 | alpha, 198 | texture: texURL, 199 | texHasAlpha: !!opts.texHasAlpha, 200 | atlasIndex: opts.atlasIndex, 201 | renderMat: opts.renderMaterial, 202 | } 203 | return matID 204 | } 205 | 206 | 207 | 208 | /* 209 | * quick accessors for querying block ID stuff 210 | */ 211 | 212 | /** 213 | * block solidity (as in physics) 214 | * @param id 215 | */ 216 | this.getBlockSolidity = function (id) { 217 | return blockSolidity[id] 218 | } 219 | 220 | /** 221 | * block opacity - whether it obscures the whole voxel (dirt) or 222 | * can be partially seen through (like a fencepost, etc) 223 | * @param id 224 | */ 225 | this.getBlockOpacity = function (id) { 226 | return blockOpacity[id] 227 | } 228 | 229 | /** 230 | * block is fluid or not 231 | * @param id 232 | */ 233 | this.getBlockFluidity = function (id) { 234 | return blockIsFluid[id] 235 | } 236 | 237 | /** 238 | * Get block property object passed in at registration 239 | * @param id 240 | */ 241 | this.getBlockProps = function (id) { 242 | return blockProps[id] 243 | } 244 | 245 | // look up a block ID's face material 246 | // dir is a value 0..5: [ +x, -x, +y, -y, +z, -z ] 247 | this.getBlockFaceMaterial = function (blockId, dir) { 248 | return blockMats[blockId * 6 + dir] 249 | } 250 | 251 | 252 | /** 253 | * General lookup for all properties of a block material 254 | * @param {number} matID 255 | * @returns {MatDef} 256 | */ 257 | this.getMaterialData = function (matID) { 258 | return matDefs[matID] 259 | } 260 | 261 | 262 | /** 263 | * Given a texture URL, does any material using that 264 | * texture need alpha? 265 | * @internal 266 | * @returns {boolean} 267 | */ 268 | this._textureNeedsAlpha = function (tex = '') { 269 | return matDefs.some(def => { 270 | if (def.texture !== tex) return false 271 | return def.texHasAlpha 272 | }) 273 | } 274 | 275 | 276 | 277 | 278 | 279 | /* 280 | * 281 | * Meant for internal use within the engine 282 | * 283 | */ 284 | 285 | 286 | // internal access to lookup arrays 287 | /** @internal */ 288 | this._solidityLookup = blockSolidity 289 | /** @internal */ 290 | this._opacityLookup = blockOpacity 291 | /** @internal */ 292 | this._fluidityLookup = blockIsFluid 293 | /** @internal */ 294 | this._objectLookup = blockIsObject 295 | /** @internal */ 296 | this._blockMeshLookup = blockMeshes 297 | /** @internal */ 298 | this._blockHandlerLookup = blockHandlers 299 | /** @internal */ 300 | this._blockIsPlainLookup = blockIsPlain 301 | /** @internal */ 302 | this._materialColorLookup = matColorLookup 303 | /** @internal */ 304 | this._matAtlasIndexLookup = matAtlasIndexLookup 305 | 306 | 307 | 308 | /* 309 | * 310 | * default initialization 311 | * 312 | */ 313 | 314 | // add a default material and set ID=1 to it 315 | // this is safe since registering new block data overwrites the old 316 | this.registerMaterial('dirt', { color: [0.4, 0.3, 0] }) 317 | this.registerBlock(1, { material: 'dirt' }) 318 | 319 | } 320 | 321 | } 322 | 323 | /* 324 | * 325 | * helpers 326 | * 327 | */ 328 | 329 | 330 | 331 | // look up material ID given its name 332 | // if lazy is set, pre-register the name and return an ID 333 | function getMaterialId(reg, matIDs, name, lazyInit) { 334 | if (!name) return 0 335 | var id = matIDs[name] 336 | if (id === undefined && lazyInit) id = reg.registerMaterial(name) 337 | return id 338 | } 339 | 340 | 341 | 342 | // data class for holding block callback references 343 | function BlockCallbackHolder(opts) { 344 | this.onLoad = opts.onLoad || null 345 | this.onUnload = opts.onUnload || null 346 | this.onSet = opts.onSet || null 347 | this.onUnset = opts.onUnset || null 348 | this.onCustomMeshCreate = opts.onCustomMeshCreate || null 349 | } 350 | 351 | 352 | 353 | 354 | /** 355 | * Default options when registering a block type 356 | */ 357 | function BlockOptions(isFluid = false) { 358 | /** Solidity for physics purposes */ 359 | this.solid = (isFluid) ? false : true 360 | /** Whether the block fully obscures neighboring blocks */ 361 | this.opaque = (isFluid) ? false : true 362 | /** whether a nonsolid block is a fluid (buoyant, viscous..) */ 363 | this.fluid = false 364 | /** The block material(s) for this voxel's faces. May be: 365 | * * one (String) material name 366 | * * array of 2 names: [top/bottom, sides] 367 | * * array of 3 names: [top, bottom, sides] 368 | * * array of 6 names: [-x, +x, -y, +y, -z, +z] 369 | * @type {string|string[]} 370 | */ 371 | this.material = null 372 | /** Specifies a custom mesh for this voxel, instead of terrain */ 373 | this.blockMesh = null 374 | /** Fluid parameter for fluid blocks */ 375 | this.fluidDensity = 1.0 376 | /** Fluid parameter for fluid blocks */ 377 | this.viscosity = 0.5 378 | /** @type {(x:number, y:number, z:number) => void} */ 379 | this.onLoad = null 380 | /** @type {(x:number, y:number, z:number) => void} */ 381 | this.onUnload = null 382 | /** @type {(x:number, y:number, z:number) => void} */ 383 | this.onSet = null 384 | /** @type {(x:number, y:number, z:number) => void} */ 385 | this.onUnset = null 386 | /** @type {(mesh:TransformNode, x:number, y:number, z:number) => void} */ 387 | this.onCustomMeshCreate = null 388 | } 389 | 390 | /** @typedef {import('@babylonjs/core/Meshes').TransformNode} TransformNode */ 391 | 392 | 393 | /** 394 | * Default options when registering a Block Material 395 | */ 396 | function MaterialOptions() { 397 | /** An array of 0..1 floats, either [R,G,B] or [R,G,B,A] 398 | * @type {number[]} 399 | */ 400 | this.color = null 401 | /** Filename of texture image, if any 402 | * @type {string} 403 | */ 404 | this.textureURL = null 405 | /** Whether the texture image has alpha */ 406 | this.texHasAlpha = false 407 | /** Index into a (vertical strip) texture atlas, if applicable */ 408 | this.atlasIndex = -1 409 | /** 410 | * An optional Babylon.js `Material`. If specified, terrain for this voxel 411 | * will be rendered with the supplied material (this can impact performance). 412 | */ 413 | this.renderMaterial = null 414 | } 415 | -------------------------------------------------------------------------------- /src/lib/sceneOctreeManager.js: -------------------------------------------------------------------------------- 1 | 2 | import { Vector3 } from '@babylonjs/core/Maths/math.vector' 3 | import { Octree } from '@babylonjs/core/Culling/Octrees/octree' 4 | import { OctreeBlock } from '@babylonjs/core/Culling/Octrees/octreeBlock' 5 | import { OctreeSceneComponent } from '@babylonjs/core/Culling/Octrees/octreeSceneComponent' 6 | 7 | import { locationHasher, removeUnorderedListItem } from './util' 8 | 9 | 10 | /* 11 | * 12 | * 13 | * 14 | * simple class to manage scene octree and octreeBlocks 15 | * 16 | * 17 | * 18 | */ 19 | 20 | /** @internal */ 21 | export class SceneOctreeManager { 22 | 23 | /** @internal */ 24 | constructor(rendering, blockSize) { 25 | var scene = rendering.scene 26 | scene._addComponent(new OctreeSceneComponent(scene)) 27 | 28 | // mesh metadata flags 29 | var octreeBlock = 'noa_octree_block' 30 | var inDynamicList = 'noa_in_dynamic_list' 31 | var inOctreeBlock = 'noa_in_octree_block' 32 | 33 | // the root octree object 34 | var octree = new Octree(NOP) 35 | scene._selectionOctree = octree 36 | octree.blocks = [] 37 | var octBlocksHash = {} 38 | 39 | 40 | /* 41 | * 42 | * public API 43 | * 44 | */ 45 | 46 | this.rebase = (offset) => { recurseRebaseBlocks(octree, offset) } 47 | 48 | this.addMesh = (mesh, isStatic, pos, chunk) => { 49 | if (!mesh.metadata) mesh.metadata = {} 50 | 51 | // dynamic content is just rendered from a list on the octree 52 | if (!isStatic) { 53 | if (mesh.metadata[inDynamicList]) return 54 | octree.dynamicContent.push(mesh) 55 | mesh.metadata[inDynamicList] = true 56 | return 57 | } 58 | 59 | // octreeBlock-space integer coords of mesh position, and hashed key 60 | var ci = Math.floor(pos[0] / bs) 61 | var cj = Math.floor(pos[1] / bs) 62 | var ck = Math.floor(pos[2] / bs) 63 | var mapKey = locationHasher(ci, cj, ck) 64 | 65 | // get or create octreeBlock 66 | var block = octBlocksHash[mapKey] 67 | if (!block) { 68 | // lower corner of new octree block position, in global/local 69 | var gloc = [ci * bs, cj * bs, ck * bs] 70 | var loc = [0, 0, 0] 71 | rendering.noa.globalToLocal(gloc, null, loc) 72 | // make the new octree block and store it 73 | block = makeOctreeBlock(loc, bs) 74 | octree.blocks.push(block) 75 | octBlocksHash[mapKey] = block 76 | block._noaMapKey = mapKey 77 | } 78 | 79 | // do the actual adding logic 80 | block.entries.push(mesh) 81 | mesh.metadata[octreeBlock] = block 82 | mesh.metadata[inOctreeBlock] = true 83 | 84 | // rely on octrees for selection, skipping bounds checks 85 | mesh.alwaysSelectAsActiveMesh = true 86 | } 87 | 88 | 89 | 90 | this.removeMesh = (mesh) => { 91 | if (!mesh.metadata) return 92 | 93 | if (mesh.metadata[inDynamicList]) { 94 | removeUnorderedListItem(octree.dynamicContent, mesh) 95 | mesh.metadata[inDynamicList] = false 96 | } 97 | if (mesh.metadata[inOctreeBlock]) { 98 | var block = mesh.metadata[octreeBlock] 99 | if (block && block.entries) { 100 | removeUnorderedListItem(block.entries, mesh) 101 | if (block.entries.length === 0) { 102 | delete octBlocksHash[block._noaMapKey] 103 | removeUnorderedListItem(octree.blocks, block) 104 | } 105 | } 106 | mesh.metadata[octreeBlock] = null 107 | mesh.metadata[inOctreeBlock] = false 108 | } 109 | } 110 | 111 | 112 | 113 | // experimental helper 114 | this.setMeshVisibility = (mesh, visible = false) => { 115 | if (mesh.metadata[octreeBlock]) { 116 | // mesh is static 117 | if (mesh.metadata[inOctreeBlock] === visible) return 118 | var block = mesh.metadata[octreeBlock] 119 | if (block && block.entries) { 120 | if (visible) { 121 | block.entries.push(mesh) 122 | } else { 123 | removeUnorderedListItem(block.entries, mesh) 124 | } 125 | } 126 | mesh.metadata[inOctreeBlock] = visible 127 | } else { 128 | // mesh is dynamic 129 | if (mesh.metadata[inDynamicList] === visible) return 130 | if (visible) { 131 | octree.dynamicContent.push(mesh) 132 | } else { 133 | removeUnorderedListItem(octree.dynamicContent, mesh) 134 | } 135 | mesh.metadata[inDynamicList] = visible 136 | } 137 | } 138 | 139 | /* 140 | * 141 | * internals 142 | * 143 | */ 144 | 145 | var NOP = () => { } 146 | var bs = blockSize * rendering.noa.world._chunkSize 147 | 148 | var recurseRebaseBlocks = (parent, offset) => { 149 | parent.blocks.forEach(child => { 150 | child.minPoint.subtractInPlace(offset) 151 | child.maxPoint.subtractInPlace(offset) 152 | child._boundingVectors.forEach(v => v.subtractInPlace(offset)) 153 | if (child.blocks) recurseRebaseBlocks(child, offset) 154 | }) 155 | } 156 | 157 | var makeOctreeBlock = (minPt, size) => { 158 | var min = new Vector3(minPt[0], minPt[1], minPt[2]) 159 | var max = new Vector3(minPt[0] + size, minPt[1] + size, minPt[2] + size) 160 | return new OctreeBlock(min, max, undefined, undefined, undefined, NOP) 161 | } 162 | 163 | } 164 | 165 | } 166 | -------------------------------------------------------------------------------- /src/lib/shims.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * This works around some old node-style code in a 4 | * dependency of box-intersect. 5 | */ 6 | if (window && !window['global']) { 7 | window['global'] = window.globalThis || {} 8 | } 9 | 10 | -------------------------------------------------------------------------------- /src/lib/terrainMaterials.js: -------------------------------------------------------------------------------- 1 | 2 | import { Engine } from '@babylonjs/core/Engines/engine' 3 | import { Texture } from '@babylonjs/core/Materials/Textures/texture' 4 | import { MaterialPluginBase } from '@babylonjs/core/Materials/materialPluginBase' 5 | import { RawTexture2DArray } from '@babylonjs/core/Materials/Textures/rawTexture2DArray' 6 | 7 | /** 8 | * 9 | * 10 | * This module creates and manages Materials for terrain meshes. 11 | * It tells the terrain mesher which block face materials can share 12 | * the same material (and should thus be joined into a single mesh), 13 | * and also creates the materials when needed. 14 | * 15 | * @internal 16 | */ 17 | 18 | export class TerrainMatManager { 19 | 20 | /** @param {import('../index').Engine} noa */ 21 | constructor(noa) { 22 | // make a baseline default material for untextured terrain with no alpha 23 | this._defaultMat = noa.rendering.makeStandardMaterial('base-terrain') 24 | this._defaultMat.freeze() 25 | 26 | this.allMaterials = [this._defaultMat] 27 | 28 | // internals 29 | this.noa = noa 30 | this._idCounter = 1000 31 | this._blockMatIDtoTerrainID = {} 32 | this._terrainIDtoMatObject = {} 33 | this._texURLtoTerrainID = {} 34 | this._renderMatToTerrainID = new Map() 35 | } 36 | 37 | 38 | 39 | /** 40 | * Maps a given `matID` (from noa.registry) to a unique ID of which 41 | * terrain material can be used for that block material. 42 | * This lets the terrain mesher map which blocks can be merged into 43 | * the same meshes. 44 | * Internally, this accessor also creates the material for each 45 | * terrainMatID as they are first encountered. 46 | */ 47 | 48 | getTerrainMatId(blockMatID) { 49 | // fast case where matID has been seen before 50 | if (blockMatID in this._blockMatIDtoTerrainID) { 51 | return this._blockMatIDtoTerrainID[blockMatID] 52 | } 53 | // decide a unique terrainID for this block material 54 | var terrID = decideTerrainMatID(this, blockMatID) 55 | // create a mat object for it, if needed 56 | if (!(terrID in this._terrainIDtoMatObject)) { 57 | var mat = createTerrainMat(this, blockMatID) 58 | this.allMaterials.push(mat) 59 | this._terrainIDtoMatObject[terrID] = mat 60 | } 61 | // cache results and done 62 | this._blockMatIDtoTerrainID[blockMatID] = terrID 63 | return terrID 64 | } 65 | 66 | 67 | /** 68 | * Get a Babylon Material object, given a terrainMatID (gotten from this module) 69 | */ 70 | getMaterial(terrainMatID = 1) { 71 | return this._terrainIDtoMatObject[terrainMatID] 72 | } 73 | 74 | 75 | 76 | 77 | 78 | } 79 | 80 | 81 | 82 | 83 | /** 84 | * 85 | * 86 | * Implementations of creating/disambiguating terrain Materials 87 | * 88 | * 89 | */ 90 | 91 | /** 92 | * Decide a unique terrainID, based on block material ID properties 93 | * @param {TerrainMatManager} self 94 | */ 95 | function decideTerrainMatID(self, blockMatID = 0) { 96 | var matInfo = self.noa.registry.getMaterialData(blockMatID) 97 | 98 | // custom render materials get one unique terrainID per material 99 | if (matInfo.renderMat) { 100 | var mat = matInfo.renderMat 101 | if (!self._renderMatToTerrainID.has(mat)) { 102 | self._renderMatToTerrainID.set(mat, self._idCounter++) 103 | } 104 | return self._renderMatToTerrainID.get(mat) 105 | } 106 | 107 | // ditto for textures, unique URL 108 | if (matInfo.texture) { 109 | var url = matInfo.texture 110 | if (!(url in self._texURLtoTerrainID)) { 111 | self._texURLtoTerrainID[url] = self._idCounter++ 112 | } 113 | return self._texURLtoTerrainID[url] 114 | } 115 | 116 | // plain color materials with an alpha value are unique by alpha 117 | var alpha = matInfo.alpha 118 | if (alpha > 0 && alpha < 1) return 10 + Math.round(alpha * 100) 119 | 120 | // the only remaining case is the baseline, which always reuses one fixed ID 121 | return 1 122 | } 123 | 124 | 125 | /** 126 | * Create (choose) a material for a given set of block material properties 127 | * @param {TerrainMatManager} self 128 | */ 129 | function createTerrainMat(self, blockMatID = 0) { 130 | var matInfo = self.noa.registry.getMaterialData(blockMatID) 131 | 132 | // custom render mats are just reused 133 | if (matInfo.renderMat) return matInfo.renderMat 134 | 135 | // if no texture: use a basic flat material, possibly with alpha 136 | if (!matInfo.texture) { 137 | var needsAlpha = (matInfo.alpha > 0 && matInfo.alpha < 1) 138 | if (!needsAlpha) return self._defaultMat 139 | var matName = 'terrain-alpha-' + blockMatID 140 | var plainMat = self.noa.rendering.makeStandardMaterial(matName) 141 | plainMat.alpha = matInfo.alpha 142 | plainMat.freeze() 143 | return plainMat 144 | } 145 | 146 | // remaining case is a new material with a diffuse texture 147 | var scene = self.noa.rendering.getScene() 148 | var mat = self.noa.rendering.makeStandardMaterial('terrain-textured-' + blockMatID) 149 | var texURL = matInfo.texture 150 | var sampling = Texture.NEAREST_SAMPLINGMODE 151 | var tex = new Texture(texURL, scene, true, false, sampling) 152 | if (matInfo.texHasAlpha) tex.hasAlpha = true 153 | mat.diffuseTexture = tex 154 | 155 | // it texture is an atlas, apply material plugin 156 | // and check whether any material for the atlas needs alpha 157 | if (matInfo.atlasIndex >= 0) { 158 | new TerrainMaterialPlugin(mat, tex) 159 | if (self.noa.registry._textureNeedsAlpha(matInfo.texture)) { 160 | tex.hasAlpha = true 161 | } 162 | } 163 | 164 | mat.freeze() 165 | return mat 166 | } 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | /** 179 | * 180 | * Babylon material plugin - twiddles the defines/shaders/etc so that 181 | * a standard material can use textures from a 2D texture atlas. 182 | * 183 | */ 184 | 185 | class TerrainMaterialPlugin extends MaterialPluginBase { 186 | constructor(material, texture) { 187 | var priority = 200 188 | var defines = { 'NOA_TWOD_ARRAY_TEXTURE': false } 189 | super(material, 'TestPlugin', priority, defines) 190 | this._enable(true) 191 | this._atlasTextureArray = null 192 | 193 | texture.onLoadObservable.add((tex) => { 194 | this.setTextureArrayData(tex) 195 | }) 196 | } 197 | 198 | setTextureArrayData(texture) { 199 | var { width, height } = texture.getSize() 200 | var numLayers = Math.round(height / width) 201 | height = width 202 | var data = texture._readPixelsSync() 203 | 204 | var format = Engine.TEXTUREFORMAT_RGBA 205 | var genMipMaps = true 206 | var invertY = false 207 | var mode = Texture.NEAREST_SAMPLINGMODE 208 | var scene = texture.getScene() 209 | 210 | this._atlasTextureArray = new RawTexture2DArray( 211 | data, width, height, numLayers, 212 | format, scene, genMipMaps, invertY, mode, 213 | ) 214 | } 215 | 216 | prepareDefines(defines, scene, mesh) { 217 | defines['NOA_TWOD_ARRAY_TEXTURE'] = true 218 | } 219 | 220 | getClassName() { 221 | return 'TerrainMaterialPluginName' 222 | } 223 | 224 | getSamplers(samplers) { 225 | samplers.push('atlasTexture') 226 | } 227 | 228 | getAttributes(attributes) { 229 | attributes.push('texAtlasIndices') 230 | } 231 | 232 | getUniforms() { 233 | return { ubo: [] } 234 | } 235 | 236 | bindForSubMesh(uniformBuffer, scene, engine, subMesh) { 237 | if (this._atlasTextureArray) { 238 | uniformBuffer.setTexture('atlasTexture', this._atlasTextureArray) 239 | } 240 | } 241 | 242 | getCustomCode(shaderType) { 243 | if (shaderType === 'vertex') return { 244 | 'CUSTOM_VERTEX_MAIN_BEGIN': ` 245 | texAtlasIndex = texAtlasIndices; 246 | `, 247 | 'CUSTOM_VERTEX_DEFINITIONS': ` 248 | uniform highp sampler2DArray atlasTexture; 249 | attribute float texAtlasIndices; 250 | varying float texAtlasIndex; 251 | `, 252 | } 253 | if (shaderType === 'fragment') return { 254 | '!baseColor\\=texture2D\\(diffuseSampler,vDiffuseUV\\+uvOffset\\);': 255 | `baseColor = texture(atlasTexture, vec3(vDiffuseUV, texAtlasIndex));`, 256 | 'CUSTOM_FRAGMENT_DEFINITIONS': ` 257 | uniform highp sampler2DArray atlasTexture; 258 | varying float texAtlasIndex; 259 | `, 260 | } 261 | return null 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /src/lib/util.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | // helper to swap item to end and pop(), instead of splice()ing 6 | export function removeUnorderedListItem(list, item) { 7 | var i = list.indexOf(item) 8 | if (i < 0) return 9 | if (i === list.length - 1) { 10 | list.pop() 11 | } else { 12 | list[i] = list.pop() 13 | } 14 | } 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | // .... 23 | export function numberOfVoxelsInSphere(rad) { 24 | if (rad === prevRad) return prevAnswer 25 | var ext = Math.ceil(rad), ct = 0, rsq = rad * rad 26 | for (var i = -ext; i <= ext; ++i) { 27 | for (var j = -ext; j <= ext; ++j) { 28 | for (var k = -ext; k <= ext; ++k) { 29 | var dsq = i * i + j * j + k * k 30 | if (dsq < rsq) ct++ 31 | } 32 | } 33 | } 34 | prevRad = rad 35 | prevAnswer = ct 36 | return ct 37 | } 38 | var prevRad = 0, prevAnswer = 0 39 | 40 | 41 | 42 | 43 | 44 | // partly "unrolled" loops to copy contents of ndarrays 45 | // when there's no source, zeroes out the array instead 46 | export function copyNdarrayContents(src, tgt, pos, size, tgtPos) { 47 | if (typeof src === 'number') { 48 | doNdarrayFill(src, tgt, tgtPos[0], tgtPos[1], tgtPos[2], 49 | size[0], size[1], size[2]) 50 | } else { 51 | doNdarrayCopy(src, tgt, pos[0], pos[1], pos[2], 52 | size[0], size[1], size[2], tgtPos[0], tgtPos[1], tgtPos[2]) 53 | } 54 | } 55 | function doNdarrayCopy(src, tgt, i0, j0, k0, si, sj, sk, ti, tj, tk) { 56 | var sdx = src.stride[2] 57 | var tdx = tgt.stride[2] 58 | for (var i = 0; i < si; i++) { 59 | for (var j = 0; j < sj; j++) { 60 | var six = src.index(i0 + i, j0 + j, k0) 61 | var tix = tgt.index(ti + i, tj + j, tk) 62 | for (var k = 0; k < sk; k++) { 63 | tgt.data[tix] = src.data[six] 64 | six += sdx 65 | tix += tdx 66 | } 67 | } 68 | } 69 | } 70 | 71 | function doNdarrayFill(value, tgt, i0, j0, k0, si, sj, sk) { 72 | var dx = tgt.stride[2] 73 | for (var i = 0; i < si; i++) { 74 | for (var j = 0; j < sj; j++) { 75 | var ix = tgt.index(i0 + i, j0 + j, k0) 76 | for (var k = 0; k < sk; k++) { 77 | tgt.data[ix] = value 78 | ix += dx 79 | } 80 | } 81 | } 82 | } 83 | 84 | 85 | 86 | 87 | // iterates over 3D positions a given manhattan distance from (0,0,0) 88 | // and exit early if the callback returns true 89 | // skips locations beyond a horiz or vertical max distance 90 | export function iterateOverShellAtDistance(d, xmax, ymax, cb) { 91 | if (d === 0) return cb(0, 0, 0) 92 | // larger top/bottom planes of current shell 93 | var dx = Math.min(d, xmax) 94 | var dy = Math.min(d, ymax) 95 | if (d <= ymax) { 96 | for (var x = -dx; x <= dx; x++) { 97 | for (var z = -dx; z <= dx; z++) { 98 | if (cb(x, d, z)) return true 99 | if (cb(x, -d, z)) return true 100 | } 101 | } 102 | } 103 | // smaller side planes of shell 104 | if (d <= xmax) { 105 | for (var i = -d; i < d; i++) { 106 | for (var y = -dy + 1; y < dy; y++) { 107 | if (cb(i, y, d)) return true 108 | if (cb(-i, y, -d)) return true 109 | if (cb(d, y, -i)) return true 110 | if (cb(-d, y, i)) return true 111 | } 112 | } 113 | } 114 | return false 115 | } 116 | 117 | 118 | 119 | 120 | 121 | 122 | // function to hash three indexes (i,j,k) into one integer 123 | // note that hash wraps around every 1024 indexes. 124 | // i.e.: hash(1, 1, 1) === hash(1025, 1, -1023) 125 | export function locationHasher(i, j, k) { 126 | return (i & 1023) 127 | | ((j & 1023) << 10) 128 | | ((k & 1023) << 20) 129 | } 130 | 131 | 132 | 133 | /* 134 | * 135 | * chunkStorage - a Map-backed abstraction for storing/ 136 | * retrieving chunk objects by their location indexes 137 | * 138 | */ 139 | 140 | /** @internal */ 141 | export class ChunkStorage { 142 | constructor() { 143 | this.hash = {} 144 | } 145 | 146 | /** @returns {import('./chunk').Chunk} */ 147 | getChunkByIndexes(i = 0, j = 0, k = 0) { 148 | return this.hash[locationHasher(i, j, k)] || null 149 | } 150 | /** @param {import('./chunk').Chunk} chunk */ 151 | storeChunkByIndexes(i = 0, j = 0, k = 0, chunk) { 152 | this.hash[locationHasher(i, j, k)] = chunk 153 | } 154 | removeChunkByIndexes(i = 0, j = 0, k = 0) { 155 | delete this.hash[locationHasher(i, j, k)] 156 | } 157 | } 158 | 159 | 160 | 161 | 162 | 163 | 164 | /* 165 | * 166 | * LocationQueue - simple array of [i,j,k] locations, 167 | * backed by a hash for O(1) existence checks. 168 | * removals by value are O(n). 169 | * 170 | */ 171 | 172 | /** @internal */ 173 | export class LocationQueue { 174 | constructor() { 175 | this.arr = [] 176 | this.hash = {} 177 | } 178 | forEach(cb, thisArg) { 179 | this.arr.forEach(cb, thisArg) 180 | } 181 | includes(i, j, k) { 182 | var id = locationHasher(i, j, k) 183 | return !!this.hash[id] 184 | } 185 | add(i, j, k, toFront = false) { 186 | var id = locationHasher(i, j, k) 187 | if (this.hash[id]) return 188 | if (toFront) { 189 | this.arr.unshift([i, j, k, id]) 190 | } else { 191 | this.arr.push([i, j, k, id]) 192 | } 193 | this.hash[id] = true 194 | } 195 | removeByIndex(ix) { 196 | var el = this.arr[ix] 197 | delete this.hash[el[3]] 198 | this.arr.splice(ix, 1) 199 | } 200 | remove(i, j, k) { 201 | var id = locationHasher(i, j, k) 202 | if (!this.hash[id]) return 203 | delete this.hash[id] 204 | for (var ix = 0; ix < this.arr.length; ix++) { 205 | if (id === this.arr[ix][3]) { 206 | this.arr.splice(ix, 1) 207 | return 208 | } 209 | } 210 | throw 'internal bug with location queue - hash value overlapped' 211 | } 212 | count() { return this.arr.length } 213 | isEmpty() { return (this.arr.length === 0) } 214 | empty() { 215 | this.arr = [] 216 | this.hash = {} 217 | } 218 | pop() { 219 | var el = this.arr.pop() 220 | delete this.hash[el[3]] 221 | return el 222 | } 223 | copyFrom(queue) { 224 | this.arr = queue.arr.slice() 225 | this.hash = {} 226 | for (var key in queue.hash) this.hash[key] = true 227 | } 228 | sortByDistance(locToDist, reverse = false) { 229 | sortLocationArrByDistance(this.arr, locToDist, reverse) 230 | } 231 | } 232 | 233 | // internal helper for preceding class 234 | function sortLocationArrByDistance(arr, distFn, reverse) { 235 | var hash = {} 236 | for (var loc of arr) { 237 | hash[loc[3]] = distFn(loc[0], loc[1], loc[2]) 238 | } 239 | if (reverse) { 240 | arr.sort((a, b) => hash[a[3]] - hash[b[3]]) // ascending 241 | } else { 242 | arr.sort((a, b) => hash[b[3]] - hash[a[3]]) // descending 243 | } 244 | hash = null 245 | } 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | // simple thing for reporting time split up between several activities 258 | export function makeProfileHook(every, title = '', filter) { 259 | if (!(every > 0)) return () => { } 260 | var times = {} 261 | var started = 0, last = 0, iter = 0, total = 0 262 | 263 | var start = () => { 264 | started = last = performance.now() 265 | iter++ 266 | } 267 | var add = (name) => { 268 | var t = performance.now() 269 | times[name] = (times[name] || 0) + (t - last) 270 | last = t 271 | } 272 | var report = () => { 273 | total += performance.now() - started 274 | if (iter < every) return 275 | var out = `${title}: ${(total / every).toFixed(2)}ms -- ` 276 | out += Object.keys(times).map(name => { 277 | if (filter && (times[name] / total) < 0.05) return '' 278 | return `${name}: ${(times[name] / iter).toFixed(2)}ms` 279 | }).join(' ') 280 | console.log(out + ` (avg over ${every} runs)`) 281 | times = {} 282 | iter = total = 0 283 | } 284 | return (state) => { 285 | if (state === 'start') start() 286 | else if (state === 'end') report() 287 | else add(state) 288 | } 289 | } 290 | 291 | 292 | 293 | 294 | // simple thing for reporting time actions/sec 295 | export function makeThroughputHook(_every, _title, filter) { 296 | var title = _title || '' 297 | var every = _every || 1 298 | var counts = {} 299 | var started = performance.now() 300 | var iter = 0 301 | return function profile_hook(state) { 302 | if (state === 'start') return 303 | if (state === 'end') { 304 | if (++iter < every) return 305 | var t = performance.now() 306 | console.log(title + ' ' + Object.keys(counts).map(k => { 307 | var through = counts[k] / (t - started) * 1000 308 | counts[k] = 0 309 | return k + ':' + through.toFixed(2) + ' ' 310 | }).join('')) 311 | started = t 312 | iter = 0 313 | } else { 314 | if (!counts[state]) counts[state] = 0 315 | counts[state]++ 316 | } 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // inputs 3 | "include": [ 4 | "src/index.js", 5 | "src/components/*.js", 6 | ], 7 | "exclude": [ 8 | "node_modules", 9 | ], 10 | "compilerOptions": { 11 | // output 12 | "outDir": "./dist", 13 | "target": "es5", 14 | "declaration": true, 15 | "emitDeclarationOnly": true, 16 | "rootDir": ".", 17 | // settings 18 | "allowJs": true, 19 | "checkJs": true, 20 | "resolveJsonModule": true, 21 | "esModuleInterop": true, 22 | "lib": [ 23 | "DOM", 24 | "DOM.Iterable", 25 | "ES2018" 26 | ], 27 | "typeRoots": [ 28 | "./types", 29 | ], 30 | // "moduleResolution": "Node", 31 | // "downlevelIteration": true, 32 | // "useDefineForClassFields": false, 33 | }, 34 | "typeAcquisition": { 35 | "exclude": [ 36 | "@types/gl-vec3", // automatic types are both wrong and out of date 37 | ] 38 | }, 39 | "typedocOptions": { 40 | "entryPoints": [ 41 | "src/index.js", 42 | ], 43 | "plugin": [ 44 | "typedoc-plugin-missing-exports", 45 | ], 46 | "entryPointStrategy": "expand", 47 | "name": "noa API reference", 48 | "out": "docs/API", 49 | "readme": "docs/api-header.md", 50 | "excludeInternal": true, // excludes stuff tagged @internal 51 | "excludeExternals": true, // excludes imports matching below 52 | "externalPattern": [ 53 | "node_modules/!(game-inputs|voxel-physics-engine)/**", 54 | ], 55 | "disableSources": true, 56 | }, 57 | } 58 | -------------------------------------------------------------------------------- /types/aabb-3d/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "aabb-3d" { 2 | export = AABB; 3 | function AABB(pos: any, vec: any): AABB; 4 | class AABB { 5 | constructor(pos: any, vec: any); 6 | base: any; 7 | vec: any; 8 | max: any; 9 | mag: any; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /types/ent-comp/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "dataStore" { 2 | export = DataStore; 3 | class DataStore { 4 | list: any[]; 5 | hash: {}; 6 | _map: {}; 7 | _pendingRemovals: any[]; 8 | add(id: any, stateObject: any): void; 9 | remove(id: any): void; 10 | dispose(): void; 11 | flush(): void; 12 | } 13 | } 14 | declare module "ent-comp" { 15 | export = ECS; 16 | /*! 17 | * ent-comp: a light, *fast* Entity Component System in JS 18 | * @url github.com/andyhall/ent-comp 19 | * @author Andy Hall 20 | * @license MIT 21 | */ 22 | /** 23 | * Constructor for a new entity-component-system manager. 24 | * 25 | * ```js 26 | * var ECS = require('ent-comp') 27 | * var ecs = new ECS() 28 | * ``` 29 | * @class 30 | * @constructor 31 | * @exports ECS 32 | * @typicalname ecs 33 | */ 34 | function ECS(): void; 35 | class ECS { 36 | /** 37 | * Hash of component definitions. Also aliased to `comps`. 38 | * 39 | * ```js 40 | * var comp = { name: 'foo' } 41 | * ecs.createComponent(comp) 42 | * ecs.components['foo'] === comp // true 43 | * ecs.comps['foo'] // same 44 | * ``` 45 | */ 46 | components: {}; 47 | comps: {}; 48 | /** @internal */ 49 | _storage: {}; 50 | /** @internal */ 51 | _systems: any[]; 52 | /** @internal */ 53 | _renderSystems: any[]; 54 | /** 55 | * Creates a new entity id (currently just an incrementing integer). 56 | * 57 | * Optionally takes a list of component names to add to the entity (with default state data). 58 | * 59 | * ```js 60 | * var id1 = ecs.createEntity() 61 | * var id2 = ecs.createEntity([ 'some-component', 'other-component' ]) 62 | * ``` 63 | */ 64 | createEntity: (compList: any) => number; 65 | /** 66 | * Deletes an entity, which in practice means removing all its components. 67 | * 68 | * ```js 69 | * ecs.deleteEntity(id) 70 | * ``` 71 | */ 72 | deleteEntity: (entID: any) => ECS; 73 | /** 74 | * Creates a new component from a definition object. 75 | * The definition must have a `name`; all other properties are optional. 76 | * 77 | * Returns the component name, to make it easy to grab when the component 78 | * is being `require`d from a module. 79 | * 80 | * ```js 81 | * var comp = { 82 | * name: 'some-unique-string', 83 | * state: {}, 84 | * order: 99, 85 | * multi: false, 86 | * onAdd: (id, state) => { }, 87 | * onRemove: (id, state) => { }, 88 | * system: (dt, states) => { }, 89 | * renderSystem: (dt, states) => { }, 90 | * } 91 | * 92 | * var name = ecs.createComponent( comp ) 93 | * // name == 'some-unique-string' 94 | * ``` 95 | * 96 | * Note the `multi` flag - for components where this is true, a given 97 | * entity can have multiple state objects for that component. 98 | * For multi-components, APIs that would normally return a state object 99 | * (like `getState`) will instead return an array of them. 100 | */ 101 | createComponent: (compDefn: any) => string; 102 | /** 103 | * Deletes the component definition with the given name. 104 | * First removes the component from all entities that have it. 105 | * 106 | * **Note:** This API shouldn't be necessary in most real-world usage - 107 | * you should set up all your components during init and then leave them be. 108 | * But it's useful if, say, you receive an ECS from another library and 109 | * you need to replace its components. 110 | * 111 | * ```js 112 | * ecs.deleteComponent( 'some-component' ) 113 | * ``` 114 | */ 115 | deleteComponent: (compName: any) => ECS; 116 | /** 117 | * Adds a component to an entity, optionally initializing the state object. 118 | * 119 | * ```js 120 | * ecs.createComponent({ 121 | * name: 'foo', 122 | * state: { val: 1 } 123 | * }) 124 | * ecs.addComponent(id1, 'foo') // use default state 125 | * ecs.addComponent(id2, 'foo', { val:2 }) // pass in state data 126 | * ``` 127 | */ 128 | addComponent: (entID: any, compName: any, state: any) => ECS; 129 | /** 130 | * Checks if an entity has a component. 131 | * 132 | * ```js 133 | * ecs.addComponent(id, 'foo') 134 | * ecs.hasComponent(id, 'foo') // true 135 | * ``` 136 | */ 137 | hasComponent: (entID: any, compName: any) => boolean; 138 | /** 139 | * Removes a component from an entity, triggering the component's 140 | * `onRemove` handler, and then deleting any state data. 141 | * 142 | * ```js 143 | * ecs.removeComponent(id, 'foo') 144 | * ecs.hasComponent(id, 'foo') // false 145 | * ``` 146 | */ 147 | removeComponent: (entID: any, compName: any) => ECS; 148 | /** 149 | * Get the component state for a given entity. 150 | * It will automatically have an `__id` property for the entity id. 151 | * 152 | * ```js 153 | * ecs.createComponent({ 154 | * name: 'foo', 155 | * state: { val: 0 } 156 | * }) 157 | * ecs.addComponent(id, 'foo') 158 | * ecs.getState(id, 'foo').val // 0 159 | * ecs.getState(id, 'foo').__id // equals id 160 | * ``` 161 | */ 162 | getState: (entID: any, compName: any) => any; 163 | /** 164 | * Get an array of state objects for every entity with the given component. 165 | * Each one will have an `__id` property for the entity id it refers to. 166 | * Don't add or remove elements from the returned list! 167 | * 168 | * ```js 169 | * var arr = ecs.getStatesList('foo') 170 | * // returns something shaped like: 171 | * // [ 172 | * // {__id:0, x:1}, 173 | * // {__id:7, x:2}, 174 | * // ] 175 | * ``` 176 | */ 177 | getStatesList: (compName: any) => any; 178 | /** 179 | * Makes a `getState`-like accessor bound to a given component. 180 | * The accessor is faster than `getState`, so you may want to create 181 | * an accessor for any component you'll be accessing a lot. 182 | * 183 | * ```js 184 | * ecs.createComponent({ 185 | * name: 'size', 186 | * state: { val: 0 } 187 | * }) 188 | * var getEntitySize = ecs.getStateAccessor('size') 189 | * // ... 190 | * ecs.addComponent(id, 'size', { val:123 }) 191 | * getEntitySize(id).val // 123 192 | * ``` 193 | */ 194 | getStateAccessor: (compName: any) => (id: any) => any; 195 | /** 196 | * Makes a `hasComponent`-like accessor function bound to a given component. 197 | * The accessor is much faster than `hasComponent`. 198 | * 199 | * ```js 200 | * ecs.createComponent({ 201 | * name: 'foo', 202 | * }) 203 | * var hasFoo = ecs.getComponentAccessor('foo') 204 | * // ... 205 | * ecs.addComponent(id, 'foo') 206 | * hasFoo(id) // true 207 | * ``` 208 | */ 209 | getComponentAccessor: (compName: any) => (id: any) => boolean; 210 | /** 211 | * Tells the ECS that a game tick has occurred, causing component 212 | * `system` functions to get called. 213 | * 214 | * The optional parameter simply gets passed to the system functions. 215 | * It's meant to be a timestep, but can be used (or not used) as you like. 216 | * 217 | * If components have an `order` property, they'll get called in that order 218 | * (lowest to highest). Component order defaults to `99`. 219 | * ```js 220 | * ecs.createComponent({ 221 | * name: foo, 222 | * order: 1, 223 | * system: function(dt, states) { 224 | * // states is the same array you'd get from #getStatesList() 225 | * states.forEach(state => { 226 | * console.log('Entity ID: ', state.__id) 227 | * }) 228 | * } 229 | * }) 230 | * ecs.tick(30) // triggers log statements 231 | * ``` 232 | */ 233 | tick: (dt: any) => ECS; 234 | /** 235 | * Functions exactly like `tick`, but calls `renderSystem` functions. 236 | * this effectively gives you a second set of systems that are 237 | * called with separate timing, in case you want to 238 | * [tick and render in separate loops](http://gafferongames.com/game-physics/fix-your-timestep/) 239 | * (which you should!). 240 | * 241 | * ```js 242 | * ecs.createComponent({ 243 | * name: foo, 244 | * order: 5, 245 | * renderSystem: function(dt, states) { 246 | * // states is the same array you'd get from #getStatesList() 247 | * } 248 | * }) 249 | * ecs.render(1000/60) 250 | * ``` 251 | */ 252 | render: (dt: any) => ECS; 253 | /** 254 | * Removes one particular instance of a multi-component. 255 | * To avoid breaking loops, the relevant state object will get nulled 256 | * immediately, and spliced from the states array later when safe 257 | * (after the current tick/render/animationFrame). 258 | * 259 | * ```js 260 | * // where component 'foo' is a multi-component 261 | * ecs.getState(id, 'foo') // [ state1, state2, state3 ] 262 | * ecs.removeMultiComponent(id, 'foo', 1) 263 | * ecs.getState(id, 'foo') // [ state1, null, state3 ] 264 | * // one JS event loop later... 265 | * ecs.getState(id, 'foo') // [ state1, state3 ] 266 | * ``` 267 | */ 268 | removeMultiComponent: (entID: any, compName: any, index: any) => ECS; 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /types/events/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "events" { 2 | export = EventEmitter; 3 | function EventEmitter(): void; 4 | class EventEmitter { 5 | /** @internal */ 6 | _events: any; 7 | /** @internal */ 8 | _eventsCount: number; 9 | /** @internal */ 10 | _maxListeners: number; 11 | setMaxListeners(n: any): EventEmitter; 12 | getMaxListeners(): any; 13 | emit(type: any, ...args: any[]): boolean; 14 | addListener(type: any, listener: any): any; 15 | on: any; 16 | prependListener(type: any, listener: any): any; 17 | once(type: any, listener: any): EventEmitter; 18 | prependOnceListener(type: any, listener: any): EventEmitter; 19 | removeListener(type: any, listener: any): EventEmitter; 20 | off: any; 21 | removeAllListeners(type: any, ...args: any[]): EventEmitter; 22 | listeners(type: any): any[]; 23 | rawListeners(type: any): any[]; 24 | listenerCount: typeof listenerCount; 25 | eventNames(): any; 26 | } 27 | namespace EventEmitter { 28 | export { EventEmitter, defaultMaxListeners, init, listenerCount, once }; 29 | } 30 | function listenerCount(type: any): any; 31 | var defaultMaxListeners: number; 32 | function init(): void; 33 | function listenerCount(emitter: any, type: any): any; 34 | function once(emitter: any, name: any): Promise; 35 | } 36 | --------------------------------------------------------------------------------