🐛 fix HUD overlay zoom + add red center crosshair

Text overlay now uses a dedicated HUD camera (zoom=1, fixed scroll)
so it's never scaled by the world zoom. World objects and HUD objects
are separated via camera ignore lists. Added red screen-center
crosshair to HUD layer as a precise alignment reference.
This commit is contained in:
2026-03-21 11:34:04 +00:00
parent 7c130763b5
commit a93e8a2c5d

View File

@@ -1,14 +1,14 @@
import Phaser from 'phaser' import Phaser from 'phaser'
import { TILE_SIZE } from '../config' import { TILE_SIZE } from '../config'
const GRID_TILES = 50 // world size in tiles const GRID_TILES = 50 // world size in tiles
const MIN_ZOOM = 0.25 const MIN_ZOOM = 0.25
const MAX_ZOOM = 4.0 const MAX_ZOOM = 4.0
const ZOOM_STEP = 0.1 const ZOOM_STEP = 0.1
const MARKER_EVERY = 5 // small crosshair every N tiles const MARKER_EVERY = 5 // small crosshair every N tiles
const LABEL_EVERY = 10 // coordinate label every N tiles const LABEL_EVERY = 10 // coordinate label every N tiles
const CAMERA_SPEED = 400 // px/s const CAMERA_SPEED = 400 // px/s
const SNAPSHOT_EVERY = 2000 // ms between periodic log snapshots const SNAPSHOT_EVERY = 2000 // ms between periodic log snapshots
/** /**
* First test scene: observes pure Phaser default zoom behavior. * First test scene: observes pure Phaser default zoom behavior.
@@ -19,6 +19,9 @@ const SNAPSHOT_EVERY = 2000 // ms between periodic log snapshots
*/ */
export class ZoomTestScene extends Phaser.Scene { export class ZoomTestScene extends Phaser.Scene {
private logText!: Phaser.GameObjects.Text private logText!: Phaser.GameObjects.Text
private hudCamera!: Phaser.Cameras.Scene2D.Camera
private worldObjects: Phaser.GameObjects.GameObject[] = []
private hudObjects: Phaser.GameObjects.GameObject[] = []
private keys!: { private keys!: {
up: Phaser.Input.Keyboard.Key up: Phaser.Input.Keyboard.Key
down: Phaser.Input.Keyboard.Key down: Phaser.Input.Keyboard.Key
@@ -43,19 +46,18 @@ export class ZoomTestScene extends Phaser.Scene {
this.drawGrid() this.drawGrid()
this.setupCamera() this.setupCamera()
this.setupInput() this.setupInput()
this.createOverlay() this.createHUD()
this.setupCameras()
} }
/** /**
* Draws the static world grid into world space. * Draws the static world grid into world space.
* - Faint tile lines on every tile boundary * All objects are registered in worldObjects for HUD-camera exclusion.
* - Small green crosshairs every MARKER_EVERY tiles
* - Yellow labeled crosshairs every LABEL_EVERY tiles
* - Red world border
*/ */
private drawGrid(): void { private drawGrid(): void {
const worldPx = GRID_TILES * TILE_SIZE const worldPx = GRID_TILES * TILE_SIZE
const g = this.add.graphics() const g = this.add.graphics()
this.worldObjects.push(g)
// Background fill // Background fill
g.fillStyle(0x111811) g.fillStyle(0x111811)
@@ -90,12 +92,13 @@ export class ZoomTestScene extends Phaser.Scene {
// Coordinate labels at LABEL_EVERY intersections // Coordinate labels at LABEL_EVERY intersections
for (let tx = 0; tx <= GRID_TILES; tx += LABEL_EVERY) { for (let tx = 0; tx <= GRID_TILES; tx += LABEL_EVERY) {
for (let ty = 0; ty <= GRID_TILES; ty += LABEL_EVERY) { for (let ty = 0; ty <= GRID_TILES; ty += LABEL_EVERY) {
this.add.text( const label = this.add.text(
tx * TILE_SIZE + 4, tx * TILE_SIZE + 4,
ty * TILE_SIZE + 4, ty * TILE_SIZE + 4,
`${tx},${ty}`, `${tx},${ty}`,
{ fontSize: '9px', color: '#ffff88', fontFamily: 'monospace' } { fontSize: '9px', color: '#ffff88', fontFamily: 'monospace' }
).setDepth(1) ).setDepth(1)
this.worldObjects.push(label)
} }
} }
@@ -141,27 +144,26 @@ export class ZoomTestScene extends Phaser.Scene {
_dx: number, _dx: number,
dy: number dy: number
) => { ) => {
const zoomBefore = cam.zoom const zoomBefore = cam.zoom
const scrollXBefore = cam.scrollX const scrollXBefore = cam.scrollX
const scrollYBefore = cam.scrollY const scrollYBefore = cam.scrollY
const newZoom = Phaser.Math.Clamp(cam.zoom - Math.sign(dy) * ZOOM_STEP, MIN_ZOOM, MAX_ZOOM) const newZoom = Phaser.Math.Clamp(cam.zoom - Math.sign(dy) * ZOOM_STEP, MIN_ZOOM, MAX_ZOOM)
cam.setZoom(newZoom) cam.setZoom(newZoom)
// Log after Phaser has applied the zoom (next microtask so values are updated)
setTimeout(() => { setTimeout(() => {
this.writeLog('zoom', { this.writeLog('zoom', {
direction: dy > 0 ? 'out' : 'in', direction: dy > 0 ? 'out' : 'in',
zoomBefore: +zoomBefore.toFixed(4), zoomBefore: +zoomBefore.toFixed(4),
zoomAfter: +cam.zoom.toFixed(4), zoomAfter: +cam.zoom.toFixed(4),
scrollX_before: +scrollXBefore.toFixed(2), scrollX_before: +scrollXBefore.toFixed(2),
scrollY_before: +scrollYBefore.toFixed(2), scrollY_before: +scrollYBefore.toFixed(2),
scrollX_after: +cam.scrollX.toFixed(2), scrollX_after: +cam.scrollX.toFixed(2),
scrollY_after: +cam.scrollY.toFixed(2), scrollY_after: +cam.scrollY.toFixed(2),
scrollX_delta: +(cam.scrollX - scrollXBefore).toFixed(2), scrollX_delta: +(cam.scrollX - scrollXBefore).toFixed(2),
scrollY_delta: +(cam.scrollY - scrollYBefore).toFixed(2), scrollY_delta: +(cam.scrollY - scrollYBefore).toFixed(2),
mouseScreen: { x: +ptr.x.toFixed(1), y: +ptr.y.toFixed(1) }, mouseScreen: { x: +ptr.x.toFixed(1), y: +ptr.y.toFixed(1) },
mouseWorld: { x: +ptr.worldX.toFixed(2), y: +ptr.worldY.toFixed(2) }, mouseWorld: { x: +ptr.worldX.toFixed(2), y: +ptr.worldY.toFixed(2) },
centerWorld_after: { centerWorld_after: {
x: +(cam.scrollX + (cam.width / cam.zoom) / 2).toFixed(2), x: +(cam.scrollX + (cam.width / cam.zoom) / 2).toFixed(2),
y: +(cam.scrollY + (cam.height / cam.zoom) / 2).toFixed(2), y: +(cam.scrollY + (cam.height / cam.zoom) / 2).toFixed(2),
@@ -176,9 +178,25 @@ export class ZoomTestScene extends Phaser.Scene {
} }
/** /**
* Creates the fixed HUD text overlay (scroll factor 0 = screen space). * Creates all HUD elements: log overlay and screen-center crosshair.
* All objects are registered in hudObjects for main-camera exclusion.
* Uses a dedicated HUD camera (zoom=1, fixed) so elements are never scaled.
*/ */
private createOverlay(): void { private createHUD(): void {
const w = this.scale.width
const h = this.scale.height
// Screen-center crosshair (red)
const cross = this.add.graphics()
const arm = 16
cross.lineStyle(1, 0xff2222, 0.9)
cross.lineBetween(w / 2 - arm, h / 2, w / 2 + arm, h / 2)
cross.lineBetween(w / 2, h / 2 - arm, w / 2, h / 2 + arm)
cross.fillStyle(0xff2222, 1.0)
cross.fillCircle(w / 2, h / 2, 2)
this.hudObjects.push(cross)
// Log text overlay
this.logText = this.add.text(10, 10, '', { this.logText = this.add.text(10, 10, '', {
fontSize: '13px', fontSize: '13px',
color: '#e8e8e8', color: '#e8e8e8',
@@ -186,16 +204,27 @@ export class ZoomTestScene extends Phaser.Scene {
padding: { x: 10, y: 8 }, padding: { x: 10, y: 8 },
lineSpacing: 3, lineSpacing: 3,
fontFamily: 'monospace', fontFamily: 'monospace',
}) }).setDepth(100)
.setScrollFactor(0) this.hudObjects.push(this.logText)
.setDepth(100) }
/**
* Adds a dedicated HUD camera (zoom=1, no scroll) and separates
* world objects from HUD objects so neither camera renders both layers.
*/
private setupCameras(): void {
this.hudCamera = this.cameras.add(0, 0, this.scale.width, this.scale.height)
this.hudCamera.setScroll(0, 0)
this.hudCamera.setZoom(1)
this.cameras.main.ignore(this.hudObjects)
this.hudCamera.ignore(this.worldObjects)
} }
update(_time: number, delta: number): void { update(_time: number, delta: number): void {
this.handleKeyboard(delta) this.handleKeyboard(delta)
this.updateOverlay() this.updateOverlay()
// Periodic snapshot
this.snapshotTimer += delta this.snapshotTimer += delta
if (this.snapshotTimer >= SNAPSHOT_EVERY) { if (this.snapshotTimer >= SNAPSHOT_EVERY) {
this.snapshotTimer = 0 this.snapshotTimer = 0
@@ -208,8 +237,8 @@ export class ZoomTestScene extends Phaser.Scene {
* @param delta - Frame delta in milliseconds * @param delta - Frame delta in milliseconds
*/ */
private handleKeyboard(delta: number): void { private handleKeyboard(delta: number): void {
const cam = this.cameras.main const cam = this.cameras.main
const speed = CAMERA_SPEED * (delta / 1000) / cam.zoom const speed = CAMERA_SPEED * (delta / 1000) / cam.zoom
const worldPx = GRID_TILES * TILE_SIZE const worldPx = GRID_TILES * TILE_SIZE
let dx = 0, dy = 0 let dx = 0, dy = 0
@@ -231,27 +260,17 @@ export class ZoomTestScene extends Phaser.Scene {
const cam = this.cameras.main const cam = this.cameras.main
const ptr = this.input.activePointer const ptr = this.input.activePointer
// Viewport size in world pixels (what is actually visible) const vpWidthPx = cam.width / cam.zoom
const vpWidthPx = cam.width / cam.zoom const vpHeightPx = cam.height / cam.zoom
const vpHeightPx = cam.height / cam.zoom
// Viewport size in tiles
const vpWidthTiles = vpWidthPx / TILE_SIZE const vpWidthTiles = vpWidthPx / TILE_SIZE
const vpHeightTiles = vpHeightPx / TILE_SIZE const vpHeightTiles = vpHeightPx / TILE_SIZE
const centerWorldX = cam.scrollX + vpWidthPx / 2
// Camera center in world coords const centerWorldY = cam.scrollY + vpHeightPx / 2
const centerWorldX = cam.scrollX + vpWidthPx / 2 const mouseTileX = Math.floor(ptr.worldX / TILE_SIZE)
const centerWorldY = cam.scrollY + vpHeightPx / 2 const mouseTileY = Math.floor(ptr.worldY / TILE_SIZE)
const centerTileX = Math.floor(centerWorldX / TILE_SIZE)
// Tile under mouse const centerTileY = Math.floor(centerWorldY / TILE_SIZE)
const mouseTileX = Math.floor(ptr.worldX / TILE_SIZE) const renderer = this.game.renderer.type === Phaser.WEBGL ? 'WebGL' : 'Canvas'
const mouseTileY = Math.floor(ptr.worldY / TILE_SIZE)
// Tile at camera center
const centerTileX = Math.floor(centerWorldX / TILE_SIZE)
const centerTileY = Math.floor(centerWorldY / TILE_SIZE)
const renderer = this.game.renderer.type === Phaser.WEBGL ? 'WebGL' : 'Canvas'
const lines = [ const lines = [
'── ZOOM TEST [Phaser default] ──', '── ZOOM TEST [Phaser default] ──',