Godot Version 4.6.1
Question : Parallax background decors in orthographic 3D, position resets incorrectly after player death/respawn
Hello there. I’m making a top-down 3D space shooter in Godot 4.6 with an orthographic camera (angle -75°, size-based zoom). I have background decorative meshes in Zone 2 that should parallax slightly as the player moves (mult ~0.97). They work fine on first visit, but after the player dies, respawns in Zone 1, and warps back to Zone 2, the decors appear at a different screen position every time. ( the game is open world with zones far away from each other )
Here what i’ve tried :
Capturing the global_position at _ready() and resetting to it on zone re-entry. But position drifts because the node isn’t fully placed yet at _ready()
Capturing position on first recentrer_sur_joueur() call instead, same results
Using player position as offset reference. Breaks because player dies at different positions each time
Disabling culling via extra_cull_margin since decors were being culled due to Y depth (-1665 vs player Y=0)
Moving decors to Y=0 — player flies through them and gets blinded by their lights
The debug logs show the decors always reset to the exact same global_position, so the script logic is correct. But visually they appear at different screen positions
Is there a clean way to handle parallax background objects in a 3D orthographic top-down game that survives player death/respawn/zoom changes? Or should I approach this differently entirely?
I’ve tried switching to perspective view with a high camera height and a low fov. But the parallax effect is limited, i can’t make objets apprear very far away just like a “mult”: 0.98},” would do.
extends Node3D
@export var mult : float = 0.97
var _joueur : Node3D = null
var _position_precedente : Vector3 = Vector3.ZERO
var _actif : bool = false
var _position_scene : Vector3 = Vector3.ZERO
var _position_capturee : bool = false
func _ready() -> void:
_joueur = get_tree().get_first_node_in_group("player")
_desactiver_culling(self)
print("[DECORS] _ready sur : ", name, " | global_position=", global_position)
func _desactiver_culling(noeud: Node) -> void:
for child in noeud.get_children():
if child is GeometryInstance3D:
child.extra_cull_margin = 10000.0
func recentrer_sur_joueur() -> void:
print("[DECORS] recentrer_sur_joueur appelé sur : ", name, " | global_position AVANT=", global_position, " | _position_capturee=", _position_capturee)
if not _position_capturee:
_position_scene = global_position
_position_capturee = true
print("[DECORS] position_scene capturée : ", _position_scene)
global_position.x = _position_scene.x
global_position.z = _position_scene.z
print("[DECORS] global_position APRÈS reset=", global_position)
_actif = false
await get_tree().physics_frame
await get_tree().physics_frame
if is_instance_valid(_joueur):
_position_precedente = _joueur.global_position
_actif = true
func joueur_sorti() -> void:
_actif = false
if _position_capturee:
global_position.x = _position_scene.x
global_position.z = _position_scene.z
func _process(_delta: float) -> void:
if not _actif:
return
if not is_instance_valid(_joueur):
_joueur = get_tree().get_first_node_in_group("player")
return
var deplacement := _joueur.global_position - _position_precedente
if deplacement.length_squared() > 0.0001:
global_position.x += deplacement.x * mult
global_position.z += deplacement.z * mult
_position_precedente = _joueur.global_position