How to manage different input situations

Hi guys,
I am making a game in which the player will be able to have 3 situations in which inputs should trigger different actions than in the actions. These 3 are:

  1. piloting a ship
  2. controlling the players avatar
  3. building a ship (in a sort of constructino view)

Now, I have a current system that works for now, but I get more and more confused, so I wanted to know what is the best approach for this. Here is what It needs to be able to do:

When the player changes the situation (i.e. stop piloting and start walking around) the inputs should only affect the current situation. That means: If “W” triggers a thruster on the ship and the player walking forward, the first should only happen when I am piloting the ship, and the second should only happen when I controll the avatar (similarly for the ship building “situation”).

Do I need differnt pawns for this? Or Input Controllers? If you have examples, I would prefer C++ over Blueprint.

Thanks for your help!

Edit: So I dug a bit, and found this:

If you want to switch between multiple Playable Characters in your game or you have a situation where you want a Character to enter and control a Vehicle (car, plane, tank, etc.) or other Pawn that has unique controls (like a mounted machine gun a player can control), you will want to take Possession of the Character or Pawn in order to pass input information to it.


The PlayerController is what is responsible for directing a Pawn and Possession of a Pawn requires a PlayerController to be specified. Pawns themselves do not need to be a humanoid character and can be anything that you want to apply basic movement to and allow a player to control. Characters on the other hand are a form of Pawn that includes Collision and a CharacterMovementComponent by default which allows it to do basic human-like movement.

This sound like what I need (possessing the avatar, the ship, and the ship-building-component), but it is unclear to me what exactly the Player Controller needs to do in these situations. Do I need to filter the input for the unpossesed pawns in the player controller? I am still confused.

The typical UE4-recommended setup is to use multiple PlayerControllers and switch between them.

I prefer having one PlayerController as my “InputController”. I have turned my PlayerController into a complete input management system with events though so it would be too difficult to use as an example.

If you’d prefer to keep input handled by one PlayerController, rather than switching out to other PlayerControllers, you could store a state for the input mode representing each input mode/type (Vehicle/Pilot, Character, Build/Construction) in the PlayerController you are using for input. This way, when you switch input mode you just set the current input mode state; when you receive input through binds, you can then do a check for what the current input mode is and call the correct function/event.

Don’t be hesitant to make input binds that may be bound to keys used by another input mode as well. If you want to make binds for each input mode, you simply do a check on the current input mode state when the bound function is called to ensure it is the correct input mode.

Example of using the same bind for multiple input modes/types:

void APlayerInputController::SetupInputComponent()
{
    Super::SetupInputComponent();
   
    check(InputComponent); // InputComponent is inherited; making sure it is valid before using it.

    InputComponent->BindAxis("MoveForward", this, &APlayerInputController::MoveForward);
}

void APlayerInputController::MoveForward(float _axisAmount)
{
    if (CurrentInputMode == EInputModeType::Character) // where CurrentInputMode is a member variable created in header file of type EInputModeType; EInputModeType is an enum you would create containing each mode.
    {
        Character->MoveForward(_axisAmount); // where Character is a member variable pointer to the character created in the header file.
    }
    else if (CurrentInputMode == EInputModeType::Pilot)
    {
        Ship->ApplyForwardThrusters(_axisAmount); // where Ship is a member variable pointer to the ship created in the header file.
    }
}

Example of using seperate binds for multiple input modes/types:

void APlayerInputController::SetupInputComponent()
{
    Super::SetupInputComponent();
   
    check(InputComponent); // InputComponent is inherited.

    InputComponent->BindAxis("CharacterMoveForward", this, &APlayerInputController::CharacterMoveForward);
    InputComponent->BindAxis("PilotMoveForward", this, &APlayerInputController::PilotMoveForward);
}

void APlayerInputController::CharacterMoveForward(float _axisAmount)
{
    // Ensure the current input mode is Character, otherwise ignore input:
    if (CurrentInputMode == EInputModeType::Character)
    {
        Character->MoveForward(_axisAmount); // where Character would be a reference to the character.
    }
}

void APlayerInputController::PilotMoveForward(float _axisAmount)
{
    // Ensure the current input mode is Pilot, otherwise ignore input:
    if (CurrentInputMode == EInputModeType::Pilot)
    {
        Ship->ApplyForwardThrusters(_axisAmount);
    }
}

Having separate binds for each allows for player (and future personal) customisation of input binds without having to worry about affecting the bindings for another input type.

I also have input binds setup for each input device I use so that I can change how input is handled depending on the device (mainly for how input is activated - eg. I may have the player do an emote when holding B on a controller while on keyboard you just tap the ‘J’ key instead of having to also hold it). That also allows me to easily know what device type is being used to determine if I should display control feedback for a keyboard & mouse, gamepad or another device (eg. “Press the ‘A’ button to jump” for controller compared to “Press spacebar to jump” for keyboard).

EDIT:
Thought it would be worth adding for more clarity - wherever you change to pilot a vehicle or return to being on foot, you can either call a function directly on your PlayerInputController to notify it that the input mode has changed or use an event.

void AMyCharacter::EnterShip() // example function in your character class that may be called when you are transitioning to pilot the ship.
{
    PlayerInputControllerReference->ChangeInputMode(EInputModeType::Pilot); // where PlayerInputControllerReference is a member variable pointer in the character header file.
}

void AMyCharacter::ExitShip()
{
    PlayerInputControllerReference->ChangeInputMode(EInputModeType::Character);
}


void APlayerInputController::ChangeInputMode(EInputModeType _newInputMode)
{
    CurrentInputMode = _newInputMode;
}

The edit you made is an example of how to switch from hosting the character to another object.

One of the implied use cases for this is to directly affect objects with similar input by using things like ControlRotation in the PlayerController.
Another use case is having input functionality in the pawns themselves, where you use GetPawn() to get the currently possessed pawn and then cast the pawn to its specific class so that you can call pawn-specific methods from the input binds in the PlayerController.

Let me know if you do not follow that 100% or if you need further example. I need to head to bed but if you haven’t figured it out when I get up or if no one else has offered examples I’ll try and help you with that approach if you would like to implement specific input handling via separate pawns.

Glad to hear it was easy to follow.

In terms of downsides of multiple PlayerControllers, I never actually ended up attempting to use multiple as when I initially looked it up in 2014 it seemed convoluted and not very runtime-friendly.

In terms of changing PlayerController - there is a “SwapPlayerControllers” function in the GameMode class. You would likely want or need to create a game mode deriving from the base AGameMode class to make use of this; though I have heard people having issues with swapping PlayerController over the years and as I have not attempted it myself there may be a better approach.
For binds - I’m not sure sorry.

On top of the “convoluted and not very runtime-friendly” issue that led me to the single-PC solution there is also my preference as a programmer to encapsulate all input handling into one input handling system. That is why I chose to stick with one PlayerController without handling input inside of pawns/characters.

I have implemented pawn/character input handling in the past but I found it gets messier the more complex input becomes and most of your input handling ends up being done in the PlayerController anyway.

If you would like to separate input for each input mode but retain a single PlayerController for simplicity, you can always create a custom sub-class for each input mode to have specific input handling contained within them to trim down the PlayerController implementation (this could be done through deriving from UObject so that it is still a UE4 class).
That may make it feel tidier and easier to work with, where you would just create and store the sub-classes in the PlayerController and then could bind the input to the specific sub-class (you would still need some sort of state so the sub-class knows it can act on input in case the same input keys/buttons are bound to multiple input modes).

I can give an example of how to setup that latter one too if you like but I really do need to get to bed first :stuck_out_tongue:

This is a great answer! You elaborated greatly on the single-PlayerController solutions (it really helps to just see some example code to see what is meant to be done in a class), but what are the downsides of using multiple player controllers? I have some questions on that.

  • If I were to change the player controller ingame, would Bound Axes and Actions still be bound? e.g. if this is in player controller 1:
    InputComponent->BindAxis("CharacterMoveForward", this, &APlayerInputController::CharacterMoveForward);
    and I changed to using player controller 2, will the bind persist, or does it get deleted when I change the player controller, or do I have to do that manually? Will the CharacterMoveForward method still be called?

  • Is changing the player controller easier? It seems to greatly reduce the control logic in both the player controller and the pawns receiving the input, and I really want to keep the input logic away from the pawns I will be controlling.

  • How would I change the player controller?

  • And also: Why did you pick the single-pc solution over the muli-pc solution?

Also thank you very much for answering, finding someone who has actual experience with the topic is a great thing!

I created a test project to try out how exactly the input behaves and I was supprised to find out, that it works exactly as I want it to. I created two pawn that Bind an action

void AP2::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);
	PlayerInputComponent->BindAction("P2_print", IE_Pressed, this, &AP2::Print);
}

And when I use the default Player Controller and posses one pawn, the other does not get any inputs. If I possess the other pawn, the old one doesn’t get any inputs again and everthing works fine. This is what I wanted. On the other hand, the documentation clearly states, that I may run into problems if I for example

change characters dynamically at runtime

I am confused (again). I am already doing that, and I didn’t notice any bugs with it. I also would like to know which PlayerInputComponent I get as an argument in the method above? Is it the component of the player controller, or of the pawn?

Have a good night :smiley:

Ah I forgot they did that. The pawn calls “CreatePlayerInputComponent()” which sets up its custom InputComponent. The InputComponent reference is inherited from the AActor class.

I believe, on possession and unpossession, they have put in fail-safes to ensure input binds only work for possessed pawns. To be on the safe side, you can call EnableInput and DisableInput for the pawn when possessing or unpossessing, respectively.

I am not sure why the docs state you may run into problems but it may be stating that in case you don’t use Enable/DisableInput. Someone with more knowledge on that may need to respond to that one but, if it seems to be working fine, it could be that the documentation is just stating that so that you don’t rely on their fail-safes to automatically ensure things go smoothly.

If you aren’t looking for custom input handling in the way I do it and you like this approach then this is definitely a viable straight-forward solution.
The major use for my custom input system is that it stores input states for each device and has the ability to determine if input is activated through holding, tapping or while held instead of storing states and duplicate functionality in specific pawns. If you don’t need any behaviour like that then using pawns the way you have tested should be the simplest approach for you.

Ok I think I will go with the simplest solution first and see if any problems arise. If I ever need something different I can look it up here :D. Thank you for your time you were a great help! UE can be confusing at times.

Sounds like a good plan. Sorry for leaning towards the more complex approach to start with, for some reason I assumed that you’d want more in-depth input handling too. Yeah UE is definitely a headache sometimes haha.

Feel free to ask again here later if you want some advice or want to get more detailed input handling.

P.S. Since you went with the simplest solution after basically leading yourself there, might help for you to post your own answer of it and mark that as the solution so that others having trouble will see the simplest approach first.

So after some researching on my own, I found out that for this functionality a customized Player Controller is not needed. I can simply register all the bindings in the pawns like this for example:

void AAvatar::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);
	PlayerInputComponent->BindAxis("avatar_move_forward", this, &AAvatar::MoveForward);
	PlayerInputComponent->BindAxis("avatar_move_right", this, &AAvatar::MoveRight);
}

If I then access the player controller of the pawn that is currently possessed, and make it possess a different Pawn all Inputs for any other Pawn will be ignored. This makes (in my case at least) any more complicated managing unnecessairy.