Files
aibis-dream/.cursor/plans/塔罗牌小游戏实现_ee409aa1.plan.md
2026-04-08 18:28:27 +08:00

357 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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: 我觉得也许你需要这个。
<<show_tarot_deck>>
【它拿出一套崭新的卡牌。】
me: 这是什么?
ql: "塔罗牌"。
ql: 试试吧。抽一张。
<<spread_tarot 5>>
-> 抽牌
<<wait_tarot_select>>
【你看了看牌。】
<<flip_tarot "inverted">>
// 翻到正面,但预设为倒置朝向(inverted)
【上面画着一个张开双臂的机体...像是...在把它吸过去。】
me: 这画的是什么?
ql: 倒吊人。你拿反了。
<<rotate_tarot "normal">>
// Yarn 命令改变朝向:倒置 -> 正向(旋转 180 度动画)
【你把牌转过来。】
【这下你看明白了。这是一个倒吊着的机体...】
// ...更多对话...
<<hide_tarot>>
```
## 架构概览
```mermaid
flowchart TD
Yarn["Yarn 脚本"] -->|"<<show_tarot_deck>>"| CMD["TarotYarnCommand\n(静态 Yarn 命令类)"]
Yarn -->|"<<spread_tarot 5>>"| CMD
Yarn -->|"<<wait_tarot_select>>"| CMD
Yarn -->|"<<flip_tarot inverted>>"| CMD
Yarn -->|"<<rotate_tarot normal>>"| CMD
Yarn -->|"<<hide_tarot>>"| 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`,设短 duration0.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<TarotManager>()` 获取 Manager 实例。
| Yarn 命令 | 方法签名 | 说明 |
| ----------------------------------------- | ------------------------------------------------------ | --------------------------------------------------------------------- |
| `<<show_tarot_deck>>` | `ShowTarotDeck(int count = 5)` | 显示牌堆(叠放状态),可指定牌数 |
| `<<spread_tarot>>``<<spread_tarot 5>>` | `IEnumerator SpreadTarot(int count = -1)` | 扇形展开,count=-1 用已有牌数;协程等待展开动画完成 |
| `<<wait_tarot_select>>` | `IEnumerator WaitTarotSelect()` | 阻塞 Yarn 直到玩家点击选中一张牌;选中后执行选牌动画(其余牌淡出、选中牌居中);结果写入 `$tarotSelectedIndex` |
| `<<flip_tarot "normal">>` | `IEnumerator FlipTarot(string orientation = "normal")` | 翻牌到正面,同时设置朝向;`"normal"` = 正向,`"inverted"` = 倒置 |
| `<<rotate_tarot "normal">>` | `IEnumerator RotateTarot(string orientation)` | 改变已翻开牌的朝向(旋转动画),不翻面 |
| `<<hide_tarot>>` | `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<TarotManager>()` 获取实例
- **Yarn 变量写入**`StorageSystem.Instance.SetValue("$tarotSelectedIndex", value)` 将选牌结果传回 Yarn