We are trying to support drop in drop out local players (couch coop) in an online multiplayer game… meaning… while connected to a dedicated server we want to be able to remove local (couch coop) players from the game seamlessly. By this I mean being able to add or remove a local player while connected to a server without interrupting gameplay.
I’ve been somewhat successful in removing the local player at the tail of GameInstance::LocalPlayers
but when attempting to remove the PrimaryPlayer (the player at index 0) things don’t behave as expected; the indices shift
Upon further investigation: LocalUserNum, LocalUserIndex, ControllerId, FPlatformUserId and XUserHandle have a somewhat shared meaning but not quite and may change when a player is removed:
These are the classes Ive been investigating while attempting to support this feature
- LocalPlayer
- PlayerController
- GameInstance
- GDKPlatformInputDeviceMapper
- GDKGameInputInterface
- GDKUserManager
- OnlineSubsystemIdentityInterfaceGDK
- OnlineEngineInterface
Our players are signed in onto multiple OSSes each of which identify the local player through a map like: TMap<LocalUserNum:int32, UniqueId:FUniqueNetIdOSS>
The LocalUserNum is generally derived from PlayerController->GetControllerId() which is assigned upon construction of the ULocalUser on UGameInstance::CreateLocalPlayer.
Here are the primary player creation relevant functions:
// GenericPlatformMisc.cpp
FPlatformUserId FGenericPlatformInputDeviceMapper::GetPrimaryPlatformUser() const
{
// Most platforms will want the primary user to be 0
static const FPlatformUserId PrimaryPlatformUser = FPlatformUserId::CreateFromInternalId(0);
return PrimaryPlatformUser;
}
// GameInstance.cpp
ULocalPlayer* UGameInstance::CreateInitialPlayer(FString& OutError)
{
return CreateLocalPlayer(IPlatformInputDeviceMapper::Get().GetPrimaryPlatformUser(), OutError, false);
}
// GameInstance.cpp
ULocalPlayer* UGameInstance::CreateLocalPlayer(int32 ControllerId, FString& OutError, bool bSpawnPlayerController)
{
// A compatibility call that will map the old int32 ControllerId to the new platform user
FPlatformUserId UserId = FGenericPlatformMisc::GetPlatformUserForUserIndex(ControllerId);
FInputDeviceId DummyInputDevice = INPUTDEVICEID_NONE;
IPlatformInputDeviceMapper::Get().RemapControllerIdToPlatformUserAndDevice(ControllerId, UserId, DummyInputDevice);
return CreateLocalPlayer(UserId, OutError, bSpawnPlayerController);
}
On Xbox the secondary players are added on powerON and/or whenever the application receives a button press from an unmapped user device. I use the account picker to trigger the mapping on the platform and things work like a charm.
Note that these events are issued by the GDKPlatformInputDeviceMapper which listens to a gdk library event directly and maps a FPlatformUserId to a DeviceId for the purposes of triggering events but it cannot (should not) change the user-device mapping as that is GDKs responsibility
NOW… back to the problem: Logging out/removing players (sorry for the long preamble)
Removing a player at index 0 makes LocalUserNum no longer match LocalUserIndex as it has shifted.
With that: consider the following function (vanilla Engine function)
FUniqueNetIdWrapper UOnlineServicesEngineInterfaceImpl::GetUniquePlayerIdWrapper(UWorld* World, int32 LocalUserNum, FName Type)
{
...
UE::Online::FAuthGetLocalOnlineUserByPlatformUserId::Params GetAccountParams = { FPlatformMisc::GetPlatformUserForUserIndex(LocalUserNum) };
...
return FUniqueNetIdWrapper();
}
This function receives a LocalUserNum as a parameter but is used as an argument for a function that expects an index parameter
This is not the only case Ive found like this. There are cases where FPlatformUserId::GetInternal() is used as LocalPlayerIndex like
// GenericPlatformMisc.cpp
int32 FGenericPlatformMisc::GetUserIndexForPlatformUser(FPlatformUserId PlatformUser)
{
return PlatformUser.GetInternalId();
}
Is this supported?
What would I need to do to support seamless drop IN/OUT of local users on an online multiplayer game?