boardgame-core/docs/animation-sync.md

91 lines
3.2 KiB
Markdown
Raw Normal View History

2026-04-04 15:27:37 +08:00
# 动画与状态更新同步
2026-04-04 22:42:23 +08:00
命令执行时,效应函数如果通过 `produce()` 立即更新状态UI 层只能看到最终结果,
2026-04-04 15:27:37 +08:00
无法在中间插入动画。为了解决这个问题,`MutableSignal` 提供了动画中断机制。
## 基本原理
核心思路:**逻辑层将 `produce` 替换为 `produceAsync`UI 层负责注册动画 interruption。**
时序如下:
```
逻辑层: produceAsync(fn1) produceAsync(fn2) produceAsync(fn3)
↓ (无 interruption, ↓ 等待 anim1 完成 ↓ 等待 anim2 完成
立即更新状态 1)
2026-04-04 22:42:23 +08:00
UI层: effect 检测到状态 1 变化 effect 检测到状态 2 变化 effect 检测到状态 3 变化
2026-04-04 15:27:37 +08:00
播放动画 1 播放动画 2 播放动画 3
2026-04-04 22:42:23 +08:00
addInterruption(anim1) addInterruption(anim2) addInterruption(anim3)
2026-04-04 15:27:37 +08:00
```
1. 第一个 `produceAsync` 没有 interruption 可等,立即更新状态
2. UI 层通过 `effect` 检测到状态变化,播放动画并调用 `addInterruption`
2026-04-04 22:42:23 +08:00
3. 第二个 `produceAsync` 被前一步注册的 interruption 阻塞,等待动画完成后再更新状态
2026-04-04 15:27:37 +08:00
4. 依此类推,形成链式等待
## 逻辑层:将 `produce` 替换为 `produceAsync`
```ts
// 之前
2026-04-04 22:42:23 +08:00
async function turn(game: BoopGame, turnPlayer: PlayerType) {
game.produce(state => {
game.scores[turnPlayer] ++;
});
// 这里不能触发动画等待
game.produce(state => {
game.currentPlayer = turnPlayer;
});
};
2026-04-04 15:27:37 +08:00
// 之后:改为 produceAsync
2026-04-04 22:42:23 +08:00
async function turn(game: BoopGame, turnPlayer: PlayerType) {
await game.produceAsync(state => {
game.scores[turnPlayer] ++;
});
// 这里会等待interruption结束再继续
await game.produceAsync(state => {
game.currentPlayer = turnPlayer;
});
};
2026-04-04 15:27:37 +08:00
```
## UI 层:监听状态变化并注册 interruption
```ts
import { effect } from '@preact/signals-core';
const host = createGameHost(module);
effect(() => {
2026-04-04 22:42:23 +08:00
const state = host.context.value;
2026-04-04 15:27:37 +08:00
// 每次 produceAsync 更新状态后,这里会被触发
// 播放对应的动画
const animation = playAnimationForState(state);
// 为下一个 produceAsync 注册 interruption
2026-04-04 22:42:23 +08:00
// 注意animation 必须是 Promise<void>,在动画完成时 resolve
host.addInterruption(animation);
2026-04-04 15:27:37 +08:00
});
```
2026-04-04 22:42:23 +08:00
> **注意**`playAnimationForState` 函数需要返回 `Promise<void>`,在动画播放完成并 resolve 后,下一个 `produceAsync` 才会继续执行状态更新。
2026-04-04 15:27:37 +08:00
## 中断 API
`GameHost` 直接暴露了以下方法,供 UI 层调用:
2026-04-04 15:27:37 +08:00
| 方法 | 说明 |
|---|---|
| `addInterruption(promise: Promise<void>)` | 注册中断,下一个 `produceAsync` 会等待它 |
2026-04-04 15:27:37 +08:00
| `clearInterruptions()` | 清除所有未完成的中断 |
`MutableSignal` 上还有 `produceAsync`,逻辑层使用。
2026-04-04 15:27:37 +08:00
## 注意事项
- `produce()` 仍然保持同步,适合不需要动画的场景(如 setup 阶段)
- `produceAsync()` 使用 `Promise.allSettled` 等待所有 interruption即使某个动画 reject 也不会阻止状态更新
- 不要忘记 `await` `produceAsync()`,否则多个效应可能并发执行导致竞态
- 第一个 `produceAsync` 总是立即执行(无前序 interruption从第二个开始等待动画