Merge pull request 'fix(engine): findOne(x).onTouch + findOne �� ������ (�������-��������� �������������)' (#19) from fix/pointer-ontouch-findone into main
This commit is contained in:
commit
5724e25340
@ -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);
|
||||
|
||||
@ -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') {
|
||||
|
||||
@ -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 || {};
|
||||
|
||||
@ -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 = [];
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user