重要前提
安装AI Skills的关键前提是:必须科学上网,且开启TUN模式,这一点至关重要,直接决定安装能否顺利完成,在此郑重提醒三遍:科学上网,科学上网,科学上网。查看完整安装教程 →
godot-genre-sandbox by thedivergentai/gd-agentic-skills
npx skills add https://github.com/thedivergentai/gd-agentic-skills --skill godot-genre-sandbox物理模拟、涌现式玩法与玩家创造力定义了此类型。
RigidBody 节点;严格使用静态碰撞体处理世界,并将物理模拟留给动态道具。MultiMesh 缓冲区;严格批量处理变更,并且仅在修改完成时(例如,玩家停止绘制)才重建缓冲区。Node;严格使用 PackedInt32Arrays 或类型化字典以保持最小的内存开销。floor(pos/size))进行直接的 O(1) 单元计算。ArrayMesh(剔除/贪婪网格生成)。广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
ResourceLoader.load_threaded_request() 以防止帧卡顿。.tscn 文件来存储体素数据集;严格使用二进制 .res 文件以获得 10 倍的解析速度。if water and fire);严格使用属性系统,让交互从材质属性(可燃性、密度)中涌现。call_deferred() 或互斥锁以确保安全。queue_free()。MultiMeshInstance3D 进行专业的区块管理。对材质属性建模,而非行为。交互从重叠的属性中涌现。
# 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 # 用于液体流动方向
@export var flammable: float = 0.0 # 0-1:点燃几率
@export var ignition_temp: float = 400.0
@export var conductivity: float = 0.0 # 用于电/热传导
@export var hardness: float = 1.0 # 挖掘时间乘数
# 边缘情况:如果两个元素密度相同但类型不同怎么办?
# 解决方案:使用次级排序(类型枚举优先级:SOLID > LIQUID > POWDER > GAS)
func should_swap_with(other: ElementData) -> bool:
if density == other.density:
return type > other.type # 枚举比较:SOLID(0) > GAS(3)
return density > other.density
更新顺序很重要。自上而下可防止粒子"瞬移"。
# world_grid.gd
var grid: Dictionary = {} # Vector2i -> ElementData
var dirty_cells: Array[Vector2i] = []
func _physics_process(_delta: float) -> void:
# 关键:自上而下排序以防止双重移动
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:
# 尝试向下,然后左下,然后右下
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:
# 气体上升(与液体相反)
var targets = [pos + Vector2i.UP,
pos + Vector2i(-1, -1),
pos + Vector2i(1, -1)]
# 相同的交换逻辑...
# 边缘情况:如果多个粒子想移动到同一个单元格怎么办?
# 解决方案:仅标记目标为脏,不进行双重交换。下一帧解决冲突。
将输入与世界修改解耦。
# 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))
# 圆形笔刷模式
for x in range(-radius, radius + 1):
for y in range(-radius, radius + 1):
if x*x + y*y <= radius*radius: # 圆形边界
var target = grid_pos + Vector2i(x, y)
world.set_cell(target, element)
# 后备方案:如果元素放置失败(例如,被不可破坏的方块占据)?
# 在 set_cell() 之前检查 world.can_place(target),显示视觉反馈。
仅渲染可见的面。使用贪婪网格生成来合并相邻的方块。
# 完整实现请参见 scripts/voxel_chunk_manager.gd
# 专家决策树:
# - 小型世界 (<100k 方块): 使用 SurfaceTool 的单个 MeshInstance
# - 中型世界 (100k-1M 方块): 分块的 MultiMesh(参见脚本)
# - 大型世界 (>1M 方块): 分块 + 贪婪网格生成 + LOD
# 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...]
# 专家技术:游程编码
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 = 空气
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
# 刷新最后的游程
if run_count > 0:
data.rle_data.append(run_type)
data.rle_data.append(run_count)
return data
# 压缩结果:空区块(16×16 = 256 个空气方块)
# 不使用 RLE:256 个整数 = 1024 字节
# 使用 RLE:[0, 256] = 8 字节(128 倍压缩!)
# 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 # 允许轻微弯曲
add_child(joint)
# 边缘情况:如果关节存在时物体被删除了怎么办?
# 在 Godot 4.x 中关节会自动断裂,但孤立的 Node 会泄漏内存。
# 解决方案:
body_a.tree_exiting.connect(func(): joint.queue_free())
body_b.tree_exiting.connect(func(): joint.queue_free())
# 后备方案:玩家将关节连接到静态几何体?
# 创建关节前检查 `body.freeze == false`。
MultiMeshInstance3D.multimesh.instance_count:必须在缓冲区分配之前设置。无法动态增长——需要重新创建。RigidBody2D.sleeping:物体在 2 秒无运动后会自动休眠。使用 apply_central_impulse(Vector2.ZERO) 来强制唤醒而不施加力。GridMap 对比 MultiMesh:GridMap 使用 MeshLibrary(适合多样性),MultiMesh 使用单一网格(适合速度)。组合使用:GridMap 用于结构,MultiMesh 用于地形。continuous_cd 需要凸碰撞形状。对抛射体使用 CapsuleShape2D,而非 RectangleShape2D。每周安装数
53
代码仓库
GitHub 星标数
62
首次出现
2026年2月10日
安全审计
安装于
opencode51
gemini-cli50
codex50
github-copilot49
kimi-cli49
amp49
Physical simulation, emergent play, and player creativity define this genre.
RigidBody nodes for every block; strictly use Static Colliders for the world and reserve physics for dynamic props.MultiMesh buffers every frame; strictly batch changes and only rebuild the buffer when a modification completes (e.g., player stops painting).Nodes for every grid cell; strictly use PackedInt32Arrays or typed Dictionaries to keep RAM overhead minimal.floor(pos/size)) for direct O(1) cell calculation.ArrayMesh that only pushes visible exterior faces to the GPU (Culling/Greedy Meshing).ResourceLoader.load_threaded_request() to prevent frame stutter..tscn files for voxel datasets; strictly use binary.res files for 10x faster parsing.if water and fire); strictly use a Property System where interactions emerge from material attributes (flammability, density).call_deferred() or Mutex locks for safety.queue_free() on discarded branches.MultiMeshInstance3D with batch update logic.Model material properties, not behaviors. Interactions emerge from overlapping properties.
# 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
Update order matters. Top-down prevents "teleporting" 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.
Decouple input from world modification.
# 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.
Only render visible faces. Use greedy meshing to merge adjacent blocks.
# See scripts/voxel_chunk_manager.gd for full implementation
# EXPERT DECISION TREE:
# - Small worlds (<100k blocks): Single MeshInstance with SurfaceTool
# - Medium worlds (100k-1M blocks): Chunked MultiMesh (see script)
# - Large worlds (>1M blocks): Chunked + greedy meshing + LOD
# 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.
MultiMeshInstance3D.multimesh.instance_count : MUST be set before buffer allocation. Cannot dynamically grow — requires recreation.RigidBody2D.sleeping : Bodies auto-sleep after 2 seconds of no movement. Use apply_central_impulse(Vector2.ZERO) to force wake without adding force.GridMap vs MultiMesh: GridMap uses MeshLibrary (great for variety), MultiMesh uses single mesh (great for speed). Combine: GridMap for structures, MultiMesh for terrain.continuous_cd requires convex collision shapes. Use CapsuleShape2D for projectiles, NOT RectangleShape2D.Weekly Installs
53
Repository
GitHub Stars
62
First Seen
Feb 10, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode51
gemini-cli50
codex50
github-copilot49
kimi-cli49
amp49
GSAP React 动画库使用指南:useGSAP Hook 与最佳实践
3,900 周安装
使用Doppler安全发布Python包到PyPI | 本地发布脚本与凭证管理指南
63 周安装
SpacetimeDB 最佳实践指南:TypeScript 模块与 React 客户端集成开发
63 周安装
skill-registry:AI技能注册与管理工具,自动化技能搜索与JSON注册表维护
63 周安装
Graphite CLI 分支栈管理指南:堆叠式 PR 与基于主干开发工作流
63 周安装
prompt-xray:AI提示词生成与解析工具,提升Claude等AI开发效率
63 周安装
Zotero MCP 代码执行技能:Python 代码安全搜索 Zotero 文献库,避免崩溃,自动去重排序
45 周安装