鼠标交互与手动模式
ArkPets 桌面宠物支持两类与鼠标相关的核心交互:指针拾取与拖拽(通过 libGDX 输入回调驱动角色移动与"被拎起/放下"动画)以及手动模式(Action Mode)(通过托盘菜单开启后,tray.keepAnim 锁定当前动画,用户可用鼠标与键盘直接控制角色行走、切换动作)。本页基于 v3.x 源码,剖析这两条交互链路从输入事件到渲染输出的完整控制流。
Purpose and Scope
本页覆盖以下内容:
- libGDX 输入事件如何经由
InputApplicationAdaptor进入应用主循环; - 手动模式的状态载体
tray.keepAnim的生命周期与托盘菜单入口("手动模式" / "退出手动"); - 手动模式下的动画决策逻辑:按住鼠标行走、松开拖拽后的
dropped()落地动画、按键切换动画; - 自动行走到达屏幕边界时的方向翻转与手动模式的互斥处理;
- 手动模式启用时的描边强调渲染(
render_outline_emphasis系列); - 拖拽移动背后的缓动过渡(
TransitionVector3/transition_duration)。
以下内容属于兄弟页面范围,本页仅引用不展开:
- 动画剪辑、动画分组与动画合成的完整实现(
AnimClip/AnimClipGroup/AnimComposer)——见"动画系统"相关页面; - 行为模型与随机状态机(
Behavior/GeneralBehavior/StochasticMatrix)——见"行为模型"相关页面; - 托盘菜单的整体架构与宿主托盘——见托盘相关章节;
- 渲染管线(
SpineRenderPass/PostProcessRenderPass)的着色器细节。
Overview
ArkPets 的角色窗口是一个透明、可穿透点击的桌面窗口,鼠标交互由 libGDX 桌面后端(LWJGL)捕获后转发给应用层:
- 拖拽移动:用户按住角色即可拖动窗口;由于角色位置通过带缓动函数的
TransitionVector3插值,拖拽释放后角色仍会平滑滑向目标位置,而不是瞬移。 - 手动模式:托盘菜单中的"手动模式"会把成员托盘的
keepAnim字段从null置为当前动画,此后主循环不再依赖行为模型的自动决策,而是:- 按住鼠标左键时调用
behavior.walkAnim(-1)让角色向左行走(松开后对称地向右); - 拖拽结束时调用
behavior.dropped()播放"落地"动画; - 键盘按键可在
keepAnim != null时直接切换到上一个/下一个动画; - 到达屏幕边界时对带位移(
mobility != 0)的手动动画执行方向翻转derive(...)。
- 按住鼠标左键时调用
- 视觉反馈:手动模式启用期间,角色描边改用强调配置
render_outline_emphasis/render_outline_emphasis_color,让用户一眼识别"手动控制中"的状态。
Architecture
图中各组件的职责与连接依据:
InputApplicationAdaptor是输入适配器,实现 libGDX 的InputProcessor风格回调(如touchDown),并记录lastActiveNanoTime等活跃时间戳,供主循环查询输入状态:
@Override
public boolean touchDown(int screenX, int screenY, int pointer, int button) {
lastActiveNanoTime = System.nanoTime();Source: InputApplicationAdaptor.java
MemberTray(成员托盘)持有手动模式的两个菜单项与状态字段。keepAnim为null表示自动模式,非null表示手动模式并锁定指定动画:
public abstract class MemberTray {
protected JMenuItem optKeepAnimEn = new JMenuItem("手动模式");
protected JMenuItem optKeepAnimDis = new JMenuItem("退出手动");Source: MemberTray.java
ArkPets主循环 每帧读取tray.keepAnim,决定是走自动行为(behavior.isAutoAnimExpired()触发随机切换)还是手动控制(walkAnim/dropped/ 键盘切换),最终通过ArkChar.setAnimation()把动画提交给合成器:
1 /** Requests to set the current animation of the character.
2 * @param animData The animation data.
3 * @return true if success.
4 */
5 public boolean setAnimation(AnimData animData) {
6 return composer.offer(animData);
7 }Source: ArkChar.java
PostProcessRenderPass负责描边后处理,手动模式时改用强调描边参数,形成可见的状态反馈。
核心控制流:每帧动画决策
ArkPets 主循环对 tray.keepAnim 的判读是整个手动模式的"心脏"。核心逻辑(位于 ArkPets 渲染循环内)分三个分支:
1. 自动模式分支(keepAnim == null)
AnimData newAnim;
if (tray.keepAnim == null) {
if (behavior.isAutoAnimExpired()) {Source: ArkPets.java
行为模型到期时重新抽取随机动画;随后针对自动行走做边界保护:
int mobility = cha.getPlaying().mobility();
if (tray.keepAnim == null && willReachBorder(mobility)) {
// Turn around if auto-walk cause the collision from screen border.Source: ArkPets.java
关键细节:即便发生转向,tray.keepAnim 仍保持原值不变(null 时仍为 null),即自动模式的转向不改变模式本身:
newAnim = new AnimData(newAnim.animClip(), null, newAnim.isLoop(), newAnim.isStrict(), mobility);
tray.keepAnim = tray.keepAnim == null ? null : newAnim;Source: ArkPets.java
这行 tray.keepAnim = tray.keepAnim == null ? null : newAnim 是刻意设计:把方向翻转后的动画重新写回 keepAnim,仅在手动模式下生效(替换为翻转后的版本),自动模式下保持 null 避免意外开启手动模式。
2. 手动模式分支(keepAnim != null)
newAnim = tray.keepAnim;
}Source: ArkPets.java
手动模式下,默认动画即用户锁定的 keepAnim;只有用户主动输入时才临时覆盖:
newAnim = behavior.dropped();
} else if (tray.keepAnim != null) { // If action-mode is enabled.
if (isLeftPressed()) newAnim = behavior.walkAnim(-1); // Left pressedSource: ArkPets.java
设计意图:手动模式是"锁定 + 临时覆盖"模型 —— 平时播放用户选定的动画,按住鼠标左键时切换为行走动画(walkAnim(-1) 表示向左),松开时回到 keepAnim;拖拽被释放(dropped())的瞬间优先播放落地动画,营造"被扔下"的物理感。
3. 键盘切换动画(仅手动模式可用)
protected void onKeyDown(int keycode) {
if (tray.keepAnim != null) { // Switch animation in action mode
AnimData data;Source: ArkPets.java
切换成功后写回 keepAnim,因此"手动选中的动画"会作为新的锁定值持续播放:
if (data != null) {
tray.keepAnim = data;
Logger.debug("Animation", "Switch to previous " + data);Source: ArkPets.java
手动模式的边界翻转
当用户在手动模式下把角色拖到屏幕边缘,带位移分量的动画会在边界处自动调头,防止角色"走出"屏幕。翻转通过 AnimData.derive(...) 派生一个仅修改 mobility 符号的新动画对象:
1 if (tray.keepAnim != null && tray.keepAnim.mobility() != 0) {
2 AnimData anim = tray.keepAnim;
3 tray.keepAnim = anim.derive(Math.abs(anim.mobility()) * sign);
4 }Source: ArkPets.java
要点:
- 只对
mobility() != 0的动画生效——原地动作(如待机、挥手)没有方向概念,无需翻转; - 使用
Math.abs(anim.mobility()) * sign而非直接取反,保证翻转结果是"严格按边界方向"的移动量,避免符号叠加错误; - 翻转直接写回
tray.keepAnim,因此用户锁定状态会随翻转更新,下一次切换方向时基于翻转后的值。
手动模式的视觉反馈:强调描边
手动模式不仅改变动画决策,也改变渲染参数。主循环在设置描边透明度与颜色时依据 tray.keepAnim 选择普通或强调配置:
boolean renderOutline = switch (ArkConfig.getRenderOutlineFrom(
tray.keepAnim != null ? config.render_outline_emphasis : config.render_outline
)) {Source: ArkPets.java
cha.setOutlineColor(ArkConfig.getGdxColorFrom(
tray.keepAnim != null ? config.render_outline_emphasis_color : config.render_outline_color
));Source: ArkPets.java
设计意图:用户在手动模式下往往正在拖拽或操控角色,视线不一定停留在托盘菜单上。通过描边颜色/样式的变化提供"即时、无歧义"的模式反馈,是一个零成本的 UX 增强——同一套后处理管线 PostProcessRenderPass 只是换了输入参数,没有引入新的渲染路径。
拖拽移动与位置过渡
角色位置并非直接赋值,而是通过 TransitionVector3(配以 EasingFunction)做帧间插值。ArkChar 构造函数中依据配置初始化过渡参数:
1 // 2.Geometry setup
2 EasingFunction easingFunction = ArkConfig.getEasingFunctionFrom(config.transition_type);
3 float easingDuration = Math.max(0, config.transition_duration);
4 position = new TransitionVector3(easingFunction, easingDuration);
5 offsetY = new TransitionFloat(easingFunction, easingDuration);Source: ArkChar.java
渲染时每帧推进过渡进度并应用到骨架:
1 protected void render() {
2 // Update skeleton position and geometry
3 position.reset(camera.getWidth() >> 1, position.end().y, position.end().z);
4 position.addProgress(Gdx.graphics.getDeltaTime());
5 offsetY.addProgress(Gdx.graphics.getDeltaTime());
6 skeleton.setPosition(position.now().x, position.now().y + offsetY.now());
7 skeleton.setScaleX(position.now().z);
8 skeleton.updateWorldTransform();Source: ArkChar.java
解读:
position的三维含义是(x, y, 面朝方向z),其中 z 通过setScaleX映射为水平镜像(正=朝右,负=朝左);position.reset(...)每帧把 x 锚定到画布中心camera.getWidth() >> 1,只让 y 与 z 参与缓动——这样角色始终居中于画布,拖拽改变的是窗口位置而非角色相对画布位置;- 动画切换时(
composer.onApply)会把当前动画的mobility与offsetY写入过渡终点,使不同动作的姿态差异也以缓动方式过渡:
1 composer = new AnimComposer(animationState) {
2 @Override
3 protected void onApply(AnimData playing) {
4 Logger.debug("Animation", "Apply " + playing);
5 // Sync skeleton position data
6 offsetY.reset(playing.animClip().type.offsetY * scale);
7 position.reset(position.end().x, position.end().y, playing.mobility() != 0 ? playing.mobility() : position.end().z);
8 }
9 };Source: ArkChar.java
注意 playing.mobility() != 0 ? playing.mobility() : position.end().z:只有带位移的动画才会改变面朝方向;原地动画(mobility 为 0)保持当前朝向不变,避免待机时角色无故翻转。
配置选项
与鼠标交互 / 手动模式直接相关的用户配置(定义于 ArkConfig,由桌面端 SettingsModule 提供 UI):
| 选项 | 类型 | 默认 | 说明 |
|---|---|---|---|
transition_type | string | — | 拖拽/方向变化的缓动函数类型,经 ArkConfig.getEasingFunctionFrom 解析 |
transition_duration | float | — | 过渡时长(秒),Math.max(0, ...) 保证非负;0 表示瞬移无缓动 |
render_outline | string | — | 自动模式下的描边样式 |
render_outline_color | string | — | 自动模式下的描边颜色 |
render_outline_emphasis | string | — | 手动模式专用的描边样式 |
render_outline_emphasis_color | string | — | 手动模式专用的描边颜色 |
API Reference
ArkChar.setAnimation(animData: AnimData): boolean
请求切换角色当前动画。
Parameters:
animData(AnimData): 目标动画数据,含剪辑、mobility、是否循环/严格模式等。
Returns: boolean — 是否成功提交给动画合成器(内部委托 composer.offer(animData))。
Source: ArkChar.java
InputApplicationAdaptor.touchDown(screenX, screenY, pointer, button): boolean
libGDX 输入回调,鼠标/触摸按下时触发,记录 lastActiveNanoTime 用于活跃状态判定。
Parameters:
screenX,screenY(int): 屏幕坐标pointer,button(int): 指针与按钮编号
Source: InputApplicationAdaptor.java
ArkPets.onKeyDown(keycode: int): void
键盘按下回调;仅在手动模式(tray.keepAnim != null)下生效,用于切换到上一个/下一个锁定动画。
Source: ArkPets.java
失败模式与边界情况
keepAnim指向不存在的动画:键盘切换时通过if (data != null)防御,切换失败时保持原锁定动画不变,不会崩溃。- 手动模式与自动行走互斥:边界检测条件
tray.keepAnim == null && willReachBorder(mobility)确保自动转向只发生在自动模式;手动模式的翻转走独立分支,二者不会同时干预同一帧决策。 - mobility 为 0 的动画:边界翻转、方向镜像同步均显式跳过(
mobility() != 0判断),避免原地动画被错误赋予方向语义。 - 配置非法值:
transition_duration通过Math.max(0, ...)钳制,负时长不会破坏过渡逻辑。 - 模型加载失败(与交互链路相关但属初始化阶段):
ArkChar构造中骨架加载异常会被包装为RuntimeException("Launch ArkPets failed, ...")抛出,窗口不会以半初始化状态进入交互循环。
Source: ArkChar.java
手动模式状态机
扩展点
- 新增输入手势:在
InputApplicationAdaptor中扩展回调,并在ArkPets主循环中参照isLeftPressed()的模式读取状态、用behavior.xxxAnim(...)或直接构造AnimData写入tray.keepAnim即可参与手动模式。 - 自定义模式反馈:手动模式的判定只有一个布尔量
tray.keepAnim != null,可在此基础上叠加自定义渲染效果(透明度、附加粒子等),调用ArkChar.setAlpha()/setOutlineColor()。 - 行为动画定制:手动模式的行走/落地动画来自
Behavior抽象(walkAnim(int)/dropped()),新行为模型只需重写这两个方法即可改变手动交互的表现。
Related Links
- 行为模型与随机动画决策(
Behavior/GeneralBehavior/StochasticMatrix):见"动画与行为"章节相关页面 - 动画剪辑与合成(
AnimClip/AnimClipGroup/AnimComposer):见"动画系统"相关页面 - 渲染管线与描边后处理(
PostProcessRenderPass):见渲染章节 - 托盘系统(
MemberTray/MemberTrayImpl/HostTray):见桌面集成章节