--- name: 塔罗牌小游戏实现 overview: 基于 Sprite + EventTriggerEx 实现塔罗牌小游戏,Yarn 命令细粒度控制每个阶段(展示牌堆、展开、等待选牌、翻牌、设置朝向),与对话深度配合。 todos: - id: tarot-card content: 创建 TarotCard.cs -- 单张牌组件:IInteraction、SpriteRenderer 正反面、EventTriggerEx 注册、翻牌动画、朝向控制(正/倒)、悬停/点击回调 status: completed - id: tarot-deck content: 创建 TarotDeck.cs -- 牌堆管理:动态生成牌、扇形展开布局、悬停推出效果、选牌流程(FadeOut + 居中检视) status: completed - id: tarot-manager content: 创建 TarotManager.cs -- 系统管理器:SystemDic 注册、视图开关、阶段状态机、选牌结果 status: completed - id: tarot-yarn content: 创建 TarotYarnCommand.cs -- 细粒度 Yarn 命令集(show/spread/wait_select/flip/set_orientation/rotate/hide) status: completed isProject: false --- # 塔罗牌小游戏实现计划 ## 设计目标 Yarn 脚本能**细粒度控制**塔罗牌的每个视觉阶段,每个命令之间可以穿插对话,实现对话与交互的深度配合。 ## Yarn 脚本示例(对应 FP_Day1_night 第 364-395 行的改造) ```yarn ql: 我觉得也许你需要这个。 <> 【它拿出一套崭新的卡牌。】 me: 这是什么? ql: "塔罗牌"。 ql: 试试吧。抽一张。 <> -> 抽牌 <> 【你看了看牌。】 <> // 翻到正面,但预设为倒置朝向(inverted) 【上面画着一个张开双臂的机体...像是...在把它吸过去。】 me: 这画的是什么? ql: 倒吊人。你拿反了。 <> // Yarn 命令改变朝向:倒置 -> 正向(旋转 180 度动画) 【你把牌转过来。】 【这下你看明白了。这是一个倒吊着的机体...】 // ...更多对话... <> ``` ## 架构概览 ```mermaid flowchart TD Yarn["Yarn 脚本"] -->|"<>"| CMD["TarotYarnCommand\n(静态 Yarn 命令类)"] Yarn -->|"<>"| CMD Yarn -->|"<>"| CMD Yarn -->|"<>"| CMD Yarn -->|"<>"| CMD Yarn -->|"<>"| CMD CMD --> TM["TarotManager\n(SystemDic 注册)"] TM --> TD["TarotDeck\n牌堆/扇形/选牌"] TD --> TC["TarotCard x N\nSprite + EventTriggerEx"] TC -->|IInteraction| ES["EventSystemEx"] ``` ## 新增文件结构 ``` Assets/Scripts/MiniGame/Tarot/ TarotManager.cs -- 管理器:SystemDic 注册、视图开关、阶段协调 TarotCard.cs -- 单张牌:IInteraction + 翻牌 + 朝向 TarotDeck.cs -- 牌堆逻辑:生成、展开、悬停、选牌 TarotYarnCommand.cs -- 细粒度 Yarn 命令集 ``` ## 核心设计 ### 1. TarotCard -- 单张牌 参考 [KnobController](Assets/Scripts/MiniGame/HuoShan/SalesSystem/KnobController.cs) 的 EventTriggerEx 注册模式(行 165-199)。 - 实现 `IInteraction` 接口(`IsActive`, `IsAvailable`, `GetGameObject()`) - 需要 `Collider2D`(BoxCollider2D,牌面大小) - `SpriteRenderer` + 两个 Sprite 引用:`frontSprite` / `backSprite` **朝向系统(Orientation)**: - 枚举 `TarotOrientation { Normal, Inverted }` - `Normal`:正面朝上(localScale.y = 1) - `Inverted`:倒置(localScale.y = -1,即上下颠倒) - `SetOrientation(orientation, animated)` 方法,animated 时用 DOTween 做 Y 轴 scale 过渡 **翻牌动画**: - `Flip(targetOrientation, duration)` 协程 - DOTween 缩放 localScale.x: 1 -> 0(前半),切换 Sprite,再 0 -> 1(后半) - 翻牌完成时根据 `targetOrientation` 设置 localScale.y **交互注册**(Awake): - 获取/添加 `EventTriggerEx` - `EnsureEventTriggerEntries()` 确保 PointerEnter/PointerExit/PointerClick 条目 - `Register(PointerEnter, OnHoverEnter)` / `Register(PointerExit, OnHoverExit)` / `Register(PointerClick, OnClick)` - 回调通过 `System.Action` 委托给 `TarotDeck` **状态**: - `bool IsFaceUp` -- 当前是否正面朝上 - `TarotOrientation Orientation` -- 当前朝向 - `bool IsInteractable` -- 是否接受交互(由 Deck 控制) ### 2. TarotDeck -- 牌堆管理 **动态生成**: - `Setup(int cardCount, Sprite front, Sprite back)` -- 创建 N 个 TarotCard 子物体 - 每张牌初始背面朝上,叠在一起(牌堆状态) **扇形展开**: - `SpreadCards(float duration)` 协程 - 配置项:`fanAngleRange`(扇形总角度,如 120 度)、`fanRadius`(扇形半径) - 每张牌的目标角度:`centerAngle + (i - (count-1)/2f) * angleStep` - 每张牌的目标位置:以旋转角度沿圆弧分布 - DOTween `DOLocalMove` + `DOLocalRotate` 同步动画 **悬停推出**: - 牌注册的 `OnHoverEnter` -> 沿牌面法线方向(局部 Y 轴上方)推出一段距离 - `OnHoverExit` -> 回到扇形位置 - 用 DOTween `DOLocalMove`,设短 duration(0.15s) **选牌**: - `OnCardClicked(TarotCard card)` -- 锁定交互,触发选牌序列 - 其余牌 `SpriteRenderer.DOFade(0, 0.3f)` 渐隐 - 选中牌移至检视位置(屏幕中央偏上),放大到检视尺寸 - 设置 `_selectedCard`,通知 Manager 选牌完成 **检视位置**: - `[SerializeField] Transform inspectPosition` -- 检视锚点 - `[SerializeField] float inspectScale` -- 检视缩放 ### 3. TarotManager -- 管理器 - `Start()` 中 `FixSystemCenter.SystemDic.Register(this)` - 持有 `TarotDeck` 引用(`GetComponentInChildren`) - 状态标记:`_isWaitingForSelection`(是否在等待玩家选牌)、`_isCardSelected` - `OpenView()` / `CloseView()` 控制根 GameObject 显隐 - `WaitForSelection()` 协程 -- while 循环等待 `_isCardSelected`,供 YarnCommand 使用 - `SelectedCard` 属性 -- 获取当前选中的牌 ### 4. TarotYarnCommand -- 细粒度 Yarn 命令集 所有命令通过 `FixSystemCenter.SystemDic.Get()` 获取 Manager 实例。 | Yarn 命令 | 方法签名 | 说明 | | ----------------------------------------- | ------------------------------------------------------ | --------------------------------------------------------------------- | | `<>` | `ShowTarotDeck(int count = 5)` | 显示牌堆(叠放状态),可指定牌数 | | `<>` 或 `<>` | `IEnumerator SpreadTarot(int count = -1)` | 扇形展开,count=-1 用已有牌数;协程等待展开动画完成 | | `<>` | `IEnumerator WaitTarotSelect()` | 阻塞 Yarn 直到玩家点击选中一张牌;选中后执行选牌动画(其余牌淡出、选中牌居中);结果写入 `$tarotSelectedIndex` | | `<>` | `IEnumerator FlipTarot(string orientation = "normal")` | 翻牌到正面,同时设置朝向;`"normal"` = 正向,`"inverted"` = 倒置 | | `<>` | `IEnumerator RotateTarot(string orientation)` | 改变已翻开牌的朝向(旋转动画),不翻面 | | `<>` | `IEnumerator HideTarot()` | 淡出并隐藏所有塔罗元素 | ### 5. 完整交互流程 ```mermaid stateDiagram-v2 [*] --> DeckVisible: show_tarot_deck DeckVisible --> DeckVisible: 对话继续\n(牌堆静态展示) DeckVisible --> FanOut: spread_tarot FanOut --> WaitSelect: wait_tarot_select WaitSelect --> WaitSelect: hover 推出/回位 WaitSelect --> Selected: 玩家点击选牌 Selected --> InspectBack: 其余牌淡出\n选中牌居中(背面) InspectBack --> InspectBack: 对话继续\n(背面检视) InspectBack --> InspectFront: flip_tarot\n(预设朝向) InspectFront --> InspectFront: 对话继续 InspectFront --> InspectRotated: rotate_tarot\n(改变朝向) InspectRotated --> InspectRotated: 对话继续 InspectRotated --> [*]: hide_tarot InspectFront --> [*]: hide_tarot ``` 关键:每个状态之间 Yarn 对话可以自由穿插,命令只推进视觉阶段,不阻塞对话本身(除了 `wait_tarot_select` 是阻塞等待玩家交互)。 ### 6. Sprite 资源 当前使用 `Assets/RawResources/Art/CG/塔罗牌/塔罗牌正面.png` 和 `塔罗牌背面.png`,所有牌共用同一对正反面。后续可扩展为每张牌不同正面。 ## 场景配置 ### 层级结构 ``` TarotSystem -- 空 GameObject,放在需要的场景中 ├── TarotManager (Component) -- 管理器脚本 │ ├── TarotView -- 视图容器(OpenView/CloseView 控制这个) │ ├── TarotDeck (Component) -- 牌堆逻辑脚本 │ │ └── [运行时动态生成的牌] -- TarotCard x N │ │ │ └── InspectAnchor -- 检视锚点(空物体,标记牌选中后居中的位置) │ (Transform 位置设在画面中央偏上) │ └── (可选) Cinemachine Camera -- 如果塔罗需要独立相机视角 ``` ### 需要手动配置的部分 **1. TarotSystem 根物体** - 放在对应场景中(如 Day1_night 所在场景) - 位置在世界空间中合适的地方(与当前相机视角对齐) **2. TarotManager 组件(Inspector 面板)** ``` [Header("视图")] TarotView -- 拖入 TarotView 子物体(或自动 Find) [Header("牌面素材")] FrontSprite -- 拖入 塔罗牌正面 Sprite BackSprite -- 拖入 塔罗牌背面 Sprite [Header("牌 Prefab")] CardPrefab -- 拖入预制的单张牌 Prefab(见下方) ``` **3. 单张牌 Prefab(预制体)** 需要提前制作一个 Prefab,结构如下: ``` TarotCard (Prefab) Components: - SpriteRenderer (默认 sprite = 背面,sortingOrder 按需) - BoxCollider2D (Size 匹配牌面大小,用于点击/悬停检测) - EventTriggerEx (Inspector 中添加 3 个 Trigger 条目: PointerEnter, PointerExit, PointerClick) - TarotCard.cs (脚本自动注册事件) ``` EventTriggerEx 的 Inspector 配置要点: - 点击 "Add New Event Type" - 分别添加 `PointerEnter`、`PointerExit`、`PointerClick` 三个条目 - 回调列表留空(代码中 `Register` 动态绑定) > 这与 KnobController 的模式一致:EventTriggerEx 需要 Inspector 中预配好条目,代码中 `TryGetTriggerEvent` 才能找到对应的 callback list。代码中也会 `EnsureEventTriggerEntries()` 作为保底自动添加。 **4. TarotDeck 组件(Inspector 面板)** ``` [Header("扇形展开参数")] FanAngleRange = 120 -- 扇形总角度(度) FanRadius = 3.0 -- 扇形半径(世界单位) FanCenter = (0,0,0) -- 扇形圆心偏移 [Header("悬停")] HoverOffset = 0.5 -- 悬停推出距离 HoverDuration = 0.15 -- 悬停动画时长 [Header("检视")] InspectAnchor -- 拖入 InspectAnchor 子物体 InspectScale = 1.5 -- 检视模式缩放倍数 [Header("动画时长")] SpreadDuration = 0.6 -- 展开动画时长 FadeOutDuration = 0.3 -- 未选中牌淡出时长 MoveToInspectDuration = 0.4 -- 选中牌移至检视位置时长 ``` ### 运行时流程 ```mermaid sequenceDiagram participant Y as Yarn participant M as TarotManager participant D as TarotDeck participant C as TarotCard Y->>M: show_tarot_deck(5) M->>M: OpenView() M->>D: Setup(5, frontSprite, backSprite) D->>D: Instantiate CardPrefab x 5 D->>C: 初始化 (背面, 叠放) Note over Y: 对话继续... Y->>D: spread_tarot D->>C: DOTween 扇形展开动画 Note over Y: 对话继续... Y->>M: wait_tarot_select (阻塞) Note over C: 玩家悬停/点击 C-->>D: OnCardClicked D->>D: 其余牌淡出, 选中牌居中 D-->>M: 选牌完成 M-->>Y: 协程返回 Note over Y: 对话继续... Y->>M: flip_tarot("inverted") M->>C: Flip(inverted) C->>C: X轴缩放动画 + 切换Sprite + Y轴倒置 Note over Y: 对话: "你拿反了" Y->>M: rotate_tarot("normal") M->>C: SetOrientation(normal, animated=true) C->>C: Y轴旋转动画 (倒置->正向) Note over Y: 对话继续... Y->>M: hide_tarot M->>D: 淡出所有牌 M->>M: CloseView() ``` ### 不需要手动做的部分(代码自动处理) - 牌的动态实例化和销毁(`TarotDeck` 负责) - EventTriggerEx 事件绑定(`TarotCard.Awake` 自动注册) - SystemDic 注册(`TarotManager.Start` 自动注册) - 扇形位置计算(`TarotDeck` 按参数自动排布) ## 关键技术点 - **EventTriggerEx 注册模式**:参考 `KnobController.Awake()` (行 165-199),需先 `EnsureEventTriggerEntries()` 确保 triggers 列表有对应条目,再调用 `Register(EventTriggerType, callback)` - **DOTween 动画**:项目已引入 `DG.Tweening`,用于扇形展开、悬停推出、翻牌、FadeOut - **IInteraction 实现**:`IsActive` / `IsAvailable` 控制交互可用性,配合 `EventSystemEx.isLocked` 在对话期间自动禁用交互(对话进行时 `isLocked=true`,`wait_tarot_select` 时对话暂停所以 `isLocked=false`,交互自然可用) - **SystemDic 注册**:`FixSystemCenter.SystemDic.Register(this)` 使 YarnCommand 能通过 `Get()` 获取实例 - **Yarn 变量写入**:`StorageSystem.Instance.SetValue("$tarotSelectedIndex", value)` 将选牌结果传回 Yarn