Repository Wiki
isHarryh/Ark-Pets

渲染主循环与桌宠生命周期

本文深入剖析 ArkPets 桌宠运行时核心(core 模块)中由 libGDX 驱动的渲染主循环(ArkPets.render())与桌宠从创建到销毁的完整生命周期(create() → render() → resize() → dispose()),覆盖每帧的动画决策、物理推进、窗口位置缓动与轮廓绘制等真实控制流。

目的与范围

本页聚焦 "一帧之内发生了什么" 与 "桌宠实例从启动到退出的状态流转",即:

  • ArkPets(继承 InputApplicationAdaptor)的四个生命周期回调 create() / render() / resize() / dispose() 的完整执行序列;
  • ArkPets.render() 每帧流水线的四个阶段(角色渲染 → 动画选择 → 窗口位置/属性 → 轮廓状态);
  • ArkChar.render() 内部的两级渲染通道(SpineRenderPass 骨骼渲染 + PostProcessRenderPass 后处理);
  • 输入事件回调(鼠标按下/拖拽/松开/移动、按键)如何与主循环协同改变桌宠行为;
  • 涉及主循环的关键配置项与性能采样(CorePerformanceSampler)接入点。

以下内容有意留给兄弟页面,本页只做交叉引用:

  • Spine 骨骼渲染与后处理着色器的具体实现(SpineRenderPass、PostProcessRenderPass、EffectShader);
  • Plane 的物理积分算法(重力、摩擦、速度上限、落地判定);
  • GeneralBehavior / AnimComposer 的行为决策状态机与动画编排;
  • ArkConfig 的配置读写与持久化。

概述

ArkPets 的每个桌宠都是一个独立的 libGDX(LWJGL3 后端)窗口进程。libGDX 在主线程上以固定回调序列驱动应用:应用启动时调用一次 create(),随后按帧率反复调用 render(),窗口尺寸变化时回调 resize(),应用退出时回调 dispose()。

ArkPets 类是这套回调的宿主,它把"桌宠"这一概念拆分为若干协作者:

协作者职责
ArkChar cha角色渲染器:持有 Spine 骨骼、动画状态机与两级渲染通道
Plane plane物理平面:以屏幕底边为地板,管理桌宠的世界坐标与运动学
GeneralBehavior behavior行为决策:自动(AI)动画、拖拽/落地动画、点击动画的选取
MemberTrayImpl tray托盘与菜单;其 keepAnim 字段代表"动作模式"下用户锁定的动画
TransitionVector2 windowPosition窗口位置的缓动(easing)过渡,让移动平滑而非瞬移
HWndCtrl hWndMine原生窗口句柄(Win32),负责置顶、穿透、任务栏、坐标设置
CorePerformanceSampler每帧 beginFrame() / endFrame() 打点,用于性能遥测

理解本子系统的关键是:主循环既是渲染循环,也是逻辑循环。动画决策、物理推进、窗口坐标更新全部内联在 render() 中逐帧执行,没有独立的后台线程,这保证了 GL 上下文的单线程安全与帧间一致性。

架构

Loading diagram...

架构要点:

  • 回调宿主:InputApplicationAdaptor 同时继承了 ApplicationAdapter 并实现 InputProcessor,把 libGDX 的渲染回调与输入回调统一到一个类中:

    java
    abstract public class InputApplicationAdaptor extends ApplicationAdapter implements InputProcessor {

    Source: InputApplicationAdaptor.java

    它声明了抽象的 render() 与 resize(int w, int h)(分别见该文件第 56、59 行),强制子类实现主循环与尺寸回调。

  • 两级渲染通道:ArkChar 不直接绘制,而是把骨骼交给 SpineRenderPass(Pass 1,渲染 Spine 骨骼到画布),再把画布交给 PostProcessRenderPass(Pass 2,叠加轮廓描边、整体透明度与背景色)。两者共享同一个 DynamicOrthographicCamara。

  • 原生窗口桥接:ArkPets 在 create() 中用 WindowSystem.findWindow(null, APP_TITLE) 找到自身 HWND 并 attachGLFWWindow((Lwjgl3Graphics) Gdx.graphics) 绑定到 GLFW 窗口,此后每帧通过 updateWindow() 把逻辑坐标同步到桌面坐标。

  • 缓存化的原生查询:窗口 Z 序查询、焦点判断等高频 Win32 调用被 Cached<T> 包装,按"帧数过期"策略刷新,见下文性能章节。

桌宠生命周期详解

ArkPets 是 libGDX 的 ApplicationAdapter,其生命周期完全由 libGDX 主循环驱动。从进程启动到退出,桌宠实例经历四个回调阶段:

Loading diagram...

阶段 1:构造(逻辑初始化)

构造函数只做纯逻辑初始化,不触碰任何 GL 资源——这是刻意设计:GL 上下文在 create() 之前尚未就绪。构造阶段的核心工作是建立三个 Cached<T> 原生查询缓存:

java
1hWndTopmostGetter = new Cached<>(); 2hWndTopmostGetter.setValueProducer(this::refreshWindowIndex); 3hWndTopmostGetter.setCacheAgeProducer(() -> 8.0 / getReducedFPS()); 4 5isFocused = new Cached<>(); 6isFocused.setValueProducer(() -> hWndMine.isForeground()); 7isFocused.setCacheAgeProducer(() -> 4.0 / getReducedFPS()); 8 9hWndTransparentSetter = new Cached<>(); 10hWndPosSetter = new Cached<>(); 11config = appConfig;

Source: ArkPets.java

两个读缓存的过期年龄以"帧"为单位(8.0 / getReducedFPS() 即 8 帧、4.0 / getReducedFPS() 即 4 帧),把昂贵的 Win32 枚举/查询摊薄到多帧执行。hWndTransparentSetter 与 hWndPosSetter 是写侧缓存,用于跳过值未变化的重复设置。

阶段 2:create() —— 一次性装配

create() 按 6 个编号步骤完成桌宠的完整装配,顺序不可调换:

java
1// 1.App setup 2Logger.info("App", "Create with title \"" + APP_TITLE + "\""); 3Gdx.input.setInputProcessor(this); 4Gdx.graphics.setForegroundFPS(config.display_fps); 5registerDebugger(); 6 7// 2.Character setup 8cha = new ArkChar(config, config.display_scale); 9behavior = new GeneralBehavior(config, cha.animList); 10cha.adjustCanvas(behavior.defaultAnim().animClip().stage); 11cha.setAnimation(behavior.defaultAnim()); 12cha.setAlpha(config.opacity_normal); 13 14// 3.Plane setup 15plane = new Plane(); 16plane.setGravity(config.physic_gravity_acc); 17plane.setResilience(0); 18plane.setFrict(config.physic_air_friction_acc, config.physic_static_friction_acc); 19plane.setObjSize(cha.camera.getWidth(), cha.camera.getHeight()); 20plane.setSpeedLimit(config.physic_speed_limit_x, config.physic_speed_limit_y); 21Monitor primaryMonitor = refreshMonitorInfo(); 22plane.changePosition(0, 23 primaryMonitor.getWidth() * config.initial_position_x - cha.camera.getWidth() / 2f, 24 -(primaryMonitor.getHeight() * config.initial_position_y + cha.camera.getHeight()) 25); 26 27// 4.Window position setup 28windowPosition = new TransitionVector2( 29 ArkConfig.getEasingFunctionFrom(config.transition_type), 30 Math.max(0, config.transition_duration) 31); 32windowPosition.reset(plane.getX(), -(cha.camera.getHeight() + plane.getY()) + offsetY); 33windowPosition.setToEnd(); 34 35// 5.Window style setup 36hWndMine = WindowSystem.findWindow(null, APP_TITLE); 37hWndMine.attachGLFWWindow((Lwjgl3Graphics) Gdx.graphics); 38if (config.window_style_topmost) 39 hWndMine.setTopmost(true); 40if (config.window_style_toolwindow) 41 hWndMine.setTaskbar(false); 42updateWindow(); 43 44// 6.Tray icon setup 45tray = new MemberTrayImpl(this, new SocketClient());

Source: ArkPets.java

每一步的设计意图:

  1. App setup:把 this 注册为全局 InputProcessor,让鼠标/键盘事件流入本类;同时把 config.display_fps 下发给 libGDX 的前台帧率限制(VSync/FPS cap)。
  2. Character setup:构建 ArkChar 是最重的一步——加载 .atlas + .skel、执行采样式画布适配(见下文 ArkChar 小节)、编译着色器。随后用 cha.animList 构造 GeneralBehavior,并以 behavior.defaultAnim() 作为首帧动画。
  3. Plane setup:物理平面以"屏幕左下角为原点、Y 轴向下为负"的坐标系工作(注意 changePosition 第三参为负值),初始落点由 initial_position_x/y 按主显示器比例计算。
  4. Window position setup:窗口位置缓动器用配置的缓动函数与时长初始化,并 setToEnd() 直接跳到目标值,避免启动时出现从 (0,0) 滑入的动画。
  5. Window style setup:按标题查找自身 HWND 并与 GLFW 窗口绑定;topmost 保证桌面宠物浮于普通窗口之上,toolwindow 使其不出现在任务栏。
  6. Tray icon setup:最后创建托盘(含 SocketClient 用于与管理器通信),并调用 writeSystemInfo() 落盘遥测信息。

阶段 3 与 4:resize() / dispose()

java
1@Override 2public void resize(int x, int y) { 3 Logger.debug("Window", "Resized to " + x + " * " + y); 4} 5 6@Override 7public void dispose() { 8 Logger.info("App", "Dispose"); 9}

Source: ArkPets.java

桌宠窗口尺寸由 ArkChar 的画布适配算法在启动时决定,运行期尺寸由逻辑内部管理(如 changeStage() 中重新 adjustCanvas + setObjSize),因此 resize() 仅记录日志、dispose() 仅打印日志——GL 资源的最终回收交由 libGDX/LWJGL3 进程退出机制处理。这是一处"轻回调"的刻意取舍:桌宠进程生命周期与进程本身同生共死。

渲染主循环核心流程

ArkPets.render() 是整个桌宠运行时的心脏。每帧执行四个阶段,顺序固定:角色渲染 → 动画选择 → 窗口位置与属性 → 轮廓状态。

Loading diagram...

主循环源码(完整四阶段)

java
1@Override 2public void render() { 3 if (performanceSampler != null) 4 performanceSampler.beginFrame(); 5 6 // 1.Render the next frame. 7 cha.render(); 8 Gdx.graphics.setForegroundFPS((int) getReducedFPS()); 9 10 // 2.Select a new animation. 11 AnimData newAnim; 12 if (tray.keepAnim == null) { 13 if (behavior.isAutoAnimExpired()) { 14 newAnim = behavior.autoAnim(); // AI anim. 15 } else { 16 newAnim = null; 17 } 18 } else { 19 newAnim = tray.keepAnim; 20 } 21 22 if (!isMouseDragging()) { // If no dragging: 23 plane.updatePosition(Gdx.graphics.getDeltaTime()); 24 if (cha.getPlaying().mobility() != 0) { 25 int mobility = cha.getPlaying().mobility(); 26 if (tray.keepAnim == null && willReachBorder(mobility)) { 27 // Turn around if auto-walk cause the collision from screen border. 28 newAnim = cha.getPlaying(); 29 mobility = -mobility; 30 newAnim = new AnimData(newAnim.animClip(), null, newAnim.isLoop(), newAnim.isStrict(), mobility); 31 tray.keepAnim = tray.keepAnim == null ? null : newAnim; 32 } 33 walkWindow(config.behavior_walk_speed * (isCtrlPressed() ? 2 : 1) * mobility); 34 } 35 } else { // If dragging: 36 newAnim = behavior.dragging(); 37 } 38 if (plane.getDropping()) { // If dropping, do not change anim. 39 newAnim = behavior.defaultAnim(); 40 } else if (plane.getDropped()) { // If dropped, play the dropped anim. 41 newAnim = behavior.dropped(); 42 } else if (tray.keepAnim != null) { // If action-mode is enabled. 43 if (isLeftPressed()) newAnim = behavior.walkAnim(-1); // Left pressed 44 else if (isRightPressed()) newAnim = behavior.walkAnim(1); // Right pressed 45 } 46 changeAnimation(newAnim); // Apply the new anim. 47 48 // 3.Window properties. 49 windowPosition.reset(plane.getX(), -(cha.camera.getHeight() + plane.getY()) + offsetY); 50 windowPosition.addProgress(Gdx.graphics.getDeltaTime()); 51 updateWindow(); 52 53 // 4.Outline. 54 boolean renderOutline = switch (ArkConfig.getRenderOutlineFrom( 55 tray.keepAnim != null ? config.render_outline_emphasis : config.render_outline 56 )) { 57 case ALWAYS -> true; 58 case PRESSING -> isMouseDown(); 59 case FOCUSED -> isFocused.getValue(); 60 case DRAGGING -> isMouseDragging(); 61 default -> false; 62 }; 63 cha.setOutlineAlpha(renderOutline ? 1f : 0f); 64 cha.setOutlineColor(ArkConfig.getGdxColorFrom( 65 tray.keepAnim != null ? config.render_outline_emphasis_color : config.render_outline_color 66 )); 67 68 if (performanceSampler != null) { 69 performanceSampler.endFrame( 70 Gdx.graphics.getDeltaTime(), 71 cha.camera.getWidth(), 72 cha.camera.getHeight() 73 ); 74 } 75}

Source: ArkPets.java

逐阶段解析

阶段 1 —— 角色渲染:cha.render() 先于一切逻辑执行,保证本帧屏幕上呈现的是"上一帧决策 + 本帧推进"的骨架姿态(ArkChar.render() 内部用 Gdx.graphics.getDeltaTime() 推进动画状态,见下节)。随后用 Gdx.graphics.setForegroundFPS((int) getReducedFPS()) 动态调整前台帧率上限——这是省电设计:当桌宠处于静止/普通状态时降低目标 FPS,交互(拖拽、聚焦)时再拉高。

阶段 2 —— 动画决策(优先级仲裁):这是一个"后写胜出"的优先级链,顺序从低到高:

  1. 普通模式下若自动动画计时到期(behavior.isAutoAnimExpired()),取 behavior.autoAnim()(AI 随机行为);动作模式下则始终使用 tray.keepAnim;
  2. 非拖拽时推进 plane.updatePosition(deltaTime) 物理;若当前动画具有位移(mobility() != 0),用 walkWindow() 平移窗口,按住 Ctrl 时速度翻倍(isCtrlPressed() ? 2 : 1);若自动行走即将撞到屏幕边界(willReachBorder(mobility)),把 mobility 取反生成"折返"动画——注意这里用 AnimData 五参构造以 null 中间参数派生新动画;
  3. 拖拽中则直接切换为 behavior.dragging();
  4. 最后的覆盖序列:下落中(Dropping)→ 恢复默认动画;刚落地(Dropped)→ 播放落地动画;动作模式且方向键按住 → behavior.walkAnim(±1) 强制行走。changeAnimation(newAnim) 收敛所有分支。

阶段 3 —— 窗口位置缓动:windowPosition.reset(plane.getX(), -(cha.camera.getHeight() + plane.getY()) + offsetY) 完成"物理世界坐标 → 桌面窗口坐标"的换算(Y 轴翻转并补偿画布高度与当前动画的 offsetY),随后 addProgress(deltaTime) 推进缓动进度、updateWindow() 落到原生 HWND。因为物理平面与缓动器都以 delta time 驱动,窗口移动天然与帧率解耦。

阶段 4 —— 轮廓状态:按 render_outline(普通态)或 render_outline_emphasis(动作模式,即 keepAnim != null 时强调态)的四种模式(ALWAYS/PRESSING/FOCUSED/DRAGGING)决定是否绘制描边,并把目标 alpha(0 或 1)与颜色传给 ArkChar。isFocused.getValue() 正是构造期建立的 4 帧缓存。

ArkChar.render() —— 两级渲染通道

java
1protected 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(); 9 // Apply current animation 10 animationState.apply(skeleton); 11 animationState.update(Gdx.graphics.getDeltaTime()); 12 // Render Pass 1: Render the skeleton 13 spineRenderPass.render(skeleton); 14 // Render Pass 2: Render additional effects 15 postProcessRenderPass.render(); 16}

Source: ArkChar.java

骨架坐标系分三层:

  • position(TransitionVector3):x 为画布内水平位置(每帧重置到画布水平中心 camera.getWidth() >> 1),y 为竖直位置,z 为朝向缩放(setScaleX 负值即翻转);x/z 由 AnimComposer.onApply 回调写入;
  • offsetY(TransitionFloat):动画类型相关的竖直偏移(例如某些动作需要整体下沉),在切换动画时通过 onApply 重置,随后逐帧缓动到目标;
  • updateWorldTransform() + animationState.apply(skeleton) + animationState.update(deltaTime) 是 Spine 标准三步:世界变换 → 应用姿态 → 按时间推进混合。

renderAdditive(float alpha) 是本类的第二条渲染路径:忽略 delta time 与复杂着色器,直接用 position.end()(目标值而非当前缓动值)以指定透明度加法叠加绘制,专供启动期采样画布尺寸时使用(见下文画布适配)。

画布适配与渲染通道初始化

ArkChar 构造函数分为 8 步,其中第 7 步"Canvas fitting"通过离屏 FBO 采样每个 AnimStage 的全部动画,估算出恰好覆盖内容的画布插入区(Insert):

java
1// 7.Canvas fitting 2stageInsertMap = new HashMap<>(); 3for (AnimStage stage : animList.clusterByStage().keySet()) { 4 // Figure out the suitable canvas size 5 adjustCanvas(stage, config.canvas_sampling_interval, config.canvas_coverage); 6 if (!camera.isInsertMaxed()) { 7 // Succeeded 8 stageInsertMap.put(stage, camera.getInsert().clone()); 9 Logger.info("Character", stage + " using " + camera); 10 } else { 11 // Failed, then not to put into stageInsertMap 12 Logger.warn("Character", stage + " unable to find a proper canvas size"); 13 } 14} 15camera.setInsertMaxed();

Source: ArkChar.java

采样算法的加法叠加 alpha 有一个解析推导的防溢出公式,保证多帧叠加后的像素饱和度逼近 coverage 阈值:

java
float alphaPerSample = (float) Math.max(1.0 - 254.0 / 255.0, Math.min(1.0, 1.0 - Math.pow(10.0, -4.0 / totalSamples) + Math.pow(10, 1.0 / totalSamples - 2.0) ));

Source: ArkChar.java

适配成功的舞台被记入 stageInsertMap,运行期 adjustCanvas(AnimStage) 直接查表:

java
1public void adjustCanvas(AnimStage animStage) { 2 if (!stageInsertMap.containsKey(animStage)) { 3 Logger.error("Character", "Failed to adjust the canvas because the given stage corrupted"); 4 throw new IndexOutOfBoundsException("No such key " + animStage); 5 } 6 camera.setInsert(stageInsertMap.get(animStage)); 7}

Source: ArkChar.java

这正是 ArkPets.changeStage()(用户切换动画舞台)的底层支撑:nextStage() → adjustCanvas(...) → plane.setObjSize(...) → changeAnimation(defaultAnim()) 四步联动(见 ArkPets.java)。

输入事件与主循环的协同

输入回调由 InputApplicationAdaptor 分发(Gdx.input.setInputProcessor(this) 在 create() 中注册),它们不直接渲染,而是改写状态供下一帧 render() 消费:

回调触发条件对主循环的影响
onMouseDown()鼠标按下命中实体像素→左键播放 behavior.clickStart()、右键 tray.toggleDialog(...);未命中→把鼠标事件透传给下层窗口 rwp.sendMouseEvent(...)
onMouseDrag()拖拽中(非右键)用 getMouseDeltaX/Y() 更新 plane.changePosition 并 windowPosition.setToEnd()(拖拽时禁用缓动,位置即时跟随),超阈值时按 behavior_direction_switching >= 2 翻转朝向
onMouseUp()松开播放 clickEnd();按 behavior_direction_switching >= 1 结束拖拽时翻转朝向;未命中时透传事件
onMouseMoved()移动未命中实体像素则透传 MOUSEMOVE;命中时按 behavior_direction_switching >= 3 翻转朝向
onKeyDown(int)按键动作模式下 ↑/↓ 切换 tray.keepAnim(跳过 MOVE 类型动画)
java
1@Override 2protected void onMouseDown() { 3 if (!isMouseAtSolidPixel()) { 4 // Transfer mouse event 5 RelativeWindowPosition rwp = getUnderlyingRWP(); 6 if (rwp != null) 7 rwp.sendMouseEvent(switch (getMouseButton()) { 8 case Input.Buttons.LEFT -> HWndCtrl.MouseEvent.LBUTTONDOWN; 9 case Input.Buttons.RIGHT -> HWndCtrl.MouseEvent.RBUTTONDOWN; 10 case Input.Buttons.MIDDLE -> HWndCtrl.MouseEvent.MBUTTONDOWN; 11 default -> HWndCtrl.MouseEvent.EMPTY; 12 }); 13 } else { 14 if (getMouseButton() == Input.Buttons.LEFT) { 15 // Left Click: Play the specified animation 16 changeAnimation(behavior.clickStart()); 17 tray.hideDialog(); 18 } else if (getMouseButton() == Input.Buttons.RIGHT) { 19 // Right Click: Toggle the menu 20 tray.toggleDialog((int) (plane.getX() + getMouseX()), (int) (-plane.getY() - cha.camera.getHeight())); 21 } 22 } 23}

Source: ArkPets.java

"实体像素"判定是穿透式桌宠窗口的关键——从帧缓冲区逐像素回读:

java
1private boolean isMouseAtSolidPixel() { 2 int pixel = cha.getPixel(getMouseX(), cha.camera.getHeight() - getMouseY() - 1); 3 return (pixel & 0x000000FF) > 0; 4}

Source: ArkPets.java

cha.getPixel(x, y) 内部用 Pixmap.createFromFrameBuffer(x, y, 1, 1) 回读 1 个像素并立即 dispose()(见 ArkChar.java),Y 坐标需按屏幕/画布坐标差异翻转(camera.getHeight() - getMouseY() - 1)。changeMobilitySign(sign) 是朝向翻转的统一入口,同时修正 cha.position 的 z 分量、当前动画与 tray.keepAnim(见 ArkPets.java)。

帧序列交互图

把主循环与渲染通道、原生窗口、物理平面之间的时序展开如下:

Loading diagram...

主循环相关配置项

以下配置项直接参与主循环行为(均来自 ArkConfig,详见配置模块页面):

配置项类型(默认)在主循环中的作用
display_fpsintcreate() 中一次性下发给 libGDX 前台帧率上限
display_scalefloatnew ArkChar(config, display_scale) 骨架缩放,影响画布尺寸
opacity_normal / opacity_dimfloat普通态/置灰态整体透明度,经 cha.setAlpha 生效;setTransparentMode(true) 切换
physic_gravity_accfloatplane.setGravity,每帧 updatePosition 的重力加速度
physic_air_friction_acc / physic_static_friction_accfloatplane.setFrict 空气/静摩擦
physic_speed_limit_x / physic_speed_limit_yfloatplane.setSpeedLimit 速度上限
behavior_walk_speedfloatwalkWindow 行走速度(Ctrl 时 ×2)
behavior_direction_switchingint(0–3)朝向翻转阈值等级:1=松开时、2=拖拽中、3=悬停移动时
transition_type / transition_durationenum / intwindowPosition、ArkChar.position/offsetY 的缓动函数与时长
render_outline / render_outline_colorenum / color普通态轮廓模式与颜色
render_outline_emphasis / render_outline_emphasis_colorenum / color动作模式(keepAnim 非空)强调轮廓
canvas_sampling_interval / canvas_coverageint / float启动期画布适配的采样间隔与覆盖率
render_enable_mipmap / render_animation_mixturebool / float纹理 mipmap;动画混合时长(applyCompleteAnimMix)
initial_position_x / initial_position_yfloat(0–1)按主显示器比例计算初始落点
window_style_topmost / window_style_toolwindowboolHWND 置顶 / 隐藏任务栏按钮

API 参考

ArkPets(渲染与生命周期相关)

方法签名说明
构造ArkPets(String title, ArkConfig appConfig, CorePerformanceSampler performanceSampler)仅初始化逻辑状态与 Cached 缓存,不触碰 GL
create()void(覆写回调)六步装配:输入/FPS → 角色与行为 → 物理平面 → 窗口位置缓动 → 窗口样式 → 托盘
render()void(覆写回调)每帧四阶段主循环,见上文
resize(int x, int y)void(覆写回调)仅记录调试日志
dispose()void(覆写回调)仅记录日志
canChangeStage()booleanbehavior.getStages().size() > 1 时才允许切舞台
changeStage()voidnextStage → adjustCanvas → setObjSize → changeAnimation(defaultAnim)
setTransparentMode(boolean)void切换 opacity_dim/opacity_normal
changeAnimation(AnimData)private voidcha.setAnimation 成功后同步 offsetY = animClip.type.offsetY * display_scale
onMouseDown/Drag/Up/Movedprotected void输入回调,见协同表
onKeyDown(int keycode)protected void动作模式下 ↑/↓ 切换 keepAnim(跳过 MOVE 类)
isMouseAtSolidPixel()private boolean帧缓冲单像素回读判定命中
updateWindow()private void经 hWndTransparentSetter/hWndPosSetter 写缓存同步 HWND 坐标与样式

ArkChar(渲染相关)

方法签名说明
构造ArkChar(ArkConfig config, float scale)八步初始化:相机 → 两级渲染通道 → 几何 → 骨骼 → 动画表 → 动画状态 → 画布 → 画布适配 → 动画混合
render()protected void推进缓动与动画状态,执行 Pass 1/Pass 2
renderAdditive(float alpha)protected void忽略 delta 与复杂着色器的加法叠加渲染(画布采样专用)
setAnimation(AnimData)boolean经 composer.offer 请求切换动画,成功返回 true
getPlaying()AnimData读取 composer.getPlaying()
setAlpha(float) / setOutlineAlpha(float) / setOutlineColor(Color)void转发给 postProcessRenderPass 对应接口
getPixel(int x, int y)intPixmap.createFromFrameBuffer 回读 RGBA8888,用后即 dispose
adjustCanvas(AnimStage)void查 stageInsertMap 设置画布插入区;无键抛 IndexOutOfBoundsException
setCanvas(Color)void居中骨架并设置背景色纹理

故障与边界情况

  • 骨骼加载失败:ArkChar 构造捕获 SerializationException | GdxRuntimeException 后包装为 RuntimeException("Launch ArkPets failed, the model asset may be inaccessible.") 上抛,进程启动即终止——主循环不会在残缺资源上空转。
  • 骨骼兼容性修复:SkeletonLoader.needFix() 为真时调用 fixed() 自动修复(启动期一次性,不进主循环)。
  • 舞台缺键:adjustCanvas(AnimStage) 查表失败记录 error 日志并抛 IndexOutOfBoundsException;而构造期适配失败的舞台仅 warn 并不入表,changeStage() 会因此被 canChangeStage() 间接限制。
  • 拖拽中的物理暂停:isMouseDragging() 为真时跳过 plane.updatePosition,位置由 onMouseDrag 直接写入 plane.changePosition 且 windowPosition.setToEnd() 立即跟随,避免缓动滞后造成的"橡皮筋"手感。
  • 单线程模型:所有 GL 操作(渲染、getPixel 回读)都发生在 libGDX 主线程;SocketClient 与托盘只改写 tray.keepAnim 等共享可变状态,供下一帧消费,规避了 GL 上下文跨线程问题。
  • 帧率自适应:getReducedFPS() 每帧重设 setForegroundFPS,静止时降帧省电;同时 Cached 把原生查询摊到 4/8 帧,是主循环的两个主要节流点。

性能与运维要点

  • 每帧成本热点依次是:cha.render()(骨骼变换 + 两级渲染通道)、updateWindow()(Win32 调用,经写缓存去重)、isFocused/hWndTopmostGetter 读缓存查询。
  • CorePerformanceSampler.beginFrame() / endFrame(deltaTime, width, height) 包裹整个帧体,writeSystemInfo() 在 create() 末尾落盘,配合 WAL 写入器(WalWriter/WalSystemInfoCodec)输出遥测。
  • 启动期画布适配(adjustCanvas(stage, framePerSample, coverage))是最重的初始化步骤,复杂度正比于舞台数 × 每舞台动画数 × 采样数;canvas_sampling_interval 越大采样越稀疏、启动越快。

相关链接

  • ArkChar.java —— 角色渲染器与画布适配
  • ArkPets.java —— 主循环与生命周期宿主
  • InputApplicationAdaptor.java —— libGDX 回调与输入适配基类
  • 渲染通道细节(SpineRenderPass / PostProcessRenderPass / EffectShader)见 渲染管线 页面
  • 物理运动学(Plane)见 物理引擎 页面
  • 行为决策(GeneralBehavior / AnimComposer)见 动画与行为系统 页面
  • 配置项定义与持久化见 配置系统 页面

Sources

(2 files)