Skill v1.0.1
currentAutomated scan100/10015 files
version: "1.0.1" name: godot-adapt-3d-to-2d description: "Expert patterns for simplifying 3D games to 2D including dimension reduction strategies, camera flattening, physics conversion, 3D-to-sprite art pipeline, and control simplification. Use when porting 3D to 2D, creating 2D versions for mobile, or prototyping. Trigger keywords: CharacterBody3D to CharacterBody2D, Camera3D to Camera2D, Vector3 to Vector2, flatten Z-axis, orthogonal projection, 3D to sprite conversion, performance optimization."
Adapt: 3D to 2D
Expert guidance for simplifying 3D games into 2D (or 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.
Available Scripts
MANDATORY: Read the appropriate script before implementing the corresponding pattern.
ortho_simulation.gd
Simulates 3D Z-axis height in 2D top-down games. Handles vertical velocity, gravity, sprite offset, and shadow scaling.
projection_utils.gd
Projects 3D world positions to 2D screen space for nameplates, healthbars, and targeting. Handles behind-camera detection and distance-based scaling.
isometric_math_core.gd
Expert utility generating translating between 2D Cartesian and True Isometric screenspace projection matrices without using 2D Node transforms.
depth_sorting_y_sort.gd
Expert dynamic Z-index Y-Sort script for fake 3D sorting isolated trees matching CanvasItem _update_sorting().
jump_z_axis_sim.gd
Complete CharacterBody2D snippet separating structural physical ground movement (X,Y) from a mathematically simulated jumping height (Z) in a topdown game.
parallax_depth_camera.gd
Fake Depth Camera applying varying offset algorithms to completely disparate CanvasLayers based on an index to simulate 3D camera translation panning.
hitbox_depth_manager.gd
Area2D derived class that requires explicit custom Z-height overlap (1D AABB collision) prior to validating 2D triggers to stop incorrect "ground vs air" collision in 2.5D.
fake_3d_shadows.gd
Sprite2D shadow simulator exploiting Godot 4.x Transform2D matrix skew shear to project and angle shadows away from a simulated 3D sun direction on a 2D floor.
billboard_sprite_manager.gd
8-directional FPS Doom-style sprite controller isolating the simulated 3D relative angle between a moving 2D CharacterBody and a Camera2D viewpoint.
nav_region_flattening.gd
Topdown 2D pathfinding workaround allowing "aerial" units to cross walls by leveraging multiple tiered 2D Navigation Layers instead of proper 3D verticality.
ortho_to_perspective_fx.gd
Screen space CanvasItem warp Shader simulating a Mode 7 / tabletop perspective pitch. Maps top screen coordinates via division pinching.
2d_lighting_normals.gd
Automatic programmatic generation of CanvasTexture combining base albedo and baked normal maps at runtime so Sprites correctly react to 2D PointLIGHTs like 3D geometry.
Why Go from 3D to 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 |
Dimension Reduction Strategies
Strategy 1: True 2D (Remove Z-axis)
# Top-down or side-view# Example: 3D isometric → 2D top-down# Before (3D):var velocity := Vector3(input.x, 0, input.y) * speed# After (2D):var velocity := Vector2(input.x, input.y) * speed# Use case: Top-down shooters, RTS, turn-based strategy
Strategy 2: 2.5D (Fake depth with layers)
# Keep visual depth perception without Z-axis gameplay# Use ParallaxBackground for depth layers# Scene structure:# ParallaxBackground# ├─ ParallaxLayer (far mountains, scroll slow)# ├─ ParallaxLayer (mid buildings, scroll medium)# └─ ParallaxLayer (near trees, scroll fast)# player.gdextends CharacterBody2Dfunc _ready() -> void:var parallax := get_node("../ParallaxBackground")parallax.scroll_base_scale = Vector2(0.5, 0.5) # Parallax strength
Strategy 3: Fixed Perspective (Isometric Stay)
# Keep isometric/dimetric view but use 2D physics# Use rotated sprites to simulate 3D anglesconst ISO_ANGLE := deg_to_rad(-30) # Isometric tiltfunc 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
# CharacterBody3D → CharacterBody2Dextends CharacterBody3D # Beforeconst SPEED = 5.0const JUMP_VELOCITY = 4.5const GRAVITY = 9.8func _physics_process(delta: float) -> void:velocity.y -= GRAVITY * deltavar input := Input.get_vector("left", "right", "forward", "back")velocity.x = input.x * SPEEDvelocity.z = input.y * SPEEDmove_and_slide()# ⬇️ Convert to:extends CharacterBody2D # Afterconst SPEED = 300.0const JUMP_VELOCITY = -400.0const GRAVITY = 980.0 # Pixels per second squaredfunc _physics_process(delta: float) -> void:velocity.y += GRAVITY * deltavar input := Input.get_vector("left", "right", "up", "down")velocity.x = input.x * SPEED# Note: No Z-axis. For platformer, use input.y for jumpmove_and_slide()
Camera Conversion
# Camera3D → Camera2D# Before: Third-person 3D cameraextends SpringArm3D@onready var camera: Camera3D = $Camera3Dfunc _process(delta: float) -> void:spring_length = 10.0rotate_y(Input.get_axis("cam_left", "cam_right") * delta)# ⬇️ Convert to:extends Camera2D # After@onready var player: CharacterBody2D = $"../Player"func _process(delta: float) -> void:global_position = player.global_positionzoom = Vector2(2.0, 2.0) # Adjust to taste
Art Pipeline: 3D Models → Sprites
Option 1: Render Sprites from 3D (Automation)
# Use Godot to render 3D model from fixed angles# sprite_renderer.gd (tool script)@toolextends 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 = trueviewport.add_child(camera)add_child(viewport)for i in range(angles):model.rotation.y = (TAU / angles) * iawait RenderingServer.frame_post_drawvar 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()
Option 2: Manual Export (Blender)
# Blender Python script (run in Blender)import bpyimport mathangles = 8output_dir = "/path/to/sprites/"model = bpy.data.objects["Character"]for i in range(angles):model.rotation_euler.z = (2 * math.pi / angles) * ibpy.ops.render.render(write_still=True)bpy.data.images['Render Result'].save_render(filepath=f"{output_dir}/sprite_{i}.png")
Option 3: Use Sprite3D as Reference
# Keep 3D model in editor, export frame-by-frame
Physics Adjustments
Gravity Scaling
# 3D gravity (m/s²): 9.8# 2D gravity (pixels/s²): Scale to pixel units# If 1 meter = 100 pixels:const GRAVITY_2D = 9.8 * 100 # = 980 pixels/s²# Adjust jump velocity proportionally:# 3D jump: 4.5 m/s# 2D jump: -450 pixels/s
Collision Simplification
# 3D: CapsuleShape3D (16 segments, expensive)var shape_3d := CapsuleShape3D.new()shape_3d.radius = 0.5shape_3d.height = 2.0# 2D: CapsuleShape2D (much simpler)var shape_2d := CapsuleShape2D.new()shape_2d.radius = 16 # pixelsshape_2d.height = 64
Control Simplification
3D Free Movement → 2D Restricted
# 3D: Full 3D movement with camera-relative controlsvar input_3d := Input.get_vector("left", "right", "forward", "back")var camera_basis := camera.global_transform.basisvar direction := (camera_basis * Vector3(input_3d.x, 0, input_3d.y)).normalized()# 2D: Simple 4-direction (or 8-direction with diagonals)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 |
Optimization Techniques
# 1. Use TileMapLayer instead of individual Sprite2D nodesvar tilemap := TileMapLayer.new()tilemap.tile_set = load("res://tileset.tres")# 2. Batch sprite rendering# Use single large sprite sheet instead of individual textures# 3. Reduce particle countvar godot-particles := GPUParticles2D.new()godot-particles.amount = 50 # Down from 200 in 3D
UI Adaptation
# Most 3D games already use 2D UI (CanvasLayer)# No changes needed!# Just verify UI scaling for new aspect ratiosget_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
# Problem: Overlapping sprites need sorting# Solution: Use Y-sort or z_indexextends Sprite2Dfunc _ready() -> void:y_sort_enabled = true # Auto-sort by Y position# Or set z_index manually:z_index = int(global_position.y)
Lost Spatial Audio
# 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 rangeaudio_2d.attenuation = 2.0add_child(audio_2d)
Decision Tree: When to Simplify to 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 |
Expert Techniques & Optimizations
1. Flattened Navigation (3D Navmesh to 2D Grid)
While Godot treats 2D and 3D navigation as separate systems, you can project 3D pathfinding logic onto a 2D grid using AStarGrid2D. This is highly optimized for 2D grid-based movement and avoids the overhead of a full 3D navmesh.
class_name GridNavBridge extends Nodevar astar_grid: AStarGrid2Dfunc _ready() -> void:astar_grid = AStarGrid2D.new()astar_grid.region = Rect2i(0, 0, 100, 100)astar_grid.cell_size = Vector2(16, 16)astar_grid.update()## Converts a 3D target position to a 2D grid path.func get_grid_path_from_3d(start_3d: Vector3, end_3d: Vector3) -> PackedVector2Array:var start_map := Vector2i(start_3d.x / 16, start_3d.z / 16)var end_map := Vector2i(end_3d.x / 16, end_3d.z / 16)return astar_grid.get_point_path(start_map, end_map)
2. Auto-LOD for 2D (Performance Optimization)
Automatic LOD is natively a 3D feature, but you can simulate it in 2D using VisibleOnScreenEnabler2D. This node automatically toggles the process_mode of target nodes (like high-res sprites or complex AI) when they leave the screen, preserving CPU cycles and GPU fill rate.
# Attach to a complex 2D entityfunc setup_2d_lod(target_node: Node2D) -> void:var enabler := VisibleOnScreenEnabler2D.new()# Define the 'high-detail' rectenabler.rect = Rect2(-64, -64, 128, 128)enabler.enable_node_path = target_node.get_path()add_child(enabler)
3. Dimensional Patcher (CharacterBody3D to 2D Regex)
To automate the down-porting of 3D controllers, use a RegEx script to map Vector3 to Vector2 and replace 3D-specific properties. This is essential for massive porting tasks where manual conversion of movement logic is prone to error.
@toolextends EditorScriptfunc _run() -> void:var regex = RegEx.new()# Pattern to find Vector3 constructors and replace with Vector2regex.compile("Vector3\\(([^,]+),\\s*([^,]+),\\s*([^)]+)\\)")var script_content = "velocity = Vector3(input.x, 0.0, input.y) * speed"var result = regex.sub(script_content, "Vector2($1, $3)", true)# Output: "velocity = Vector2(input.x, input.y) * speed"print(result)
Reference
- Master Skill: godot-master