重要前提
安装AI Skills的关键前提是:必须科学上网,且开启TUN模式,这一点至关重要,直接决定安装能否顺利完成,在此郑重提醒三遍:科学上网,科学上网,科学上网。查看完整安装教程 →
godot-adapt-2d-to-3d by thedivergentai/gd-agentic-skills
npx skills add https://github.com/thedivergentai/gd-agentic-skills --skill godot-adapt-2d-to-3d将 2D 游戏迁移到三维空间的专家指南。
collision_layer/collision_mask。Quaternion 进行 3D 旋转插值,或使用 Basis 矩阵处理方向向量。_process 中更新摄像机位置以跟随在 中移动的物体会导致抖动。使用 来获得平滑的变换。广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
_physics_processNode3D.get_global_transform_interpolated()强制要求:在实现相应模式之前,请阅读适当的脚本。
Sprite3D 广告牌配置和世界到屏幕的投影,用于在 3D 对象上放置 2D UI。处理摄像机后检测。
用于 2D→3D 向量转换的静态实用工具。Y 到 Z 规则:2D 的 Y(向下)映射到 3D 的 Z(向前)。对于移动代码至关重要。
用于 3D 对象的投影 2D UI 映射代码片段。用从 3D 空间投影的真实 2D Canvas 空间定位替换模糊的文本元素。
| 2D 节点 | 3D 等效节点 | 备注 |
|---|---|---|
| CharacterBody2D | CharacterBody3D | 添加 Z 轴移动,用鼠标旋转 |
| RigidBody2D | RigidBody3D | 重力现在是 Vector3(0, -9.8, 0) |
| StaticBody2D | StaticBody3D | 碰撞形状使用 Shape3D |
| Area2D | Area3D | 触发器工作方式相同 |
| Sprite2D | MeshInstance3D + QuadMesh | 或使用 Sprite3D(广告牌模式) |
| AnimatedSprite2D | AnimatedSprite3D | 提供广告牌模式 |
| TileMapLayer | GridMap | 需要创建 MeshLibrary |
| Camera2D | Camera3D | 需要重新定位逻辑 |
| CollisionShape2D | CollisionShape3D | BoxShape2D → BoxShape3D,等等 |
| RayCast2D | RayCast3D | target_position 现在是 Vector3 |
# 2D 碰撞层与 3D 是分开的
# 你必须在 项目设置 → 层名称 → 3D 物理 中重新配置
# 之前 (2D):
# 层 1: 玩家
# 层 2: 敌人
# 层 3: 世界
# 之后 (3D) - 相同的名称,但不同的系统
# 在代码中,更新所有碰撞层引用:
# 2D 版本:
# collision_layer = 0b0001
# 3D 版本 (相同逻辑,不同节点):
var character_3d := CharacterBody3D.new()
character_3d.collision_layer = 0b0001 # 层 1: 玩家
character_3d.collision_mask = 0b0110 # 检测 敌人 + 世界
# ❌ 错误:直接的 2D 跟随逻辑
extends Camera3D
@onready var player: Node3D = $"../Player"
func _process(delta: float) -> void:
global_position = player.global_position # 穿模,方向迷失!
# ✅ 正确:带有 SpringArm3D 的第三人称摄像机
# 场景结构:
# Player (CharacterBody3D)
# └─ SpringArm3D
# └─ Camera3D
# player.gd
extends CharacterBody3D
@onready var spring_arm: SpringArm3D = $SpringArm3D
@onready var camera: Camera3D = $SpringArm3D/Camera3D
func _ready() -> void:
spring_arm.spring_length = 10.0 # 与玩家的距离
spring_arm.position = Vector3(0, 2, 0) # 玩家上方
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventMouseMotion:
spring_arm.rotate_y(-event.relative.x * 0.005) # 水平旋转
spring_arm.rotate_object_local(Vector3.RIGHT, -event.relative.y * 0.005) # 垂直旋转
# 限制垂直旋转
spring_arm.rotation.x = clamp(spring_arm.rotation.x, -PI/3, PI/6)
# 2D 平台游戏移动
extends CharacterBody2D
const SPEED = 300.0
const JUMP_VELOCITY = -400.0
func _physics_process(delta: float) -> void:
if not is_on_floor():
velocity.y += gravity * delta
if Input.is_action_just_pressed("jump") and is_on_floor():
velocity.y = JUMP_VELOCITY
var direction := Input.get_axis("left", "right")
velocity.x = direction * SPEED
move_and_slide()
# ✅ 3D 等效 (第三人称平台游戏)
extends CharacterBody3D
const SPEED = 5.0
const JUMP_VELOCITY = 4.5
const GRAVITY = 9.8
@onready var spring_arm: SpringArm3D = $SpringArm3D
func _physics_process(delta: float) -> void:
if not is_on_floor():
velocity.y -= GRAVITY * delta
if Input.is_action_just_pressed("jump") and is_on_floor():
velocity.y = JUMP_VELOCITY
# 相对于摄像机方向的移动
var input_dir := Input.get_vector("left", "right", "forward", "back")
var camera_basis := spring_arm.global_transform.basis
var direction := (camera_basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()
if direction:
velocity.x = direction.x * SPEED
velocity.z = direction.z * SPEED
# 旋转玩家面向移动方向
rotation.y = lerp_angle(rotation.y, atan2(-direction.x, -direction.z), 0.1)
else:
velocity.x = move_toward(velocity.x, 0, SPEED)
velocity.z = move_toward(velocity.z, 0, SPEED)
move_and_slide()
# 使用 Sprite3D 进行快速转换
extends Sprite3D
func _ready() -> void:
texture = load("res://sprites/character.png")
billboard = BaseMaterial3D.BILLBOARD_ENABLED # 始终面向摄像机
pixel_size = 0.01 # 在 3D 空间中缩放精灵
# 创建带纹理的四边形
var mesh_instance := MeshInstance3D.new()
var quad := QuadMesh.new()
quad.size = Vector2(1, 1)
mesh_instance.mesh = quad
var material := StandardMaterial3D.new()
material.albedo_texture = load("res://sprites/character.png")
material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
material.cull_mode = BaseMaterial3D.CULL_DISABLED # 显示两面
mesh_instance.material_override = material
# 导入 .glb, .fbx 模型
var character := load("res://models/character.glb").instantiate()
add_child(character)
# 访问动画
var anim_player := character.get_node("AnimationPlayer")
anim_player.play("idle")
# 添加到主场景
var sun := DirectionalLight3D.new()
sun.rotation_degrees = Vector3(-45, 30, 0)
sun.light_energy = 1.0
sun.shadow_enabled = true
add_child(sun)
# 环境光
var env := WorldEnvironment.new()
var environment := Environment.new()
environment.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
environment.ambient_light_color = Color(0.3, 0.3, 0.4) # 微妙的蓝色
environment.ambient_light_energy = 0.5
env.environment = environment
add_child(env)
# ✅ 正确:保持 2D UI 覆盖层
# 场景结构:
# Main (Node3D)
# ├─ WorldEnvironment
# ├─ DirectionalLight3D
# ├─ Player (CharacterBody3D)
# └─ CanvasLayer # 2D UI 位于 3D 世界之上
# └─ Control (HUD)
# UI 保持 2D (Control 节点,Sprite2D 用于 HUD 元素)
| 指标 | 2D 预算 | 3D 预算 | 备注 |
|---|---|---|---|
| 绘制调用 | 100-200 | 50-100 | 使用更少的网格 |
| 顶点数 | 无限制 | 100K-500K | LOD 很重要 |
| 光源 | 不适用 | 3-5 个带阴影的 | 开销大 |
| 透明对象 | 很多 | <10 | 排序开销 |
| 粒子系统 | 很多 | 最多 2-3 个 | 仅限 GPU godot-particles |
# 1. 对远处的对象使用 LOD
var mesh_instance := MeshInstance3D.new()
mesh_instance.lod_bias = 1.0 # 更早降低细节
# 2. 遮挡剔除
# 对大型墙壁/建筑使用 OccluderInstance3D
# 3. 减少阴影距离
var sun := DirectionalLight3D.new()
sun.directional_shadow_max_distance = 50.0 # 不渲染远处的阴影
# 4. 对远处的对象使用无光照材质
var material := StandardMaterial3D.new()
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
# 2D: left/right 用于水平移动
Input.get_axis("left", "right")
# 3D: 添加 forward/back,使用 get_vector()
var input := Input.get_vector("left", "right", "forward", "back")
# 返回 Vector2(horizontal, vertical) 用于 3D 移动
# 在 项目设置 → 输入映射 中配置:
# forward: W, 上箭头
# back: S, 下箭头
# left: A, 左箭头
# right: D, 右箭头
# 鼠标视角 (锁定光标)
func _ready() -> void:
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
func _input(event: InputEvent) -> void:
if event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
rotate_camera(event.relative)
# 问题:忘记为 3D 设置碰撞层
# 解决方案:重新配置层
var body := CharacterBody3D.new()
body.collision_layer = 0b0001 # 我是什么?
body.collision_mask = 0b0110 # 我检测什么?
# SpringArm3D 在受阻时会自动将摄像机向前拉
spring_arm.spring_length = 10.0
spring_arm.collision_mask = 0b0100 # 层 3: 世界
# 问题:StaticBody3D 地板没有 CollisionShape3D
# 解决方案:添加碰撞
var floor_collision := CollisionShape3D.new()
var box_shape := BoxShape3D.new()
box_shape.size = Vector3(100, 1, 100)
floor_collision.shape = box_shape
floor.add_child(floor_collision)
| 因素 | 保持 2D | 转向 3D |
|---|---|---|
| 游戏玩法 | 平台游戏,俯视角,不需要深度 | 探索,第一人称,3D 空间战斗 |
| 美术预算 | 像素美术,资源有限 | 有 3D 模型可用或必需 |
| 性能目标 | 移动端,网页,低端设备 | 桌面端,主机,高端移动设备 |
| 开发时间 | 有限 | 有时间学习 3D 曲线 |
| 团队技能 | 仅有 2D 美术师 | 有 3D 美术师或资源库 |
移动 3D 角色时,应大量依赖 Transform3D 的基础向量,而不是计算三角函数角度。要局部向前移动,请提取变换基础矩阵的负 Z 轴:velocity = transform.basis.z * speed。
在 2D 中,Y 轴指向下方。在 3D 中,Godot 使用右手坐标系,其中 Y 轴指向上方,向前是 -Z。将 2D 跳跃转换为 3D 需要反转 Y 速度逻辑(例如,使用 velocity.y = JUMP_SPEED 而不是 -JUMP_SPEED)。
每周安装次数
48
代码仓库
GitHub 星标数
59
首次出现
2026年2月10日
安全审计
安装于
opencode48
gemini-cli47
codex47
kimi-cli46
amp46
github-copilot46
Expert guidance for migrating 2D games into the third dimension.
Quaternion for 3D rotation interpolation or the Basis matrix for directional vectors._process to follow a body moving in _physics_process causes jitter. Use Node3D.get_global_transform_interpolated() for smooth transforms.MANDATORY : Read the appropriate script before implementing the corresponding pattern.
Sprite3D billboard configuration and world-to-screen projection for placing 2D UI over 3D objects. Handles behind-camera detection.
Static utility for 2D→3D vector translation. The Y-to-Z rule: 2D Y (down) maps to 3D Z (forward). Essential for movement code.
Projected 2D UI for 3D Objects mapping snippet. Replaces blurry text elements with true 2D Canvas space positioning projected from 3D space.
| 2D Node | 3D Equivalent | Notes |
|---|---|---|
| CharacterBody2D | CharacterBody3D | Add Z-axis movement, rotate with mouse |
| RigidBody2D | RigidBody3D | Gravity now Vector3(0, -9.8, 0) |
| StaticBody2D | StaticBody3D | Collision shapes use Shape3D |
| Area2D | Area3D | Triggers work the same way |
| Sprite2D | MeshInstance3D + QuadMesh | Or use Sprite3D (billboarded) |
| AnimatedSprite2D | AnimatedSprite3D | Billboard mode available |
| TileMapLayer | GridMap | Requires MeshLibrary creation |
| Camera2D | Camera3D | Requires repositioning logic |
| CollisionShape2D | CollisionShape3D | BoxShape2D → BoxShape3D, etc. |
# 2D collision layers are SEPARATE from 3D
# You must reconfigure in Project Settings → Layer Names → 3D Physics
# Before (2D):
# Layer 1: Player
# Layer 2: Enemies
# Layer 3: World
# After (3D) - same names, but different system
# In code, update all collision layer references:
# 2D version:
# collision_layer = 0b0001
# 3D version (same logic, different node):
var character_3d := CharacterBody3D.new()
character_3d.collision_layer = 0b0001 # Layer 1: Player
character_3d.collision_mask = 0b0110 # Detect Enemies + World
# ❌ BAD: Direct 2D follow logic
extends Camera3D
@onready var player: Node3D = $"../Player"
func _process(delta: float) -> void:
global_position = player.global_position # Clipping, disorienting!
# ✅ GOOD: Third-person camera with SpringArm3D
# Scene structure:
# Player (CharacterBody3D)
# └─ SpringArm3D
# └─ Camera3D
# player.gd
extends CharacterBody3D
@onready var spring_arm: SpringArm3D = $SpringArm3D
@onready var camera: Camera3D = $SpringArm3D/Camera3D
func _ready() -> void:
spring_arm.spring_length = 10.0 # Distance from player
spring_arm.position = Vector3(0, 2, 0) # Above player
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventMouseMotion:
spring_arm.rotate_y(-event.relative.x * 0.005) # Horizontal rotation
spring_arm.rotate_object_local(Vector3.RIGHT, -event.relative.y * 0.005) # Vertical
# Clamp vertical rotation
spring_arm.rotation.x = clamp(spring_arm.rotation.x, -PI/3, PI/6)
# 2D platformer movement
extends CharacterBody2D
const SPEED = 300.0
const JUMP_VELOCITY = -400.0
func _physics_process(delta: float) -> void:
if not is_on_floor():
velocity.y += gravity * delta
if Input.is_action_just_pressed("jump") and is_on_floor():
velocity.y = JUMP_VELOCITY
var direction := Input.get_axis("left", "right")
velocity.x = direction * SPEED
move_and_slide()
# ✅ 3D equivalent (third-person platformer)
extends CharacterBody3D
const SPEED = 5.0
const JUMP_VELOCITY = 4.5
const GRAVITY = 9.8
@onready var spring_arm: SpringArm3D = $SpringArm3D
func _physics_process(delta: float) -> void:
if not is_on_floor():
velocity.y -= GRAVITY * delta
if Input.is_action_just_pressed("jump") and is_on_floor():
velocity.y = JUMP_VELOCITY
# Movement relative to camera direction
var input_dir := Input.get_vector("left", "right", "forward", "back")
var camera_basis := spring_arm.global_transform.basis
var direction := (camera_basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()
if direction:
velocity.x = direction.x * SPEED
velocity.z = direction.z * SPEED
# Rotate player to face movement direction
rotation.y = lerp_angle(rotation.y, atan2(-direction.x, -direction.z), 0.1)
else:
velocity.x = move_toward(velocity.x, 0, SPEED)
velocity.z = move_toward(velocity.z, 0, SPEED)
move_and_slide()
# Use Sprite3D for quick conversion
extends Sprite3D
func _ready() -> void:
texture = load("res://sprites/character.png")
billboard = BaseMaterial3D.BILLBOARD_ENABLED # Always face camera
pixel_size = 0.01 # Scale sprite in 3D space
# Create textured quads
var mesh_instance := MeshInstance3D.new()
var quad := QuadMesh.new()
quad.size = Vector2(1, 1)
mesh_instance.mesh = quad
var material := StandardMaterial3D.new()
material.albedo_texture = load("res://sprites/character.png")
material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
material.cull_mode = BaseMaterial3D.CULL_DISABLED # Show both sides
mesh_instance.material_override = material
# Import .glb, .fbx models
var character := load("res://models/character.glb").instantiate()
add_child(character)
# Access animations
var anim_player := character.get_node("AnimationPlayer")
anim_player.play("idle")
# Add to main scene
var sun := DirectionalLight3D.new()
sun.rotation_degrees = Vector3(-45, 30, 0)
sun.light_energy = 1.0
sun.shadow_enabled = true
add_child(sun)
# Ambient light
var env := WorldEnvironment.new()
var environment := Environment.new()
environment.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
environment.ambient_light_color = Color(0.3, 0.3, 0.4) # Subtle blue
environment.ambient_light_energy = 0.5
env.environment = environment
add_child(env)
# ✅ GOOD: Keep 2D UI overlay
# Scene structure:
# Main (Node3D)
# ├─ WorldEnvironment
# ├─ DirectionalLight3D
# ├─ Player (CharacterBody3D)
# └─ CanvasLayer # 2D UI on top of 3D world
# └─ Control (HUD)
# UI remains 2D (Control nodes, Sprite2D for HUD elements)
| Metric | 2D Budget | 3D Budget | Notes |
|---|---|---|---|
| Draw calls | 100-200 | 50-100 | Use fewer meshes |
| Vertices | Unlimited | 100K-500K | LOD important |
| Lights | N/A | 3-5 shadowed | Expensive |
| Transparent objects | Many | <10 | Sorting overhead |
| Particle systems | Many | 2-3 max | GPU godot-particles only |
# 1. Use LOD for distant objects
var mesh_instance := MeshInstance3D.new()
mesh_instance.lod_bias = 1.0 # Lower detail sooner
# 2. Occlusion culling
# Use OccluderInstance3D for large walls/buildings
# 3. Reduce shadow distance
var sun := DirectionalLight3D.new()
sun.directional_shadow_max_distance = 50.0 # Don't render far shadows
# 4. Use unlit materials for distant objects
var material := StandardMaterial3D.new()
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
# 2D: left/right for horizontal movement
Input.get_axis("left", "right")
# 3D: Add forward/back, use get_vector()
var input := Input.get_vector("left", "right", "forward", "back")
# Returns Vector2(horizontal, vertical) for 3D movement
# Configure in Project Settings → Input Map:
# forward: W, Up Arrow
# back: S, Down Arrow
# left: A, Left Arrow
# right: D, Right Arrow
# Mouse look (lock cursor)
func _ready() -> void:
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
func _input(event: InputEvent) -> void:
if event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
rotate_camera(event.relative)
# Problem: Forgot to set collision layers for 3D
# Solution: Reconfigure layers
var body := CharacterBody3D.new()
body.collision_layer = 0b0001 # What AM I?
body.collision_mask = 0b0110 # What do I DETECT?
# SpringArm3D automatically pulls camera forward when obstructed
spring_arm.spring_length = 10.0
spring_arm.collision_mask = 0b0100 # Layer 3: World
# Problem: StaticBody3D floor has no CollisionShape3D
# Solution: Add collision
var floor_collision := CollisionShape3D.new()
var box_shape := BoxShape3D.new()
box_shape.size = Vector3(100, 1, 100)
floor_collision.shape = box_shape
floor.add_child(floor_collision)
| Factor | Stay 2D | Go 3D |
|---|---|---|
| Gameplay | Platformer, top-down, no depth needed | Exploration, first-person, 3D space combat |
| Art budget | Pixel art, limited resources | 3D models available or necessary |
| Performance target | Mobile, web, low-end | Desktop, console, high-end mobile |
| Development time | Limited | Have time for 3D learning curve |
| Team skills | 2D artists only | 3D artists or asset library |
When moving a 3D character, rely heavily on Transform3D basis vectors rather than calculating trigonometric angles. To move forward locally, extract the negative Z-axis of your transform's basis: velocity = transform.basis.z * speed.
In 2D, the Y-axis points down. In 3D, Godot uses a right-handed system where Y-axis points UP, and forward is -Z. Translating 2D jumps to 3D requires inverting the Y velocity logic (e.g., velocity.y = JUMP_SPEED instead of -JUMP_SPEED).
Weekly Installs
48
Repository
GitHub Stars
59
First Seen
Feb 10, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode48
gemini-cli47
codex47
kimi-cli46
amp46
github-copilot46
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
127,000 周安装
| RayCast2D | RayCast3D | target_position is now Vector3 |