# Movement modes: fly, swim, glide, space and your own A movement mode is the ordinary character movement with other variables: gravity, speed, acceleration (inertia), drag, vertical control, buoyancy, fall speed, plus a few entry and exit rules. Flying, swimming or floating in space is not another controller: it is the same Rust controller of `character.third_person` and `character.first_person`, so it behaves the same in first and third person, on the web, in Bevy, Godot, Unity and Unreal (the engines only configure, listen and decide through the hook). ## Turn it on Every game with one of these kits gets the registry: `walk` (always on) and the proposed `fly`, `swim`, `glide` and `space`, **off** until the game enables them. Nothing changes for a game that enables none. ```json { "id": "character.third_person", "config": { "modes": { "fly": { "enabled": true }, "swim": { "enabled": true, "speed": 3.5 }, "glide": { "enabled": true, "maxFall": 2 }, "space": { "enabled": true, "drag": 0 } } } } ``` Default entries and exits: `fly` and `glide` start with a jump pressed in the air (`air`); `fly` ends on landing (`ground`), `glide` on landing or a second jump; `swim` follows the zones tagged `water` and `space` the zones tagged `space` (`volume:`). In a vertical mode, holding `jump` climbs and `move_down` (registered only then) descends. ## Reference Mode settings (each optional, over the preset): `enabled`, `label`, `speed`, `runSpeed` (m/s), `gravity` (× 9.81, 0 = weightless), `accel` (m/s² toward the asked velocity; low = inertia), `drag` (1/s slow-down without input; 0 keeps the velocity), `vertical`, `verticalSpeed`, `buoyancy` (m/s² up), `maxFall` (0 = no cap), `jump` (the ground jump still works), `surface` (metres the feet stay under the top of the volume), `anim` / `animMove` (clips), `from` (modes it may be entered from), `enter` / `exit` (`air`, `ground`, `action:`, `volume:`), `driver: "kit"` (another kit moves the capsule). `character.control` methods: `mode()`, `modeIndex()`, `modeOwner()`, `modes()`, `modeInfo(id?)`, `setMode(id, reason?)` (a controller mode or `walk`; answers the mode entered or null), `enterMode(id, owner, reason?)` (a kit that drives the capsule itself, then `claim` / `release` as before), `registerMode(spec)`, `setModeEnabled(id, enabled)`. Event `character.mode` `{ entity, from, to, reason, owner, index, seq }`, typed (`kit.character.modes.events`), once per change. Hook `character.mode` `{ entity, from, to, reason, owner }`: cancel to keep the current mode, set `to` to redirect. ```json { "hooks": [ { "id": "no_fly_indoors", "hook": "character.mode", "match": { "to": "fly" }, "cancel": true, "reason": "indoors" }, { "id": "fly_is_space", "hook": "character.mode", "match": { "to": "fly" }, "set": { "to": "space" } } ] } ``` ## Add your own mode Most modes are only values. Declare them in the config `modes`, in the data registry `movementModes` (what a mod adds), or at run time with `registerMode`: ```json { "data": { "movementModes": [ { "id": "jetpack", "gravity": 0.4, "buoyancy": 6, "speed": 6, "vertical": true, "from": ["walk"] }, { "id": "levitate", "gravity": 0, "speed": 2, "vertical": true, "enter": ["action:levitate"], "exit": ["action:levitate"] } ] } } ``` It then has an index, the hook, the event, the save and the accessor like the others. Code is needed only for a truly special movement: a kit calls `enterMode(id, owner)`, claims the capsule and moves it itself. From Godot (GDScript), through the `GfGameHost` node, nothing is reimplemented (test: `wrappers/demo/godot/tests/movement_modes_test.gd`): ```gdscript var host := GfGameHost.new() host.load_game("res://game/my.game.json") # as exported, with its game schema (the columns below) host.register_entry("movementModes", {"id": "jetpack", "gravity": 0.4, "buoyancy": 6, "vertical": true}) host.game_event.connect(func(topic, payload, _event): if topic == "character.mode": print(payload.from, " -> ", payload.to, " (", payload.reason, ")")) host.start() host.watch("character.mode") host.tap_hook("character.mode", func(payload, _hook): if payload.to == "fly" and indoors: return {"cancel": true, "reason": "indoors"} return null, {"id": "no_fly_indoors"}) host.call_service("character.control", "setMode", ["jetpack", "pickup"]) var player: String = host.call_service("character.control", "player", []) var mode_index: float = host.column(player, "character.mode@1")[0] # every frame, read in place ``` From Bevy (`gfkits_bevy`), the same calls in Rust, events as Bevy messages: ```rust use bevy::prelude::*; use gfkits_bevy::*; App::new() .add_plugins(KitsPlugin::new(game).watch("character.mode")) // events as `KitEvent` messages .tap_kit_hook("character.mode", 0.0, |call| match call.text("to") { Some("fly") => HookReply::cancel("indoors"), _ => HookReply::Pass, }) .add_systems(Update, on_mode.after(KitsSet::Step)); fn on_mode(mut events: MessageReader) { for e in events.read().filter(|e| e.topic == "character.mode") { info!("{:?} -> {:?}", e.payload.text("from"), e.payload.text("to")); } } fn pick_up_jetpack(kits: &mut KitGame) -> Option { kits.call_with("character.control", "setMode", &[Arg::Str("jetpack"), Arg::Str("pickup")], |v| v.as_str().map(String::from)).ok()? } ``` ## Read the mode from an engine The component `character.motion` of the player (`mode` index, `velocity`, `gravity`, `speedCap`, `seq`) is mirrored by the columns `character.mode@1`, `character.velocity@1`, `character.gravity@1`, `character.speed_cap@1` and `character.mode_seq@1` of the entity rows: read them in place every frame (`gfk_column`, Godot `column`), nothing is copied. It is written when the mode changes, and every tick once the game enabled a mode. `modes()` maps an index to its id (JSON, for setup and tools). On a native host the columns are in the rows when the game carries its game schema (`schema`, written by the export); without it a row has the fixed base only and `column` answers empty. ## Limits - `movement.flight` (fly) and `movement.traversal` (climb, ledge, swim, glide, slide) enter their mode with `enterMode`, so the hook, the event and the accessor cover them; they keep driving the capsule themselves. Grind, grapple and dash do not hold a mode yet. Do not enable both the `swim` preset and traversal swimming in one game. - Volumes are zones (`data.zones`); a terrain water level is not a `swim` volume yet. ## Versions `character.third_person` 1.17.0, `character.first_person` 1.9.0.