Course: The EOS Online Subsystem (OSS) Plugin

You’ll want to omit LOBBYSEARCH query setting. EOS Sessions typically don’t have an owner and are typically used on dedicated servers. EOS Lobbies NEED an owner and are typically used for P2P games. You can of course combine both in your game, but the session hosted on a userless dedicated server is an EOS Session, not an EOS Lobby.

1 Like

Thanks, and yes I understand the difference but as it was set to “false” I had hoped no matter what it would never search or look for p2p games/lobbies. I will update my code accordingly. Again thanks for taking the time to respond.

1 Like

Here is achievements. We’ll update the GitHub repo when we publish the 3rd part of this course.

// Additional code to query achievements -  example not in course
void AEOSPlayerState::QueryAchievements()
{
	// This function will query EOS achievements for a given player
	// To query achievements we need to cache the achievement descriptions AND achievement data
	
	IOnlineSubsystem* Subsystem = Online::GetSubsystem(GetWorld());
	IOnlineIdentityPtr Identity = Subsystem->GetIdentityInterface();
	IOnlineAchievementsPtr Achievements = Subsystem->GetAchievementsInterface(); 

	// Check if player is online before trying to update stat -- this code is repeated and could go in it's own function
	FUniqueNetIdPtr NetId = Identity->GetUniquePlayerId(0);

	if (!NetId || Identity->GetLoginStatus(*NetId) != ELoginStatus::LoggedIn)
	{
		return;
	}

	// Cache description
	Achievements->QueryAchievementDescriptions(*NetId, FOnQueryAchievementsCompleteDelegate::CreateLambda([this](
		const FUniqueNetId& NetId,
		const bool bQueryResultSuccessful
		)
		{
			if (bQueryResultSuccessful)
			{
				QueryAchievementsData();
			}
			else
			{
				UE_LOG(LogTemp, Error, TEXT("Failed to query achievement description."));
			}
		}));
}

void AEOSPlayerState::QueryAchievementsData()
{
	IOnlineSubsystem* Subsystem = Online::GetSubsystem(GetWorld());
	IOnlineIdentityPtr Identity = Subsystem->GetIdentityInterface();
	IOnlineAchievementsPtr Achievements = Subsystem->GetAchievementsInterface();

	// Check if player is online before trying to update stat -- this code is repeated and could go in it's own function
	FUniqueNetIdPtr NetId = Identity->GetUniquePlayerId(0);

	if (!NetId || Identity->GetLoginStatus(*NetId) != ELoginStatus::LoggedIn)
	{
		return;
	}

	// Cache achievement data 
	Achievements->QueryAchievements(*NetId, FOnQueryAchievementsCompleteDelegate::CreateLambda([this](
		const FUniqueNetId& NetId,
		const bool bQueryResultSuccessful
		)
		{
			if (bQueryResultSuccessful)
			{
				GetAchievements(); 
			}
			else
			{
				UE_LOG(LogTemp, Error, TEXT("Failed to query achievement data.")); 
			}
		}));
}

void AEOSPlayerState::GetAchievements()
{
	IOnlineSubsystem* Subsystem = Online::GetSubsystem(GetWorld());
	IOnlineIdentityPtr Identity = Subsystem->GetIdentityInterface();
	IOnlineAchievementsPtr Achievements = Subsystem->GetAchievementsInterface();

	// Check if player is online before trying to update stat -- this code is repeated and could go in it's own function
	FUniqueNetIdPtr NetId = Identity->GetUniquePlayerId(0);

	if (!NetId || Identity->GetLoginStatus(*NetId) != ELoginStatus::LoggedIn)
	{
		return;
	}

	// Read cache achievement description and data
	TArray<FOnlineAchievement> AchievementsData;
	if (Achievements->GetCachedAchievements(*NetId, AchievementsData) == EOnlineCachedResult::Success)
	{
		for (auto AchievementData : AchievementsData)
		{
			FOnlineAchievementDesc AchievementDescription;

			if (Achievements->GetCachedAchievementDescription(AchievementData.Id, AchievementDescription)
				== EOnlineCachedResult::Success)
			{
				FString AchievementId = AchievementData.Id;
				double AchievementProgress = AchievementData.Progress;
				FText AchievementTitle = AchievementDescription.Title;
				FText LockedDescription = AchievementDescription.LockedDesc;
				FText UnlockedDescription = AchievementDescription.UnlockedDesc;
			}
			else
			{
				UE_LOG(LogTemp, Error, TEXT("Failed to get cached achievement description achievement with id: %s."),
					*AchievementData.Id);
			}
		}
	}
	else
	{
		UE_LOG(LogTemp, Error, TEXT("Failed to get cached achievement data."))
	}
}
3 Likes

I’m curious, I tried to implement Achievements and Leaderboards last summer, waiting for the release of the relevant course module and now I’m studying it to see similarities and differences.
Main difference is that instead of using the PlayerState I created a class derived from UserWidget, as I will only use these features in the menus, in order to have direct access to the functions I need in the UI Blueprint.

Other difference is about delegate: I was intrigued by this comment in the module code:

// Unlike other OSS functions we've seen in previous modules, there is no delegate handle for Stat Updates. 
// Instead we will use an inline lambda

but since I’m not comfortable with Lambda functions I’ve solved this way:

StatsPtr->UpdateStats(NetId.ToSharedRef(), StatArray, FOnlineStatsUpdateStatsComplete::CreateUObject(this, &UEOSGameInstance::OnUpdateStatsComplete));

and I have defined OnUpdateStatsComplete function to be called on UpdateStats success.

I’m pretty happy to see that I somehow managed to get the Stats to work and I feel like I did everything pretty similar.
Can I ask for a comment on the differences highlighted above? Are there any particular pros or cons to using one method rather than another or is it the same?

Hey, There is no fix rule on which class type to use to host your code for stats/leaderboards/achievements. It depends on your game design and features. I think APlayerController or APlayerState makes sense in a lot of cases as this type of data is associated to a player. That’s why I went with this design for this course. If having this code on a UI object is what makes the most sense for your game, that’s not a problem. My word choice made it seem like you had to use an inline lambda. Sorry for the confusion there! I was trying to call out the difference with setting an FDelegateHandle as we had done for other OSS functions (like AutoLogin() for example). Using CreateUObject is fine.

2 Likes

Has anybody tried running a dedicated → client model and play 3 rounds or more? I’m running into issues where at the 3rd round I don’t receive a pawn and get stuck in the ground (camera only) I have to exit the game and re-connect (with open IP:PORT) to be able to play again.

1 Like

Great course, I was able to get the dedicated server example working. However, having a hard time getting the P2P one working. I am able to log into both but they seem to be in different lobby. Can you provide some pointers on where I can hardcode/modify so that they always spawn in the same lobby?

There were a few things that I think is worth mentioning for others who might be in the same situation. First, I think it might be worth mentioning to link your newly created client to the product (this step was missing in the guide). Second, in order to test with your secondary account, you need to add the account to your organization and assign it some role. Otherwise, your secondary account will not be able to access the application.

1 Like

Hi there! For some reason, when i run the clients bat files, they seem to open the the unreal interface with two different windows and i see individual players in both windows, but not two of them at the same time. Do you know how to resolve that?

I’m sorry for the delay in getting back to you. I was out on paternity leave. Thank you for the constructive feedback. The course is still relatively new. We will improve it using feedback from the community like the feedback you shared here.

I added this sentence in the Test Implementation section:

" The accounts will need to be members of your organization if your brand settings have NOT been approved."

Can you elaborate a little more on what you meant by " link your newly created client to the product" ? I want to add this information to the course!

For your issue with P2P, it seems like the second player isn’t finding the lobby. Can you set a breakpoint in AEOSPlayerController::FindSessions and AEOSPlayerController::HandleFindSessionsCompleted to see if you can figure out why the lobby isn’t found. Can you also double-check that the lobby is created on the EOS Developer Portal? Thanks!

Are you running your game in client-server or P2P mode? If you’re running your game in client-server, make sure the server is fully started before launching the clients.

SOLVED: Issue with my code…

@KillerSneak I’m experiencing the same thing.

I was able to join-leave-rejoin 11 times by running the server and client on different internet connections (haven’t had time to test more). I did find after the 10 exit from the game that HandleUnregisterPlayerCompleted fired 21 times with “LogTemp: Warning: Player unregistered in EOS Session!” so something is up.

How are you executing the open IP:PORT?

@SebBergy2.0 Congratulations are in order I believe!?! :smiley:

1 Like

Thank you! If you join - leave - rejoin on the same machine, do you see the issue? I’ll see if I can reproduce this!

@SebBergy2.0 Apologizes the problem is in my derived project. Thanks so much for getting back to me lightning quick. Please forgive me, I’m still learning, and using this course to learn both cpp and EOS backend (talk about biting off more than a person can chew!).

This morning I re-downloaded your reference design fresh from Git (without any of my changes (except for turning off achievement and save game functions)) and it is running perfectly fine on the “same machine” with multiple — login, register, unregister, quit and then rejoin (all verified on the Dev Portal Matchmaking section). So, there is definitely something wrong with my derived project.

I did notice on the reference design, same as my project, which has unregister firing additional times on each successive exit of the game, 3 the first time, 5, 7, and 9 respectively. Is this perhaps a part of the EOS back-end which limits the max 100 session connections per minute LINK? Would it be safe to run an IsPlayerInSession check on the unregister function? I’m having a little bit of confusion on what to do when you have a persistent server (up for several days before restart) and want to register, unregister players during that time - similar to Fortnight Lego or a Minecraft clone. Does the client need to request a destroy session on the client in addition to unregister?

Happy to hear you’re using the course while learning C++ and EOS! Yeah you’ve definitely given yourself a good challenge :slight_smile:!

I think we need to figure out why UnregisterPlayers() is being called multiple times for the same player. How are you quitting your game? I’ll try to reproduce what you’re seeing. Using IsPlayerInSession would work to workaround the issue. The player is unregistered in the OSS on the server before the ASYNC call is made to the backend. It’s not really a “solution” in my opinion, it’s working around the issue instead.

DestroySession() should be getting called on your client from AEOSGameSession::EndPlay() when the player leaves the dedicated server / session. You can set a breakpoint and debug through to confirm!

Thank you so much @SebBergy2.0 for creating this course. It helped me to quickly and easily integrate Online Stats and Online Leaderboards into my project.
I have gotten a bit ahead of the course content however, and tried to apply what you have taught me to the awesome OnlineStoreInterfaceV2.

I have created by offers in the DEV Portal (although I do not have everything setup in my test project as far as taxes go - so perhaps this is the problem? and I have not fully setup my “brand”).

I have been able to interface with the Stats / Leaderboard EOS interfaces without any issue. However, there is something different about the GetStoreV2Interface that never seems to return a valid pointer for me.
I have setup my Offers (but not completed ALL of the Epic DEV portal setup).
I created a very simple method to try and narrow down the problem by checking for each valid pointer (as your code does in EIK_GetOffers_AsyncFunction.cpp ).
Here are the results:

All the pointers are valid other than the StoreV2Ptr.
StoreV2Ptr is not valid.

I suspect I am missing something simple here.

Here is the simple method I have implemented to try and return the Offers I have created.

// Query Offers
void UOffersSubsystem::QueryOffers()
{

const IOnlineSubsystem* SubsystemRef = Online::GetSubsystem(this->GetWorld());
// check IOnlineSubsystem is valid and debug out
if (SubsystemRef)
{
UE_LOG(LogTemp, Log, TEXT(“Subsystem is valid”));
}
else
{
UE_LOG(LogTemp, Log, TEXT(“Subsystem is not valid”));
}
const IOnlineStoreV2Ptr StoreV2Ptr = SubsystemRef->GetStoreV2Interface();
// check IOnlineStoreV2Ptr is valid and debug out
if (StoreV2Ptr)
{
UE_LOG(LogTemp, Log, TEXT(“StoreV2Ptr is valid”));
}
else
{
UE_LOG(LogTemp, Log, TEXT(“StoreV2Ptr is not valid”));
}
const IOnlineIdentityPtr IdentityPointerRef = SubsystemRef->GetIdentityInterface();
// check IOnlineIdentityPtr is valid and debug out
if (IdentityPointerRef)
{
UE_LOG(LogTemp, Log, TEXT(“IdentityPointerRef is valid”));
}
else
{
UE_LOG(LogTemp, Log, TEXT(“IdentityPointerRef is not valid”));
}
const FUniqueNetIdPtr UserIdPtr{ IdentityPointerRef->GetUniquePlayerId(0) };
// check UserIdPtr is valid and debug out
if (UserIdPtr)
{
UE_LOG(LogTemp, Log, TEXT(“UserIdPtr is valid”));
}
else
{
UE_LOG(LogTemp, Log, TEXT(“UserIdPtr is not valid”));
}

if (UserIdPtr)
{
StoreV2Ptr->QueryOffersByFilter(*UserIdPtr.Get(), FOnlineStoreFilter(), FOnQueryOnlineStoreOffersComplete::CreateUObject(this, &UOffersSubsystem::HandleQueryOffersComplete));
}

}

Thank you again for creating this course and for helping me as I work ahead of the course content a bit.

I find this is the perfect course to learn from! Just downloading plugins might work but you may never know how things work, especially if you manage developers, and then there is bloat. Lyra is awesome, however, is like learning three systems simultaneously, hence really, really hard to follow the logic. Here I’m actually learning about how the EOS back-end and game flow works and the logic is very self-evident as you have coded it. AND the documentation with images is first class!

Exiting the game: For your reference design, I am leaving everything “as is” except for adding a UE_LOG at warning level on the “HandleUnregisterPlayerCompleted” function “if(bWasSucccesful), I am exiting the client with “exit” key or ctrl-c, both of which log the player out as I can see on the server log and on the EOS website.

In my derived code, I am calling the standard Quit Game Blueprint node on a Widget on my player controller derived from my cpp controller. Eventually we will want the option to return to main menu or simple exit the software.

In both cases I see the UE_LOG message appears multiple times on the dedicated server log. I followed the EndPlayer as you suggested and all is well!

BTW, we had a huge cheer of success in our team of 4 when I finally fixed the join session code on my derived project, and it now no longer has the issues mentioned in one of my previous posts (I had the mistaken idea to move the Find and Join code to Blueprint Async Action Base which caused a 9 minute timeout). Now we have our splash screen up while login takes place. Smooth transition to a lobby with a widget for each dedicated server! We spent the better part of two days just stress testing login-register-unregister-quit and rejoin with no issues. We still have a long way to go (if you have any good pointers to network error handling I would love it :wink: ) but our rag-tag team of ex-electronic engineers, an industrial design artist and a recent game design graduate thanks to you!!!

2 Likes

Hey, thanks for the feedback! You can test store functionality by passing -EpicPortal via command line parameters.

I’m using UE5.3, so it might be useful for me to share how to figure this out in case the code is different in older versions (I didn’t check!).

If you look at GetStoreV2Interface() you’ll see that the function returns StoreInterfacePtr. If you do a find all on this pointer name in Visual Studio you’ll find a few results. The only one where the pointer is set is in FonlineSubsystemEOS::Init():

// Disable ecom if not part of EGS
if (bWasLaunchedByEGS)
{
	EcomHandle = EOS_Platform_GetEcomInterface(*EOSPlatformHandle);
	if (EcomHandle == nullptr)
	{
		UE_LOG_ONLINE(Error, TEXT("FOnlineSubsystemEOS: failed to init EOS platform, couldn't get ecom handle"));
		return false;
	}
	StoreInterfacePtr = MakeShareable(new FOnlineStoreEOS(this));
}

So the pointer only get’s set if the game is launched by EGS. If you do a find all references on the bWasLaunchedByEGS you’ll see that it’s set several lines above in the same function:

bWasLaunchedByEGS = FParse::Param(FCommandLine::Get(), TEXT("EpicPortal"));

Hope this unblocks you!

3 Likes

Let me try to reproduce the multiple function calls and see if I can explain / fix it!

For the pointers on network error handling could you create another post? I can give a few pointers and others in the community can also help out. It would be better in a new post as the topic is different than what the course is on!

Thank you for the nice feedback!!

1 Like

The EOS Online Subsystem (OSS) Plugin course offers a comprehensive exploration of integrating EOS into game development. With clear instructions and hands-on exercises, it equips developers with the skills to seamlessly integrate online features, enhancing multiplayer functionality and providing a robust foundation for creating engaging gaming experiences.

3 Likes

Thank you so much for your reply and help with this.
I will try this out tomorrow - but I am pretty confident this will resolve my issue.