Skip to content

Instantly share code, notes, and snippets.

@bruteforks
Last active January 31, 2024 22:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bruteforks/a074e05b06658820a8779d276e3f1b16 to your computer and use it in GitHub Desktop.
Save bruteforks/a074e05b06658820a8779d276e3f1b16 to your computer and use it in GitHub Desktop.
Unreal engine 5.3.2 Lyra Different AI Pawns
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "Engine/DataAsset.h"
#include "LyraExperienceDefinition.generated.h"
class UGameFeatureAction;
class ULyraPawnData;
class ULyraExperienceActionSet;
/**
* Definition of an experience
*/
UCLASS(BlueprintType, Const)
class ULyraExperienceDefinition : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
ULyraExperienceDefinition();
//~UObject interface
#if WITH_EDITOR
virtual EDataValidationResult IsDataValid(class FDataValidationContext& Context) const override;
#endif
//~End of UObject interface
//~UPrimaryDataAsset interface
#if WITH_EDITORONLY_DATA
virtual void UpdateAssetBundleData() override;
#endif
//~End of UPrimaryDataAsset interface
public:
// List of Game Feature Plugins this experience wants to have active
UPROPERTY(EditDefaultsOnly, Category = Gameplay)
TArray<FString> GameFeaturesToEnable;
/** The default pawn class to spawn for players */
//@TODO: Make soft?
UPROPERTY(EditDefaultsOnly, Category=Gameplay)
TObjectPtr<const ULyraPawnData> DefaultPawnData;
/** Online solution for separate pawn class to spawn for players https://forums.unrealengine.com/t/lyra-ue5-1-spawn-different-ai-pawns-setpawndata/740673/10 https://forums.unrealengine.com/t/lyra-ue5-1-spawn-different-ai-pawns-setpawndata/740673/6 */
//@TODO: Make soft?
UPROPERTY(EditDefaultsOnly, Category = Gameplay)
TObjectPtr<const ULyraPawnData> CustomAIPawnData;
// List of actions to perform as this experience is loaded/activated/deactivated/unloaded
UPROPERTY(EditDefaultsOnly, Instanced, Category="Actions")
TArray<TObjectPtr<UGameFeatureAction>> Actions;
// List of additional action sets to compose into this experience
UPROPERTY(EditDefaultsOnly, Category=Gameplay)
TArray<TObjectPtr<ULyraExperienceActionSet>> ActionSets;
};
// Copyright Epic Games, Inc. All Rights Reserved.
#include "LyraGameMode.h"
#include "AssetRegistry/AssetData.h"
#include "Engine/GameInstance.h"
#include "Engine/World.h"
#include "LyraLogChannels.h"
#include "Misc/CommandLine.h"
#include "System/LyraAssetManager.h"
#include "LyraGameState.h"
#include "System/LyraGameSession.h"
#include "Player/LyraPlayerController.h"
#include "Player/LyraPlayerBotController.h"
#include "Player/LyraPlayerState.h"
#include "Character/LyraCharacter.h"
#include "UI/LyraHUD.h"
#include "Character/LyraPawnExtensionComponent.h"
#include "Character/LyraPawnData.h"
#include "GameModes/LyraWorldSettings.h"
#include "GameModes/LyraExperienceDefinition.h"
#include "GameModes/LyraExperienceManagerComponent.h"
#include "GameModes/LyraUserFacingExperienceDefinition.h"
#include "Kismet/GameplayStatics.h"
#include "Development/LyraDeveloperSettings.h"
#include "Player/LyraPlayerSpawningManagerComponent.h"
#include "CommonUserSubsystem.h"
#include "CommonSessionSubsystem.h"
#include "TimerManager.h"
#include "GameMapsSettings.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(LyraGameMode)
ALyraGameMode::ALyraGameMode(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
GameStateClass = ALyraGameState::StaticClass();
GameSessionClass = ALyraGameSession::StaticClass();
PlayerControllerClass = ALyraPlayerController::StaticClass();
ReplaySpectatorPlayerControllerClass = ALyraReplayPlayerController::StaticClass();
PlayerStateClass = ALyraPlayerState::StaticClass();
DefaultPawnClass = ALyraCharacter::StaticClass();
HUDClass = ALyraHUD::StaticClass();
}
const ULyraPawnData* ALyraGameMode::GetPawnDataForController(const AController* InController) const
{
// See if pawn data is already set on the player state
if (InController != nullptr)
{
if (const ALyraPlayerState* LyraPS = InController->GetPlayerState<ALyraPlayerState>())
{
if (const ULyraPawnData* PawnData = LyraPS->GetPawnData<ULyraPawnData>())
{
return PawnData;
}
}
}
// If not, fall back to the the default for the current experience
check(GameState);
ULyraExperienceManagerComponent* ExperienceComponent = GameState->FindComponentByClass<ULyraExperienceManagerComponent>();
check(ExperienceComponent);
if (ExperienceComponent->IsExperienceLoaded())
{
const ULyraExperienceDefinition* Experience = ExperienceComponent->GetCurrentExperienceChecked();
if (Experience->DefaultPawnData != nullptr)
{
if (InController != nullptr && InController->IsA<AAIController>())
{
return Experience->CustomAIPawnData;
}
else
{
return Experience->DefaultPawnData;
}
}
// Experience is loaded and there's still no pawn data, fall back to the default for now
return ULyraAssetManager::Get().GetDefaultPawnData();
}
// Experience not loaded yet, so there is no pawn data to be had
return nullptr;
}
void ALyraGameMode::InitGame(const FString& MapName, const FString& Options, FString& ErrorMessage)
{
Super::InitGame(MapName, Options, ErrorMessage);
// Wait for the next frame to give time to initialize startup settings
GetWorld()->GetTimerManager().SetTimerForNextTick(this, &ThisClass::HandleMatchAssignmentIfNotExpectingOne);
}
void ALyraGameMode::HandleMatchAssignmentIfNotExpectingOne()
{
FPrimaryAssetId ExperienceId;
FString ExperienceIdSource;
// Precedence order (highest wins)
// - Matchmaking assignment (if present)
// - URL Options override
// - Developer Settings (PIE only)
// - Command Line override
// - World Settings
// - Dedicated server
// - Default experience
UWorld* World = GetWorld();
if (!ExperienceId.IsValid() && UGameplayStatics::HasOption(OptionsString, TEXT("Experience")))
{
const FString ExperienceFromOptions = UGameplayStatics::ParseOption(OptionsString, TEXT("Experience"));
ExperienceId = FPrimaryAssetId(FPrimaryAssetType(ULyraExperienceDefinition::StaticClass()->GetFName()), FName(*ExperienceFromOptions));
ExperienceIdSource = TEXT("OptionsString");
}
if (!ExperienceId.IsValid() && World->IsPlayInEditor())
{
ExperienceId = GetDefault<ULyraDeveloperSettings>()->ExperienceOverride;
ExperienceIdSource = TEXT("DeveloperSettings");
}
// see if the command line wants to set the experience
if (!ExperienceId.IsValid())
{
FString ExperienceFromCommandLine;
if (FParse::Value(FCommandLine::Get(), TEXT("Experience="), ExperienceFromCommandLine))
{
ExperienceId = FPrimaryAssetId::ParseTypeAndName(ExperienceFromCommandLine);
if (!ExperienceId.PrimaryAssetType.IsValid())
{
ExperienceId = FPrimaryAssetId(FPrimaryAssetType(ULyraExperienceDefinition::StaticClass()->GetFName()), FName(*ExperienceFromCommandLine));
}
ExperienceIdSource = TEXT("CommandLine");
}
}
// see if the world settings has a default experience
if (!ExperienceId.IsValid())
{
if (ALyraWorldSettings* TypedWorldSettings = Cast<ALyraWorldSettings>(GetWorldSettings()))
{
ExperienceId = TypedWorldSettings->GetDefaultGameplayExperience();
ExperienceIdSource = TEXT("WorldSettings");
}
}
ULyraAssetManager& AssetManager = ULyraAssetManager::Get();
FAssetData Dummy;
if (ExperienceId.IsValid() && !AssetManager.GetPrimaryAssetData(ExperienceId, /*out*/ Dummy))
{
UE_LOG(LogLyraExperience, Error, TEXT("EXPERIENCE: Wanted to use %s but couldn't find it, falling back to the default)"), *ExperienceId.ToString());
ExperienceId = FPrimaryAssetId();
}
// Final fallback to the default experience
if (!ExperienceId.IsValid())
{
if (TryDedicatedServerLogin())
{
// This will start to host as a dedicated server
return;
}
//@TODO: Pull this from a config setting or something
ExperienceId = FPrimaryAssetId(FPrimaryAssetType("LyraExperienceDefinition"), FName("B_LyraDefaultExperience"));
ExperienceIdSource = TEXT("Default");
}
OnMatchAssignmentGiven(ExperienceId, ExperienceIdSource);
}
bool ALyraGameMode::TryDedicatedServerLogin()
{
// Some basic code to register as an active dedicated server, this would be heavily modified by the game
FString DefaultMap = UGameMapsSettings::GetGameDefaultMap();
UWorld* World = GetWorld();
UGameInstance* GameInstance = GetGameInstance();
if (GameInstance && World && World->GetNetMode() == NM_DedicatedServer && World->URL.Map == DefaultMap)
{
// Only register if this is the default map on a dedicated server
UCommonUserSubsystem* UserSubsystem = GameInstance->GetSubsystem<UCommonUserSubsystem>();
// Dedicated servers may need to do an online login
UserSubsystem->OnUserInitializeComplete.AddDynamic(this, &ALyraGameMode::OnUserInitializedForDedicatedServer);
// There are no local users on dedicated server, but index 0 means the default platform user which is handled by the online login code
if (!UserSubsystem->TryToLoginForOnlinePlay(0))
{
OnUserInitializedForDedicatedServer(nullptr, false, FText(), ECommonUserPrivilege::CanPlayOnline, ECommonUserOnlineContext::Default);
}
return true;
}
return false;
}
void ALyraGameMode::HostDedicatedServerMatch(ECommonSessionOnlineMode OnlineMode)
{
FPrimaryAssetType UserExperienceType = ULyraUserFacingExperienceDefinition::StaticClass()->GetFName();
// Figure out what UserFacingExperience to load
FPrimaryAssetId UserExperienceId;
FString UserExperienceFromCommandLine;
if (FParse::Value(FCommandLine::Get(), TEXT("UserExperience="), UserExperienceFromCommandLine) ||
FParse::Value(FCommandLine::Get(), TEXT("Playlist="), UserExperienceFromCommandLine))
{
UserExperienceId = FPrimaryAssetId::ParseTypeAndName(UserExperienceFromCommandLine);
if (!UserExperienceId.PrimaryAssetType.IsValid())
{
UserExperienceId = FPrimaryAssetId(FPrimaryAssetType(UserExperienceType), FName(*UserExperienceFromCommandLine));
}
}
// Search for the matching experience, it's fine to force load them because we're in dedicated server startup
ULyraAssetManager& AssetManager = ULyraAssetManager::Get();
TSharedPtr<FStreamableHandle> Handle = AssetManager.LoadPrimaryAssetsWithType(UserExperienceType);
if (ensure(Handle.IsValid()))
{
Handle->WaitUntilComplete();
}
TArray<UObject*> UserExperiences;
AssetManager.GetPrimaryAssetObjectList(UserExperienceType, UserExperiences);
ULyraUserFacingExperienceDefinition* FoundExperience = nullptr;
ULyraUserFacingExperienceDefinition* DefaultExperience = nullptr;
for (UObject* Object : UserExperiences)
{
ULyraUserFacingExperienceDefinition* UserExperience = Cast<ULyraUserFacingExperienceDefinition>(Object);
if (ensure(UserExperience))
{
if (UserExperience->GetPrimaryAssetId() == UserExperienceId)
{
FoundExperience = UserExperience;
break;
}
if (UserExperience->bIsDefaultExperience && DefaultExperience == nullptr)
{
DefaultExperience = UserExperience;
}
}
}
if (FoundExperience == nullptr)
{
FoundExperience = DefaultExperience;
}
UGameInstance* GameInstance = GetGameInstance();
if (ensure(FoundExperience && GameInstance))
{
// Actually host the game
UCommonSession_HostSessionRequest* HostRequest = FoundExperience->CreateHostingRequest();
if (ensure(HostRequest))
{
HostRequest->OnlineMode = OnlineMode;
// TODO override other parameters?
UCommonSessionSubsystem* SessionSubsystem = GameInstance->GetSubsystem<UCommonSessionSubsystem>();
SessionSubsystem->HostSession(nullptr, HostRequest);
// This will handle the map travel
}
}
}
void ALyraGameMode::OnUserInitializedForDedicatedServer(const UCommonUserInfo* UserInfo, bool bSuccess, FText Error, ECommonUserPrivilege RequestedPrivilege, ECommonUserOnlineContext OnlineContext)
{
UGameInstance* GameInstance = GetGameInstance();
if (GameInstance)
{
// Unbind
UCommonUserSubsystem* UserSubsystem = GameInstance->GetSubsystem<UCommonUserSubsystem>();
UserSubsystem->OnUserInitializeComplete.RemoveDynamic(this, &ALyraGameMode::OnUserInitializedForDedicatedServer);
if (bSuccess)
{
// Online login worked, start a full online game
UE_LOG(LogLyraExperience, Log, TEXT("Dedicated server online login succeeded, starting online server"));
HostDedicatedServerMatch(ECommonSessionOnlineMode::Online);
}
else
{
// Go ahead and try to host anyway, but without online support
// This behavior is fairly game specific, but this behavior provides the most flexibility for testing
UE_LOG(LogLyraExperience, Log, TEXT("Dedicated server online login failed, starting LAN-only server"));
HostDedicatedServerMatch(ECommonSessionOnlineMode::LAN);
}
}
}
void ALyraGameMode::OnMatchAssignmentGiven(FPrimaryAssetId ExperienceId, const FString& ExperienceIdSource)
{
if (ExperienceId.IsValid())
{
UE_LOG(LogLyraExperience, Log, TEXT("Identified experience %s (Source: %s)"), *ExperienceId.ToString(), *ExperienceIdSource);
ULyraExperienceManagerComponent* ExperienceComponent = GameState->FindComponentByClass<ULyraExperienceManagerComponent>();
check(ExperienceComponent);
ExperienceComponent->SetCurrentExperience(ExperienceId);
}
else
{
UE_LOG(LogLyraExperience, Error, TEXT("Failed to identify experience, loading screen will stay up forever"));
}
}
void ALyraGameMode::OnExperienceLoaded(const ULyraExperienceDefinition* CurrentExperience)
{
// Spawn any players that are already attached
//@TODO: Here we're handling only *player* controllers, but in GetDefaultPawnClassForController_Implementation we skipped all controllers
// GetDefaultPawnClassForController_Implementation might only be getting called for players anyways
for (FConstPlayerControllerIterator Iterator = GetWorld()->GetPlayerControllerIterator(); Iterator; ++Iterator)
{
APlayerController* PC = Cast<APlayerController>(*Iterator);
if ((PC != nullptr) && (PC->GetPawn() == nullptr))
{
if (PlayerCanRestart(PC))
{
RestartPlayer(PC);
}
}
}
}
bool ALyraGameMode::IsExperienceLoaded() const
{
check(GameState);
ULyraExperienceManagerComponent* ExperienceComponent = GameState->FindComponentByClass<ULyraExperienceManagerComponent>();
check(ExperienceComponent);
return ExperienceComponent->IsExperienceLoaded();
}
UClass* ALyraGameMode::GetDefaultPawnClassForController_Implementation(AController* InController)
{
if (const ULyraPawnData* PawnData = GetPawnDataForController(InController))
{
if (PawnData->PawnClass)
{
return PawnData->PawnClass;
}
}
return Super::GetDefaultPawnClassForController_Implementation(InController);
}
APawn* ALyraGameMode::SpawnDefaultPawnAtTransform_Implementation(AController* NewPlayer, const FTransform& SpawnTransform)
{
FActorSpawnParameters SpawnInfo;
SpawnInfo.Instigator = GetInstigator();
SpawnInfo.ObjectFlags |= RF_Transient; // Never save the default player pawns into a map.
SpawnInfo.bDeferConstruction = true;
if (UClass* PawnClass = GetDefaultPawnClassForController(NewPlayer))
{
if (APawn* SpawnedPawn = GetWorld()->SpawnActor<APawn>(PawnClass, SpawnTransform, SpawnInfo))
{
if (ULyraPawnExtensionComponent* PawnExtComp = ULyraPawnExtensionComponent::FindPawnExtensionComponent(SpawnedPawn))
{
if (const ULyraPawnData* PawnData = GetPawnDataForController(NewPlayer))
{
PawnExtComp->SetPawnData(PawnData);
}
else
{
UE_LOG(LogLyra, Error, TEXT("Game mode was unable to set PawnData on the spawned pawn [%s]."), *GetNameSafe(SpawnedPawn));
}
}
SpawnedPawn->FinishSpawning(SpawnTransform);
return SpawnedPawn;
}
else
{
UE_LOG(LogLyra, Error, TEXT("Game mode was unable to spawn Pawn of class [%s] at [%s]."), *GetNameSafe(PawnClass), *SpawnTransform.ToHumanReadableString());
}
}
else
{
UE_LOG(LogLyra, Error, TEXT("Game mode was unable to spawn Pawn due to NULL pawn class."));
}
return nullptr;
}
bool ALyraGameMode::ShouldSpawnAtStartSpot(AController* Player)
{
// We never want to use the start spot, always use the spawn management component.
return false;
}
void ALyraGameMode::HandleStartingNewPlayer_Implementation(APlayerController* NewPlayer)
{
// Delay starting new players until the experience has been loaded
// (players who log in prior to that will be started by OnExperienceLoaded)
if (IsExperienceLoaded())
{
Super::HandleStartingNewPlayer_Implementation(NewPlayer);
}
}
AActor* ALyraGameMode::ChoosePlayerStart_Implementation(AController* Player)
{
if (ULyraPlayerSpawningManagerComponent* PlayerSpawningComponent = GameState->FindComponentByClass<ULyraPlayerSpawningManagerComponent>())
{
return PlayerSpawningComponent->ChoosePlayerStart(Player);
}
return Super::ChoosePlayerStart_Implementation(Player);
}
void ALyraGameMode::FinishRestartPlayer(AController* NewPlayer, const FRotator& StartRotation)
{
if (ULyraPlayerSpawningManagerComponent* PlayerSpawningComponent = GameState->FindComponentByClass<ULyraPlayerSpawningManagerComponent>())
{
PlayerSpawningComponent->FinishRestartPlayer(NewPlayer, StartRotation);
}
Super::FinishRestartPlayer(NewPlayer, StartRotation);
}
bool ALyraGameMode::PlayerCanRestart_Implementation(APlayerController* Player)
{
return ControllerCanRestart(Player);
}
bool ALyraGameMode::ControllerCanRestart(AController* Controller)
{
if (APlayerController* PC = Cast<APlayerController>(Controller))
{
if (!Super::PlayerCanRestart_Implementation(PC))
{
return false;
}
}
else
{
// Bot version of Super::PlayerCanRestart_Implementation
if ((Controller == nullptr) || Controller->IsPendingKillPending())
{
return false;
}
}
if (ULyraPlayerSpawningManagerComponent* PlayerSpawningComponent = GameState->FindComponentByClass<ULyraPlayerSpawningManagerComponent>())
{
return PlayerSpawningComponent->ControllerCanRestart(Controller);
}
return true;
}
void ALyraGameMode::InitGameState()
{
Super::InitGameState();
// Listen for the experience load to complete
ULyraExperienceManagerComponent* ExperienceComponent = GameState->FindComponentByClass<ULyraExperienceManagerComponent>();
check(ExperienceComponent);
ExperienceComponent->CallOrRegister_OnExperienceLoaded(FOnLyraExperienceLoaded::FDelegate::CreateUObject(this, &ThisClass::OnExperienceLoaded));
}
void ALyraGameMode::GenericPlayerInitialization(AController* NewPlayer)
{
Super::GenericPlayerInitialization(NewPlayer);
OnGameModePlayerInitialized.Broadcast(this, NewPlayer);
}
void ALyraGameMode::RequestPlayerRestartNextFrame(AController* Controller, bool bForceReset)
{
if (bForceReset && (Controller != nullptr))
{
Controller->Reset();
}
if (APlayerController* PC = Cast<APlayerController>(Controller))
{
GetWorldTimerManager().SetTimerForNextTick(PC, &APlayerController::ServerRestartPlayer_Implementation);
}
else if (ALyraPlayerBotController* BotController = Cast<ALyraPlayerBotController>(Controller))
{
GetWorldTimerManager().SetTimerForNextTick(BotController, &ALyraPlayerBotController::ServerRestartController);
}
}
bool ALyraGameMode::UpdatePlayerStartSpot(AController* Player, const FString& Portal, FString& OutErrorMessage)
{
// Do nothing, we'll wait until PostLogin when we try to spawn the player for real.
// Doing anything right now is no good, systems like team assignment haven't even occurred yet.
return true;
}
void ALyraGameMode::FailedToRestartPlayer(AController* NewPlayer)
{
Super::FailedToRestartPlayer(NewPlayer);
// If we tried to spawn a pawn and it failed, lets try again *note* check if there's actually a pawn class
// before we try this forever.
if (UClass* PawnClass = GetDefaultPawnClassForController(NewPlayer))
{
if (APlayerController* NewPC = Cast<APlayerController>(NewPlayer))
{
// If it's a player don't loop forever, maybe something changed and they can no longer restart if so stop trying.
if (PlayerCanRestart(NewPC))
{
RequestPlayerRestartNextFrame(NewPlayer, false);
}
else
{
UE_LOG(LogLyra, Verbose, TEXT("FailedToRestartPlayer(%s) and PlayerCanRestart returned false, so we're not going to try again."), *GetPathNameSafe(NewPlayer));
}
}
else
{
RequestPlayerRestartNextFrame(NewPlayer, false);
}
}
else
{
UE_LOG(LogLyra, Verbose, TEXT("FailedToRestartPlayer(%s) but there's no pawn class so giving up."), *GetPathNameSafe(NewPlayer));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment