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

14 KiB
Raw Blame History

name, overview, todos, isProject
name overview todos isProject
塔罗牌小游戏实现 基于 Sprite + EventTriggerEx 实现塔罗牌小游戏,Yarn 命令细粒度控制每个阶段(展示牌堆、展开、等待选牌、翻牌、设置朝向),与对话深度配合。
id content status
tarot-card 创建 TarotCard.cs -- 单张牌组件:IInteraction、SpriteRenderer 正反面、EventTriggerEx 注册、翻牌动画、朝向控制(正/倒)、悬停/点击回调 completed
id content status
tarot-deck 创建 TarotDeck.cs -- 牌堆管理:动态生成牌、扇形展开布局、悬停推出效果、选牌流程(FadeOut + 居中检视) completed
id content status
tarot-manager 创建 TarotManager.cs -- 系统管理器:SystemDic 注册、视图开关、阶段状态机、选牌结果 completed
id content status
tarot-yarn 创建 TarotYarnCommand.cs -- 细粒度 Yarn 命令集(show/spread/wait_select/flip/set_orientation/rotate/hide completed
false

塔罗牌小游戏实现计划

设计目标

Yarn 脚本能细粒度控制塔罗牌的每个视觉阶段,每个命令之间可以穿插对话,实现对话与交互的深度配合。

Yarn 脚本示例(对应 FP_Day1_night 第 364-395 行的改造)

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>>

架构概览

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 的 EventTriggerEx 注册模式(行 165-199)。

  • 实现 IInteraction 接口(IsActive, IsAvailable, GetGameObject()
  • 需要 Collider2DBoxCollider2D,牌面大小)
  • 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. 完整交互流程

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"
  • 分别添加 PointerEnterPointerExitPointerClick 三个条目
  • 回调列表留空(代码中 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  -- 选中牌移至检视位置时长

运行时流程

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=truewait_tarot_select 时对话暂停所以 isLocked=false,交互自然可用)
  • SystemDic 注册FixSystemCenter.SystemDic.Register(this) 使 YarnCommand 能通过 Get<TarotManager>() 获取实例
  • Yarn 变量写入StorageSystem.Instance.SetValue("$tarotSelectedIndex", value) 将选牌结果传回 Yarn