godot-adapt-3d-to-2d
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAdapt: 3D to 2D
适配:3D转2D
Expert guidance for simplifying 3D games into 2D (or 2.5D).
将3D游戏简化为2D(或2.5D)的专业指南。
NEVER Do
绝对不要做这些
- NEVER remove Z-axis without gameplay compensation — Blindly flattening 3D to 2D removes spatial strategy. Add other depth mechanics (layers, jump height variations).
- NEVER keep 3D collision shapes — Use simpler 2D shapes (CapsuleShape2D, RectangleShape2D). 3D shapes don't convert automatically.
- NEVER use orthographic Camera3D as "2D mode" — Use actual Camera2D for proper 2D rendering pipeline and performance.
- NEVER assume automatic performance gain — Poorly optimized 2D (too many draw calls, large sprite sheets) can be slower than optimized 3D.
- NEVER forget to adjust gravity — 3D gravity is Vector3(0, -9.8, 0). 2D gravity is float (980 pixels/s²). Scale appropriately.
- 绝对不要在没有游戏玩法补偿的情况下移除Z轴 — 盲目地将3D扁平化到2D会消除空间策略性。应添加其他深度机制(如分层、跳跃高度变化)。
- 绝对不要保留3D碰撞形状 — 使用更简单的2D形状(CapsuleShape2D、RectangleShape2D)。3D形状无法自动转换。
- 绝对不要将正交Camera3D当作「2D模式」使用 — 使用真正的Camera2D以获得正确的2D渲染管线和性能表现。
- 绝对不要想当然地认为性能会自动提升 — 优化不佳的2D游戏(比如过多绘制调用、过大精灵图集)可能比优化后的3D游戏运行更慢。
- 绝对不要忘记调整重力参数 — 3D重力是Vector3(0, -9.8, 0),2D重力是浮点数(980像素/秒²),需进行相应缩放。
Available Scripts
可用脚本
MANDATORY: Read the appropriate script before implementing the corresponding pattern.
强制要求:在实现对应模式前,请先阅读相应脚本。
ortho_simulation.gd
ortho_simulation.gd
Simulates 3D Z-axis height in 2D top-down games. Handles vertical velocity, gravity, sprite offset, and shadow scaling.
在2D俯视角游戏中模拟3D Z轴高度。处理垂直速度、重力、精灵偏移和阴影缩放。
projection_utils.gd
projection_utils.gd
Projects 3D world positions to 2D screen space for nameplates, healthbars, and targeting. Handles behind-camera detection and distance-based scaling.
将3D世界坐标投影到2D屏幕空间,用于名称牌、血条和目标锁定。处理相机后方检测和基于距离的缩放。
Why Go from 3D to 2D?
为什么要从3D转2D?
| Reason | Benefit |
|---|---|
| Mobile performance | 5-10x faster on low-end devices |
| Simpler art pipeline | Sprites easier to create than 3D models |
| Faster iteration | 2D level design is quicker |
| Accessibility | Lower hardware requirements |
| Clarity | Reduce visual clutter for puzzle/strategy games |
| 原因 | 优势 |
|---|---|
| 移动平台性能 | 在低端设备上运行速度提升5-10倍 |
| 更简单的美术工作流 | 制作精灵图比3D模型更容易 |
| 更快的迭代速度 | 2D关卡设计耗时更短 |
| 更低的硬件门槛 | 降低设备配置要求 |
| 更清晰的视觉表现 | 减少解谜/策略游戏中的视觉杂乱 |
Dimension Reduction Strategies
降维策略
Strategy 1: True 2D (Remove Z-axis)
策略1:纯2D(移除Z轴)
gdscript
undefinedgdscript
undefinedTop-down or side-view
Top-down or side-view
Example: 3D isometric → 2D top-down
Example: 3D isometric → 2D top-down
Before (3D):
Before (3D):
var velocity := Vector3(input.x, 0, input.y) * speed
var velocity := Vector3(input.x, 0, input.y) * speed
After (2D):
After (2D):
var velocity := Vector2(input.x, input.y) * speed
var velocity := Vector2(input.x, input.y) * speed
Use case: Top-down shooters, RTS, turn-based strategy
Use case: Top-down shooters, RTS, turn-based strategy
undefinedundefinedStrategy 2: 2.5D (Fake depth with layers)
策略2:2.5D(用分层模拟深度)
gdscript
undefinedgdscript
undefinedKeep visual depth perception without Z-axis gameplay
Keep visual depth perception without Z-axis gameplay
Use ParallaxBackground for depth layers
Use ParallaxBackground for depth layers
Scene structure:
Scene structure:
ParallaxBackground
ParallaxBackground
├─ ParallaxLayer (far mountains, scroll slow)
├─ ParallaxLayer (far mountains, scroll slow)
├─ ParallaxLayer (mid buildings, scroll medium)
├─ ParallaxLayer (mid buildings, scroll medium)
└─ ParallaxLayer (near trees, scroll fast)
└─ ParallaxLayer (near trees, scroll fast)
player.gd
player.gd
extends CharacterBody2D
func _ready() -> void:
var parallax := get_node("../ParallaxBackground")
parallax.scroll_base_scale = Vector2(0.5, 0.5) # Parallax strength
undefinedextends CharacterBody2D
func _ready() -> void:
var parallax := get_node("../ParallaxBackground")
parallax.scroll_base_scale = Vector2(0.5, 0.5) # Parallax strength
undefinedStrategy 3: Fixed Perspective (Isometric Stay)
策略3:固定视角(保持等距视角)
gdscript
undefinedgdscript
undefinedKeep isometric/dimetric view but use 2D physics
Keep isometric/dimetric view but use 2D physics
Use rotated sprites to simulate 3D angles
Use rotated sprites to simulate 3D angles
const ISO_ANGLE := deg_to_rad(-30) # Isometric tilt
func world_to_iso(pos: Vector2) -> Vector2:
return Vector2(
pos.x - pos.y,
(pos.x + pos.y) * 0.5
)
func iso_to_world(iso_pos: Vector2) -> Vector2:
return Vector2(
(iso_pos.x + iso_pos.y * 2) * 0.5,
(iso_pos.y * 2 - iso_pos.x) * 0.5
)
---const ISO_ANGLE := deg_to_rad(-30) # Isometric tilt
func world_to_iso(pos: Vector2) -> Vector2:
return Vector2(
pos.x - pos.y,
(pos.x + pos.y) * 0.5
)
func iso_to_world(iso_pos: Vector2) -> Vector2:
return Vector2(
(iso_pos.x + iso_pos.y * 2) * 0.5,
(iso_pos.y * 2 - iso_pos.x) * 0.5
)
---Node Conversion
节点转换
Physics Bodies
物理刚体
gdscript
undefinedgdscript
undefinedCharacterBody3D → CharacterBody2D
CharacterBody3D → CharacterBody2D
extends CharacterBody3D # Before
const SPEED = 5.0
const JUMP_VELOCITY = 4.5
const GRAVITY = 9.8
func _physics_process(delta: float) -> void:
velocity.y -= GRAVITY * delta
var input := Input.get_vector("left", "right", "forward", "back")
velocity.x = input.x * SPEED
velocity.z = input.y * SPEED
move_and_slide()
extends CharacterBody3D # Before
const SPEED = 5.0
const JUMP_VELOCITY = 4.5
const GRAVITY = 9.8
func _physics_process(delta: float) -> void:
velocity.y -= GRAVITY * delta
var input := Input.get_vector("left", "right", "forward", "back")
velocity.x = input.x * SPEED
velocity.z = input.y * SPEED
move_and_slide()
⬇️ Convert to:
⬇️ Convert to:
extends CharacterBody2D # After
const SPEED = 300.0
const JUMP_VELOCITY = -400.0
const GRAVITY = 980.0 # Pixels per second squared
func _physics_process(delta: float) -> void:
velocity.y += GRAVITY * delta
var input := Input.get_vector("left", "right", "up", "down")
velocity.x = input.x * SPEED
# Note: No Z-axis. For platformer, use input.y for jump
move_and_slide()
undefinedextends CharacterBody2D # After
const SPEED = 300.0
const JUMP_VELOCITY = -400.0
const GRAVITY = 980.0 # Pixels per second squared
func _physics_process(delta: float) -> void:
velocity.y += GRAVITY * delta
var input := Input.get_vector("left", "right", "up", "down")
velocity.x = input.x * SPEED
# Note: No Z-axis. For platformer, use input.y for jump
move_and_slide()
undefinedCamera Conversion
相机转换
gdscript
undefinedgdscript
undefinedCamera3D → Camera2D
Camera3D → Camera2D
Before: Third-person 3D camera
Before: Third-person 3D camera
extends SpringArm3D
@onready var camera: Camera3D = $Camera3D
func _process(delta: float) -> void:
spring_length = 10.0
rotate_y(Input.get_axis("cam_left", "cam_right") * delta)
extends SpringArm3D
@onready var camera: Camera3D = $Camera3D
func _process(delta: float) -> void:
spring_length = 10.0
rotate_y(Input.get_axis("cam_left", "cam_right") * delta)
⬇️ Convert to:
⬇️ Convert to:
extends Camera2D # After
@onready var player: CharacterBody2D = $"../Player"
func _process(delta: float) -> void:
global_position = player.global_position
zoom = Vector2(2.0, 2.0) # Adjust to taste
---extends Camera2D # After
@onready var player: CharacterBody2D = $"../Player"
func _process(delta: float) -> void:
global_position = player.global_position
zoom = Vector2(2.0, 2.0) # Adjust to taste
---Art Pipeline: 3D Models → Sprites
美术工作流:3D模型 → 精灵图
Option 1: Render Sprites from 3D (Automation)
选项1:从3D渲染精灵图(自动化)
gdscript
undefinedgdscript
undefinedUse Godot to render 3D model from fixed angles
Use Godot to render 3D model from fixed angles
sprite_renderer.gd (tool script)
sprite_renderer.gd (tool script)
@tool
extends Node3D
@export var model_path: String = "res://models/character.glb"
@export var output_dir: String = "res://sprites/"
@export var angles: int = 8 # 8-directional sprites
@export var render: bool = false:
set(value):
if value:
render_sprites()
func render_sprites() -> void:
var model := load(model_path).instantiate()
add_child(model)
var camera := Camera3D.new()
camera.position = Vector3(0, 2, 5)
camera.look_at(Vector3.ZERO)
add_child(camera)
var viewport := SubViewport.new()
viewport.size = Vector2i(256, 256)
viewport.transparent_bg = true
viewport.add_child(camera)
add_child(viewport)
for i in range(angles):
model.rotation.y = (TAU / angles) * i
await RenderingServer.frame_post_draw
var img := viewport.get_texture().get_image()
img.save_png("%s/sprite_%d.png" % [output_dir, i])
model.queue_free()
camera.queue_free()
viewport.queue_free()undefined@tool
extends Node3D
@export var model_path: String = "res://models/character.glb"
@export var output_dir: String = "res://sprites/"
@export var angles: int = 8 # 8-directional sprites
@export var render: bool = false:
set(value):
if value:
render_sprites()
func render_sprites() -> void:
var model := load(model_path).instantiate()
add_child(model)
var camera := Camera3D.new()
camera.position = Vector3(0, 2, 5)
camera.look_at(Vector3.ZERO)
add_child(camera)
var viewport := SubViewport.new()
viewport.size = Vector2i(256, 256)
viewport.transparent_bg = true
viewport.add_child(camera)
add_child(viewport)
for i in range(angles):
model.rotation.y = (TAU / angles) * i
await RenderingServer.frame_post_draw
var img := viewport.get_texture().get_image()
img.save_png("%s/sprite_%d.png" % [output_dir, i])
model.queue_free()
camera.queue_free()
viewport.queue_free()undefinedOption 2: Manual Export (Blender)
选项2:手动导出(Blender)
python
undefinedpython
undefinedBlender Python script (run in Blender)
Blender Python script (run in Blender)
import bpy
import math
angles = 8
output_dir = "/path/to/sprites/"
model = bpy.data.objects["Character"]
for i in range(angles):
model.rotation_euler.z = (2 * math.pi / angles) * i
bpy.ops.render.render(write_still=True)
bpy.data.images['Render Result'].save_render(
filepath=f"{output_dir}/sprite_{i}.png"
)
undefinedimport bpy
import math
angles = 8
output_dir = "/path/to/sprites/"
model = bpy.data.objects["Character"]
for i in range(angles):
model.rotation_euler.z = (2 * math.pi / angles) * i
bpy.ops.render.render(write_still=True)
bpy.data.images['Render Result'].save_render(
filepath=f"{output_dir}/sprite_{i}.png"
)
undefinedOption 3: Use Sprite3D as Reference
选项3:使用Sprite3D作为参考
gdscript
undefinedgdscript
undefinedKeep 3D model in editor, export frame-by-frame
Keep 3D model in editor, export frame-by-frame
---
---Physics Adjustments
物理系统调整
Gravity Scaling
重力缩放
gdscript
undefinedgdscript
undefined3D gravity (m/s²): 9.8
3D gravity (m/s²): 9.8
2D gravity (pixels/s²): Scale to pixel units
2D gravity (pixels/s²): Scale to pixel units
If 1 meter = 100 pixels:
If 1 meter = 100 pixels:
const GRAVITY_2D = 9.8 * 100 # = 980 pixels/s²
const GRAVITY_2D = 9.8 * 100 # = 980 pixels/s²
Adjust jump velocity proportionally:
Adjust jump velocity proportionally:
3D jump: 4.5 m/s
3D jump: 4.5 m/s
2D jump: -450 pixels/s
2D jump: -450 pixels/s
undefinedundefinedCollision Simplification
碰撞简化
gdscript
undefinedgdscript
undefined3D: CapsuleShape3D (16 segments, expensive)
3D: CapsuleShape3D (16 segments, expensive)
var shape_3d := CapsuleShape3D.new()
shape_3d.radius = 0.5
shape_3d.height = 2.0
var shape_3d := CapsuleShape3D.new()
shape_3d.radius = 0.5
shape_3d.height = 2.0
2D: CapsuleShape2D (much simpler)
2D: CapsuleShape2D (much simpler)
var shape_2d := CapsuleShape2D.new()
shape_2d.radius = 16 # pixels
shape_2d.height = 64
---var shape_2d := CapsuleShape2D.new()
shape_2d.radius = 16 # pixels
shape_2d.height = 64
---Control Simplification
控制简化
3D Free Movement → 2D Restricted
3D自由移动 → 2D受限移动
gdscript
undefinedgdscript
undefined3D: Full 3D movement with camera-relative controls
3D: Full 3D movement with camera-relative controls
var input_3d := Input.get_vector("left", "right", "forward", "back")
var camera_basis := camera.global_transform.basis
var direction := (camera_basis * Vector3(input_3d.x, 0, input_3d.y)).normalized()
var input_3d := Input.get_vector("left", "right", "forward", "back")
var camera_basis := camera.global_transform.basis
var direction := (camera_basis * Vector3(input_3d.x, 0, input_3d.y)).normalized()
2D: Simple 4-direction (or 8-direction with diagonals)
2D: Simple 4-direction (or 8-direction with diagonals)
var input_2d := Input.get_vector("left", "right", "up", "down")
velocity = input_2d.normalized() * SPEED
---var input_2d := Input.get_vector("left", "right", "up", "down")
velocity = input_2d.normalized() * SPEED
---Performance Gains
性能提升
Expected Improvements
预期改进
| Metric | 3D | 2D | Improvement |
|---|---|---|---|
| Draw calls | 100 | 20 | 5x |
| GPU load | High | Low | 10x |
| Battery life (mobile) | 1 hour | 5 hours | 5x |
| RAM usage | 500MB | 100MB | 5x |
| 指标 | 3D | 2D | 提升幅度 |
|---|---|---|---|
| 绘制调用 | 100 | 20 | 5倍 |
| GPU负载 | 高 | 低 | 10倍 |
| 移动设备续航 | 1小时 | 5小时 | 5倍 |
| RAM占用 | 500MB | 100MB | 5倍 |
Optimization Techniques
优化技巧
gdscript
undefinedgdscript
undefined1. Use TileMapLayer instead of individual Sprite2D nodes
1. Use TileMapLayer instead of individual Sprite2D nodes
var tilemap := TileMapLayer.new()
tilemap.tile_set = load("res://tileset.tres")
var tilemap := TileMapLayer.new()
tilemap.tile_set = load("res://tileset.tres")
2. Batch sprite rendering
2. Batch sprite rendering
Use single large sprite sheet instead of individual textures
Use single large sprite sheet instead of individual textures
3. Reduce particle count
3. Reduce particle count
var godot-particles := GPUParticles2D.new()
godot-particles.amount = 50 # Down from 200 in 3D
---var godot-particles := GPUParticles2D.new()
godot-particles.amount = 50 # Down from 200 in 3D
---UI Adaptation
UI适配
gdscript
undefinedgdscript
undefinedMost 3D games already use 2D UI (CanvasLayer)
Most 3D games already use 2D UI (CanvasLayer)
No changes needed!
No changes needed!
Just verify UI scaling for new aspect ratios
Just verify UI scaling for new aspect ratios
get_viewport().size_changed.connect(_on_viewport_resized)
func _on_viewport_resized() -> void:
var viewport_size := get_viewport().get_visible_rect().size
# Adjust UI anchors/margins
---get_viewport().size_changed.connect(_on_viewport_resized)
func _on_viewport_resized() -> void:
var viewport_size := get_viewport().get_visible_rect().size
# Adjust UI anchors/margins
---Edge Cases
边缘情况
Depth Sorting
深度排序
gdscript
undefinedgdscript
undefinedProblem: Overlapping sprites need sorting
Problem: Overlapping sprites need sorting
Solution: Use Y-sort or z_index
Solution: Use Y-sort or z_index
extends Sprite2D
func _ready() -> void:
y_sort_enabled = true # Auto-sort by Y position
# Or set z_index manually:
z_index = int(global_position.y)
undefinedextends Sprite2D
func _ready() -> void:
y_sort_enabled = true # Auto-sort by Y position
# Or set z_index manually:
z_index = int(global_position.y)
undefinedLost Spatial Audio
丢失的空间音频
gdscript
undefinedgdscript
undefined3D spatial audio (AudioStreamPlayer3D) → 2D panning (AudioStreamPlayer2D)
3D spatial audio (AudioStreamPlayer3D) → 2D panning (AudioStreamPlayer2D)
var audio_2d := AudioStreamPlayer2D.new()
audio_2d.stream = load("res://sounds/footstep.ogg")
audio_2d.max_distance = 1000.0 # 2D range
audio_2d.attenuation = 2.0
add_child(audio_2d)
---var audio_2d := AudioStreamPlayer2D.new()
audio_2d.stream = load("res://sounds/footstep.ogg")
audio_2d.max_distance = 1000.0 # 2D range
audio_2d.attenuation = 2.0
add_child(audio_2d)
---Decision Tree: When to Simplify to 2D
决策树:何时简化为2D
| Factor | Keep 3D | Go 2D |
|---|---|---|
| Target platform | Desktop, console | Mobile, web |
| Art style | Realistic, immersive | Stylized, retro |
| Gameplay | Requires 3D space | Works in 2D plane |
| Performance | Have GPU budget | Need 60 FPS on low-end |
| Team skills | 3D artists | 2D artists or pixel art |
| 因素 | 保留3D | 转2D |
|---|---|---|
| 目标平台 | 桌面、主机 | 移动、网页 |
| 美术风格 | 写实、沉浸式 | 风格化、复古 |
| 游戏玩法 | 需要3D空间 | 可在2D平面实现 |
| 性能需求 | 有足够GPU预算 | 需要在低端设备上跑60帧 |
| 团队技能 | 有3D美术 | 有2D美术或像素画能力 |
Reference
参考
- Master Skill: godot-master
- 核心技能:godot-master