Repository Wiki
isHarryh/Ark-Pets

行为状态机与随机行为决策

Ark-Pets 桌面角色的"会自己动"来自两个紧密协作的类:抽象行为控制器 Behavior 与马尔可夫转移矩阵 StochasticMatrix。前者定义角色对外暴露的全部行为入口(自动待机、手动切换、走动、点击、拖拽、落地等),后者以带权重的随机转移矩阵决定"下一段自动播放什么动画",使角色行为看起来自然而不机械。

目的与范围

本页覆盖 core/src/cn/harryh/arkpets/animations 包中的行为决策核心:

  • Behavior(抽象类):行为控制器的统一门面,含自动动画的缓存决策机制与全部可覆写的交互钩子。
  • StochasticMatrix:管理自动播放动画状态转移的随机(马尔可夫)矩阵,包括默认权重表、状态禁用、动画绑定与调试输出。
  • StochasticState / StochasticMatrixRow:六态行为枚举(含环形遍历)与矩阵行记录类型(轮盘赌随机算法)。

以下内容有意留给兄弟页面,本页不展开:

  • 动画剪辑与合成(AnimClip、AnimClipGroup、AnimComposer、AnimData 的内部实现)——本页仅将 AnimData 视为决策结果的数据载体。
  • 模型资产加载与骨骼装配(assets 包)、桌面窗口与渲染流程、多进程通信(concurrent 包)。
  • 同包中的 GeneralBehavior 是 Behavior 的具体实现(通用行为集),本页聚焦抽象决策机制,其内部绑定细节请直接参阅其源码。

概述

桌宠角色的行为分为两类,二者由同一个 Behavior 实例统一管理:

  1. 自动行为(随机决策):角色在无人干预时循环播放待机、坐下、睡眠、左右移动、特殊动作等动画。下一段播什么不是均匀随机,而是由一个 6×6 加权转移矩阵(马尔可夫链)决定——例如"睡眠"状态有 60% 倾向继续睡眠,且完全不会直接跳到移动;"特殊动作"永不自环,避免连续重复。这使行为序列呈现自然的节奏感。
  2. 交互行为(确定性钩子):走动(walkAnim)、鼠标按下/抬起(clickStart/clickEnd)、拖拽(dragging)、抛下落地(dropped)、默认动画(defaultAnim)等,由子类覆写提供,基类默认返回 null。

自动决策带缓存机制:结果 AnimData 会被缓存,缓存有效期取「动画片段时长」与 0.5 秒下限中的较大值,避免同一动画被瞬间重复决策、也避免每帧都掷骰子。

架构

Loading diagram...

各组件职责与设计意图:

组件职责设计意图
Behavior对外行为门面;装配自动决策缓存把"随机决策"与"交互钩子"收敛到一个对象,调用方无需理解矩阵
Cached<AnimData>(actionAutoGetter)缓存自动动画决策结果决策昂贵且不应每帧执行;有效期与动画时长对齐
StochasticMatrix持有权重行、禁用数组、动画绑定数组把"状态图 + 概率 + 动画映射"集中在一处,可整体替换
StochasticMatrixRow单状态的出边权重 + 轮盘赌采样record 类型保证不可变长度;disabledRef 共享引用实现"一处禁用、全行生效"
StochasticState六个行为状态 + 环形 next()/prev()支持用户手动逐态切换(右键/快捷键场景)

类型关系(全部经源码验证):

Loading diagram...

核心流程

自动行为决策:马尔可夫转移 + 缓存

自动动画的决策入口是 Behavior 构造器中装配的 Cached 值生产器(value producer)。每当缓存过期,autoAnim() 才会真正触发一次矩阵采样:

java
1public Behavior() { 2 actionAutoGetter = new Cached<>(); 3 actionAutoGetter.setValueProducer(() -> { 4 StochasticState newState = currentMatrix.transitedAnimOf(currentState); 5 if (newState == null) { 6 return currentMatrix.getStateAnim(currentState); 7 } else { 8 currentState = newState; 9 return currentMatrix.getStateAnim(newState); 10 } 11 }); 12 actionAutoGetter.setCacheAgeProducer(() -> { 13 AnimData cache = actionAutoGetter.getCachedValue(); 14 return cache == null ? minAnimCacheAge : Math.max(minAnimCacheAge, cache.animClip().duration); 15 }); 16 currentMatrix = null; 17 currentState = null; 18}

Source: Behavior.java

逐行解读:

  1. transitedAnimOf(currentState) 调用当前状态对应矩阵行的 random(),按权重掷骰子得到新状态。
  2. 若采样返回 null(所有状态都被禁用导致 random() 走到末尾),则保持当前状态并返回其绑定动画——这是优雅降级,而不是抛异常。
  3. 否则更新 currentState 并返回新状态的绑定动画,状态机就此"前进一步"。
  4. 缓存年龄由上一次缓存的动画时长决定(不小于 0.5 秒 minAnimCacheAge),意味着"播放完这段再决定下一段",天然避免抖动。

轮盘赌采样:StochasticMatrixRow.random()

java
1public StochasticState random() { 2 int sum = IntStream.range(0, weights.length).filter(i -> !disabledRef[i]).map(i -> weights[i]).sum(); 3 int rnd = (int) (Math.random() * sum); 4 int acc = 0; 5 for (int i = 0; i < weights.length; i++) { 6 if (disabledRef[i]) 7 continue; 8 acc += weights[i]; 9 if (rnd < acc) 10 return StochasticState.values()[i]; 11 } 12 return null; 13}

Source: StochasticMatrix.java

算法是标准轮盘赌选择:先对未被禁用状态的权重求和 sum,在 [0, sum) 内取随机数,再顺序累加权重命中第一个超过随机数的状态。被禁用的状态权重直接跳过,等价于从状态空间中移除该列。当 sum == 0(全部禁用或权重全为 0)时 Math.random() * 0 == 0,循环不会命中任何分支,返回 null,由 Behavior 的降级逻辑兜底。

手动逐态切换:nextAnim() / prevAnim()

java
1public final AnimData nextAnim() { 2 if (currentMatrix.isAllDisabled()) return null; 3 AnimData newAnim = currentMatrix.nextAnimOf(currentState); 4 currentState = currentState.next(); 5 return newAnim; 6}

Source: Behavior.java

手动切换不走概率,而是沿 StochasticState 的环形序(IDLE → SIT → SLEEP → MOVE_L → MOVE_R → SPECIAL → IDLE)前进/后退。nextAnimOf 会跳过被禁用的状态:

java
1public AnimData nextAnimOf(StochasticState state) { 2 StochasticState newState = state; 3 for (int i = 0; i < StochasticState.values().length; i++) { 4 newState = newState.next(); 5 if (!disabled[newState.ordinal()]) 6 return binds[newState.ordinal()]; 7 } 8 return binds[state.ordinal()]; 9}

Source: StochasticMatrix.java

循环至多走满一圈(values().length 次);若一圈内全是禁用状态,回落到当前状态自身的绑定动画,保证调用者永远拿到非空结果(除非 binds 里未绑定,见失败模式)。

状态生命周期时序

Loading diagram...

默认权重矩阵

StochasticMatrix.DEFAULT_WEIGHTS 是 6×6 转移权重表,行是当前状态、列是目标状态(IDLE, SIT, SLEEP, MOVE_L, MOVE_R, SPECIAL):

java
1public static int[][] DEFAULT_WEIGHTS = new int[][]{ 2 // IDLE SIT SLEEP MOVE_L MOVE_R SPECIAL 3 {40, 20, 10, 10, 10, 10}, // IDLE -> ? 4 {30, 40, 20, 10, 10, 10}, // SIT -> ? 5 {20, 20, 60, 0, 0, 0}, // SLEEP -> ? 6 {40, 10, 0, 20, 20, 10}, // MOVE_L -> ? 7 {40, 10, 0, 20, 20, 10}, // MOVE_R -> ? 8 {50, 20, 10, 10, 10, 0} // SPECIAL -> ? 9};

Source: StochasticMatrix.java

权重语义与设计意图:

当前状态主要去向(权重最高)关键约束(权重为 0)行为效果
IDLE回到 IDLE (40)无待机最常见,是"稳态锚点"
SIT回到 SIT (40),转 IDLE (30)无坐姿短暂持续后回到待机
SLEEP继续睡 (60)→ MOVE_L / MOVE_R 均为 0睡觉不会"秒起就走",醒来必经 IDLE/SIT 过渡
MOVE_L转 IDLE (40)→ SLEEP 为 0移动后不会直接入睡
MOVE_R转 IDLE (40)→ SLEEP 为 0与 MOVE_L 对称,左右移动等价
SPECIAL转 IDLE (50)→ SPECIAL 为 0(不自环)特殊动作绝不连续播放两次

注意 MOVE_L → MOVE_L 与 MOVE_L → MOVE_R 各为 20:角色移动态有概率"再多走一段"或"折返",配合 MOVE_L/MOVE_R 行整体对称,保证左右行为无偏。这组默认值体现了马尔可夫链建模"自然行为节奏"的意图:长驻状态(睡眠)有惯性,过渡态(移动、特殊)快速回归锚点(IDLE)。

使用示例

构造矩阵、绑定动画、禁用状态

典型的具体行为实现(如 GeneralBehavior)会以默认权重建矩阵,按模型实际拥有的动画绑定各状态,并禁用模型缺失的状态:

java
1public StochasticMatrix(int[][] weights) { 2 if (weights.length != StochasticState.values().length) 3 throw new IllegalArgumentException("Weights length mismatch"); 4 this.weights = new StochasticMatrixRow[weights.length]; 5 this.disabled = new boolean[StochasticState.values().length]; 6 this.binds = new AnimData[StochasticState.values().length]; 7 for (int i = 0; i < weights.length; i++) 8 this.weights[i] = new StochasticMatrixRow(weights[i], this.disabled); 9} 10 11public void bind(StochasticState state, AnimData anim) { 12 binds[state.ordinal()] = anim; 13} 14 15public void disable(StochasticState state) { 16 disabled[state.ordinal()] = true; 17}

Source: StochasticMatrix.java

关键点:构造器为每一行传入同一个 this.disabled 引用(disabledRef),因此对任一状态调用 disable() 会立即影响所有行的采样与遍历——一次禁用全局生效,无需遍历更新各行。

交互钩子的默认契约

Behavior 基类对所有交互钩子返回 null,子类按需覆写;调用方必须接受"该行为不可用"的语义:

java
1/** Gets the walk animation. 2 * @param mobility 1=GoRight, -1=GoLeft. 3 * @return Animation data, or {@code null} if not available. 4 */ 5public AnimData walkAnim(int mobility) { 6 return null; 7} 8 9/** Gets the animation when the user starts dragging. 10 * @return Animation data, or {@code null} if not available. 11 */ 12public AnimData dragging() { 13 return null; 14}

Source: Behavior.java

调试矩阵输出

调试用叠加层(getDebugMatrix)把禁用状态编码为负数权重,便于 UI 上直观区分:

java
1public int[][] getDebugMatrix() { 2 int[][] matrix = new int[StochasticState.values().length][StochasticState.values().length]; 3 for (int i = 0; i < weights.length; i++) { 4 StochasticMatrixRow row = weights[i]; 5 for (int j = 0; j < row.weights.length; j++) { 6 if (row.disabledRef[j]) matrix[i][j] = -row.weights[j]; 7 else matrix[i][j] = row.weights[j]; 8 } 9 } 10 return matrix; 11}

Source: StochasticMatrix.java

API 参考

Behavior(抽象类)

方法签名说明
isAutoAnimExpiredpublic final boolean isAutoAnimExpired()自动动画缓存是否过期/为空;调用方用它决定是否刷新
autoAnimpublic final AnimData autoAnim()获取随机自动动画(带缓存);内部驱动状态转移
nextAnimpublic final AnimData nextAnim()环形序下一状态的动画,同时推进 currentState;全禁用时返回 null
prevAnimpublic final AnimData prevAnim()环形序上一状态的动画,同时回退 currentState;全禁用时返回 null
defaultAnimpublic AnimData defaultAnim()默认动画钩子,基类返回 null
walkAnimpublic AnimData walkAnim(int mobility)走动动画,mobility 取 1(向右)/ -1(向左);基类返回 null
clickStart / clickEndpublic AnimData clickStart() / clickEnd()鼠标按下/抬起动画;基类返回 null
dragging / droppedpublic AnimData dragging() / dropped()拖拽/抛下落地动画;基类返回 null
getDebugMatrixpublic int[][] getDebugMatrix()返回调试权重矩阵(禁用列为负值)
getCurrentMatrixStatepublic StochasticState getCurrentMatrixState()当前所处状态

StochasticMatrix

方法签名异常
构造器public StochasticMatrix(int[][] weights)行数不等于状态数时抛 IllegalArgumentException("Weights length mismatch")
transitedAnimOfpublic StochasticState transitedAnimOf(StochasticState state)—
nextAnimOf / prevAnimOfpublic AnimData nextAnimOf(StochasticState state)—
getStateAnimpublic AnimData getStateAnim(StochasticState state)—
bindpublic void bind(StochasticState state, AnimData anim)—
scalepublic void scale(StochasticState state, float factor)factor < 0 时抛 IllegalArgumentException("Scale factor cannot be positive")
disablepublic void disable(StochasticState state)—
isAllDisabledpublic boolean isAllDisabled()—
getDebugMatrixpublic int[][] getDebugMatrix()—

Source: StochasticMatrix.java

失败模式、边界与并发

  • 全禁用降级:StochasticMatrixRow.random() 在所有可用权重和为 0 时返回 null;Behavior 的自动决策生产器据此保持原状态继续播放当前动画,不抛异常。nextAnim()/prevAnim() 则在 isAllDisabled() 为真时直接返回 null,由调用方判断。
  • 未绑定的状态:binds[state.ordinal()] 若从未 bind,getStateAnim 会返回 null。矩阵本身不校验"启用但未绑定",正确用法是:模型缺少某状态动画时应同时调用 disable()(否则采样可能命中一个没有动画的状态)。
  • scale 的舍入:scale 使用 Math.round,factor < 1 多次缩放可能把权重缩到 0,等效于隐式禁用该出边;且源码中的校验文案为 "Scale factor cannot be positive"(实际拒绝的是负数),属于历史命名瑕疵。
  • StochasticMatrixRow 长度校验:record 的紧凑构造器要求 weights.length 必须等于状态数,否则 IllegalArgumentException("Weights length mismatch"),防止矩阵形状错误静默错位。
  • 空指针窗口:Behavior 构造后 currentMatrix/currentState 均为 null;子类必须在首次 autoAnim()/nextAnim() 前完成赋值,否则会 NPE(构造器仅装配生产器,不触发取值)。
  • 并发性:StochasticMatrix 与 Behavior 均未加锁。weights 数组是可变的(scale 直接改写)、disabled/binds 也是共享可变数组。这些对象假定在**单一线程(JavaFX 渲染线程)**内创建和使用,跨线程并发调用采样与禁用没有同步保证。
  • 缓存下限 0.5 秒:minAnimCacheAge 防止极短动画导致决策频率过高;首次取值前缓存为空时同样使用 0.5 秒兜底。

扩展点

  • 新增行为子类:继承 Behavior,在初始化时设置 currentMatrix(可用 DEFAULT_WEIGHTS 起步)与起始 currentState,按模型动画资产逐个 bind,缺哪个就 disable 哪个;再按需覆写 defaultAnim、walkAnim、clickStart/End、dragging、dropped。GeneralBehavior 即此模式的参考实现。
  • 自定义行为概率:向 StochasticMatrix 构造器传入自定义 int[][] 权重即可改变性格(如"多动"角色提高 MOVE 权重、"嗜睡"角色提高 SLEEP 权重);scale 可对单行整体缩放微调,配合 disable 完全剔除状态。
  • 调试可视化:Behavior.getDebugMatrix() + getCurrentMatrixState() 提供了实现调试 HUD 所需的全部数据(负值列即禁用项)。

相关链接

  • Behavior.java — 行为抽象基类与自动决策缓存
  • StochasticMatrix.java — 马尔可夫转移矩阵、状态枚举与轮盘赌采样
  • GeneralBehavior.java — 通用行为实现(兄弟页面素材)
  • 动画剪辑与合成机制(AnimClip、AnimClipGroup、AnimComposer、AnimData)见同层级动画页