Cursor
Cursor manages the local client cursor through handle-based ownership.
The same cursor manager is used by:
- core game UI
- main menu
- debug console
- modal dialogs
- client-side Lua resources
Use Cursor when a resource needs a visible pointer for interactive UI such as:
- admin panels
- inventories
- custom editors
- modal tools
Do not toggle the engine cursor directly from scripts.
Use Cursor.Acquire(...) and Cursor.Release(...) so multiple systems can coexist safely.
Error Handling Pattern​
- Success:
err == nil - Failure:
erris an error code string
Scope Model​
Cursoris client-only.- Server scripts do not directly control the local cursor.
- Cursor state is local presentation state, not authoritative gameplay state.
Function List​
| Function | Description | Scope |
|---|---|---|
Cursor.Acquire | Acquire a cursor handle for the current resource. | C |
Cursor.Release | Release one acquired cursor handle. | C |
Cursor.GetState | Read the effective cursor state and active owner count. | C |
Ownership Model​
Cursor is handle-based.
That means:
- each caller acquires its own cursor handle
- each caller releases only its own cursor handle
- the engine resolves the effective cursor state from all active handles
This avoids broken behavior when:
- a Lua resource opens UI
- the main menu opens above it
- the debug console opens above both
- one layer closes while another still needs the cursor
Typical flow:
local cursorHandle, err = Cursor.Acquire({
visible = true,
capture = "free",
reason = "admin_panel"
})
-- later
Cursor.Release(cursorHandle)
Effective State Rules​
Public scripting rule:
- if at least one active handle requires a visible cursor, the effective cursor remains visible
- releasing one handle does not hide the cursor if another active handle still requires it
Engine arbitration details such as core UI priority are internal implementation detail.
Automatic Cleanup​
Cursor handles owned by a resource are automatically released when:
- the resource stops
- the client disconnects
- the client Lua runtime resets
Scripts should still release handles explicitly when no longer needed.
Controller and Pointer Behavior​
When the effective cursor is visible:
- mouse input moves the same visible cursor on keyboard/mouse setups
- controller pointer sources may move the same cursor locally
Current client behavior:
- keyboard/mouse: native mouse movement controls the visible cursor
- controller: controller-provided pointer movement can drive the same visible cursor while the active device is a controller
Current v1 guarantee:
- controller
ui_acceptis translated into a left mouse click while the cursor is visible - native pointer movement still controls the same visible cursor, including controller pointer sources such as touchpads when the platform exposes them as pointer motion
Current limitation:
- controller touchpad pointer movement is not guaranteed yet across all platforms/runtime setups
- if the platform does not expose controller touchpad motion to Godot as native pointer motion, the visible cursor will not move from the touchpad today
Touchpad-capable controllers use the same cursor model, but exact touchpad motion support remains platform-dependent and is not yet considered production-complete.
Cursor controls cursor visibility/capture.
It does not replace focus navigation:
- d-pad / stick UI navigation is still handled by UI/input systems
- pointer movement is only used when a visible pointer is active
Capture Modes​
Supported capture modes:
free: visible free cursor for UI interactioncaptured: cursor hidden/captured for gameplay look inputconfined: cursor visible but confined to the game window
Resources should usually use:
freefor UI panels
Functions​
Cursor.Acquire​
C Client Only
local handleId, err = Cursor.Acquire(options)
Parameters:
options(table):visible(boolean): requested cursor visibilitycapture("free" | "captured" | "confined", default"free")reason(string, optional): diagnostic reason for the request
Returns:
handleId(string | nil)err(nil| string)
Example:
local cursorHandle, err = Cursor.Acquire({
visible = true,
capture = "free",
reason = "admin_panel"
})
if err then
Logger.Error("Cursor.Acquire failed: " .. tostring(err))
end
Cursor.Release​
C Client Only
local err = Cursor.Release(handleId)
Parameters:
handleId(string)
Returns:
err(nil| string)
Example:
if cursorHandle then
local err = Cursor.Release(cursorHandle)
if err then
Logger.Error("Cursor.Release failed: " .. tostring(err))
end
cursorHandle = nil
end
Cursor.GetState​
C Client Only
local state, err = Cursor.GetState()
Returns:
state(table | nil)err(nil| string)
State fields:
visible(boolean)capture("free" | "captured" | "confined")activeHandleCount(number)position({ x, y }) current viewport mouse/pointer position
Example:
local state, err = Cursor.GetState()
if not err and state then
print("visible=" .. tostring(state.visible) .. " capture=" .. tostring(state.capture))
end
UI Relationship​
Cursor and UI are related, but separate:
UIcreates controlsCursorcontrols whether the player can point at them
Opening a UI panel does not automatically imply cursor visibility unless the resource explicitly acquires a cursor handle.
Typical UI pattern:
local panelVisible = false
local cursorHandle = nil
local function setPanelVisible(visible)
panelVisible = visible and true or false
if panelVisible and not cursorHandle then
cursorHandle = Cursor.Acquire({
visible = true,
capture = "free",
reason = "admin_panel"
})
elseif not panelVisible and cursorHandle then
Cursor.Release(cursorHandle)
cursorHandle = nil
end
end
Input Relationship​
Cursor is not an input binding API.
Use:
Inputfor binds and action callbacksUIfor controls and focusable elementsCursorfor pointer visibility/capture ownership
Text input and UI focus rules still belong to UI / Input, not Cursor.