Migrating a Unity 3D Game to Godot

Rebuild a Unity 3D game in Godot 4.7: fix coordinate systems, replace CharacterController with CharacterBody3D, import glTF models, light the scene, and move physics callbacks to signals.

Unity 3D to Godot 4 migration
On this page

A Unity 3D game does not translate into Godot by swapping component names for node names. The two engines model a 3D scene differently enough that a CharacterController script, a follow camera, and a handful of OnCollisionEnter callbacks all need to be rebuilt against Godot's own model rather than mapped line for line. This guide sequences that rebuild for a Unity 3D developer: get the coordinate system right first, stand up the scene in an order that lets you see what you are building, replace the character controller, and only then bring in imported meshes, lighting, and collision callbacks.

How Godot 3D Differs from Unity 3D

Unity separates a GameObject from the components attached to it: a Transform, a CharacterController, a MeshRenderer, and your scripts all sit side by side on the same object, and a prefab is a saved template of that object graph. Godot collapses most of that into the node itself. A Node3D already carries its own transform; a CharacterBody3D is simultaneously the transform, the physics body, and the attachment point for a script, and a .tscn scene is the equivalent of a prefab, but it is also just a tree of nodes you can open, edit, and instance anywhere, including inside another scene.

The physics backend has changed too, so numeric behavior will not match even after you translate the API correctly. Godot 4.4 and later default new projects to Jolt Physics rather than the older Godot Physics solver, and neither one behaves like Unity's PhysX. Treat masses, forces, and joint values as ones you retune in the target engine, not numbers you carry over from a Unity inspector.

Fix Coordinate Systems, Handedness, and Units First

This is the trap that quietly breaks imported rotations, forward-facing logic, and follow cameras, so fix it before writing any movement code. Unity uses a left-handed coordinate system: Y is up, Z is forward, and X is right. Godot's 3D space follows the OpenGL convention instead: X is right, Y is up, and Z is nominally the forward axis, but the documentation is explicit that -Z is forward in practice, which makes Godot right-handed. Both engines put Y up, so vertical is not the problem. The handedness flip and the reversed forward axis are.

  • A model authored facing +Z in Unity will often face backward in Godot until you rotate it 180 degrees on import or in the scene, because Godot's forward is -Z.
  • Mirrored rotations are the usual symptom of a handedness mismatch: a turn that reads correctly in Unity can read as spinning the wrong way once the same angles are applied in Godot's right-handed basis.
  • Unity's 1 unit equals 1 meter convention carries over loosely. Godot does not enforce a unit system, but CharacterBody3D's velocity is documented as meters per second, so treat 1 unit as 1 meter and re-check import scale on every FBX or glTF asset rather than assuming it matched on the first try.

Confirm this early with something you can see: instance a single imported mesh, add a CharacterBody3D with a forward-facing arrow or debug mesh, and turn it using your own script before you build a whole level around assumptions borrowed from Unity's axes. Read the engine comparison for the wider set of conceptual differences before you invest a timeline in the rebuild.

Rebuild the 3D Scene in the Right Order

Signature lab · Migration planner

Sequence the 3D rebuild

Click each step in the order you would tackle it.

Your order will appear here.

A Unity 3D scene usually grows GameObject by GameObject, in whatever order features were added. Rebuilding that same scene in Godot goes faster when you deliberately reverse the instinct to import everything at once: stand up the player body first, give it a shape to collide with, frame it with a camera, light the empty box so you can actually see it, then bring in real meshes, and tune movement last once every earlier piece already exists to react to it.

Use the planner below to put your own 3D scene's pieces in that same order. Skipping ahead to imported art or lighting before the CharacterBody3D and its CollisionShape3D exist just means testing movement against nothing, which hides the very bugs — wrong forward axis, wrong collision shape, wrong camera offset — that are cheapest to catch early.

Replace the CharacterController with CharacterBody3D

Unity's CharacterController exposes a Move() call and CollisionFlags, and it applies its own capsule-based collision resolution outside the main physics simulation. Godot's equivalent is CharacterBody3D: your script owns a Vector3 velocity property, and you call move_and_slide() once per physics step so the body moves and resolves collisions against the world in one pass. move_and_slide() takes no arguments — it reads and updates the node's own velocity — and it returns true if the body collided with something, so trust the return value and is_on_floor() instead of tracking collision state yourself.

gdscript
# player.gd — CharacterBody3D replacing a CharacterController
extends CharacterBody3D

@export var speed := 5.0
@export var jump_velocity := 4.5
@export var gravity := 9.8

func _physics_process(delta: float) -> void:
    if not is_on_floor():
        velocity.y -= gravity * delta
    elif Input.is_action_just_pressed("jump"):
        velocity.y = jump_velocity

    var input_dir := Input.get_vector("move_left", "move_right", "move_forward", "move_back")
    var direction := (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()
    velocity.x = direction.x * speed
    velocity.z = direction.z * speed

    move_and_slide()

Notice that direction is built from the body's own basis rather than a world-space axis, which is what keeps movement facing the way the character is actually turned instead of a fixed compass direction. This is also where a wrong coordinate-system assumption shows up fastest: if forward feels reversed, it is almost always the -Z convention from the previous section, not a bug in this script.

Set Up Camera3D and Follow Behavior

Camera3D registers itself with the nearest Viewport, and only one camera can be current per viewport at a time; call make_current() if you need to switch explicitly, though a single Camera3D in a simple scene becomes current automatically. Godot has no built-in Cinemachine equivalent, so a Unity project that leaned on virtual cameras for framing and follow behavior needs that logic rebuilt as a small script rather than a drop-in package.

gdscript
# camera_rig.gd — simple third-person follow, parented under the player
extends Node3D

@export var target: Node3D
@export var follow_speed := 6.0

func _process(delta: float) -> void:
    if target:
        global_position = global_position.lerp(target.global_position, follow_speed * delta)
        look_at(target.global_position + Vector3.UP * 1.0)

Put the Camera3D as a child of this rig node so you can offset its local position for distance and height without fighting the follow logic. Start simple — a lerp and a look_at — before reaching for spring arms, collision-avoidance raycasts, or shake effects; those are worth adding once the basic frame already reads correctly against the coordinate fix from earlier in this guide.

Import Models Through the glTF Pipeline

Godot imports FBX files directly through its built-in ufbx library, so a model exported from Unity's source DCC tool as FBX will generally import without an external converter. glTF 2.0 remains the format the documentation recommends when you have a choice, because it round-trips materials, skinning, and animation more predictably than FBX or Collada. If your source files still live in Blender or Maya, re-exporting the ones you actually reuse as glTF is worth the extra step rather than routing everything through FBX by habit.

  • Recheck import scale on every model — a mesh authored at Unity's 1-unit-per-meter scale can still arrive too large or too small depending on export settings, and a rescaled root breaks the coordinate assumptions from earlier.
  • Verify a forward-facing character or prop actually faces -Z once imported; rotate the imported scene's root, not the mesh data, if it is turned around.
  • Confirm skeletons and animations survive the import by playing one clip before wiring up gameplay code against it, since a broken rig is much cheaper to catch on one test mesh than after a dozen scripts depend on it.

Bring in one representative mesh before batch-importing the rest of the art. Cross-check anything you are unsure how to translate against the equivalents reference and the broader asset pipeline guide rather than guessing from Unity import settings that do not carry over.

Light the Scene with WorldEnvironment and Lights

A WorldEnvironment node holds a single Environment resource that defines the scene's default background, ambient lighting, and post-processing — it is the closest match to Unity's Lighting settings window, and only one WorldEnvironment should be active per scene, though an individual Camera3D can still override it with its own Environment. Add one to an empty 3D scene before you judge whether anything else looks right; an unlit or default-lit test scene will make correctly imported meshes look broken when the actual problem is missing lighting.

For direct light, add a DirectionalLight3D. Its direction comes entirely from the node's own rotation — light is emitted along the -Z axis of its basis — and its position in the scene is ignored, which surprises anyone used to placing a Unity directional light like it has a meaningful location. Rotate the node to aim the light; moving it does nothing.

Move Physics Callbacks to Signals

Unity's OnCollisionEnter and OnTriggerEnter are virtual methods Unity calls on your MonoBehaviour. Godot's closest equivalents are signals: an Area3D emits body_entered(body: Node3D) and body_exited(body: Node3D) for physics bodies passing through it, and area_entered(area: Area3D) / area_exited(area: Area3D) for overlaps with other areas — but only while the Area3D's monitoring property is enabled. Connect a method to the signal instead of overriding a callback method Godot calls for you.

gdscript
# pickup.gd — Area3D replacing an OnTriggerEnter callback
extends Area3D

signal collected(points: int)

@export var points := 10

func _ready() -> void:
    body_entered.connect(_on_body_entered)

func _on_body_entered(body: Node) -> void:
    if body.is_in_group("player"):
        collected.emit(points)
        queue_free()

A solid collision between two physics bodies, rather than an overlap, is read from CharacterBody3D's own move_and_slide() result and its slide-collision queries instead of a signal — there is no per-body OnCollisionEnter equivalent for CharacterBody3D. Reach for Area3D signals when you need trigger-style overlap notifications, and read is_on_floor(), is_on_wall(), and the slide collision data directly when you need to react to the character's own solid contacts. The signal flow guide covers who should own the emitted event once you have more than one listener.

Practice and Sources

Sequence your own scene with the planner above, then rebuild it in that order in a small test level before touching the rest of your Unity project's 3D content. Cross-check unfamiliar calls against the general porting playbook and the physics guide if collision behavior still feels unpredictable once CharacterBody3D is in place. The official sources below verify the coordinate system, CharacterBody API, import pipeline, and physics backend claims in this guide; the sequencing and the coordinate-system trap are the parts a straight API lookup will not tell you.

Frequently asked questions

Is Godot's 3D coordinate system the same as Unity's?
Both are Y-up, but they are not identical. Unity is left-handed with +Z as forward. Godot follows the OpenGL convention: X is right, Y is up, and -Z is forward, which makes it right-handed. Rotations and imported forward-facing assets can end up mirrored or facing backward if you assume the two are interchangeable.
What replaces Unity's CharacterController in Godot?
CharacterBody3D. Your script sets its Vector3 velocity property and calls move_and_slide() once per physics frame; the node then exposes is_on_floor(), is_on_wall(), and slide-collision data, similar to what CharacterController's Move() and CollisionFlags gave you.
Should I import FBX or convert everything to glTF?
Godot imports FBX directly through the built-in ufbx library, so individual models generally work without conversion. glTF 2.0 is still the format the documentation recommends when you have a choice, because it round-trips materials, skinning, and animation more predictably.
Verification notes

Sources and revision context

New for the migration spine; fills the 3D-specific gap left by the mostly-2D migration content, with a verified coordinate-system comparison and a CharacterController-to-CharacterBody3D rebuild path.