Multiple local controllers II

In KOPI, we needed to implement couch co-op mode with multiple controllers all driving the game’s user interface using Unreal Engine’s CommonUI. All controllers should share the same focus, but a APlayerController should only be spawned for a controller that joins. To implement this kind of functionality without strange glitches, we needed to get your hands on the innards of the FSlateApplication: the FGenericApplicationMessageHandler and ISlateInputManager. The former allowed us to intercept and controllers connecting and disconnecting and creating the appropriate APlayerControllers for them; the ISlateInputManager provided a way to treat all already connected controllers as a single connected controller.

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,
		FKopiOtherGamepadKeyDelegate::CreateUObject(this, ...),
		FKopiUnifiedGamepadKeyDelegate::CreateUObject(this, ...),
		FSimpleDelegate::CreateUObject(this, ...));
	GenericApplication->SetMessageHandler(DelegatingHandler.ToSharedRef());
	InputManager = MakeShared<FKopiSlateInputManager>();
	FSlateApplication::Get().SetInputManager(InputManager.ToSharedRef());
#endif
	IPlatformInputDeviceMapper::Get().GetOnInputDeviceConnectionChange().AddUObject(this, &ThisClass::OnInputDeviceConnectionChange);
}

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

We already discussed the FGenericApplicationMessageHandler in Multiple controllers and UI, and it’s a good first start. However, the FGenericApplicationMessageHandler alone leads to strange “multiple focus” in standalone game builds. To ensure that once unified, all controllers are treated as one also for the purposes of Slate, we needed to add the appropriate implementation of ISlateInputManager.

// Declaration
class FKopiSlateInputManager final : public FSlateDefaultInputMapping {
	bool bUnifyGamepads = false;

public:
	void SetUnifyGamepads(const bool bUnify);
	virtual int32 GetUserIndexForController(int32 ControllerId) const override;
	virtual TOptional<int32> GetUserIndexForInputDevice(FInputDeviceId InputDeviceId) const override;
	virtual TOptional<int32> GetUserIndexForPlatformUser(FPlatformUserId PlatformUser) const override;
};


// Definition
void FKopiSlateInputManager::SetUnifyGamepads(const bool bUnify) {
	bUnifyGamepads = bUnify;
}

int32 FKopiSlateInputManager::GetUserIndexForController(int32 ControllerId) const {
	if (bUnifyGamepads)
		return 0;

	return FSlateDefaultInputMapping::GetUserIndexForController(ControllerId);
}

TOptional<int32> FKopiSlateInputManager::GetUserIndexForInputDevice(FInputDeviceId InputDeviceId) const {
	if (bUnifyGamepads)
		return 0;

	return FSlateDefaultInputMapping::GetUserIndexForInputDevice(InputDeviceId);
}

TOptional<int32> FKopiSlateInputManager::GetUserIndexForPlatformUser(FPlatformUserId PlatformUser) const {
	if (bUnifyGamepads)
		return 0;

	return FSlateDefaultInputMapping::GetUserIndexForPlatformUser(PlatformUser);
}

With this change in, all local controllers behave correctly: for UI, they in effect become a single local controller; but for gameplay–when bUnifyGamepads == false–they are the same old local controllers again.

Leave a Reply

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