From 314385188efc9d139be28b20576c94d15349db35 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 24 Nov 2025 22:28:25 +0000 Subject: [PATCH 1/4] Initial plan From 2a18c26d0adfc50f9e2069f2b5be5ebe1391d050 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 24 Nov 2025 22:36:43 +0000 Subject: [PATCH 2/4] Add mobile and desktop support with controls Co-authored-by: mrhegemon <5104160+mrhegemon@users.noreply.github.com> --- client/index.html | 7 +- client/src/game.ts | 283 +++++++++++++++++++++++++++++++++++++++++++-- client/src/menu.ts | 24 +++- 3 files changed, 298 insertions(+), 16 deletions(-) diff --git a/client/index.html b/client/index.html index 57f6d16..1f676b3 100644 --- a/client/index.html +++ b/client/index.html @@ -2,11 +2,14 @@ + + + Colyseus + Babylon.js Demo diff --git a/client/src/game.ts b/client/src/game.ts index 5a6b45d..cdcef84 100644 --- a/client/src/game.ts +++ b/client/src/game.ts @@ -28,10 +28,26 @@ export default class Game { private peers: { [sessionId: string]: RTCPeerConnection } = {}; private videoMeshes: { [sessionId: string]: BABYLON.Mesh } = {}; + // Mobile/Desktop Controls + private isMobile: boolean = false; + private virtualJoystick: GUI.Ellipse | null = null; + private joystickContainer: GUI.Ellipse | null = null; + private joystickActive: boolean = false; + private joystickOffset: BABYLON.Vector2 = BABYLON.Vector2.Zero(); + private keyboardMovement: BABYLON.Vector3 = BABYLON.Vector3.Zero(); + constructor(canvas: HTMLCanvasElement, engine: BABYLON.Engine, room: Room) { this.canvas = canvas; this.engine = engine; this.room = room; + + // Detect mobile device + this.isMobile = this.detectMobileDevice(); + } + + private detectMobileDevice(): boolean { + return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) + || (window.innerWidth <= 768); } initPlayers(): void { @@ -107,10 +123,10 @@ export default class Game { const advancedTexture = GUI.AdvancedDynamicTexture.CreateFullscreenUI("textUI"); const playerInfo = new GUI.TextBlock("playerInfo"); - playerInfo.text = `Room name: ${this.room.name} Player: ${this.room.sessionId}`.toUpperCase(); + playerInfo.text = `Room: ${this.room.name} Player: ${this.room.sessionId}`.toUpperCase(); playerInfo.color = "#eaeaea"; playerInfo.fontFamily = "Roboto"; - playerInfo.fontSize = 20; + playerInfo.fontSize = this.isMobile ? 14 : 20; playerInfo.textHorizontalAlignment = GUI.Control.HORIZONTAL_ALIGNMENT_LEFT; playerInfo.textVerticalAlignment = GUI.Control.VERTICAL_ALIGNMENT_TOP; playerInfo.paddingTop = "10px"; @@ -119,10 +135,14 @@ export default class Game { advancedTexture.addControl(playerInfo); const instructions = new GUI.TextBlock("instructions"); - instructions.text = "CLICK ANYWHERE ON THE GROUND!"; + if (this.isMobile) { + instructions.text = "USE JOYSTICK TO MOVE OR TAP GROUND!"; + } else { + instructions.text = "CLICK GROUND TO MOVE OR USE WASD KEYS!"; + } instructions.color = "#fff000" instructions.fontFamily = "Roboto"; - instructions.fontSize = 24; + instructions.fontSize = this.isMobile ? 16 : 24; instructions.textHorizontalAlignment = GUI.Control.HORIZONTAL_ALIGNMENT_CENTER; instructions.textVerticalAlignment = GUI.Control.VERTICAL_ALIGNMENT_BOTTOM; instructions.paddingBottom = "10px"; @@ -130,9 +150,10 @@ export default class Game { // back to menu button const button = GUI.Button.CreateImageWithCenterTextButton("back", "<- BACK", "./public/btn-default.png"); - button.width = "100px"; - button.height = "50px"; + button.width = this.isMobile ? "80px" : "100px"; + button.height = this.isMobile ? "40px" : "50px"; button.fontFamily = "Roboto"; + button.fontSize = this.isMobile ? 12 : 16; button.thickness = 0; button.color = "#f8f8f8"; button.horizontalAlignment = GUI.Control.HORIZONTAL_ALIGNMENT_RIGHT; @@ -147,6 +168,79 @@ export default class Game { await this.room.leave(true); }); advancedTexture.addControl(button); + + // Add virtual joystick for mobile + if (this.isMobile) { + this.createVirtualJoystick(advancedTexture); + } + } + + private createVirtualJoystick(advancedTexture: GUI.AdvancedDynamicTexture): void { + // Joystick container (outer circle) + this.joystickContainer = new GUI.Ellipse("joystickContainer"); + this.joystickContainer.width = "120px"; + this.joystickContainer.height = "120px"; + this.joystickContainer.color = "white"; + this.joystickContainer.thickness = 4; + this.joystickContainer.background = "rgba(0, 0, 0, 0.3)"; + this.joystickContainer.horizontalAlignment = GUI.Control.HORIZONTAL_ALIGNMENT_LEFT; + this.joystickContainer.verticalAlignment = GUI.Control.VERTICAL_ALIGNMENT_BOTTOM; + this.joystickContainer.left = 60; + this.joystickContainer.top = -60; + advancedTexture.addControl(this.joystickContainer); + + // Joystick thumb (inner circle) + this.virtualJoystick = new GUI.Ellipse("joystickThumb"); + this.virtualJoystick.width = "60px"; + this.virtualJoystick.height = "60px"; + this.virtualJoystick.color = "white"; + this.virtualJoystick.thickness = 3; + this.virtualJoystick.background = "rgba(255, 255, 255, 0.6)"; + this.joystickContainer.addControl(this.virtualJoystick); + + // Joystick touch handling + this.joystickContainer.onPointerDownObservable.add((coords) => { + this.joystickActive = true; + }); + + this.joystickContainer.onPointerUpObservable.add(() => { + this.joystickActive = false; + this.joystickOffset = BABYLON.Vector2.Zero(); + if (this.virtualJoystick) { + this.virtualJoystick.left = 0; + this.virtualJoystick.top = 0; + } + }); + + this.joystickContainer.onPointerMoveObservable.add((coords) => { + if (this.joystickActive && this.virtualJoystick && this.joystickContainer) { + // Get container size + const containerSize = 120; + const maxDistance = 30; // Half of thumb movement range + + // Calculate offset from center + const centerX = parseInt(this.joystickContainer.leftInPixels.toString()) + containerSize / 2; + const centerY = parseInt(this.joystickContainer.topInPixels.toString()) + containerSize / 2; + + let deltaX = coords.x - centerX; + let deltaY = coords.y - centerY; + + // Limit thumb movement + const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); + if (distance > maxDistance) { + deltaX = (deltaX / distance) * maxDistance; + deltaY = (deltaY / distance) * maxDistance; + } + + // Update thumb position + this.virtualJoystick.left = deltaX; + this.virtualJoystick.top = deltaY; + + // Store normalized offset for movement + this.joystickOffset.x = deltaX / maxDistance; + this.joystickOffset.y = deltaY / maxDistance; + } + }); } async bootstrap(): Promise { @@ -172,10 +266,21 @@ export default class Game { this.initSignalHandler(); // 5. Input Logic + this.initInputHandlers(); + + this.doRender(); + } + + private initInputHandlers(): void { + // Mouse/Touch click to move this.scene.onPointerDown = (event, pointer) => { + // Only handle left mouse button or touch if (event.button == 0) { + // Ignore clicks on joystick + if (this.joystickActive) return; + const pickInfo = this.scene.pick(this.scene.pointerX, this.scene.pointerY); - if (pickInfo.hit) { + if (pickInfo.hit && pickInfo.pickedMesh?.name === "plane") { const targetPosition = pickInfo.pickedPoint.clone(); // Position adjustments for the current play ground. @@ -197,7 +302,100 @@ export default class Game { } }; - this.doRender(); + // Desktop keyboard controls (WASD) + if (!this.isMobile) { + this.initKeyboardControls(); + } + } + + private initKeyboardControls(): void { + const inputMap: { [key: string]: boolean } = {}; + + this.scene.actionManager = new BABYLON.ActionManager(this.scene); + + // Key down events + this.scene.actionManager.registerAction( + new BABYLON.ExecuteCodeAction(BABYLON.ActionManager.OnKeyDownTrigger, (evt) => { + inputMap[evt.sourceEvent.key.toLowerCase()] = true; + }) + ); + + // Key up events + this.scene.actionManager.registerAction( + new BABYLON.ExecuteCodeAction(BABYLON.ActionManager.OnKeyUpTrigger, (evt) => { + inputMap[evt.sourceEvent.key.toLowerCase()] = false; + }) + ); + + // Movement processing in render loop + this.scene.onBeforeRenderObservable.add(() => { + const speed = 5; // Movement speed + let hasMovement = false; + + // Calculate movement direction + let forward = 0; + let right = 0; + + if (inputMap["w"] || inputMap["arrowup"]) { + forward = 1; + hasMovement = true; + } + if (inputMap["s"] || inputMap["arrowdown"]) { + forward = -1; + hasMovement = true; + } + if (inputMap["a"] || inputMap["arrowleft"]) { + right = -1; + hasMovement = true; + } + if (inputMap["d"] || inputMap["arrowright"]) { + right = 1; + hasMovement = true; + } + + if (hasMovement) { + const playerMesh = this.playerEntities[this.room.sessionId]; + if (playerMesh) { + // Get camera direction for movement relative to camera + let cameraDirection = BABYLON.Vector3.Zero(); + let cameraRight = BABYLON.Vector3.Zero(); + + if (this.camera && this.camera instanceof BABYLON.ArcRotateCamera) { + const arcCamera = this.camera as BABYLON.ArcRotateCamera; + cameraDirection = arcCamera.target.subtract(arcCamera.position).normalize(); + cameraDirection.y = 0; // Keep movement horizontal + cameraDirection.normalize(); + cameraRight = BABYLON.Vector3.Cross(cameraDirection, BABYLON.Vector3.Up()); + } else { + // Default to world axes if camera is not available + cameraDirection = new BABYLON.Vector3(0, 0, 1); + cameraRight = new BABYLON.Vector3(1, 0, 0); + } + + // Calculate new position + const movement = cameraDirection.scale(forward * speed) + .add(cameraRight.scale(right * speed)); + + const newPosition = playerMesh.position.add(movement); + newPosition.y = -1; + + // Clamp to boundaries + if (newPosition.x > 245) newPosition.x = 245; + else if (newPosition.x < -245) newPosition.x = -245; + if (newPosition.z > 245) newPosition.z = 245; + else if (newPosition.z < -245) newPosition.z = -245; + + this.playerNextPosition[this.room.sessionId] = newPosition; + + // Send position update to server (throttled by render loop) + this.room.send("updatePosition", { + x: newPosition.x, + y: newPosition.y, + z: newPosition.z, + }); + } + } + }); } async initXR(): Promise { @@ -213,6 +411,11 @@ export default class Game { optionalFeatures: true }); console.log("WebXR AR initialized successfully"); + + // Store reference to XR camera if needed + if (xr.baseExperience.camera) { + this.camera = xr.baseExperience.camera; + } return; // Exit early if WebXR initialized successfully } catch (error) { console.error("WebXR AR initialization failed:", error); @@ -222,10 +425,26 @@ export default class Game { } // Fallback to standard camera if WebXR is not supported or failed - console.log("Using desktop camera fallback"); + console.log("Using desktop/mobile camera fallback"); const camera = new BABYLON.ArcRotateCamera("camera", Math.PI / 2, 1.0, 550, BABYLON.Vector3.Zero(), this.scene); camera.setTarget(BABYLON.Vector3.Zero()); camera.attachControl(this.canvas, true); + + // Optimize camera for mobile devices + if (this.isMobile) { + camera.lowerRadiusLimit = 300; + camera.upperRadiusLimit = 800; + camera.panningSensibility = 50; // Less sensitive panning for touch + camera.pinchPrecision = 100; // Pinch to zoom sensitivity + camera.wheelPrecision = 50; // Scroll zoom sensitivity + // Adjust touch sensitivity using available properties + camera.angularSensibilityX = 5000; + camera.angularSensibilityY = 5000; + } else { + camera.lowerRadiusLimit = 200; + camera.upperRadiusLimit = 1000; + } + this.camera = camera; createSkyBox(this.scene); // Only add skybox in non-AR } @@ -371,6 +590,52 @@ export default class Game { const targetPosition = this.playerNextPosition[sessionId]; entity.position = BABYLON.Vector3.Lerp(entity.position, targetPosition, 0.05); } + + // Process joystick input for mobile + if (this.isMobile && this.joystickActive && this.joystickOffset.length() > 0.1) { + const playerMesh = this.playerEntities[this.room.sessionId]; + if (playerMesh) { + const speed = 3; + + // Get camera direction for movement relative to camera + let cameraDirection = BABYLON.Vector3.Zero(); + let cameraRight = BABYLON.Vector3.Zero(); + + if (this.camera && this.camera instanceof BABYLON.ArcRotateCamera) { + const arcCamera = this.camera as BABYLON.ArcRotateCamera; + cameraDirection = arcCamera.target.subtract(arcCamera.position).normalize(); + cameraDirection.y = 0; + cameraDirection.normalize(); + cameraRight = BABYLON.Vector3.Cross(cameraDirection, BABYLON.Vector3.Up()); + } else { + // Default movement direction + cameraDirection = new BABYLON.Vector3(0, 0, 1); + cameraRight = new BABYLON.Vector3(1, 0, 0); + } + + // Calculate movement from joystick + const movement = cameraDirection.scale(-this.joystickOffset.y * speed) + .add(cameraRight.scale(this.joystickOffset.x * speed)); + + const newPosition = playerMesh.position.add(movement); + newPosition.y = -1; + + // Clamp to boundaries + if (newPosition.x > 245) newPosition.x = 245; + else if (newPosition.x < -245) newPosition.x = -245; + if (newPosition.z > 245) newPosition.z = 245; + else if (newPosition.z < -245) newPosition.z = -245; + + this.playerNextPosition[this.room.sessionId] = newPosition; + + // Send position update to server + this.room.send("updatePosition", { + x: newPosition.x, + y: newPosition.y, + z: newPosition.z, + }); + } + } }); // Run the render loop. diff --git a/client/src/menu.ts b/client/src/menu.ts index 07eceb9..abafedc 100644 --- a/client/src/menu.ts +++ b/client/src/menu.ts @@ -19,11 +19,17 @@ export default class Menu { private _colyseus: Client = new Client(ENDPOINT); private _errorMessage: GUI.TextBlock = new GUI.TextBlock("errorText"); + + private _isMobile: boolean = false; constructor(canvasElement: string) { // Create canvas and engine. this._canvas = document.getElementById(canvasElement) as HTMLCanvasElement; this._engine = new BABYLON.Engine(this._canvas, true); + + // Detect mobile device + this._isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) + || (window.innerWidth <= 768); } createMenu(): void { @@ -31,6 +37,14 @@ export default class Menu { this._camera = new BABYLON.ArcRotateCamera("camera", Math.PI / 2, 1.0, 110, BABYLON.Vector3.Zero(), this._scene); this._camera.useAutoRotationBehavior = true; this._camera.setTarget(BABYLON.Vector3.Zero()); + this._camera.attachControl(this._canvas, true); + + // Optimize camera for mobile + if (this._isMobile) { + this._camera.panningSensibility = 1000; + this._camera.pinchPrecision = 200; + } + this._advancedTexture = GUI.AdvancedDynamicTexture.CreateFullscreenUI("UI"); createSkyBox(this._scene); @@ -40,13 +54,13 @@ export default class Menu { controlBox.horizontalAlignment = GUI.Control.HORIZONTAL_ALIGNMENT_CENTER; controlBox.verticalAlignment = GUI.Control.VERTICAL_ALIGNMENT_TOP; controlBox.height = "100%"; - controlBox.width = "40%"; + controlBox.width = this._isMobile ? "80%" : "40%"; controlBox.thickness = 0; const logo = new GUI.Image("ColyseusLogo", "./public/colyseus.png"); logo.horizontalAlignment = GUI.Control.HORIZONTAL_ALIGNMENT_CENTER; logo.verticalAlignment = GUI.Control.VERTICAL_ALIGNMENT_TOP; - logo.height = "40%"; + logo.height = this._isMobile ? "25%" : "40%"; logo.paddingTop = "10px"; logo.stretch = GUI.Image.STRETCH_UNIFORM; controlBox.addControl(logo); @@ -93,10 +107,10 @@ export default class Menu { private createMenuButton(name: string, text: string): GUI.Button { const button = GUI.Button.CreateImageWithCenterTextButton(name, text, "./public/btn-default.png"); - button.width = "45%"; - button.height = "55px"; + button.width = this._isMobile ? "70%" : "45%"; + button.height = this._isMobile ? "50px" : "55px"; button.fontFamily = "Roboto"; - button.fontSize = "6%"; + button.fontSize = this._isMobile ? "5%" : "6%"; button.thickness = 0; button.paddingTop = "10px" button.color = "#c0c0c0"; From 2441566b653a9fbff60fc22fb9e6c38c1257cc6f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 24 Nov 2025 22:41:18 +0000 Subject: [PATCH 3/4] Refactor: Extract movement logic to reduce code duplication Co-authored-by: mrhegemon <5104160+mrhegemon@users.noreply.github.com> --- README.md | 68 +++++++++++++++++++++- client/src/game.ts | 138 ++++++++++++++++++--------------------------- 2 files changed, 121 insertions(+), 85 deletions(-) diff --git a/README.md b/README.md index 069bb35..3199a18 100644 --- a/README.md +++ b/README.md @@ -173,13 +173,20 @@ Tests include: ### Usage -1. Open `http://localhost:8080` in your browser +1. Open `http://localhost:8080` in your browser (desktop or mobile) 2. Choose an option: - **CREATE GAME**: Create a new room - **JOIN GAME**: Join an existing room - **CREATE OR JOIN**: Automatically join or create 3. Once in game: - - Click anywhere on the ground to move your player + - **Desktop Controls**: + - Click anywhere on the ground to move your player + - Use **WASD** or **Arrow Keys** for continuous movement + - Mouse drag to rotate camera, scroll to zoom + - **Mobile Controls**: + - Tap anywhere on the ground to move your player + - Use the **virtual joystick** (bottom-left) for continuous movement + - Touch drag to rotate camera, pinch to zoom - See other players as gray spheres (you're orange) - Video/audio streams appear above players (if media permitted) 4. **For AR Mode** (mobile devices with AR support): @@ -219,6 +226,51 @@ EE2/ └── README.md # This file ``` +## Mobile and Desktop Support + +BXR provides full cross-platform support with optimized controls for both mobile and desktop devices. + +### Device Detection + +The application automatically detects the device type and adapts the interface and controls accordingly: +- **Mobile**: Virtual joystick, touch-optimized camera, responsive UI +- **Desktop**: Keyboard controls (WASD/Arrows), mouse controls, full-size UI + +### Control Schemes + +#### Desktop Controls +- **Movement**: + - **Mouse Click**: Click on ground to move to that location + - **WASD Keys**: Continuous movement relative to camera + - **Arrow Keys**: Alternative continuous movement controls +- **Camera**: + - **Mouse Drag**: Rotate camera around scene + - **Mouse Wheel**: Zoom in/out + +#### Mobile Controls +- **Movement**: + - **Tap Ground**: Tap on ground to move to that location + - **Virtual Joystick**: Bottom-left circular joystick for continuous movement +- **Camera**: + - **Touch Drag**: Rotate camera around scene + - **Pinch**: Zoom in/out + +### Responsive Features + +- **Viewport Configuration**: Optimized meta tags for mobile browsers +- **Touch Handling**: Prevents scroll interference and overscroll behavior +- **UI Scaling**: Buttons, text, and controls scale based on screen size +- **Camera Settings**: Adjusted sensitivity and limits for touch vs. mouse input +- **WebRTC Compatibility**: Video/audio streaming works across all devices + +### Cross-Device Multiplayer + +The multiplayer system seamlessly supports mixed-device sessions: +- Desktop and mobile players can join the same game room +- WebRTC peer-to-peer connections work across all device types +- Position synchronization is optimized for different input methods +- Video/audio streams display correctly regardless of device + ## Technology Stack | Component | Technology | Purpose | @@ -256,15 +308,27 @@ EE2/ - Grant camera/microphone permissions when prompted - Check browser console for WebRTC errors - Ensure HTTPS for production (HTTP works for localhost) +- On mobile: Tap the screen to enable autoplay if blocked **AR Mode Not Available**: - AR requires WebXR-compatible device (recent Android/iOS) - Desktop browsers will automatically use fallback camera +**Mobile Controls Not Responding**: +- Ensure touch-action is properly disabled (check CSS) +- Try refreshing the page if joystick becomes unresponsive +- Check that browser supports touch events + +**Keyboard Controls Not Working (Desktop)**: +- Click on the game canvas to ensure it has focus +- Check browser console for any JavaScript errors +- Ensure you're not in AR mode (keyboard is desktop-only) + **Connection Issues**: - Verify server is running on port 2567 - Check firewall settings - Ensure client endpoint matches server address +- For mobile: Check that device is on same network as server ## License diff --git a/client/src/game.ts b/client/src/game.ts index cdcef84..736a374 100644 --- a/client/src/game.ts +++ b/client/src/game.ts @@ -6,6 +6,9 @@ import Menu from "./menu"; import { createSkyBox } from "./utils"; const GROUND_SIZE = 500; +const BOUNDARY_LIMIT = 245; // Position boundary limit +const KEYBOARD_SPEED = 5; // Movement speed for keyboard controls +const JOYSTICK_SPEED = 3; // Movement speed for joystick controls // WebRTC Configuration const RTC_CONFIG = { @@ -282,22 +285,7 @@ export default class Game { const pickInfo = this.scene.pick(this.scene.pointerX, this.scene.pointerY); if (pickInfo.hit && pickInfo.pickedMesh?.name === "plane") { const targetPosition = pickInfo.pickedPoint.clone(); - - // Position adjustments for the current play ground. - targetPosition.y = -1; - if (targetPosition.x > 245) targetPosition.x = 245; - else if (targetPosition.x < -245) targetPosition.x = -245; - if (targetPosition.z > 245) targetPosition.z = 245; - else if (targetPosition.z < -245) targetPosition.z = -245; - - this.playerNextPosition[this.room.sessionId] = targetPosition; - - // Send position update to the server - this.room.send("updatePosition", { - x: targetPosition.x, - y: targetPosition.y, - z: targetPosition.z, - }); + this.updatePlayerPosition(targetPosition); } } }; @@ -329,7 +317,6 @@ export default class Game { // Movement processing in render loop this.scene.onBeforeRenderObservable.add(() => { - const speed = 5; // Movement speed let hasMovement = false; // Calculate movement direction @@ -357,42 +344,14 @@ export default class Game { const playerMesh = this.playerEntities[this.room.sessionId]; if (playerMesh) { // Get camera direction for movement relative to camera - let cameraDirection = BABYLON.Vector3.Zero(); - let cameraRight = BABYLON.Vector3.Zero(); - - if (this.camera && this.camera instanceof BABYLON.ArcRotateCamera) { - const arcCamera = this.camera as BABYLON.ArcRotateCamera; - cameraDirection = arcCamera.target.subtract(arcCamera.position).normalize(); - cameraDirection.y = 0; // Keep movement horizontal - cameraDirection.normalize(); - cameraRight = BABYLON.Vector3.Cross(cameraDirection, BABYLON.Vector3.Up()); - } else { - // Default to world axes if camera is not available - cameraDirection = new BABYLON.Vector3(0, 0, 1); - cameraRight = new BABYLON.Vector3(1, 0, 0); - } + const { forward: cameraDirection, right: cameraRight } = this.getCameraDirectionVectors(); // Calculate new position - const movement = cameraDirection.scale(forward * speed) - .add(cameraRight.scale(right * speed)); + const movement = cameraDirection.scale(forward * KEYBOARD_SPEED) + .add(cameraRight.scale(right * KEYBOARD_SPEED)); const newPosition = playerMesh.position.add(movement); - newPosition.y = -1; - - // Clamp to boundaries - if (newPosition.x > 245) newPosition.x = 245; - else if (newPosition.x < -245) newPosition.x = -245; - if (newPosition.z > 245) newPosition.z = 245; - else if (newPosition.z < -245) newPosition.z = -245; - - this.playerNextPosition[this.room.sessionId] = newPosition; - - // Send position update to server (throttled by render loop) - this.room.send("updatePosition", { - x: newPosition.x, - y: newPosition.y, - z: newPosition.z, - }); + this.updatePlayerPosition(newPosition); } } }); @@ -576,6 +535,49 @@ export default class Game { this.videoMeshes[sessionId] = plane; } + private getCameraDirectionVectors(): { forward: BABYLON.Vector3, right: BABYLON.Vector3 } { + let cameraDirection = BABYLON.Vector3.Zero(); + let cameraRight = BABYLON.Vector3.Zero(); + + if (this.camera && this.camera instanceof BABYLON.ArcRotateCamera) { + const arcCamera = this.camera as BABYLON.ArcRotateCamera; + cameraDirection = arcCamera.target.subtract(arcCamera.position).normalize(); + cameraDirection.y = 0; // Keep movement horizontal + cameraDirection.normalize(); + cameraRight = BABYLON.Vector3.Cross(cameraDirection, BABYLON.Vector3.Up()); + } else { + // Default to world axes if camera is not available + cameraDirection = new BABYLON.Vector3(0, 0, 1); + cameraRight = new BABYLON.Vector3(1, 0, 0); + } + + return { forward: cameraDirection, right: cameraRight }; + } + + private clampToBoundaries(position: BABYLON.Vector3): BABYLON.Vector3 { + const clampedPosition = position.clone(); + clampedPosition.y = -1; + + if (clampedPosition.x > BOUNDARY_LIMIT) clampedPosition.x = BOUNDARY_LIMIT; + else if (clampedPosition.x < -BOUNDARY_LIMIT) clampedPosition.x = -BOUNDARY_LIMIT; + if (clampedPosition.z > BOUNDARY_LIMIT) clampedPosition.z = BOUNDARY_LIMIT; + else if (clampedPosition.z < -BOUNDARY_LIMIT) clampedPosition.z = -BOUNDARY_LIMIT; + + return clampedPosition; + } + + private updatePlayerPosition(newPosition: BABYLON.Vector3): void { + const clampedPosition = this.clampToBoundaries(newPosition); + this.playerNextPosition[this.room.sessionId] = clampedPosition; + + // Send position update to server + this.room.send("updatePosition", { + x: clampedPosition.x, + y: clampedPosition.y, + z: clampedPosition.z, + }); + } + private gotoMenu() { this.scene.dispose(); const menu = new Menu('renderCanvas'); @@ -595,45 +597,15 @@ export default class Game { if (this.isMobile && this.joystickActive && this.joystickOffset.length() > 0.1) { const playerMesh = this.playerEntities[this.room.sessionId]; if (playerMesh) { - const speed = 3; - // Get camera direction for movement relative to camera - let cameraDirection = BABYLON.Vector3.Zero(); - let cameraRight = BABYLON.Vector3.Zero(); - - if (this.camera && this.camera instanceof BABYLON.ArcRotateCamera) { - const arcCamera = this.camera as BABYLON.ArcRotateCamera; - cameraDirection = arcCamera.target.subtract(arcCamera.position).normalize(); - cameraDirection.y = 0; - cameraDirection.normalize(); - cameraRight = BABYLON.Vector3.Cross(cameraDirection, BABYLON.Vector3.Up()); - } else { - // Default movement direction - cameraDirection = new BABYLON.Vector3(0, 0, 1); - cameraRight = new BABYLON.Vector3(1, 0, 0); - } + const { forward: cameraDirection, right: cameraRight } = this.getCameraDirectionVectors(); // Calculate movement from joystick - const movement = cameraDirection.scale(-this.joystickOffset.y * speed) - .add(cameraRight.scale(this.joystickOffset.x * speed)); + const movement = cameraDirection.scale(-this.joystickOffset.y * JOYSTICK_SPEED) + .add(cameraRight.scale(this.joystickOffset.x * JOYSTICK_SPEED)); const newPosition = playerMesh.position.add(movement); - newPosition.y = -1; - - // Clamp to boundaries - if (newPosition.x > 245) newPosition.x = 245; - else if (newPosition.x < -245) newPosition.x = -245; - if (newPosition.z > 245) newPosition.z = 245; - else if (newPosition.z < -245) newPosition.z = -245; - - this.playerNextPosition[this.room.sessionId] = newPosition; - - // Send position update to server - this.room.send("updatePosition", { - x: newPosition.x, - y: newPosition.y, - z: newPosition.z, - }); + this.updatePlayerPosition(newPosition); } } }); From 2821c8988b65ff88843202d15e6567c76994164b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 24 Nov 2025 22:45:06 +0000 Subject: [PATCH 4/4] Fix code review issues: Add missing semicolon and fix fontSize format Co-authored-by: mrhegemon <5104160+mrhegemon@users.noreply.github.com> --- client/src/game.ts | 2 +- client/src/menu.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/game.ts b/client/src/game.ts index 736a374..18c086c 100644 --- a/client/src/game.ts +++ b/client/src/game.ts @@ -156,7 +156,7 @@ export default class Game { button.width = this.isMobile ? "80px" : "100px"; button.height = this.isMobile ? "40px" : "50px"; button.fontFamily = "Roboto"; - button.fontSize = this.isMobile ? 12 : 16; + button.fontSize = this.isMobile ? "12px" : "16px"; button.thickness = 0; button.color = "#f8f8f8"; button.horizontalAlignment = GUI.Control.HORIZONTAL_ALIGNMENT_RIGHT; diff --git a/client/src/menu.ts b/client/src/menu.ts index abafedc..e9e615e 100644 --- a/client/src/menu.ts +++ b/client/src/menu.ts @@ -112,7 +112,7 @@ export default class Menu { button.fontFamily = "Roboto"; button.fontSize = this._isMobile ? "5%" : "6%"; button.thickness = 0; - button.paddingTop = "10px" + button.paddingTop = "10px"; button.color = "#c0c0c0"; return button; }