• 3D
  • Camera controller, with switching focus, without parenting

I have a very basic setup for a 3d character controller with a camera. This is kept basic to explain my question.

The Setup

Level.tscn Level (Spatial node with Level.gd) - CameraAnchor (Position3D node) - Camera (Camera Node) - Ground (CSGBox node)

Level.gd:
extends Spatial export(PackedScene) var playerPack onready var cam = $CameraAnchor var pawns : Array var activePawn : Spatial func _ready(): populateLevel(4) activePawn = pawns[0] activePawn.isActive = true func _process(delta): handleInput() moveCamera() func moveCamera(): cam.transform.origin = activePawn.transform.origin func setNewActive(ID : int): ID -= 1 #array starts at 0 if pawns[ID]: activePawn.isActive = false activePawn = pawns[ID] activePawn.isActive = true func handleInput(): if Input.is_action_just_pressed("one"): setNewActive(1) elif Input.is_action_just_pressed("two"): setNewActive(2) elif Input.is_action_just_pressed("three"): setNewActive(3) elif Input.is_action_just_pressed("four"): setNewActive(4) func populateLevel(amount : int): pawns = [] pawns.resize(amount) for i in amount: pawns[i] = playerPack.instance() add_child(pawns[i]) pawns[i].transform.origin.x = i * 3

Player.tscn Player (KinematicBody node with Player.gd) - Body (MeshInstance node) - Goggles (MeshInstance node)

Player.gd:
extends KinematicBody var isActive = false var speed = 500 var velocity = Vector3.ZERO func _process(delta): if isActive: handleInput() move_and_slide(velocity.normalized() * speed * delta) func handleInput(): velocity = Vector3.ZERO if Input.is_action_pressed("left"): velocity.x -= 1 if Input.is_action_pressed("right"): velocity.x += 1 if Input.is_action_pressed("up"): velocity.z -= 1 if Input.is_action_pressed("down"): velocity.z += 1

The Problem

When the player moves the camera follows its position, however first the Level.gd updates and moves the camera to the position of the player. After this, player.gd updates and moves the player. This causes the player to seem jittery and not stay in the middle of view when moving.

Possible Solutions

I could try to replace the process(delta) in player.gd with a custom update function and call that in Level.gd before moveCamera(). But then I would need to replace all process(delta) function in scripts where the camera would be able to focus on.

I could reparent the $CameraAnchor to the active player. But then I would have to reparent it everytime I moved the camera focus to a new entity. Also this would make it difficult to have the camera zoom and focus on multiple players on screen in the future.

Desired Solution

As in the example code above I am looking for a solution that is ready for easy switching between entities. Later on I might even add switching to focus view of enviroment or multiple players at once, and even lerp between them. For this it seems like a bad idea to parent the $CameraAnchor to an entity. However pretty much all tutorials I can find use the parenting technique.

Does anyone know of a possible solution? Or maybe some tutorial or example code I could look into?

Try having the camera anchor loose (not a parent) and on the same level as the camera. When you want to switch camera position, set the camera anchor to the relevant position. Then on the camera itself, have a script that uses a Tween or linear_interpolate to smoothly move to the new camera position/rotation.

Thanks for the feedback! I now have a newer version with basic switching/multi-focus/lerp without parenting. The lerping of the camera definitely hides the jitter. I made the lerping a toggle with a keystroke to show the difference.

Although it looks nice, its maybe not really the feel of movement I was going for. So how do other fast paced twitchy games with high precision in Godot make their camera system? I am guessing going back to a workaround with parenting the camera?

Setup V2

Level.tscn Level (Spatial node with Level.gd) - CameraAnchor (Position3D node) - DebugAnchor (MeshInstance node) - CameraFocus (Position3D node) - Camera (Camera node) - Ground (CSGBox node)

Level.gd:
extends Spatial export(PackedScene) var playerPack export(bool) var isSmoothOn = true onready var cam = $CameraFocus var pawns = [] var activePawns = [] var smoothSpeed = 3 func _ready(): populateLevel(4) func _process(delta): handleInput() adjustCamAnchor() moveCamera(delta) func moveCamera(delta): if isSmoothOn: cam.transform.origin = cam.transform.origin.linear_interpolate($CameraAnchor.transform.origin, smoothSpeed * delta) else: cam.transform.origin = $CameraAnchor.transform.origin func togglePawnActive(ID : int): ID -= 1 #array starts at 0 if pawns[ID]: if pawns[ID].isActive: pawns[ID].setActive(false) activePawns.erase(pawns[ID]) else: pawns[ID].setActive(true) if activePawns.find(pawns[ID]) == -1: activePawns.append(pawns[ID]) func adjustCamAnchor(): if activePawns.size() == 0: $CameraAnchor.transform.origin = Vector3.ZERO else: var result = Vector3.ZERO for i in activePawns.size(): result += activePawns[i].transform.origin $CameraAnchor.transform.origin = result / activePawns.size() func handleInput(): if Input.is_action_just_pressed("toggleSmoothCam"): isSmoothOn = !isSmoothOn if Input.is_action_just_pressed("one"): togglePawnActive(1) elif Input.is_action_just_pressed("two"): togglePawnActive(2) elif Input.is_action_just_pressed("three"): togglePawnActive(3) elif Input.is_action_just_pressed("four"): togglePawnActive(4) func populateLevel(amount : int): pawns.resize(amount) for i in amount: pawns[i] = playerPack.instance() add_child(pawns[i]) pawns[i].transform.origin.x = i * 3

Player.tscn

Player (KinematicBody node with Player.gd) - Body (MeshInstance node) - Goggles (MeshInstance node) - BodyCollision (CollisionShape node)

Player.gd:
extends KinematicBody export(Material) var matActive export(Material) var matInactive var isActive = false var speed = 500 var velocity = Vector3.ZERO func _ready(): setActive(false) func _process(delta): velocity = Vector3.ZERO if isActive: handleInput() move_and_slide(velocity.normalized() * speed * delta) func handleInput(): if Input.is_action_pressed("left"): velocity.x -= 1 if Input.is_action_pressed("right"): velocity.x += 1 if Input.is_action_pressed("up"): velocity.z -= 1 if Input.is_action_pressed("down"): velocity.z += 1 func setActive(mode : bool): if mode: isActive = true $Body.set_surface_material(0, matActive) else: isActive = false $Body.set_surface_material(0, matInactive)