fix(player): кнопки 3D-табличек + управление камерой как в Roblox (задачи 01, 02)
Задача 01 — billboard-клик не работал: _handlePlayClick не проверял кнопки табличек. Добавил pick по billboard-мешу из центра экрана → pickButtonAt → fireClick (BabylonScene._handlePlayClick). Задача 02 — управление было старым (всегда pointer-lock, ПКМ не работал): - start(): lock только в perma-режимах (first/lockfirst/sideview/shift-lock), в third курсор виден свободно - onCanvasClick: не лочит в third (курсор для GUI/3D-табличек) - ПКМ-orbit: зажал ПКМ в third → lock+вращение, отпустил → курсор вернулся - onWheel: авто-переход third↔first при зуме (порог 0.7), экспоненциальный шаг - onPointerLockChange: отпускание ПКМ в third НЕ выходит из Play (раньше выходило) - _applyCursorVisibility / _isPermaLockMode хелперы Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e61c398eeb
commit
2afd6a287a
@ -2934,6 +2934,29 @@ export class BabylonScene {
|
||||
}
|
||||
|
||||
if (!this.gameRuntime) return;
|
||||
|
||||
// === Задача 01: клик по КНОПКЕ 3D-таблички (billboard) ===
|
||||
// Пикаем из центра экрана (как _pickFromCenter — в Play обычно
|
||||
// pointer-lock). Если попали в кнопку таблички → fireClick и выходим.
|
||||
if (this.billboardUiManager && this.primitiveManager) {
|
||||
const w = this.engine?.getRenderWidth?.() || this.canvas.width;
|
||||
const h = this.engine?.getRenderHeight?.() || this.canvas.height;
|
||||
const bpick = this.scene.pick(w / 2, h / 2, (m) =>
|
||||
m && m.metadata && m.metadata.primitiveId != null
|
||||
&& this.primitiveManager.instances.get(m.metadata.primitiveId)?.type === 'billboard');
|
||||
if (bpick && bpick.hit && bpick.pickedMesh) {
|
||||
const bdata = this.primitiveManager.instances.get(bpick.pickedMesh.metadata.primitiveId);
|
||||
const uv = bpick.getTextureCoordinates ? bpick.getTextureCoordinates() : null;
|
||||
if (bdata && uv) {
|
||||
const buttonId = this.billboardUiManager.pickButtonAt(bdata, uv.x, uv.y);
|
||||
if (buttonId) {
|
||||
this.billboardUiManager.fireClick(bdata, buttonId);
|
||||
return; // клик по табличке обработан
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const pick = this._pickFromCenter();
|
||||
const target = pick?.mesh ? this._meshToTarget(pick.mesh) : null;
|
||||
const point = pick?.point ? { x: pick.point.x, y: pick.point.y, z: pick.point.z } : null;
|
||||
|
||||
@ -150,6 +150,9 @@ export class PlayerController {
|
||||
this._lockFirstPerson = false;
|
||||
// Shift-Lock (Roblox-style): курсор зафиксирован, корпус лицом к камере.
|
||||
this._shiftLock = false;
|
||||
// Задача 02: ПКМ зажата сейчас (orbit в third) + видимость курсора.
|
||||
this._rmbHeld = false;
|
||||
this._mouseIconVisible = true;
|
||||
|
||||
// Ввод
|
||||
this._codes = new Set();
|
||||
@ -395,10 +398,28 @@ export class PlayerController {
|
||||
this._beforeRender = () => this._tick();
|
||||
this.scene.registerBeforeRender(this._beforeRender);
|
||||
|
||||
// Сразу запросить Pointer Lock. Promise-форма (новые Chrome) может
|
||||
// отклониться с SecurityError если предыдущий lock ещё не отпущен —
|
||||
// в этом случае ждём отпускания и пробуем снова.
|
||||
this._requestPointerLockSafe();
|
||||
// === Задача 02: pointer-lock берём ТОЛЬКО в режимах с постоянным lock
|
||||
// (first/lockfirst/sideview/shift-lock). В third курсор виден свободно —
|
||||
// кликает GUI и 3D-таблички, камера крутится только при зажатой ПКМ.
|
||||
if (this._isPermaLockMode()) {
|
||||
this._requestPointerLockSafe();
|
||||
}
|
||||
this._applyCursorVisibility();
|
||||
}
|
||||
|
||||
/** Задача 02: режим с ПОСТОЯННЫМ pointer-lock (мышь всегда крутит камеру). */
|
||||
_isPermaLockMode() {
|
||||
return this._cameraMode === 'first' || this._cameraMode === 'lockfirst'
|
||||
|| this._cameraMode === 'sideview' || this._shiftLock;
|
||||
}
|
||||
|
||||
/** Задача 02: показать/скрыть курсор. В third виден (если нет lock), в
|
||||
* first/lock — скрыт. Учитывает game.input.setMouseIconVisible. */
|
||||
_applyCursorVisibility() {
|
||||
if (!this.canvas) return;
|
||||
const locked = (document.pointerLockElement === this.canvas);
|
||||
const show = (this._mouseIconVisible !== false) && !locked;
|
||||
try { this.canvas.style.cursor = show ? '' : 'none'; } catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -2191,10 +2212,13 @@ export class PlayerController {
|
||||
_setupInput() {
|
||||
const canvas = this.canvas;
|
||||
|
||||
// Задача 02: ЛКМ-клик НЕ берёт pointer-lock в third (курсор свободен для
|
||||
// GUI/3D-табличек). Lock берётся только в perma-режимах (first/lock) —
|
||||
// но там он уже взят в start(). В third lock включает только зажатая ПКМ.
|
||||
const onCanvasClick = () => {
|
||||
// В UI-режиме клик по канвасу НЕ перехватывает мышь
|
||||
if (this._uiCursorMode) return;
|
||||
if (this._active && document.pointerLockElement !== canvas) {
|
||||
if (this._active && this._isPermaLockMode()
|
||||
&& document.pointerLockElement !== canvas) {
|
||||
try {
|
||||
const p = canvas.requestPointerLock?.();
|
||||
if (p && typeof p.catch === 'function') p.catch(() => {});
|
||||
@ -2203,6 +2227,32 @@ export class PlayerController {
|
||||
};
|
||||
canvas.addEventListener('click', onCanvasClick);
|
||||
|
||||
// Задача 02: ПКМ-orbit — зажал ПКМ в third → lock + камера крутится,
|
||||
// отпустил → курсор вернулся. В perma-режимах ПКМ не нужен.
|
||||
const onRmbDown = (e) => {
|
||||
if (e.button !== 2 || this._uiCursorMode) return;
|
||||
if (!this._isPermaLockMode() && document.pointerLockElement !== canvas) {
|
||||
this._rmbHeld = true;
|
||||
try {
|
||||
const p = canvas.requestPointerLock?.();
|
||||
if (p && typeof p.catch === 'function') p.catch(() => {});
|
||||
} catch (err) { /* ignore */ }
|
||||
}
|
||||
};
|
||||
const onRmbUp = (e) => {
|
||||
if (e.button !== 2) return;
|
||||
if (this._rmbHeld) {
|
||||
this._rmbHeld = false;
|
||||
if (!this._isPermaLockMode() && document.pointerLockElement === canvas) {
|
||||
try { document.exitPointerLock(); } catch (err) { /* ignore */ }
|
||||
}
|
||||
}
|
||||
};
|
||||
const onCtxMenu = (e) => { if (!this._uiCursorMode) e.preventDefault(); };
|
||||
canvas.addEventListener('mousedown', onRmbDown);
|
||||
document.addEventListener('mouseup', onRmbUp);
|
||||
canvas.addEventListener('contextmenu', onCtxMenu);
|
||||
|
||||
// === UI-режим: mousedown / mouseup → callback (для drag-игр) ===
|
||||
const onCanvasMouseDown = (e) => {
|
||||
if (!this._uiCursorMode) return;
|
||||
@ -2254,15 +2304,38 @@ export class PlayerController {
|
||||
};
|
||||
document.addEventListener('mousemove', onMouseMove);
|
||||
|
||||
// Колесо в 3rd-person — меняет дистанцию
|
||||
// Задача 02: колесо = зум third-камеры с авто-переходом third↔first.
|
||||
const onWheel = (e) => {
|
||||
if (!this._active) return;
|
||||
// Задача 04: модал с freezeCamera — колесо не зумит.
|
||||
if (this._cameraFrozen) { e.preventDefault(); return; }
|
||||
if (this._cameraMode !== 'third') return;
|
||||
this._thirdDistance += Math.sign(e.deltaY) * 0.5;
|
||||
if (this._cameraFrozen) { e.preventDefault(); return; } // модал
|
||||
if (this._cameraMode === 'sideview') { e.preventDefault(); return; } // GD
|
||||
// В first зум наружу возвращает в third (если не lockfirst).
|
||||
if (this._cameraMode === 'first') {
|
||||
if (e.deltaY > 0 && !this._lockFirstPerson) {
|
||||
this._cameraMode = 'third';
|
||||
this._thirdDistance = (this.FIRST_PERSON_ZOOM_THRESHOLD || 0.7) + 0.5;
|
||||
if (!this._isPermaLockMode() && document.pointerLockElement === canvas) {
|
||||
try { document.exitPointerLock(); } catch (err) { /* ignore */ }
|
||||
}
|
||||
this._applyCursorVisibility();
|
||||
this._applyCameraMode?.();
|
||||
}
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (this._cameraMode !== 'third') { e.preventDefault(); return; }
|
||||
// Экспоненциальный шаг (плавнее вблизи).
|
||||
this._thirdDistance += Math.sign(e.deltaY) * Math.max(0.3, this._thirdDistance * 0.15);
|
||||
if (this._thirdDistance < this.THIRD_DISTANCE_MIN) this._thirdDistance = this.THIRD_DISTANCE_MIN;
|
||||
if (this._thirdDistance > this.THIRD_DISTANCE_MAX) this._thirdDistance = this.THIRD_DISTANCE_MAX;
|
||||
// Зум внутрь до порога → авто-переход в first (Roblox-style).
|
||||
const THRESH = this.FIRST_PERSON_ZOOM_THRESHOLD || 0.7;
|
||||
if (this._thirdDistance <= THRESH) {
|
||||
this._cameraMode = 'first';
|
||||
this._requestPointerLockSafe();
|
||||
this._applyCursorVisibility();
|
||||
this._applyCameraMode?.();
|
||||
}
|
||||
e.preventDefault();
|
||||
};
|
||||
canvas.addEventListener('wheel', onWheel, { passive: false });
|
||||
@ -2270,11 +2343,17 @@ export class PlayerController {
|
||||
let wasLocked = false;
|
||||
const onPointerLockChange = () => {
|
||||
const locked = document.pointerLockElement === canvas;
|
||||
this._applyCursorVisibility(); // задача 02: вернуть/скрыть курсор
|
||||
if (locked) {
|
||||
wasLocked = true;
|
||||
} else if (wasLocked && this._active) {
|
||||
// Если мы САМИ переключились в UI-cursor mode — не выходим из Play
|
||||
wasLocked = false;
|
||||
if (this._uiCursorMode) return;
|
||||
// Задача 02: в third потеря lock = отпустили ПКМ (orbit) ИЛИ
|
||||
// вышли зумом из first — это НЕ выход из Play. Выход (Esc) только
|
||||
// из perma-режимов с постоянным lock (first/lockfirst/shift-lock).
|
||||
if (this._rmbHeld) { this._rmbHeld = false; return; }
|
||||
if (this._cameraMode === 'third' || this._cameraMode === 'front') return;
|
||||
if (this._onExitRequest) this._onExitRequest();
|
||||
}
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user