/** * LabelManager — billboard-метки (текст-плашки) над 3D-объектами. * * Используется для game.scene.setLabel(ref, text) — имена/HP над * персонажами, врагами, предметами. Метка всегда повёрнута лицом к камере * (billboardMode=7), всегда поверх геометрии (renderingGroupId=1). * * Метка привязывается к мешу объекта (parent) и висит над ним. */ import { DynamicTexture } from '@babylonjs/core/Materials/Textures/dynamicTexture'; import { StandardMaterial } from '@babylonjs/core/Materials/standardMaterial'; import { MeshBuilder } from '@babylonjs/core/Meshes/meshBuilder'; import { Color3 } from '@babylonjs/core/Maths/math.color'; export class LabelManager { constructor(scene) { this.scene = scene; // ref-строка объекта → { plane, tex, mat } this.labels = new Map(); } /** * Установить/обновить метку над объектом. * ref — ref-строка объекта (от scene.spawn / scene.find). * anchorMesh — Babylon-меш объекта (метка крепится к нему). * text — текст метки. * opts — { color: '#fff', height: 2.5 (м над объектом), size: 1 } */ setLabel(ref, anchorMesh, text, opts = {}) { if (!anchorMesh) return; const color = opts.color || '#ffffff'; const heightAbove = Number.isFinite(opts.height) ? opts.height : 2.5; const sizeMul = Number.isFinite(opts.size) ? opts.size : 1; // Если метка уже есть — пересоздаём (текст/цвет могли измениться). this.clearLabel(ref); const W = 1024, H = 256; const tex = new DynamicTexture(`lblTex_${ref}_${Date.now()}`, { width: W, height: H }, this.scene, true); tex.updateSamplingMode?.(3); // TRILINEAR tex.anisotropicFilteringLevel = 8; const ctx = tex.getContext(); ctx.clearRect(0, 0, W, H); ctx.font = 'bold 120px "Roboto Condensed", "Segoe UI", sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.lineWidth = 16; ctx.lineJoin = 'round'; ctx.strokeStyle = '#000'; ctx.strokeText(String(text), W / 2, H / 2); ctx.fillStyle = color; ctx.fillText(String(text), W / 2, H / 2); tex.update(true); tex.hasAlpha = true; const plane = MeshBuilder.CreatePlane(`lbl_${ref}`, { width: 2.2 * sizeMul, height: 0.55 * sizeMul }, this.scene); const mat = new StandardMaterial(`lblMat_${ref}`, this.scene); mat.diffuseTexture = tex; mat.diffuseTexture.hasAlpha = true; mat.emissiveColor = new Color3(1, 1, 1); mat.disableLighting = true; mat.backFaceCulling = false; mat.disableDepthWrite = true; plane.material = mat; plane.billboardMode = 7; // всегда лицом к камере plane.renderingGroupId = 1; // поверх геометрии plane.isPickable = false; // Крепим к объекту: метка висит над ним и двигается вместе с ним. plane.parent = anchorMesh; plane.position.set(0, heightAbove, 0); this.labels.set(ref, { plane, tex, mat }); } /** Убрать метку с объекта. */ clearLabel(ref) { const rec = this.labels.get(ref); if (!rec) return; try { rec.plane.dispose(); } catch (e) { /* ignore */ } try { rec.tex.dispose(); } catch (e) { /* ignore */ } try { rec.mat.dispose(); } catch (e) { /* ignore */ } this.labels.delete(ref); } /** Удалить все метки (при выходе из Play). */ clearAll() { for (const ref of [...this.labels.keys()]) this.clearLabel(ref); } }