- FPSCharacterController: removed SimulationServer dependency, added _rollback_tick() with acceleration-based CharacterBody3D movement, gravity, jump, crouch/sprint speed. Kept mouse look, crouch lerp, sprint/crouch toggle, _move_local() standalone fallback. - Player: extends FPSCharacterController (CharacterBody3D). Added authority-guarded _rollback_tick that delegates to super for server and local client prediction. TickInterpolator created dynamically (avoids headless class_name parse errors). PlayerNetInput child wired via existing RollbackSynchronizer input_properties. - player.tscn: CharacterBody3D root with FpsCamera, CollisionShape3D, PlayerNetInput children. TickInterpolator created in code. - client_main.gd: _spawn_local_player() creates FPS player node with first-person camera for own peer. _spawn_remote_player() removes FpsCamera for remote players. - fps_camera.gd: duck-typed _controller (Node) instead of class_name cast for headless safety. Fixed type inference warnings. Also includes parent task P7.5-P7.7 changes: - project.godot: main_scene -> server_main.tscn - network_manager.gd: Chan enum -> raw channel ints - server_main.gd: deferred ServerConfig load, GameServer load() - game_server.gd: commented out dev deps for headless compilation
FPS Character Controller — Tactical Shooter
First-person character controller for the tactical shooter GDExtension simulation core.
Architecture
The character controller is a GDScript bridge between Godot's input/physics system and the C++ SimulationServer (built in task t_aca0f251).
Godot Input
│
├─► FPSCharacterController._physics_process()
│ ├─ capture input (WASD, mouse, jump, sprint, crouch, shoot)
│ ├─ build EntityInput Dictionary
│ ├─ SimulationServer.apply_input(entity_id, input_dict)
│ ├─ SimulationServer.fire_weapon(entity_id) [if shoot]
│ ├─ read entity position from server → apply to CharacterBody3D
│ └─ apply mouse look (yaw on body, pitch on camera)
│
├─► FpsCamera._process()
│ ├─ view bobbing (head bob from movement)
│ ├─ weapon sway
│ └─ sprint FOV kick
│
└─► SimulationServer (C++ GDExtension)
├─ process_tick() @ 128Hz
├─ MovementComponent.update()
├─ HitDetection.process_shot()
└─→ returns serialized snapshot
Files
| File | Purpose |
|---|---|
character/fps_character_controller.gd |
Main controller (CharacterBody3D). Input capture, simulation bridge, walk toggle, crouch, jump. |
character/fps_camera.gd |
First-person camera (Camera3D). Mouse look pitch, view bobbing, weapon sway, FOV kick. |
input/input_handler.gd |
Documentation of expected input map bindings. |
Scene Setup
-
Create a CharacterBody3D scene:
FPSCharacterController (CharacterBody3D) — script: fps_character_controller.gd ├── FpsCamera (Camera3D) — script: fps_camera.gd ├── CollisionShape3D (CapsuleShape3D) — radius: 0.4, height: 1.8 └── Weapon (Node3D, optional) — child of camera, rest transform tracked -
Configure exports on the root node:
mouse_sensitivity: 0.1–0.2 for typical tactical feelwalk_toggle:true= tap Shift to toggle sprint (CS-style)crouch_toggle:false= hold Ctrl to crouch (tactical-style)eye_height_stand/eye_height_crouch: match your capsule height
-
Set up Input Map in Project Settings:
move_forward→ Wmove_backward→ Smove_left→ Amove_right→ Djump→ Spacesprint→ Shiftcrouch→ Ctrlshoot→ Mouse LMBaim→ Mouse RMBui_cancel→ Esc (built-in, releases mouse capture)
-
Integration with SimulationServer:
# In your game manager (autoload or scene root): var server = SimulationServer.new() server.set_tick_rate(128) server.start() var player_id = server.spawn_entity(Vector3(0, 0, 0)) # When player scene is ready: $Player/FPSCharacterController.set_entity_id(player_id) # In _process(delta): while server.can_tick(delta): var snapshot = server.tick() # send snapshot to network layer
Walk Toggle Behavior
walk_toggle = true(default): Press Shift to toggle between walk and sprint. Press again to toggle off. Same feel as Counter-Strike's walk/run toggle.walk_toggle = false: Hold Shift for temporary sprint. Release to decelerate to walk speed.
Crouch Behavior
crouch_toggle = false(default): Hold Ctrl to crouch. Release to stand up.crouch_toggle = true: Press Ctrl once to crouch, press again to stand.
Crouch transitions are smooth lerps over crouch_transition_time seconds. The collision capsule height and camera eye height both lerp simultaneously.
Standalone Mode
When no SimulationServer singleton is available, the controller falls back to built-in CharacterBody3D movement (_move_local()). This is useful for:
- Testing the character controller in isolation
- Prototyping without the GDExtension compiled
- Single-player scenarios without the full simulation stack
Configure local_walk_speed, local_sprint_speed, local_crouch_speed, etc. for standalone tuning.
Dependencies
- Godot 4.2+ (for
Input.get_vector,move_toward,StringNameliterals) - SimulationServer GDExtension (from task
t_aca0f251) for server-authoritative mode - Input map actions listed above
See Also
- Scaffold:
t_aca0f251—src/movement_component.h(C++ Quake/CS movement physics) - Greybox map:
t_p1_greybox— test environment for this controller - Hitscan weapon:
t_p1_hitscan— weapon firing + lag compensation - Client prediction:
t_p1_pred— client-side prediction + server reconciliation - Round system:
t_p1_round— win/loss/sudden death on top of character controller