渲染主循环与桌宠生命周期
本文深入剖析 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 上下文的单线程安全与帧间一致性。
架构
架构要点:
-
回调宿主:
InputApplicationAdaptor同时继承了ApplicationAdapter并实现InputProcessor,把 libGDX 的渲染回调与输入回调统一到一个类中:javaabstract 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 主循环驱动。从进程启动到退出,桌宠实例经历四个回调阶段:
阶段 1:构造(逻辑初始化)
构造函数只做纯逻辑初始化,不触碰任何 GL 资源——这是刻意设计:GL 上下文在 create() 之前尚未就绪。构造阶段的核心工作是建立三个 Cached<T> 原生查询缓存:
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 个编号步骤完成桌宠的完整装配,顺序不可调换:
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
每一步的设计意图:
- App setup:把
this注册为全局InputProcessor,让鼠标/键盘事件流入本类;同时把config.display_fps下发给 libGDX 的前台帧率限制(VSync/FPS cap)。 - Character setup:构建
ArkChar是最重的一步——加载.atlas+.skel、执行采样式画布适配(见下文 ArkChar 小节)、编译着色器。随后用cha.animList构造GeneralBehavior,并以behavior.defaultAnim()作为首帧动画。 - Plane setup:物理平面以"屏幕左下角为原点、Y 轴向下为负"的坐标系工作(注意
changePosition第三参为负值),初始落点由initial_position_x/y按主显示器比例计算。 - Window position setup:窗口位置缓动器用配置的缓动函数与时长初始化,并
setToEnd()直接跳到目标值,避免启动时出现从 (0,0) 滑入的动画。 - Window style setup:按标题查找自身 HWND 并与 GLFW 窗口绑定;
topmost保证桌面宠物浮于普通窗口之上,toolwindow使其不出现在任务栏。 - Tray icon setup:最后创建托盘(含
SocketClient用于与管理器通信),并调用writeSystemInfo()落盘遥测信息。
阶段 3 与 4:resize() / dispose()
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() 是整个桌宠运行时的心脏。每帧执行四个阶段,顺序固定:角色渲染 → 动画选择 → 窗口位置与属性 → 轮廓状态。
主循环源码(完整四阶段)
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 —— 动画决策(优先级仲裁):这是一个"后写胜出"的优先级链,顺序从低到高:
- 普通模式下若自动动画计时到期(
behavior.isAutoAnimExpired()),取behavior.autoAnim()(AI 随机行为);动作模式下则始终使用tray.keepAnim; - 非拖拽时推进
plane.updatePosition(deltaTime)物理;若当前动画具有位移(mobility() != 0),用walkWindow()平移窗口,按住 Ctrl 时速度翻倍(isCtrlPressed() ? 2 : 1);若自动行走即将撞到屏幕边界(willReachBorder(mobility)),把 mobility 取反生成"折返"动画——注意这里用AnimData五参构造以null中间参数派生新动画; - 拖拽中则直接切换为
behavior.dragging(); - 最后的覆盖序列:下落中(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() —— 两级渲染通道
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):
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 阈值:
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) 直接查表:
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 类型动画) |
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
"实体像素"判定是穿透式桌宠窗口的关键——从帧缓冲区逐像素回读:
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)。
帧序列交互图
把主循环与渲染通道、原生窗口、物理平面之间的时序展开如下:
主循环相关配置项
以下配置项直接参与主循环行为(均来自 ArkConfig,详见配置模块页面):
| 配置项 | 类型(默认) | 在主循环中的作用 |
|---|---|---|
display_fps | int | create() 中一次性下发给 libGDX 前台帧率上限 |
display_scale | float | new ArkChar(config, display_scale) 骨架缩放,影响画布尺寸 |
opacity_normal / opacity_dim | float | 普通态/置灰态整体透明度,经 cha.setAlpha 生效;setTransparentMode(true) 切换 |
physic_gravity_acc | float | plane.setGravity,每帧 updatePosition 的重力加速度 |
physic_air_friction_acc / physic_static_friction_acc | float | plane.setFrict 空气/静摩擦 |
physic_speed_limit_x / physic_speed_limit_y | float | plane.setSpeedLimit 速度上限 |
behavior_walk_speed | float | walkWindow 行走速度(Ctrl 时 ×2) |
behavior_direction_switching | int(0–3) | 朝向翻转阈值等级:1=松开时、2=拖拽中、3=悬停移动时 |
transition_type / transition_duration | enum / int | windowPosition、ArkChar.position/offsetY 的缓动函数与时长 |
render_outline / render_outline_color | enum / color | 普通态轮廓模式与颜色 |
render_outline_emphasis / render_outline_emphasis_color | enum / color | 动作模式(keepAnim 非空)强调轮廓 |
canvas_sampling_interval / canvas_coverage | int / float | 启动期画布适配的采样间隔与覆盖率 |
render_enable_mipmap / render_animation_mixture | bool / float | 纹理 mipmap;动画混合时长(applyCompleteAnimMix) |
initial_position_x / initial_position_y | float(0–1) | 按主显示器比例计算初始落点 |
window_style_topmost / window_style_toolwindow | bool | HWND 置顶 / 隐藏任务栏按钮 |
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() | boolean | behavior.getStages().size() > 1 时才允许切舞台 |
changeStage() | void | nextStage → adjustCanvas → setObjSize → changeAnimation(defaultAnim) |
setTransparentMode(boolean) | void | 切换 opacity_dim/opacity_normal |
changeAnimation(AnimData) | private void | cha.setAnimation 成功后同步 offsetY = animClip.type.offsetY * display_scale |
onMouseDown/Drag/Up/Moved | protected 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) | int | Pixmap.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)见 动画与行为系统 页面 - 配置项定义与持久化见 配置系统 页面