From 7c928462fc094cb8d32058c07d86d2493e97a79f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=98=D0=9D?= Date: Sun, 31 May 2026 08:28:02 +0300 Subject: [PATCH] =?UTF-8?q?fix(engine):=20findOne(x).onTouch=20=D1=80?= =?UTF-8?q?=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=D0=B5=D1=82=20+=20findOne=20?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=81=D1=82=D0=B0=D1=80=D1=82=D0=B5=20=D1=81?= =?UTF-8?q?=D0=BA=D1=80=D0=B8=D0=BF=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Баг: стрелка-указатель game.fx.pointer не переключалась на следующую цель — при касании цель не менялась, стрелка не выключалась. Первопричина (две движковые проблемы): 1. findOne(x).onTouch(...) не существовал: Instance-proxy не имел методов касания, движок ловил touch только объектов со скриптом-target/триггеров. 2. Race: скрипт исполняется синхронно в init, а sceneSnapshot приходил позже (rAF) → findOne() на старте = null → подписки onTouch молча не вешались. Фикс: - Instance-proxy: + onTouch/onUntouch/onClick → шлёт inst.watchTouch{ref}. Worker: _instTouchHandlers + маршрут instTouch/instUntouch/instClick по ref. - GameRuntime: handler inst.watchTouch/watchClick → _watchedTouchRefs; routeInstEvent(ref,type); сброс в teardown. - BabylonScene._detectTouchEvents: блок watched-объектов (AABB по ref, rising/ falling edge → routeInstEvent), _refToTarget(ref)→{kind,id}, _touchState.clear() в enterPlayMode. - Первичный snapshot сцены передаётся прямо в init (ScriptSandbox.setInitialScene → worker заполняет _sceneIndex до userFn) → findOne работает в синхронном теле скрипта на старте. Проверено: телепорт игрока по 3 целям игры 333 — стрелка переключается red-cube→blue-sphere→gold-chest, на финале удаляется. Co-Authored-By: Claude Opus 4.8 --- src/editor/engine/BabylonScene.js | 56 ++++++++++++++++++++++++ src/editor/engine/GameRuntime.js | 41 +++++++++++++++++ src/editor/engine/ScriptSandbox.js | 8 ++++ src/editor/engine/ScriptSandboxWorker.js | 48 ++++++++++++++++++++ 4 files changed, 153 insertions(+) diff --git a/src/editor/engine/BabylonScene.js b/src/editor/engine/BabylonScene.js index 561296e..55d9cbc 100644 --- a/src/editor/engine/BabylonScene.js +++ b/src/editor/engine/BabylonScene.js @@ -2887,12 +2887,65 @@ export class BabylonScene { } } + // 3) Касания объектов, на которые подписан ГЛОБАЛЬНЫЙ скрипт через + // findOne(x).onTouch(...) (rt._watchedTouchRefs). Это объекты без + // собственного скрипта-target и не триггеры — например цели туториала. + // Ключ touchState = 'w:'+ref, событие — адресное (routeInstEvent по ref). + const watched = rt._watchedTouchRefs; + if (watched && watched.size > 0) { + for (const ref of watched) { + const target = this._refToTarget(ref); + if (!target) continue; + // Не дублируем, если на этот же объект уже висит target-скрипт + // (он обработан в блоке 1 и сам отправит touch своему скрипту — + // но адресное instTouch всё равно нужно глобальному подписчику, + // поэтому НЕ пропускаем, просто используем отдельный ключ 'w:'). + const aabb = this._targetAABB(target); + if (!aabb) continue; + const key = 'w:' + ref; + seen.add(key); + const overlap = + px + phw > aabb.minX - EPS && px - phw < aabb.maxX + EPS && + py + phh > aabb.minY - EPS && py - phh < aabb.maxY + EPS && + pz + phd > aabb.minZ - EPS && pz - phd < aabb.maxZ + EPS; + const wasTouching = this._touchState.get(key); + if (overlap && !wasTouching) { + this._touchState.set(key, true); + rt.routeInstEvent(ref, 'instTouch', {}); + } else if (!overlap && wasTouching) { + this._touchState.set(key, false); + rt.routeInstEvent(ref, 'instUntouch', {}); + } + } + } + // Чистим устаревшие записи (удалённые скрипты/триггеры) for (const id of this._touchState.keys()) { if (!seen.has(id)) this._touchState.delete(id); } } + /** Ref-строка 'primitive:NN' | 'model:NN' → {kind,id} для _targetAABB. */ + _refToTarget(ref) { + if (typeof ref !== 'string') return null; + const colon = ref.indexOf(':'); + if (colon < 0) return null; + const kind = ref.slice(0, colon); + const rest = ref.slice(colon + 1); + if (kind === 'primitive') { + // через runtime — корректно разрешает и local-ref, и числовой id + const id = this.gameRuntime?._resolvePrimitiveId + ? this.gameRuntime._resolvePrimitiveId(rest) + : (Number.isFinite(Number(rest)) ? Number(rest) : rest); + return { kind: 'primitive', id }; + } + if (kind === 'model') { + const n = Number(rest); + return { kind: 'model', id: Number.isFinite(n) ? n : rest }; + } + return null; + } + /** Получить мировой AABB target-объекта (для touch-детекции). */ _targetAABB(target) { if (!target) return null; @@ -5258,6 +5311,9 @@ export class BabylonScene { enterPlayMode() { if (this._isPlaying) return; this._isPlaying = true; + // Сброс состояния касаний — каждый прогон начинается «не касаясь», + // иначе rising-edge touch не сработает, если при стопе игрок стоял на цели. + if (this._touchState) this._touchState.clear(); // По умолчанию стандартный HUD видим в Play. // Скрипт может скрыть через game.hud.setVisible(false). this._setStdHudVisible(true); diff --git a/src/editor/engine/GameRuntime.js b/src/editor/engine/GameRuntime.js index 2aaae16..60a6fea 100644 --- a/src/editor/engine/GameRuntime.js +++ b/src/editor/engine/GameRuntime.js @@ -87,6 +87,13 @@ export class GameRuntime { modules[s.name] = s.code; } } + // Первичный snapshot сцены — собираем СИНХРОННО ДО запуска скриптов и + // передаём прямо в init. Иначе findOne() в синхронном теле скрипта + // (на старте) возвращает null, т.к. snapshot раньше слался лишь в rAF + // ПОСЛЕ init → подписки obj.onTouch/find на старте не работали + // (баг «стрелка-указатель не переключается на след. цель»). + let initialScene = null; + try { initialScene = this._buildSceneSnapshot(); } catch (e) { initialScene = null; } for (const s of scripts) { if (!s || typeof s.code !== 'string' || !s.code.trim()) { // eslint-disable-next-line no-console @@ -96,6 +103,7 @@ export class GameRuntime { const sb = new ScriptSandbox(s.code, s.target || null); sb.scriptId = s.id; sb.setModules(modules); + if (initialScene) sb.setInitialScene(initialScene); // Если target есть — передаём начальную позицию self до старта if (s.target) { const pos = this._collectSelfPosition(s.target); @@ -347,6 +355,8 @@ export class GameRuntime { this._objectData = {}; this._interactables = []; this._activeInteractRef = null; + this._watchedTouchRefs = null; + this._watchedClickRefs = null; this._roomState = {}; this._seenSessions = null; this._teams = new Map(); @@ -1142,6 +1152,17 @@ export class GameRuntime { } } + /** + * Адресное событие касания/клика КОНКРЕТНОГО объекта по его ref. + * Доставляется всем sandbox'ам как globalEvent с type='instTouch'|... + ref; + * worker матчит по ref на findOne(x).onTouch/onUntouch/onClick. + * type: 'instTouch' | 'instUntouch' | 'instClick'. + */ + routeInstEvent(ref, type, extra = {}) { + if (!ref || !type) return; + this.routeGlobalEvent(type, { ref, ...extra }); + } + /** * Уведомление: моб убит. Рассылается во все скрипты как 'mobKilled'. * Скрипт может подписаться через `game.onMobKilled(fn)`. @@ -2694,6 +2715,26 @@ export class GameRuntime { return; } // === Phase 6.2: Instance-модель === + // inst.watchTouch / inst.watchClick — скрипт подписался на касание/клик + // ПРОИЗВОЛЬНОГО объекта (findOne(x).onTouch/onClick). Движок начинает + // следить за AABB этого объекта в _detectTouchEvents и слать обратно + // instTouch/instUntouch (через routeInstEvent). Клик — через picking. + if (cmd === 'inst.watchTouch') { + const ref = payload && payload.ref; + if (typeof ref === 'string') { + if (!this._watchedTouchRefs) this._watchedTouchRefs = new Set(); + this._watchedTouchRefs.add(ref); + } + return; + } + if (cmd === 'inst.watchClick') { + const ref = payload && payload.ref; + if (typeof ref === 'string') { + if (!this._watchedClickRefs) this._watchedClickRefs = new Set(); + this._watchedClickRefs.add(ref); + } + return; + } // inst.set — изменить простое свойство Instance (name). // Сложные свойства (color/visible/...) идут через scene.set* и реализованы выше. if (cmd === 'inst.set') { diff --git a/src/editor/engine/ScriptSandbox.js b/src/editor/engine/ScriptSandbox.js index 9e56276..3e32ec4 100644 --- a/src/editor/engine/ScriptSandbox.js +++ b/src/editor/engine/ScriptSandbox.js @@ -53,6 +53,8 @@ export class ScriptSandbox { }); }; // Передаём код + target (если есть). Worker внутри сам выполнит его в new Function(game, code). + // initialScene — первичный snapshot сцены, чтобы findOne() работал + // в синхронном теле скрипта на старте (до прихода sceneSnapshot в rAF). this.worker.postMessage({ cmd: 'init', payload: { @@ -60,6 +62,7 @@ export class ScriptSandbox { target: this.target, selfPosition: this._initialSelfPosition || null, modules: this._modules || {}, + initialScene: this._initialScene || null, }, }); } @@ -69,6 +72,11 @@ export class ScriptSandbox { this._initialSelfPosition = p; } + /** Первичный snapshot сцены (до start) — чтобы findOne работал на старте. */ + setInitialScene(snap) { + this._initialScene = snap; + } + /** Установить код модулей для game.require — { 'имя': 'код' }. Вызывать до start(). */ setModules(modules) { this._modules = modules || {}; diff --git a/src/editor/engine/ScriptSandboxWorker.js b/src/editor/engine/ScriptSandboxWorker.js index eb4469f..471dc19 100644 --- a/src/editor/engine/ScriptSandboxWorker.js +++ b/src/editor/engine/ScriptSandboxWorker.js @@ -97,6 +97,15 @@ let _selfTouchHandlers = []; let _selfUntouchHandlers = []; // Подписки self.onInteract — взаимодействие по клавише E (ProximityPrompt) let _selfInteractHandlers = []; +// Подписки на касание/клик ПРОИЗВОЛЬНОГО объекта (через findOne(x).onTouch). +// ref → { touch:[fn], untouch:[fn], click:[fn] }. Движок следит за AABB этих +// объектов (cmd 'inst.watchTouch') и шлёт обратно instTouch/instUntouch/instClick. +const _instTouchHandlers = new Map(); +function _instHandlerBucket(ref) { + let b = _instTouchHandlers.get(ref); + if (!b) { b = { touch: [], untouch: [], click: [] }; _instTouchHandlers.set(ref, b); } + return b; +} // Зеркало всех GUI-элементов (синхронизируется main через cmd='guiSnapshot') let _guiIndex = []; // Задача 07: зеркало скинов (синхронизируется через 'skinsSnapshot'). @@ -467,6 +476,26 @@ function _getOrCreateInstance(ref, kindHint) { if (prop === 'setLabel') return (text, o) => _send('scene.setLabel', { ref, text, opts: o || {} }); if (prop === 'clearLabel') return () => _send('scene.clearLabel', { ref }); + // === События касания/клика ПРОИЗВОЛЬНОГО объекта === + // findOne('coin').onTouch(fn) — fn() когда игрок коснулся объекта. + // Аналог Roblox part.Touched:Connect. Движок начинает следить за AABB + // объекта (inst.watchTouch) и шлёт instTouch на rising edge. + if (prop === 'onTouch') return (fn) => { + if (typeof fn !== 'function') return; + _instHandlerBucket(ref).touch.push(fn); + _send('inst.watchTouch', { ref }); + }; + if (prop === 'onUntouch') return (fn) => { + if (typeof fn !== 'function') return; + _instHandlerBucket(ref).untouch.push(fn); + _send('inst.watchTouch', { ref }); + }; + if (prop === 'onClick') return (fn) => { + if (typeof fn !== 'function') return; + _instHandlerBucket(ref).click.push(fn); + _send('inst.watchClick', { ref }); + }; + return undefined; }, set(t, prop, value) { @@ -3538,6 +3567,17 @@ self.onmessage = (e) => { if (payload && payload.modules && typeof payload.modules === 'object') { _moduleCode = payload.modules; } + // Первичный snapshot сцены — заполняем _sceneIndex ДО исполнения кода, + // чтобы findOne()/find() работали в синхронном теле скрипта на старте + // (иначе obj.onTouch(...) не подписывался — объект ещё «не существовал»). + if (payload && payload.initialScene && typeof payload.initialScene === 'object') { + const s = payload.initialScene; + _sceneIndex = { + blocks: s.blocks || [], + models: s.models || [], + primitives: s.primitives || [], + }; + } try { const exportsObj = {}; const userFn = new Function('game', 'exports', '"use strict";\\n' + payload.code); @@ -3683,6 +3723,13 @@ self.onmessage = (e) => { } } else if (t === 'playerTouch') { for (const fn of _globalTouchHandlers) _safeCall(fn, payload, 'onPlayerTouch'); + } else if (t === 'instTouch' || t === 'instUntouch' || t === 'instClick') { + // Касание/клик произвольного объекта (findOne(x).onTouch/onUntouch/onClick). + const b = _instTouchHandlers.get(payload && payload.ref); + if (b) { + const list = t === 'instTouch' ? b.touch : t === 'instUntouch' ? b.untouch : b.click; + for (const fn of list) _safeCall(fn, payload, 'inst.' + t); + } } else if (t === 'hpChange') { for (const fn of _hpChangeHandlers) _safeCall(fn, payload, 'onHpChange'); } else if (t === 'mobKilled') { @@ -3927,6 +3974,7 @@ self.onmessage = (e) => { _selfTouchHandlers = []; _selfUntouchHandlers = []; _selfInteractHandlers = []; + _instTouchHandlers.clear(); _globalKeyDownHandlers = {}; _globalKeyUpHandlers = {}; _globalClickHandlers = []; -- 2.47.2