From aaa8318c84b53c9ec0074ec2d01eae782fdb00de Mon Sep 17 00:00:00 2001 From: Shaun O'Connell Date: Mon, 19 Feb 2024 14:32:15 +1300 Subject: [PATCH] Capture more than one key at a time, and use the gameloop to increment each input --- README.md | 2 +- assets/css/wc/lander-vehicle.css | 6 +- assets/mjs/model.mjs | 45 ++++++--- assets/mjs/wc/game-controls.mjs | 67 ------------- assets/mjs/wc/game-engine.mjs | 157 +++++++++++++++++++------------ assets/mjs/wc/lander-vehicle.mjs | 19 ---- 6 files changed, 130 insertions(+), 166 deletions(-) diff --git a/README.md b/README.md index 9f18663..cca8247 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ I'm fascinated by the prospect of a little lander working its way to the surface of an alien world. -This project is a Web-based (and overly) simulation of a craft trying to successfully softly land on the surface of a moon or planet. +This project is a Web-based (and overly) simple simulation of a craft trying to successfully softly land on the surface of a moon or planet. I wanted to use this project as a vehicle for learning more about Web Components, ES Modules, Event-based communication, and using PWA technologies. [Check out the work in progress at `ndorfin.github.io/moon-lander`](https://ndorfin.github.io/moon-lander/) diff --git a/assets/css/wc/lander-vehicle.css b/assets/css/wc/lander-vehicle.css index ddde64a..793b28b 100644 --- a/assets/css/wc/lander-vehicle.css +++ b/assets/css/wc/lander-vehicle.css @@ -7,7 +7,7 @@ lander-vehicle { block-size: calc(4 * var(--unit_root)); transform-origin: 50% 50%; transition-property: transform, inset-block-end, inset-inline-start; - transition-duration: 0.35s; + transition-duration: 0.05s; transition-timing-function: linear; transform: translate(-50%, -50%) @@ -18,8 +18,8 @@ lander-vehicle { lander-vehicle .leg { transition-property: transform; - transition-duration: 0.5s; - transition-timing-function: linear; + transition-duration: 2s; + transition-timing-function: ease-in-out; transform: rotate(0deg); } diff --git a/assets/mjs/model.mjs b/assets/mjs/model.mjs index e582704..6900d37 100644 --- a/assets/mjs/model.mjs +++ b/assets/mjs/model.mjs @@ -1,23 +1,21 @@ export const MODEL = { - position_y: { - name: 'Y Position', + position_x: { + name: 'X Position', type: 'integer', formElement: 'range', - initial: 100, + initial: 50, min: 0, - max: 120, + max: 100, affects: 'lander', - eventName: 'LanderStateChanged', }, - position_x: { - name: 'X Position', + position_y: { + name: 'Y Position', type: 'integer', formElement: 'range', - initial: 50, + initial: 60, min: 0, - max: 100, + max: 120, affects: 'lander', - eventName: 'LanderStateChanged', }, rotation: { name: 'Rotation', @@ -27,7 +25,6 @@ export const MODEL = { min: -100, max: 100, affects: 'lander', - eventName: 'LanderStateChanged', }, running: { name: 'Running', @@ -37,7 +34,6 @@ export const MODEL = { labelTrue: 'Running', labelFalse: 'Stopped', affects: 'game', - eventName: 'GameStateChanged', }, speed: { name: 'Speed', @@ -47,7 +43,6 @@ export const MODEL = { min: 0, max: 100, affects: 'lander', - eventName: 'LanderStateChanged', }, thruster: { name: 'Thruster', @@ -57,6 +52,28 @@ export const MODEL = { min: 0, max: 100, affects: 'lander', - eventName: 'LanderStateChanged', + }, +}; + +export const KEYMAP = { + 'ArrowUp': { + affects: 'thruster', + change: 1, + active: false, + }, + 'ArrowDown': { + affects: 'thruster', + change: -1, + active: false, + }, + 'ArrowLeft': { + affects: 'rotation', + change: -1, + active: false, + }, + 'ArrowRight': { + affects: 'rotation', + change: 1, + active: false, }, }; \ No newline at end of file diff --git a/assets/mjs/wc/game-controls.mjs b/assets/mjs/wc/game-controls.mjs index 9ea736f..0bb6bad 100644 --- a/assets/mjs/wc/game-controls.mjs +++ b/assets/mjs/wc/game-controls.mjs @@ -5,11 +5,6 @@ import { dispatchEventWithDetails } from './../events.mjs'; export default class GameControls extends GameElement { #formElements = MODEL; - constructor() { - super(); - this.registerHandlers(); - } - createRangeControl(keyName, modelValue) { let controlId = `rng_${keyName}`; @@ -63,68 +58,6 @@ export default class GameControls extends GameElement { this.dispatchEvent(new Event('FormElementsAdded', { bubbles: true })); } - gameStateChangedEventHandler(event) { - if (event.detail) { - this.querySelector(`[name="running"][value="${event.detail.running}"]`).click(); - } - } - - landerStateChangedEventHandler(event) { - if (event.detail) { - Object.keys(event.detail).forEach((keyName) => { - let currentValue = parseInt(this.querySelector(`[name="${keyName}"]`).value); - this.querySelector(`[name="${keyName}"]`).value = currentValue + event.detail[keyName]; - }); - } - } - - keyboardHandler(event) { - let landerChange = null; - let gameChange = null; - - switch (event.key) { - case 'ArrowUp': - landerChange = { - thruster: 10 - }; - break; - case 'ArrowDown': - landerChange = { - thruster: -10 - }; - break; - case 'ArrowRight': - landerChange = { - rotation: 10 - }; - break; - case 'ArrowLeft': - landerChange = { - rotation: -10 - }; - break; - case 'Escape': - gameChange = { - running: false - }; - break; - case 'Enter': - gameChange = { - running: true - }; - break; - } - - if (landerChange) dispatchEventWithDetails('LanderStateChanged', landerChange); - if (gameChange) dispatchEventWithDetails('GameStateChanged', gameChange); - } - - registerHandlers() { - document.addEventListener('GameStateChanged', this.gameStateChangedEventHandler); - document.addEventListener('LanderStateChanged', this.landerStateChangedEventHandler); - document.addEventListener('keyup', this.keyboardHandler); - } - connectedCallback() { super.connectedCallback(); this.createFormControls(); diff --git a/assets/mjs/wc/game-engine.mjs b/assets/mjs/wc/game-engine.mjs index b672ef6..5989423 100644 --- a/assets/mjs/wc/game-engine.mjs +++ b/assets/mjs/wc/game-engine.mjs @@ -1,107 +1,140 @@ import GameElement from './game-element.mjs'; -import { MODEL } from './../model.mjs'; -import { dispatchEventWithDetails } from './../events.mjs'; +import { MODEL, KEYMAP } from './../model.mjs'; + +const keyboardEventsToWatch = ['keydown', 'keyup']; export default class GameEngine extends GameElement { modelLander = {}; + #gameRunning; #gameStarted; #gameEnded; - #gameEventFrequency = 100; // Milliseconds + #gameEventFrequency = 50; // Milliseconds #gameDuration = 0; // Counts the time elapsed based on multiples of `this.#gameEventFrequency` #gameInterval; // Placeholder for window.setInterval so that it can be cleared later. + #keyMap = KEYMAP; #gameRunningStateChanged = (runningState) => { this.#gameRunning = runningState; - dispatchEventWithDetails('GameStateChange', {running: runningState}); - } - - #landerStateChanged = (values) => { - dispatchEventWithDetails('LanderStateChanged', values); - } - - constructor() { - super(); - - this.handleKeyboardInterrupts = this.handleKeyboardInterrupts.bind(this); - this.setInitialValuesAndStart = this.setInitialValuesAndStart.bind(this); - this.#gameLoop = this.#gameLoop.bind(this); - } - - #gameLoop = function () { - this.#gameDuration = this.#gameDuration + this.#gameEventFrequency; - let newPositionYAdjustment = -1; - - /* - If thruster = 0 - newSpeed = currentSpeed + gravity - If thruster > 0 - newSpeed = currentSpeed - (thruster - gravity) - */ - - this.modelLander.position_y = this.modelLander.position_y + newPositionYAdjustment; - this.#landerStateChanged({position_y: newPositionYAdjustment}); } - startGame() { + #startGame() { this.#gameStarted = true; this.#gameRunningStateChanged(true); + this.#addLanderKeyboardHandlers(); this.#gameDuration = 0; - this.#gameInterval = window.setInterval(this.#gameLoop, this.#gameEventFrequency); - } - - pauseGame() { - this.#gameRunning = false; - this.#gameRunningStateChanged(false); - window.clearInterval(this.#gameInterval); + this.#gameInterval = window.setInterval(this.gameLoop, this.#gameEventFrequency); } - unpauseGame() { + #unpauseGame() { if (!this.#gameRunning) { - this.#gameRunning = true; + this.#addLanderKeyboardHandlers(); this.#gameRunningStateChanged(true); - this.#gameInterval = window.setInterval(this.#gameLoop, this.#gameEventFrequency); + this.#gameInterval = window.setInterval(this.gameLoop, this.#gameEventFrequency); } } - stopGame() { + #pauseGame() { + this.#gameRunningStateChanged(false); + this.#removeLanderKeyboardHandlers(); + window.clearInterval(this.#gameInterval); + } + + #stopGame() { this.#gameEnded = true; console.log('Game ended after duration', this.#gameDuration); - document.removeEventListener('keyup', this.handleKeyboardInterrupts); + this.#removeLanderKeyboardHandlers(); window.clearInterval(this.#gameInterval); } + #addLanderKeyboardHandlers() { + keyboardEventsToWatch.forEach(eventName => { + document.addEventListener(eventName, this.handleLanderKeyboardInupts); + }); + } + + #removeLanderKeyboardHandlers() { + keyboardEventsToWatch.forEach(eventName => { + document.removeEventListener(eventName, this.handleLanderKeyboardInupts); + }); + } + + #updateCustomProperties() { + Object.keys(this.modelLander).forEach(landerProperty => { + let value = this.modelLander[landerProperty]; + this.style.setProperty(`--lander_${landerProperty}`, value); + }) + } + + constructor() { + super(); + + this.handleGameStateKeyboardInupts = this.handleGameStateKeyboardInupts.bind(this); + this.handleLanderKeyboardInupts = this.handleLanderKeyboardInupts.bind(this); + this.setInitialValuesAndStart = this.setInitialValuesAndStart.bind(this); + this.gameLoop = this.gameLoop.bind(this); + } + setInitialValuesAndStart() { Object.keys(MODEL).forEach((keyName) => { let currentItem = MODEL[keyName]; - if (currentItem.affects === 'lander') this.modelLander[keyName] = currentItem.initial; - this.style.setProperty(`--lander_${keyName}`, currentItem.initial); + if (currentItem.affects === 'lander') { + this.modelLander[keyName] = currentItem.initial; + } }); - this.startGame(); + this.#updateCustomProperties(); + this.#startGame(); } - handleKeyboardInterrupts(event) { - switch (event.key) { - case 'Enter': - if (this.#gameStarted && !this.#gameRunning) { - this.unpauseGame(); - } else if (!this.#gameStarted) { - this.startGame(); - } - break; - case 'Escape': - if (this.#gameStarted) { - (this.#gameRunning) ? this.pauseGame() : this.stopGame(); - } - break; + handleGameStateKeyboardInupts(event) { + let keyName = event.key; + + if (keyName == 'Enter') { + if (this.#gameStarted && !this.#gameRunning) { + this.#unpauseGame(); + } else if (!this.#gameStarted) { + this.#startGame(); + } + } else if (keyName == 'Escape' && this.#gameStarted) { + this.#gameRunning ? this.#pauseGame() : this.#stopGame(); } } + handleLanderKeyboardInupts(event) { + let keyName = event.key; + let eventType = event.type; + + if (this.#keyMap[keyName]) { + this.#keyMap[keyName].active = (eventType === 'keydown') ? true : false; + } + } + + gameLoop() { + // Increment game duration counter + this.#gameDuration = this.#gameDuration + this.#gameEventFrequency; + + // Go through the batched inputs and change the lander's position + Object.keys(this.#keyMap).forEach(keyName => { + let keyItem = this.#keyMap[keyName]; + let landerProperty = this.#keyMap[keyName].affects; + if (keyItem.active) { + this.modelLander[landerProperty] = this.modelLander[landerProperty] + keyItem.change; + } + }); + + this.#updateCustomProperties(); + } + connectedCallback() { super.connectedCallback(); this.addEventListener('FormElementsAdded', this.setInitialValuesAndStart, {once: true}); - document.addEventListener('keyup', this.handleKeyboardInterrupts); + document.addEventListener('keydown', this.handleGameStateKeyboardInupts); + } + + disconnectedCallback() { + super.disconnectedCallback(); + document.removeEventListener('keydown', this.handleGameStateKeyboardInupts); } diff --git a/assets/mjs/wc/lander-vehicle.mjs b/assets/mjs/wc/lander-vehicle.mjs index 981de59..5f55dcb 100644 --- a/assets/mjs/wc/lander-vehicle.mjs +++ b/assets/mjs/wc/lander-vehicle.mjs @@ -1,27 +1,8 @@ import GameElement from './game-element.mjs'; export default class LanderVehicle extends GameElement { - - gameEngineElement = document.querySelector('game-engine'); - - changeLanderProperties(event) { - let changes = event.detail; - - Object.keys(changes).forEach((keyName) => { - let newValue = changes[keyName]; - let propertyName = `--lander_${keyName}`; - let currentValue = parseFloat(getComputedStyle(this.gameEngineElement).getPropertyValue(propertyName)); - - - if (currentValue !== null || undefined) { - this.gameEngineElement.style.setProperty(propertyName, currentValue + newValue); - } - }); - } - connectedCallback() { super.connectedCallback(); - document.addEventListener('LanderStateChanged', this.changeLanderProperties.bind(this)); } }