diff --git a/README.md b/README.md index a34ff74..725d614 100644 --- a/README.md +++ b/README.md @@ -276,8 +276,39 @@ is on very early stage. The script to make glb+meta from blender file is here: [build_blender_scene.py](packages/core/blender_exporter/build_blender_scene.py) ### Console -Engine provides a simple console, which can be used at runtime (if enabled in world) by pressing \`. Your game can -provide custom console commands using `world.registerConsoleCommand` function +Engine provides a simple console, which can be used at runtime (if enabled in world) by pressing \`. Your game can +provide custom console commands using `GgStatic.instance.registerConsoleCommand` function. + +#### Default global console commands +| Command | Arguments | Description | +|---------------|---------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `commands` | - | Print all available commands. List includes global commands and commands, specific to currently selected world. Run "world" to check which world is currently selected and "world {world_name}" to select desired world | +| `help` | `string` | Print doc string of provided command | +| `worlds` | - | Print all currently available worlds | +| `world` | `string?` | Get name of selected world or select world by name. Use "worlds" to get list of currently available worlds | +| `stats_panel` | `0\|1?` | Turn on/off stats panel, skip argument to toggle value | +| `debug_panel` | `0\|1?` | Turn on/off debug panel, skip argument to toggle value | +| `bind_key` | `string, ...string` | Bind a keyboard key by code to console command. Check key codes [here](https://www.toptal.com/developers/keycode). Use "unbind_key" command to unbind it | +| `unbind_key` | `string` | Unbind a keyboard key from console command | + +#### Default world-specific console commands +| Command | Arguments | Description | +|---------------|--------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `timescale` | `float?` | Get current time scale of selected world clock or set it. Default value is 1.0 (no time scale applied) | +| `fps_limit` | `int?` | Get current tick rate limit of selected world clock or set it. 0 means no limit applied | +| `renderers` | - | Print all renderers in selected world | +| `debug_view` | `0\|1?, string?` | Turn on/off physics debug view, skip first argument to toggle value. Second argument expects renderer name, if not provided first renderer will be picked. Use "renderers" to get list of renderers in the world | +| `performance` | `int?, avg\|peak?` | Measure how much time was spent per entity in world. Arguments are samples amount (20 by default) and "peak" or "avg" choice, both arguments are optional. "avg" report sorts entities by average time consumed, "peak" records highest value for each entity | + +#### Default 2D world-specific console commands +| Command | Arguments | Description | +|-----------|------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------| +| `gravity` | `?float, ?float` | Get or set 2D world gravity vector. 1 argument sets vector {x: 0, y: value}, 2 arguments sets the whole vector. Default value is "9.82" or "0 9.82" | + +#### Default 3D world-specific console commands +| Command | Arguments | Description | +|-----------|--------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `gravity` | `?float, ?float, ?float` | Get or set 3D world gravity vector. 1 argument sets vector {x: 0, y: 0, z: -value}, 3 arguments set the whole vector. Default value is "9.82" or "0 0 -9.82" | ## Support You can support project by: diff --git a/examples/ammo-car-three-ammo/index.ts b/examples/ammo-car-three-ammo/index.ts index 6036b9e..3d78f1b 100644 --- a/examples/ammo-car-three-ammo/index.ts +++ b/examples/ammo-car-three-ammo/index.ts @@ -13,6 +13,9 @@ import { ThreeDisplayObject3dOpts, ThreeSceneComponent, ThreeVisualTypeDocRepo } import { AmbientLight, DirectionalLight } from 'three'; import { AmmoPhysicsTypeDocRepo, AmmoWorldComponent } from '@gg-web-engine/ammo'; +GgStatic.instance.showStats = true; +GgStatic.instance.devConsoleEnabled = true; + const world = new Gg3dWorld< ThreeVisualTypeDocRepo, AmmoPhysicsTypeDocRepo, @@ -22,8 +25,6 @@ const world = new Gg3dWorld< new ThreeSceneComponent(), new AmmoWorldComponent(), ); -GgStatic.instance.showStats = true; -// GgStatic.instance.devConsoleEnabled = true; world.init().then(async () => { // init graphics const canvas = document.getElementById('gg')! as HTMLCanvasElement; diff --git a/examples/collision-groups-pool-three-ammo/index.ts b/examples/collision-groups-pool-three-ammo/index.ts index a1fcba7..8dd9309 100644 --- a/examples/collision-groups-pool-three-ammo/index.ts +++ b/examples/collision-groups-pool-three-ammo/index.ts @@ -11,6 +11,9 @@ import { ThreeSceneComponent, ThreeVisualTypeDocRepo } from '@gg-web-engine/thre import { AmbientLight, DirectionalLight, Material, Mesh } from 'three'; import { AmmoPhysicsTypeDocRepo, AmmoWorldComponent } from '@gg-web-engine/ammo'; +GgStatic.instance.showStats = true; +GgStatic.instance.devConsoleEnabled = true; + const world = new Gg3dWorld< ThreeVisualTypeDocRepo, AmmoPhysicsTypeDocRepo, @@ -23,8 +26,6 @@ const world = new Gg3dWorld< world.physicsWorld.maxSubSteps = 25; world.init().then(async () => { - GgStatic.instance.showStats = true; - // GgStatic.instance.devConsoleEnabled = true; const canvas = document.getElementById('gg')! as HTMLCanvasElement; const renderer = world.addRenderer( world.visualScene.factory.createPerspectiveCamera(), diff --git a/examples/collision-groups-pool-three-rapier3d/index.ts b/examples/collision-groups-pool-three-rapier3d/index.ts index a545d97..1499f71 100644 --- a/examples/collision-groups-pool-three-rapier3d/index.ts +++ b/examples/collision-groups-pool-three-rapier3d/index.ts @@ -11,6 +11,9 @@ import { ThreeSceneComponent, ThreeVisualTypeDocRepo } from '@gg-web-engine/thre import { AmbientLight, DirectionalLight, Material, Mesh } from 'three'; import { Rapier3dPhysicsTypeDocRepo, Rapier3dWorldComponent } from '@gg-web-engine/rapier3d'; +GgStatic.instance.showStats = true; +GgStatic.instance.devConsoleEnabled = true; + const world = new Gg3dWorld< ThreeVisualTypeDocRepo, Rapier3dPhysicsTypeDocRepo, @@ -21,8 +24,6 @@ const world = new Gg3dWorld< new Rapier3dWorldComponent(), ); world.init().then(async () => { - GgStatic.instance.showStats = true; - // GgStatic.instance.devConsoleEnabled = true; const canvas = document.getElementById('gg')! as HTMLCanvasElement; const renderer = world.addRenderer( world.visualScene.factory.createPerspectiveCamera(), diff --git a/examples/collision-groups-three-ammo/index.ts b/examples/collision-groups-three-ammo/index.ts index 4741391..4426e14 100644 --- a/examples/collision-groups-three-ammo/index.ts +++ b/examples/collision-groups-three-ammo/index.ts @@ -3,13 +3,14 @@ import { ThreeSceneComponent } from '@gg-web-engine/three'; import { AmbientLight, DirectionalLight } from 'three'; import { AmmoWorldComponent } from '@gg-web-engine/ammo'; +GgStatic.instance.showStats = true; +GgStatic.instance.devConsoleEnabled = true; + const world = new Gg3dWorld( new ThreeSceneComponent(), new AmmoWorldComponent(), ); world.init().then(async () => { - GgStatic.instance.showStats = true; - // GgStatic.instance.devConsoleEnabled = true; const canvas = document.getElementById('gg')! as HTMLCanvasElement; const renderer = world.addRenderer( world.visualScene.factory.createPerspectiveCamera(), diff --git a/examples/collision-groups-three-rapier3d/index.ts b/examples/collision-groups-three-rapier3d/index.ts index 9844887..2995ac0 100644 --- a/examples/collision-groups-three-rapier3d/index.ts +++ b/examples/collision-groups-three-rapier3d/index.ts @@ -3,13 +3,14 @@ import { ThreeSceneComponent } from '@gg-web-engine/three'; import { AmbientLight, DirectionalLight } from 'three'; import { Rapier3dWorldComponent } from '@gg-web-engine/rapier3d'; +GgStatic.instance.showStats = true; +GgStatic.instance.devConsoleEnabled = true; + const world = new Gg3dWorld( new ThreeSceneComponent(), new Rapier3dWorldComponent(), ); world.init().then(async () => { - GgStatic.instance.showStats = true; - // GgStatic.instance.devConsoleEnabled = true; const canvas = document.getElementById('gg')! as HTMLCanvasElement; const renderer = world.addRenderer( world.visualScene.factory.createPerspectiveCamera(), diff --git a/examples/fly-city-three-ammo/src/app/game-factory.ts b/examples/fly-city-three-ammo/src/app/game-factory.ts index b5b2f5e..eed90b1 100644 --- a/examples/fly-city-three-ammo/src/app/game-factory.ts +++ b/examples/fly-city-three-ammo/src/app/game-factory.ts @@ -19,14 +19,14 @@ import { CAR_SPECS, LAMBO_SPECS, TRUCK_SPECS } from './car-specs'; import { FlyCityPTypeDoc, FlyCityVTypeDoc, FlyCityWorld } from './app.component'; import { takeUntil } from 'rxjs/operators'; +GgStatic.instance.showStats = true; +GgStatic.instance.devConsoleEnabled = true; export class GameFactory { constructor(public readonly world: FlyCityWorld) { } public async initGame(canvas: HTMLCanvasElement): Promise<[Renderer3dEntity, MapGraph3dEntity, Trigger3dEntity]> { - GgStatic.instance.showStats = true; - // GgStatic.instance.devConsoleEnabled = true; await this.world.init(); const renderer = await this.initRenderer(canvas); this.addLights(); diff --git a/examples/framework-angular-three-ammo/src/main.ts b/examples/framework-angular-three-ammo/src/main.ts index 5a3fd14..2a6f473 100644 --- a/examples/framework-angular-three-ammo/src/main.ts +++ b/examples/framework-angular-three-ammo/src/main.ts @@ -5,6 +5,9 @@ import { ThreeSceneComponent, ThreeVisualTypeDocRepo } from '@gg-web-engine/thre import { AmmoPhysicsTypeDocRepo, AmmoWorldComponent } from '@gg-web-engine/ammo'; import { Subject } from 'rxjs'; +GgStatic.instance.showStats = true; +GgStatic.instance.devConsoleEnabled = true; + @Component({ selector: 'my-app', standalone: true, @@ -21,8 +24,6 @@ export class App implements OnInit, OnDestroy { new AmmoWorldComponent(), ); await this.world.init(); - GgStatic.instance.showStats = true; - // GgStatic.instance.devConsoleEnabled = true; const canvas = document.getElementById('gg')! as HTMLCanvasElement; const renderer = this.world.addRenderer( diff --git a/examples/framework-react-three-rapier3d/src/App.tsx b/examples/framework-react-three-rapier3d/src/App.tsx index 412af48..9a143e9 100644 --- a/examples/framework-react-three-rapier3d/src/App.tsx +++ b/examples/framework-react-three-rapier3d/src/App.tsx @@ -1,14 +1,11 @@ -import { useRef, useEffect } from 'react'; -import { - Trigger3dEntity, - OrbitCameraController, - Gg3dWorld, - Entity3d, - GgStatic, -} from '@gg-web-engine/core'; +import { useEffect, useRef } from 'react'; +import { Entity3d, Gg3dWorld, GgStatic, OrbitCameraController, Trigger3dEntity } from '@gg-web-engine/core'; import { ThreeSceneComponent } from '@gg-web-engine/three'; import { Rapier3dWorldComponent } from '@gg-web-engine/rapier3d'; +GgStatic.instance.showStats = true; +GgStatic.instance.devConsoleEnabled = true; + const isNewWorld = !GgStatic.instance.selectedWorld; const world = (GgStatic.instance.selectedWorld as Gg3dWorld) || @@ -16,22 +13,19 @@ const world = if (isNewWorld) { world.init().then(() => { world.start(); - GgStatic.instance.showStats = true; - GgStatic.instance.devConsoleEnabled = true; }); } function App() { const canvasRef = useRef(null); - const spawnTimerRef = useRef(null); useEffect(() => { const initializeWorld = async () => { while (!world.isRunning) { await new Promise((r) => setTimeout(r, 50)); } - if (spawnTimerRef.current) { - clearInterval(spawnTimerRef.current); + for (const c of world.worldClock.children) { + c.dispose(); } for (const e of world.children) { if (!e.name.startsWith('fps_meter')) { @@ -40,7 +34,7 @@ function App() { } const renderer = world.addRenderer( world.visualScene.factory.createPerspectiveCamera({}), - canvasRef.current! + canvasRef.current!, ); renderer.position = { x: 9, y: 12, z: 9 }; @@ -58,7 +52,7 @@ function App() { world.physicsWorld.factory.createTrigger({ shape: 'BOX', dimensions: { x: 1000, y: 1000, z: 1 }, - }) + }), ); destroyTrigger.position = { x: 0, y: 0, z: -5 }; destroyTrigger.onEntityEntered.subscribe((entity) => { @@ -70,7 +64,9 @@ function App() { }); world.addEntity(destroyTrigger); - spawnTimerRef.current = window.setInterval(() => { + const spawnTimer = world.createClock(true); + spawnTimer.tickRateLimit = 2; + spawnTimer.tick$.subscribe(() => { let item: Entity3d; const random = Math.random(); if (random < 0.2) { @@ -104,22 +100,14 @@ function App() { y: Math.random() * 5 - 2.5, z: 10, }; - }, 500); - }; - - initializeWorld(); - - // Cleanup on unmount or dependency change - return () => { - if (spawnTimerRef.current) { - clearInterval(spawnTimerRef.current); - } + }); }; + initializeWorld().then(); }, []); return ( <> - + ); } diff --git a/examples/glb-loader-three-ammo/index.ts b/examples/glb-loader-three-ammo/index.ts index 9ff7a47..e12ceb4 100644 --- a/examples/glb-loader-three-ammo/index.ts +++ b/examples/glb-loader-three-ammo/index.ts @@ -3,6 +3,9 @@ import { ThreeSceneComponent, ThreeVisualTypeDocRepo } from '@gg-web-engine/thre import { AmbientLight, DirectionalLight } from 'three'; import { AmmoPhysicsTypeDocRepo, AmmoWorldComponent } from '@gg-web-engine/ammo'; +GgStatic.instance.showStats = true; +GgStatic.instance.devConsoleEnabled = true; + const world = new Gg3dWorld< ThreeVisualTypeDocRepo, AmmoPhysicsTypeDocRepo, @@ -13,8 +16,6 @@ const world = new Gg3dWorld< new AmmoWorldComponent(), ); world.init().then(async () => { - GgStatic.instance.showStats = true; - // GgStatic.instance.devConsoleEnabled = true; const canvas = document.getElementById('gg')! as HTMLCanvasElement; const renderer = world.addRenderer(world.visualScene.factory.createPerspectiveCamera(), canvas); renderer.position = { x: 9, y: 12, z: 9 }; diff --git a/examples/glb-loader-three-rapier3d/index.ts b/examples/glb-loader-three-rapier3d/index.ts index 4df4b80..a6a402d 100644 --- a/examples/glb-loader-three-rapier3d/index.ts +++ b/examples/glb-loader-three-rapier3d/index.ts @@ -3,6 +3,9 @@ import { ThreeSceneComponent, ThreeVisualTypeDocRepo } from '@gg-web-engine/thre import { AmbientLight, DirectionalLight } from 'three'; import { Rapier3dPhysicsTypeDocRepo, Rapier3dWorldComponent } from '@gg-web-engine/rapier3d'; +GgStatic.instance.showStats = true; +GgStatic.instance.devConsoleEnabled = true; + const world = new Gg3dWorld< ThreeVisualTypeDocRepo, Rapier3dPhysicsTypeDocRepo, @@ -13,8 +16,6 @@ const world = new Gg3dWorld< new Rapier3dWorldComponent(), ); world.init().then(async () => { - GgStatic.instance.showStats = true; - // GgStatic.instance.devConsoleEnabled = true; const canvas = document.getElementById('gg')! as HTMLCanvasElement; const renderer = world.addRenderer(world.visualScene.factory.createPerspectiveCamera(), canvas); renderer.position = { x: 9, y: 12, z: 9 }; diff --git a/examples/primitives-pixi-matter/index.ts b/examples/primitives-pixi-matter/index.ts index 2b17878..98a79f1 100644 --- a/examples/primitives-pixi-matter/index.ts +++ b/examples/primitives-pixi-matter/index.ts @@ -2,13 +2,14 @@ import { Entity2d, Gg2dWorld, GgStatic } from '@gg-web-engine/core'; import { PixiSceneComponent } from '@gg-web-engine/pixi'; import { MatterWorldComponent } from '@gg-web-engine/matter'; +GgStatic.instance.showStats = true; +GgStatic.instance.devConsoleEnabled = true; + const world = new Gg2dWorld( new PixiSceneComponent(), new MatterWorldComponent(), ); world.init().then(async () => { - GgStatic.instance.showStats = true; - // GgStatic.instance.devConsoleEnabled = true; const canvas = document.getElementById('gg')! as HTMLCanvasElement; const renderer = world.addRenderer(canvas); diff --git a/examples/primitives-pixi-rapier2d/index.ts b/examples/primitives-pixi-rapier2d/index.ts index fa60a79..c324ae7 100644 --- a/examples/primitives-pixi-rapier2d/index.ts +++ b/examples/primitives-pixi-rapier2d/index.ts @@ -2,13 +2,14 @@ import { Entity2d, Gg2dWorld, GgStatic } from '@gg-web-engine/core'; import { PixiSceneComponent } from '@gg-web-engine/pixi'; import { Rapier2dWorldComponent } from '@gg-web-engine/rapier2d'; +GgStatic.instance.showStats = true; +GgStatic.instance.devConsoleEnabled = true; + const world = new Gg2dWorld( new PixiSceneComponent(), new Rapier2dWorldComponent(), ); world.init().then(async () => { - GgStatic.instance.showStats = true; - // GgStatic.instance.devConsoleEnabled = true; const canvas = document.getElementById('gg')! as HTMLCanvasElement; const renderer = world.addRenderer(canvas); diff --git a/examples/primitives-three-ammo/index.ts b/examples/primitives-three-ammo/index.ts index 0f6c268..cf26cf8 100644 --- a/examples/primitives-three-ammo/index.ts +++ b/examples/primitives-three-ammo/index.ts @@ -2,13 +2,14 @@ import { Entity3d, Gg3dWorld, GgStatic, OrbitCameraController, Trigger3dEntity } import { ThreeSceneComponent } from '@gg-web-engine/three'; import { AmmoWorldComponent } from '@gg-web-engine/ammo'; +GgStatic.instance.showStats = true; +GgStatic.instance.devConsoleEnabled = true; + const world = new Gg3dWorld( new ThreeSceneComponent(), new AmmoWorldComponent(), ); world.init().then(async () => { - GgStatic.instance.showStats = true; - // GgStatic.instance.devConsoleEnabled = true; const canvas = document.getElementById('gg')! as HTMLCanvasElement; const renderer = world.addRenderer(world.visualScene.factory.createPerspectiveCamera(), canvas); renderer.position = { x: 9, y: 12, z: 9 }; diff --git a/examples/primitives-three-rapier3d/index.ts b/examples/primitives-three-rapier3d/index.ts index 5d1cfac..ab10469 100644 --- a/examples/primitives-three-rapier3d/index.ts +++ b/examples/primitives-three-rapier3d/index.ts @@ -2,13 +2,14 @@ import { Entity3d, Gg3dWorld, GgStatic, OrbitCameraController, Trigger3dEntity } import { ThreeSceneComponent } from '@gg-web-engine/three'; import { Rapier3dWorldComponent } from '@gg-web-engine/rapier3d'; +GgStatic.instance.showStats = true; +GgStatic.instance.devConsoleEnabled = true; + const world = new Gg3dWorld( new ThreeSceneComponent(), new Rapier3dWorldComponent(), ); world.init().then(async () => { - GgStatic.instance.showStats = true; - // GgStatic.instance.devConsoleEnabled = true; const canvas = document.getElementById('gg')! as HTMLCanvasElement; const renderer = world.addRenderer(world.visualScene.factory.createPerspectiveCamera(), canvas); renderer.position = { x: 9, y: 12, z: 9 }; diff --git a/examples/shooter-three-ammo/index.ts b/examples/shooter-three-ammo/index.ts index 6d8aa43..dce39f0 100644 --- a/examples/shooter-three-ammo/index.ts +++ b/examples/shooter-three-ammo/index.ts @@ -3,6 +3,9 @@ import { ThreeSceneComponent, ThreeVisualTypeDocRepo } from '@gg-web-engine/thre import { AmbientLight, DirectionalLight, Mesh, MeshPhongMaterial, RepeatWrapping, TextureLoader } from 'three'; import { AmmoPhysicsTypeDocRepo, AmmoWorldComponent } from '@gg-web-engine/ammo'; +GgStatic.instance.showStats = true; +GgStatic.instance.devConsoleEnabled = true; + const world = new Gg3dWorld< ThreeVisualTypeDocRepo, AmmoPhysicsTypeDocRepo, @@ -15,9 +18,6 @@ const world = new Gg3dWorld< world.physicsWorld.maxSubSteps = 10; world.physicsWorld.fixedTimeStep = null; -GgStatic.instance.showStats = true; -// GgStatic.instance.devConsoleEnabled = true; - world.init().then(async () => { // init graphics const canvas = document.getElementById('gg')! as HTMLCanvasElement; diff --git a/packages/core/src/2d/gg-2d-world.ts b/packages/core/src/2d/gg-2d-world.ts index 0449be5..2374147 100644 --- a/packages/core/src/2d/gg-2d-world.ts +++ b/packages/core/src/2d/gg-2d-world.ts @@ -73,18 +73,22 @@ export class Gg2dWorld< super.registerConsoleCommands(ggstatic); ggstatic.registerConsoleCommand( this, - 'ph_gravity', + 'gravity', async (...args: string[]) => { if (args.length == 1) { args = ['0', args[0]]; // mean Y axis } if (args.length > 0) { + if (isNaN(+args[0]) || isNaN(+args[1])) { + throw new Error('Wrong arguments'); + } this.physicsWorld.gravity = { x: +args[0], y: +args[1] }; } return JSON.stringify(this.physicsWorld.gravity); }, - 'args: [float] or [float float]; change 2D world gravity vector. 1 argument means ' + - '{x: 0, y: value}, 2 arguments set the whole vector. Default value is "9.82" or "0 9.82"', + 'args: [ ?float, ?float ]; Get or set 2D world gravity vector. 1 argument sets' + + ' vector {x: 0, y: value}, 2 arguments sets the whole vector.' + + ' Default value is "9.82" or "0 9.82"', ); } } diff --git a/packages/core/src/3d/gg-3d-world.ts b/packages/core/src/3d/gg-3d-world.ts index 8bb33d3..e3a56aa 100644 --- a/packages/core/src/3d/gg-3d-world.ts +++ b/packages/core/src/3d/gg-3d-world.ts @@ -85,18 +85,22 @@ export class Gg3dWorld< super.registerConsoleCommands(ggstatic); ggstatic.registerConsoleCommand( this, - 'ph_gravity', + 'gravity', async (...args: string[]) => { if (args.length == 1) { args = ['0', '0', '' + -+args[0]]; // mean -Z axis } if (args.length > 0) { + if (isNaN(+args[0]) || isNaN(+args[1]) || isNaN(+args[2])) { + throw new Error('Wrong arguments'); + } this.physicsWorld.gravity = { x: +args[0], y: +args[1], z: +args[2] }; } return JSON.stringify(this.physicsWorld.gravity); }, - 'args: [float] or [float float float]; change 3D world gravity vector. 1 argument means ' + - '{x: 0, y: 0, z: -value}, 3 arguments set the whole vector. Default value is "9.82" or "0 0 -9.82"', + 'args: [ ?float, ?float, ?float ]; Get or set 3D world gravity vector. 1 argument sets ' + + 'vector {x: 0, y: 0, z: -value}, 3 arguments set the whole vector.' + + ' Default value is "9.82" or "0 0 -9.82"', ); } } diff --git a/packages/core/src/base/gg-world.ts b/packages/core/src/base/gg-world.ts index 53bb50e..019455c 100644 --- a/packages/core/src/base/gg-world.ts +++ b/packages/core/src/base/gg-world.ts @@ -212,56 +212,78 @@ export abstract class GgWorld< }) { ggstatic.registerConsoleCommand( this, - 'ph_timescale', + 'timescale', async (...args: string[]) => { - this.worldClock.timeScale = +args[0]; - return JSON.stringify(this.worldClock.timeScale); + if (!isNaN(+args[0])) { + this.worldClock.timeScale = +args[0]; + } + return this.worldClock.timeScale.toString(); + }, + 'args: [ float? ]; Get current time scale of selected world clock or set it.' + + ' Default value is 1.0 (no time scale applied)', + ); + ggstatic.registerConsoleCommand( + this, + 'fps_limit', + async (...args: string[]) => { + if (!isNaN(+args[0])) { + this.worldClock.tickRateLimit = +args[0]; + } + return this.worldClock.tickRateLimit.toString(); }, - 'args: [float]; change time scale of physics engine. Default value is 1.0', + 'args: [ int? ]; Get current tick rate limit of selected world clock or set it. 0 means no limit applied', ); ggstatic.registerConsoleCommand( this, - 'ls_renderers', + 'renderers', async () => { return this.children .filter(e => e instanceof IRendererEntity) .map(r => r.name) .join('\n'); }, - 'no args; print all renderers in selected world', + 'no args; Print all renderers in selected world', ); ggstatic.registerConsoleCommand( this, - 'dr_drawphysics', + 'debug_view', async (...args: string[]) => { - const value = ['1', 'true', '+'].includes(args[0]); - const rendererName = args[1]; - let renderer: IEntity; - if (rendererName) { - renderer = this.children.find(x => x instanceof IRendererEntity && x.name === rendererName)!; - } else { - renderer = this.children.find(x => x instanceof IRendererEntity)!; + let value: boolean | 'toggle' = 'toggle'; + let rendererName: string | undefined = undefined; + for (let arg of args) { + if (['1', '0'].includes(arg)) { + value = arg === '1'; + } else { + rendererName = arg; + } } + let renderer: IRendererEntity | null = this.children.find( + x => x instanceof IRendererEntity && (!rendererName || x.name === rendererName), + ) as any; if (renderer) { - (renderer as IRendererEntity).physicsDebugViewActive = value; - return '' + value; + renderer.physicsDebugViewActive = value === 'toggle' ? !renderer.physicsDebugViewActive : value; + return renderer.physicsDebugViewActive ? '1' : '0'; + } else if (rendererName) { + throw new Error(`Renderer with name "${rendererName}" not found`); + } else { + throw new Error(`No renderer found`); } - return 'false'; }, - 'args: [0 or 1] or [0 or 1, string]; turn on/off physics debug view. Second argument expects renderer ' + - 'name, if not provided first renderer will be picked. Look up for renderer names using command "ls_renderers"', + 'args: [ 0|1?, string? ]; Turn on/off physics debug view, skip first argument to toggle value.' + + ' Second argument expects renderer name, if not provided first renderer will be picked.' + + ' Use "renderers" to get list of renderers in the world', ); ggstatic.registerConsoleCommand( this, - 'performance_report', + 'performance', async (...args: string[]) => { let mode: 'avg' | 'peak' = 'avg'; let samples = 20; - for (let i = 0; i < 2; i++) { - if (['avg', 'peak'].includes(args[i])) { - mode = args[i] as any; - } else if (!isNaN(+args[i])) { - samples = +args[i]; + for (let arg of args) { + if (['avg', 'peak'].includes(arg)) { + mode = arg as any; + } else if (!isNaN(+arg)) { + samples = +arg; } } const meter = new PerformanceMeterEntity(samples, 250); @@ -291,7 +313,7 @@ export abstract class GgWorld< renderItems.unshift(`Performance report (${samples} samples)`); return renderItems.join('\n'); }, - 'args: [int, avg|peak]; [avg|peak, int]; [avg|peak]; [int] or []; measure how much time was spent per ' + + 'args: [ int?, avg|peak? ]; Measure how much time was spent per ' + 'entity in world. Arguments are samples amount (20 by default) and "peak" or "avg" choice, both arguments are ' + 'optional. "avg" report sorts entities by average time consumed, "peak" records highest value for each entity', ); diff --git a/packages/core/src/base/inputs/keyboard.input.ts b/packages/core/src/base/inputs/keyboard.input.ts index 82f1185..83aa56a 100644 --- a/packages/core/src/base/inputs/keyboard.input.ts +++ b/packages/core/src/base/inputs/keyboard.input.ts @@ -18,7 +18,7 @@ export class KeyboardInput extends IInput { /** * Which element types should filter key downs when focused */ - public externalFocusBlacklist: { new (): HTMLElement }[] = [ + public static externalFocusBlacklist: { new (): HTMLElement }[] = [ HTMLInputElement, HTMLTextAreaElement, HTMLSelectElement, @@ -146,7 +146,7 @@ export class KeyboardInput extends IInput { } const pressed = e.type == 'keydown'; if (pressed && this.skipKeyDownsOnExternalFocus && document.activeElement) { - for (const k of this.externalFocusBlacklist) { + for (const k of KeyboardInput.externalFocusBlacklist) { if (document.activeElement instanceof k) { return; } diff --git a/packages/core/src/dev/gg-console.ui.ts b/packages/core/src/dev/gg-console.ui.ts index 9f4a8cf..57b1fdd 100644 --- a/packages/core/src/dev/gg-console.ui.ts +++ b/packages/core/src/dev/gg-console.ui.ts @@ -18,7 +18,7 @@ export class GgConsoleUI { Welcome to GG web engine UI console. Enter command in input below. -List of available commands: `.replace(/ /g, ' ') + `ls_commands`; +List of available commands: `.replace(/ /g, ' ') + `commands`; private commandHistory: string[] = []; private currentCommandIndex = 0; // for repeating command using up/down arrow keys @@ -94,9 +94,14 @@ List of available commands: `.replace(/ /g, ' ') + ` ' + command); this.stdout('\n' + (await (window as any).ggstatic.console(command))); diff --git a/packages/core/src/dev/gg-static.ts b/packages/core/src/dev/gg-static.ts index 2f73de1..df96cde 100644 --- a/packages/core/src/dev/gg-static.ts +++ b/packages/core/src/dev/gg-static.ts @@ -1,7 +1,18 @@ -import { GgWorld } from '../base'; +import { GgWorld, KeyboardInput } from '../base'; import { GgConsoleUI } from './gg-console.ui'; import { GgDebuggerUI } from './gg-debugger.ui'; -import { BehaviorSubject, combineLatest, NEVER, switchMap, take, takeWhile } from 'rxjs'; +import { + BehaviorSubject, + combineLatest, + filter, + fromEvent, + NEVER, + Subject, + switchMap, + take, + takeUntil, + takeWhile, +} from 'rxjs'; export class GgStatic { public static get instance(): GgStatic { @@ -104,7 +115,7 @@ export class GgStatic { public get availableCommands(): [string, { handler: (...args: string[]) => Promise; doc?: string }][] { let commands = this.consoleCommands.get(null) || {}; if (this.selectedWorld) { - commands = { ...(this.consoleCommands.get(this.selectedWorld) || {}), ...commands }; + commands = { ...commands, ...(this.consoleCommands.get(this.selectedWorld) || {}) }; } return Object.entries(commands); } @@ -112,7 +123,7 @@ export class GgStatic { private constructor() { this.registerConsoleCommand( null, - 'ls_commands', + 'commands', async () => { return this.availableCommands .map( @@ -121,24 +132,37 @@ export class GgStatic { value.doc ? '\t// ' + value.doc + '' : '' }`, ) - .sort() .join('\n\n'); }, - 'no args; print all available commands', + 'no args; Print all available commands. List includes global commands and commands, ' + + 'specific to currently selected world. Run "world" to check which world is currently ' + + 'selected and "world {world_name}" to select desired world', ); this.registerConsoleCommand( null, - 'ls_worlds', + 'help', + async (keyword: string) => { + const command: { doc?: string } | undefined = this.availableCommands.find(([key]) => key === keyword)?.[1]; + if (!command) { + throw new Error(`Unrecognized command: ${keyword}`); + } + return `${command.doc || 'No doc string given'}`; + }, + 'args: [ string ]; Print doc string of provided command', + ); + this.registerConsoleCommand( + null, + 'worlds', async () => { return GgWorld.documentWorlds .map(w => (w === this.selectedWorld ? `* ${w.name}` : ` ${w.name}`)) .join('\n'); }, - 'no args; print all available worlds', + 'no args; Print all currently available worlds', ); this.registerConsoleCommand( null, - 'select_world', + 'world', async (...args: string[]) => { for (const w of GgWorld.documentWorlds) { if (w.name === args[0]) { @@ -148,25 +172,65 @@ export class GgStatic { } return this.selectedWorld?.name || 'null'; }, - 'args: [string]; select world by name', + 'args: [ string? ]; Get name of selected world or select world by name. Use ' + + '"worlds" to get list of currently available worlds', ); this.registerConsoleCommand( null, - 'show_debugger', + 'stats_panel', async (...args: string[]) => { - this.showDebugControls = ['1', 'true', '+'].includes(args[0]); - return '' + this.showDebugControls; + this.showStats = args[0] === undefined ? !this.showStats : args[0] === '1'; + return this.showStats ? '1' : '0'; + }, + 'args: [ 0|1? ]; Turn on/off stats panel, skip argument to toggle value', + ); + this.registerConsoleCommand( + null, + 'debug_panel', + async (...args: string[]) => { + this.showDebugControls = args[0] === undefined ? !this.showDebugControls : args[0] === '1'; + return this.showDebugControls ? '1' : '0'; + }, + 'args: [ 0|1? ]; Turn on/off debug panel, skip argument to toggle value', + ); + let unbindKey$: Subject = new Subject(); + this.registerConsoleCommand( + null, + 'bind_key', + async (keyCode: string, command: string, ...args: string[]) => { + fromEvent(window, 'keydown') + .pipe( + takeUntil(unbindKey$.pipe(filter(x => x === keyCode))), + filter(e => { + if ((e as KeyboardEvent).code !== keyCode) { + return false; + } + if (document.activeElement) { + for (const k of KeyboardInput.externalFocusBlacklist) { + if (document.activeElement instanceof k) { + return false; + } + } + } + return true; + }), + ) + .subscribe(() => { + this.runConsoleCommand(command, args); + }); + return 'Ok'; }, - 'args: [0 or 1]; turn on/off debug panel. Default value is 0', + 'args: [ string, ...string ]; Bind a keyboard key by code to console command. Check key codes' + + ' here. Use "unbind_key" command to unbind it', ); this.registerConsoleCommand( null, - 'show_stats', + 'unbind_key', async (...args: string[]) => { - this.showStats = ['1', 'true', '+'].includes(args[0]); - return '' + this.showStats; + unbindKey$.next(args[0]); + return 'Ok'; }, - 'args: [0 or 1]; turn on/off stats. Default value is 0', + 'args: [ string ]; Unbind a keyboard key from console command', ); (window as any).ggstatic = this; this.autoAssignSelectedWorld();