Claude
Skills
Sign in
Back

godot-adapt-2d-to-3d

Included with Lifetime
$97 forever

Expert patterns for migrating 2D games to 3D including node type conversions, camera systems (third-person, first-person, orbit), physics layer migration, sprite-to-model art pipeline, and control scheme adaptations. Use when porting 2D projects to 3D or adding 3D elements. Trigger keywords: CharacterBody2D to CharacterBody3D, Area2D to Area3D, Camera2D to Camera3D, Vector2 to Vector3, collision_layer migration, sprite to MeshInstance3D, 2D to 3D conversion.

Ads & Marketingscripts

What this skill does


# Adapt: 2D to 3D

Expert guidance for migrating 2D games into the third dimension.

## 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.
- **NEVER use the rotation property for complex 3D logic** — 3D rotation uses Euler angles. Interpolating Euler angles causes unpredictable paths and Gimbal Lock. Always use `Quaternion` for 3D rotation interpolation or the `Basis` matrix for directional vectors.
- **NEVER ignore metric scaling** — 3D physics and lighting assume 1 unit = 1 meter. Scaling models inside the engine introduces precision errors. Export assets from DCCs at the correct metric scale.
- **NEVER disable physics interpolation when using custom camera follow scripts** — Updating camera position in `_process` to follow a body moving in `_physics_process` causes jitter. Use `Node3D.get_global_transform_interpolated()` for smooth transforms.

---

## Available Scripts

> **MANDATORY**: Read the appropriate script before implementing the corresponding pattern.

### [sprite_plane.gd](scripts/sprite_plane.gd)
Sprite3D billboard configuration and world-to-screen projection for placing 2D UI over 3D objects. Handles behind-camera detection.

### [vector_mapping.gd](scripts/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.

### [crisp_projected_ui.gd](scripts/crisp_projected_ui.gd)
Projected 2D UI for 3D Objects mapping snippet. Replaces blurry text elements with true 2D Canvas space positioning projected from 3D space.

---

## Node Conversion Matrix

| 2D Node | 3D Equivalent | Notes |
|---------|---------------|-------|
| CharacterBody2D | CharacterBody3D | Add Z-axis movement, rotate with mouse |
| RigidBody2D | RigidBody3D | Gravity now Vector3(0, -9.8, 0) |
| StaticBody2D | StaticBody3D | Collision shapes use Shape3D |
| Area2D | Area3D | Triggers work the same way |
| Sprite2D | MeshInstance3D + QuadMesh | Or use Sprite3D (billboarded) |
| AnimatedSprite2D | AnimatedSprite3D | Billboard mode available |
| TileMapLayer | GridMap | Requires MeshLibrary creation |
| Camera2D | Camera3D | Requires repositioning logic |
| CollisionShape2D | CollisionShape3D | BoxShape2D → BoxShape3D, etc. |
| RayCast2D | RayCast3D | target_position is now Vector3 |

---

## Migration Steps

### Step 1: Physics Layer Reconfiguration

```gdscript
# 2D collision layers are SEPARATE from 3D
# You must reconfigure in Project Settings → Layer Names → 3D Physics

# Before (2D):
# Layer 1: Player
# Layer 2: Enemies
# Layer 3: World

# After (3D) - same names, but different system
# In code, update all collision layer references:

# 2D version:
# collision_layer = 0b0001

# 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
```

### Step 2: Camera Conversion

```gdscript
# ❌ BAD: Direct 2D follow logic
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
# Scene structure:
# Player (CharacterBody3D)
#   └─ SpringArm3D
#       └─ Camera3D

# 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)
```

### Step 3: Movement Conversion

```gdscript
# 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()

# ✅ 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()
```

---

## Art Pipeline: Sprites → 3D Models

### Option 1: Billboard Sprites (2.5D)

```gdscript
# 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
```

### Option 2: Quad Meshes (Floating Sprites)

```gdscript
# 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
```

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

```gdscript
# Import .glb, .fbx models
var character := load("res://models/character.glb").instantiate()
add_child(character)

# Access animations
var anim_player := character.get_node("AnimationPlayer")
anim_player.play("idle")
```

---

## Lighting Considerations

### Minimum Lighting Setup

```gdscript
# 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)

# 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)
```

---

## UI Adaptation

```gdscript
# ✅ GOOD: Keep 2D UI overlay
# Scene structure:
# Main (Node3D)
#   ├─ WorldEnvironment
#   ├─ Dir

Related in Ads & Marketing