update chat exp

This commit is contained in:
2025-09-16 19:03:00 +08:00
parent 141c6a5909
commit 4e00f2a382
6 changed files with 1104 additions and 874 deletions
@@ -3,6 +3,7 @@ import {
Component,
instantiate,
Node,
ScrollView,
UITransform,
Vec3,
view,
@@ -16,122 +17,198 @@ const { ccclass, property } = _decorator;
export class ChatContentsLayout extends Component {
manager: DialogManager = null;
@property(Node)
initPos: Node = null;
@property(DialogBubble)
lBubble: DialogBubble = null;
@property(DialogBubble)
rBubble: DialogBubble = null;
bubbles: DialogBubble[] = [];
cachedBubbles: DialogBubble[] = [];
// 当前显示的气泡
private bubbles: DialogBubble[] = [];
// 缓存池
private cachedBubbles: DialogBubble[] = [];
// 等待回复气泡的引用
private waitingBubble: DialogBubble = null;
// 延时任务ID数组
private delayedTasks: number[] = [];
// ScrollView组件引用
@property(ScrollView)
private scrollView: ScrollView = null;
fixMaxWidth: number;
// 用于跟踪延时任务,支持中断清理
private delayedTasks: number[] = [];
protected start(): void {
this.lBubble.node.active = false;
this.rBubble.node.active = false;
this.fixMaxWidth = view.getVisibleSize().width * 0.8;
this.fixMaxWidth = 730;
// 获取父节点的ScrollView组件
}
protected onEnable(): void {
if (!this.manager) this.manager = DialogManager.getInstance();
this.manager.layoutout = this;
}
UpdateDialog(dialogs: Dialog[]) {
// 清理所有正在进行的延时任务
/**
* 更新玩家输入的对话
* @param dialog 玩家输入的对话数据
*/
updatePlayerDialog(dialog: Dialog): void {
// 清理所有现有气泡到缓存池
this.clearAllBubbles();
// 清理所有延时任务
this.clearDelayedTasks();
// 隐藏当前使用的气泡并放入缓存
for (let i = 0; i < this.bubbles.length; i++) {
if (this.bubbles[i].node) {
this.bubbles[i].node.active = false;
this.cachedBubbles.push(this.bubbles[i]);
// 创建玩家气泡(插入到首位,Layout会让它显示在最下面)
const playerBubble = this.createOrGetBubble(true);
playerBubble.node.active = true;
playerBubble.updateBubbleContent(dialog.content, true);
this.bubbles.push(playerBubble);
// 创建等待回复气泡(插入到首位,显示在玩家消息上面)
this.waitingBubble = this.createOrGetBubble(false);
this.waitingBubble.node.active = true;
this.waitingBubble.updateBubbleContent("...", false, undefined, true);
this.waitingBubble.node.setSiblingIndex(0);
this.bubbles.push(this.waitingBubble);
this.node.position = Vec3.ZERO;
// 滚动到底部(等待下一帧Layout更新完成)
this.scheduleOnce(() => {
this.scrollView.scrollToBottom();
}, 0);
}
/**
* 更新AI回复的对话
* @param dialogs AI回复的对话数据数组
*/
updateAIDialogs(dialogs: Dialog[]): void {
// 清除等待回复气泡
if (this.waitingBubble) {
this.removeWaitingBubble();
}
// 清理之前的延时任务
this.clearDelayedTasks();
// 逐个添加AI对话,带延时
this.addAIDialogsWithDelay(dialogs, 0);
}
/**
* 清理所有气泡到缓存池
*/
private clearAllBubbles(): void {
for (const bubble of this.bubbles) {
if (bubble && bubble.node) {
bubble.node.active = false;
this.cachedBubbles.push(bubble);
}
}
this.bubbles = [];
this.waitingBubble = null;
this.updateDialogLayoutWithDelay(dialogs);
// 清理多余的缓存气泡,避免内存泄漏
// 清理多余的缓存气泡
this.cleanupExcessCachedBubbles();
}
private updateDialogLayout(dialogs: Dialog[]) {
let initPosY: number = this.initPos.position.y;
let pendingUpdates = 0;
const updateNextBubble = (index: number) => {
if (index < 0) {
return;
}
const dialog = dialogs[index];
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;
}
/**
* 从缓存池获取或创建新的气泡
* @param isPlayer 是否为玩家气泡
* @returns DialogBubble实例
*/
private createOrGetBubble(isPlayer: boolean): DialogBubble {
// 尝试从缓存中获取合适的气泡
for (let i = 0; i < this.cachedBubbles.length; i++) {
const cached = this.cachedBubbles[i];
if (cached && cached.node) {
// 通过原始模板判断气泡类型
const isRightBubble = cached.node.position.x > 0;
if ((isPlayer && isRightBubble) || (!isPlayer && !isRightBubble)) {
this.cachedBubbles.splice(i, 1);
return cached;
}
}
}
// 如果缓存中没有合适的气泡,创建新的
if (!newBubble) {
let newBubbleNode = isPlayerBubble
? instantiate(this.rBubble.node)
: instantiate(this.lBubble.node);
newBubble = newBubbleNode.getComponent(DialogBubble);
newBubble.init(650);
newBubbleNode.setParent(this.node);
}
// 缓存中没有合适的气泡,创建新的
const templateBubble = isPlayer ? this.rBubble : this.lBubble;
const newBubbleNode = instantiate(templateBubble.node);
const newBubble = newBubbleNode.getComponent(DialogBubble);
newBubble.init(this.fixMaxWidth);
newBubble.node.active = true;
let pos = newBubble.node.position;
newBubble.node.position = new Vec3(pos.x, initPosY, pos.z);
// 设置为子节点,具体位置由调用者决定
newBubbleNode.setParent(this.node);
pendingUpdates++;
newBubble.updateBubbleContent(
dialog.content,
dialog.isPlayer,
(actualHeight: number) => {
initPosY += actualHeight + 40;
pendingUpdates--;
// 当当前气泡更新完成后,处理下一个
if (pendingUpdates === 0) {
updateNextBubble(index - 1);
}
},
dialog.isLoading || false
);
this.bubbles.push(newBubble);
};
// 从最后一个对话开始处理(倒序)
updateNextBubble(dialogs.length - 1);
return newBubble;
}
private cleanupExcessCachedBubbles() {
const maxCachedBubbles = 20; // 最大缓存数量
/**
* 移除等待回复气泡
*/
private removeWaitingBubble(): void {
if (this.waitingBubble && this.waitingBubble.node) {
this.waitingBubble.node.active = false;
this.cachedBubbles.push(this.waitingBubble);
// 从当前气泡列表中移除
const index = this.bubbles.indexOf(this.waitingBubble);
if (index > -1) {
this.bubbles.splice(index, 1);
}
}
this.waitingBubble = null;
}
/**
* 带延时地添加AI对话
* @param dialogs AI对话数组
* @param index 当前处理的对话索引
*/
private addAIDialogsWithDelay(dialogs: Dialog[], index: number): void {
if (index >= dialogs.length) {
return;
}
const dialog = dialogs[index];
const delay = index > 0 ? Math.random() * 1000 + 500 : 0; // 第一条立即显示,后续延时500-1500ms
const addCurrentDialog = () => {
const aiBubble = this.createOrGetBubble(false);
aiBubble.node.active = true;
aiBubble.updateBubbleContent(dialog.content, false);
// AI消息插入到首位,显示在最上面
aiBubble.node.setSiblingIndex(0);
this.scheduleOnce(() => {
this.scrollView.scrollToBottom();
}, 0);
this.bubbles.push(aiBubble);
// 递归处理下一条对话
this.addAIDialogsWithDelay(dialogs, index + 1);
};
if (delay > 0) {
const taskId = setTimeout(() => {
// 从任务列表中移除
const taskIndex = this.delayedTasks.indexOf(taskId);
if (taskIndex > -1) {
this.delayedTasks.splice(taskIndex, 1);
}
addCurrentDialog();
}, delay);
this.delayedTasks.push(taskId);
} else {
addCurrentDialog();
}
}
/**
* 清理多余的缓存气泡
*/
private cleanupExcessCachedBubbles(): void {
const maxCachedBubbles = 20;
if (this.cachedBubbles.length > maxCachedBubbles) {
const excessCount = this.cachedBubbles.length - maxCachedBubbles;
for (let i = 0; i < excessCount; i++) {
@@ -143,153 +220,35 @@ export class ChatContentsLayout extends Component {
}
}
private clearDelayedTasks() {
// 清理所有正在进行的延时任务
/**
* 清理所有延时任务
*/
private clearDelayedTasks(): void {
for (const taskId of this.delayedTasks) {
clearTimeout(taskId);
}
this.delayedTasks = [];
}
private groupConsecutiveNonPlayerDialogs(dialogs: Dialog[]): Dialog[][] {
const groups: Dialog[][] = [];
let currentGroup: Dialog[] = [];
/**
* 保持向后兼容性的方法
* @param dialogs 对话数组
* @deprecated 请使用 updatePlayerDialog 和 updateAIDialogs 替代
*/
UpdateDialog(dialogs: Dialog[]): void {
// 为了保持兼容性,我们假设这是一个完整的对话更新
// 清理所有现有气泡
this.clearAllBubbles();
this.clearDelayedTasks();
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);
}
// 逐个添加对话,从最后一个开始倒序插入(保持显示顺序)
for (let i = dialogs.length - 1; i >= 0; i--) {
const dialog = dialogs[i];
const bubble = this.createOrGetBubble(dialog.isPlayer);
bubble.node.active = true;
bubble.updateBubbleContent(dialog.content, dialog.isPlayer, undefined);
bubble.node.setSiblingIndex(0);
this.bubbles.push(bubble);
}
// 处理最后一组非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);
}
}