Appearance
行为驱动动画执行模型(鱼 + 尾巴示例)
目标:
在 不污染全局状态 的前提下,
使用 时间 + 快照 + 行为描述
统一驱动动画、交互与中断。
一、核心设计原则(先立规矩)
- 渲染阶段不写状态,只做求值
- 行为之间不直接读写彼此的数据
- 行为可以声明影响,但不能直接操作调度结构
- 任何动画结果,必须可由 (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 的最终结构
你已经不需要“再理解一次”,
你现在需要的是 定稿与落地。