为什么值得单独做成系统
社交小游戏里,玩家布置自己的小屋:把沙发贴到墙边,旋转地毯,叠放盆栽和矮桌,保存后邀请朋友访问。这个体验看起来轻松,但编辑器一旦不好用,玩家会很快放弃创作。
房间装饰不是自由拖 Sprite。家具有占地、层级、墙面挂件、地面物件、碰撞、吸附、撤销、保存和分享码。编辑器要让玩家自由,同时保护布局数据不坏。 本文会按一个可上线的小系统来拆,不追求炫技,而是把数据结构、状态流、玩家反馈、调试工具和发布检查说清楚。Phaser 的优势是让画面和交互快速成型,但越是快速,越需要把规则层和表现层分开。
核心架构
flowchart TD
N1["FurnitureCatalog"] --> N2["EditorSelection"]
N2["EditorSelection"] --> N3["SnapGrid"]
N3["SnapGrid"] --> N4["CollisionRule"]
N4["CollisionRule"] --> N5["LayerRule"]
N5["LayerRule"] --> N6["FurniturePreview"]
N6["FurniturePreview"] --> N7["UndoRedoStack"]
N7["UndoRedoStack"] --> N8["RoomSaveData"]
这张图的重点是单向流动。FurnitureCatalog、EditorSelection、SnapGrid、LayerRule、CollisionRule、UndoRedoStack、RoomSaveData 不应该互相随意读写。输入或场景事件进入模型,模型输出快照或事件,Phaser 表现层再根据结果更新 Sprite、Graphics、Sound 和 UI。只要这条边界稳定,后续加内容、加难度、加存档或加多人同步,都不会把系统推倒重写。
家具目录要区分类型
地毯、桌子、墙画、灯、窗帘和角色摆件的规则不同。FurnitureCatalog 应记录类别、尺寸、可旋转、可叠放、是否贴墙、默认层级、占用形状和缩略图。编辑器只读取目录,不把规则写死在拖拽回调里。
实现时建议先用最简单的调试图形验证规则,再接正式美术。比如先画出区域、方向、时间轴、占用格或检测范围,确认数据正确后再添加粒子、镜头、音效和过渡动画。这样做不花哨,但能避免很多“看起来对,规则其实错”的问题。
吸附要兼顾精确和自由
格子吸附适合家具对齐,但太硬会限制装饰感。可以提供半格吸附、按住快捷键临时关闭吸附、靠近墙边自动贴齐。移动端没有键盘,可以用磁吸强度和按钮切换。吸附结果要稳定,不能因为手指轻微抖动让家具跳来跳去。
实现时建议先用最简单的调试图形验证规则,再接正式美术。比如先画出区域、方向、时间轴、占用格或检测范围,确认数据正确后再添加粒子、镜头、音效和过渡动画。这样做不花哨,但能避免很多“看起来对,规则其实错”的问题。
层级规则防止穿帮
地毯在桌子下,墙画在墙面层,吊灯在顶部层。LayerRule 根据家具类型和 y 坐标决定 depth。允许玩家微调前后顺序,但不能让墙画跑到地板上。规则边界明确后,房间看起来才不会乱。
实现时建议先用最简单的调试图形验证规则,再接正式美术。比如先画出区域、方向、时间轴、占用格或检测范围,确认数据正确后再添加粒子、镜头、音效和过渡动画。这样做不花哨,但能避免很多“看起来对,规则其实错”的问题。
碰撞要服务访问体验
装饰编辑时玩家可能想把家具摆得很密,但访问房间时角色仍要能走。CollisionRule 可以把家具分为阻挡、半阻挡和纯装饰。保存前运行可达性检查,确保入口到关键交互点仍可走。若不可达,提示具体被哪组家具堵住。
实现时建议先用最简单的调试图形验证规则,再接正式美术。比如先画出区域、方向、时间轴、占用格或检测范围,确认数据正确后再添加粒子、镜头、音效和过渡动画。这样做不花哨,但能避免很多“看起来对,规则其实错”的问题。
撤销要记录操作意图
拖动、旋转、删除、批量移动、换皮肤都应进 UndoRedoStack。拖动过程中不要每一帧都记录,按下时记录 before,松手时记录 after。这样撤销一次就是一次玩家意图,而不是倒退几十个像素。
实现时建议先用最简单的调试图形验证规则,再接正式美术。比如先画出区域、方向、时间轴、占用格或检测范围,确认数据正确后再添加粒子、镜头、音效和过渡动画。这样做不花哨,但能避免很多“看起来对,规则其实错”的问题。
保存数据要稳定
RoomSaveData 保存 furnitureId、catalogId、x、y、rotation、variant、layerOffset 和自定义颜色。不要保存 Sprite 内部状态。读取时根据目录重建对象。目录更新后,旧房间也能尽量迁移。若某家具下架,保留占位或替代物,不要直接丢失玩家布置。
实现时建议先用最简单的调试图形验证规则,再接正式美术。比如先画出区域、方向、时间轴、占用格或检测范围,确认数据正确后再添加粒子、镜头、音效和过渡动画。这样做不花哨,但能避免很多“看起来对,规则其实错”的问题。
分享前要做体积控制
房间分享码或云存档需要控制大小。可以按家具列表压缩字段名,或只保存和默认值不同的字段。截图分享则另走渲染流程。不要为了分享方便把整张房间截图当存档,那样无法编辑,也占空间。
实现时建议先用最简单的调试图形验证规则,再接正式美术。比如先画出区域、方向、时间轴、占用格或检测范围,确认数据正确后再添加粒子、镜头、音效和过渡动画。这样做不花哨,但能避免很多“看起来对,规则其实错”的问题。
TypeScript 实现骨架
interface FurnitureItem { id: string; catalogId: string; x: number; y: number; rotation: number; layerOffset: number }
interface FurnitureDef { id: string; width: number; height: number; wallOnly?: boolean; blocking?: boolean }
function snap(value: number, grid = 16) { return Math.round(value / grid) * grid; }
function moveFurniture(item: FurnitureItem, x: number, y: number, grid = 16) {
return { ...item, x: snap(x, grid), y: snap(y, grid) };
}
class UndoRedoStack<T> {
private undo: T[] = [];
private redo: T[] = [];
push(snapshot: T) { this.undo.push(snapshot); this.redo.length = 0; }
undoTo(current: T) {
const prev = this.undo.pop();
if (prev) this.redo.push(current);
return prev ?? current;
}
}
function serializeRoom(items: FurnitureItem[]) {
return JSON.stringify(items.map(i => [i.catalogId, i.x, i.y, i.rotation, i.layerOffset]));
}
这段代码只展示核心边界,不是完整项目代码。真实项目里还需要补配置加载、错误码、事件派发、对象池、性能采样和测试。关键是让核心规则能独立运行,Phaser 层只是把规则结果变成玩家能感知的反馈。
落地步骤
- 第一,先把 FurnitureCatalog 和 EditorSelection 写成普通 TypeScript 模型。不要让它们依赖 Phaser Scene、Sprite 或 Camera。核心模型越普通,越容易写测试、做编辑器预览和复现玩家问题。
- 第二,Phaser 层只做适配:接收输入、播放动画、更新图形、触发音效。它可以很薄,但必须清楚。只要某段规则开始读取 Sprite 的 visible、alpha 或动画状态,就说明边界正在变脏。
- 第三,给 SnapGrid 或同等复杂的中间结果做调试显示。开发模式里能看到状态、阈值、候选对象、失败原因和耗时,后续调参才不会靠猜。
- 第四,准备三组测试夹具:正常流程、边界流程、错误配置。正常流程验证体验,边界流程验证稳定性,错误配置验证系统会报出人能看懂的问题。
检查清单
- 确认 FurnitureCatalog 的状态可以序列化,能写入存档或调试日志。
- 确认 EditorSelection 的配置有默认值、版本号和校验错误。
- 确认快速点击、暂停、切后台、读档和切场景不会重复提交关键事件。
- 确认失败反馈足够具体,玩家能知道是条件不足、输入中断、资源不够还是规则禁止。
- 确认低端机有降级策略,尤其是粒子、音效、动态对象和调试图层。
- 确认开发模式可以导出最近关键事件,方便复现玩家反馈。
常见误区
第一类误区,是把表现当成事实。动画播完、按钮亮着、Sprite 存在,都只能说明表现层当前长什么样,不能说明规则已经完成。规则事实应该存在于模型和事件里。
第二类误区,是只为了第一个关卡写逻辑。第一个关卡对象少、输入慢、节奏简单,临时判断很难暴露问题。等内容增加,重复触发、配置错误和性能峰值会一起出现,早期的边界会决定后期成本。
第三类误区,是没有设计失败路径。复杂系统一定会遇到失败,好的失败路径会告诉玩家和开发者发生了什么;坏的失败路径只会留下一句操作失败,甚至什么都不显示。
发布前验证
发布前至少跑一次规则级测试和一次运行时冒烟。规则级测试不需要启动浏览器,直接喂数据,断言状态和事件。运行时冒烟则在 Phaser 场景里验证输入、反馈、暂停、重开和边界情况。若系统涉及经济、存档或排行榜,还要记录 requestId 或事件 id,保证重复提交不会造成重复奖励或重复扣费。
额外实践建议
- 拖拽过程中只预览,松手时才提交一次撤销记录。
- 家具目录要有版本号,方便下架和迁移。
- 保存前做可达性检查,避免玩家把入口堵死。
运行时观测与调参
装饰编辑器要观察玩家的创作摩擦。记录每次拖拽是否提交、撤销次数、最常见的碰撞失败原因、家具旋转次数、保存前可达性失败位置,以及玩家是否在移动端频繁误触删除。若撤销次数很高,可能是吸附太强、层级不符合直觉或操作按钮太近。若很多房间保存前堵住入口,说明碰撞提示出现得太晚。还可以记录热门家具组合,帮助后续做套装推荐。编辑器不是一次性功能,它会随着家具目录增长越来越复杂,早期的观测数据能避免后期靠猜重做交互。
移动端还要单独看长按、拖拽和滚动画面的冲突。玩家想移动房间视角时不应误选家具,想拖家具时不应触发页面滚动。可以设置编辑锁定模式、拖拽延迟和选中描边,并记录误取消频率。装饰玩法越休闲,越不能让基础操作制造压力。
最后,保存成功后的反馈要明确。玩家布置了十几分钟,点击保存后需要看到版本号、时间或同步状态,而不是一个转瞬即逝的小提示。
结语
房间装饰编辑器:家具吸附、层级、保存和撤销 的关键不是某个 API,而是把可解释的规则交给模型,把可感知的反馈交给 Phaser。只要这条线清楚,项目就能持续扩展;如果所有逻辑都塞进 Scene 回调,第一版越快,后面的维护压力越大。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。