import { _decorator, CCInteger, CCFloat, Vec3, IAssembler, IAssemblerManager, RenderData, IRenderData, UIRenderer, SpriteFrame, v3, macro, DynamicAtlasManager, Color, Mat4, Node, sys, } from 'cc'; import { JSB, MINIGAME } from 'cc/env'; import { BattleType } from '../../script/manager/ChapterDataManager'; /** 微信 iOS:拖尾环形缓冲区最大段数上限(Inspector 更大时仍按此封顶,减轻顶点与 overdraw) */ const _WX_IOS_TRAIL_MAX_POINTS_CAP = 12; function _isWeChatIosTrail(): boolean { return sys.platform === sys.Platform.WECHAT_GAME && sys.os === sys.OS.IOS; } const { ccclass, property, menu, help } = _decorator; if (JSB || MINIGAME) { macro.CLEANUP_IMAGE_CACHE = false; DynamicAtlasManager.instance.enabled = true; } /**拖尾 +++ 会打断合批渲染(微信 iOS 下见 {@link _ringCap} / {@link _minDistForSample} 等降载) */ @ccclass @menu('SuperTrail') @help('https://github.com/soidaken/SuperTrail') export class SuperTrail extends UIRenderer { @property(SpriteFrame) private _spriteFrame: SpriteFrame = null!; @property({ type: SpriteFrame, tooltip: '拖尾纹理', displayName: '纹理文件', }) get spriteFrame(): SpriteFrame { return this._spriteFrame; } // 添加缓存变量 private _uvMin: number = 0; private _uvMax: number = 1; private _vMin: number = 0; private _vMax: number = 1; private _uvAreaH: number = 1; private _uvDirty: boolean = true; set spriteFrame(value: SpriteFrame) { if (this._spriteFrame === value) return; this._spriteFrame = value; if (this.renderData) this.renderData.textureDirty = true; if (this._spriteFrame) { DynamicAtlasManager.instance.packToDynamicAtlas(this, this._spriteFrame); } this._uvDirty = true; this.markForUpdateRenderData(); } @property({ type: CCInteger, min: 4, tooltip: '最多保留多少个采样点(点越多越平滑,但更耗)' }) public maxPoints = 20; @property({ type: CCFloat, min: 0.1, tooltip: '两次采样点的最小距离(越大越省)' }) public minDistance = 3; @property({ type: CCFloat, min: 0, tooltip: '沿「上一帧→当前帧」位移方向相对 followTarget 前伸(px),贴弹头;0 关闭。勿用轨迹末点算方向,否则带宽条易扭成螺旋', }) public sampleLeadPx = 0; @property({ type: CCFloat, min: 0, tooltip: '头部宽度(最新点)' }) public headWidth = 32; @property({ type: CCFloat, min: 0, tooltip: '尾部宽度(最旧点)' }) public tailWidth = 0; @property({ type: CCInteger, min: 0, max: 255, tooltip: '头部透明度(最新点)' }) public headAlpha = 255; @property({ type: CCInteger, min: 0, max: 255, tooltip: '尾部透明度(最旧点)' }) public tailAlpha = 0; @property({ type: CCFloat, min: 0, tooltip: '停止移动后拖尾完全消失所需时间(秒),0表示不自动衰减' }) public fadeTime = 0.1; @property({ type: Color, tooltip: '头部颜色(最新点)' }) public headColor: Color = new Color(255, 255, 255, 255); @property({ type: Color, tooltip: '尾部颜色(最旧点)' }) public tailColor: Color = new Color(255, 255, 255, 255); /** * 若指定则从该节点读取世界坐标采样轨迹;不指定则从本节点采样(默认)。 * 拖尾挂在 ui_skillTraillayer 等统一层、跟随点仍在武器上时绑定武器上的挂点节点即可。 */ @property({ type: Node, tooltip: '可选:世界坐标采样节点(如枪口挂点)。不填则用本节点世界坐标。', }) public followTarget: Node | null = null; @property({ type: CCInteger, min: 0, tooltip: '建筑堆叠高度(米)达到该值及以上时不显示拖尾(与 BattleCore.TetriStackHeightMeters 一致,默认 30m)', }) public hideTrailFromHeightMeters = 30; /** setParent 到统一拖尾层前记录的原父节点(仅内部使用) */ private _originalParent: Node | null = null; /** 原父节点下的 sibling 顺序 */ private _originalSiblingIndex = 0; /** 相对原父节点的局部变换(进池还原必须与 prefab 一致) */ private readonly _originalLocalPos = new Vec3(); private readonly _originalLocalScale = new Vec3(); private readonly _originalLocalEuler = new Vec3(); // 是否暂停采样 private _paused: boolean = false; /** 是否因建筑高度过高而处于抑制状态(用于跨阈值时清一次尾) */ private _heightTrailSuppressed = false; /** 上一帧 follow 的原始世界坐标(用于 sampleLeadPx 的速度方向,避免用轨迹点算方向导致螺旋) */ private readonly _prevFollowWorld = new Vec3(); private _hasPrevFollowWorld = false; // 计算后的渲染数据 private _positions: number[] = []; private _uvs: number[] = []; private _indices: number[] = []; private _alphas: number[] = []; // 存储每个顶点的颜色 private _colors: number[] = []; // [r, g, b, r, g, b, ...] // 上一帧是否有新增点(用于判断是否停止移动) private _hasNewPoint: boolean = false; // 衰减累积器(支持小数累积) private _fadeAccum: number = 0; // 环形缓冲区:预分配的点对象池 private _pointPool: Vec3[] = []; // 环形缓冲区:头部索引(最旧的点) private _pointHead: number = 0; // 环形缓冲区:当前有效点数量 private _pointCount: number = 0; get positions(): number[] { return this._positions; } get uvs(): number[] { return this._uvs; } get indices(): number[] { return this._indices; } get alphas(): number[] { return this._alphas; } get colors(): number[] { return this._colors; } public __preload(): void { super.__preload(); } public onLoad(): void { super.onLoad(); // 预分配点对象池 this._initPointPool(); if (this._spriteFrame) { DynamicAtlasManager.instance.packToDynamicAtlas(this, this._spriteFrame); } } /** 环形缓冲区逻辑容量:微信 iOS 封顶,减轻每帧顶点计算与半透明 overdraw */ private _ringCap(): number { if (_isWeChatIosTrail()) { return Math.min(this.maxPoints, _WX_IOS_TRAIL_MAX_POINTS_CAP); } return this.maxPoints; } /** 微信 iOS 略增大采样间距,减少加点频率 */ private _minDistForSample(): number { if (_isWeChatIosTrail()) { return Math.max(this.minDistance * 1.35, this.minDistance + 2.5); } return this.minDistance; } private _initPointPool(): void { const poolSize = this.maxPoints; // 确保对象池大小足够 while (this._pointPool.length < poolSize) { this._pointPool.push(v3()); } this._pointHead = 0; this._pointCount = 0; } private _addPoint(x: number, y: number, z: number): void { const cap = this._ringCap(); if (this._pointCount < cap) { // 还没填满,直接在末尾添加 const idx = (this._pointHead + this._pointCount) % cap; this._pointPool[idx].set(x, y, z); this._pointCount++; } else { // 已满,覆盖最旧的点(头部),头部后移 this._pointPool[this._pointHead].set(x, y, z); this._pointHead = (this._pointHead + 1) % cap; } } private _removeOldestPoint(): void { if (this._pointCount > 0) { this._pointHead = (this._pointHead + 1) % this._ringCap(); this._pointCount--; } } private _getPoint(index: number): Vec3 { const cap = this._ringCap(); // index: 0 = 最旧的点,_pointCount - 1 = 最新的点 const idx = (this._pointHead + index) % cap; return this._pointPool[idx]; } private _getLastPoint(): Vec3 | null { if (this._pointCount === 0) return null; const cap = this._ringCap(); const idx = (this._pointHead + this._pointCount - 1) % cap; return this._pointPool[idx]; } public onEnable(): void { super.onEnable(); // 清空采样点状态 this._pointHead = 0; this._pointCount = 0; this._fadeAccum = 0; this._hasNewPoint = false; this._hasPrevFollowWorld = false; // 清空渲染数据缓存(与 onDisable 对应) this._positions.length = 0; this._uvs.length = 0; this._indices.length = 0; this._alphas.length = 0; this._colors.length = 0; //确保对象池已初始化 if (this._pointPool.length < this.maxPoints) { this._initPointPool(); } //重新刷新 assembler(会自动重建 renderData) this._flushAssembler(); // 标记 this.markForUpdateRenderData(); } onDisable(): void { super.onDisable(); // 清空状态(与 onEnable 对应) this._pointHead = 0; this._pointCount = 0; this._fadeAccum = 0; this._hasNewPoint = false; this._hasPrevFollowWorld = false; // 清空渲染缓存 this._positions.length = 0; this._uvs.length = 0; this._indices.length = 0; this._alphas.length = 0; this._colors.length = 0; // 销毁 renderData if (this.renderData) { this.destroyRenderData(); } } private currentWorldPos = new Vec3(); // 用于世界坐标到局部坐标的转换 private _inverseWorldMatrix = new Mat4(); private _tempVec3 = new Vec3(); /** 轨迹采样用的世界坐标来源:followTarget 有效则用其世界坐标,否则用本节点 */ private _getSampleSourceNode(): Node { const t = this.followTarget; return t?.isValid ? t : this.node; } /** * 把拖尾挂到 `ui_skillTraillayer` 等节点**之前**调用,记录当前父节点与兄弟序, * 子弹回收时再调用 {@link restoreOriginalPlacement} 即可挂回原位。 */ public stashOriginalPlacement(): void { const p = this.node.parent; if (!p?.isValid) return; this._originalParent = p; this._originalSiblingIndex = this.node.getSiblingIndex(); this._originalLocalPos.set(this.node.position); this._originalLocalScale.set(this.node.scale); this._originalLocalEuler.set(this.node.eulerAngles); } /** * 挂回 {@link stashOriginalPlacement} 时记录的父节点、sibling 与**局部 position/scale/euler**(与 prefab 一致,避免进池后变换错乱)。 * 并清除 `followTarget`(恢复用本节点世界坐标采样)。 * @param clearTrail 是否调用 {@link clear}(进池一般建议 true) */ public restoreOriginalPlacement(clearTrail = true): void { this.followTarget = null; const p = this._originalParent; if (!p?.isValid) { this._originalParent = null; if (clearTrail) this.clear(); return; } this.node.setParent(p, false); this.node.setPosition(this._originalLocalPos); this.node.setScale(this._originalLocalScale); this.node.eulerAngles = this._originalLocalEuler; const len = p.children.length; if (len > 0) { const idx = Math.max(0, Math.min(this._originalSiblingIndex, len - 1)); this.node.setSiblingIndex(idx); } this._originalParent = null; if (clearTrail) this.clear(); } /** 建筑堆叠高度是否已达到「不显示拖尾」阈值(米,来自 BattleCore.TetriStackHeightMeters) */ private _isBuildingTooHighForTrail(): boolean { const battle = gg.game?.CurentBattle; if (!battle) return false; return battle.TetriStackHeightMeters >= this.hideTrailFromHeightMeters; } /** * 高度 ≥ hideTrailFromHeightMeters 时清空并停止采样;降至阈值以下后恢复。 * @returns true 表示本帧应跳过后续拖尾逻辑 */ private _updateBuildingHeightSuppression(): boolean { if(gg.game?.CurentBattle?.BattleType == BattleType.OrangeWeaponMode_Ya){ return false; } const suppress = this._isBuildingTooHighForTrail(); if (suppress) { if (!this._heightTrailSuppressed) { this.clear(); } this._heightTrailSuppressed = true; if (this.node?.isValid) { this.node.active = false; } return true; } if (this._heightTrailSuppressed && this.node?.isValid) { this.node.active = true; } this._heightTrailSuppressed = false; return false; } /** * 使用 lateUpdate:followTarget 常挂在 RigidBody2D 上,世界坐标在物理步之后才最终确定; * 若在 update 里采样会落后一帧,拖尾与弹头之间会出现可见间隙。 */ protected lateUpdate(dt: number): void { if (!gg.game?.CurentBattle?.canUpdateFrame()) return; if (!this._spriteFrame?.texture) return; if (this._updateBuildingHeightSuppression()) return; // 跟随引用已失效时仅解引用,不销毁拖尾;回收时请调用 restoreOriginalPlacement const ft = this.followTarget; if (ft != null && !ft.isValid) { this.followTarget = null; } // 确保对象池已初始化且大小正确 if (this._pointPool.length < this.maxPoints) { this._initPointPool(); } const minDist = this._minDistForSample(); const minDistSq = minDist * minDist; /** 微信 iOS 关闭前伸采样,少一次分支与距离计算 */ const sampleLeadPxEff = _isWeChatIosTrail() ? 0 : this.sampleLeadPx; // 自动采样节点位置(可选 followTarget:拖尾节点与采样点分离) if (!this._paused) { this._getSampleSourceNode().getWorldPosition(this.currentWorldPos); const rawX = this.currentWorldPos.x; const rawY = this.currentWorldPos.y; const rawZ = this.currentWorldPos.z; let sx = rawX; let sy = rawY; let sz = rawZ; if (sampleLeadPxEff > 0 && this._hasPrevFollowWorld) { const rdx = rawX - this._prevFollowWorld.x; const rdy = rawY - this._prevFollowWorld.y; const lenSq = rdx * rdx + rdy * rdy; if (lenSq > 1e-6) { const inv = sampleLeadPxEff / Math.sqrt(lenSq); sx = rawX + rdx * inv; sy = rawY + rdy * inv; } } this._prevFollowWorld.set(rawX, rawY, rawZ); this._hasPrevFollowWorld = true; const last = this._getLastPoint(); this._hasNewPoint = false; if (last) { const dx = sx - last.x; const dy = sy - last.y; if (dx * dx + dy * dy >= minDistSq) { this._addPoint(sx, sy, sz); this._hasNewPoint = true; } } else { this._addPoint(sx, sy, sz); this._hasNewPoint = true; } } else { // 暂停时也需要更新世界坐标(用于渲染计算) this._getSampleSourceNode().getWorldPosition(this.currentWorldPos); this._hasNewPoint = false; } // 自动衰减:如果没有新增点且 fadeTime > 0,逐渐移除尾部点 let dataChanged = false; const fadeTimeEff = _isWeChatIosTrail() && this.fadeTime > 0 ? Math.max(0.04, this.fadeTime * 0.82) : this.fadeTime; if (!this._hasNewPoint && fadeTimeEff > 0 && this._pointCount > 0) { const fadeSpeed = this._pointCount / fadeTimeEff; this._fadeAccum += fadeSpeed * dt; const removeCount = Math.floor(this._fadeAccum); if (removeCount > 0) { this._fadeAccum -= removeCount; for (let i = 0; i < removeCount && this._pointCount > 0; i++) { this._removeOldestPoint(); } dataChanged = true; // 点数减少了,数据发生了变化 } } else { this._fadeAccum = 0; } // 如果有新点添加,也标记数据变化 if (this._hasNewPoint) { dataChanged = true; } // 只有有足够的点才计算和渲染 if (this._pointCount >= 2) { this._calculateVerticesAndUVIndices(); } else { this._positions.length = 0; this._uvs.length = 0; this._indices.length = 0; this._alphas.length = 0; this._colors.length = 0; dataChanged = true; // 清空数据也是一种变化 } // 只有在数据真正变化时才标记为 dirty if (dataChanged) { if (this.renderData) { this.renderData.vertDirty = true; } this.markForUpdateRenderData(); } } /** 有时候 避免出现跨越式的拖尾渲染 / 需要立即清空拖尾 / 对象池复用需要清空状态 */ public clear(): void { // 清空采样点 this._pointHead = 0; this._pointCount = 0; //重置衰减累积器,避免清空后立即触发衰减逻辑 this._fadeAccum = 0; //重置新点标记 this._hasNewPoint = false; this._hasPrevFollowWorld = false; // 清空渲染数据缓存 this._positions.length = 0; this._uvs.length = 0; this._indices.length = 0; this._alphas.length = 0; this._colors.length = 0; // 标记渲染数据需要更新 if (this.renderData) { this.renderData.vertDirty = true; } this.markForUpdateRenderData(); } /** * 暂停采样(停止添加新点,但保留现有拖尾,衰减仍会继续) */ public pause(): void { this._paused = true; } /** * 恢复采样 */ public resume(): void { this._paused = false; } /** * 获取当前是否暂停 */ public isPaused(): boolean { return this._paused; } private _calculateVerticesAndUVIndices(): void { const n = this._pointCount; if (n < 2) { this._positions.length = 0; this._uvs.length = 0; this._indices.length = 0; this._alphas.length = 0; this._colors.length = 0; return; } const sf = this._spriteFrame; if (!sf) return; if (this._uvDirty) { const uv8 = sf.uv; this._uvMin = uv8[0]; this._uvMax = uv8[0]; this._vMin = uv8[1]; this._vMax = uv8[1]; for (let i = 0; i < 8; i += 2) { const u = uv8[i]; const v = uv8[i + 1]; if (u < this._uvMin) this._uvMin = u; if (u > this._uvMax) this._uvMax = u; if (v < this._vMin) this._vMin = v; if (v > this._vMax) this._vMax = v; } this._uvAreaH = this._vMax - this._vMin; this._uvDirty = false; } // 获取逆世界矩阵,用于将世界坐标转换为局部坐标 Mat4.invert(this._inverseWorldMatrix, this.node.worldMatrix); // 获取当前节点的世界坐标,用于计算相对位置 // const nodeWorldPos = this.node.worldPosition; // const nodeWorldPos = this.currentWorldPos; // 在循环外预计算 const tailAlphaNorm = this.tailAlpha / 255; const headAlphaNorm = (_isWeChatIosTrail() ? Math.min(255, Math.max(0, Math.floor(this.headAlpha * 0.9))) : this.headAlpha) / 255; const tailColorR = this.tailColor.r / 255; const tailColorG = this.tailColor.g / 255; const tailColorB = this.tailColor.b / 255; const headColorR = this.headColor.r / 255; const headColorG = this.headColor.g / 255; const headColorB = this.headColor.b / 255; // 预计算数组大小 const vertCount = n * 2; const posLen = vertCount * 3; const uvLen = vertCount * 2; const colorLen = vertCount * 3; // 直接设置数组长度,确保 Native 平台数据一致性 this._positions.length = posLen; this._uvs.length = uvLen; this._alphas.length = vertCount; this._colors.length = colorLen; // 使用索引赋值 let posIdx = 0, uvIdx = 0, alphaIdx = 0, colorIdx = 0; // 缓存逆矩阵元素,避免循环内重复访问 const im = this._inverseWorldMatrix; for (let i = 0; i < n; i++) { const p = this._getPoint(i); const pPrev = this._getPoint(i > 0 ? i - 1 : i); const pNext = this._getPoint(i < n - 1 ? i + 1 : i); // 计算切线方向 let dx = pNext.x - pPrev.x; let dy = pNext.y - pPrev.y; // const len = Math.hypot(dx, dy) || 1; const lenSq = dx * dx + dy * dy; const len = lenSq > 0.0001 ? Math.sqrt(lenSq) : 1; dx /= len; dy /= len; // 法线方向(垂直于切线) const nx = -dy; const ny = dx; // 插值参数:0 = 尾部,1 = 头部 const t = i / (n - 1); const halfW = (this.tailWidth + (this.headWidth - this.tailWidth) * t) * 0.5; const alpha = tailAlphaNorm + (headAlphaNorm - tailAlphaNorm) * t; // 添加颜色插值计算,直接内联插值函数 const r = tailColorR + (headColorR - tailColorR) * t; const g = tailColorG + (headColorG - tailColorG) * t; const b = tailColorB + (headColorB - tailColorB) * t; // 左右两个顶点的世界坐标 const wlx = p.x + nx * halfW; const wly = p.y + ny * halfW; const wrx = p.x - nx * halfW; const wry = p.y - ny * halfW; // 关键:将世界坐标转换为相对于当前节点的局部坐标 // const localLx = wlx - nodeWorldPos.x; // const localLy = wly - nodeWorldPos.y; // const localRx = wrx - nodeWorldPos.x; // const localRy = wry - nodeWorldPos.y; //使用逆世界矩阵将世界坐标正确转换为局部坐标; // 这样可以正确处理节点的旋转、缩放 let rhw = im.m03 * wlx + im.m07 * wly + im.m15; rhw = rhw ? 1 / rhw : 1; const localLx = (im.m00 * wlx + im.m04 * wly + im.m12) * rhw; const localLy = (im.m01 * wlx + im.m05 * wly + im.m13) * rhw; rhw = im.m03 * wrx + im.m07 * wry + im.m15; rhw = rhw ? 1 / rhw : 1; const localRx = (im.m00 * wrx + im.m04 * wry + im.m12) * rhw; const localRy = (im.m01 * wrx + im.m05 * wry + im.m13) * rhw; // this._positions.push(localLx, localLy, 0); // this._positions.push(localRx, localRy, 0); this._positions[posIdx++] = localLx; this._positions[posIdx++] = localLy; this._positions[posIdx++] = 0; this._positions[posIdx++] = localRx; this._positions[posIdx++] = localRy; this._positions[posIdx++] = 0; // UV坐标:沿着拖尾方向从尾到头 const vV = this._vMin + this._uvAreaH * (1 - t); // this._uvs.push(this._uvMin, vV); // 左顶点 // this._uvs.push(this._uvMax, vV); // 右顶点 this._uvs[uvIdx++] = this._uvMin; this._uvs[uvIdx++] = vV; this._uvs[uvIdx++] = this._uvMax; this._uvs[uvIdx++] = vV; // 存储每个顶点的透明度 // this._alphas.push(alpha); // this._alphas.push(alpha); this._alphas[alphaIdx++] = alpha; this._alphas[alphaIdx++] = alpha; //存储每个顶点的颜色; // this._colors.push(r, g, b); // this._colors.push(r, g, b); this._colors[colorIdx++] = r; this._colors[colorIdx++] = g; this._colors[colorIdx++] = b; this._colors[colorIdx++] = r; this._colors[colorIdx++] = g; this._colors[colorIdx++] = b; } // 数组长度已在开始时设置,无需再次调整 // this._positions.length = posIdx; // this._uvs.length = uvIdx; // this._alphas.length = alphaIdx; // this._colors.length = colorIdx; // 生成索引:每两个相邻的点构成一个四边形(两个三角形) const indexCount = (n - 1) * 6; // 每个四边形 2 个三角形 * 3 个顶点 this._indices.length = indexCount; let indexIdx = 0; for (let i = 0; i < n - 1; i++) { const v0 = i * 2; const v1 = i * 2 + 1; const v2 = i * 2 + 2; const v3 = i * 2 + 3; this._indices[indexIdx++] = v0; this._indices[indexIdx++] = v1; this._indices[indexIdx++] = v2; this._indices[indexIdx++] = v2; this._indices[indexIdx++] = v1; this._indices[indexIdx++] = v3; } } protected _canRender(): boolean { if (!super._canRender()) return false; if (this._isBuildingTooHighForTrail()) return false; if (!this._spriteFrame || !this._spriteFrame.texture) return false; return this._pointCount >= 2; } // @ts-ignore protected _render(render: IBatcher): void { render.commitComp(this, this.renderData, this._spriteFrame, this._assembler!, null); } protected _flushAssembler(): void { const assembler = SuperTrail.Assembler.getAssembler(this); if (this._assembler !== assembler) { this.destroyRenderData(); this._assembler = assembler; } if (!this.renderData) { if (this._assembler && this._assembler.createData) { this._renderData = this._assembler.createData(this) as RenderData; this.renderData!.material = this.material; this._updateColor(); } } } public static Assembler: IAssemblerManager; } class SuperTrailAssemblerImpl implements IAssembler { createData(comp: SuperTrail): RenderData { const renderData = comp.requestRenderData(); renderData.dataLength = 4; renderData.resize(4, 6); return renderData; } updateRenderData(comp: SuperTrail): void { if (!comp) return; if (!comp.spriteFrame?.texture) return; const renderData = comp.renderData; if (!renderData) return; const vertCount = comp.positions.length / 3; const indexCount = comp.indices.length; if (vertCount === 0 || indexCount === 0) { renderData.vertDirty = false; return; } if (renderData.dataLength !== vertCount) { renderData.dataLength = vertCount; } const dataList: IRenderData[] = renderData.data; for (let i = 0; i < vertCount; ++i) { const item = dataList[i]; item.x = comp.positions[i * 3]; item.y = comp.positions[i * 3 + 1]; } if (renderData.vertexCount !== vertCount || renderData.indexCount !== indexCount) { // comp.renderEntity.colorDirty = true; renderData.resize(vertCount, indexCount); } if (JSB) { // 在 JSB 模式下,确保所有数据数组长度匹配后再更新 const expectedUvLen = vertCount * 2; const expectedColorLen = vertCount * 3; const expectedAlphaLen = vertCount; // 只有当所有数据完整时才更新,否则跳过本次更新避免读取不一致的数据 if ( comp.uvs.length === expectedUvLen && comp.colors.length === expectedColorLen && comp.alphas.length === expectedAlphaLen && comp.indices.length === indexCount ) { const tmp = new Uint16Array(indexCount); const indices = comp.indices; for (let i = 0; i < indexCount; ++i) { tmp[i] = indices[i]; } renderData.chunk.setIndexBuffer(tmp); this._updateJustUV(comp); } } renderData.updateRenderData(comp, comp.spriteFrame); renderData.vertDirty = false; } // @ts-ignore fillBuffers(comp: SuperTrail, renderer: IBatcher): void { if (!comp) return; const renderData = comp.renderData; if (!renderData) return; const vertCount = comp.positions.length / 3; const indexCount = comp.indices.length; if (vertCount === 0 || indexCount === 0) return; this._updateVertexsAndUV(comp); this._updateIndices(comp); } private _updateIndices(comp: SuperTrail): void { const renderData = comp.renderData; if (!renderData) return; const chunk = renderData.chunk; const vid = chunk.vertexOffset; const meshBuffer = chunk.meshBuffer; const ib = meshBuffer.iData; let indexOffset = meshBuffer.indexOffset; const indices = comp.indices; for (let i = 0; i < indices.length; ++i) { ib[indexOffset++] = vid + indices[i]; } meshBuffer.indexOffset += indices.length; } private _updateVertexsAndUV(comp: SuperTrail): void { const renderData = comp.renderData; if (!renderData) return; const vertCount = comp.positions.length / 3; // 验证数据完整性 const expectedPosLen = vertCount * 3; const expectedUvLen = vertCount * 2; const expectedColorLen = vertCount * 3; const expectedAlphaLen = vertCount; // 如果数据不完整,直接返回 if ( comp.positions.length !== expectedPosLen || comp.uvs.length !== expectedUvLen || comp.colors.length !== expectedColorLen || comp.alphas.length !== expectedAlphaLen ) { return; } const chunk = renderData.chunk; const vb = chunk.vb; const m = comp.node.worldMatrix; const stride = renderData.floatStride; const baseColor = comp.color; const colorR = baseColor.r / 255; const colorG = baseColor.g / 255; const colorB = baseColor.b / 255; for (let i = 0; i < vertCount; ++i) { const posIdx = i * 3; const uvIdx = i * 2; const colorIdx = i * 3; const x = comp.positions[posIdx]; const y = comp.positions[posIdx + 1]; let rhw = m.m03 * x + m.m07 * y + m.m15; rhw = rhw ? 1 / rhw : 1; const offset = i * stride; vb[offset + 0] = (m.m00 * x + m.m04 * y + m.m12) * rhw; vb[offset + 1] = (m.m01 * x + m.m05 * y + m.m13) * rhw; vb[offset + 2] = (m.m02 * x + m.m06 * y + m.m14) * rhw; vb[offset + 3] = comp.uvs[uvIdx]; vb[offset + 4] = comp.uvs[uvIdx + 1]; vb[offset + 5] = comp.colors[colorIdx]; // r vb[offset + 6] = comp.colors[colorIdx + 1]; // g vb[offset + 7] = comp.colors[colorIdx + 2]; // b vb[offset + 8] = comp.alphas[i]; // a } } private _updateJustUV(comp: SuperTrail): void { const renderData = comp.renderData; if (!renderData) return; const vertCount = comp.positions.length / 3; // 验证数据完整性 - 关键!确保所有数组长度匹配 const expectedUvLen = vertCount * 2; const expectedColorLen = vertCount * 3; const expectedAlphaLen = vertCount; // 如果数据不完整,直接返回,不更新(避免访问越界或未初始化的数据) if (comp.uvs.length !== expectedUvLen || comp.colors.length !== expectedColorLen || comp.alphas.length !== expectedAlphaLen) { return; } const chunk = renderData.chunk; const vb = chunk.vb; const stride = renderData.floatStride; for (let i = 0; i < vertCount; ++i) { const offset = i * stride; const uvIdx = i * 2; const colorIdx = i * 3; vb[offset + 3] = comp.uvs[uvIdx]; vb[offset + 4] = comp.uvs[uvIdx + 1]; vb[offset + 5] = comp.colors[colorIdx]; // r vb[offset + 6] = comp.colors[colorIdx + 1]; // g vb[offset + 7] = comp.colors[colorIdx + 2]; // b vb[offset + 8] = comp.alphas[i]; // a } } updateColor(comp: SuperTrail): void { const renderData = comp.renderData; if (!renderData) return; const vertCount = comp.positions.length / 3; if (vertCount === 0) return; // 验证数据完整性 const expectedColorLen = vertCount * 3; const expectedAlphaLen = vertCount; // 如果数据不完整,直接返回 if (comp.colors.length !== expectedColorLen || comp.alphas.length !== expectedAlphaLen) { return; } const chunk = renderData.chunk; const vb = chunk.vb; const stride = renderData.floatStride; for (let i = 0; i < vertCount; ++i) { const offset = i * stride; const colorIdx = i * 3; vb[offset + 5] = comp.colors[colorIdx]; // r vb[offset + 6] = comp.colors[colorIdx + 1]; // g vb[offset + 7] = comp.colors[colorIdx + 2]; // b vb[offset + 8] = comp.alphas[i]; // a } } } const superTrailAssemblerImpl = new SuperTrailAssemblerImpl(); const superTrailAssemblerImplMgr: IAssemblerManager = { getAssembler(_comp: SuperTrail): IAssembler { return superTrailAssemblerImpl; }, }; SuperTrail.Assembler = superTrailAssemblerImplMgr;