refactor: 重构新手引导系统,替换旧引导方案

1.  删除旧的GuideComp组件和guide1引导预制体
2.  移除UIConfig中旧的Guide1配置项
3.  改用基于smc.data.tip_done的精细化单步引导
4.  为主页开始按钮新增首次启动引导逻辑
5.  优化引导状态管理,仅单次提示未完成步骤
This commit is contained in:
panFD
2026-08-07 00:17:31 +08:00
parent 31f5340866
commit f841779e94
9 changed files with 1059 additions and 2098 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +0,0 @@
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "93fda804-b794-4119-a287-952a70be15cb",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "guide1"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -56,10 +56,15 @@ export class SingletonModuleComp extends ecs.Comp {
task: 0,
noStop: false,
showInfo: true,
/** 新手指引完成标记false=需要提醒true=已提醒过(仅提醒 1 次) */
tip_done: {
start: false, // 主页开始按钮
hero: false, // 召唤英雄面板按钮
equip: false, // 购买装备面板按钮
skill: false, // 购买技能卷轴面板按钮
shop: false, // 购买药品面板按钮
},
}
guides: any = [0, 0, 0, 0, 0]
current_guide: number = 0
finish_guides: number[] = [0]
vmdata: any = {
game_over: false,

View File

@@ -26,7 +26,6 @@ export enum UIID {
HInfo,
/** 技能卡牌系统核心控制器 */
SkillBox,
Guide1,
}
/** 打开界面方式的配置数据 */
@@ -44,6 +43,5 @@ export var UIConfigData: { [key: number]: UIConfig } = {
[UIID.Mission]: { layer: LayerType.UI, prefab: "gui/element/mission" },
[UIID.HInfo]: { layer: LayerType.UI, prefab: "gui/element/hnode" },
[UIID.SkillBox]: { layer: LayerType.UI, prefab: "gui/element/skillbox" },
[UIID.Guide1]: { layer: LayerType.PopUp, prefab: "gui/element/guide1" },
}

View File

@@ -1,71 +0,0 @@
import { mLogger } from "../common/Logger";
import { _decorator, Animation, AnimationClip, EventTouch, Label, Node, NodeEventType, Sprite, SpriteAtlas, Tween, tween, UIOpacity, Vec3, resources, Light, UITransform, Widget, CCInteger, CCString, Component } from "cc";
import { smc } from "../common/SingletonModuleComp";
import { UIID } from "../common/config/GameUIConfig";
import { oops } from "db://oops-framework/core/Oops";
const { ccclass, property } = _decorator;
@ccclass('GuideComp')
export class GuideComp extends Component {
/** 是否开启调试日志 */
private debugMode: boolean = true;
// ======================== 编辑器绑定节点 ========================
/** 引导编号 ID */
@property({ type: CCInteger })
guide_id: number = 0
onLoad() {
// 监听自身节点(即弹窗任意位置)的点击事件
this.node.on(NodeEventType.TOUCH_START, this.onTouchStart, this);
this.node.on(NodeEventType.TOUCH_END, this.onGuideClick, this);
}
private onTouchStart(event: EventTouch) {
// 允许事件穿透,确保底层的实际功能按钮能正常接收到 TOUCH_START 事件
event.preventSwallow = true;
}
private onGuideClick(event: EventTouch) {
// 记录该引导 ID 已完成
if (!smc.finish_guides.includes(this.guide_id)) {
smc.finish_guides.push(this.guide_id);
mLogger.log(this.debugMode, 'Guide', `完成引导 ID: ${this.guide_id}`);
// TODO: 若需要持久化保存,可在此处调用本地存储或同步到服务器的方法
// oops.storage.set("finish_guides", smc.finish_guides);
}
// 允许事件穿透,确保底层的实际功能按钮能正常接收到点击事件
event.preventSwallow = true;
// 因为通过 oops.gui.open(UIID) 弹出的界面根节点可能会带有一个阻挡点击的遮罩组件BlockInputEvents
// 所以应当通过 oops.gui.remove 彻底从框架的层级管理中移除当前界面。
let targetUIID = -1;
if (this.guide_id === 1) targetUIID = UIID.Guide1;
if (targetUIID !== -1) {
oops.gui.remove(targetUIID);
} else {
// 兜底方案
this.node.active = false;
this.node.destroy();
}
}
/** 组件销毁时解绑所有事件,防止残留回调 */
onDestroy() {
if (this.node && this.node.isValid) {
this.node.off(NodeEventType.TOUCH_START, this.onTouchStart, this);
this.node.off(NodeEventType.TOUCH_END, this.onGuideClick, this);
}
}
/** 外部初始化入口(由 MissionGuideComp 调用) */
init() {
}
}

View File

@@ -1,9 +0,0 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "25122cff-7274-4f86-9f74-f82258b39fab",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -226,20 +226,18 @@ export class MissionCardComp extends CCComp {
// ======================== 新手按钮指引 ========================
/** 新手按钮指引 ID写入 finish_guides已占用 0~4此处用 5 */
private static readonly BUTTON_GUIDE_ID: number = 5;
/** 新手按钮指引抖动最大角度(度) */
private static readonly BUTTON_GUIDE_SHAKE_ANGLE: number = 10;
/** 新手按钮指引每步时长(秒) */
private static readonly BUTTON_GUIDE_STEP_DURATION: number = 4.2;
/** 新手按钮指引气泡停留时长(秒) */
private static readonly BUTTON_GUIDE_TIP_STAY: number = 3.4;
/** 指引步骤配置:按钮节点取值器 + 气泡文案 */
private readonly guideSteps: Array<{ getBtn: () => Node | null, text: string }> = [
{ getBtn: () => this.showHeros, text: "这里召唤英雄" },
{ getBtn: () => this.showEquips, text: "这里购买装备" },
{ getBtn: () => this.showSkills, text: "这里购买技能卷轴" },
{ getBtn: () => this.showShop, text: "这里购买药品" },
/** 指引步骤配置:完成标记键smc.data.tip_done+ 按钮节点取值器 + 气泡文案 */
private readonly guideSteps: Array<{ key: "hero" | "equip" | "skill" | "shop", getBtn: () => Node | null, text: string }> = [
{ key: "hero", getBtn: () => this.showHeros, text: "这里召唤英雄" },
{ key: "equip", getBtn: () => this.showEquips, text: "这里购买装备" },
{ key: "skill", getBtn: () => this.showSkills, text: "这里购买技能卷轴" },
{ key: "shop", getBtn: () => this.showShop, text: "这里购买药品" },
];
/** 当前指引下标(-1 = 未在指引中) */
private guideStepIndex: number = -1;
@@ -489,8 +487,9 @@ export class MissionCardComp extends CCComp {
this.enterBattlePhase();
// 首次进入战斗阶段:启动按钮逐个指引(仅提醒 1 次)
if (!smc.finish_guides.includes(MissionCardComp.BUTTON_GUIDE_ID)) {
// 首次进入战斗阶段:启动按钮逐个指引(仅提醒 1 次,全部完成则自动跳过
const tips = smc.data.tip_done;
if (!tips.hero || !tips.equip || !tips.skill || !tips.shop) {
this.playButtonGuide();
}
}
@@ -931,12 +930,27 @@ export class MissionCardComp extends CCComp {
/**
* 启动新手按钮指引:依次指引 英雄→装备→技能→药品 四个面板按钮。
* 每步播放 bg/icon 循环抖动 + smalltip 气泡文案,
* 全部播放完成后写入 smc.finish_guides,仅提醒 1 次。
* 已完成的步骤smc.data.tip_done 对应键为 true自动跳过,仅提醒 1 次。
*/
private playButtonGuide() {
// 确保无旧指引残留,再从头开始
this.stopButtonGuide();
this.guideStepIndex = 0;
this.guideStepIndex = -1;
this.nextButtonGuideStep();
}
/** 推进到下一个未完成指引的步骤;全部完成则结束 */
private nextButtonGuideStep() {
this.guideStepIndex++;
// 跳过已完成的步骤
while (this.guideStepIndex < this.guideSteps.length
&& smc.data.tip_done[this.guideSteps[this.guideStepIndex].key]) {
this.guideStepIndex++;
}
if (this.guideStepIndex >= this.guideSteps.length) {
this.finishButtonGuide();
return;
}
this.playButtonGuideStep();
}
@@ -951,22 +965,16 @@ export class MissionCardComp extends CCComp {
}
this.scheduleOnce(() => {
this.stopGuideShake(btn);
this.guideStepIndex++;
if (this.guideStepIndex < this.guideSteps.length) {
this.playButtonGuideStep();
} else {
this.finishButtonGuide();
}
// 本步提醒完成:记录完成标记(即使中止也只标记已播完的步骤)
smc.data.tip_done[step.key] = true;
this.nextButtonGuideStep();
}, MissionCardComp.BUTTON_GUIDE_STEP_DURATION);
}
/** 指引全部完成:还原气泡文案并记录引导完成标记 */
/** 指引全部完成:还原气泡文案 */
private finishButtonGuide() {
this.restoreGuideTipTexts();
this.guideStepIndex = -1;
if (!smc.finish_guides.includes(MissionCardComp.BUTTON_GUIDE_ID)) {
smc.finish_guides.push(MissionCardComp.BUTTON_GUIDE_ID);
}
mLogger.log(this.debugMode, "MissionCardComp", "button guide finished");
}

View File

@@ -17,7 +17,7 @@
* - GameEvent.MissionStart / MissionEnd —— 游戏生命周期事件
* - UIID.Mission —— 战斗界面 ID
*/
import { _decorator, instantiate, Prefab, resources, Sprite, SpriteAtlas, UITransform ,Node} from "cc";
import { _decorator, instantiate, Label, Prefab, resources, Sprite, SpriteAtlas, Tween, tween, UITransform, Vec3, Node } from "cc";
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp";
import { oops } from "../../../../extensions/oops-plugin-framework/assets/core/Oops";
@@ -39,7 +39,9 @@ const { ccclass, property } = _decorator;
export class MissionHomeComp extends CCComp {
/** 调试日志开关 */
debugMode: boolean = false;
/** 开始游戏按钮节点(预留) */
@property(Node)
start_btn=null!
/** 主页按钮节点(预留) */
@property(Node)
home_btn=null!
@@ -61,6 +63,20 @@ export class MissionHomeComp extends CCComp {
@property(Node)
ranks_page=null!
// ======================== 新手指引(开始按钮抖动 + 气泡) ========================
/** 抖动最大角度(度) */
private static readonly SHAKE_ANGLE: number = 10;
/** 气泡文案 */
private static readonly TIP_TEXT: string = "点击开始游戏";
/** 气泡弹出/停留/收起节奏(秒) */
private static readonly TIP_STAY: number = 3.4;
/** 指引进行中标记(避免重复启动) */
private guiding: boolean = false;
/** 气泡原文缓存(指引结束后还原) */
private guideTipOriginText: string = "";
/** 注册任务结束事件 */
protected onLoad(): void {
@@ -70,10 +86,10 @@ export class MissionHomeComp extends CCComp {
/** 启动时显示主页 */
start() {
this.home_active()
// 首次打开游戏打开guide1
if (!smc.finish_guides.includes(1)) {
oops.gui.open(UIID.Guide1);
// 新玩家start_btn 抖动 + smalltip 气泡引导点击开始(仅 1 次)
if (!smc.data.tip_done.start) {
this.playStartGuide();
}
}
@@ -93,19 +109,91 @@ export class MissionHomeComp extends CCComp {
*/
start_mission() {
mLogger.log(this.debugMode, 'MissionHomeComp', "start_mission")
// 进入战斗后关闭guide1
if (!smc.finish_guides.includes(1)) {
smc.finish_guides.push(1);
oops.gui.remove(UIID.Guide1);
}
// 点击开始后完成指引:停止抖动/气泡并记录完成标记
this.stopStartGuide();
smc.data.tip_done.start = true;
oops.gui.open(UIID.Mission)
this.node.active=false;
// 隐藏 mapLayer 下的 main 节点
}
// ======================== 新手指引实现 ========================
/** 启动指引button 子节点循环抖动 + smalltip 气泡弹出 */
private playStartGuide() {
const btn = this.start_btn;
if (!btn || !btn.isValid) return;
if (this.guiding) return;
this.guiding = true;
// start_btn 下名为 "Button" 的子节点循环抖动angle 往复,不触碰 position避免与布局冲突
const shakeNode = this.getShakeNode();
if (shakeNode) {
Tween.stopAllByTarget(shakeNode);
shakeNode.angle = 0;
tween(shakeNode)
.to(0.15, { angle: MissionHomeComp.SHAKE_ANGLE })
.to(0.3, { angle: -MissionHomeComp.SHAKE_ANGLE })
.to(0.15, { angle: 0 })
.delay(0.34)
.repeatForever()
.start();
}
// smalltip 气泡:缓存原文后覆盖文案,弹出后保持显示直至点击
const tip = btn.getChildByName("smalltip");
if (tip) {
const label = tip.getComponentInChildren(Label);
if (label) {
this.guideTipOriginText = label.string;
label.string = MissionHomeComp.TIP_TEXT;
}
tip.active = true;
Tween.stopAllByTarget(tip);
tip.setScale(0, 0, 1);
tween(tip)
.to(0.15, { scale: new Vec3(1.1, 1.1, 1) }, { easing: 'quadOut' })
.to(0.05, { scale: new Vec3(1, 1, 1) })
.start();
}
}
/** 停止指引:抖动复位、气泡收起并还原原文 */
private stopStartGuide() {
if (!this.guiding) return;
this.guiding = false;
const btn = this.start_btn;
if (btn && btn.isValid) {
const shakeNode = this.getShakeNode();
if (shakeNode && shakeNode.isValid) {
Tween.stopAllByTarget(shakeNode);
shakeNode.angle = 0;
}
const tip = btn.getChildByName("smalltip");
if (tip && tip.isValid) {
Tween.stopAllByTarget(tip);
const label = tip.getComponentInChildren(Label);
if (label) label.string = this.guideTipOriginText;
tip.active = false;
}
}
}
/**
* 获取 start_btn 下名为 "Button" 的子节点(即要抖动的节点)。
* @returns Button 子节点;未找到返回 null
*/
private getShakeNode(): Node | null {
const btn = this.start_btn;
if (!btn || !btn.isValid) return null;
const node = btn.getChildByName("Button");
return node && node.isValid ? node : null;
}
private showPage(page: Node) {
@@ -153,6 +241,8 @@ export class MissionHomeComp extends CCComp {
/** 激活主页显示:刷新数据并显示节点 */
home_active(){
// 任务结束回到主页:确保指引动画与气泡无残留
this.stopStartGuide();
this.uodate_data()
this.hideAllPages()
this.node.active=true
@@ -177,6 +267,8 @@ export class MissionHomeComp extends CCComp {
/** ECS 组件移除时销毁节点 */
reset() {
// 销毁前清理指引 tween防止回调残留
this.stopStartGuide();
this.node.destroy();
}
}