357 lines
14 KiB
Markdown
357 lines
14 KiB
Markdown
---
|
||
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`,设短 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<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
|
||
|