Makes sense, hopefully this code helps:
CharacterBody3D:
extends CharacterBody3D
@export var move_speed := 6.8
@export var rotation_speed := 8.6
@export var gravity := 0.0
@export var deceleration := 36.0 # higher = shorter slide
const Z_COMP := sqrt(2.0)
# our “north star” yaw for the box
var desired_yaw: float
func _ready():
# start off facing however the box is rotated in the editor
desired_yaw = $Box4.rotation.y
func _input(event):
# only catch key-presses for our movement actions
if event is InputEventKey and event.pressed:
for action in ["move_left","move_right","move_down","move_up"]:
if event.is_action(action):
_update_desired_yaw()
break
func _update_desired_yaw():
# calculate the current input vector & camera-flattened dir
var iv = Input.get_vector("move_left","move_right","move_down","move_up")
if iv == Vector2.ZERO:
return # no movement
var cr = $"../../SubViewport/CameraRig/Camera3D".global_transform.basis.x
var cf = -$"../../SubViewport/CameraRig/Camera3D".global_transform.basis.z
cr.y = 0; cr = cr.normalized()
cf.y = 0; cf = cf.normalized()
# stretch forward before normalizing for your √2 world-scale
var raw = cr * iv.x + cf * (iv.y * Z_COMP)
var d = raw.normalized()
desired_yaw = atan2(d.x, d.z)
func _physics_process(delta):
# — velocity XZ —
var iv = Input.get_vector("move_left","move_right","move_down","move_up")
if iv != Vector2.ZERO:
# build dir & apply full-speed immediately
var cr = $"../../SubViewport/CameraRig/Camera3D".global_transform.basis.x
var cf = -$"../../SubViewport/CameraRig/Camera3D".global_transform.basis.z
cr.y = 0; cr = cr.normalized()
cf.y = 0; cf = cf.normalized()
var raw = cr * iv.x + cf * (iv.y * Z_COMP)
var d = raw.normalized()
velocity.x = d.x * move_speed
velocity.z = d.z * move_speed * Z_COMP
else:
# brake evenly
velocity.x = move_toward(velocity.x, 0, deceleration * delta)
velocity.z = move_toward(velocity.z, 0, deceleration * delta)
# — always lerp the box toward desired_yaw —
var box = $Rogue
var current = box.rotation.y
var diff = wrapf(desired_yaw - current, -PI, PI)
var step = rotation_speed * delta
if abs(diff) <= step:
box.rotation.y = desired_yaw
else:
box.rotation.y = lerp_angle(current, desired_yaw, step)
# — gravity + slide —
velocity.y -= gravity * delta
move_and_slide()
# — animation state —
_update_animation_state()
func _update_animation_state():
var sprite = $AnimatedSprite3D
if not sprite:
return
var moving = abs(velocity.x) > 0.1 or abs(velocity.z) > 0.1
# use GDScript’s ?: ternary, not Python’s if-else
var want_anim = moving if "walk" else "idle"
if sprite.animation != want_anim:
sprite.animation = want_anim
sprite.play()
CameraRig Node3D:
extends Node3D
@export var move_speed: float = 60.0 # How fast the camera rig position lerps towards player
@export_range(0, 50) var orbit_speed: float = 4.0
var _target_orbit := rotation.y
@export var circular_radius: float = 0.0
@export var circular_speed: float = 0.2
var _circ_angle: float = 0
@export var player: Node3D # Assign your player node in the inspector
@onready var cam: Camera3D = $Camera3D
func _process(delta: float) -> void:
if player:
# Lerp the CameraRig's position towards the player
global_position = global_position.lerp(player.global_position, 1.0 - exp(-move_speed * delta))
rotation.y = lerpf(rotation.y, _target_orbit, 1.0 - exp(-orbit_speed * delta))
if absf(rotation.y - _target_orbit) < 0.02:
rotation.y = _target_orbit
# Optional: move camera in a small circle around rig
#_circ_angle -= TAU * circular_speed * delta
#cam.position.x = cos(_circ_angle) * circular_radius
#cam.position.y = sin(_circ_angle) * circular_radius
Camera3D:
class_name Camera3DTexelSnapped3
extends Camera3D
@export var snap := true
@export var snap_objects := true
var texel_error := Vector2.ZERO
@onready var _prev_rotation := global_rotation
@onready var _snap_space := global_transform
var _texel_size: float = 0.0
var _snap_nodes: Array[Node]
var _pre_snapped_positions: Array[Vector3]
func _ready() -> void:
RenderingServer.frame_post_draw.connect(_snap_objects_revert)
func _process(_delta: float) -> void:
# rotation changes the snap space
if global_rotation != _prev_rotation:
_prev_rotation = global_rotation
_snap_space = global_transform
_texel_size = size / float((get_viewport() as SubViewport).size.y)
# camera position in snap space
var snap_space_position := global_position * _snap_space
# snap!
var snapped_snap_space_position := snap_space_position.snapped(Vector3.ONE * _texel_size)
# how much we snapped (in snap space)
var snap_error := snapped_snap_space_position - snap_space_position
if snap:
# apply camera offset as to not affect the actual transform
h_offset = snap_error.x
v_offset = snap_error.y
# error in screen texels (will be used later)
texel_error = Vector2(snap_error.x, -snap_error.y) / _texel_size
if snap_objects:
_snap_objects.call_deferred()
else:
texel_error = Vector2.ZERO
func _snap_objects() -> void:
_snap_nodes = get_tree().get_nodes_in_group("snap")
_pre_snapped_positions.resize(_snap_nodes.size())
for i in _snap_nodes.size():
var node := _snap_nodes[i] as Node3D
var pos := node.global_position
_pre_snapped_positions[i] = pos
var snap_space_pos := pos * _snap_space
var snapped_snap_space_pos := snap_space_pos.snapped(Vector3(_texel_size, _texel_size, 0.0))
node.global_position = _snap_space * snapped_snap_space_pos
func _snap_objects_revert() -> void:
for i in _snap_nodes.size():
(_snap_nodes[i] as Node3D).global_position = _pre_snapped_positions[i]
_snap_nodes.clear()
Display Control:
extends Control
@export var viewport: SubViewport
@export var pixel_movement := true
@export var sub_pixel_movement_at_integer_scale := true
@onready var _sprite: Sprite2D = $Sprite2D
func _process(_delta: float) -> void:
var screen_size := Vector2(get_window().size)
# viewport size minus padding
var game_size := Vector2(viewport.size - Vector2i(2, 2))
var display_scale := screen_size / game_size
# maintain aspect ratio
var display_scale_min: float = minf(display_scale.x, display_scale.y)
_sprite.scale = Vector2(display_scale_min, display_scale_min)
# scale and center control node
size = (_sprite.scale * game_size).round()
position = ((screen_size - size) / 2).round()
# smooth!
if pixel_movement:
var cam := viewport.get_camera_3d() as Camera3DTexelSnapped3
var pixel_error: Vector2 = cam.texel_error * _sprite.scale
_sprite.position = -_sprite.scale + pixel_error
var is_integer_scale := display_scale == display_scale.floor()
if is_integer_scale and not sub_pixel_movement_at_integer_scale:
_sprite.position = _sprite.position.round()
Structure:
Node3D
|- Level
|- CharacterBody3D
|- SubViewport
|- CameraRig
|- Camera3D
|- Display
|- Sprite2D