Skip to content

行为驱动动画执行模型(鱼 + 尾巴示例)

目标:
不污染全局状态 的前提下,
使用 时间 + 快照 + 行为描述
统一驱动动画、交互与中断。

一、核心设计原则(先立规矩)

  1. 渲染阶段不写状态,只做求值
  2. 行为之间不直接读写彼此的数据
  3. 行为可以声明影响,但不能直接操作调度结构
  4. 任何动画结果,必须可由 (time + snapshot) 唯一计算得到

二、世界结构(World Structure)

ts
World {
  time: number

  entities: {
    fish: FishEntity
  }
}

三、实体结构(Fish)

鱼是一个分层行为实体

ts
FishEntity {
  baseSnapshot: {
    x: number
    y: number
    rotation: number
  }

  scopes: {
    body: BehaviorScope
    tail: BehaviorScope
  }
}

BehaviorScope 定义

ts
BehaviorScope {
  candidates: BehaviorInstance[]   // 当前候选行为
  active: BehaviorInstance | null  // 本帧被选中的行为
}

注意:

  • candidates ≠ 执行顺序
  • active 由调度器决定

四、行为声明(Behavior Declaration)

行为声明是动画库中的常量定义,不可变。

ts
BehaviorDeclaration {
  type: string
  evaluate: (snapshot, time, params) => PoseDelta
  duration?: number
}

示例:鱼绕圈行为(Orbit)

ts
OrbitBehavior {
  type: "orbit"

  evaluate(snapshot, time, params) {
    const t = (time - snapshot.startTime) / params.period
    return {
      x: snapshot.centerX + Math.cos(t) * params.radius,
      y: snapshot.centerY + Math.sin(t) * params.radius,
      rotation: t + params.baseRotation
    }
  }
}

五、行为实例(Behavior Instance)

行为实例 = 声明 + 快照 + 参数

ts
BehaviorInstance {
  declaration: BehaviorDeclaration

  snapshot: {
    startTime: number
    startPose: Pose
  }

  params: object

  evaluate(time): BehaviorResult
}

BehaviorResult

ts
BehaviorResult {
  status: "running" | "success"
  suppress?: string[]   // 抑制哪些行为类型
}

六、默认状态(无交互)

初始化

ts
fish.scopes.body.candidates = [Orbit((center = canvasCenter))];

fish.scopes.tail.candidates = [TailWagSlow()];

调度规则(极简版)

ts
selectActive(scope) {
  return scope.candidates[0]
}

七、点击事件触发流程(核心)

用户点击画布某点

ts
onClick(targetX, targetY) {
  // 1. 冻结当前姿态,生成快照
  const currentPose = evaluateCurrentPose(fish)

  // 2. 替换 body scope 的候选行为
  fish.scopes.body.candidates = [
    Wait(duration = tailWhipDuration),
    MoveTo(targetX, targetY),
    Orbit(center = target)
  ]

  // 3. 替换 tail scope 的候选行为
  fish.scopes.tail.candidates = [
    TailWhip(direction = computeDirection(target)),
    TailWagSlow()
  ]
}

关键点:

  • 没有修改任何 x / y
  • 只是替换了“候选行为集合”

八、调度与执行(每一帧)

Tick 流程

ts
tick(time) {
  world.time = time

  for each entity:
    for each scope:
      scope.active = selectActive(scope)

  // 执行求值
  pose = compose(
    body.active.evaluate(time),
    tail.active.evaluate(time)
  )

  render(pose)
}

九、行为如何“影响决策”(但不操作数组)

TailWhip 行为示例

ts
TailWhip.evaluate(snapshot, time) {
  if (time < snapshot.startTime + duration) {
    return {
      status: "running",
      suppress: ["move"]   // 告诉调度器:我在时,move 不应被选中
    }
  }

  return { status: "success" }
}

调度器响应 suppress

ts
selectActive(scope) {
  const suppressed = collectSuppressSignals()

  return scope.candidates.find(
    b => !suppressed.includes(b.declaration.type)
  )
}

十、为什么这个模型是“可控的”

你现在具备了这些能力:

  • ✅ 中断(点击即切换候选)
  • ✅ 打断安全(靠快照)
  • ✅ 并行行为(body / tail)
  • ✅ 无状态污染(纯求值)
  • ✅ 行为组合(候选顺序 + suppress)

同时避免了这些问题:

  • ❌ 行为互相写状态
  • ❌ FSM 爆炸
  • ❌ 时间线和事件互相耦合
  • ❌ “到底谁在改 x” 的混乱

十一、一句非常重要的总结

你现在这套东西,本质上是:

“以 BT 的行为语义 + 动画引擎的时间求值模型 + 极简调度规则”
组合出来的一种

动画专用行为系统

不是 FSM
不需要完整 BT 教科书
而是一个足够强、但可控的中间形态

你完全可以以此为 v0 内核继续往下做。

如果你愿意,下一步我可以帮你做的事是:

  • 把这套模型 映射回你最初的 animationConfig / loader / library 三分法
  • 或者直接帮你设计 animationConfig.hero 的最终结构

你已经不需要“再理解一次”,
你现在需要的是 定稿与落地