From 54c6327ac7479031b906b0a21859f3f76cd41eaf Mon Sep 17 00:00:00 2001 From: Armel van Ravels Date: Mon, 7 Sep 2026 21:22:08 +0200 Subject: [PATCH] Joystick for mobile --- resources/css/game.css | 24 +++++++++ resources/js/engine/Input.js | 92 +++++++++++++++++++++++++++++++++++ resources/js/main.js | 5 +- resources/views/app.blade.php | 2 +- 4 files changed, 120 insertions(+), 3 deletions(-) diff --git a/resources/css/game.css b/resources/css/game.css index 91253ed..2d3cd86 100644 --- a/resources/css/game.css +++ b/resources/css/game.css @@ -9,6 +9,30 @@ html, body { width: 100vw; height: 100vh; background: #000; + touch-action: none; +} +.joystick-base { + position: fixed; + width: 120px; + height: 120px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.12); + border: 2px solid rgba(255, 255, 255, 0.25); + pointer-events: none; + display: none; + z-index: 5; + transform: translate(-50%, -50%); +} +.joystick-thumb { + position: fixed; + width: 44px; + height: 44px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.45); + pointer-events: none; + display: none; + z-index: 6; + transform: translate(-50%, -50%); } #fullscreen { position: fixed; diff --git a/resources/js/engine/Input.js b/resources/js/engine/Input.js index 429bde7..80b74a7 100644 --- a/resources/js/engine/Input.js +++ b/resources/js/engine/Input.js @@ -7,6 +7,12 @@ export class Input { this.mouse = new Vector2D(viewport.width / 2, viewport.height / 2); this.mouseDown = false; this.mouseClicked = false; + this.viewport = viewport; + this.isMobile = 'ontouchstart' in window || navigator.maxTouchPoints > 0; + this.joystickActive = false; + this.joystickTouchId = null; + this.joystickBasePos = { x: 0, y: 0 }; + this.joystickRadius = 60; window.addEventListener('keydown', (event) => { this.keys.add(event.key.toLowerCase()); @@ -47,6 +53,92 @@ export class Input { window.addEventListener('mouseup', () => { this.mouseDown = false; }); + + if (this.isMobile) { + this._initTouchJoystick(canvas); + } + } + + _initTouchJoystick(canvas) { + this.joystickBaseEl = document.createElement('div'); + this.joystickBaseEl.className = 'joystick-base'; + this.joystickBaseEl.style.display = 'none'; + + this.joystickThumbEl = document.createElement('div'); + this.joystickThumbEl.className = 'joystick-thumb'; + this.joystickThumbEl.style.display = 'none'; + + document.body.appendChild(this.joystickBaseEl); + document.body.appendChild(this.joystickThumbEl); + + canvas.addEventListener('touchstart', (event) => this._handleTouchStart(event), { passive: false }); + canvas.addEventListener('touchmove', (event) => this._handleTouchMove(event), { passive: false }); + canvas.addEventListener('touchend', (event) => this._handleTouchEnd(event), { passive: false }); + canvas.addEventListener('touchcancel', (event) => this._handleTouchEnd(event), { passive: false }); + } + + _handleTouchStart(event) { + event.preventDefault(); + for (const touch of event.changedTouches) { + const x = touch.clientX; + if (x < this.viewport.width / 2 && !this.joystickActive) { + this.joystickActive = true; + this.joystickTouchId = touch.identifier; + this.joystickBasePos.x = x; + this.joystickBasePos.y = touch.clientY; + + this.joystickBaseEl.style.left = `${x}px`; + this.joystickBaseEl.style.top = `${this.joystickBasePos.y}px`; + this.joystickBaseEl.style.display = 'block'; + this.joystickThumbEl.style.left = `${x}px`; + this.joystickThumbEl.style.top = `${this.joystickBasePos.y}px`; + this.joystickThumbEl.style.display = 'block'; + } + } + } + + _handleTouchMove(event) { + event.preventDefault(); + for (const touch of event.changedTouches) { + if (touch.identifier !== this.joystickTouchId) continue; + const dx = touch.clientX - this.joystickBasePos.x; + const dy = touch.clientY - this.joystickBasePos.y; + const dist = Math.sqrt(dx * dx + dy * dy); + let clampedDx = dx; + let clampedDy = dy; + if (dist > this.joystickRadius) { + clampedDx = (dx / dist) * this.joystickRadius; + clampedDy = (dy / dist) * this.joystickRadius; + } + + this.joystickThumbEl.style.left = `${this.joystickBasePos.x + clampedDx}px`; + this.joystickThumbEl.style.top = `${this.joystickBasePos.y + clampedDy}px`; + + const deadZone = 12; + this.keys.delete('w'); + this.keys.delete('a'); + this.keys.delete('s'); + this.keys.delete('d'); + if (dy < -deadZone) this.keys.add('w'); + if (dy > deadZone) this.keys.add('s'); + if (dx < -deadZone) this.keys.add('a'); + if (dx > deadZone) this.keys.add('d'); + } + } + + _handleTouchEnd(event) { + for (const touch of event.changedTouches) { + if (touch.identifier === this.joystickTouchId) { + this.joystickActive = false; + this.joystickTouchId = null; + this.keys.delete('w'); + this.keys.delete('a'); + this.keys.delete('s'); + this.keys.delete('d'); + this.joystickBaseEl.style.display = 'none'; + this.joystickThumbEl.style.display = 'none'; + } + } } clearPressed() { diff --git a/resources/js/main.js b/resources/js/main.js index 278b2d6..f4c73d1 100644 --- a/resources/js/main.js +++ b/resources/js/main.js @@ -46,6 +46,7 @@ window.addEventListener('orientationchange', resizeToWindow); resizeToWindow(); const input = new Input(canvas, viewport); +const isMobile = input.isMobile; const fullscreenButton = document.getElementById('fullscreen'); const updateFullscreenLabel = () => { @@ -86,7 +87,7 @@ const updateCamera = (cx, cy) => { camera.y = Math.min(Math.max(0, targetY), Math.max(0, WORLD_H - viewport.height)); }; -let autoAim = false; +let autoAim = isMobile; let muted = false; let autoAimNotice = 0; let muteNotice = 0; @@ -977,7 +978,7 @@ const gameLoop = (now) => { muted = sounds.toggleMuted(); muteNotice = 1.2; } - if (input.pressed.has('q')) { + if (!isMobile && input.pressed.has('q')) { input.clearPressed(); autoAim = !autoAim; autoAimNotice = 1.2; diff --git a/resources/views/app.blade.php b/resources/views/app.blade.php index 1b4de9d..16fbb23 100644 --- a/resources/views/app.blade.php +++ b/resources/views/app.blade.php @@ -2,7 +2,7 @@ - + Zombies @vite(['resources/css/game.css', 'resources/js/main.js'])