2025-08-15 18:17:12 +08:00
|
|
|
import {
|
|
|
|
|
_decorator,
|
|
|
|
|
Component,
|
|
|
|
|
instantiate,
|
|
|
|
|
Node,
|
|
|
|
|
UITransform,
|
|
|
|
|
Vec3,
|
2025-08-27 16:12:19 +08:00
|
|
|
view,
|
2025-08-15 18:17:12 +08:00
|
|
|
} from "cc";
|
2025-08-11 16:08:59 +08:00
|
|
|
import { DialogManager } from "../../manager/DialogManager";
|
2025-07-21 22:14:06 +08:00
|
|
|
import { DialogBubble } from "./DialogBubble";
|
2025-08-13 11:31:21 +08:00
|
|
|
import { Dialog } from "../../data/DialogData";
|
2025-07-21 22:14:06 +08:00
|
|
|
const { ccclass, property } = _decorator;
|
|
|
|
|
|
|
|
|
|
@ccclass("ChatContentsLayout")
|
|
|
|
|
export class ChatContentsLayout extends Component {
|
2025-08-11 16:08:59 +08:00
|
|
|
manager: DialogManager = null;
|
2025-07-21 22:14:06 +08:00
|
|
|
|
2025-08-15 18:17:12 +08:00
|
|
|
@property(Node)
|
|
|
|
|
initPos: Node = null;
|
|
|
|
|
|
2025-07-21 22:14:06 +08:00
|
|
|
@property(DialogBubble)
|
|
|
|
|
lBubble: DialogBubble = null;
|
|
|
|
|
@property(DialogBubble)
|
|
|
|
|
rBubble: DialogBubble = null;
|
|
|
|
|
|
|
|
|
|
bubbles: DialogBubble[] = [];
|
2025-08-13 11:31:21 +08:00
|
|
|
cachedBubbles: DialogBubble[] = [];
|
2025-07-21 22:14:06 +08:00
|
|
|
|
2025-08-15 18:17:12 +08:00
|
|
|
fixMaxWidth: number;
|
|
|
|
|
|
2025-09-16 16:55:39 +08:00
|
|
|
// 用于跟踪延时任务,支持中断清理
|
|
|
|
|
private delayedTasks: number[] = [];
|
|
|
|
|
|
2025-07-21 22:14:06 +08:00
|
|
|
protected start(): void {
|
|
|
|
|
this.lBubble.node.active = false;
|
|
|
|
|
this.rBubble.node.active = false;
|
2025-08-27 16:12:19 +08:00
|
|
|
this.fixMaxWidth = view.getVisibleSize().width * 0.8;
|
2025-07-22 16:43:10 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected onEnable(): void {
|
2025-08-11 16:08:59 +08:00
|
|
|
if (!this.manager) this.manager = DialogManager.getInstance();
|
2025-07-22 16:43:10 +08:00
|
|
|
|
2025-07-21 22:14:06 +08:00
|
|
|
this.manager.layoutout = this;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
UpdateDialog(dialogs: Dialog[]) {
|
2025-09-16 16:55:39 +08:00
|
|
|
// 清理所有正在进行的延时任务
|
|
|
|
|
this.clearDelayedTasks();
|
|
|
|
|
|
2025-08-13 11:31:21 +08:00
|
|
|
// 隐藏当前使用的气泡并放入缓存
|
|
|
|
|
for (let i = 0; i < this.bubbles.length; i++) {
|
2025-07-21 22:14:06 +08:00
|
|
|
if (this.bubbles[i].node) {
|
2025-08-13 11:31:21 +08:00
|
|
|
this.bubbles[i].node.active = false;
|
|
|
|
|
this.cachedBubbles.push(this.bubbles[i]);
|
2025-07-21 22:14:06 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
this.bubbles = [];
|
2025-08-15 18:17:12 +08:00
|
|
|
|
2025-09-16 16:55:39 +08:00
|
|
|
this.updateDialogLayoutWithDelay(dialogs);
|
2025-08-15 18:17:12 +08:00
|
|
|
|
2025-08-13 12:55:27 +08:00
|
|
|
// 清理多余的缓存气泡,避免内存泄漏
|
|
|
|
|
this.cleanupExcessCachedBubbles();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private updateDialogLayout(dialogs: Dialog[]) {
|
2025-08-15 18:17:12 +08:00
|
|
|
let initPosY: number = this.initPos.position.y;
|
2025-08-13 12:55:27 +08:00
|
|
|
let pendingUpdates = 0;
|
2025-08-15 18:17:12 +08:00
|
|
|
|
2025-08-13 12:55:27 +08:00
|
|
|
const updateNextBubble = (index: number) => {
|
|
|
|
|
if (index < 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-08-15 18:17:12 +08:00
|
|
|
|
2025-08-13 12:55:27 +08:00
|
|
|
const dialog = dialogs[index];
|
2025-08-13 11:31:21 +08:00
|
|
|
let newBubble: DialogBubble = null;
|
2025-07-21 22:14:06 +08:00
|
|
|
|
2025-08-13 11:31:21 +08:00
|
|
|
// 尝试从缓存中获取合适的气泡
|
|
|
|
|
const isPlayerBubble = dialog.isPlayer;
|
|
|
|
|
for (let j = 0; j < this.cachedBubbles.length; j++) {
|
|
|
|
|
const cached = this.cachedBubbles[j];
|
|
|
|
|
if (cached && cached.node) {
|
|
|
|
|
// 检查气泡类型是否匹配(通过位置判断左右气泡)
|
|
|
|
|
const isRightBubble = cached.node.position.x > 0;
|
2025-08-15 18:17:12 +08:00
|
|
|
if (
|
|
|
|
|
(isPlayerBubble && isRightBubble) ||
|
|
|
|
|
(!isPlayerBubble && !isRightBubble)
|
|
|
|
|
) {
|
2025-08-13 11:31:21 +08:00
|
|
|
newBubble = cached;
|
|
|
|
|
this.cachedBubbles.splice(j, 1);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 如果缓存中没有合适的气泡,创建新的
|
|
|
|
|
if (!newBubble) {
|
|
|
|
|
let newBubbleNode = isPlayerBubble
|
|
|
|
|
? instantiate(this.rBubble.node)
|
|
|
|
|
: instantiate(this.lBubble.node);
|
|
|
|
|
newBubble = newBubbleNode.getComponent(DialogBubble);
|
2025-09-16 16:55:39 +08:00
|
|
|
newBubble.init(650);
|
2025-08-13 11:31:21 +08:00
|
|
|
newBubbleNode.setParent(this.node);
|
|
|
|
|
}
|
2025-07-21 22:14:06 +08:00
|
|
|
|
|
|
|
|
newBubble.node.active = true;
|
|
|
|
|
let pos = newBubble.node.position;
|
|
|
|
|
newBubble.node.position = new Vec3(pos.x, initPosY, pos.z);
|
2025-08-15 18:17:12 +08:00
|
|
|
|
2025-08-13 12:55:27 +08:00
|
|
|
pendingUpdates++;
|
2025-09-10 18:12:26 +08:00
|
|
|
newBubble.updateBubbleContent(
|
|
|
|
|
dialog.content,
|
2025-09-16 16:55:39 +08:00
|
|
|
dialog.isPlayer,
|
2025-09-10 18:12:26 +08:00
|
|
|
(actualHeight: number) => {
|
|
|
|
|
initPosY += actualHeight + 40;
|
|
|
|
|
pendingUpdates--;
|
2025-08-15 18:17:12 +08:00
|
|
|
|
2025-09-10 18:12:26 +08:00
|
|
|
// 当当前气泡更新完成后,处理下一个
|
|
|
|
|
if (pendingUpdates === 0) {
|
|
|
|
|
updateNextBubble(index - 1);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
dialog.isLoading || false
|
|
|
|
|
);
|
2025-08-15 18:17:12 +08:00
|
|
|
|
2025-07-21 22:14:06 +08:00
|
|
|
this.bubbles.push(newBubble);
|
2025-08-13 12:55:27 +08:00
|
|
|
};
|
2025-08-15 18:17:12 +08:00
|
|
|
|
2025-08-13 12:55:27 +08:00
|
|
|
// 从最后一个对话开始处理(倒序)
|
|
|
|
|
updateNextBubble(dialogs.length - 1);
|
2025-08-13 11:31:21 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private cleanupExcessCachedBubbles() {
|
|
|
|
|
const maxCachedBubbles = 20; // 最大缓存数量
|
|
|
|
|
if (this.cachedBubbles.length > maxCachedBubbles) {
|
|
|
|
|
const excessCount = this.cachedBubbles.length - maxCachedBubbles;
|
|
|
|
|
for (let i = 0; i < excessCount; i++) {
|
|
|
|
|
const bubble = this.cachedBubbles.shift();
|
|
|
|
|
if (bubble && bubble.node) {
|
|
|
|
|
bubble.node.destroy();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-07-21 22:14:06 +08:00
|
|
|
}
|
2025-09-16 16:55:39 +08:00
|
|
|
|
|
|
|
|
private clearDelayedTasks() {
|
|
|
|
|
// 清理所有正在进行的延时任务
|
|
|
|
|
for (const taskId of this.delayedTasks) {
|
|
|
|
|
clearTimeout(taskId);
|
|
|
|
|
}
|
|
|
|
|
this.delayedTasks = [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private groupConsecutiveNonPlayerDialogs(dialogs: Dialog[]): Dialog[][] {
|
|
|
|
|
const groups: Dialog[][] = [];
|
|
|
|
|
let currentGroup: Dialog[] = [];
|
|
|
|
|
|
|
|
|
|
for (const dialog of dialogs) {
|
|
|
|
|
if (dialog.isPlayer) {
|
|
|
|
|
// 遇到player消息,结束当前非player组
|
|
|
|
|
if (currentGroup.length > 0) {
|
|
|
|
|
groups.push([...currentGroup]);
|
|
|
|
|
currentGroup = [];
|
|
|
|
|
}
|
|
|
|
|
// player消息单独成组
|
|
|
|
|
groups.push([dialog]);
|
|
|
|
|
} else {
|
|
|
|
|
// 非player消息加入当前组
|
|
|
|
|
currentGroup.push(dialog);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 处理最后一组非player消息
|
|
|
|
|
if (currentGroup.length > 0) {
|
|
|
|
|
groups.push(currentGroup);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return groups;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private updateDialogLayoutWithDelay(dialogs: Dialog[]) {
|
|
|
|
|
const groups = this.groupConsecutiveNonPlayerDialogs(dialogs);
|
|
|
|
|
let initPosY: number = this.initPos.position.y;
|
|
|
|
|
|
|
|
|
|
// 倒序处理组,保持原有的显示顺序
|
|
|
|
|
this.processGroupsWithDelay(groups, groups.length - 1, initPosY);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private processGroupsWithDelay(
|
|
|
|
|
groups: Dialog[][],
|
|
|
|
|
groupIndex: number,
|
|
|
|
|
currentPosY: number
|
|
|
|
|
) {
|
|
|
|
|
if (groupIndex < 0) return;
|
|
|
|
|
|
|
|
|
|
const currentGroup = groups[groupIndex];
|
|
|
|
|
|
|
|
|
|
// 直接渲染组,延时逻辑已移到单条消息级别
|
|
|
|
|
this.renderDialogGroup(currentGroup, currentPosY, (newPosY) => {
|
|
|
|
|
// 递归处理下一组,传递更新后的位置
|
|
|
|
|
this.processGroupsWithDelay(groups, groupIndex - 1, newPosY);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private renderDialogGroup(
|
|
|
|
|
dialogs: Dialog[],
|
|
|
|
|
startPosY: number,
|
|
|
|
|
onComplete: (finalPosY: number) => void
|
|
|
|
|
) {
|
|
|
|
|
let currentPosY = startPosY;
|
|
|
|
|
|
|
|
|
|
const updateNextBubble = (index: number) => {
|
|
|
|
|
if (index < 0) {
|
|
|
|
|
onComplete(currentPosY);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const dialog = dialogs[index];
|
|
|
|
|
|
|
|
|
|
// 计算当前消息的延时:非player消息且不是组内最后一条时添加延时
|
|
|
|
|
const isNonPlayerMessage = !dialog.isPlayer;
|
|
|
|
|
const isLastInGroup = index === dialogs.length - 1;
|
|
|
|
|
const delay =
|
|
|
|
|
isNonPlayerMessage && !isLastInGroup ? Math.random() * 1000 + 500 : 0; // 500-1500ms随机延时
|
|
|
|
|
|
|
|
|
|
const renderCurrentBubble = () => {
|
|
|
|
|
let newBubble: DialogBubble = null;
|
|
|
|
|
|
|
|
|
|
// 尝试从缓存中获取合适的气泡
|
|
|
|
|
const isPlayerBubble = dialog.isPlayer;
|
|
|
|
|
for (let j = 0; j < this.cachedBubbles.length; j++) {
|
|
|
|
|
const cached = this.cachedBubbles[j];
|
|
|
|
|
if (cached && cached.node) {
|
|
|
|
|
const isRightBubble = cached.node.position.x > 0;
|
|
|
|
|
if (
|
|
|
|
|
(isPlayerBubble && isRightBubble) ||
|
|
|
|
|
(!isPlayerBubble && !isRightBubble)
|
|
|
|
|
) {
|
|
|
|
|
newBubble = cached;
|
|
|
|
|
this.cachedBubbles.splice(j, 1);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 如果缓存中没有合适的气泡,创建新的
|
|
|
|
|
if (!newBubble) {
|
|
|
|
|
let newBubbleNode = isPlayerBubble
|
|
|
|
|
? instantiate(this.rBubble.node)
|
|
|
|
|
: instantiate(this.lBubble.node);
|
|
|
|
|
newBubble = newBubbleNode.getComponent(DialogBubble);
|
|
|
|
|
newBubble.init(650);
|
|
|
|
|
newBubbleNode.setParent(this.node);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
newBubble.node.active = true;
|
|
|
|
|
let pos = newBubble.node.position;
|
|
|
|
|
newBubble.node.position = new Vec3(pos.x, currentPosY, pos.z);
|
|
|
|
|
|
|
|
|
|
newBubble.updateBubbleContent(
|
|
|
|
|
dialog.content,
|
|
|
|
|
dialog.isPlayer,
|
|
|
|
|
(actualHeight: number) => {
|
|
|
|
|
currentPosY += actualHeight + 40;
|
|
|
|
|
// 渲染完当前气泡后,处理下一个
|
|
|
|
|
updateNextBubble(index - 1);
|
|
|
|
|
},
|
|
|
|
|
dialog.isLoading || false
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
this.bubbles.push(newBubble);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (delay > 0) {
|
|
|
|
|
// 添加延时任务ID到跟踪数组
|
|
|
|
|
const taskId = setTimeout(() => {
|
|
|
|
|
// 从跟踪数组中移除已完成的任务
|
|
|
|
|
const taskIndex = this.delayedTasks.indexOf(taskId);
|
|
|
|
|
if (taskIndex > -1) {
|
|
|
|
|
this.delayedTasks.splice(taskIndex, 1);
|
|
|
|
|
}
|
|
|
|
|
renderCurrentBubble();
|
|
|
|
|
}, delay);
|
|
|
|
|
|
|
|
|
|
this.delayedTasks.push(taskId);
|
|
|
|
|
} else {
|
|
|
|
|
// 立即渲染
|
|
|
|
|
renderCurrentBubble();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 从组内最后一个对话开始处理(倒序)
|
|
|
|
|
updateNextBubble(dialogs.length - 1);
|
|
|
|
|
}
|
2025-07-21 22:14:06 +08:00
|
|
|
}
|