Curved collision Path3D that will restrict the player's movement towards you within a specified radius (tolerance)

Godot Version

4.5.1

Question

Goal: Cheap good looking scenes for prototypes mechanics

Need to reach: 3d character walking on the 2d photo but flawlessly

first i added root 3d world node, then Sprite3d, rotate it by y-axis, give root node static body and collision shape3d, make ant farm from collision shapes, add camera and player (no gravity in player script), asked gpt to cheap way through this, he said “Add Path3d => Make the curve => In player scipt look for position and if not match tolarance don’t add velocity” i tried that but in my interpretation it didn’t work, can somebody help me?

(player has decrease for up down for imitation of distance and approach)

(If it can’t be done so easily, then it would have been easier to build the desired shape from rectangles from the very beginning; it would have taken less time than searching for a solution in a “quick” way :skull:)

If you share code, please wrap it inside three backticks or replace the code in the next block:

extends CharacterBody3D

const SPEED = 5.0 # Твоя базовая скорость

@export var path: Path3D
@export var road_half_width := 1.5

@onready var anim_player = $"Clean New Male Ultimate/AnimationPlayer"
@export var min_size: float = 0.3
@export var max_size: float = 1.2

func _ready() -> void:
	print("!!! СТАРТ: Теперь я РЕАЛЬНЫЙ CharacterBody3D, скрипт ожил !!!")

func _physics_process(delta):
	# Обнуляем скорость по X и Z перед каждым кадром, 
	# чтобы персонаж останавливался, если кнопку отпустили
	velocity.x = 0
	velocity.z = 0
	velocity.y = 0
		# Set absolute scale (doubles the original size)
		
	# Вычисляем текущую скорость прямо здесь.
	# Если scale.x = 0.5, то скорость будет 5 * 0.5 = 2.5
	# Если scale.x = 1.0, то скорость будет 5 * 1 = 5

	
	
	#там чел сказал закоментить и норм будет склей если
	# scale = Vector3(1.0, 1.0, 1.0)
	
	# Прямые проверки через just_pressed (или просто pressed, если хочешь удержанием)
	# Если хочешь, чтобы бежал пока держишь — меняй на is_action_pressed
	if Input.is_action_pressed("ui_left"):
		velocity.x = -SPEED
	if Input.is_action_pressed("ui_right"):
		velocity.x = SPEED
	if Input.is_action_pressed("ui_up"):
		velocity.y += 0.5
		
		scale = scale - Vector3(0.1, 0.1, 0.1)
		if scale.y < min_size:
			scale = Vector3(min_size, min_size, min_size)

	if Input.is_action_pressed("ui_down"):
		velocity.y -= 1.5
		
		scale = scale + Vector3(0.1, 0.1, 0.1)
		if scale.y > max_size:
			scale = Vector3(max_size, max_size, max_size)

	# ===== Ограничение дорожкой =====

	var next_pos = global_position + Vector3(velocity.x, 0.0, velocity.z) * delta

	var curve = path.curve
	var offset = curve.get_closest_offset(next_pos)
	var center = curve.sample_baked(offset)

	if next_pos.distance_to(center) > road_half_width:
		print('я пытаюсь ограничить но не получается')
		velocity.x = 0
		velocity.z = 0

# ===============================
	# Применяем движение
	move_and_slide()

There is close to no motivation to review ai slop here in the forum.
Why don’t you post this back to ChatGPT?

I don’t know if this is correct, I haven’t reviewed it.
To get at least some response, here’s the answer from Gemini:

__

Here is a corrected version of your script and an explanation of why the path
limitation and scaling weren’t working as expected.

What went wrong:

  1. Local vs. Global Coordinates: Path3D’s curve functions (get_closest_offset
    and sample_baked) work in local space. If your Path3D node is not located
    exactly at (0, 0, 0) with no rotation, passing global_position into them and
    comparing the result directly to your global position will cause incorrect
    distance calculations. You need to convert the coordinates using to_local()
    and to_global().
  2. Missing Y-Velocity in Calculations: In your original script, you changed
    velocity.y when pressing Up/Down, but when calculating next_pos for the path
    check, you hardcoded 0.0 for the Y-axis. This meant the path check
    completely ignored any vertical movement.
  3. Framerate-Dependent Scaling: Changing the scale by 0.1 directly on button
    press in _physics_process makes the character grow/shrink extremely fast (in
    just a few frames). A much more robust way for pseudo-3D is to tie the
    player’s scale directly to their vertical/depth position using remap().

The Fixed Script

Here is the updated script. It fixes the coordinate space mismatch, includes the
Y velocity in the path check, and automates the scaling based on how “high” (far
back) the player is on the screen.

extends CharacterBody3D

const SPEED = 5.0

@export var path: Path3D
@export var road_half_width := 1.5

@onready var anim_player = $"Clean New Male Ultimate/AnimationPlayer"
@export var min_size: float = 0.3
@export var max_size: float = 1.2

# Tweak these values to match the bottom and top boundaries of your walkable screen area:
@export var min_y_position: float = -5.0 # Bottom of the screen (closest to camera, largest scale)
@export var max_y_position: float = 5.0  # Top of the screen (furthest from camera, smallest scale)

func _ready() -> void:
	print("!!! СТАРТ: Скрипт ожил !!!")

func _physics_process(delta: float) -> void:
	# Reset velocity each frame
	velocity = Vector3.ZERO

	# Movement inputs
	if Input.is_action_pressed("ui_left"):
		velocity.x = -SPEED
	if Input.is_action_pressed("ui_right"):
		velocity.x = SPEED
	if Input.is_action_pressed("ui_up"):
		velocity.y = SPEED
	if Input.is_action_pressed("ui_down"):
		velocity.y = -SPEED

	# --- Dynamic scaling based on Y-position (Depth) ---
	# This automatically scales the player based on how high up they are on the screen.
	var depth_ratio = remap(global_position.y, min_y_position, max_y_position, 1.0, 0.0)
	depth_ratio = clamp(depth_ratio, 0.0, 1.0)
	var current_scale = lerp(min_size, max_size, depth_ratio)
	scale = Vector3(current_scale, current_scale, current_scale)

	# Optional: Slow down movement when scale is smaller (further away)
	var movement_speed = velocity * current_scale

	# --- Path Limitation Logic ---
	if path:
		var next_pos_global = global_position + movement_speed * delta
		
		# 1. Convert global target position to the Path3D's local coordinates
		var next_pos_local = path.to_local(next_pos_global)
		
		# 2. Find the closest point on the curve in local space
		var curve = path.curve
		var offset = curve.get_closest_offset(next_pos_local)
		var center_local = curve.sample_baked(offset)
		
		# 3. Convert that point back to global space for an accurate distance check
		var center_global = path.to_global(center_local)
		
		# 4. Check distance in global 3D space
		if next_pos_global.distance_to(center_global) > road_half_width:
			# If the next move would take us too far from the path, stop moving
			movement_speed = Vector3.ZERO
		
		velocity = movement_speed
	else:
		velocity = movement_speed

	# Apply movement
	move_and_slide()

An Alternative Approach: Colliders vs. Paths

As you noted, if path-following math gets too tedious, building the boundary
from simple 3D collision boxes (the “ant farm”) is actually a very common and
practical way to handle this.

Instead of writing custom code to keep the player on a curve, you can:

  1. Put static colliders (invisible StaticBody3D nodes with CollisionShape3D
    boxes) on the left, right, top, and bottom limits of your background image.
  2. Let Godot’s built-in physics engine and move_and_slide() handle the
    collisions automatically.
  3. Use a simple script to handle just the player scaling based on their Y (or
    Z) position.

Often, the collider approach is much more forgiving when adjusting level layouts
later on.
__