Multiple controllers and UI

Multiple controllers and UI

In addition to network multiplayer, our game needed local couch mode multiplayer, too. It is just so much fine to grab a game controller and have a go with one’s friends. The challenge was how to use the game controllers to drive the user interface. One option was to have the first local player be the MC, and for the other players to “just” join. However, we wanted all controllers to be equal when driving the user interface; to have the full couch mode experience where everyone can interact with the game completely.

A simple approach where we create multiple (four in our case) APlayerController instances, and assign each increasing controller ID did not work; without the Common UI plugin, each controller maintained its own user-interface focus; with Common UI only the primary [local] controller was able to use the UI. Our first attempt was to subclass FCommonViewportClient, and override

  • virtual bool InputKey(const FInputKeyEventArgs&)
  • virtual bool InputAxis(FViewport*, FInputDeviceId, FKey, float, float, int32, bool)

In the implementations, the intention was to set the InputDeviceId to 0 (or more precisely to the value of IPlatformInputDeviceMapper::Get()->GetDefaultInputDevice()). However, when you implement the InputKey and InputAxis methods, modifying the InputDeviceId, what implementation do you call next? The inherited one, of course; but the bodies of FViewportClient::InputKey and FViewportClient::InputAxis are–in their full glory–just { return false; }. Moreover, by the time these methods are executed, the Common UI navigation has already happened.

The stack trace above shows where the routing happens; we are happy with the routing logic, so we need to needed to modify the processing much earlier; at the FSlateApplication-ish level; one that is specific to the platform, yet remains generic enough so we do not tie ourselves to a specific platform.

GenericApplication

A sufficiently low-level and yet generic enough is to get our hands on the TSharedPtr<GenericApplication>, and then use its SetMessageHandler(const TSharedRef< FGenericApplicationMessageHandler>&) to set our implementation of the FGenericApplicationMessageHandler. In our FGenericApplicationMessageHandler, we delegate almost all methods to the original FGenericApplicationMessageHandler, except for

  • OnControllerButtonPressed(FGamepadKeyNames::Type, FPlatformUserId, FInputDeviceId, bool)
  • OnControllerButtonReleased(FGamepadKeyNames::Type, FPlatformUserId, FInputDeviceId, bool)
  • OnControllerAnalog(FGamepadKeyNames::Type, FPlatformUserId, FInputDeviceId, float)

These methods need to do perform additional logic before delegating to the original FGenericApplicationMessageHandler; this additional logic is, of course, the unification code.

class FKopiGamepadApplicationMessageHandler final : public FGenericApplicationMessageHandler {
public:
  FKopiGamepadApplicationMessageHandler(const TSharedPtr<FGenericApplicationMessageHandler> &InDelegate);
  
#pragma region Unification methods
  virtual bool OnControllerAnalog(FGamepadKeyNames::Type KeyName, FPlatformUserId PlatformUserId, FInputDeviceId InputDeviceId, float AnalogValue) override;
  virtual bool OnControllerAnalog(FGamepadKeyNames::Type KeyName, int32 ControllerId, float AnalogValue) override;
  virtual bool OnControllerButtonPressed(FGamepadKeyNames::Type KeyName, FPlatformUserId PlatformUserId, FInputDeviceId InputDeviceId,bool IsRepeat) override;
  virtual bool OnControllerButtonPressed(FGamepadKeyNames::Type KeyName, int32 ControllerId, bool IsRepeat) override;
  virtual bool OnControllerButtonReleased(FGamepadKeyNames::Type KeyName, FPlatformUserId PlatformUserId, FInputDeviceId InputDeviceId, bool IsRepeat) override;
  virtual bool OnControllerButtonReleased(FGamepadKeyNames::Type KeyName, int32 ControllerId, bool IsRepeat) override;
#pragma endregion

  // all other methods from FGenericApplicationMessageHandler here
};
const TSharedPtr<GenericApplication> GenericApplication = FSlateApplication::Get().GetPlatformApplication();
OriginalHandler = GenericApplication->GetMessageHandler();
DelegatingHandler = MakeShared<FKopiGamepadApplicationMessageHandler>(OriginalHandler);
GenericApplication->SetMessageHandler(DelegatingHandler.ToSharedRef());

The construction of our custom FKopiGamepadApplicationMessageHandler can be done in a UGameInstanceSubsystem. On the UGameInstanceSubsystem initialization, we construct and set our custom FGenericApplicationMessageHandler; on deinitialization, we set back the original delegated handler: this turns out to be very important for in-editor play.

class KOPI_API UKopiGamepadSubsystem : public UGameInstanceSubsystem {
  GENERATED_BODY()

#if PLATFORM_DESKTOP
  TSharedPtr<FKopiGamepadApplicationMessageHandler> DelegatingHandler = nullptr;
  TSharedPtr<FGenericApplicationMessageHandler> OriginalHandler = nullptr;
#endif

public:
  virtual void Initialize(FSubsystemCollectionBase &Collection) override;
  virtual void Deinitialize() override;
}

The implementation of these two lifecycle methods does not involve any special trickery, only the usual Unreal Engine object creation.

void UKopiGamepadSubsystem::Initialize(FSubsystemCollectionBase &Collection) {
  Super::Initialize(Collection);
#if PLATFORM_DESKTOP
  const TSharedPtr<GenericApplication> GenericApplication = FSlateApplication::Get().GetPlatformApplication();
  OriginalHandler = GenericApplication->GetMessageHandler();
  DelegatingHandler = MakeShared<FKopiGamepadApplicationMessageHandler>(OriginalHandler);
  GenericApplication->SetMessageHandler(DelegatingHandler.ToSharedRef());
#endif
}

void UKopiGamepadSubsystem::Deinitialize() {
#if PLATFORM_DESKTOP
  if (OriginalHandler) {
    const TSharedPtr<GenericApplication> GenericApplication = FSlateApplication::Get().GetPlatformApplication();
    GenericApplication->SetMessageHandler(OriginalHandler.ToSharedRef());
  }
  Super::Deinitialize();
#endif
}

Finally, the meat of the unification code is in our implementation of the FKopiGamepadApplicationMessageHandler; all that is needed to be done is to re-map the input device ID to the default device ID before calling the matching method from the delegate.

bool FKopiGamepadApplicationMessageHandler::OnControllerButtonPressed(FGamepadKeyNames::Type KeyName, FPlatformUserId PlatformUserId, FInputDeviceId InputDeviceId, bool IsRepeat) {
  const bool bUnify = true;
  const FPlatformUserId FirstPlatformUserId = FPlatformUserId::CreateFromInternalId(0)

  if (bUnify) 
    return Delegate->OnControllerButtonPressed(KeyName, FirstPlatformUserId, FirstInputDeviceId, IsRepeat);

  return Delegate->OnControllerButtonPressed(KeyName, PlatformUserId, InputDeviceId, IsRepeat);
}

The variables bUnify and FirstPlatformUserId should, of course, be set somewhere else; but this code sufficiently explains how to perform the re-mapping. After adding this UGameInstanceSubsystem to your game, the game will allow you to control the game’s Common UI using any controller. This is a perfect set-up for couch coop game, you no longer have to rely on any “primary” player to control the game’s UI leaving the other players passively watching.

Leave a Reply

Your email address will not be published. Required fields are marked *