Character switching 2D

Godot Version

4

Question

image

As the title suggests, my goal is that I want the user to be controlling the "player node from the start. Then once they input the event “launch”, they switch to the gauntlet node and then begin controlling that.

When the gaunlet is controlled, its starting position will be relative to the player, a bit to the right of it. Then, when controlling the gauntlet, the camera follows its movement and then once the user inputs “launch” the camera and controls go back to the player.

I’m still working on it, would something like a state machine work? I’ve already got one for my player character.

Here’s what I have right now:
Controller.gd:

extends Node

@export var player: NodePath
@export var gauntlet: NodePath

var current_character: CharacterBody2D
var characters: Array

func _ready():
	characters = [
		get_node(player),
		get_node(gauntlet)
	]
	current_character = characters[0]
	controlling(current_character)

func _input(event):
	if Input.is_action_just_pressed("launch"):
		print("switching")
		switch_character()

func switch_character():
	var current_index = characters.find(current_character)
	var next_index = (current_index + 1) % characters.size()
	current_character = characters[next_index]
	controlling(current_character)

func controlling(character):
	for char in characters:
		char.set_process_input(false)
	character.set_process_input(true)

I think this is overcomplicating it.

When I start the game, both characters are on screen and are controlled. I just want it so that you start with the player, and then inputting “launch” kind of just spawns the gauntlet in.

1 Like

I would do it with a state machine but it would be a bit complicated to get into. I would make some changes to the scene as well. So, buckle up buckaroo.

  1. Name the Node to “PlayerController” and put the gauntlet as a child of this rather than of the player. Put one camera as a child of Player and another as a child of Gauntlet. Also add a RemoteTransform as a child of Player.
  2. I would put two state machines in each script of the Player and Gauntlet. One state called ‘Active’ and another ‘Inactive’. Example of a very basic script and state machine which can be used by both Player and Gauntlet :
signal switch_to_gauntlet # connect this to the PlayerController
var can_launch : bool = true # check if player is allowed to launch

enum STATES{ ACTIVE, INACTIVE }
var cur_state = STATES.ACTIVE

func _input(event):
    if Input.is_action_just_pressed("launch") and can_launch:
        switch_to_gauntlet.emit()
        cur_state = STATE.INACTIVE

func _physics_process(delta):
    match cur_state:
        STATES.ACTIVE:
            can_launch = true
            # Any code you want whenever the entity is active
            # Put movement code in here
        STATES.INACTIVE:
            can_launch = false
            # Any code to make the entity inactive
  1. Repeat for the Gauntlet script and don’t forget to add a signal called “switch_to_player” as well as an additional one called “follow_player” and connect it to PlayerController. Put a “follow_player.emit()” where ever you want the player to assume control again.

  2. In PlayerController script:

@export var player: CharacterBody2D # Child of Controller
@export var player_cam: Camera2D # Child of Player
@export var player_remote: RemoteTransform2D # Child of Player
@export var gauntlet: CharacterBody2D # Child of Controller
@export var gauntlet_cam: Camera2D # Child of Gauntlet
var characters : Array[CharacterBody2D] = [player, gauntlet] # Using ": Array[CharacterBody2D]" defines the array as a CB2D Array to only allow CB2D's inside
var active_char : Characterbody = characters[0]

func _ready():
    swap_cam(player_cam, gauntlet_cam) # Initialize camera to follow Player

func swap_cam(new : Camera2D, old : Camera2D):
    new.set_enabled(true)
    new.make_current()
    old.set_enabled(false)

func _on_switch_to_player(): # Signal emitted from Gauntlet
    active_char = characters[0]
    swap_cam(player_cam, gauntlet_cam)

func _on_switch_to_gauntlet(): # Signal emitted from Player
    active_char = characters[1]
    swap_cam(gauntlet_cam, player_cam)
    player_remote.set_remote_path(null) # stops the gauntlet from using the player's transform

func _on_follow_player(): # Signal emitted from Gauntlet
    player_remote.set_remote_path(gauntlet) # gauntlet uses the players transform

There are a ton of different ways to achieve this and by no means am I saying this is the best way or even the correct way. This is simply how I would go about it. Note that I tried to make this as bare bones as possible to where I’m not just giving you the answers straight up. It’s best to experiment and try things that works best for you.

Note on the RemoteTransform2D and Camera2D:
I’m unsure if you can use RT2D and apply an offset like you want. If you can’t. I would put the gauntlet as a child of a Node2D and offset it properly but it would require some changes to the gauntlet script.
I haven’t used the native camera in a while but I believe this is how you swap them. Take everything with a grain of salt.

I also noticed that there’s a bit of redundant code here:

@export var player: NodePath
@export var gauntlet: NodePath
var current_character: CharacterBody2D
var characters: Array

func _ready():
	characters = [
		get_node(player),
		get_node(gauntlet)
	]

You’re creating new references to the same nodes in the array when you can put the ones you have in the ‘characters’ array as shown in my example. You can use NodePath but I’d recommend using the base export with “@export Player : CharacterBody2D” which is what I used in my example. Also you can drag a node from the scene tree and once the drag has started, hold Ctrl, then drop it into a script to automatically create an @onready reference. But that’s just preference, you do you Terrence.

Thanks for the help. I am going to be working on my project using your tips and I had some questions I hope you can help clarify:

  1. “Name the Node to “PlayerController”.” By this you mean the “Controller” node from my image right?

  2. “put the gauntlet as a child of this rather than of the player”. I had both the player and the gauntlet as children of the controller node, though I still do understand what you’re saying.
    So basically this was what you were saying yea?:
    image

  3. As for the active/inactive state. I already have an active state for my player character, with the launching state essentially acting as my inactive state:
    image
    With the code you showed, it looks like the states are all within one script, but I just wanted to confirm that separating them would achieve similar results yea?

Again thanks for the help :pray: I’ll look into some docs for some stuff I wasn’t familiar with like that remotetransform2d node. And i’ll def address that redundancy code :sweat_smile:

1 Like

Glad I could help! Sure thing, I’ll do my best to answer everything. Sorry for the late response.

  1. You can name it whatever you want. I missed you had named it anything other than “Node” so that’s on me. Looking back at the examples, I don’t see the name having any effect on the code.

  2. I see the confusion. Yes, you’re correct. Use the PlayerController as the root node with Player and Gauntlet as separate children exactly as the screenshot. In fact, you don’t even need to have the Player and Gauntlet as children at all as long as the references are correct, signals are connected correctly.

a)That’s a great implementation of a state machine on your player. The one I showed was very basic which I use on lightweight objects like the Gauntlet that might only handle a couple states and doesn’t need any enter_state() / exit_state() functionality. Definitely use your own state machine for your Player and probably the Gauntlet too. It’s a much better implementation that the one I made and even better to integrate it into the states you already have.

b) Now to answer the question. If I understand the question correctly. You want to know whether or not you can incorporate the states into something similar to the states like you have with the player. Where you have a separate script for each state. Yes, that doesn’t matter as long as the state logic is contained in the same state machine.

On a side note. There’s something called hierarchical state machines which I haven’t used much which is basically a state machine within state machine. In your use case, there could be an argument for using it. If you want the player to only launch at the idle state for instance. Then you have a state machine for idle, run, jump etc(just an example). There’s an additional state machine that is only engaged in the idle state which has kind of sub-states called active and inactive. But from what I can tell from your project, I do think that would be overkill currently but might be something to keep in mind in the future perhaps or future projects

No problem, happy to help :wink: If you have anymore questions or if I made something confusing, I’ll try to come back here again later. Happy coding!

So I got a decent way of it done but I feel like I stuck on this last part.

I think I correctly connected all the signals and have them emitting correctly because this print statement “6789” does print just before it crashes. That crash had this output:

image
image

Shouldn’t the RemoteTransform2d point to the player node?

Also, something that is happening is that when the game runs, I see both the player and the gauntlet and am able to control both.

image
What I do want is have the player be the only thing controlled and on screen at the start. Pressing the “launch” action would spawn in the gauntlet. In this case, would using something like an instantiate work? Or even something like having the one character. But recreate the gauntlet inside the character itself. I’ll try this approach, reusing some of the code you provided. Thanks again.

Please let me know if you need more info.

1 Like

I was able to do it! So basially I went with my idea of using instantiate and utilizing the state machines that I had gotten the hang of to handle inputs and state transitions. I was even able to implement a timer that frees the gauntlet node after some time.

Now, if I get more ideas in the future, I could scale my game further.

Thanks so much for the help, I got introduced to some neat concepts that I will definitely look into as I keep working on this!

1 Like

Fantastic! That’s awesome. That’s exactly what I was about to suggest before seeing that you solved it yourself. This is why I tried to keep the code as bare as possible. You get to try stuff out, debug and figure out the logic by yourself. It gives you a better understanding of the code and logic which is a thousand times better than just copy + paste some code.

Great job!