
Godot Adapt 3d To 2d
- 154 installs
- 454 repo stars
- Updated July 28, 2026
- thedivergentai/gd-agentic-skills
Use godot-adapt-3d-to-2d for development tasks
About
godot-adapt-3d-to-2d: A skill for development. This provides functionality for development workflows.
- godot-adapt-3d-to-2d
Godot Adapt 3d To 2d by the numbers
- 154 all-time installs (skills.sh)
- +16 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #2,466 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/thedivergentai/gd-agentic-skills --skill godot-adapt-3d-to-2dAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 154 |
|---|---|
| repo stars | ★ 454 |
| Last updated | July 28, 2026 |
| Repository | thedivergentai/gd-agentic-skills ↗ |
What it does
Use godot-adapt-3d-to-2d for development tasks
Files
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 strategyStrategy 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.gd
extends CharacterBody2D
func _ready() -> void:
var parallax := get_node("../ParallaxBackground")
parallax.scroll_base_scale = Vector2(0.5, 0.5) # Parallax strengthStrategy 3: Fixed Perspective (Isometric Stay)
# Keep isometric/dimetric view but use 2D physics
# 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
)---
Node Conversion
Physics Bodies
# 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()
# ⬇️ 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()Camera Conversion
# Camera3D → Camera2D
# 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)
# ⬇️ 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---
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)
@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()Option 2: Manual Export (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"
)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/sCollision Simplification
# 3D: CapsuleShape3D (16 segments, expensive)
var shape_3d := CapsuleShape3D.new()
shape_3d.radius = 0.5
shape_3d.height = 2.0
# 2D: CapsuleShape2D (much simpler)
var shape_2d := CapsuleShape2D.new()
shape_2d.radius = 16 # pixels
shape_2d.height = 64---
Control Simplification
3D Free Movement → 2D Restricted
# 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()
# 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 nodes
var 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 count
var 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 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---
Edge Cases
Depth Sorting
# Problem: Overlapping sprites need sorting
# 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)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 range
audio_2d.attenuation = 2.0
add_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 Node
var astar_grid: AStarGrid2D
func _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 entity
func setup_2d_lod(target_node: Node2D) -> void:
var enabler := VisibleOnScreenEnabler2D.new()
# Define the 'high-detail' rect
enabler.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.
@tool
extends EditorScript
func _run() -> void:
var regex = RegEx.new()
# Pattern to find Vector3 constructors and replace with Vector2
regex.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
# 2d_lighting_normals.gd
extends Sprite2D
class_name LitSprite2D
## Expert Script for generating 3D-like lighting on 2D sprites
## In 3D, StandardMaterial handles normal maps reacting to lights.
## In 2D, we must construct a CanvasTexture via code if we are generating assets procedurally,
## or properly assign it in the editor.
@export var albedo_texture: Texture2D
@export var normal_texture: Texture2D # Generated via tools like SpriteIlluminator
func _ready() -> void:
if albedo_texture == null or normal_texture == null:
push_warning("LitSprite2D requires both albedo and normal textures.")
return
var canvas_texture = CanvasTexture.new()
# Base color texture
canvas_texture.diffuse_texture = albedo_texture
# The normal map calculates angles for the 2D PointLight
canvas_texture.normal_texture = normal_texture
# Optional specular map to give 2D materials specific shininess
# canvas_texture.specular_texture = load("...")
# canvas_texture.specular_color = Color(1.0, 1.0, 1.0)
# canvas_texture.specular_shininess = 20.0
# Apply to the Sprite2D node
texture = canvas_texture
# adapt_3d_to_2d_patterns.gd
extends Node
# 1. Perspective-to-Orthographic Transition
# EXPERT NOTE: Smoothly transition between 3D perspective and a flat 2D look.
func set_ortho_camera(camera: Camera3D, size: float) -> void:
camera.projection = Camera3D.PROJECTION_ORTHOGONAL
camera.size = size
# 2. Rendering 3D Models as 2D Sprites (SubViewport)
# EXPERT NOTE: The "Donkey Kong Country" style. Render 3D at runtime for dynamic 2D.
func capture_3d_to_sprite(viewport: SubViewport, target: Sprite2D) -> void:
target.texture = viewport.get_texture()
# 3. Y-Sort Simulation for 3D-in-2D
# EXPERT NOTE: Use Z-Index or sorting_offset to mimic 2D Y-Sorting depth.
func apply_pseudo_y_sort(node: Node2D) -> void:
node.z_index = int(node.global_position.y)
# 4. Isometric 3D Math for 2D Placement
# EXPERT NOTE: Calculate 2D screen positions based on a "fake" 3D coordinate system.
func cartesian_to_isometric(vec: Vector2) -> Vector2:
return Vector2(vec.x - vec.y, (vec.x + vec.y) / 2)
# 5. Parallax for 3D Backgrounds in 2D
# EXPERT NOTE: Moving a 3D camera slowly mimics a massive distant world.
func setup_3d_parallax(camera: Camera3D, player_pos: Vector2) -> void:
camera.position.x = player_pos.x * 0.1
camera.position.z = player_pos.y * 0.1
# 6. Hybrid Collision (2D Physics / 3D Graphics)
# EXPERT NOTE: Use Area2D for logic while a 3D model follows the 2D body.
func sync_3d_mesh_to_2d_body(mesh: Node3D, body: CharacterBody2D) -> void:
mesh.global_position = Vector3(body.global_position.x, 0, body.global_position.y)
# 7. Disabling 3D Lighting for Unlit 2D Look
# EXPERT NOTE: Force 3D models to look flat using unshaded materials.
func flat_shade_mesh(mesh: MeshInstance3D) -> void:
var mat := mesh.get_active_material(0) as StandardMaterial3D
if mat:
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
# 8. Handling 3D Depth Pre-Pass in 2D Viewports
# EXPERT NOTE: Ensures correct layering when multiple 3D viewports overlap 2D.
func setup_depth_sorting(viewport: SubViewport) -> void:
viewport.transparent_bg = true
viewport.gui_disable_input = true # Pass clicks to 2D UI below
# 9. Normal Mapping 2D with 3D Lights
# EXPERT NOTE: Use 3D DirectionalLight3D to cast 2D shadows via Light2D.
func setup_hybrid_lighting() -> void:
# Requires complex CanvasItem shader logic
pass
# 10. Frame-Rate Locking for "Retro" 3D
# EXPERT NOTE: Drop 3D render rate to 12fps/15fps to match stop-motion 2D art.
func set_low_fidelity_3d_timer(viewport: SubViewport) -> void:
viewport.render_target_update_mode = SubViewport.UPDATE_WHEN_VISIBLE
extends CharacterBody2D
class_name BillboardSpriteManager
## Expert 2D Sprite Animator simulating 3D Camera Angles
## Used in "Doom-style" FPS or 2.5D games.
## Chooses the correct animation frame based on viewing angle between actor and camera.
@export var sprite: AnimatedSprite2D
@export var simulated_camera: Camera2D
func _process(_delta: float) -> void:
_update_facing_angle()
func _update_facing_angle() -> void:
# Vector from us to camera
var dir_to_cam = global_position.direction_to(simulated_camera.global_position)
# The direction the character is actually looking/moving
var facing_dir = velocity.normalized() if velocity.length() > 0 else Vector2.RIGHT
# Get angle between facing direction and camera view
var angle_diff = facing_dir.angle_to(dir_to_cam)
# Map from -PI to PI into 8 discrete indices (0-7)
var octant = int(snapped(rad_to_deg(angle_diff) + 180, 45) / 45) % 8
var animation_names = [
"idle_front", "idle_front_right", "idle_right", "idle_back_right",
"idle_back", "idle_back_left", "idle_left", "idle_front_left"
]
sprite.play(animation_names[octant])
extends Node2D
## Expert Z-Index management for 2.5D games
## Instead of relying purely on Y-Sort (which groups by Node hierarchy),
## this script dynamically assigns absolute Z-Index values based on the global Y position,
## allowing completely disconnected trees (particles, UI, actors) to sort together.
@export var depth_offset: float = 0.0
@export var is_static: bool = false
func _ready() -> void:
if is_static:
_update_sorting()
set_process(false)
func _process(_delta: float) -> void:
_update_sorting()
func _update_sorting() -> void:
# Z-index is clamped in Godot to [-4096, 4096].
# If the world is larger, you must use CanvasLayer or scaled division.
var calculated_z = int(global_position.y + depth_offset)
z_index = clampi(calculated_z, RenderingServer.CANVAS_ITEM_Z_MIN, RenderingServer.CANVAS_ITEM_Z_MAX)
class_name DirectionalSpriteResolver
extends Sprite3D
## Expert Directional Sprite Resolver (Godot 4.6).
## Calculates the 8-way (Doom/Octopath style) frame index based on camera view.
@export var camera: Camera3D
@export var character: Node3D
func _process(_delta: float) -> void:
if not camera or not character: return
# 1. Extract Forward Vectors (-Z is forward in Godot)
var cam_forward: Vector3 = -camera.global_transform.basis.z
var char_forward: Vector3 = -character.global_transform.basis.z
# 2. Get Signed Angle around UP axis
var angle: float = char_forward.signed_angle_to(cam_forward, Vector3.UP)
# 3. Map Radians (-PI to PI) to 8 Slices (45 degrees each)
var direction_index: int = int(round(angle / (PI / 4.0)))
if direction_index < 0:
direction_index += 8
# 4. Set Frame (Ensure your sprite sheet is ordered 0-7 for N, NE, E, SE, etc.)
frame = direction_index
## [SKILL NOTICE]: Use 'signed_angle_to' with Vector3.UP to accurately map
## character orientation relative to the camera for 2.5D directional sprites.
extends Sprite2D
class_name Fake3DShadow2D
## Simulates a dynamic 3D shadow projected onto a 2D floor
## using matrix skewing (shear) based on a simulated sun position.
@export var sun_direction: Vector2 = Vector2(-0.5, 0.2)
@export var shadow_length: float = 1.5
@export var shadow_color: Color = Color(0, 0, 0, 0.4)
func _ready() -> void:
# Ensure this shadow sits behind the original object
show_behind_parent = true
modulate = shadow_color
func _process(delta: float) -> void:
# In Godot 4.x, you skew a CanvasItem using its Transform2D
var t = Transform2D()
# 1. Scale down Y to simulate lying flat on the ground
t = t.scaled_local(Vector2(1.0, 0.3))
# 2. Shear (Skew) the matrix toward the sun direction
t.x.y = sun_direction.x * shadow_length
# 3. Apply the custom transform
transform = t
# Optional: Rotate the shadow entirely if the light source orbits
# rotation = sun_direction.angle()
extends Area2D
class_name SimulatedDepthArea2D
## Expert Hitbox Management in 2.5D
## Prevents a ground attack from hitting a jumping character by simulating Z-height intersection
@export var base_z_height: float = 0.0
@export var z_thickness: float = 20.0 # How "tall" the hitbox is
func _ready() -> void:
area_entered.connect(_on_area_entered)
func _on_area_entered(other_area: Area2D) -> void:
if other_area is SimulatedDepthArea2D:
if _check_z_overlap(other_area):
print("Valid 3D Hit! Simulated hit detected at Z-height.")
# Trigger combat logic...
else:
print("Missed! They were at different vertical heights!")
func _check_z_overlap(other: SimulatedDepthArea2D) -> bool:
# 1D AABB intersection math applied to our simulated vertical "Z" dimension
var my_top = base_z_height + z_thickness
var my_bottom = base_z_height
var other_top = other.base_z_height + other.z_thickness
var other_bottom = other.base_z_height
return my_top >= other_bottom and my_bottom <= other_top
## Call this to dynamically update height if attached to a jumping player
func update_current_z_height(new_z: float) -> void:
base_z_height = new_z
class_name IsoMath
extends RefCounted
## Expert utility for translating between 2D Cartesian and True Isometric screenspace.
## A true isometric projection uses a 2:1 ratio (width:height).
## This is faster than manipulating Node2D transforms and works perfectly for tilemaps and entity sorting.
const ISO_RATIO = 0.5
## Converts a 2D world position (e.g. CharacterBody2D global_position)
## into an Isometric screen position for rendering or sprite offsets.
static func cartesian_to_iso(cart_pos: Vector2) -> Vector2:
return Vector2(
cart_pos.x - cart_pos.y,
(cart_pos.x + cart_pos.y) * ISO_RATIO
)
## Converts an Isometric screen position (e.g. mouse click)
## back into the 2D Cartesian world position for pathfinding or logic.
static func iso_to_cartesian(iso_pos: Vector2) -> Vector2:
return Vector2(
(iso_pos.x / ISO_RATIO + iso_pos.y) / 2.0,
(iso_pos.y - iso_pos.x / ISO_RATIO) / 2.0
)
## Get the Z-index sorting value based on Cartesian Y (depth in 3D)
static func get_iso_z_index(cart_pos: Vector2) -> int:
# We multiply by 10 to ensure granular sorting for sub-pixel precise games
return int(cart_pos.y * 10.0)
extends CharacterBody2D
class_name PlatformerTopDown2D
## Expert Z-Axis Simulator for Top-Down 2D Games
## Simulates a 3rd dimension (Jumping in a top-down game like Zelda)
## Separates physical collision (X/Y on the ground) from the visual Sprite (Z height).
@export var move_speed: float = 200.0
@export var jump_force: float = 300.0
@export var gravity: float = 980.0
var z_height: float = 0.0
var z_velocity: float = 0.0
@onready var visual_root: Node2D = $VisualRoot # Holds the sprite
@onready var shadow: Sprite2D = $Shadow
func _physics_process(delta: float) -> void:
_handle_z_axis(delta)
_handle_movement(delta)
func _handle_z_axis(delta: float) -> void:
if z_height > 0 or z_velocity > 0:
z_velocity -= gravity * delta
z_height += z_velocity * delta
# Hit the ground
if z_height <= 0:
z_height = 0.0
z_velocity = 0.0
# Jump input
if Input.is_action_just_pressed("jump") and is_on_ground():
z_velocity = jump_force
# Apply to visuals (negative Y moves up on screen)
visual_root.position.y = -z_height
# Scale shadow based on height
var scale_amount = remap(z_height, 0.0, 100.0, 1.0, 0.5)
shadow.scale = Vector2(scale_amount, scale_amount)
shadow.modulate.a = remap(z_height, 0.0, 100.0, 1.0, 0.2)
func _handle_movement(delta: float) -> void:
var input = Input.get_vector("move_left", "move_right", "move_up", "move_down")
velocity = input * move_speed
move_and_slide()
func is_on_ground() -> bool:
return is_zero_approx(z_height)
# nav_region_flattening.gd
extends NavigationAgent2D
class_name NavRegionFlattener2D
## Expert 2D Pathfinding adapting 3D navigation logic.
## In 3D games, actors often navigate over varying terrain heights.
## In top-down 2D, we bake a flat NavigationPolygon but need to
## account for simulated "Z" height obstacles (walls, platforms)
## by manipulating navigation layers instead of Y-up vectors.
@export var is_flying: bool = false
@export var movement_speed: float = 150.0
@onready var character: CharacterBody2D = get_parent()
func _ready() -> void:
# A top-down 2D map doesn't have true verticality.
# To simulate flying over walls (a 3D concept), we change the 2D Nav layer.
# Ground units use navigation layer 1
# Flying units use layer 1 AND layer 2 (which spans over walls)
if is_flying:
set_navigation_layer_value(1, true)
set_navigation_layer_value(2, true)
else:
set_navigation_layer_value(1, true)
set_navigation_layer_value(2, false)
func seek_target(target_global_position: Vector2) -> void:
target_position = target_global_position
func _physics_process(delta: float) -> void:
if is_navigation_finished():
character.velocity = Vector2.ZERO
return
var current_agent_pos = global_position
var next_path_pos = get_next_path_position()
# Simple 2D steering that simulates reaching a specific 3D destination
var new_velocity = current_agent_pos.direction_to(next_path_pos) * movement_speed
# Optional: If your character has pseudo-3D height (jump_z_axis_sim.gd),
# you can check if they are "above" the target here before considering them "arrived"
character.velocity = new_velocity
character.move_and_slide()
# skills/adapt-3d-to-2d/scripts/ortho_simulation.gd
extends CharacterBody2D
## Ortho Simulation Expert Pattern
## Simulates 3D mechanics (gravity, jump, shadow) in a 2D top-down view.
class_name OrthoSimulation
@export var jump_height: float = 50.0 # Pixels
@export var jump_duration: float = 0.5 # Seconds
@export var base_scale: float = 1.0
# Nodes
@onready var sprite: Sprite2D = $Sprite2D
@onready var shadow: Sprite2D = $Shadow # Must be separate child
# State
var _z_height: float = 0.0
var _z_velocity: float = 0.0
var _gravity: float
var _jump_impulse: float
func _ready() -> void:
# Calculate gravity physics based on desired jump arc
_gravity = (2.0 * jump_height) / pow(jump_duration / 2.0, 2)
_jump_impulse = sqrt(2.0 * _gravity * jump_height)
func _physics_process(delta: float) -> void:
# 1. 3D Z-Axis Simulation
if _z_height > 0 or _z_velocity > 0:
_z_velocity -= _gravity * delta
_z_height += _z_velocity * delta
if _z_height <= 0:
_z_height = 0
_z_velocity = 0
_on_land()
# 2. Visual Offset (Y-axis displacement)
# in 2.5D, Y-up typically maps to -Y in 2D
sprite.position.y = -_z_height
# 3. Dynamic Shadow Scaling
# Shadow stays on "ground" (local 0,0), shrinks as unit jumps high
var shadow_scale = 1.0 - clamp(_z_height / (jump_height * 2.0), 0.0, 0.5)
shadow.scale = Vector2.ONE * shadow_scale * base_scale
# Input & Movement (Standard 2D)
var input = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
velocity = input * 200.0
move_and_slide()
if Input.is_action_just_pressed("ui_accept") and _z_height == 0:
jump()
func jump() -> void:
_z_velocity = _jump_impulse
func _on_land() -> void:
# Landing particles, sound, etc.
pass
## EXPERT USAGE:
## Sprite2D must be child. Shadow must be child (and below sprite).
## Simulates "Z-Jump" in top-down games like Zelda/CrossCode.
shader_type canvas_item;
// Expert Ortho-to-Perspective Shader
// Apply this to a ColorRect covering your 2D game to simulate a 3D perspective warp.
// Highly useful for Mode 7 style racing games or tilting a 2D tabletop view.
uniform float vanishing_point_y : hint_range(-2.0, 2.0) = 1.0;
uniform float tilt_amount : hint_range(0.0, 5.0) = 1.0;
void fragment() {
// Treat the screen UV as Cartesian coordinates from the center
vec2 pos = UV * 2.0 - 1.0;
// Warp the X coordinate based on its distance to the vanishing point Y
// Objects further "up" the screen pinch inwards
float pinch = 1.0 - (pos.y * tilt_amount);
// Prevent division by zero
pinch = max(pinch, 0.001);
// Re-calculate the UV
vec2 warped_uv = vec2((pos.x / pinch) * 0.5 + 0.5, UV.y);
// Discard pixels mapped outside the screen bounds
if (warped_uv.x < 0.0 || warped_uv.x > 1.0) {
COLOR = vec4(0.0);
} else {
// Sample standard texture
COLOR = texture(TEXTURE, warped_uv);
}
}
extends Camera2D
class_name FakeDepthCamera2D
## Expert Camera2D for simulating 3D depth via Parallax
## Replaces FOV and 3D positioning with scale-based depth layers.
@export var depth_layers: Array[CanvasLayer] = []
@export var base_camera_speed: float = 10.0
@onready var player: Node2D = get_parent()
func _ready() -> void:
set_process(true)
# Ensure smoothing is active for that "cinematic 3D" feel
position_smoothing_enabled = true
position_smoothing_speed = base_camera_speed
func _process(delta: float) -> void:
# Simulating a perspective shift when the player moves rapidly
# by slightly nudging the furthest background CanvasLayers in opposition to camera movement.
var cam_velocity = get_screen_center_position() - global_position
for i in range(depth_layers.size()):
var layer = depth_layers[i]
# Deeper layers (higher index) move slower, causing a parallax effect without using ParallaxBackground
var depth_factor = 1.0 - (float(i) / depth_layers.size()) * 0.5
layer.offset = global_position * (1.0 - depth_factor)
# skills/adapt-3d-to-2d/scripts/projection_utils.gd
class_name ProjectionUtils
## Projection Utils Expert Pattern
## Helpers for projecting 3D positions to 2D screen space (UI, Indicators).
# Check if a 3D point is visible on screen and in front of camera
static func is_on_screen(camera: Camera3D, global_pos: Vector3, margin: float = 0.0) -> bool:
if not camera: return false
# Check if behind camera
if camera.is_position_behind(global_pos):
return false
var screen_pos = camera.unproject_position(global_pos)
var viewport_rect = camera.get_viewport().get_visible_rect()
# Apply margin
viewport_rect = viewport_rect.grow(margin)
return viewport_rect.has_point(screen_pos)
# Get screen position, clamping to edges if off-screen (for indicators)
static func get_clamped_screen_pos(camera: Camera3D, global_pos: Vector3, edge_margin: float = 20.0) -> Vector2:
var screen_pos = camera.unproject_position(global_pos)
var viewport_rect = camera.get_viewport().get_visible_rect()
var center = viewport_rect.get_center()
# If behind camera, mirror logic to show indicator at bottom/top correctly
if camera.is_position_behind(global_pos):
screen_pos = center + (center - screen_pos)
# Clamp to screen rect
var min_pos = Vector2(edge_margin, edge_margin)
var max_pos = viewport_rect.size - Vector2(edge_margin, edge_margin)
return screen_pos.clamp(min_pos, max_pos)
# Calculate scale factor based on distance (simulate perspective for 2D UI)
static func get_perspective_scale(camera: Camera3D, global_pos: Vector3, reference_dist: float = 10.0) -> float:
var dist = camera.global_position.distance_to(global_pos)
if dist == 0: return 1.0
return reference_dist / dist
## EXPERT USAGE:
## target_indicator.position = ProjectionUtils.get_clamped_screen_pos(cam, enemy.position)