下载 App
生成正面、背面、侧面,可以走动的角色形象
当前 NPC 角色编辑器的数据结构
编辑器使用 32×32 像素画布,数据结构大致是:
3 个朝向:正面、背面、侧面
2 个动画状态:静止(idle)、行走(walk)
每个状态可以有多帧,支持帧率控制
每帧本质上是一个 32×32 的颜色数组
要解决的 3 个问题
步骤 问题 方案
1. 图片→编辑器 128×128 PNG 导入为 32×32 像素数据 加载图片后逐像素采样(每4×4取1像素)

耳内有灰:
对话框和列表都正常了。但有两个问题:
导入了 0 个像素 — 日志显示 (128.0x128.0 -> 32x32) 但 导入完成: 0 个像素
KTX 格式错误 — Unsupported texture format in KTX file,说明 cache:GetResource("Image", ...) 加载了 GPU 压缩格式,GetPixel 无法正确读取
需要改用 Image() 直接加载文件来读取原始像素数据。
Image 有 IsCompressed() 方法。问题是 cache:GetResource("Image", ...) 可能自动加载了 KTX 压缩格式。需要用 Image:new() 创建并手动从文件加载,绕过缓存的压缩格式。
我可以用 cache:GetFile() 获取文件,然后用 Image:new() 创建 Image 对象并从文件 load。
Bot Avatar
现在修改 CharData:ImportFromImage,改用 Image:new() + cache:GetFile() 加载原始 PNG 数据。
构建成功。修复方案:
改用 cache:GetFile() + Image:new() + image:Load(file) 加载原始 PNG 数据
绕过 cache:GetResource("Image") 可能返回的 KTX GPU 压缩格式
加了 IsCompressed() 检查作为额外保险
可以预览测试——再次点击「导入」选择一张图片,应该能看到像素被正确导入到画布上了。
:
掉落物深度排序修复
问题根因:MapCanvas 的渲染分两个阶段:
_RenderBuildings — 建筑和树木统一按行号(sortRow)深度排序,远处先画、近处后画
_RenderNPCs — 角色和掉落物,没有排序,且在建筑/树木之后绘制
这导致所有掉落物无条件画在所有树木上面,不管掉落物实际上在哪棵树的前面还是后面。
修复方案:将掉落物纳入 _RenderBuildings 的统一深度排序:
_RenderBuildings 新增第 3 步:收集 _isDrop 标记的 NPC,根据 content 坐标 cy / ts + 1 计算 sortRow,与建筑/树木一起排序
新增 _RenderOneDrop 方法:处理掉落物的坐标变换和 renderFunc 调用
_RenderNPCs 增加 npc._isDrop 跳过判断,避免重复渲染
现在掉落物会和树木按同一深度轴排序,后方树木掉出的木头会被前方树木正确遮挡,Q/E 旋转后也不会出现"木头跑到树冠上"的视觉错误。
:
分析两个核心 bug:
Bug 1 - 加载界面不显示:beginPreloading() 的"快速路径"调用 PreloadOne() 时,实际执行了加载(不是仅检查缓存),所以 15 个纹理全部同步加载完,allCached=true,直接跳到 setupGameUI(),跳过了加载界面。
Bug 2 - 预加载无效,地图仍然渐进加载:预加载用的 NVG 上下文来自 ImageCache.GetContext(),但当 MapCanvas:Render(nvg) 被调用时,它传入的 nvg 参数再次调用 TerrainRegistry.SetNVG(nvg) — 如果这两个 nvg 的 Lua userdata 身份不同(即使指向同一个底层对象),nvg_ ~= nvg 判断为 true,整个 texCache_ 被清空!
解决方案:
移除快速路径,添加一个仅检查缓存的方法(不加载)
不在 beginPreloading 中设置 NVG 上下文,而是让预加载在 HandleUpdate 中进行,并确保使用的 NVG 上下文与 MapCanvas 后续使用的完全相同
但根本问题是:我们无法保证 ImageCache.GetContext() 返回的 userdata 与 Widget Render(nvg) 回调中的 nvg 是同一个 Lua 对象引用。让我先验证这一点。
不同玩家“发送”流言内容即时存入云端
我试着在分享链接测试留言板流言,重进分享链接测试发现流言没了,有没有办法保存其他玩家的流言,点击“发送”流言内容可以即时存入云端?核心规则:游戏体验不会改变 —— 不需要匹配、不需要房间
嗒:
当前问题:留言数据存在内存中,gameSaveMap_() 尝试写入 clientCloud,但 clientCloud 是按用户隔离的——每个玩家只能看到自己的数据,其他玩家的留言互不可见。要实现跨玩家共

:
WASM 下完整链路
玩家打开分享链接 (WASM)
→ background_match 秒连 → ServerReady
→ Server 实例启动
→ serverCloud:Get(0, "wall_bricks_all") ← 1 次读,全量加载
→ SYNC_ALL → 客户端合并 → 壁上看到所有人的留言 ✅
玩家点"发送"
→ RemoteEvent → Server 入队
→ FlushWriteQueue: Get → 合并 → Set ← 写完即持久化
→ broadcastBrickUpdate → 客户端立即看到 ✅
玩家刷新页面 / 重新进入
→ 新 Server 实例启动
→ serverCloud:Get(0, "wall_bricks_all") ← 读到上次写入的数据
→ SYNC_ALL → 留言还在 ✅
每个环节都是确定性的:
预加载:1 次 Get,不依赖注册表,不会遗漏
写入:RMW 写完 serverCloud:Set 返回即入云,不存在"队列没 flush"的窗口
重进:新实例同一个 key 直接读,数据完整
对比当前方案为什么 WASM 下会丢
当前方案的断裂点:
serverCloud.list:Add("brick_31", data) ← 数据入云了
registerBrickId(31) ← 异步,可能没写完
玩家刷新页面 ← 服务器销毁
新服务器 → 读注册表 → 没有 31 → 不去加载 → 留言"消失"
单 key 整包方案没有这个断裂点,因为数据和索引是同一个东西。
旧代码处理
serverCloud.list + 注册表的代码注释保留,不删。后面如果需要按交互类型做审计日志(谁在什么时间点了什么赞),list 的逐条记录模式更合适,可以从冷宫捞回来做"热数据整包 + 冷数据 list 归档"的双写。
确认方向没问题的话,我就开始改 Server.lua。
留言墙牌匾设计
1. 列表项跳转 + 返回定位
VirtualList 本身只渲染可见行,点击跳转到砖块详情不会有性能问题。返回定位的方案:
牌匾弹窗(VirtualList,记住 scrollOffset)
↓ 点击某条留言
砖块详情弹窗(showBrickViewModal)
↓ 关闭
牌匾弹窗(恢复之前的 scrollOffset)
实现方式:打开砖块详情前,记录当前 VirtualList 的 scr

:
问题原因:拆除屋檐/柱子时调用的是旧版全局方法 mapData:UninstallRoof(),它只检查已弃用的 self.roofInstalled 全局字段。而实际屋檐数据存储在 wallParts[wk].roofInstalled 中。
修复:从 piece.data.wallKey 解析出 anchorRow, anchorCol,改为调用 mapData:UninstallWallParts(row, col),这个方法会正确清除对应墙段的 pillarsInstalled 和 roofInstalled。
:
改动清单
CollisionMap.lua:
构造函数新增 self.wallStrips = {} 数组
Rebuild() 步骤 4 从网格阻挡改为门槛条:遍历 GetWallGroups() 按砖块跨度计算宽度,有柱子时加 PILLAR_VIS_W
新增 RebuildWallStrips() 方法(增量更新,添砖/装柱后调用)
IsAreaBlocked() 新增段落 C 检测 wallStrips
_IsInAnyStrip() 新增 wallStrips 循环(防 Q/E 旋转卡住)
删除旧的 MarkWallBlocked() 方法
main.lua:3 处 MarkWallBlocked() 调用全部替换为 RebuildWallStrips()
MapCanvas.lua:新增绿色调试可视化,渲染留言墙门槛条(与建筑的蓝色区分)
宽度计算逻辑
halfW = (maxBX - minBX + 1) * 16 / 2
如果 pillarsInstalled:
halfW += 48 * 0.32 (≈15.36)
1 砖 → halfW=8
3 砖 → halfW=24
6 砖 → halfW=48
6 砖+柱子 → halfW≈63
可以预览看看绿色门槛条的位置是否和砖墙对齐。
:
修改总结:
问题根因:掉落物的下落动画在内容坐标系中操作 Y 坐标(currentY = originY - FALL_HEIGHT),但在等距视角(isometric)下,内容坐标 Y 的偏移会被旋转+压缩变换成屏幕上的对角线方向移动,导致木头看起来是"从旁边飘进来"而不是"从上往下掉"。
修复方案:
分离内容坐标与视觉高度:新增 visualHeight 属性,独立于内容坐标
内容坐标只做水平插值:currentX/currentY 从树中心平滑移动到散落终点,不再包含竖直偏移
使用 renderFunc 施加屏幕空间偏移:在 counter-rotation + undo-ISO 变换之后,通过 yOff = -visualHeight 将精灵向上偏移。这样下落动画就是在屏幕空间中垂直方向的移动,视觉上就是"从树冠正上方往下掉"
弹跳也改为屏幕空间:落地后的小弹跳同样通过 visualHeight 实现
:
构建成功。完整流程总结:
保存并同步到游戏选角的完整流程
步骤 操作 背后发生了什么
1 编辑器里修改角色 画布上编辑像素
2 点击 “保存” CharStorage.Save() → 写入 characters/{id}.json
3 点击 “<- 返回” 回到游戏 进入主界面
4 角色选择弹窗自动弹出 buildRoster() 扫描 characters/*.json,你的角色出现在列表最前面,带缩略图
5 选角色 → 点"进入游戏" loadCharData() 从 sandbox 读取你修改后的角色数据
两种场景都已覆盖
全新自建角色:编辑器新建 → 保存 → 游戏选角弹窗自动出现(带动态生成的缩略图)
导入并修改内置角色(如"爸爸"):导入 → 修改 → 保存 → 游戏中选"爸爸"时会优先加载你修改后的版本(sandbox 优先于 assets)
不需要任何额外操作,编辑器的"保存"就是唯一入口。


