Character possession

Godot Version

4.6.2

Question

I need a way to swap control to vehicles, characters, and enemies of different class types. So I moved input commands to a player controller that possesses other objects with a “possessable” component attached to them.

It works, but I realized a flaw. By having a single possessable component, it makes per-possessable-character logic awkward (vehicle powers down, character triggers sit animation, etc). I also realize it’s a bit odd that I’m possessing a component on characters, rather than the characters directly.

So now I’m now thinking it would make more sense to have all characters/vehicles inherit from a base class that has possession logic. But I’d like to know; Is there’s a better way to go about this entirely?

EDIT: And now I’ve learned what multiple inheritance is, and that GDScript doesn’t support it…



PlayerController singleton:

extends InputProvider
## Routes player input to the currently possess character or vehicle
## Overrides getter methods from InputProvider that playable characters
## know to look at for input


# Updates the target for the follow cam rig
signal possession_changed(new_subject: Node3D)
var possessed_subject: Node = null


## Called externally - depossess current subject if any, and possess new subject
func possess(target: Node) -> void:
	if possessed_subject:
		possessed_subject.on_depossess()
	possessed_subject = target
	possessed_subject.on_possess(self)
	possession_changed.emit(target.get_parent()) # follow cam targets the character root


func get_move_direction() -> Vector3:
	return Input.get_vector("move_left", "move_right", "move_forward", "move_backward")


func get_jump_action() -> bool:
	return Input.is_action_pressed("jump")

The Possessable component:

class_name Possessable 
extends Node
## Provides the ability for character/vehicle/etc to be possessed


var character: Node = null


func _ready() -> void:
	character = get_parent()
	character.set_physics_process(false)


func on_possess(new_controller: PlayerController) -> void:
	character.is_possessed = true
	character.input_provider = new_controller
	character.set_physics_process(true)


func on_depossess() -> void:
	character.is_possessed = false
	character.input_provider = null
	character.set_physics_process(false)

You could make the posessable class a base class for subcalsses specific to character types. Like:

class_name PosessableVehicle
extends Posessable

func on_possess(new_controller: PlayerController) -> void:

    # Vehicle specific code here

    super.on_possess(new_controller) # call on_possess in the base class to run the non-vehicle specific code

But just an idea.

Yeah that was the direction I was thinking, but I’ve just learned about multiple inheritance and that it’s unsupported in GDScript. So I can’t have a CharacterBody3D, VehicleBody3D, etc inherit from this.

Though I suppose it’s simple enough code to just type it out for each base class and be done with it.

Yeah, I meant if you keep it as a component, not as a base class for the characters themselves, then it could work.

edit: I think it’s normal to use components like this, character scripts can become huge and messy if you want everything in the attached script. But that’s just my opinion. :slight_smile: There might be better way to do it..

I see. Well that’s certainly one option to consider, and would mean only needing to maintain one source of the, admittedly simple, logic. I’ll have to think on it. I’m still having second thoughts about composition for this. Especially as I think about what would go inside the on_possess and on_depossess methods. The component would either need to know more about its parent to have it perform various actions, or call some methods on the parent, in which case, I might as well just have the possession related methods directly on the parent.

By your description, what happens when something is possessed differs between possessable entity types. So the functionality cannot be reduced to a single unified component. Best to handle possession logic separately for each distinct type of possessed entity and then just switch that logic on or off depending on the possession status. If you want to handle it via node components, you’ll need multiple types of possessable components.

Does this even make sense as a component? It would either need to know about its sibling components (eg play “sit_down_in_vehicle” animation) or call an on_possess() method on the parent. But then if I define on_possess() on the parent, I might as well skip the component all together.

My current thinking is that I should self impose a policy that both vehicle and character base classes will have on_possess(): pass and on_depossess(): pass, then overwrite as needed. Is there any reason this wouldn’t be a good idea? I know repeating code isn’t good practice, but what about simply defining some methods?

It totally doesn’t have to be a component. If from your current perspective it looks good to do it by implementing some functions - just do that. You can always refactor into components later.

Nothing wrong with a bit of repetition. It’s always better to have some repetition than to pick a wrong set of abstractions. DRY principle is often misunderstood. Here’s a quote (I forgot the source):

DRY isn’t about code - it’s about eliminating duplication of knowledge and intent - expressing the same idea in different ways across your system.

Got it, then I’ll go with my gut. I think I just needed to hear that, to be honest.

I’ve noticed from many of your replies on this forum that you’re all about making shit work, rather than over analyzing the problem. That’s something I get stuck on sometimes and I end up thinking about code instead of writing it.

From a fellow “it just ain’t right!” programmer:

Move input_provider to possessable, character no longer worries who or what is giving him input.

Expose signals on possessable node:

signal possessed()
signal depossessed()

func attach_possession_hooks (possess_func: Callable, depossess_func: Callable):
	possessed.connect(possess_func)
	depossessed.connect(depossess_func)

func _ready() -> void:
	character = get_parent()
	character.set_physics_process(false)
	if (character.possession_hooks != null):
		possessed.connect(possess_hooks.possessed
func on_possess(new_controller: PlayerController) -> void:
	character.is_possessed = true
	character.input_provider = new_controller
	character.set_physics_process(true)
	possessed.emit()
func on_depossess() -> void:
	character.is_possessed = false
	character.input_provider = null
	character.set_physics_process(false)
	depossessed.emit()

Character still doesn’t need possession logic but it can optionally include its own possessed and unpossessed transition behaviour:

class_name CoffeePot extends CharacterBody3D


func _ready():
	...
	# you can include or omit this, it doesn't rely on inheritance.
	$"possessable".attach_possession_hooks(
	(func():
		if (possessor_is_bot):
			trigger_blink_lights_yellow()
		else:
			trigger_blink_lights_blue()
		automated = true)),
	(func():
		trigger_blink_lights_red()
		automated = false))
	)

Alternatively you can make possessed and depossessed callables instead of signals and set each one individually as properties from the character script, or do it the caveman way and manually attach the signals to from the signals tab on the editor.

That’s already the job of input_provider. It’s the base class of anything that provides input to a character, be it PlayerController, or enemy/npc AI.

It implements..

class_name InputProvider 
extends Node

func get_move_direction() -> Vector3:
	return Vector3.ZERO

func get_jump_action() -> bool:
	return false

# etc...

Then PlayerController/AI defines them..

class_name PlayerController 
extends InputProvider

func get_move_direction() -> Vector3:
	var move_input: Vector2 = Input.get_vector("move_left", "move_right", "move_forward", "move_backward")
	return _camera_relative_direction(move_input)


func get_jump_action() -> bool:
	return Input.is_action_pressed("jump")

# etc...

At any rate, I’m going to stick with what I described in my last post for now. It works and the logic of it tickles my brain in the right way.