Movement modes
The movement modes of the controlled character (first and third person): walk, fly, swim, glide, space and the game's own modes as parameter sets of the same controller; enable and tune them, listen to character.mode, refuse or redirect a change with the hook, add a mode from config, data or code, read the mode in place from an engine.
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.
{ "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:<tag>). 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:<name>, volume:<tag>), 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.
{ "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" } }
] }
<!-- test: kits/character_third_person/tests/movement_modes.rs -->
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:
{ "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):
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:
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<KitEvent>) {
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<String> {
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) andmovement.traversal(climb, ledge, swim, glide, slide) enter their mode withenterMode, 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 theswimpreset and traversal swimming in one game.- Volumes are zones (
data.zones); a terrain water level is not aswimvolume yet.
Versions #
character.third_person 1.17.0, character.first_person 1.9.0.