godot-adapt-2d-to-3d

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Adapt: 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 Node3D EquivalentNotes
CharacterBody2DCharacterBody3DAdd Z-axis movement, rotate with mouse
RigidBody2DRigidBody3DGravity now Vector3(0, -9.8, 0)
StaticBody2DStaticBody3DCollision shapes use Shape3D
Area2DArea3DTriggers work the same way
Sprite2DMeshInstance3D + QuadMeshOr use Sprite3D (billboarded)
AnimatedSprite2DAnimatedSprite3DBillboard mode available
TileMapLayerGridMapRequires MeshLibrary creation
Camera2DCamera3DRequires repositioning logic
CollisionShape2DCollisionShape3DBoxShape2D → BoxShape3D, etc.
RayCast2DRayCast3Dtarget_position is now Vector3

2D节点3D等效节点说明
CharacterBody2DCharacterBody3D添加Z轴移动,支持鼠标旋转
RigidBody2DRigidBody3D重力现在为Vector3(0, -9.8, 0)
StaticBody2DStaticBody3D碰撞形状使用Shape3D
Area2DArea3D触发逻辑保持不变
Sprite2DMeshInstance3D + QuadMesh或使用Sprite3D(公告板模式)
AnimatedSprite2DAnimatedSprite3D支持公告板模式
TileMapLayerGridMap需要创建MeshLibrary
Camera2DCamera3D需要重新设置定位逻辑
CollisionShape2DCollisionShape3DBoxShape2D → BoxShape3D等
RayCast2DRayCast3Dtarget_position现在为Vector3类型

Migration Steps

迁移步骤

Step 1: Physics Layer Reconfiguration

步骤1:物理层重新配置

gdscript
undefined
gdscript
undefined

2D 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
undefined
var character_3d := CharacterBody3D.new() character_3d.collision_layer = 0b0001 # Layer 1: Player character_3d.collision_mask = 0b0110 # Detect Enemies + World
undefined

Step 2: Camera Conversion

步骤2:相机转换

gdscript
undefined
gdscript
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)
undefined
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)
undefined

Step 3: Movement Conversion

步骤3:移动转换

gdscript
undefined
gdscript
undefined

2D 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
undefined
gdscript
undefined

Use 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
undefined
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
undefined

Option 2: Quad Meshes (Floating Sprites)

选项2:四边形网格(悬浮精灵)

gdscript
undefined
gdscript
undefined

Create 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
undefined
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
undefined

Option 3: Full 3D Models (Blender/Asset Library)

选项3:完整3D模型(Blender/资源库)

gdscript
undefined
gdscript
undefined

Import .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
undefined
gdscript
undefined

Add 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
undefined
gdscript
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性能对比

Metric2D Budget3D BudgetNotes
Draw calls100-20050-100Use fewer meshes
VerticesUnlimited100K-500KLOD important
LightsN/A3-5 shadowedExpensive
Transparent objectsMany<10Sorting overhead
Particle systemsMany2-3 maxGPU godot-particles only
指标2D预算3D预算说明
绘制调用100-20050-100使用更少的网格
顶点数无限制100K-500KLOD(细节层次)很重要
光照3-5个带阴影的开销大
透明对象大量<10个排序开销大
粒子系统大量最多2-3个仅使用GPU加速的Godot粒子

Optimization Checklist

优化检查清单

gdscript
undefined
gdscript
undefined

1. 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
undefined
gdscript
undefined

2D: 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
undefined
gdscript
undefined

Problem: 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?
undefined
var body := CharacterBody3D.new() body.collision_layer = 0b0001 # What AM I? body.collision_mask = 0b0110 # What do I DETECT?
undefined

Camera Clipping Through Walls

相机穿模

gdscript
undefined
gdscript
undefined

SpringArm3D 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
undefined
spring_arm.spring_length = 10.0 spring_arm.collision_mask = 0b0100 # Layer 3: World
undefined

Player Falling Through Floor

玩家穿过地面

gdscript
undefined
gdscript
undefined

Problem: 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

FactorStay 2DGo 3D
GameplayPlatformer, top-down, no depth neededExploration, first-person, 3D space combat
Art budgetPixel art, limited resources3D models available or necessary
Performance targetMobile, web, low-endDesktop, console, high-end mobile
Development timeLimitedHave time for 3D learning curve
Team skills2D artists only3D artists or asset library
因素保留2D转向3D
玩法平台跳跃、俯视角,无需深度探索类、第一人称、3D空间战斗
美术预算像素画、资源有限已有3D模型或必须使用3D
性能目标移动设备、网页、低端设备桌面、主机、高端移动设备
开发时间时间有限有时间学习3D开发曲线
团队技能仅2D美术有3D美术或可使用资源库

Reference

参考

  • Master Skill: godot-master
  • 核心技能:godot-master