godot-adapt-2d-to-3d
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAdapt: 2D to 3D
适配:从2D到3D
Expert guidance for migrating 2D games into the third dimension.
将2D游戏迁移到三维空间的专家指南。
NEVER Do
绝对不要做的事
- NEVER directly replace Vector2 with Vector3(x, y, 0) — This creates a "flat 3D" game with no depth gameplay. Add Z-axis movement or camera rotation to justify 3D.
- NEVER keep 2D collision layers — 2D and 3D physics use separate layer systems. You must reconfigure collision_layer/collision_mask for 3D nodes.
- NEVER forget to add lighting — 3D without lights is pitch black (unless using unlit materials). Add at least one DirectionalLight3D.
- NEVER use Camera2D follow logic in 3D — Camera3D needs spring arm or look-at logic. Direct position copying causes clipping and disorientation.
- NEVER assume same performance — 3D is 5-10x more demanding. Budget for lower draw calls, smaller viewport resolution on mobile.
- 绝对不要直接用Vector3(x, y, 0)替换Vector2 —— 这会创建一个没有深度玩法的"平面3D"游戏。添加Z轴移动或相机旋转来体现3D的价值。
- 绝对不要保留2D碰撞层 —— 2D和3D物理系统使用独立的层体系。你必须为3D节点重新配置collision_layer/collision_mask。
- 绝对不要忘记添加光照 —— 没有光照的3D场景会一片漆黑(除非使用无光照材质)。至少添加一个DirectionalLight3D。
- 绝对不要在3D中使用Camera2D的跟随逻辑 —— Camera3D需要SpringArm或看向目标的逻辑。直接复制位置会导致穿模和视角混乱。
- 绝对不要假设性能表现一致 —— 3D的性能需求是2D的5-10倍。在移动平台上要控制绘制调用数量,降低视口分辨率。
Available Scripts
可用脚本
MANDATORY: Read the appropriate script before implementing the corresponding pattern.
强制要求:在实现对应方案前,请先阅读相应脚本。
sprite_plane.gd
sprite_plane.gd
Sprite3D billboard configuration and world-to-screen projection for placing 2D UI over 3D objects. Handles behind-camera detection.
Sprite3D公告板配置以及世界坐标到屏幕坐标的投影,用于在3D对象上方放置2D UI。处理相机后方的检测逻辑。
vector_mapping.gd
vector_mapping.gd
Static utility for 2D→3D vector translation. The Y-to-Z rule: 2D Y (down) maps to 3D Z (forward). Essential for movement code.
用于2D→3D向量转换的静态工具类。Y转Z规则:2D的Y轴(向下)对应3D的Z轴(向前)。是移动代码转换的核心工具。
Node Conversion Matrix
节点转换矩阵
| 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. |
| RayCast2D | RayCast3D | target_position is now Vector3 |
| 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类型 |
Migration Steps
迁移步骤
Step 1: Physics Layer Reconfiguration
步骤1:物理层重新配置
gdscript
undefinedgdscript
undefined2D collision layers are SEPARATE from 3D
2D collision layers are SEPARATE from 3D
You must reconfigure in Project Settings → Layer Names → 3D Physics
You must reconfigure in Project Settings → Layer Names → 3D Physics
Before (2D):
Before (2D):
Layer 1: Player
Layer 1: Player
Layer 2: Enemies
Layer 2: Enemies
Layer 3: World
Layer 3: World
After (3D) - same names, but different system
After (3D) - same names, but different system
In code, update all collision layer references:
In code, update all collision layer references:
2D version:
2D version:
collision_layer = 0b0001
collision_layer = 0b0001
3D version (same logic, different node):
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
undefinedvar character_3d := CharacterBody3D.new()
character_3d.collision_layer = 0b0001 # Layer 1: Player
character_3d.collision_mask = 0b0110 # Detect Enemies + World
undefinedStep 2: Camera Conversion
步骤2:相机转换
gdscript
undefinedgdscript
undefined❌ BAD: Direct 2D follow logic
❌ BAD: Direct 2D follow logic
extends Camera3D
@onready var player: Node3D = $"../Player"
func _process(delta: float) -> void:
global_position = player.global_position # Clipping, disorienting!
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
✅ GOOD: Third-person camera with SpringArm3D
Scene structure:
Scene structure:
Player (CharacterBody3D)
Player (CharacterBody3D)
└─ SpringArm3D
└─ SpringArm3D
└─ Camera3D
└─ Camera3D
player.gd
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)undefinedextends 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)undefinedStep 3: Movement Conversion
步骤3:移动转换
gdscript
undefinedgdscript
undefined2D platformer movement
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()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)
✅ 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()
---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()
---Art Pipeline: Sprites → 3D Models
美术流程:精灵 → 3D模型
Option 1: Billboard Sprites (2.5D)
选项1:公告板精灵(2.5D)
gdscript
undefinedgdscript
undefinedUse Sprite3D for quick conversion
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
undefinedextends 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
undefinedOption 2: Quad Meshes (Floating Sprites)
选项2:四边形网格(悬浮精灵)
gdscript
undefinedgdscript
undefinedCreate textured quads
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
undefinedvar 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
undefinedOption 3: Full 3D Models (Blender/Asset Library)
选项3:完整3D模型(Blender/资源库)
gdscript
undefinedgdscript
undefinedImport .glb, .fbx models
Import .glb, .fbx models
var character := load("res://models/character.glb").instantiate()
add_child(character)
var character := load("res://models/character.glb").instantiate()
add_child(character)
Access animations
Access animations
var anim_player := character.get_node("AnimationPlayer")
anim_player.play("idle")
---var anim_player := character.get_node("AnimationPlayer")
anim_player.play("idle")
---Lighting Considerations
光照注意事项
Minimum Lighting Setup
最低光照配置
gdscript
undefinedgdscript
undefinedAdd to main scene
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)
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
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)
---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)
---UI Adaptation
UI适配
gdscript
undefinedgdscript
undefined✅ GOOD: Keep 2D UI overlay
✅ GOOD: Keep 2D UI overlay
Scene structure:
Scene structure:
Main (Node3D)
Main (Node3D)
├─ WorldEnvironment
├─ WorldEnvironment
├─ DirectionalLight3D
├─ DirectionalLight3D
├─ Player (CharacterBody3D)
├─ Player (CharacterBody3D)
└─ CanvasLayer # 2D UI on top of 3D world
└─ CanvasLayer # 2D UI on top of 3D world
└─ Control (HUD)
└─ Control (HUD)
UI remains 2D (Control nodes, Sprite2D for HUD elements)
UI remains 2D (Control nodes, Sprite2D for HUD elements)
---
---Performance Budgeting
性能预算
2D vs 3D Performance
2D vs 3D性能对比
| 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 |
| 指标 | 2D预算 | 3D预算 | 说明 |
|---|---|---|---|
| 绘制调用 | 100-200 | 50-100 | 使用更少的网格 |
| 顶点数 | 无限制 | 100K-500K | LOD(细节层次)很重要 |
| 光照 | 无 | 3-5个带阴影的 | 开销大 |
| 透明对象 | 大量 | <10个 | 排序开销大 |
| 粒子系统 | 大量 | 最多2-3个 | 仅使用GPU加速的Godot粒子 |
Optimization Checklist
优化检查清单
gdscript
undefinedgdscript
undefined1. Use LOD for distant objects
1. Use LOD for distant objects
var mesh_instance := MeshInstance3D.new()
mesh_instance.lod_bias = 1.0 # Lower detail sooner
var mesh_instance := MeshInstance3D.new()
mesh_instance.lod_bias = 1.0 # Lower detail sooner
2. Occlusion culling
2. Occlusion culling
Use OccluderInstance3D for large walls/buildings
Use OccluderInstance3D for large walls/buildings
3. Reduce shadow distance
3. Reduce shadow distance
var sun := DirectionalLight3D.new()
sun.directional_shadow_max_distance = 50.0 # Don't render far shadows
var sun := DirectionalLight3D.new()
sun.directional_shadow_max_distance = 50.0 # Don't render far shadows
4. Use unlit materials for distant objects
4. Use unlit materials for distant objects
var material := StandardMaterial3D.new()
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
---var material := StandardMaterial3D.new()
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
---Input Scheme Changes
输入方案变更
2D → 3D Input Mapping
2D → 3D输入映射
gdscript
undefinedgdscript
undefined2D: left/right for horizontal movement
2D: left/right for horizontal movement
Input.get_axis("left", "right")
Input.get_axis("left", "right")
3D: Add forward/back, use get_vector()
3D: Add forward/back, use get_vector()
var input := Input.get_vector("left", "right", "forward", "back")
var input := Input.get_vector("left", "right", "forward", "back")
Returns Vector2(horizontal, vertical) for 3D movement
Returns Vector2(horizontal, vertical) for 3D movement
Configure in Project Settings → Input Map:
Configure in Project Settings → Input Map:
forward: W, Up Arrow
forward: W, Up Arrow
back: S, Down Arrow
back: S, Down Arrow
left: A, Left Arrow
left: A, Left Arrow
right: D, Right Arrow
right: D, Right Arrow
Mouse look (lock cursor)
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)
---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)
---Edge Cases
边缘情况
Physics Not Working
物理系统不工作
gdscript
undefinedgdscript
undefinedProblem: Forgot to set collision layers for 3D
Problem: Forgot to set collision layers for 3D
Solution: Reconfigure layers
Solution: Reconfigure layers
var body := CharacterBody3D.new()
body.collision_layer = 0b0001 # What AM I?
body.collision_mask = 0b0110 # What do I DETECT?
undefinedvar body := CharacterBody3D.new()
body.collision_layer = 0b0001 # What AM I?
body.collision_mask = 0b0110 # What do I DETECT?
undefinedCamera Clipping Through Walls
相机穿模
gdscript
undefinedgdscript
undefinedSpringArm3D automatically pulls camera forward when obstructed
SpringArm3D automatically pulls camera forward when obstructed
spring_arm.spring_length = 10.0
spring_arm.collision_mask = 0b0100 # Layer 3: World
undefinedspring_arm.spring_length = 10.0
spring_arm.collision_mask = 0b0100 # Layer 3: World
undefinedPlayer Falling Through Floor
玩家穿过地面
gdscript
undefinedgdscript
undefinedProblem: StaticBody3D floor has no CollisionShape3D
Problem: StaticBody3D floor has no CollisionShape3D
Solution: Add collision
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)
---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)
---Decision Tree: When to Go 3D
决策树:何时转向3D
| 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 |
| 因素 | 保留2D | 转向3D |
|---|---|---|
| 玩法 | 平台跳跃、俯视角,无需深度 | 探索类、第一人称、3D空间战斗 |
| 美术预算 | 像素画、资源有限 | 已有3D模型或必须使用3D |
| 性能目标 | 移动设备、网页、低端设备 | 桌面、主机、高端移动设备 |
| 开发时间 | 时间有限 | 有时间学习3D开发曲线 |
| 团队技能 | 仅2D美术 | 有3D美术或可使用资源库 |
Reference
参考
- Master Skill: godot-master
- 核心技能:godot-master