Appearance
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:
- 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 = …). - The
camerascript API. From any script, drive the camera directly. This is Unity's model: the main Camera entity is the camera's transform, so acamera.*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 / raycastsWARNING
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.
CharacterController (recommended)
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.sphereCast — sphereCast(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:
PlayerControlleron a capsule entity that has acharacterControllercomponent — WASD viainput.getAxis, jump on Space, gravity, camera-relative movement.CameraControlleron the main Camera entity — third-person orbit withinput.getMouseDelta, framing thePlayerentity withcamera.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
- Transform constraints — make a companion or a turret follow / face the player with no script (the example's pink Companion drone does this).
camera,cursor,characterController, andinputin the Script API reference.