Cocos Creator 3.x 高维护性打字机对话系统设计与实现

在 Cocos Creator 项目中,对话系统是 RPG、冒险、视觉小说等类型游戏的核心功能之一。如何设计一个维护性高、可扩展、策划友好、支持存档的打字机(Typewriter)系统,是许多开发者面临的挑战。

该系统采用组件化 + 配置化 + JSON 数据驱动 + 事件解耦 + 状态机 + 存档集成的设计理念,代码清晰、参数集中、业务与逻辑分离,极大降低了后期维护和迭代成本。

1. 系统整体架构

系统由以下核心模块组成,每个模块职责单一,便于独立维护和测试:

模块名称 主要职责 设计优势
TypewriterComponent 核心打字逻辑(逐字符显示、计时、跳过) 独立、可复用、事件驱动
TypewriterConfig 打字参数配置(速度、音效、标签等) 数据与逻辑分离,支持 JSON 热更新
DialogueEntry 单条对话数据结构 支持富文本、头像、分支等
DialogueManager 对话序列管理、UI 更新、存档集成 业务层核心,JSON 驱动,解耦彻底
SaveData 存档数据结构 轻量、稳定,支持多槽位

推荐节点层级

  • DialoguePanel(根节点)
    • Portrait(Sprite)------ 角色头像
    • SpeakerName(Label)------ 说话者名字
    • DialogueText(Label 或 RichText)------ 挂载 TypewriterComponent
    • DialogueManager(脚本挂载)

2. TypewriterConfig(配置类)

tsx 复制代码
// TypewriterConfig.ts
import { _decorator, CCString, CCFloat, CCBoolean, CCInteger } from 'cc';

const { ccclass, property } = _decorator;

@ccclass('TypewriterConfig')
export class TypewriterConfig {
    @property(CCFloat) public speed: number = 0.05;
    @property(CCBoolean) public canSkip: boolean = true;
    @property(CCBoolean) public autoNext: boolean = false;
    @property(CCString) public typeSound: string = '';
    @property(CCString) public completeSound: string = '';
    @property(CCInteger) public defaultPauseMs: number = 300;
    @property(CCBoolean) public enableRichParse: boolean = true;

    constructor(data?: Partial<TypewriterConfig>) {
        if (data) Object.assign(this, data);
    }

    public static fromJSON(json: any): TypewriterConfig {
        return new TypewriterConfig(json);
    }

    public toJSON(): any { /* ... */ }
    public clone(): TypewriterConfig { /* ... */ }
}

3. TypewriterComponent(核心打字组件)

核心逻辑封装在此组件中,支持 Label/RichText、自动开始、跳过、每字符/完成事件。

tsx 复制代码
import { _decorator, Component, Label, RichText, CCString, CCFloat, CCBoolean, EventHandler } from 'cc';

const { ccclass, property, menu } = _decorator;

@ccclass('TypewriterComponent')
@menu('UI/TypewriterComponent')  // 在组件菜单中一键添加
export class TypewriterComponent extends Component {
    @property(Label)
    public label: Label | null = null;

    @property(RichText)
    public richText: RichText | null = null;

    @property(CCString)
    public fullText: string = '';

    @property(CCFloat)
    public speed: number = 0.05;  // 秒/字符

    @property(CCBoolean)
    public autoStart: boolean = true;

    @property(CCBoolean)
    public canSkip: boolean = true;

    @property(EventHandler)
    public onCharacterTyped: EventHandler = new EventHandler();

    @property(EventHandler)
    public onComplete: EventHandler = new EventHandler();

    private _currentIndex: number = 0;
    private _isTyping: boolean = false;
    private _timer: number = 0;
    private _currentText: string = '';

    onLoad() {
        if (!this.label && !this.richText) {
            console.warn('【TypewriterComponent】必须绑定 Label 或 RichText 组件!');
        }
    }

    start() {
        if (this.autoStart && this.fullText) {
            this.startTyping();
        }
    }

    /** 开始打字(支持外部传入新文本) */
    public startTyping(text?: string): void {
        if (text !== undefined) this.fullText = text;
        this._currentIndex = 0;
        this._currentText = '';
        this._isTyping = true;
        this._timer = 0;
        this.updateDisplay();
        this.node.emit('typing-start');
    }

    /** 玩家点击跳过 */
    public skipTyping(): void {
        if (!this.canSkip || !this._isTyping) return;
        this._currentIndex = this.fullText.length;
        this._currentText = this.fullText;
        this.updateDisplay();
        this._isTyping = false;
        this.node.emit('typing-complete');
        EventHandler.emitEvents(this.onComplete);
    }

    update(dt: number) {
        if (!this._isTyping) return;

        this._timer += dt;
        if (this._timer >= this.speed) {
            this._timer -= this.speed;  // 支持掉帧补偿
            this.typeNextCharacter();
        }
    }

    private typeNextCharacter(): void {
        if (this._currentIndex < this.fullText.length) {
            const nextChar = this.fullText[this._currentIndex];
            this._currentText += nextChar;
            this._currentIndex++;
            this.updateDisplay();

            // 每字符事件(可播放打字音效)
            this.node.emit('character-typed', nextChar);
            EventHandler.emitEvents(this.onCharacterTyped, nextChar);
        } else {
            this._isTyping = false;
            this.node.emit('typing-complete');
            EventHandler.emitEvents(this.onComplete);
        }
    }

    private updateDisplay(): void {
        if (this.label) this.label.string = this._currentText;
        if (this.richText) this.richText.string = this._currentText;  // 基础版支持标签
    }
}

富文本高级扩展提示(维护性高):

若 fullText 包含 <color=#ff0000>红色文字</color>,基础 append 即可工作。若需更精确(不打断标签),可在 parseTokens 中将文本拆成"可见字符 + 标签"队列,逐个处理可见字符即可。

4. DialogueEntry(对话条目数据)

tsx 复制代码
// DialogueEntry.ts
@ccclass('DialogueEntry')
export class DialogueEntry {
    @property(CCString) public id: string = '';
    @property(CCString) public speaker: string = '';
    @property(CCString) public text: string = '';
    @property(CCString) public portrait: string = '';
    @property(TypewriterConfig) public config: TypewriterConfig = new TypewriterConfig();
    @property(CCBoolean) public autoNext: boolean = false;
    @property(CCString) public nextId: string = '';

    public static fromJSON(json: any): DialogueEntry { /* ... */ }
}

5. DialogueManager(对话管理器 + 存档集成)

这是系统的业务核心,负责加载 JSON 对话表、切换对话、更新 UI、自动保存进度。

tsx 复制代码
// DialogueManager.ts (关键存档部分已集成)
import { sys } from 'cc';
import { SaveData } from './SaveData';

@ccclass('DialogueManager')
export class DialogueManager extends Component {
    // ... 属性绑定(typewriter、portraitSprite、speakerLabel 等)

    private _dialogues: DialogueEntry[] = [];
    private _currentIndex: number = -1;

    // 加载对话 JSON
    public loadDialoguesFromJSON(jsonPath: string = 'dialogues/main') { /* ... */ }

    public startDialogue(index: number) { /* 更新 UI、应用 config、开始打字 */ }

    public nextDialogue() { /* ... */ }

    // ====================== 存档系统 ======================
    private getSaveKey(slot: number = 1): string {
        return `dialogue_save_${this.defaultDialogueGroup}_slot${slot}`;
    }

    public saveProgress(slot: number = 1): void {
        if (this._currentIndex < 0) return;
        const entry = this._dialogues[this._currentIndex];
        const saveData = new SaveData(this.defaultDialogueGroup);
        saveData.currentDialogueId = entry.id;
        saveData.currentIndex = this._currentIndex;

        sys.localStorage.setItem(this.getSaveKey(slot), JSON.stringify(saveData.toJSON()));
        console.log(`存档成功:槽位 ${slot},对话ID ${entry.id}`);
    }

    public loadProgress(slot: number = 1): boolean {
        const jsonStr = sys.localStorage.getItem(this.getSaveKey(slot));
        if (!jsonStr) return false;
        try {
            const saveData = SaveData.fromJSON(JSON.parse(jsonStr));
            let index = this._dialogues.findIndex(d => d.id === saveData.currentDialogueId);
            if (index === -1) index = saveData.currentIndex;
            if (index >= 0) {
                this.startDialogue(index);
                return true;
            }
        } catch (e) { console.error('读档失败', e); }
        return false;
    }

    public deleteSave(slot: number = 1) { sys.localStorage.removeItem(this.getSaveKey(slot)); }

    // 打字完成时自动保存
    private _onTypingComplete() {
        this.saveProgress();   // 关键:自动保存进度
        // ... 处理 autoNext 等
    }
}

SaveData.ts (轻量存档结构):

仅保存 dialogueGroupcurrentDialogueIdcurrentIndextimestampextraData,避免存储大量冗余文本。

6. JSON 数据驱动示例(dialogues/main.json)

json 复制代码
[
  {
    "id": "d001",
    "speaker": "村长",
    "text": "勇者,你终于醒了![pause=500] 魔王又在作乱了!",
    "portrait": "portraits/village-chief",
    "config": { "speed": 0.04, "canSkip": true },
    "autoNext": false
  }
]

7. 使用与集成示例

在场景控制器中:

tsx 复制代码
start() {
    this.dialogueManager.loadDialoguesFromJSON('dialogues/main');
    const loaded = this.dialogueManager.loadProgress(1);  // 启动时读档
    if (!loaded) {
        // 从头开始
    }
}

onClickContinue() {
    this.dialogueManager.nextDialogue();
}

onClickSave() {
    this.dialogueManager.saveProgress(1);
}
相关推荐
LcGero13 小时前
Cocos Creator 三端接入穿山甲 SDK
sdk·cocos creator·穿山甲
LcGero2 天前
Cocos Creator平台适配层框架设计
cocos creator·平台·框架设计
LcGero2 天前
Cocos Creator 业务与原生通信详解
android·ios·cocos creator·游戏开发·jsb
LcGero4 天前
TypeScript 快速上手:前言
typescript·cocos creator·游戏开发
Setsuna_F_Seiei4 天前
CocosCreator 游戏开发 - 多维度状态机架构设计与实现
前端·cocos creator·游戏开发
CodeCaptain3 个月前
cocoscreator 2.4.x 场景运行时的JS生命周期浅析
cocos creator·开发经验
灰海3 个月前
vue实现即开即用的AI对话打字机效果
前端·javascript·vue.js·打字机
CodeCaptain3 个月前
CocosCreator 3.8.x [.gitignore]文件内容,仅供参考
经验分享·cocos creator
VaJoy4 个月前
Cocos Creator Shader 入门 (21) —— 高斯模糊的高性能实现
前端·cocos creator