From e079e94ab45b3c689c470a6ec4c854068328059c Mon Sep 17 00:00:00 2001 From: malted Date: Wed, 17 Aug 2022 01:22:50 +0100 Subject: [PATCH 1/4] :sparkles: Start new input handling implementation --- package-lock.json | 4 +-- package.json | 68 ++++++++++++++++++------------------- src/controllerUtils.js | 46 +++++++++++++++++++++++++ src/index.js | 76 ++++++++++++++---------------------------- src/playerUtils.js | 30 ++++++++--------- 5 files changed, 122 insertions(+), 102 deletions(-) create mode 100644 src/controllerUtils.js diff --git a/package-lock.json b/package-lock.json index b40421b..dda4c75 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "charactercontroller", - "version": "1.0.2", + "version": "2.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "charactercontroller", - "version": "1.0.2", + "version": "2.1.0", "license": "MIT", "dependencies": { "three": "^0.141.0" diff --git a/package.json b/package.json index 221de5d..1639a16 100644 --- a/package.json +++ b/package.json @@ -1,36 +1,36 @@ { - "name": "charactercontroller", - "version": "2.1.0", - "description": "A first person character controller for the Three.js graphics library", - "main": "src/index.js", - "scripts": { - "lint": "eslint src/index.js", - "format": "prettier --write src/index.js" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/ma1ted/charactercontroller.git" - }, - "keywords": [ - "threejs", - "fps", - "controller" - ], - "author": "malted", - "license": "MIT", - "bugs": { - "url": "https://github.com/ma1ted/charactercontroller/issues" - }, - "homepage": "https://github.com/ma1ted/charactercontroller#readme", - "devDependencies": { - "eslint": "^8.16.0", - "eslint-config-prettier": "^8.5.0", - "prettier": "^2.6.2" - }, - "dependencies": { - "three": "^0.141.0" - }, - "publishConfig": { - "registry": "https://registry-url" - } + "name": "charactercontroller", + "version": "2.1.0", + "description": "A first person character controller for the Three.js graphics library", + "main": "src/index.js", + "scripts": { + "lint": "eslint src", + "format": "prettier --write src" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/ma1ted/charactercontroller.git" + }, + "keywords": [ + "threejs", + "fps", + "controller" + ], + "author": "malted", + "license": "MIT", + "bugs": { + "url": "https://github.com/ma1ted/charactercontroller/issues" + }, + "homepage": "https://github.com/ma1ted/charactercontroller#readme", + "devDependencies": { + "eslint": "^8.16.0", + "eslint-config-prettier": "^8.5.0", + "prettier": "^2.6.2" + }, + "dependencies": { + "three": "^0.141.0" + }, + "publishConfig": { + "registry": "https://registry-url" + } } diff --git a/src/controllerUtils.js b/src/controllerUtils.js new file mode 100644 index 0000000..2ef6073 --- /dev/null +++ b/src/controllerUtils.js @@ -0,0 +1,46 @@ +export function registerKeyEvents() { + document.addEventListener("keydown", (e) => { + this.keysDown[e.code] = true; + }); + document.addEventListener("keyup", (e) => { + this.keysDown[e.code] = false; + }); +} + +export function registerMouseMoveEvent() { + window.addEventListener("mousemove", (e) => { + clearTimeout(this.cancelDriftTimeout); + this.mouse.x = e.movementX; + this.mouse.y = e.movementY; + this.cancelDriftTimeout = setTimeout(() => { + this.mouse.x = this.mouse.y = 0; + }, 10); + }); +} + +export function checkInputs(inputMappings, keysDown) { + const inputs = {}; + + // Check scalar values + Object.entries(inputMappings.scalar).forEach(([axisName, axis]) => { + axis.forEach((axisInput) => { + inputs[axisName] = 0; + if (axisInput.inputs.some((input) => keysDown.includes(input))) { + inputs[axisName] += axisInput.value; + } + }); + }); + + // Check discrete values + Object.entries(inputMappings.discrete).forEach(([inputName, inputEvent]) => { + inputEvent.forEach((axisInput) => { + if (axisInput.inputs.some((inputCode) => keysDown.includes(inputCode))) { + inputs[inputName] = true; + } else { + inputs[inputName] = false; + } + }); + }); + + return inputs; +} diff --git a/src/index.js b/src/index.js index fbfc638..a3453f4 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,6 @@ import { Group, Clock, PerspectiveCamera, Raycaster, Vector3, MathUtils } from "three"; import * as PlayerUtils from "./playerUtils.js"; +import * as ControllerUtils from "./controllerUtils.js"; export default class CharacterController { constructor( @@ -19,13 +20,21 @@ export default class CharacterController { up: 180, }, cameraFov = 75, - inputs = { - forwards: ["KeyW", "ArrowUp"], - backwards: ["KeyS", "ArrowDown"], - left: ["KeyA", "ArrowLeft"], - right: ["KeyD", "ArrowRight"], - jump: ["Space"], - sprint: ["ShiftLeft", "ShiftRight"], + inputMappings = { + scalar: { + horizontalAxis: [ + { inputs: ["KeyA", "ArrowLeft"], value: -1 }, + { inputs: ["KeyD", "ArrowRight"], value: 1 }, + ], + verticalAxis: [ + { inputs: ["KeyS", "ArrowDown"], value: -1 }, + { inputs: ["KeyW", "ArrowUp"], value: 1 }, + ], + }, + discrete: { + jump: ["Space"], + sprint: ["ShiftLeft", "ShiftRight"], + }, }, } ) { @@ -39,7 +48,7 @@ export default class CharacterController { this.sensitivity = sensitivity; this.lookLimit = lookLimit; this.cameraFov = cameraFov; - this.inputs = inputs; + this.inputMappings = inputMappings; this.player = new Group(); this.clock = new Clock(); @@ -52,7 +61,11 @@ export default class CharacterController { this.camera.rotation.x = MathUtils.degToRad(90); this.player.add(this.camera); + // A raw list of all the keys currently pressed. Internal use only. this.keysDown = {}; + + // A processed list of inputs corresponding to the input mappings. + this.inputs = {}; this.mouse = { x: 0, y: 0 }; this.isGrounded; @@ -65,58 +78,19 @@ export default class CharacterController { this.floorDistance ); - document.addEventListener("keydown", (e) => { - this.keysDown[e.code] = true; - }); - document.addEventListener("keyup", (e) => { - this.keysDown[e.code] = false; - }); - this.cancelDriftTimeout; - window.addEventListener("mousemove", (e) => { - clearTimeout(this.cancelDriftTimeout); - this.mouse.x = e.movementX; - this.mouse.y = e.movementY; - this.cancelDriftTimeout = setTimeout(() => { - this.mouse.x = this.mouse.y = 0; - }, 10); - }); + ControllerUtils.registerMouseMoveEvent.call(this); - this.clock.start(); - } + ControllerUtils.registerKeyEvents.call(this); - get horizontalAxis() { - let res = 0; - this.inputs.left.forEach((key) => { - if (this.keysDown[key]) res = -1; - }); - this.inputs.right.forEach((key) => { - if (this.keysDown[key]) res = 1; - }); - return res; - } - get verticalAxis() { - let res = 0; - this.inputs.forwards.forEach((key) => { - if (this.keysDown[key]) res = 1; - }); - this.inputs.backwards.forEach((key) => { - if (this.keysDown[key]) res = -1; - }); - return res; - } - get sprintKeyPressed() { - let res = false; - this.inputs.sprint.forEach((key) => { - if (this.keysDown[key]) res = true; - }); - return res; + this.clock.start(); } update() { const clock = this.clock; const elapsed = this.clock.elapsedTime; const delta = clock.getDelta(); + this.inputs = ControllerUtils.checkInputs(this.inputMappings, this.keysDown); // Cast a ray straight down from the player's position. this.raycaster.set(this.player.position, new Vector3(0, 0, -1)); diff --git a/src/playerUtils.js b/src/playerUtils.js index 15ee460..4bc519b 100644 --- a/src/playerUtils.js +++ b/src/playerUtils.js @@ -1,27 +1,27 @@ import { MathUtils } from "three"; function playerMove(delta) { - // Move the player - const speed = this.sprintKeyPressed ? this.sprintSpeed : this.walkSpeed; - this.player.translateX(this.horizontalAxis * speed * delta); - this.player.translateY(this.verticalAxis * speed * delta); + // Move the player + const speed = this.sprintKeyPressed ? this.sprintSpeed : this.walkSpeed; + this.player.translateX(this.horizontalAxis * speed * delta); + this.player.translateY(this.verticalAxis * speed * delta); } function playerLook(delta) { - // Rotate the player left and right - this.player.rotation.z += -this.mouse.x * delta * this.sensitivity.x; + // Rotate the player left and right + this.player.rotation.z += -this.mouse.x * delta * this.sensitivity.x; - // Rotate the camera up and down - this.player.children[0].rotation.x -= this.mouse.y * delta * this.sensitivity.y; + // Rotate the camera up and down + this.player.children[0].rotation.x -= this.mouse.y * delta * this.sensitivity.y; } function playerClamp() { - // Clamp the up and down camera movement - if (this.camera.rotation.x < MathUtils.degToRad(this.lookLimit.down)) { - this.camera.rotation.x = MathUtils.degToRad(this.lookLimit.down); - } else if (this.camera.rotation.x > MathUtils.degToRad(this.lookLimit.up)) { - this.camera.rotation.x = MathUtils.degToRad(this.lookLimit.up); - } + // Clamp the up and down camera movement + if (this.camera.rotation.x < MathUtils.degToRad(this.lookLimit.down)) { + this.camera.rotation.x = MathUtils.degToRad(this.lookLimit.down); + } else if (this.camera.rotation.x > MathUtils.degToRad(this.lookLimit.up)) { + this.camera.rotation.x = MathUtils.degToRad(this.lookLimit.up); + } } -export { playerClamp, playerLook, playerMove } \ No newline at end of file +export { playerClamp, playerLook, playerMove }; From 69d972a24499ef82225a06d96c750f40c033a02e Mon Sep 17 00:00:00 2001 From: malted Date: Wed, 17 Aug 2022 02:09:00 +0100 Subject: [PATCH 2/4] =?UTF-8?q?=E2=9C=A8=20Finish=20new=20input=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllerUtils.js | 46 ------------------------------------------ src/index.js | 10 +++++---- src/inputUtils.js | 36 +++++++++++++++++++++++++++++++++ src/playerUtils.js | 6 +++--- 4 files changed, 45 insertions(+), 53 deletions(-) delete mode 100644 src/controllerUtils.js create mode 100644 src/inputUtils.js diff --git a/src/controllerUtils.js b/src/controllerUtils.js deleted file mode 100644 index 2ef6073..0000000 --- a/src/controllerUtils.js +++ /dev/null @@ -1,46 +0,0 @@ -export function registerKeyEvents() { - document.addEventListener("keydown", (e) => { - this.keysDown[e.code] = true; - }); - document.addEventListener("keyup", (e) => { - this.keysDown[e.code] = false; - }); -} - -export function registerMouseMoveEvent() { - window.addEventListener("mousemove", (e) => { - clearTimeout(this.cancelDriftTimeout); - this.mouse.x = e.movementX; - this.mouse.y = e.movementY; - this.cancelDriftTimeout = setTimeout(() => { - this.mouse.x = this.mouse.y = 0; - }, 10); - }); -} - -export function checkInputs(inputMappings, keysDown) { - const inputs = {}; - - // Check scalar values - Object.entries(inputMappings.scalar).forEach(([axisName, axis]) => { - axis.forEach((axisInput) => { - inputs[axisName] = 0; - if (axisInput.inputs.some((input) => keysDown.includes(input))) { - inputs[axisName] += axisInput.value; - } - }); - }); - - // Check discrete values - Object.entries(inputMappings.discrete).forEach(([inputName, inputEvent]) => { - inputEvent.forEach((axisInput) => { - if (axisInput.inputs.some((inputCode) => keysDown.includes(inputCode))) { - inputs[inputName] = true; - } else { - inputs[inputName] = false; - } - }); - }); - - return inputs; -} diff --git a/src/index.js b/src/index.js index a3453f4..403a1fa 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,6 @@ import { Group, Clock, PerspectiveCamera, Raycaster, Vector3, MathUtils } from "three"; import * as PlayerUtils from "./playerUtils.js"; -import * as ControllerUtils from "./controllerUtils.js"; +import * as InputUtils from "./inputUtils.js"; export default class CharacterController { constructor( @@ -79,9 +79,9 @@ export default class CharacterController { ); this.cancelDriftTimeout; - ControllerUtils.registerMouseMoveEvent.call(this); + InputUtils.registerMouseMoveEvent.call(this); - ControllerUtils.registerKeyEvents.call(this); + InputUtils.registerKeyEvents.call(this); this.clock.start(); } @@ -90,7 +90,9 @@ export default class CharacterController { const clock = this.clock; const elapsed = this.clock.elapsedTime; const delta = clock.getDelta(); - this.inputs = ControllerUtils.checkInputs(this.inputMappings, this.keysDown); + + // Update the player's currently activated inputs for this frame. + InputUtils.checkInputs.call(this); // Cast a ray straight down from the player's position. this.raycaster.set(this.player.position, new Vector3(0, 0, -1)); diff --git a/src/inputUtils.js b/src/inputUtils.js new file mode 100644 index 0000000..1a3186f --- /dev/null +++ b/src/inputUtils.js @@ -0,0 +1,36 @@ +export function registerKeyEvents() { + document.addEventListener("keydown", (e) => { + this.keysDown[e.code] = true; + }); + document.addEventListener("keyup", (e) => { + this.keysDown[e.code] = false; + }); +} + +export function registerMouseMoveEvent() { + window.addEventListener("mousemove", (e) => { + clearTimeout(this.cancelDriftTimeout); + this.mouse.x = e.movementX; + this.mouse.y = e.movementY; + this.cancelDriftTimeout = setTimeout(() => { + this.mouse.x = this.mouse.y = 0; + }, 10); + }); +} + +export function checkInputs() { + // Check scalar values + Object.entries(this.inputMappings.scalar).forEach(([axisName, axisInfo]) => { + this.inputs[axisName] = 0; + axisInfo.forEach((axisInput) => { + if (axisInput.inputs.some((code) => this.keysDown[code])) { + this.inputs[axisName] += axisInput.value; + } + }); + }); + + // Check discrete values + Object.entries(this.inputMappings.discrete).forEach(([eventName, eventCodes]) => { + this.inputs[eventName] = eventCodes.some((code) => this.keysDown[code]); + }); +} diff --git a/src/playerUtils.js b/src/playerUtils.js index 4bc519b..b01de5a 100644 --- a/src/playerUtils.js +++ b/src/playerUtils.js @@ -2,9 +2,9 @@ import { MathUtils } from "three"; function playerMove(delta) { // Move the player - const speed = this.sprintKeyPressed ? this.sprintSpeed : this.walkSpeed; - this.player.translateX(this.horizontalAxis * speed * delta); - this.player.translateY(this.verticalAxis * speed * delta); + const speed = this.inputs.sprint ? this.sprintSpeed : this.walkSpeed; + this.player.translateX(this.inputs.horizontalAxis * speed * delta); + this.player.translateY(this.inputs.verticalAxis * speed * delta); } function playerLook(delta) { From 9eafc847196b632e8c47aaeb6252e724e8998d30 Mon Sep 17 00:00:00 2001 From: malted Date: Wed, 17 Aug 2022 02:11:53 +0100 Subject: [PATCH 3/4] :bookmark: Bump to version 3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1639a16..d78acf1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "charactercontroller", - "version": "2.1.0", + "version": "3.0.0", "description": "A first person character controller for the Three.js graphics library", "main": "src/index.js", "scripts": { From 5fb88679a6cb9038f15eafbe9cdcd995bad9408c Mon Sep 17 00:00:00 2001 From: malted Date: Wed, 17 Aug 2022 02:31:29 +0100 Subject: [PATCH 4/4] :memo: Insert placeholder version 3 schema change notice --- README.md | 107 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 59 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index 140b983..7b98878 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # charactercontroller + A first person character controller for the Three.js graphics library ![GitHub Lint Workflow Status](https://img.shields.io/github/workflow/status/ma1ted/charactercontroller/CI?label=Lint) @@ -11,9 +12,11 @@ A first person character controller for the Three.js graphics library ## [Demo](https://controller.malted.dev) ## Installation + `npm install charactercontroller` ## Usage + ```javascript import CharacterController from "charactercontroller"; @@ -22,64 +25,72 @@ import CharacterController from "charactercontroller"; const controller = new CharacterController(scene, options); function animate() { - requestAnimationFrame(animate); - // ... - controller.update(); - renderer.render(scene, controller.camera); + requestAnimationFrame(animate); + // ... + controller.update(); + renderer.render(scene, controller.camera); } ``` -* `scene` - + An instance of `THREE.Scene` that the Character Controller is to become a child of. -* `options` - + An object defining options for the Character Controller. The valid fields are described below +- `scene` + + - An instance of `THREE.Scene` that the Character Controller is to become a child of. + +- `options` + - An object defining options for the Character Controller. The valid fields are described below ## Constructor Options -* `walkSpeed` - + The rate at which the controller is translated in the scene in response to player inputs, when the sprint key (left shift) **is not** being pressed. - + Default: `5` -* `sprintSpeed` - + The rate at which the controller is translated in the scene in response to player inputs, when the sprint key (left shift) **is** being pressed. - + Default: `10` -* `floorDistance` - + How far above a surface the controller can get before stopping falling. - + Could be interpreted as the height of the player. - + Default: `1` -* `gravity` - + How quickly the controller is pulled down when there is no surface beneath it. - + Default: `-9.81` -* `jumpPower` - + With how much force the controller is projected upwards when a jump is initiated. - + Default: `5` -* `sensitivity` - + `x` + +- `walkSpeed` + - The rate at which the controller is translated in the scene in response to player inputs, when the sprint key (left shift) **is not** being pressed. + - Default: `5` +- `sprintSpeed` + - The rate at which the controller is translated in the scene in response to player inputs, when the sprint key (left shift) **is** being pressed. + - Default: `10` +- `floorDistance` + - How far above a surface the controller can get before stopping falling. + - Could be interpreted as the height of the player. + - Default: `1` +- `gravity` + - How quickly the controller is pulled down when there is no surface beneath it. + - Default: `-9.81` +- `jumpPower` + - With how much force the controller is projected upwards when a jump is initiated. + - Default: `5` +- `sensitivity` + - `x` - How much the camera should move in response to the player moving the mouse left and right. - Default: `0.1` - + `y` + - `y` - How much the camera should move in response to the player moving the mouse up and down. - Default: `0.1` -* `lookLimit` - + `down` +- `lookLimit` + - `down` - The angle in degrees that the camera's `x` rotation should be clamped to when looking down - Default: `0` - + `up` + - `up` - The angle in degrees that the camera's `x` rotation should be clamped to when looking up - Default: `180` -* `cameraFov` - + The field of view of the camera attatched to the character controller. - + Default: `75` -* `inputs` - + The `KeyboardEvent.code`s that control the character controller. An array of `code`s are used to support multiple keys controlling the same actions; primarily for accessability reasons. - + `forwards` - - Default: [`KeyW`, `ArrowUp`] - + `backwards` - - Default: [`KeyS`, `ArrowDown`] - + `left` - - Default: [`KeyA`, `ArrowLeft`] - + `right` - - Default: [`KeyD`, `ArrowRight`] - + `jump` - - Default: [`Space`] - + `sprint` - - Default: [`ShiftLeft`, `ShiftRight`] - +- `cameraFov` + - The field of view of the camera attatched to the character controller. + - Default: `75` +- `inputMappings` + + - The input mapping schema has changed in version 3. The docs are currently being rewitten to reflect this. In the meantime, look at the `inputMappings` property in `/src/index.js` for the new schema. + +