Skip to content

Scripting your own player

By default a Galatrix scene spawns a built-in player — a capsule character with WASD movement, a mouse-look camera that follows it, and the gameplay zones (water, damage, checkpoints, …). That is the fastest way to make a game, and most maps want it.

But sometimes you want full control: a top-down character, a vehicle, a twin-stick shooter, a fixed cinematic camera. For that, turn the built-in player off and script your own character and camera — the Unity way.

Try it

Open the worked example this guide is built from: editor.galatrix.com/?example=scriptedplayer. It is a fully scripted third-person character + orbit camera with no built-in player.

Turn off the built-in player

In Scene Settings → Player, check Disable Built-in Player. Now nothing is spawned for you — no avatar, no controller, and (importantly) no camera. You are responsible for both.

The scene still does everything else: physics runs, NPCs and navMeshAgents move, your scripts run, audio plays. Only the default player is gone.

You still get keyboard, mouse, and pointer lock — scripts need them — so input.* and cursor.* work exactly as before.

Drive the camera

With no built-in player there is no follow camera, so the camera becomes yours to place. Two ways, which combine freely:

  1. A Camera entity. Add a Camera to the scene and check Main Camera on it. Its transform drives the render camera every frame — move the entity, move the view. A script on that entity can move it like any other entity (this.entity.position = …).
  2. The camera script API. From any script, drive the camera directly. This is Unity's model: the main Camera entity is the camera's transform, so a camera.* pose call writes through to that entity — one call moves the camera, it stays moved, and the entity/Inspector always agree. The two ways are the same thing seen from two sides; mix them freely.
js
camera.setPosition([x, y, z])     // place the camera (also moves the main Camera entity)
camera.lookAt([x, y, z])          // aim it at a world point
camera.setRotation([x, y, z, w])  // or set a quaternion directly
camera.setFov(70)                 // vertical FOV in degrees
camera.getPosition()              // where it is
camera.getForward()               // facing unit vector — handy for shooting / raycasts

WARNING

If you disable the built-in player and add no main Camera entity and never call camera.setPosition(...), the engine falls back to a default vantage point and warns in the console. Place a camera or drive it from a script.

Taking the camera with the built-in player on

You don't have to disable the built-in player just for a cutscene or kill-cam. With the player active, the follow rig owns the camera (a bare camera.setPosition is re-driven next frame — same as writing camera.CFrame while CameraType = Custom in Roblox). Detach and re-attach it explicitly:

js
camera.setType('scriptable')      // the rig lets go — your script owns the camera
camera.setPosition([0, 20, 0])
camera.lookAt(this.game.findEntity('Boss').position)
// … later, hand it back:
camera.setType('default')         // the follow rig resumes (resets every play)

'default' and 'custom' are the same mode under two names — 'default' says what it is, 'custom' matches Roblox's Enum.CameraType.Custom if you're coming from there; use whichever reads better. camera.getType() reads the current mode (it reports 'custom'). 'scriptable' also works in no-player mode if you want the render camera fully detached from the main Camera entity.

Mouse-look

For first/third-person look you want mouse delta (Unity's "Mouse X / Mouse Y"), not the absolute cursor position (which sits at the center under pointer lock). Lock the cursor on start, then read the per-frame delta:

js
class CameraController extends Behaviour {
  // [Range(3, 16)]
  distance = 8
  height = 2.5
  sensitivity = 0.0025
  _yaw = 0
  _pitch = 0.35

  onUpdate() {
    // Orbit with the mouse (works under pointer lock).
    const md = this.input.getMouseDelta()
    this._yaw -= md[0] * this.sensitivity
    this._pitch = Math.max(-0.2, Math.min(1.2, this._pitch + md[1] * this.sensitivity))

    const t = this.game.findEntity('Player')
    const tp = t ? t.position : [0, 1, 0]
    const cd = Math.cos(this._pitch) * this.distance
    const cx = tp[0] - Math.sin(this._yaw) * cd
    const cy = tp[1] + this.height + Math.sin(this._pitch) * this.distance
    const cz = tp[2] - Math.cos(this._yaw) * cd
    this.camera.setPosition([cx, cy, cz])
    this.camera.lookAt([tp[0], tp[1] + 1, tp[2]])
  }
}

cursor.lock() / cursor.unlock() / cursor.setVisible(v) map to Unity's Cursor — call lock() once (e.g. in your player's onStart) so the mouse feeds look instead of moving a pointer.

Move the character

You have two good options for the body.

A characterController component is a kinematic capsule with Unity-style collide-and-slide, auto-step, and ground-snap — the same controller the built-in player uses, exposed to your script. Add it to your character entity (don't also add a rigidBody — the controller owns the body), then drive it each frame with characterController.move(id, motion). You apply your own gravity, exactly like Unity's CharacterController:

js
class PlayerController extends Behaviour {
  // [Range(1, 20)]
  speed = 7
  // [Range(1, 20)]
  jumpForce = 9
  gravity = 25
  _vy = 0

  onStart() { this.cursor.lock() }

  onUpdate() {
    const dt = this.time.deltaTime
    const id = this.entity.id

    // Camera-relative horizontal direction.
    const f = this.camera.getForward()
    let fx = f[0], fz = f[2]
    const fl = Math.hypot(fx, fz) || 1
    fx /= fl; fz /= fl
    const rx = -fz, rz = fx
    const v = this.input.getAxis('vertical')
    const h = this.input.getAxis('horizontal')
    let mx = fx * v + rx * h, mz = fz * v + rz * h
    const ml = Math.hypot(mx, mz)
    if (ml > 0) { mx /= ml; mz /= ml }

    // Gravity + jump (apply your own gravity, Unity-style).
    if (this.characterController.isGrounded(id)) {
      if (this._vy < 0) this._vy = -2          // stick to the ground
      if (this.input.getKeyDown(' ')) this._vy = this.jumpForce
    }
    this._vy -= this.gravity * dt

    // Collide-and-slide + auto-step + ground-snap, all in one call.
    this.characterController.move(id, [mx * this.speed * dt, this._vy * dt, mz * this.speed * dt])
  }
}

move() returns { grounded, collided } for the move you just applied; isGrounded(id) reports the last move's ground state. The capsule's radius, height, slopeLimit, stepOffset, and skinWidth are component fields you set in the Inspector (or via setComponent).

Physics body

If you want bouncing, knockback, or full rigid-body dynamics, give the character a dynamicrigidBody + collider instead and drive it with the physics API (physics.setVelocity, applyImpulse, raycast, …). The engine's gravity moves it; you steer.

Collide-and-slide it yourself (no component)

For a truly from-scratch controller — the character analogue of the Custom Vehicle Controller — skip the characterController component entirely and do the collide-and-slide in the script. Give the entity no characterController, rigidBody, or collider, position it directly each frame, and test the capsule against the world yourself with physics.sphereCastsphereCast(origin, radius, dir, maxDist) returns { point, normal, distance } where distance is how far the sphere centre can travel before it touches something. Approximate the capsule with two spheres, advance to the contact, project the leftover motion along the surface normal to slide, cast down to snap to the ground, and apply your own gravity + jump. The Custom Character Controller kit (Project → Import Kit) ships exactly this — one editable PlayerController script that walls-slides, ground-snaps, walks ramps, and steps up ledges, all from sphereCast. Reach for it when you want to own the collision model itself; use the characterController component above when you just want a solid capsule that works.

Put it together

The example scene wires exactly two scripts:

  • PlayerController on a capsule entity that has a characterController component — WASD via input.getAxis, jump on Space, gravity, camera-relative movement.
  • CameraController on the main Camera entity — third-person orbit with input.getMouseDelta, framing the Player entity with camera.setPosition + camera.lookAt.

That's the whole Unity-style loop: one script owns the body, one owns the camera, and the engine just runs the world around them.

This works the same in the editor Play Mode, in a standalone build, and follows the same shared runtime as published play.

See also