From 760f3d7acb4f5cd7a6e86b893e260cf06831d222 Mon Sep 17 00:00:00 2001 From: Krisztian Somoracz Date: Mon, 30 May 2022 23:32:41 +0200 Subject: [PATCH] New world module: Verlet integration - With this it's possible to link easily 2 objects without any physic library. New world module: Octree car - Simple car physics based on spheres. Breaking Change! Unit controller was renamed to player controller. It was needed since from now it can control anything. Breaking Change! world.onUpdate now is available as world.on.update. Breaking Change! Unit config is not an initial config anymore, from now it's part of the world config. Possible to listen world.on.dispose. Octree car related boilerplates were added. Octree module can handle spheres by default. Possible to create units without animations. Missing dispose methods were added into: region module, collectibles module. Pause related time fixes. Dependency updates. --- package.json | 8 +- .../boilerplates/car-boilerplates.js | 29 ++ .../player-controller-boilerplates.js | 167 +++++++ .../unit-controller-boilerplates.js | 88 ---- .../three-game/modules/module-enums.js | 4 +- .../modules/abilities/abilities-module.js | 2 +- src/js/newkrok/three-game/unit/unit.js | 4 +- src/js/newkrok/three-game/world.js | 33 +- .../collectibles/collectibles-module.js | 18 +- .../modules/octree-car/octree-car-module.js | 425 ++++++++++++++++++ .../world/modules/octree/octree-module.js | 114 ++++- .../player-controller-module.js} | 18 +- .../modules/projectiles/projectiles-module.js | 11 +- .../world/modules/region/region-module.js | 6 + .../verlet-integration-module.js | 58 +++ 15 files changed, 857 insertions(+), 128 deletions(-) create mode 100644 src/js/newkrok/three-game/boilerplates/car-boilerplates.js create mode 100644 src/js/newkrok/three-game/boilerplates/player-controller-boilerplates.js delete mode 100644 src/js/newkrok/three-game/boilerplates/unit-controller-boilerplates.js create mode 100644 src/js/newkrok/three-game/world/modules/octree-car/octree-car-module.js rename src/js/newkrok/three-game/{unit/modules/unit-controller/unit-controller-module.js => world/modules/player-controller/player-controller-module.js} (83%) create mode 100644 src/js/newkrok/three-game/world/modules/verlet-integration/verlet-integration-module.js diff --git a/package.json b/package.json index b4d398a..a0c285f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@newkrok/three-game", - "version": "0.6.1", + "version": "0.7.0", "description": "ThreeJS based game engine", "main": "src/js/newkrok/three-game/world.js", "exports": { @@ -23,9 +23,9 @@ }, "homepage": "https://github.com/NewKrok/three-game#readme", "dependencies": { - "three": "0.140.2", - "@newkrok/three-utils": "0.2.0", - "detect-browser": "5.3.0" + "three": "^0.141.0", + "@newkrok/three-utils": "^0.3.0", + "detect-browser": "^5.3.0" }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1" diff --git a/src/js/newkrok/three-game/boilerplates/car-boilerplates.js b/src/js/newkrok/three-game/boilerplates/car-boilerplates.js new file mode 100644 index 0000000..9fa777e --- /dev/null +++ b/src/js/newkrok/three-game/boilerplates/car-boilerplates.js @@ -0,0 +1,29 @@ +import * as THREE from "three"; + +export const basicCar = { + body: { + model: {}, + width: 1.6, + height: 1.5, + length: 2.72, + offset: new THREE.Vector3(), + }, + wheel: { + radius: 0.4, + mass: 50, + }, + speedReductionByCollision: 0.85, + maxSpeed: 60, + maxReverseSpeed: 20, + accelerationForward: 0.07, + accelerationBackward: 0.04, + breakRatio: 0.99, + damping: 0.998, + friction: 0.999, + minAcceleration: 1, + steeringResetSpeed: 3, + maxSteeringSpeed: 5, + maxWheelAngle: Math.PI / 3, + frontWheelSpeedMultiplier: 1, + rearWheelSpeedMultiplier: 0.75, +}; diff --git a/src/js/newkrok/three-game/boilerplates/player-controller-boilerplates.js b/src/js/newkrok/three-game/boilerplates/player-controller-boilerplates.js new file mode 100644 index 0000000..2daa7e6 --- /dev/null +++ b/src/js/newkrok/three-game/boilerplates/player-controller-boilerplates.js @@ -0,0 +1,167 @@ +import { ButtonKey } from "@newkrok/three-game/src/js/newkrok/three-game/control/gamepad.js"; +import { Key } from "@newkrok/three-game/src/js/newkrok/three-game/control/keyboard-manager.js"; + +export const PlayerActionId = { + PAUSE: "PAUSE", + FORWARD: "FORWARD", + BACKWARD: "BACKWARD", + LEFT: "LEFT", + RIGHT: "RIGHT", + JUMP: "JUMP", + SPRINT: "SPRINT", + TOOL_ACTION: "TOOL_ACTION", + CHOOSE_NEXT_TOOL: "CHOOSE_NEXT_TOOL", + CHOOSE_PREV_TOOL: "CHOOSE_PREV_TOOL", + ...Array.from({ length: 10 }).reduce( + (prev, _, index) => ({ + ...prev, + [`CHOOSE_TOOL_${index}`]: `CHOOSE_TOOL_${index}`, + }), + {} + ), +}; + +export const unitControllerConfig = { + actionConfig: [ + { + actionId: PlayerActionId.PAUSE, + enableDuringPause: true, + keys: [Key.P], + gamepadButtons: [ButtonKey.Options], + }, + { + actionId: PlayerActionId.FORWARD, + listenForDeactivation: true, + keys: [Key.W, Key.ARROW_UP], + gamepadButtons: [ButtonKey.Up], + axis: ButtonKey.LeftAxisY, + axisValidator: (v) => v < -0.1, + axisValueModifier: (v) => v * -1, + }, + { + actionId: PlayerActionId.BACKWARD, + listenForDeactivation: true, + keys: [Key.S, Key.ARROW_DOWN], + gamepadButtons: [ButtonKey.Down], + axis: ButtonKey.LeftAxisY, + axisValidator: (v) => v > 0.1, + }, + { + actionId: PlayerActionId.LEFT, + listenForDeactivation: true, + keys: [Key.A, Key.ARROW_LEFT], + gamepadButtons: [ButtonKey.Left], + axis: ButtonKey.LeftAxisX, + axisValidator: (v) => v < -0.1, + axisValueModifier: (v) => v * -1, + }, + { + actionId: PlayerActionId.RIGHT, + listenForDeactivation: true, + keys: [Key.D, Key.ARROW_RIGHT], + gamepadButtons: [ButtonKey.Right], + axis: ButtonKey.LeftAxisX, + axisValidator: (v) => v > 0.1, + }, + { + actionId: PlayerActionId.JUMP, + keys: [Key.SPACE], + gamepadButtons: [ButtonKey.ActionBottom], + }, + ], + + handlers: [ + { + actionId: PlayerActionId.PAUSE, + callback: ({ world }) => { + if (world.cycleData.isPaused) world.resume(); + else world.pause(); + }, + }, + { + actionId: PlayerActionId.JUMP, + callback: ({ target }) => { + if (target.onGround) target.jump(); + }, + }, + ], +}; + +export const carControllerConfig = { + actionConfig: [ + { + actionId: PlayerActionId.PAUSE, + enableDuringPause: true, + keys: [Key.P], + gamepadButtons: [ButtonKey.Options], + }, + { + actionId: PlayerActionId.FORWARD, + listenForDeactivation: true, + keys: [Key.W, Key.ARROW_UP], + gamepadButtons: [ButtonKey.Up], + axis: ButtonKey.LeftAxisY, + axisValidator: (v) => v < -0.1, + axisValueModifier: (v) => v * -1, + }, + { + actionId: PlayerActionId.BACKWARD, + listenForDeactivation: true, + keys: [Key.S, Key.ARROW_DOWN], + gamepadButtons: [ButtonKey.Down], + axis: ButtonKey.LeftAxisY, + axisValidator: (v) => v > 0.1, + }, + { + actionId: PlayerActionId.LEFT, + listenForDeactivation: true, + keys: [Key.A, Key.ARROW_LEFT], + gamepadButtons: [ButtonKey.Left], + axis: ButtonKey.LeftAxisX, + axisValidator: (v) => v < -0.1, + axisValueModifier: (v) => v * -1, + }, + { + actionId: PlayerActionId.RIGHT, + listenForDeactivation: true, + keys: [Key.D, Key.ARROW_RIGHT], + gamepadButtons: [ButtonKey.Right], + axis: ButtonKey.LeftAxisX, + axisValidator: (v) => v > 0.1, + }, + ], + + handlers: [ + { + actionId: PlayerActionId.PAUSE, + callback: ({ world }) => { + if (world.cycleData.isPaused) world.resume(); + else world.pause(); + }, + }, + { + actionId: PlayerActionId.FORWARD, + callback: ({ target }) => { + target.accelerate(); + }, + }, + { + actionId: PlayerActionId.BACKWARD, + callback: ({ target }) => { + target.reverse(); + }, + }, + { + actionId: PlayerActionId.LEFT, + callback: ({ target }) => { + target.rotateLeft(); + }, + }, + { + actionId: PlayerActionId.RIGHT, + callback: ({ target }) => { + target.rotateRight(); + }, + }, + ], +}; diff --git a/src/js/newkrok/three-game/boilerplates/unit-controller-boilerplates.js b/src/js/newkrok/three-game/boilerplates/unit-controller-boilerplates.js deleted file mode 100644 index 8c2f0ca..0000000 --- a/src/js/newkrok/three-game/boilerplates/unit-controller-boilerplates.js +++ /dev/null @@ -1,88 +0,0 @@ -import { ButtonKey } from "@newkrok/three-game/src/js/newkrok/three-game/control/gamepad.js"; -import { Key } from "@newkrok/three-game/src/js/newkrok/three-game/control/keyboard-manager.js"; - -export const UnitActionId = { - PAUSE: "PAUSE", - FORWARD: "FORWARD", - BACKWARD: "BACKWARD", - LEFT: "LEFT", - RIGHT: "RIGHT", - JUMP: "JUMP", - SPRINT: "SPRINT", - TOOL_ACTION: "TOOL_ACTION", - CHOOSE_NEXT_TOOL: "CHOOSE_NEXT_TOOL", - CHOOSE_PREV_TOOL: "CHOOSE_PREV_TOOL", - ...Array.from({ length: 10 }).reduce( - (prev, _, index) => ({ - ...prev, - [`CHOOSE_TOOL_${index}`]: `CHOOSE_TOOL_${index}`, - }), - {} - ), -}; - -export const unitControllerConfig = { - actionConfig: [ - { - actionId: UnitActionId.PAUSE, - enableDuringPause: true, - keys: [Key.P], - gamepadButtons: [ButtonKey.Options], - }, - { - actionId: UnitActionId.FORWARD, - listenForDeactivation: true, - keys: [Key.W, Key.ARROW_UP], - gamepadButtons: [ButtonKey.Up], - axis: ButtonKey.LeftAxisY, - axisValidator: (v) => v < -0.1, - axisValueModifier: (v) => v * -1, - }, - { - actionId: UnitActionId.BACKWARD, - listenForDeactivation: true, - keys: [Key.S, Key.ARROW_DOWN], - gamepadButtons: [ButtonKey.Down], - axis: ButtonKey.LeftAxisY, - axisValidator: (v) => v > 0.1, - }, - { - actionId: UnitActionId.LEFT, - listenForDeactivation: true, - keys: [Key.A, Key.ARROW_LEFT], - gamepadButtons: [ButtonKey.Left], - axis: ButtonKey.LeftAxisX, - axisValidator: (v) => v < -0.1, - axisValueModifier: (v) => v * -1, - }, - { - actionId: UnitActionId.RIGHT, - listenForDeactivation: true, - keys: [Key.D, Key.ARROW_RIGHT], - gamepadButtons: [ButtonKey.Right], - axis: ButtonKey.LeftAxisX, - axisValidator: (v) => v > 0.1, - }, - { - actionId: UnitActionId.JUMP, - keys: [Key.SPACE], - gamepadButtons: [ButtonKey.ActionBottom], - }, - ], - - handlers: [ - { - actionId: UnitActionId.PAUSE, - callback: ({ world }) => { - if (world.cycleData.isPaused) world.resume(); - else world.pause(); - }, - }, - { - actionId: UnitActionId.JUMP, - callback: ({ unit }) => { - if (unit.onGround) unit.jump(); - }, - }, - ], -}; diff --git a/src/js/newkrok/three-game/modules/module-enums.js b/src/js/newkrok/three-game/modules/module-enums.js index c8e73a7..ff1ff07 100644 --- a/src/js/newkrok/three-game/modules/module-enums.js +++ b/src/js/newkrok/three-game/modules/module-enums.js @@ -3,10 +3,12 @@ export const WorldModuleId = { PROJECTILES: "PROJECTILES", COLLECTIBLE: "COLLECTIBLE", REGION: "REGION", + VERLET_INTEGRATION: "VERLET_INTEGRATION", + OCTREE_CAR: "OCTREE_CAR", + PLAYER_CONTROLLER: "PLAYER_CONTROLLER", }; export const UnitModuleId = { AIMING: "AIMING", - UNIT_CONTROLLER: "UNIT_CONTROLLER", ABILITIES: "ABILITIES", }; diff --git a/src/js/newkrok/three-game/unit/modules/abilities/abilities-module.js b/src/js/newkrok/three-game/unit/modules/abilities/abilities-module.js index c828fd1..dd9a61a 100644 --- a/src/js/newkrok/three-game/unit/modules/abilities/abilities-module.js +++ b/src/js/newkrok/three-game/unit/modules/abilities/abilities-module.js @@ -20,7 +20,7 @@ const create = ({ world, unit, config }) => { (key) => (abilityStates[key].isActive = false) ); const abilityState = abilityStates[ability]; - abilityState.lastActivationTime = Date.now(); + abilityState.lastActivationTime = world.cycleData.now; abilityState.isActive = true; }, deactivate: (ability) => { diff --git a/src/js/newkrok/three-game/unit/unit.js b/src/js/newkrok/three-game/unit/unit.js index 1951cc9..afb037c 100644 --- a/src/js/newkrok/three-game/unit/unit.js +++ b/src/js/newkrok/three-game/unit/unit.js @@ -98,7 +98,9 @@ export const createUnit = ({ world.scene.add( boxHelper ); */ - const mixer = new THREE.AnimationMixer(model); + const mixer = Object.keys(config.animations).length + ? new THREE.AnimationMixer(model) + : null; const addAnimation = (key) => { const anim = getFBXSkeletonAnimation(config.animations[key]); diff --git a/src/js/newkrok/three-game/world.js b/src/js/newkrok/three-game/world.js index b6b2ca2..2c7c40c 100644 --- a/src/js/newkrok/three-game/world.js +++ b/src/js/newkrok/three-game/world.js @@ -25,6 +25,7 @@ const DEFAULT_WORLD_CONFIG = { gltfModels: [], audio: [], }, + unitConfig: [], scene: { background: 0x000000, }, @@ -59,28 +60,25 @@ export const createWorld = ({ target, camera, worldConfig, - unitConfig, unitTickRoutine, }) => { const normalizedWorldConfig = patchObject(DEFAULT_WORLD_CONFIG, worldConfig); - let _onUpdate; + let onUpdateCallbacks = []; const staticModels = []; const units = []; const destroyables = []; const pauseCallbacks = []; const resumeCallbacks = []; + const disposeCallbacks = []; const cycleData = { isPaused: false, now: 0, delta: 0, elapsed: 0, - inactivePauseStartTime: 0, - inactiveTotalPauseTime: 0, - manualPauseStartTime: 0, - manualTotalPauseTime: 0, + totalPauseTime: 0, }; const clock = new THREE.Clock(); const browserInfo = detect(); @@ -120,9 +118,10 @@ export const createWorld = ({ const update = () => { if (!cycleData.isPaused) { const rawDelta = clock.getDelta(); - cycleData.now = Date.now() - cycleData.inactiveTotalPauseTime; + cycleData.now = Date.now() - cycleData.totalPauseTime; cycleData.delta = rawDelta > 0.1 ? 0.1 : rawDelta; - cycleData.elapsed = clock.getElapsedTime(); + cycleData.elapsed = + clock.getElapsedTime() - cycleData.totalPauseTime / 1000; } moduleHandler.update(cycleData); @@ -133,7 +132,7 @@ export const createWorld = ({ }); renderer.render(scene, camera); - _onUpdate && _onUpdate(cycleData); + onUpdateCallbacks.forEach((callback) => callback(cycleData)); }; const animate = () => { @@ -169,7 +168,7 @@ export const createWorld = ({ const promise = new Promise((resolve, reject) => { try { - const { assetsConfig } = normalizedWorldConfig; + const { assetsConfig, unitConfig } = normalizedWorldConfig; const normalizedAssetsConfig = Object.keys(assetsConfig).reduce( (prev, key) => { prev[key] = [ @@ -218,8 +217,8 @@ export const createWorld = ({ ); } renderer.dispose(); + disposeCallbacks.forEach((callback) => callback()); }, - onUpdate: (onUpdate) => (_onUpdate = onUpdate), getUnits: () => units, getUnit: (idOrSelector) => typeof idOrSelector === "function" @@ -231,10 +230,16 @@ export const createWorld = ({ : staticModels.find((model) => model.id === idOrSelector) ).model, on: { + update: (callback) => onUpdateCallbacks.push(callback), pause: (callback) => pauseCallbacks.push(callback), resume: (callback) => resumeCallbacks.push(callback), + dispose: (callback) => disposeCallbacks.push(callback), }, off: { + update: (callback) => + onUpdateCallbacks.filter( + (onUpdateCallbacks) => onUpdateCallbacks !== callback + ), pause: (callback) => (pauseCallbacks = pauseCallbacks.filter( (pauseCallback) => pauseCallback !== callback @@ -243,11 +248,15 @@ export const createWorld = ({ (resumeCallbacks = resumeCallbacks.filter( (resumeCallback) => resumeCallback !== callback )), + dispose: (callback) => + (disposeCallbacks = disposeCallbacks.filter( + (disposeCallback) => disposeCallback !== callback + )), }, userData: {}, }; - moduleHandler.init(world); + moduleHandler.init({ world }); applyConfigToWorld({ world, diff --git a/src/js/newkrok/three-game/world/modules/collectibles/collectibles-module.js b/src/js/newkrok/three-game/world/modules/collectibles/collectibles-module.js index f5a7d46..b69d702 100644 --- a/src/js/newkrok/three-game/world/modules/collectibles/collectibles-module.js +++ b/src/js/newkrok/three-game/world/modules/collectibles/collectibles-module.js @@ -2,7 +2,7 @@ import { WorldModuleId } from "@newkrok/three-game/src/js/newkrok/three-game/mod import { getModel } from "@newkrok/three-game/src/js/newkrok/three-game/helpers/asset-helper.js"; import { getUniqueId } from "@newkrok/three-utils/src/js/newkrok/three-utils/token.js"; -const create = ({ getUnits }) => { +const create = ({ world: { getUnits, cycleData } }) => { let collectibles = []; const activate = (collectible, now) => { @@ -18,13 +18,12 @@ const create = ({ getUnits }) => { const model = getModel(config.model); model.position.copy(config.collisionObject.position); - const now = Date.now(); - const collectible = { id: getUniqueId(), isInited: false, - initialActivationTime: now + config.initialActivationDelay * 1000, - lastActivationTime: now, + initialActivationTime: + cycleData.now + config.initialActivationDelay * 1000, + lastActivationTime: cycleData.now, lastCollectionTime: 0, isCollected: false, config, @@ -39,7 +38,9 @@ const create = ({ getUnits }) => { collectibles = collectibles.filter((element) => element !== collectible); }; - const update = ({ now }) => { + const update = ({ isPaused, now }) => { + if (isPaused) return; + collectibles.forEach((collectible) => { const { isInited, @@ -89,10 +90,15 @@ const create = ({ getUnits }) => { }); }; + const dispose = () => { + collectibles = []; + }; + return { addCollectible, removeCollectible, update, + dispose, }; }; diff --git a/src/js/newkrok/three-game/world/modules/octree-car/octree-car-module.js b/src/js/newkrok/three-game/world/modules/octree-car/octree-car-module.js new file mode 100644 index 0000000..22eefc6 --- /dev/null +++ b/src/js/newkrok/three-game/world/modules/octree-car/octree-car-module.js @@ -0,0 +1,425 @@ +import * as THREE from "three"; + +import { Object3D } from "three"; +import { WorldModuleId } from "@newkrok/three-game/src/js/newkrok/three-game/modules/module-enums.js"; +import { deepDispose } from "@newkrok/three-utils/src/js/newkrok/three-utils/dispose-utils.js"; +import { deepMerge } from "@newkrok/three-utils/src/js/newkrok/three-utils/object-utils.js"; +import { getModel } from "@newkrok/three-game/src/js/newkrok/three-game/helpers/asset-helper.js"; +import { getUniqueId } from "@newkrok/three-utils/src/js/newkrok/three-utils/token.js"; + +const create = ({ world: { scene, getModule }, config: { debug } }) => { + let cars = []; + const lookAtHelper = new THREE.Vector3(); + const speedVectorHelper = new THREE.Vector3(); + + // TODO: handle rotation + const createCar = ({ id, position, config }) => { + const verletIntegration = getModule(WorldModuleId.VERLET_INTEGRATION); + const { createSphere } = getModule(WorldModuleId.OCTREE); + + const { + body: { width, height, length }, + wheel: { radius, mass }, + } = config; + + const frontLeftWheel = createSphere({ + radius, + position: new THREE.Vector3( + position.x + length / 2, + position.y + radius * 2, + position.z + -width / 2 + radius + ), + mass, + }); + const frontRightWheel = createSphere({ + radius, + position: new THREE.Vector3( + position.x + length / 2, + position.y + radius * 2, + position.z + width / 2 - radius + ), + mass, + }); + const rearLeftWheel = createSphere({ + radius, + position: new THREE.Vector3( + position.x + -length / 2, + position.y + radius * 2, + position.z + -width / 2 + radius + ), + mass, + }); + const rearRightWheel = createSphere({ + radius, + position: new THREE.Vector3( + position.x + -length / 2, + position.y + radius * 2, + position.z + width / 2 - radius + ), + mass, + }); + + if (debug) { + [frontLeftWheel, frontRightWheel, rearLeftWheel, rearRightWheel].forEach( + (wheel) => { + wheel.mesh.material.transparent = true; + wheel.mesh.material.opacity = 0.5; + scene.add(wheel.mesh); + } + ); + } + + verletIntegration.createConstraintGroup({ + constraints: [ + { + pointA: frontLeftWheel.collider.center, + pointB: rearLeftWheel.collider.center, + }, + { + pointA: frontRightWheel.collider.center, + pointB: rearRightWheel.collider.center, + }, + { + pointA: frontLeftWheel.collider.center, + pointB: frontRightWheel.collider.center, + }, + { + pointA: rearLeftWheel.collider.center, + pointB: rearRightWheel.collider.center, + }, + { + pointA: frontLeftWheel.collider.center, + pointB: rearRightWheel.collider.center, + }, + { + pointA: frontRightWheel.collider.center, + pointB: rearLeftWheel.collider.center, + }, + ], + config: { + useAutoDistances: true, + }, + }); + + const model = getModel(config.body.model); + scene.add(model); + + // TODO: handle rotation + const box = new THREE.Box3( + new THREE.Vector3( + position.x + -length / 2, + position.y + -height / 2, + position.z + -width / 2 + ), + new THREE.Vector3( + position.x + length / 2, + position.y + height / 2, + position.z + width / 2 + ) + ); + + if (debug) { + var boxHelper = new THREE.Box3Helper(box); + scene.add(boxHelper); + } + + const getWheelMeshFromModel = (keywords) => + model.children.find((child) => { + const name = child.name.toLowerCase(); + return !keywords.some((keyword) => !name.includes(keyword)); + }); + const frontLeftWheelMesh = getWheelMeshFromModel(["front", "left"]); + const frontRightWheelMesh = getWheelMeshFromModel(["front", "right"]); + const rearLeftWheelMesh = getWheelMeshFromModel(["rear", "left"]); + const rearRightWheelMesh = getWheelMeshFromModel(["rear", "right"]); + + const frontLeftWheelContainer = new Object3D(); + frontLeftWheelMesh.parent.add(frontLeftWheelContainer); + frontLeftWheelContainer.add(frontLeftWheelMesh); + frontLeftWheelContainer.position.copy(frontLeftWheelMesh.position); + frontLeftWheelMesh.position.set(0, 0, 0); + + const frontRightWheelContainer = new Object3D(); + frontRightWheelMesh.parent.add(frontRightWheelContainer); + frontRightWheelContainer.add(frontRightWheelMesh); + frontRightWheelContainer.position.copy(frontRightWheelMesh.position); + frontRightWheelMesh.position.set(0, 0, 0); + + const car = { + id: id ?? getUniqueId(), + model, + box, + config, + wheels: { + frontLeftWheel: { + octreeSphere: frontLeftWheel, + mesh: frontLeftWheelContainer, + defaultRotation: frontLeftWheelMesh.rotation.x, + }, + frontRightWheel: { + octreeSphere: frontRightWheel, + mesh: frontRightWheelContainer, + defaultRotation: frontRightWheelMesh.rotation.x, + }, + rearLeftWheel: { + octreeSphere: rearLeftWheel, + mesh: rearLeftWheelMesh, + defaultRotation: rearLeftWheelMesh.rotation.x, + }, + rearRightWheel: { + octreeSphere: rearRightWheel, + mesh: rearRightWheelMesh, + defaultRotation: rearRightWheelMesh.rotation.x, + }, + }, + wheelDirection: 0, + currentSpeed: 0, + realSpeed: 0, + isAccelerating: false, + isReversing: false, + isRotatingToLeft: false, + isRotatingToRight: false, + }; + + frontLeftWheel.on.collision((result) => { + if ( + Math.abs(result.normal.x) > 0.01 || + Math.abs(result.normal.z) > 0.01 + ) { + car.currentSpeed *= config.speedReductionByCollision; + } + }); + frontRightWheel.on.collision((result) => { + if ( + Math.abs(result.normal.x) > 0.01 || + Math.abs(result.normal.z) > 0.01 + ) { + car.currentSpeed *= config.speedReductionByCollision; + } + }); + + const accelerate = () => (car.isAccelerating = !car.isAccelerating); + const reverse = () => (car.isReversing = !car.isReversing); + const rotateLeft = () => (car.isRotatingToLeft = !car.isRotatingToLeft); + const rotateRight = () => (car.isRotatingToRight = !car.isRotatingToRight); + + deepMerge( + car, + { accelerate, reverse, rotateLeft, rotateRight }, + { applyToFirstObject: true } + ); + cars.push(car); + + return car; + }; + + const update = ({ isPaused, delta }) => { + if (isPaused) return; + + if (cars.length) { + cars.forEach((car) => { + const { + isAccelerating, + isReversing, + isRotatingToLeft, + isRotatingToRight, + model, + box, + config, + wheels: { + frontLeftWheel, + frontRightWheel, + rearLeftWheel, + rearRightWheel, + }, + } = car; + + model.position + .set( + frontLeftWheel.octreeSphere.mesh.position.x + + (rearRightWheel.octreeSphere.mesh.position.x - + frontLeftWheel.octreeSphere.mesh.position.x) / + 2, + frontLeftWheel.octreeSphere.mesh.position.y + + (rearRightWheel.octreeSphere.mesh.position.y - + frontLeftWheel.octreeSphere.mesh.position.y) / + 2, + frontLeftWheel.octreeSphere.mesh.position.z + + (rearRightWheel.octreeSphere.mesh.position.z - + frontLeftWheel.octreeSphere.mesh.position.z) / + 2 + ) + .add(config.body.offset); + + if ( + rearLeftWheel.octreeSphere.mesh.position.z < + rearRightWheel.octreeSphere.mesh.position.z + ) { + lookAtHelper.copy(frontLeftWheel.octreeSphere.mesh.position); + lookAtHelper.y = + frontLeftWheel.octreeSphere.mesh.position.y > + frontRightWheel.octreeSphere.mesh.position.y + ? frontLeftWheel.octreeSphere.mesh.position.y + : frontRightWheel.octreeSphere.mesh.position.y; + rearLeftWheel.octreeSphere.mesh.lookAt(lookAtHelper); + model.quaternion.copy(rearLeftWheel.octreeSphere.mesh.quaternion); + } else { + lookAtHelper.copy(frontRightWheel.octreeSphere.mesh.position); + lookAtHelper.y = + frontLeftWheel.octreeSphere.mesh.position.y < + frontRightWheel.octreeSphere.mesh.position.y + ? frontLeftWheel.octreeSphere.mesh.position.y + : frontRightWheel.octreeSphere.mesh.position.y; + rearRightWheel.octreeSphere.mesh.lookAt(lookAtHelper); + model.quaternion.slerp( + rearRightWheel.octreeSphere.mesh.quaternion, + 5 * delta + ); + model.quaternion.copy(rearRightWheel.octreeSphere.mesh.quaternion); + } + + const { + maxSpeed, + maxReverseSpeed, + accelerationForward, + accelerationBackward, + breakRatio, + damping, + friction, + minAcceleration, + steeringResetSpeed, + maxSteeringSpeed, + maxWheelAngle, + frontWheelSpeedMultiplier, + rearWheelSpeedMultiplier, + } = config; + + let currentSpeed = car.currentSpeed; + car.realSpeed = frontLeftWheel.octreeSphere.velocity.length(); + + const currentSteering = + maxSteeringSpeed * Math.log10(1.5 + (1 - currentSpeed / maxSpeed)); + if (isRotatingToLeft) { + car.wheelDirection -= currentSteering * delta; + } else if (isRotatingToRight) { + car.wheelDirection += currentSteering * delta; + } + + if (!isRotatingToLeft && !isRotatingToRight) + car.wheelDirection = THREE.MathUtils.lerp( + car.wheelDirection, + 0, + steeringResetSpeed * delta + ); + + car.wheelDirection = Math.max( + -maxWheelAngle, + Math.min(maxWheelAngle, car.wheelDirection) + ); + + rearLeftWheel.mesh.rotation.x += Math.min( + 0.4, + (currentSpeed * 0.5 * Math.PI) / 180 + ); + rearRightWheel.mesh.rotation.x = rearLeftWheel.mesh.rotation.x; + + frontLeftWheel.mesh.children[0].rotation.x = + rearLeftWheel.mesh.rotation.x; + frontRightWheel.mesh.children[0].rotation.x = + rearRightWheel.mesh.rotation.x; + + frontLeftWheel.mesh.rotation.y = + frontLeftWheel.defaultRotation - car.wheelDirection; + frontRightWheel.mesh.rotation.y = + frontRightWheel.defaultRotation - car.wheelDirection; + + if (isAccelerating || isReversing) { + if (isAccelerating) { + if ( + currentSpeed < minAcceleration && + currentSpeed > -minAcceleration + ) + currentSpeed = minAcceleration; + else if (currentSpeed < 0) currentSpeed *= breakRatio; + else + currentSpeed += + (1 - Math.log(1 + currentSpeed / maxSpeed)) * + accelerationForward; + car.currentSpeed = Math.min(currentSpeed, maxSpeed); + } + + if (isReversing) { + if ( + currentSpeed < minAcceleration && + currentSpeed > -minAcceleration + ) + currentSpeed = -minAcceleration; + else if (currentSpeed > 0) currentSpeed *= breakRatio; + else + currentSpeed -= + (1 - Math.log(1 + currentSpeed / maxReverseSpeed)) * + accelerationBackward; + car.currentSpeed = Math.max(currentSpeed, -maxReverseSpeed); + } + } else { + car.currentSpeed *= damping; + } + + speedVectorHelper.set(0, 0, 0); + frontLeftWheel.mesh.getWorldDirection(speedVectorHelper); + speedVectorHelper.multiplyScalar( + car.currentSpeed * delta * frontWheelSpeedMultiplier + ); + + frontLeftWheel.octreeSphere.velocity.multiplyScalar(friction); + if (frontLeftWheel.octreeSphere.isColliding) + frontLeftWheel.octreeSphere.velocity.add(speedVectorHelper); + frontRightWheel.octreeSphere.velocity.multiplyScalar(friction); + if (frontRightWheel.octreeSphere.isColliding) + frontRightWheel.octreeSphere.velocity.add(speedVectorHelper); + + speedVectorHelper.set(0, 0, 0); + model.getWorldDirection(speedVectorHelper); + speedVectorHelper.multiplyScalar( + car.currentSpeed * delta * rearWheelSpeedMultiplier + ); + + rearLeftWheel.octreeSphere.velocity.multiplyScalar(friction); + if (rearLeftWheel.octreeSphere.isColliding) + rearLeftWheel.octreeSphere.velocity.add(speedVectorHelper); + rearRightWheel.octreeSphere.velocity.multiplyScalar(friction); + if (rearRightWheel.octreeSphere.isColliding) + rearRightWheel.octreeSphere.velocity.add(speedVectorHelper); + + const { width, height, length } = config.body; + box.min.set( + -length / 2 + model.position.x, + 0 + model.position.y, + -width / 2 + model.position.z + ); + box.max.set( + length / 2 + model.position.x, + height + model.position.y, + width / 2 + model.position.z + ); + }); + } + }; + + const dispose = () => { + cars.forEach((car) => deepDispose(car.model)); + cars = []; + }; + + return { + createCar, + update, + dispose, + }; +}; + +export const octreeCarModule = { + id: WorldModuleId.OCTREE_CAR, + create, + config: { debug: false }, +}; diff --git a/src/js/newkrok/three-game/world/modules/octree/octree-module.js b/src/js/newkrok/three-game/world/modules/octree/octree-module.js index 1a002bd..ae706bd 100644 --- a/src/js/newkrok/three-game/world/modules/octree/octree-module.js +++ b/src/js/newkrok/three-game/world/modules/octree/octree-module.js @@ -1,9 +1,119 @@ +import * as THREE from "three"; + import { Octree } from "three/examples/jsm/math/Octree.js"; import { WorldModuleId } from "@newkrok/three-game/src/js/newkrok/three-game/modules/module-enums.js"; +import { getUniqueId } from "@newkrok/three-utils/src/js/newkrok/three-utils/token.js"; -const create = () => { +const create = ({ config: { gravity = 40, mass = 1 } }) => { const worldOctree = new Octree(); - return { worldOctree }; + const normalVectorHelper = new THREE.Vector3(); + const velocity1Helper = new THREE.Vector3(); + const velocity2Helper = new THREE.Vector3(); + let spheres = []; + + const createSphere = ({ id, radius, position, mesh, material }) => { + let sphereMesh = mesh; + if (!sphereMesh) { + const sphereGeometry = new THREE.IcosahedronGeometry(radius, 5); + const sphereMaterial = material ?? new THREE.MeshPhongMaterial(); + sphereMesh = new THREE.Mesh(sphereGeometry, sphereMaterial); + sphereMesh.castShadow = true; + sphereMesh.receiveShadow = true; + } + sphereMesh.position.copy(position); + + const collisionListeners = []; + const sphere = { + id: id ?? getUniqueId(), + mesh: sphereMesh, + collider: new THREE.Sphere( + sphereMesh.position.clone(position), + radius ?? globalRadius + ), + velocity: new THREE.Vector3(), + collisionListeners, + isColliding: false, + on: { + collision: (callback) => collisionListeners.push(callback), + }, + }; + + spheres.push(sphere); + + return sphere; + }; + + const spheresCollisions = () => { + for (let i = 0, length = spheres.length; i < length; i++) { + const sphere1 = spheres[i]; + + for (let j = i + 1; j < length; j++) { + const sphere2 = spheres[j]; + + const distance = sphere1.collider.center.distanceToSquared( + sphere2.collider.center + ); + const r = sphere1.collider.radius + sphere2.collider.radius; + const r2 = r * r; + + if (distance < r2) { + const normal = normalVectorHelper + .subVectors(sphere1.collider.center, sphere2.collider.center) + .normalize(); + const v1 = velocity1Helper + .copy(normal) + .multiplyScalar(normal.dot(sphere1.velocity)); + const v2 = velocity2Helper + .copy(normal) + .multiplyScalar(normal.dot(sphere2.velocity)); + + sphere1.velocity.add(v2).sub(v1); + sphere2.velocity.add(v1).sub(v2); + + const d = (r - Math.sqrt(distance)) / 2; + + sphere1.collider.center.addScaledVector(normal, d); + sphere2.collider.center.addScaledVector(normal, -d); + } + } + } + }; + + const update = ({ isPaused, delta }) => { + if (isPaused) return; + + spheres.forEach((sphere) => { + sphere.collider.center.addScaledVector(sphere.velocity, delta); + + const result = worldOctree.sphereIntersect(sphere.collider); + + if (result) { + sphere.isColliding = true; + sphere.velocity.addScaledVector( + result.normal, + -result.normal.dot(sphere.velocity) * 1.5 + ); + sphere.collider.center.add(result.normal.multiplyScalar(result.depth)); + sphere.collisionListeners.forEach((callback) => callback(result)); + } + sphere.velocity.y -= gravity * mass * delta; + + const damping = Math.exp(-1.5 * delta) - 1; + sphere.velocity.addScaledVector(sphere.velocity, damping); + }); + + spheresCollisions(); + + spheres.forEach(({ mesh, collider }) => + mesh.position.copy(collider.center) + ); + }; + + const dispose = () => { + spheres = []; + }; + + return { worldOctree, update, createSphere, dispose }; }; export const octreeModule = { diff --git a/src/js/newkrok/three-game/unit/modules/unit-controller/unit-controller-module.js b/src/js/newkrok/three-game/world/modules/player-controller/player-controller-module.js similarity index 83% rename from src/js/newkrok/three-game/unit/modules/unit-controller/unit-controller-module.js rename to src/js/newkrok/three-game/world/modules/player-controller/player-controller-module.js index a275c83..606a82a 100644 --- a/src/js/newkrok/three-game/unit/modules/unit-controller/unit-controller-module.js +++ b/src/js/newkrok/three-game/world/modules/player-controller/player-controller-module.js @@ -4,18 +4,23 @@ import { updateGamePad, } from "@newkrok/three-game/src/js/newkrok/three-game/control/gamepad.js"; -import { UnitModuleId } from "@newkrok/three-game/src/js/newkrok/three-game/modules/module-enums.js"; +import { WorldModuleId } from "@newkrok/three-game/src/js/newkrok/three-game/modules/module-enums.js"; import { createKeyboardManager } from "@newkrok/three-game/src/js/newkrok/three-game/control/keyboard-manager.js"; import { createMouseManager } from "@newkrok/three-game/src/js/newkrok/three-game/control/mouse-manager.js"; -const create = ({ world, unit, config: { actionConfig, handlers } }) => { +const create = ({ world, config: { actionConfig, handlers } }) => { let isControlPaused = false; + let target; const trigger = ({ actionId, value }) => { - if (!world.cycleData.isPaused || actionStates[actionId].enableDuringPause) + if ( + (target && !world.cycleData.isPaused) || + actionStates[actionId].enableDuringPause + ) handlers.forEach( (entry) => - entry.actionId === actionId && entry.callback({ unit, value, world }) + entry.actionId === actionId && + entry.callback({ target, value, world }) ); }; @@ -72,6 +77,7 @@ const create = ({ world, unit, config: { actionConfig, handlers } }) => { }; return { + setTarget: (value) => (target = value), update: ({ isPaused }) => { if (isControlPaused) return; updateGamePad(); @@ -93,8 +99,8 @@ const create = ({ world, unit, config: { actionConfig, handlers } }) => { }; }; -export const unitControllerModule = { - id: UnitModuleId.UNIT_CONTROLLER, +export const playerControllerModule = { + id: WorldModuleId.PLAYER_CONTROLLER, create, config: {}, }; diff --git a/src/js/newkrok/three-game/world/modules/projectiles/projectiles-module.js b/src/js/newkrok/three-game/world/modules/projectiles/projectiles-module.js index 4b86e5d..bd127ef 100644 --- a/src/js/newkrok/three-game/world/modules/projectiles/projectiles-module.js +++ b/src/js/newkrok/three-game/world/modules/projectiles/projectiles-module.js @@ -2,7 +2,7 @@ import * as THREE from "three"; import { WorldModuleId } from "@newkrok/three-game/src/js/newkrok/three-game/modules/module-enums.js"; -const create = ({ scene, modules }) => { +const create = ({ world: { scene, getModule, cycleData } }) => { const projectileGeometry = new THREE.SphereGeometry(0.02, 8, 8); const projectileMaterial = new THREE.MeshLambertMaterial({ color: 0x0000ff }); @@ -12,9 +12,7 @@ const create = ({ scene, modules }) => { let worldOctreeCache; const getWorldOctree = () => { if (!worldOctreeCache) - worldOctreeCache = modules.find( - ({ id }) => id === WorldModuleId.OCTREE - ).worldOctree; + worldOctreeCache = getModule(WorldModuleId.OCTREE).worldOctree; return worldOctreeCache; }; @@ -46,9 +44,8 @@ const create = ({ scene, modules }) => { mesh.position.copy(collider.center); }); - const now = Date.now(); projectiles = projectiles.filter(({ mesh, bornTime, config }) => { - const old = now - bornTime > config.lifeTime; + const old = cycleData.now - bornTime > config.lifeTime; const isCollided = projectilesToRemove.includes(mesh); if (old || isCollided) { config?.on?.destroy({ mesh }); @@ -72,7 +69,7 @@ const create = ({ scene, modules }) => { projectiles.push({ id: bulletIndex++, - bornTime: Date.now(), + bornTime: cycleData.now, mesh, collider, direction, diff --git a/src/js/newkrok/three-game/world/modules/region/region-module.js b/src/js/newkrok/three-game/world/modules/region/region-module.js index 4780264..bb1bdd2 100644 --- a/src/js/newkrok/three-game/world/modules/region/region-module.js +++ b/src/js/newkrok/three-game/world/modules/region/region-module.js @@ -138,9 +138,15 @@ const create = ({ config: { debug } }) => { return createListeners(region); }; + const dispose = () => { + regions = []; + collisionData = {}; + }; + return { createRegion, update, + dispose, }; }; diff --git a/src/js/newkrok/three-game/world/modules/verlet-integration/verlet-integration-module.js b/src/js/newkrok/three-game/world/modules/verlet-integration/verlet-integration-module.js new file mode 100644 index 0000000..fdc94d9 --- /dev/null +++ b/src/js/newkrok/three-game/world/modules/verlet-integration/verlet-integration-module.js @@ -0,0 +1,58 @@ +import * as THREE from "three"; + +import { WorldModuleId } from "@newkrok/three-game/src/js/newkrok/three-game/modules/module-enums.js"; + +const create = () => { + let constraintGroups = []; + const diff = new THREE.Vector3(); + + const createConstraintGroup = ({ + constraints, + config: { useAutoDistances }, + }) => { + const group = constraints.map(({ pointA, pointB, distance }) => ({ + pointA, + pointB, + distance: useAutoDistances ? pointA.distanceTo(pointB) : distance ?? 0, + })); + + constraintGroups.push(group); + }; + + const calculateConstrains = ({ + pointA, + pointB, + distance: requestedDistance, + }) => { + diff.subVectors(pointB, pointA); + var distance = diff.length(); + if (distance === 0) return; + var correction = diff.multiplyScalar(1 - requestedDistance / distance); + var correctionHalf = correction.multiplyScalar(0.5); + + pointA.add(correctionHalf); + pointB.sub(correctionHalf); + }; + + const update = () => { + constraintGroups.forEach((group) => { + group.forEach((element) => calculateConstrains(element)); + }); + }; + + const dispose = () => { + constraintGroups = []; + }; + + return { + createConstraintGroup, + update, + dispose, + }; +}; + +export const verletIntegrationModule = { + id: WorldModuleId.VERLET_INTEGRATION, + create, + config: { debug: false }, +};