blob: ca3a8414411f330f82abf10f6c0176659dd6d0b0 (
plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
|
# 桌面渲染(Game / VillageArt / 字形图集)
源码:`src/LivingVillage.Desktop/Game.fs`、`src/LivingVillage.Desktop/VillageArt.fs`、
`src/LivingVillage.Desktop/CjkGlyphAtlas.fs`、`src/LivingVillage.Desktop/ChineseText.fs`、
`src/LivingVillage.Desktop/HudLayout.fs`。
## 职责
MonoGame 渲染与主循环:把 `World` 画成江南水乡画面(地面/河道/建筑/人物/夜景光晕/室内),
并绘制 HUD、对话浮层与各类面板。不负责模拟推进(`Sim.step` 在 Kernel)、不负责地图生成算法。
## 对外 API 概览
- `LivingVillageGame`(`Game` 子类):`override Update(gameTime)` / `override Draw(gameTime)`。
默认 `IsFixedTimeStep = true`、`TargetElapsedTime = 1/60s`、垂直同步开启;
`LV_UNLOCK_FPS=1` 时放开两者以测渲染上限。
- `VillageArt.loadTextures device` → `ArtTextures`;`VillageArt.drawWorld`、
`VillageArt.drawMapViewport` / `drawMapFitted` / `drawMapRegion`、`VillageArt.drawCharacter`、
`VillageArt.drawInterior` / `drawInteriorCharacter`、`VillageArt.drawNightGlows`、
`VillageArt.drawCc0BoatOverlay`、`VillageArt.drawGroundShadow`。
- 动画:`VillageArt.characterFrame isMoving tick`、`npcSpriteSpec*`、`avatarSpriteSpec`、
`waterFrameTickAt tick x y`、`smokeFrameTick tick`。
- 预乘与光晕:`VillageArt.premultiply r g b a`、`VillageArt.lanternGlowLayers`、
`VillageArt.ellipseRuns`。
- 中文字形:`CjkGlyphAtlas.columns = 16`、`cellPixels = 24`、`contains char`、
`sourceRectangle char`、`load device`;文案清单 `ChineseText.requiredUiLabels`;
HUD 几何 `HudLayout.barHeight = 36`、`textScale = 2`。
## 单位与量纲
- 视口默认 1280×720;世界像素 = 瓦片 × `Sim.tilePixels`(32)。
- 字形:每字一格 24×24 px,绘制时按 `PixelText` 的 `scale` 放大(HUD 常用 `scale = 2` → 每字 16px;
图集源格固定 24px)。ASCII 走 `PixelText.glyphs` 位图,空格宽 `4*scale`,其余 ASCII 宽 `6*scale`。
- 动画:字符格 32×48;`animationFrame = (tick/4) % 4`、`idleFrame = (tick/24) % 2`。
## 不变量
1. **预乘 alpha**:任何 `alpha < 255` 的前景绘制必须用 `VillageArt.premultiply`,
否则预乘混合会把 rgb 直接加进帧缓冲导致纯白块(见 `docs/维护说明.md` §预乘 alpha)。
2. **无缺字**:`PixelText.isRenderable` 要求每个字符 ∈ ASCII 位图 ∪ `CjkGlyphAtlas.contains`;
不可渲染字符按设计**留空**,绝不画假字。
3. **图集几何**:`CjkGlyphAtlas` 表保持有序稳定,`pixelWidth = 16*24 = 384`、
`pixelHeight = rowCount*24`;`load` 校验尺寸否则 `invalidOp`。
4. **帧选择纯函数**:动画/水波/炊烟只由 `tick` 与「是否移动」决定,同 tick 必得同帧。
5. **绘制分层**:`drawWorld` 末尾单独绘制炊烟;夜间光晕只在真实灯笼实体处产生。
## 失败分支
- `CjkGlyphAtlas.load`:文件缺失 → `FileNotFoundException`;尺寸不符 → `invalidOp` 并释放纹理。
- 文本:`PixelText.draw` 对不支持字符静默跳过(推进光标),不抛异常。
- 纹理加载:`VillageArt.loadTexture` 校验期望宽高,资产缺失/尺寸不符会失败(构建期资产必在)。
## 算法与性能取舍
- 绘制分层(`Game.DrawWorldView`):地面 → 结构/民居 → 夜间光晕 → 水面波纹/船 →
角色阴影与角色 → 黄金时刻暖幕 → 月光幕 → 面板/浮层(`DrawM5Overlay`)。
- 只绘制可见瓦片(`isVisible` 视口裁剪);光晕用预乘同心圆 pixel run 近似径向衰减。
- 帧循环结构:`Update`(键盘/菜单/证据钩子/`Sim` 推进)→ `Draw`(世界或菜单/证据帧);
证据钩子(`LV_*_SHOT`)在 Playing 帧后落盘并退出。
- 实测(`docs/evidence/p70-analysis.txt`,1280×720 / Xvfb 软件渲染 / 解锁 vsync):
60 秒 8,788 帧、`measured_fps ≈ 139.3`;帧间隔 mean 7.179 / max 33.919 / p95 9.847 ms;
掉帧 > 25ms = 4(0.05%)、> 33.3ms = 2(0.02%);每帧分配 mean ≈ 231 KB / p95 297 KB。
帧率随机器与软件渲染波动,只列本次实测,不外推。
- 复跑:`P70_SECONDS=60 bash scripts/bench-p70.sh /tmp/opencode/lv-p70`
(逐帧日志经 `LV_FRAME_LOG=1`,仅日志、不改渲染逻辑)。
|