关于 game-design
game-design 是面向 Decentraland 场景与游戏开发者的设计和优化技能,涵盖共享 3D 世界中的玩法约束、资源预算、性能和交互设计。它还提供 MVP 规划建议,并指向相关实现技能。
主要功能
- 01Decentraland 共享场景设计原则
- 02按地块数估算场景资源预算
- 03纹理与资源预加载建议
- 04对象复用、LOD 与绘制调用优化
- 05输入、状态和界面设计指导
- 06核心循环与 MVP 测试规划
适用场景
- 01规划适应玩家随时进入或离开的玩法
- 02估算目标地块规模下的场景资源
- 03减少场景的绘制调用与物理负担
- 04制定原型、测试和迭代计划
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ dcl-regenesislabs/ opendcl --skill game-design安装后在 Claude Code 中输入/game-design确认已加载。
详细介绍
Decentraland 游戏设计与场景优化
1. DCL 游戏设计理念
Decentraland 是一个持续运行的共享 3D 世界。设计时请考虑以下限制:
- 没有启动画面:场景始终处于运行状态。玩家会从相邻地块走进来——没有闪屏,也没有“按下开始”。玩家到达的瞬间,你的场景就必须有意义。
- 没有强制结束:你无法强制进入“游戏结束”状态。玩家可以随时走开或传送离开。设计能够自然适应玩家随时加入或离开的循环。
- 无法移除玩家:没有 API 可以将玩家逐出场景。你可以传送玩家,但必须征得他们同意(他们必须接受提示)。面对行为不当的玩家时,应通过游戏机制而非驱逐来处理。
- 注意场景边界:站在地块外的玩家可以看到地块内部。你的场景始终展示在外。相邻场景也清晰可见——请考虑视觉协调性。
- 共享空间:始终可能有多名玩家同时在场。即使是“单人”谜题,其他玩家也能看到。请善加利用,或将此纳入设计考虑。
2. 场景限制公式
所有限制都随地块数量 n 按比例变化。除部署时固定的 MB 文件大小限制外,其他限制都可以超出——场景不会崩溃,但性能会下降,在低端设备上甚至可能无法使用。请将下表作为参考,而非强制标准。
| 资源 | 公式 | 1 个地块 | 2 个地块 | 4 个地块 | 9 个地块 | 16 个地块 |
|---|---|---|---|---|---|---|
| 三角形 | n x 10,000 | 10,000 | 20,000 | 40,000 | 90,000 | 160,000 |
| 实体 | n x 200 | 200 | 400 | 800 | 1,800 | 3,200 |
| 物理体 | n x 300 | 300 | 600 | 1,200 | 2,700 | 4,800 |
| 材质 | log2(n+1) x 20 | 20 | 31 | 46 | 66 | 81 |
| 纹理 | log2(n+1) x 10 | 10 | 15 | 23 | 33 | 40 |
| 高度限制 | log2(n+1) x 20m | 20m | 31m | 46m | 66m | 81m |
| 绘制调用 | n x 300(目标值) | 300 | 600 | 1,200 | 2,700 | 4,800 |
**文件限制:**每个地块 15 MB,总计最多 300 MB,每个地块 200 个文件,单个文件最大 50 MB。
3. 纹理要求
- 尺寸必须为 2 的幂次方:256、512、1024、2048
- 建议尺寸:场景物体使用 1024x1024,可穿戴物品使用 512x512
- 使用纹理图集将多个小纹理合并为一个,以减少绘制调用和材质数量
- 尽可能优先使用压缩格式(WebP),而非原始 PNG
- 在材质之间共享纹理引用——不要重复纹理文件
4. 资源预加载(AssetLoad 组件)
对于会造成明显渐显效果的大型资源,可使用 AssetLoad 在渲染前预下载:
import { engine, AssetLoad, LoadingState, GltfContainer, Transform } from '@dcl/sdk/ecs'
import { Vector3 } from '@dcl/sdk/math'
const preloadEntity = engine.addEntity()
AssetLoad.create(preloadEntity, { src: 'models/large-model.glb' })
function assetLoadingSystem(dt: number) {
for (const [entity] of engine.getEntitiesWith(AssetLoad)) {
const state = AssetLoad.get(entity)
if (state.loadingState === LoadingState.FINISHED) {
GltfContainer.create(entity, { src: 'models/large-model.glb' })
Transform.create(entity, { position: Vector3.create(8, 0, 8) })
AssetLoad.deleteFrom(entity)
}
}
}
engine.addSystem(assetLoadingSystem)
对于任何大于约 1 MB 的模型,或应在游戏阶段开始前就绪的资源,都应使用此模式。
5. 性能优化模式
对象池
重复使用实体,而不是创建后再销毁:
const pool: Entity[] = []
function getFromPool(): Entity {
const existing = pool.pop()
if (existing) return existing
return engine.addEntity()
}
function returnToPool(entity: Entity) {
Transform.getMutable(entity).position = Vector3.create(0, -100, 0)
pool.push(entity)
}
LOD(细节层次)
根据玩家的距离切换模型或隐藏实体:
function lodSystem() {
const playerPos = Transform.get(engine.PlayerEntity).position
for (const [entity, transform] of engine.getEntitiesWith(Transform, GltfContainer)) {
const distance = Vector3.distance(playerPos, transform.position)
VisibilityComponent.createOrReplace(entity, { visible: distance <= 30 })
}
}
engine.addSystem(lodSystem)
减少绘制调用
- 在导出前于 Blender 中合并网格
- 使用纹理图集(多个物体共用一种材质)
- 限制不同材质的数量——在多个实体之间重复使用
- 尽可能避免透明效果(透明物体会额外增加绘制调用)
系统优化
- 不要每帧都运行繁重逻辑。请使用计时器:
let timer = 0 function heavySystem(dt: number) { timer += dt if (timer < 0.5) return // Run every 500ms, not every frame timer = 0 // ... expensive work here } - 尽量减少
engine.getEntitiesWith()查询——实体集合稳定时缓存查询结果 - 避免在每帧运行的系统中分配新对象(Vector3.create、数组)
禁用未使用的碰撞体
移除玩家永远不会与之交互的装饰物体上的碰撞网格。这可以显著减少物理体数量。
6. 输入系统设计
| 输入 | 操作 | 备注 |
|---|---|---|
| E key | 主要操作(IA_PRIMARY) | 主要交互 |
| F key | 次要操作(IA_SECONDARY) | 备用交互 |
| Pointer click | IA_POINTER | 鼠标左键单击 / 点按 |
| Keys 1-4 | IA_ACTION_3 至 IA_ACTION_6 | 操作栏槽位 |
设计注意事项
- 不能使用鼠标滚轮作为输入
- 始终兼顾 桌面端和移动端。移动端没有键盘,因此应依赖指针和屏幕按钮
- 为指针事件设置
maxDistance(通常为 8-10 米),以防玩家从场景另一端进行交互 - 使用
hoverText,在玩家确认交互之前说明交互的作用
7. 状态管理模式
模块级状态(简单游戏)
// game-state.ts
export let score = 0
export let gamePhase: 'waiting' | 'playing' | 'ended' = 'waiting'
export function addScore(points: number) { score += points }
基于组件的状态(复杂游戏)
使用自定义组件作为结构化数据容器:
import { engine, Schemas } from '@dcl/sdk/ecs'
const EnemyState = engine.defineComponent('EnemyState', {
health: Schemas.Number,
speed: Schemas.Number,
target: Schemas.Entity
})
状态机
将游戏阶段建模为具有明确转换规则的显式状态:
type GameState = 'lobby' | 'countdown' | 'active' | 'cooldown'
let currentState: GameState = 'lobby'
function gameStateSystem(dt: number) {
switch (currentState) {
case 'lobby': handleLobby(dt); break
case 'countdown': handleCountdown(dt); break
case 'active': handleActive(dt); break
case 'cooldown': handleCooldown(dt); break
}
}
8. 用户体验 / UI 指南
- 保持 UI 简洁:元宇宙注重 3D 临场感,而非 2D 覆盖层。避免使用遮挡世界的大型 HUD。
- 优先使用空间 UI:如果信息与某个地点或对象相关,优先使用实体上的
TextShape和 3D 标牌,而非屏幕空间 UI。 - 清晰的可操作提示:交互对象应让人一看便知可以交互。使用发光效果、轮廓、浮动指示器或细微动画,提示“你可以点击这里”。
- 声音反馈:玩家的每个重要操作都应产生音频反馈。这能确认操作已生效,也能提升体验质感。
- 渐进式呈现:不要一次性展示所有信息。随着玩家参与,逐步揭示玩法机制和故事。从简单开始,逐步增加复杂度。
- 即时反馈:玩家交互时,应在同一帧内作出响应。使用补间动画、声音或 UI 弹窗,让玩家明确知道“操作是否生效”。
- 无障碍性:使用高对比度文本、易读的字号(屏幕 UI 的
fontSize >= 16),并搭配音频提示。
9. MVP 规划
从核心循环开始
思考:玩家要做什么? 答案应当是一个简单句:
- “玩家探索房间并寻找隐藏物品。”
- “玩家与其他玩家在障碍赛道上竞速。”
- “玩家收集资源并建造结构。”
快速制作原型
- 先在 1-2 个地块中搭建,即使最终场景会更大
- 使用基本形状(方块、球体)作为占位符——不要等待最终美术资源
- 先让核心循环正常运行,再添加任何次要功能
尽早测试
- 部署到测试世界,亲自走一遍
- 邀请 2-3 名真实玩家并观察他们(不要解释游戏,看看他们能否自行理解)
- 衡量:玩家能否在 30 秒内明白该做什么?
迭代打磨乐趣
- 最后再打磨。如果使用占位美术资源时核心循环都不好玩,更好的美术也无法解决问题
- 果断删减功能。紧凑的小型体验胜过庞大却未完成的体验
- 在 DCL 中,可重玩性比内容量更重要(玩家会回到自己喜欢的场景)
MVP 检查清单
- 核心循环可在 60 秒内上手游玩
- 支持 1 名玩家游玩,也支持 5 名以上玩家同时游玩
- 符合目标地块数量对应的场景限制
- 所有交互都有清晰的视觉 / 音频反馈
- 玩家无需外部说明即可理解目标
从零开始? 请先查看 create-scene 技能,在设计游戏之前搭建项目框架。
10. 交叉引用
| 主题 | 技能 | 适用场景 |
|---|---|---|
| 交互性、输入处理、射线投射 | add-interactivity | 实现点击处理器、触发器、输入 |
| 多人同步、服务器通信 | multiplayer-sync | 网络游戏状态、实时同步 |
| 屏幕 UI、React-ECS、HUD 元素 | build-ui | 构建菜单、记分板、对话框 |
| 性能优化、实体/三角形预算 | optimize-scene | 详细的优化技巧 |
此技能重点关注影响实现方案的设计决策和优化约束。有关详细的代码模式,请参阅引用的技能。
评论
支持 Markdown