FPS Movement and Aiming in Godot

One of the rudimentary starts for a first person shooter (FPS) game is the ability to move and aim. This tutorial walks through how to quickly setup with support for keyboard/mouse and controller. This tutorial focuses on the script portion. Let’s begin.

Prerequisites

Within your player scene, create a CharacterBody3D node and update your scene tree similar to the following. I recommend starting out with a capsule shape for the collision and mesh as a start.

To support mouse/keyboard and controller, I have the following input maps defined within Project Settings:

You may also noticed that I configured the reduced the deadzones from the default 0.5.

The Script

In Godot, the HORIZONTAL_LOOK_SENSITIVITY and VERTICAL_LOOK_SENSITIVITY will need to be configured within the inspector. I used values 7 and 5 respectfully.

The VERTICAL_LOOK_LOWER_LIMIT and VERTICAL_LOOK_UPPER_LIMIT configure angle limits for looking up and down (don’t want to go in circles).

Copy the following as the player script and try it out!

extends CharacterBody3D

const SPEED: float = 5.0
const JUMP_VELOCITY: float = 4.5
@export var HORIZONTAL_LOOK_SENSITIVITY: float
@export var VERTICAL_LOOK_SENSITIVITY: float
const CONTROLLER_LOOK_MULTIPLIER: int = 50
const VERTICAL_LOOK_LOWER_LIMIT: float = -89
const VERTICAL_LOOK_UPPER_LIMIT: float = 89

var gravity = ProjectSettings.get_setting("physics/3d/default_gravity")
var camera: Camera3D
var mouseDelta: Vector2 = Vector2()

func _ready():
	Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
	camera = get_node("CameraController/Camera3D")

func _input(event):
	if event is InputEventMouseMotion:
		mouseDelta = event.relative

func _physics_process(delta):
	apply_gravity(delta)
	move(delta)
	aim(delta)
	handle_jump()

func apply_gravity(delta):
	if not is_on_floor():
		velocity.y -= gravity * delta
	
func move(delta):
	var input_dir = Input.get_vector("move_left", "move_right", "move_forward", "move_backward")
	var direction = (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()
	if direction:
		velocity.x = direction.x * SPEED
		velocity.z = direction.z * SPEED
	else:
		velocity.x = move_toward(velocity.x, 0, SPEED)
		velocity.z = move_toward(velocity.z, 0, SPEED)
	move_and_slide()

func aim(delta):
	if mouseDelta != Vector2.ZERO:
		mouse_look(delta)
	else:
		controller_look(delta)
	
func mouse_look(delta):
	adjust_camera_look(delta, mouseDelta)
	mouseDelta = Vector2()

func controller_look(delta):
	var aim_dir = Input.get_vector("aim_left", "aim_right", "aim_up", "aim_down")
	adjust_camera_look(delta, aim_dir * CONTROLLER_LOOK_MULTIPLIER)

func adjust_camera_look(delta, look_rotation: Vector2):
	look_rotation *= delta
	camera.rotation_degrees.x -= look_rotation.y * VERTICAL_LOOK_SENSITIVITY
	camera.rotation_degrees.x = clamp(camera.rotation_degrees.x, VERTICAL_LOOK_LOWER_LIMIT, VERTICAL_LOOK_UPPER_LIMIT)
	rotation_degrees.y -= look_rotation.x * HORIZONTAL_LOOK_SENSITIVITY

func handle_jump():
	if Input.is_action_just_pressed("jump") and is_on_floor():
		velocity.y = JUMP_VELOCITY

The _ready function configures the mouse to be captured. You may want to consider and exit button like below:

func _input(event):
	if event.is_action_pressed("exit"):
		get_tree().quit()