关于 optimize-scene
这是面向 Decentraland 场景开发者的性能优化技能,涵盖实体、三角形、纹理、系统和加载开销。它提供资源预算参考及可用于场景代码的优化模式。
主要功能
- 01按地块数量估算场景资源预算
- 02使用对象池和实体层级
- 03通过低多边形模型与 LOD 控制几何开销
- 04复用材质并优化纹理
- 05节流系统并预加载资源
适用场景
- 01减少场景中的实体和三角形数量
- 02优化低端设备上的场景表现
- 03降低大型模型加载时的弹出延迟
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dcl-regenesislabs/ opendcl --skill optimize-scene安装后在 Claude Code 中输入/optimize-scene确认已加载。
详细介绍
优化 Decentraland 场景
编写拆分
此技能中的模式——对象池、LOD、资产预加载、系统节流——都是运行时机制,应放在 src/index.ts 中。静态布局(椅子、墙壁、灯具以及应用 LOD 的道具)仍应在 main-entities.ts 中声明;优化代码会按名称(engine.getEntityOrNullByName)或组件查询(engine.getEntitiesWith(Transform))读取这些实体。
父级优化(包含许多子级的静态容器)最好在 main-entities.ts 中通过 Transform.parent: 'parent_name' 实现,这样编辑器就能将整个组作为一个整体移动。
场景限制(按地块数计算)
所有限制都随地块数 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 |
**文件限制:**每个地块 15 MB,总计最多 300 MB,每个地块最多 200 个文件,单个文件最大 50 MB。
优化实体数量
复用实体
// BAD: Creating new entity each time
function spawnBullet() {
const bullet = engine.addEntity() // Creates entity every call
// ...
}
// GOOD: Object pooling
const bulletPool: Entity[] = []
function getBullet(): Entity {
const existing = bulletPool.find(e => !ActiveBullet.has(e))
if (existing) return existing
const newBullet = engine.addEntity()
bulletPool.push(newBullet)
return newBullet
}
移除未使用的实体
engine.removeEntity(entity) // Frees the entity slot
使用父子层级
不要为每个子级分别设置变换,而应使用实体层级:
const parent = engine.addEntity()
Transform.create(parent, { position: Vector3.create(8, 0, 8) })
// Children inherit parent transform
const child1 = engine.addEntity()
Transform.create(child1, { position: Vector3.create(0, 1, 0), parent })
const child2 = engine.addEntity()
Transform.create(child2, { position: Vector3.create(1, 1, 0), parent })
优化三角形数量
使用低多边形模型
- 小型道具:100-500 个三角形
- 中型物体:500-1,500 个三角形
- 大型建筑:1,500-5,000 个三角形
- 主体物件:最多 10,000 个三角形
使用 LOD(细节层次)
在远处显示更简单的模型:
engine.addSystem(() => {
// Check distance to player and swap models
const playerPos = Transform.get(engine.PlayerEntity).position
const objPos = Transform.get(myEntity).position
const distance = Vector3.distance(playerPos, objPos)
const gltf = GltfContainer.getMutable(myEntity)
if (distance > 30) {
gltf.src = 'models/building_lod2.glb' // Low poly
} else if (distance > 15) {
gltf.src = 'models/building_lod1.glb' // Medium poly
} else {
gltf.src = 'models/building_lod0.glb' // High poly
}
})
使用基本图形代替模型
对于简单形状,MeshRenderer 比加载 .glb 更轻量:
MeshRenderer.setBox(entity) // Very cheap
MeshRenderer.setSphere(entity) // Cheap
MeshRenderer.setPlane(entity) // Very cheap
优化纹理
- 尺寸必须是 2 的幂次方:256、512、1024、2048
- 推荐尺寸:大多数物体使用 512x512,主体物件最大使用 1024x1024
- UI/精灵图需要透明度时使用
.png - 照片和不需要透明度的纹理使用
.jpg - 尽可能优先使用压缩格式(WebP),而非原始 PNG
- 使用纹理图集(将多张纹理合并到一张图像中)来减少绘制调用和材质数量
- 在多个材质间共享纹理引用——不要重复纹理文件
- 在多个实体间复用材质:
// GOOD: Define material once, apply to many
Material.setPbrMaterial(entity1, { texture: Material.Texture.Common({ src: 'images/wall.jpg' }) })
Material.setPbrMaterial(entity2, { texture: Material.Texture.Common({ src: 'images/wall.jpg' }) })
// Same texture URL = shared in memory
优化系统
避免逐帧分配
// BAD: Creates new Vector3 every frame
engine.addSystem(() => {
const target = Vector3.create(8, 1, 8) // Allocation!
})
// GOOD: Reuse constants
const TARGET = Vector3.create(8, 1, 8)
engine.addSystem(() => {
// Use TARGET
})
限制高开销操作的频率
let lastCheck = 0
engine.addSystem((dt) => {
lastCheck += dt
if (lastCheck < 0.5) return // Only run every 0.5 seconds
lastCheck = 0
// Expensive operation here
})
不需要时移除系统
const systemFn = (dt: number) => { /* ... */ }
engine.addSystem(systemFn)
// When no longer needed:
engine.removeSystem(systemFn)
资产预加载(AssetLoad 组件)
对于可能导致明显弹入效果的大型资产,可使用 AssetLoad 在渲染前预先下载:
import { engine, AssetLoad, LoadingState, GltfContainer, Transform } from '@dcl/sdk/ecs'
import { Vector3 } from '@dcl/sdk/math'
// Create a preload entity at scene startup
const preloadEntity = engine.addEntity()
AssetLoad.create(preloadEntity, { src: 'models/large-model.glb' })
// System to track loading progress
function assetLoadingSystem(dt: number) {
for (const [entity] of engine.getEntitiesWith(AssetLoad)) {
const state = AssetLoad.get(entity)
if (state.loadingState === LoadingState.FINISHED) {
// Asset is cached — now safe to create the visible entity
GltfContainer.create(entity, { src: 'models/large-model.glb' })
Transform.create(entity, { position: Vector3.create(8, 0, 8) })
AssetLoad.deleteFrom(entity) // Remove preload component
}
}
}
engine.addSystem(assetLoadingSystem)
对于任何超过约 1 MB 的模型,或应在游戏阶段开始前准备就绪的资产,都可使用此模式。
优化加载时间
- 延迟加载 3D 模型(按需加载,而不是在场景启动时全部加载)
- 使用经过压缩的 .glb 文件(Draco 压缩)
- 尽量减小资产总大小
- 尽可能为大型共享资产使用 CDN URL
- 使用
AssetLoad预加载关键资产,延后加载非必要资产
常见性能陷阱
- 系统过多:每个系统都会在每一帧运行。合并相关逻辑。
- 不必要的组件查询:如果实体集合没有变化,请缓存
engine.getEntitiesWith()的结果。 - GLTF 文件过大:导出前在 Blender 中进行优化(简化模型、移除隐藏面)。
- 未压缩的音频:音乐请使用 .mp3 而非 .wav(文件小 10 倍)。
- 连续射线检测:除非需要逐帧进行射线检测,否则请设置
continuous: false。 - 文本渲染:
TextShape的开销较大。对于不需要位于 3D 空间中的文本,请使用Label(UI)。
交叉引用
- add-3d-models — 模型加载、碰撞体和文件组织
- game-design — 性能预算、设计模式和 MVP 规划
- advanced-rendering — 纹理模式、材质复用,以及使用 VisibilityComponent 实现 LOD