消消方块阵换皮表情
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

988 lines
31 KiB

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;