godot-adapt-3d-to-2d

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Adapt: 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?

ReasonBenefit
Mobile performance5-10x faster on low-end devices
Simpler art pipelineSprites easier to create than 3D models
Faster iteration2D level design is quicker
AccessibilityLower hardware requirements
ClarityReduce visual clutter for puzzle/strategy games

原因优势
移动平台性能在低端设备上运行速度提升5-10倍
更简单的美术工作流制作精灵图比3D模型更容易
更快的迭代速度2D关卡设计耗时更短
更低的硬件门槛降低设备配置要求
更清晰的视觉表现减少解谜/策略游戏中的视觉杂乱

Dimension Reduction Strategies

降维策略

Strategy 1: True 2D (Remove Z-axis)

策略1:纯2D(移除Z轴)

gdscript
undefined
gdscript
undefined

Top-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

undefined
undefined

Strategy 2: 2.5D (Fake depth with layers)

策略2:2.5D(用分层模拟深度)

gdscript
undefined
gdscript
undefined

Keep 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
undefined
extends CharacterBody2D
func _ready() -> void: var parallax := get_node("../ParallaxBackground") parallax.scroll_base_scale = Vector2(0.5, 0.5) # Parallax strength
undefined

Strategy 3: Fixed Perspective (Isometric Stay)

策略3:固定视角(保持等距视角)

gdscript
undefined
gdscript
undefined

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

CharacterBody3D → 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()
undefined
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()
undefined

Camera Conversion

相机转换

gdscript
undefined
gdscript
undefined

Camera3D → 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
undefined
gdscript
undefined

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

Option 2: Manual Export (Blender)

选项2:手动导出(Blender)

python
undefined
python
undefined

Blender 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" )
undefined
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" )
undefined

Option 3: Use Sprite3D as Reference

选项3:使用Sprite3D作为参考

gdscript
undefined
gdscript
undefined

Keep 3D model in editor, export frame-by-frame

Keep 3D model in editor, export frame-by-frame


---

---

Physics Adjustments

物理系统调整

Gravity Scaling

重力缩放

gdscript
undefined
gdscript
undefined

3D 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

undefined
undefined

Collision Simplification

碰撞简化

gdscript
undefined
gdscript
undefined

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

3D: 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

预期改进

Metric3D2DImprovement
Draw calls100205x
GPU loadHighLow10x
Battery life (mobile)1 hour5 hours5x
RAM usage500MB100MB5x
指标3D2D提升幅度
绘制调用100205倍
GPU负载10倍
移动设备续航1小时5小时5倍
RAM占用500MB100MB5倍

Optimization Techniques

优化技巧

gdscript
undefined
gdscript
undefined

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

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

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

Lost Spatial Audio

丢失的空间音频

gdscript
undefined
gdscript
undefined

3D 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

FactorKeep 3DGo 2D
Target platformDesktop, consoleMobile, web
Art styleRealistic, immersiveStylized, retro
GameplayRequires 3D spaceWorks in 2D plane
PerformanceHave GPU budgetNeed 60 FPS on low-end
Team skills3D artists2D artists or pixel art
因素保留3D转2D
目标平台桌面、主机移动、网页
美术风格写实、沉浸式风格化、复古
游戏玩法需要3D空间可在2D平面实现
性能需求有足够GPU预算需要在低端设备上跑60帧
团队技能有3D美术有2D美术或像素画能力

Reference

参考

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