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
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;
|
|
|