I'm just looking to hear some opinions on the best way to handle moving a character in different ways based on the character's situation.

The issue is that when a character is in a different state (i.e. climbing as opposed to walking normally), you want to handle the way input is processed to move the character (i.e. parallel to the surface as opposed to relative to the camera). Generally speaking, the standard approach would be to have flags set when the player enters a certain movement state and check for it when determining how to move the character. These different movement forms could be separated out into individual functions for readability, or handled with branching if statements in a single function. Examples are provided below:

Separate Functions:

func _process(delta)
	if (climbing)
		_move_climbing(delta)
	else
		_move_normal(delta)

func _move_climbing(delta)
	# Code to move character while climbing

func _move_normal(delta)
	# Code to move character normally (i.e. relative to camera)

Branching if Statements:

func _process(delta)
	var movement = Vector3()
	if (climbing)
		# Calculate movement for climbing
	else
		# Calculate movement for normal walking
	
	move_and_slide(movement)

Just wondering if anyone else has made a character controller that required similar methods and if there's any better way to go about moving a character under different conditions.

    You could also do the branching thing via match(godot GDScript equivalent of a switch statement).

    Musicgun47
    You can move (hide) calculation of movement to other class, which will extends "abstract" class. Example:

    file movement_type.gd (autoload asMovementType):

    extends Node
    
    const CLIMBING = "movement-climbing"
    const WALKING = "movement-walking"

    file abstract_movement.gd:

    extends Node
    
    class_name AbstractMovement  # let's pretend it is abstract class :)
    
    var movement_type: String
    
    func _init(movement_type: String):
    	self.movement_type = movement_type
    
    func calc_movement(delta: float, velocity: Vector3):
    	assert(false, "Could not call abstract function")

    file climbing.gd (autoload as Climbing):

    extends AbstractMovement
    
    # autoload
    
    func _init().(MovementType.CLIMBING):
    	pass
    
    func calc_movement(delta: float, velocity: Vector3):
    	var new_velocity := Vector3.ZERO
    	# do some magic and return new velocity
    	return new_velocity

    file walking.gd (autoload as Walking):

    extends AbstractMovement
    
    # autoload
    
    func _init().(MovementType.WALKING):
    	pass
    
    func calc_movement(delta: float, velocity: Vector3):
    	var new_velocity := Vector3.ZERO
    	# do some magic and return new velocity
    	return new_velocity

    file Player.gd (see logic behind movement calculation is hidden, you just call movement.calc_movement function):

    extends KinematicBody
    
    var movement: AbstractMovement = Walking
    
    func _ready():
    	pass
    
    func _process(delta):
    	if Input.is_action_just_pressed("ui_home"):
    		print("switching movement to climbing")
    		movement = Climbing
    	elif Input.is_action_just_pressed("ui_end"):
    		print("switching movement to walking")
    		movement = Walking
    	# or if player bump into ladder, etc...
    	
    	# ...
    	
    	if movement.movement_type == MovementType.CLIMBING:
    		print("climbing mode active")
    	if movement.movement_type == MovementType.CLIMBING:
    		print("walking mode active")
    
    	# ...
    
    	var velocity = Vector3.ZERO
    	# ...
    	var new_velocity = movement.calc_movement(delta, velocity)
    	
    	move_and_slide(new_velocity)

    🙂

    That's a really nice solution and allows for good encapsulation and abstraction. Also keeps the player script cleaner which is nice. I currently have the movement type stored as an enum in the player instead of a separate script, but it works the same I guess.

    AutoLoading seems a bit unnecessary as the player is the only script that needs to access the movement calculations and they also don't need to store any persistent data. I may be wrong though as I haven't used AutoLoad very much.