Rotate the Camera Toward the Player

Godot Version

4.7

Question

Hello! I was trying out something new (maybe not a great idea because I’m still a little new to programming in gdscript) and was trying to develop a game where the player is stalked by an NPC who follows them. I wanted the game to take place through the NPC’s eyes, making movement more difficult and tense for the player. However I was having trouble trying to make a basic camera which rotates toward the player- it will try to for the most part, but will also have trouble spinning in circles or will even try to look the other way.

I’m hoping to build something more basic, but let me know if this is something i might have to develop like an NPC AI for or something like that.

Most of the main code I put below, it’s attached to a camera on my stalker’s head:

func _process(delta: float) → void:
# the “target” is the player’s current position
# "get_parent() gets the parent node (in this case “watcher”)
# watcher’s XZ + the camera’s Y is the camera’s location
distance = (get_parent().position + position) - Globals.playerLocation

# direction changes whether it turns right or left (+1/-1)
direction.y = distance.x/abs(distance.x)
# there's distance.x, distance.y and distance.z
# use trig to calculate the angle (target) we need to rotate the camera

# calculate target.y (desired rotation around y axis)
if not distance.x == 0 and not distance.z == 0:
	# arctangent (atan) of opposite over adjacent
	# this is the equation to find the angle we need
	# 180 / PI is what you need to multiply it by
	# in order to convert angle from RADIANS to DEGREES
	target.y = atan((distance.x)/(distance.z)) * (180/PI)
	if target.y < 0:
		target.y = 360.0 - target.y
	
	# need_to_turn is how many degrees we need to turn
	need_to_turn = round(target.y) - round(rotation_degrees.y)
	# this alters need_to_turn so that the camera will take
	# the shortest path to achieve target
	if need_to_turn > 180:
		need_to_turn = need_to_turn - 360
	if need_to_turn < -180:
		need_to_turn = need_to_turn + 360

# begin rotation around axis y
# I set it to the target +-10 so that it looks realistic and hopefully to get rid of more finicky effects that set in if it misses the target. Setting it exactly at the target, I had similar issues anyways
	if round(rotation_degrees.y) >= (round(target.y) + 10) or round(rotation_degrees.y) <= (round(target.y) - 10):
		
# this makes the head turn faster if it's going to rotate further, or at least that's the idea. I haven't had a ton of issues with this part of the code, though I do wonder if there's a better way to write it
		if abs(need_to_turn) > 90 and not speed_y >= 5.0:
			speed_y += 0.1
		if abs(need_to_turn) < 45 and not speed_y == 0.5:
			speed_y -= 0.1
	
# This is to reset the rotation when it goes past 360 degrees. I had MORE issues before I implemented this, where the head would spin forever and hit like 400 degrees rotated... I still have issues after. Namely, I think there's an issue with my equations where the target would be at like 400 degrees needed to rotate, which of course my program can't reach since I disabled it.
		if round(rotation_degrees.y) > 360:
			rotation_degrees.y = 0.0
		if round(rotation_degrees.y) < 0:
			rotation_degrees.y = 360
		rotation_degrees.y += (direction.y * speed_y)

If anyone knows of perhaps some better equations or shortcuts I could use, maybe something to help with the bugs, I would appreciate it! :blush:

You do not need NPC AI just to rotate the camera toward the player. The spinning comes from manually calculating and resetting the angle.

@export var turn_speed: float = 2.5

func _process(delta: float) -> void:
    var to_player: Vector3 = Globals.playerLocation - global_position
    to_player.y = 0.0

    if to_player.length_squared() < 0.001:
        return

    var target_yaw: float = atan2(-to_player.x, -to_player.z)
    var new_rotation: Vector3 = global_rotation

    new_rotation.y = rotate_toward(
        new_rotation.y,
        target_yaw,
        turn_speed * delta
    )

    global_rotation = new_rotation

This rotates the camera only around the Y axis and automatically takes the shortest path. Globals.playerLocation must contain the player’s global position.

You would only need navigation or NPC AI if the stalker itself also needs to move and avoid obstacles.

Hope that help you !

You can literally just attach the camera to the player and it will follow the player around…

You can use look_at() function for looking at the player. Call the function every frame, and the camera will always look at the player.
Simply tell the Camera3D to look at the desired global position every frame. You can then tweak things to your liking afterwards.

Written an example code to give you some ideas:

extends Camera3D

## Set this to either the player, 
## or a Marker3D node you want the camera to look at.
## Use the Inspector tab in the editor to set it.
@export var look_position: Node3D
## How fast the camera looks toward the position.
@export var look_speed: float = 5

var target: Vector3

func _ready() -> void:
	if look_position:
		target = look_position.global_position

func _physics_process(delta: float) -> void:
	if look_position:
		target = target.lerp(look_position.global_position, delta * look_speed)
		look_at(target)

If there’s anything else I can help you with, feel free to ask!

Thank you both Asago and Vachivenza! I ended up going more with Asago’s solution (I had actually spent almost a full day looking up equations for this program, and really wanted to make the atan() one work haha) but I really appreciate the simplicity of Vachivenza’s solution and might use it in the future on other projects.

Dragonforge-dev, I knew about that trick but intentionally didn’t want the camera to be connected to the player. If I made the game from the stalker’s POV that way, it would essentially never reach the player, that’s why I didn’t do it.

Thank you all for your replies! It was very helpful :blush:

You could change to a new camera when needed. Main camera on player, while stalked camera on npc.

Calling npc_camera.make_current() or player_camera.make_current() when needed would probably be enough

Glad that help you! Good luck with your project :slight_smile: