mcpField
G
godot-genre-sandbox

godot-genre-sandbox

github.com/thedivergentai开发907264 次安装2 周前更新
代码仓库

关于 godot-genre-sandbox

面向 Godot 开发者的沙盒技能,涵盖可编辑体素世界与元胞自动机模拟。内容包括区块渲染、基于材质属性的交互、放置验证和压缩存档。

主要功能

  1. 01区块式体素世界架构
  2. 02可见面网格生成
  3. 03基于材质属性的交互
  4. 04元胞自动机液体模拟
  5. 05RLE 与二进制世界存档
  6. 06异步加载与浮动原点模式

适用场景

  1. 01构建可编辑的 3D 体素地形
  2. 02实现落沙式元素交互
  3. 03保存大型区块世界
  4. 04验证多人游戏中的玩家放置操作

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/thedivergentai/game-dev-agentic-skills --skill godot-genre-sandbox

安装后在 Claude Code 中输入/godot-genre-sandbox确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
elite-technical-patterns.md
migration-notes.md
scripts/
blueprint_manager.gd
cellular_automata_liquid.gd
dynamic_placement_validator.gd
sandbox_patterns.gd
sandbox_world_serializer.gd
voxel_chunk_manager.gd
voxel_chunk_mesher.gd
voxel_world.gd

详细介绍

绝对不要做(专家级反模式)

性能与可扩展性
  • 绝对不要为每个方块都使用单独的 RigidBody 节点;世界中严格使用静态碰撞体,仅为动态道具保留物理模拟。
  • 绝对不要每帧模拟整个世界;严格处理有活动变化的 “脏”区块。休眠区块必须完全不占用 CPU。
  • 绝对不要每帧更新 MultiMesh 缓冲区;严格批量处理更改,仅在修改完成时(例如玩家停止绘制后)重建缓冲区。
  • 绝对不要为每个网格单元都使用标准 Godot Nodes;严格使用 PackedInt32Arrays 或类型化 Dictionary,将 RAM 开销降至最低。
  • 绝对不要针对每个单独的体素进行放置射线检测;严格使用网格量化(floor(pos/size))直接以 O(1) 计算单元格。
  • 绝对不要渲染区块中的每个方块面;严格生成 ArrayMesh,只将可见的外表面提交给 GPU(剔除/贪心网格划分)。
数据与持久化
  • 绝对不要保存每个方块变换的原始数组;严格使用**游程编码(RLE)**压缩均匀区域(例如,“空气 x 50,000”)。
  • 绝对不要同步加载大型地形区块;严格使用 ResourceLoader.load_threaded_request(),避免帧卡顿。
  • 绝对不要将标准文本 .tscn 文件用于体素数据集;严格使用二进制 .res 文件,解析速度快 10 倍。
  • 绝对不要忽略浮点精度限制(32,768 个单位);大型世界必须严格实现浮动原点偏移。
系统与架构
  • 绝对不要硬编码元素交互(if water and fire);严格使用属性系统,让交互由材质属性(易燃性、密度)决定。
  • 绝对不要在多人游戏中信任客户端的放置操作;严格要求服务器验证边界和资源。
  • 绝对不要从后台生成线程操作 SceneTree;必须使用 call_deferred() 或 Mutex 锁来确保安全。
  • 绝对不要让孤立区块留在内存中;严格跟踪已加载区域,并对丢弃的分支调用 queue_free()。

🛠 专家组件(scripts/)

MANDATORY / Do NOT Load by path

  • 仅限 2D 落沙/CA:加载 cellular_automata_liquid.gd + 下方的属性/工具模式。不要加载 voxel_chunk_*.gd、voxel_world.gd 或贪心网格路径。
  • 3D 体素/区块世界:加载 voxel_world.gd → voxel_chunk_manager.gd → MANDATORY voxel_chunk_mesher.gd,用于生成外表面网格。除非同时运行 2D 元素层,否则不要加载 2D CA 液体。
  • 放置/多人游戏验证:在信任客户端挖掘/放置操作前,加载 dynamic_placement_validator.gd。
  • 持久化:加载 sandbox_world_serializer.gd 以处理 RLE/二进制区块 IO;保留 sandbox_patterns.gd 以支持异步加载 + 浮动原点。
区块/体素(3D)
  • voxel_world.gd — 顶层世界控制器,负责网格状态、基于工具的编辑和区块生命周期。
  • voxel_chunk_manager.gd — 区块生命周期 + 面向中型世界的 MultiMeshInstance3D 批量更新。
  • voxel_chunk_mesher.gd — 大型世界的 MANDATORY 组件:使用 WorkerThreadPool 构建可见表面 ArrayMesh,并通过延迟调用 set_mesh。
元素/工具(2D CA)
放置/保存/实用工具

架构模式

1. 元素系统(基于属性的涌现)

对材料属性建模,而不是对行为建模。交互由重叠的属性自然涌现。

# element_data.gd
class_name ElementData extends Resource

enum Type { SOLID, LIQUID, GAS, POWDER }
@export var id: String = "air"
@export var type: Type = Type.GAS
@export var density: float = 0.0      # For liquid flow direction
@export var flammable: float = 0.0    # 0-1: Chance to ignite
@export var ignition_temp: float = 400.0
@export var conductivity: float = 0.0  # For electricity/heat
@export var hardness: float = 1.0     # Mining time multiplier

# EDGE CASE: What if two elements have same density but different types?
# SOLUTION: Use secondary sort (type enum priority: SOLID > LIQUID > POWDER > GAS)
func should_swap_with(other: ElementData) -> bool:
    if density == other.density:
        return type > other.type  # Enum comparison: SOLID(0) > GAS(3)
    return density > other.density
2. 元胞自动机网格(落沙模拟)

更新顺序很重要。自上而下更新可防止 godot-particles“瞬移”。

# world_grid.gd
var grid: Dictionary = {}  # Vector2i -> ElementData
var dirty_cells: Array[Vector2i] = []

func _physics_process(_delta: float) -> void:
    # CRITICAL: Sort top-to-bottom to prevent double-moves
    dirty_cells.sort_custom(func(a, b): return a.y < b.y)
    
    for pos in dirty_cells:
        simulate_cell(pos)
    dirty_cells.clear()

func simulate_cell(pos: Vector2i) -> void:
    var cell = grid.get(pos)
    if not cell: return
    
    match cell.type:
        ElementData.Type.LIQUID, ElementData.Type.POWDER:
            # Try down, then down-left, then down-right
            var targets = [pos + Vector2i.DOWN, 
                           pos + Vector2i(- 1, 1), 
                           pos + Vector2i(1, 1)]
            for target in targets:
                var neighbor = grid.get(target)
                if neighbor and cell.should_swap_with(neighbor):
                    swap_cells(pos, target)
                    mark_dirty(target)
                    return
        
        ElementData.Type.GAS:
            # Gases rise (inverse of liquids)
            var targets = [pos + Vector2i.UP,
                           pos + Vector2i(-1, -1),
                           pos + Vector2i(1, -1)]
            # Same swap logic...

# EDGE CASE: What if multiple godot-particles want to move into same cell?
# SOLUTION: Only mark target dirty, don't double-swap. Next frame resolves conflicts.
3. 工具系统(策略模式)

将输入与世界修改解耦。

# tool_base.gd
class_name Tool extends Resource
func use(world_pos: Vector2, world: WorldGrid) -> void: pass

# tool_brush.gd
extends Tool
@export var element: ElementData
@export var radius: int = 1

func use(world_pos: Vector2, world: WorldGrid) -> void:
    var grid_pos = Vector2i(floor(world_pos.x), floor(world_pos.y))
    
    # Circle brush pattern
    for x in range(-radius, radius + 1):
        for y in range(-radius, radius + 1):
            if x*x + y*y <= radius*radius:  # Circle boundary
                var target = grid_pos + Vector2i(x, y)
                world.set_cell(target, element)

# FALLBACK: If element placement fails (e.g., occupied by indestructible block)?
# Check world.can_place(target) before set_cell(), show visual feedback.
4. 基于区块的渲染(3D 体素)——MultiMesh 与 ArrayMesh

必须:对于外表面/贪心式区块网格,请阅读并改造 voxel_chunk_mesher.gd(WorkerThreadPool + SurfaceTool + call_deferred("set_mesh"))。不要在项目代码中内联不完整的网格生成器桩代码。

世界规模渲染路径负载
小型(<100k 个方块)单个 MeshInstance3D + SurfaceTool仅网格生成器模式
中型(100k–1M)分块的 MultiMeshInstance3D(一个网格,多个实例;编辑完成时批量更新缓冲区)必须 voxel_chunk_manager.gd
大型(>1M)/可编辑地形分块的 ArrayMesh,使用可见面/贪心四边形 + LOD;可选 RenderingServer 实例 RID必须 voxel_chunk_mesher.gd + 管理器

规则:当所有实例共享一个网格,且只需要实例级变换/颜色时,优先使用 MultiMesh。当相邻体素必须合并为独特表面(贪心面、UV 图集、每区块碰撞体)时,优先使用 ArrayMesh 网格生成。

沙盒世界的保存系统

# chunk_save_data.gd
class_name ChunkSaveData extends Resource

@export var chunk_coord: Vector2i
@export var rle_data: PackedInt32Array  # [type_id, count, type_id, count...]

# EXPERT TECHNIQUE: Run-Length Encoding
static func encode_chunk(grid: Dictionary, chunk_pos: Vector2i, chunk_size: int) -> ChunkSaveData:
    var data = ChunkSaveData.new()
    data.chunk_coord = chunk_pos
    
    var run_type: int = -1
    var run_count: int = 0
    
    for y in range(chunk_size):
        for x in range(chunk_size):
            var world_pos = chunk_pos * chunk_size + Vector2i(x, y)
            var cell = grid.get(world_pos)
            var type_id = cell.id if cell else 0  # 0 = air
            
            if type_id == run_type:
                run_count += 1
            else:
                if run_count > 0:
                    data.rle_data.append(run_type)
                    data.rle_data.append(run_count)
                run_type = type_id
                run_count = 1
    
    # Flush final run
    if run_count > 0:
        data.rle_data.append(run_type)
        data.rle_data.append(run_count)
    
    return data

# COMPRESSION RESULT: Empty chunk (16×16 = 256 blocks of air)
# Without RLE: 256 integers = 1024 bytes
# With RLE: [0, 256] = 8 bytes (128x compression!)

玩家创作的物理关节

# joint_tool.gd
func create_hinge(body_a: RigidBody2D, body_b: RigidBody2D, anchor: Vector2) -> void:
    var joint = PinJoint2D.new()
    joint.global_position = anchor
    joint.node_a = body_a.get_path()
    joint.node_b = body_b.get_path()
    joint.softness = 0.5  # Allows slight flex
    add_child(joint)
    
    # EDGE CASE: What if bodies are deleted while joint exists?
    # Joint will auto-break in Godot 4.x, but orphaned Node leaks memory.
# SOLUTION:
    body_a.tree_exiting.connect(func(): joint.queue_free())
    body_b.tree_exiting.connect(func(): joint.queue_free())

# FALLBACK: Player attaches joint to static geometry?
# Check `body.freeze == false` before creating joint.

Godot 专家专属说明

  • MultiMeshInstance3D.multimesh.instance_count:必须在分配缓冲区之前设置。无法动态扩容——需要重新创建。
  • RigidBody2D.sleeping:物体静止 2 秒后会自动休眠。使用 apply_central_impulse(Vector2.ZERO) 强制唤醒,且不会施加额外作用力。
  • GridMap 与 MultiMesh:GridMap 使用 MeshLibrary(适合多样化内容),MultiMesh 使用单个网格(适合高性能)。两者可以结合使用:用 GridMap 构建结构,用 MultiMesh 渲染地形。
  • 连续 CD:continuous_cd 要求使用凸碰撞形状。投射物请使用 CapsuleShape2D,而非 RectangleShape2D。

🚀 精英级技术实现(批次 09)

1. 贪心/可见面网格生成

不要粘贴占位用的网格生成器。必须阅读 voxel_chunk_mesher.gd,了解多线程可见面生成。将该模式扩展为完整的贪心四边形合并;关于 MultiMesh 与 ArrayMesh 的选择,请参阅上文第 4 节。极限绘制路径可以通过 RenderingServer 推送已提交的数组(参见官方文档 → 使用服务器),前提是网格生成器已取得表面数据的所有权。

2. VoxelGI(降级——使用文档和光照技能)

此处不通过不完整的 RenderingServer.voxel_gi_allocate_data 桩代码处理沙盒区块光照。对于程序化体积的动态 GI,请遵循 使用 VoxelGI,并将实现细节交由 godot-3d-lighting 处理。除非确实需要运行时 VoxelGI,否则优先采用该技能中的烘焙/探针策略。

3. 蓝图分享(Base64/JSON 序列化)

允许玩家通过简单字符串分享创作。使用 JSON 进行可读序列化,并使用 DisplayServer 集成剪贴板。

class_name BlueprintManager extends Node

## Exports chunk data to the OS clipboard.
static func export_blueprint_to_clipboard(blueprint_data: Dictionary) -> void:
    var json_string: String = JSON.stringify(blueprint_data)
    DisplayServer.clipboard_set(json_string)

## Imports blueprint from clipboard.
static func import_blueprint_from_clipboard() -> Dictionary:
    var json_string: String = DisplayServer.clipboard_get()
    var parsed_data = JSON.parse_string(json_string)
    return parsed_data if parsed_data is Dictionary else {}

深度配方(按需)

主题参考资料 / 脚本
精英网格生成与蓝图共享elite-technical-patterns.md + voxel_chunk_mesher.gd
元素 / CA 网格SKILL.md 中的架构模式 §1–3 + cellular_automata_liquid.gd
区块 RLE 持久化SKILL.md 中的保存系统 § + sandbox_world_serializer.gd

参考资料

渐进式披露:仅在研究特定 API 时打开官方文档链接;在转向相关领域时加载相关技能——不要预加载整个技能网络。

官方文档
  • 使用 MultiMesh — 为每个区块批量处理体素 / 道具实例,并避免每帧重建缓冲区。
  • 使用 GridMaps — 当结构需要单个 MultiMesh 网格之外的多样性时,使用 MeshLibrary 放置单元格。
  • ArrayMesh — 将贪心网格生成的外表面作为一个表面提交,而不是逐块创建网格。
  • SurfaceTool — 在提交给 MeshInstance3D 前构建区块网格并生成法线。
  • 后台加载 — 使用 ResourceLoader 在线程中流式加载区块,避免探索时出现卡顿。
  • 保存游戏 — 持久化玩家建造的世界(组、JSON/var_to_str、二进制 Resources)。
  • 使用多个线程 — 使用 WorkerThreadPool 进行网格生成 / 世界生成,并延后执行 SceneTree 修改。
  • 使用服务器 — 绕过 SceneTree 绘制区块时使用 RenderingServer 网格 / 实例 RID。
  • 使用 VoxelGI — 为大型程序化沙盒空间分配动态 GI。
  • 大型世界坐标 — 了解超过约 32k 单位后的精度限制和浮动原点策略。
  • 射线投射 — 通过直接空间状态执行瞄准 / 放置 / 破坏查询,而不是逐体素进行射线检测。
  • PinJoint2D — 用于玩家创建的物理装置的铰链式关节。
相关技能
前置条件
  • godot-project-foundations — 在处理区块场景和二进制 .res 世界数据前,先了解场景树、Resources 和导入基础。
  • godot-physics-3d — 用于地形的 StaticBody 碰撞体、RigidBody 道具,以及放置验证中使用的形状查询。
  • godot-gdscript-mastery — 网格生成器中使用的类型化 Dictionaries / Packed 数组、WorkerThreadPool 任务和延后执行的 SceneTree 编辑。
互补技能
下游/消费者
主技能
  • godot-master — 用于跨技能发现的技能库路由器和镜像模块入口。

评论

支持 Markdown