Need help with first person

I’m a beginner programmer in godot 4. I need help for my first person horror game. I was working with a tutorial. How do i make the mouse not visible and how do i make so that the mouse when it reaches the side of the window won’t stop the camera from rotating. Also my character doesn’t fall for some reason. Here’s my code:

extends CharacterBody3D

const GRAVITY = 200
const SPEED = 20
@export var jump_velocity = 4.5
var look_sensitivity = 0.001
@onready var camera = $CameraControl
func _physics_process(delta: float) -> void:
	if not is_on_floor():
		velocity.y = -GRAVITY * delta
	var horizontal_velocity = Input.get_vector("Move_Left", "Move_Right", "Move_Forward", "Move_Backwards").normalized() * SPEED
	velocity = horizontal_velocity.x * global_transform.basis.x + horizontal_velocity.y * global_transform.basis.z
	move_and_slide()
	if Input.is_action_just_pressed("ui_cancel"): 
		Input.mouse_mode = Input.MOUSE_MODE_CAPTURED if Input.mouse_mode == Input.MOUSE_MODE_VISIBLE else Input.MOUSE_MODE_VISIBLE
func _input(event):
	if event is InputEventMouseMotion:
		rotate_y(-event.relative.x * look_sensitivity)
		camera.rotate_x(-event.relative.y * look_sensitivity)
		camera.rotation.x = clamp(camera.rotation.x, -PI/2, PI/2)

Seems like your mouse lock is toggled based on “ui_cancel”, I recommend changing this action from “ui_cancel” to a custom one, and split this from a toggled action to separate lock and un-lock

Moving into _unhandled_input allows you to detect any mouse click not handled by UI

func _unhandled_input(event: InputEvent) -> void:
    if event is InputEventMouseButton:
        Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
    elif event.is_action_pressed("UnlockMouse"):
        Input.mouse_mode = Input.MOUSE_MODE_VISIBLE

Your gravity is failing to work because your overriding the xyz components of velocity all at once with this line

If you want to separately modify the horizontal and vertical components you must keep two variables for velocity and combine them just before move_and_slide().

# split velocity
var vertical_velocity := velocity.project(up_direction)
var horizontal_velocity := velocity - vertical_velocity

# vertical-only gravity
if not is_on_floor():
	vertical_velocity += get_gravity() * delta
 
# horizontal-only movement
var input_dir := Input.get_vector("left", "right", "forward", "backward")
var direction := basis * Vector3(input_dir.x, 0, input_dir.y)
horizontal_velocity = horizontal_velocity.move_toward(direction * SPEED, ACCELERATION * delta)

# re-combine velocity components
velocity = horizontal_velocity + vertical_velocity
move_and_slide()
3 Likes

A mouse lock/unlock button is useful in development when you’re debugging. I prefer for games however to just trigger that when you pause/unpause the game. So I put something like this in an autoload or static class:

func _notification(what: int) -> void:
	match what:
		NOTIFICATION_PAUSED:
			Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
			print_rich("[color=red][b]PAUSED[/b][/color]")
		NOTIFICATION_UNPAUSED:
			Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
			print_rich("[color=red][b]UNPAUSED[/b][/color]")
		NOTIFICATION_WM_CLOSE_REQUEST:
			save_game()
			print_rich("[color=red][b]CLOSE REQUEST RECEIVED[/b][/color]")

The print statements aren’t necessary, but I like to know things are working. Also the save game I just left in there as an example of how you can save on exit automatically. So the simplified version would be:

func _notification(what: int) -> void:
	match what:
		NOTIFICATION_PAUSED:
			Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
		NOTIFICATION_UNPAUSED:
			Input.mouse_mode = Input.MOUSE_MODE_VISIBLE

And you can literally put it in any file, so you could just put it in your CharacterBody3D script and it’ll work. Of course, you’ll also need to implement a pause button that uses get_tree().paused. I just do this in my Game autoload:

## Pauses or unpauses the game based on the boolean sent. Defaults to pausing the game.
## [br]Convenience method.
func pause(pause: bool = true) -> void:
	get_tree().paused = pause


## Unpauses the game.
## [br]Convenience method.
func unpause() -> void:
	pause(false)


## Returns whether or not the game is paused.
## [br]Convenience method.
func is_paused() -> bool:
	return get_tree().paused

Then I hook the pause key up to call Game.pause(). If I need to know if the game is paused, I can evaluate Game.is_paused().

2 Likes

(Sorry for late reply) So this actually works except one thing, the acceleration variable is pretty hard for me to understand. I know what it does but when i put it on 1 it feels like my character moves on ice, how can i fix this issue, do i just play with the value untill it works how i want to? Also how do i stop the acceleration when it reaches a certain point, and how do i make so that it will take faster for the character to stop on higher acceleration?

wait so it’s that easy to save progress in game using godot?

You should try using higher values for your acceleration. The acceleration value is the amount of units per second your velocity can change. So, with an acceleration of 1, it would take 20 seconds to reach your previous default speed of 20.

Since your horizontal velocity always moves towards direction * SPEED, the speed value already limits your maximum horizontal velocity.

And the acceleration is also used for slowing down, so increasing its value will make you stop faster too.

No, but it’s not a lot of code. That was an example function. Sorry for the confusion.

However this is all my save game code:

#TODO: Add unit tests.
extends Node

const SETTINGS_PATH = "user://configuration.settings"
const SAVE_GAME_PATH = "user://game.save"

## If this value is On, save_game() will be called when the player quits the game.
@export var save_on_quit: bool = false

var configuration_settings: Dictionary
var game_information: Dictionary
var is_ready = false


func _ready() -> void:
	if FileAccess.file_exists(SETTINGS_PATH):
		configuration_settings = _load_file(SETTINGS_PATH)
	ready.connect(func(): is_ready = true)


func _notification(what) -> void:
	match what:
		NOTIFICATION_WM_CLOSE_REQUEST: #Called when the application quits.
			if save_on_quit:
				save_game()


## Returns true if the save was successful, otherwise false.
## Calls every node added to the Persist Global Group to save data. Works by
## calling every node in the group and running its `save_node()` function, then
## storing everything in the save file. If a node is in the group, but didn't
## implement the `save_node()` function, it is skipped.
func save_game() -> bool:
	var saved_nodes = get_tree().get_nodes_in_group("Persist")
	for node in saved_nodes:
		# Check the node has a save function.
		if not node.has_method("save_node"):
			print("Setting node '%s' is missing a save_node() function, skipped" % node.name)
			continue
		
		game_information[node.name] = node.save_node()
		print("Saving Info for %s: %s" % [node.name, game_information[node.name]])
	return _save_file(game_information, SAVE_GAME_PATH)


## Call this to call the `load_node()` function for every node in the Persist
## Global Group. The save game, if it exists, will be loaded from disk and the
## values propagated to the game objects.
func load_game() -> void:
	game_information = _load_file(SAVE_GAME_PATH)
	if game_information.is_empty():
		return
	var saved_nodes = get_tree().get_nodes_in_group("Persist")
	for node in saved_nodes:
		# Check the node has a load function.
		if not node.has_method("load_node"):
			print("Setting node '%s' is missing a load_node() function, skipped" % node.name)
			continue
		# Check if we have information to load for the value
		if game_information.has(node.name):
			print("Loading Info for %s: %s" % [node.name, game_information[node.name]])
			node.load_node(game_information[node.name])


## Stores the passed data under the indicated setting catergory.
func save_setting(data: Variant, category: String) -> void:
	configuration_settings[category] = data
	_save_file(configuration_settings, SETTINGS_PATH)


## Returns the stored data for the passed setting category.
func load_setting(category: String) -> Variant:
	if !is_ready:
		if FileAccess.file_exists(SETTINGS_PATH):
			configuration_settings = _load_file(SETTINGS_PATH)
	if configuration_settings.has(category):
		return configuration_settings[category]
	return ERR_DOES_NOT_EXIST


## Takes data and serializes it for saving.
func _serialize_data(data: Variant) -> String:
	return JSON.stringify(data)


## Takes serialized data and deserializes it for loading.
func _deserialize_data(data: String) -> Variant:
	var json = JSON.new()
	var error = json.parse(data)
	if error == OK:
		return json.data
	else:
		print("JSON Parse Error: ", json.get_error_message(), " in ", data, " at line ", json.get_error_line())
	return null


func _save_file(save_information: Dictionary, path: String) -> bool:
	var file = FileAccess.open(path, FileAccess.WRITE)
	if file == null:
		print("File '%s' could not be opened. File not saved." % path)
		return false
	file.store_var(save_information)
	return true


func _load_file(path: String) -> Variant:
	if not FileAccess.file_exists(path):
		print("File '%s' does not exist. File not loaded." % path)
		var return_value: Dictionary = {}
		return return_value
	var file = FileAccess.open(path, FileAccess.READ)
	return file.get_var()

I turned it into an autoload you can just use if you like.

1 Like

Oh, thanks! that was really helpful to know, since i’m not really good with code i didn’t know that it was an acceleration to the speed, i thought it was just accelerating it infinitely :sweat_smile:

Wait you’re a person who makes things on github for godot devs!? that’s so cool and i’m really thankful, i will use this in my future projects (or the one i’m working on rn if i’ll think that this will be necessary)!

1 Like

It’s one of the ways I can give back to the open source community. :slight_smile:

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.