So I am making a local multiplayer game and I have this system where it would put out a number on the top of the player, showing which player you are. For some reason, this number system works in editor but when I build it, there is no number on the top of any players. What’s weird about this is that the rest of the UI seems to work (item duration, item icon, etc) when put on the top of the player’s head. Here is what it looks like :
This is on the editor :
This is on build :
Here is a snippet of the code :
Gamemode Class :
#include "BrawlbblesGameMode.h"
#include "BrawlbblesGameInstance.h"
#include "Camera.h"
#include "EngineUtils.h"
#include "SpawnPoint.h"
#include "Algo/ForEach.h"
#include "Algo/RandomShuffle.h"
#include "Android/AndroidPlatformApplicationMisc.h"
#include "GameFramework/InputDeviceLibrary.h"
#include "Kismet/GameplayStatics.h"
ABrawlbblesGameMode::ABrawlbblesGameMode()
{
bStartPlayersAsSpectators = false;
DefaultPawnClass = nullptr;
}
void ABrawlbblesGameMode::BeginPlay()
{
Super::BeginPlay();
UBrawlbblesGameInstance* GameInstance = Cast<UBrawlbblesGameInstance>(GetWorld()->GetGameInstance());
if (GameInstance)
{
if (!GameInstance->bIsGameStarted)
{
GameInstance->bIsGameStarted = true;
// ✅ Load and add the Controls UI to the viewport
if (ControlsUIClass)
{
ControlsUI = CreateWidget<UUserWidget>(GetWorld(), ControlsUIClass);
if (ControlsUI)
{
ControlsUI->AddToViewport();
UE_LOG(LogTemp, Warning, TEXT("Showing Controls UI. Waiting for input to start..."));
}
}
// ✅ Set a persistent timer to remove UI and start the game
GetWorld()->GetTimerManager().SetTimer(
GameStartTimerHandle,
this,
&ABrawlbblesGameMode::StartGameAfterDelay,
5.0f,
false
);
}
}
// Find all SpawnPoints in the world
SpawnPoints.Empty();
TArray<AActor*> FoundActors;
UGameplayStatics::GetAllActorsOfClass(this, ASpawnPoint::StaticClass(), FoundActors);
for (AActor* Actor : FoundActors)
{
if (ASpawnPoint* SpawnPoint = Cast<ASpawnPoint>(Actor))
{
SpawnPoints.Add(SpawnPoint);
}
}
Algo::RandomShuffle(SpawnPoints);
// Remove auto-spawned player
if (APlayerController* AutoPlayer = UGameplayStatics::GetPlayerController(GetWorld(), 0))
{
UGameplayStatics::RemovePlayer(AutoPlayer, true);
}
// Restore players if switching levels
if (GameInstance && GameInstance->StoredPlayerIndexes.Num() > 0)
{
UE_LOG(LogTemp, Warning, TEXT("Restoring Players from Game Instance..."));
AmountOfPlayers = GameInstance->StoredPlayerIndexes.Num();
PlayerScores.Empty();
// Recreate players in the correct order using ControllerId
for (const auto& Elem : GameInstance->StoredPlayerIndexes)
{
int32 ControllerId = Elem.Key;
int32 PlayerIndex = Elem.Value;
FString Error = "Error";
// ✅ FIX: Try to get the existing Local Player instead of always creating a new one
ULocalPlayer* ExistingLocalPlayer = GetGameInstance()->FindLocalPlayerFromControllerId(ControllerId);
ULocalPlayer* LocalPlayer = ExistingLocalPlayer ? ExistingLocalPlayer : GetGameInstance()->CreateLocalPlayer(PlayerIndex, Error, true);
if (!LocalPlayer)
{
UE_LOG(LogTemp, Error, TEXT("Failed to create local player %d: %s"), PlayerIndex, *Error);
continue;
}
APlayerController* PlayerController = LocalPlayer->GetPlayerController(GetWorld());
if (!PlayerController)
{
UE_LOG(LogTemp, Error, TEXT("Failed to get player controller for local player %d"), PlayerIndex);
continue;
}
// Restore scores
int32 RestoredScore = GameInstance->StoredPlayerScores.Contains(ControllerId) ? GameInstance->StoredPlayerScores[ControllerId] : 0;
PlayerScores.Add(PlayerController, RestoredScore);
UE_LOG(LogTemp, Warning, TEXT("Restored Score: Player %d -> %d"), PlayerIndex, RestoredScore);
// Destroy any auto-spawned pawn
if (APawn* ExistingPawn = PlayerController->GetPawn())
{
ExistingPawn->Destroy();
}
// Restore character mapping
if (GameInstance->StoredControllerToCharacter.Contains(ControllerId))
{
if (ABrawlbblesCharacter* StoredCharacter = GameInstance->StoredControllerToCharacter[ControllerId].Get())
{
// ✅ FIX: Reset the character's position to a spawn point
FTransform SpawnTransform = SpawnPoints[PlayerIndex % SpawnPoints.Num()]->GetActorTransform();
StoredCharacter->SetActorTransform(SpawnTransform);
PlayerController->Possess(StoredCharacter);
StoredCharacter->EnableInput(PlayerController);
// ✅ FIX: Ensure UI is updated
FText PlayerNumber = FText::AsNumber(PlayerIndex + 1);
StoredCharacter->SetPlayerName(PlayerNumber);
int32 ColorIndex = PlayerIndex % PlayerTextColors.Num();
StoredCharacter->SetPlayerTextColor(PlayerTextColors[ColorIndex]);
continue;
}
}
// Spawn a new character if no stored character
SpawnPlayer(PlayerController, PlayerIndex);
}
// Clear stored data
GameInstance->StoredPlayerScores.Empty();
GameInstance->StoredControllerToCharacter.Empty();
GameInstance->StoredPlayerIndexes.Empty();
}
else
{
// Normal game start
SpawnLocalPlayers();
}
// ✅ Load and add the Score UI to the viewport
if (ScoreUIClass)
{
UScoreUI* ScoreWidget = CreateWidget<UScoreUI>(GetWorld(), ScoreUIClass);
if (ScoreWidget)
{
ScoreUI = ScoreWidget;
ScoreUI->InitializeScoreUI(AmountOfPlayers);
ScoreUI->AddToViewport();
ScoreUI->SetVisibility(ESlateVisibility::Hidden); // ✅ Hides UI initially
}
}
// Spawn and assign shared camera
SpawnSharedCamera();
// Mark game as started
bIsGameStarted = true;
}
void ABrawlbblesGameMode::HandleStartingNewPlayer_Implementation(APlayerController* NewPlayer)
{
Super::HandleStartingNewPlayer_Implementation(NewPlayer);
if (ABrawlbblesCharacter* Character = Cast<ABrawlbblesCharacter>(NewPlayer->GetPawn()))
{
Character->StartController();
}
}
void ABrawlbblesGameMode::PlayerEliminated(APlayerController* Killer, APlayerController* Victim)
{
if (bIsWinUIActive) return;
if (Victim)
{
EliminatedPlayers.Add(Victim); // Track eliminated player
}
if (Killer && Killer != Victim)
{
if (PlayerScores.Contains(Killer))
{
PlayerScores[Killer] += 1;
UE_LOG(LogTemp, Warning, TEXT("Player %s scored! Total Score: %d"),
*Killer->GetName(), PlayerScores[Killer]);
// ✅ Update the UI with all players' scores
UpdateAllScoresInUI();
// ✅ Update the UI
if (ScoreUI)
{
int32 MaxScore = ScoreUI->GetMaxScore();
if (PlayerScores[Killer] >= MaxScore)
{
ScoreUI->SetVisibility(ESlateVisibility::Visible);
ScoreUI->UpdateScore(Killer->GetPlatformUserId(), PlayerScores[Killer]);
// ✅ Delay before showing "Player X Wins" UI
FTimerHandle WinUITimer;
GetWorldTimerManager().SetTimer(WinUITimer, [this, Killer]()
{
ShowWinUI(Killer);
}, 2.0f, false); // Show after 2 seconds
return; // Skip further processing
}
}
}
else
{
UE_LOG(LogTemp, Error, TEXT("ERROR: Player %s is not in PlayerScores!"), *Killer->GetName());
}
}
int AlivePlayers = 0;
APlayerController* LastAlivePlayer = nullptr;
for (const auto& Elem : PlayerScores)
{
APlayerController* PlayerController = Elem.Key;
if (PlayerController && PlayerController->GetPawn() && !EliminatedPlayers.Contains(PlayerController))
{
AlivePlayers++;
LastAlivePlayer = PlayerController;
}
}
UE_LOG(LogTemp, Warning, TEXT("Alive players count: %d"), AlivePlayers);
// ✅ Show score UI only if one player remains
if (AlivePlayers == 1 && ScoreUI)
{
ScoreUI->SetVisibility(ESlateVisibility::Visible);
}
// ✅ FIX: Assign camera to the last alive player
if (Victim && Victim->GetViewTarget() == SharedCamera)
{
UE_LOG(LogTemp, Warning, TEXT("Eliminated player's view target was the shared camera. Reassigning..."));
// Find a new player to assign the camera to
APlayerController* NewCameraOwner = nullptr;
for (const auto& Elem : PlayerScores)
{
APlayerController* PlayerController = Elem.Key;
if (PlayerController && PlayerController->GetPawn() && !EliminatedPlayers.Contains(PlayerController))
{
NewCameraOwner = PlayerController;
break;
}
}
if (NewCameraOwner)
{
UE_LOG(LogTemp, Warning, TEXT("New camera owner: %s"), *NewCameraOwner->GetName());
NewCameraOwner->SetViewTarget(SharedCamera);
}
else
{
UE_LOG(LogTemp, Warning, TEXT("No alive players found to assign the camera."));
}
}
if (AlivePlayers <= 1)
{
UE_LOG(LogTemp, Warning, TEXT("Restarting game in 3 seconds..."));
// ✅ Show the Score UI before switching levels
if (ScoreUI)
{
ScoreUI->SetVisibility(ESlateVisibility::Visible);
}
FTimerHandle RestartTimer;
GetWorldTimerManager().SetTimer(RestartTimer, this, &ABrawlbblesGameMode::SwitchLevel, 3.0f, false);
}
}
void ABrawlbblesGameMode::SwitchLevel()
{
FString CurrentLevel = GetWorld()->GetMapName();
CurrentLevel.RemoveFromStart(GetWorld()->StreamingLevelsPrefix);
FString NextLevel = (CurrentLevel == "GalaxyLevel") ? "Fishbowl" : "GalaxyLevel";
UBrawlbblesGameInstance* GameInstance = GetGameInstance<UBrawlbblesGameInstance>();
if (GameInstance)
{
GameInstance->StoredPlayerScores.Empty();
GameInstance->StoredPlayerIndexes.Empty();
GameInstance->StoredControllerToCharacter.Empty();
GameInstance->SetMusicParameter("Invincible", 0.0f);
GameInstance->SetMusicParameter("Pause", 0.0f);
// Force re-enable input for all players
EnableAllPlayers();
APlayerController* LastAlivePlayer = nullptr;
// Store player data using ControllerId
for (const auto& Elem : PlayerScores)
{
APlayerController* PlayerController = Elem.Key;
if (PlayerController)
{
int32 ControllerId = PlayerController->GetLocalPlayer()->GetControllerId();
GameInstance->StoredPlayerScores.Add(ControllerId, Elem.Value);
GameInstance->StoredPlayerIndexes.Add(ControllerId, GameInstance->StoredPlayerIndexes.Num());
if (ABrawlbblesCharacter* BrawlbblesPlayerCharacter = Cast<ABrawlbblesCharacter>(PlayerController->GetPawn()))
{
GameInstance->StoredControllerToCharacter.Add(ControllerId, BrawlbblesPlayerCharacter);
}
// ✅ FIX: Ensure the last alive player has the camera before switching levels
if (!EliminatedPlayers.Contains(PlayerController))
{
LastAlivePlayer = PlayerController;
}
}
}
// ✅ FIX: Assign the camera before switching levels
if (LastAlivePlayer && SharedCamera)
{
UE_LOG(LogTemp, Warning, TEXT("Assigning camera to last alive player before switching levels."));
LastAlivePlayer->SetViewTarget(SharedCamera);
}
// Reset Game Mode variables
ResetGameModeVariables();
// Reset camera reference
SharedCamera = nullptr;
// Open the next level
UGameplayStatics::OpenLevel(this, FName(*NextLevel));
RestoreLocalPlayers();
}
}
void ABrawlbblesGameMode::AssignCameraToAllPlayers()
{
if (!SharedCamera)
{
return;
}
// Assign the same camera to every local player
for (int i = 0; i < AmountOfPlayers; i++)
{
APlayerController* PlayerController = UGameplayStatics::GetPlayerController(GetWorld(), i);
if (PlayerController)
{
PlayerController->SetViewTarget(SharedCamera);
PlayerController->bAutoManageActiveCameraTarget = false;
// 🔥 Log the new view target for verification
UE_LOG(LogTemp, Warning, TEXT("Player %d now has ViewTarget: %s"),
i,
*GetNameSafe(PlayerController->GetViewTarget()));
}
}
}
void ABrawlbblesGameMode::ResetGameModeVariables()
{
// Reset all relevant game mode variables
PlayerScores.Empty();
SpawnPoints.Empty();
AmountOfPlayers = 0;
bIsGameStarted = false;
// Stop any active timers
GetWorldTimerManager().ClearAllTimersForObject(this);
}
void ABrawlbblesGameMode::SpawnLocalPlayers()
{
/*if (UInputDeviceLibrary::GetAllActiveUsers(OutUsers) > 0)
{
for (int i = 0; i < OutUsers.Num(); i++)
{
FString OutError;
ULocalPlayer* NewLocalPlayer = GetGameInstance()->CreateLocalPlayer(i, OutError, true);
UE_LOG(LogTemp, Warning, TEXT("Detected active users: %d"), OutUsers.Num());
UE_LOG(LogTemp, Warning, TEXT("Local Player %d, Controller ID: %d"), i, NewLocalPlayer->GetControllerId());
if (NewLocalPlayer)
{
APlayerController* NewController = NewLocalPlayer->GetPlayerController(GetWorld());
if (NewController)
{
NewController->bAutoManageActiveCameraTarget = false;
// Destroy any auto-spawned pawn
if (APawn* AutoPawn = NewController->GetPawn())
{
AutoPawn->Destroy();
}
// Initialize score
PlayerScores.Add(NewController, 0);
// Spawn the player
SpawnPlayer(NewController, i);
}
}
AmountOfPlayers++;
}
}*/
int32 MaxLocalPlayers = 4; // Adjust based on your game design
for (int i = 0; i < MaxLocalPlayers; i++)
{
FString OutError;
ULocalPlayer* NewLocalPlayer = GetGameInstance()->CreateLocalPlayer(i, OutError, true);
if (NewLocalPlayer)
{
APlayerController* NewController = NewLocalPlayer->GetPlayerController(GetWorld());
if (NewController)
{
NewController->bAutoManageActiveCameraTarget = false;
// Destroy any auto-spawned pawn
if (APawn* AutoPawn = NewController->GetPawn())
{
AutoPawn->Destroy();
}
// Initialize score tracking
PlayerScores.Add(NewController, 0);
// Spawn and assign the player
SpawnPlayer(NewController, i);
UE_LOG(LogTemp, Warning, TEXT("✅ Local Player %d Created: Controller ID %d"), i, NewController->GetLocalPlayer()->GetControllerId());
}
else
{
UE_LOG(LogTemp, Error, TEXT("❌ Failed to get Player Controller for Local Player %d"), i);
}
}
else
{
UE_LOG(LogTemp, Error, TEXT("❌ Failed to create Local Player %d: %s"), i, *OutError);
}
}
AmountOfPlayers = MaxLocalPlayers;
}
void ABrawlbblesGameMode::SpawnPlayer(APlayerController* PlayerController, int PlayerIndex)
{
if (!PlayerController || SpawnPoints.Num() == 0)
{
UE_LOG(LogTemp, Error, TEXT("Failed to spawn player: Invalid controller or no spawn points available."));
return;
}
FTransform SpawnTransform = SpawnPoints[PlayerIndex % SpawnPoints.Num()]->GetActorTransform();
FActorSpawnParameters SpawnParams;
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
ABrawlbblesCharacter* NewPlayerPawn = GetWorld()->SpawnActor<ABrawlbblesCharacter>(PlayerCharacter, SpawnTransform, SpawnParams);
if (NewPlayerPawn)
{
PlayerController->Possess(NewPlayerPawn);
UE_LOG(LogTemp, Warning, TEXT("Player %d Possessed Character: %s"), PlayerIndex, *NewPlayerPawn->GetName());
NewPlayerPawn->EnableInput(PlayerController);
FText PlayerNumber = FText::AsNumber(PlayerIndex + 1);
NewPlayerPawn->SetPlayerName(PlayerNumber);
int32 ColorIndex = PlayerIndex % PlayerTextColors.Num();
NewPlayerPawn->SetPlayerTextColor(PlayerTextColors[ColorIndex]);
}
else
{
UE_LOG(LogTemp, Error, TEXT("Failed to spawn character for player %d"), PlayerIndex);
}
}
void ABrawlbblesGameMode::SpawnSharedCamera()
{
if (!SharedCamera && CameraActorClass)
{
FActorSpawnParameters SpawnParams;
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
SharedCamera = GetWorld()->SpawnActor<ACamera>(CameraActorClass, FVector(0, 0, 2000), FRotator(-45, 90, 0), SpawnParams);
}
if (SharedCamera)
{
GetWorldTimerManager().SetTimerForNextTick(this, &ABrawlbblesGameMode::AssignCameraToAllPlayers);
}
}
void ABrawlbblesGameMode::ShowWinUI(APlayerController* WinningPlayer)
{
if (!WinUIClass) return; // Ensure the widget class is assigned
// ✅ Set flag to prevent Score UI from being triggered
bIsWinUIActive = true;
// ✅ Hide Score UI when the Win UI appears
if (ScoreUI)
{
ScoreUI->SetVisibility(ESlateVisibility::Hidden);
}
UBrawlbblesGameInstance* GameInstance = Cast<UBrawlbblesGameInstance>(UGameplayStatics::GetGameInstance(GetWorld()));
if (GameInstance)
{
// Example: Set "Invincible" to 1 (Activate)
GameInstance->SetMusicParameter("Win", 1.0f);
}
// ✅ Create the "Player X Wins" UI
WinUI = CreateWidget<UUserWidget>(GetWorld(), WinUIClass);
if (WinUI)
{
WinUI->AddToViewport();
// ✅ Set "Player X Wins!" text
if (UTextBlock* WinText = Cast<UTextBlock>(WinUI->GetWidgetFromName(TEXT("WinText"))))
{
int32 PlayerIndex = WinningPlayer->GetLocalPlayer()->GetControllerId();
FText WinMessage = FText::Format(NSLOCTEXT("Brawlbbles", "WinMessage", "Player {0} Wins!"), FText::AsNumber(PlayerIndex + 1));
WinText->SetText(WinMessage);
}
}
// ✅ Prevent further gameplay interactions
DisableAllPlayers();
// ✅ Restart the game after showing the win message
FTimerHandle RestartTimer;
GetWorldTimerManager().SetTimer(RestartTimer, this, &ABrawlbblesGameMode::RestartGame, 10.0f, false);
}
void ABrawlbblesGameMode::DisableAllPlayers()
{
for (FConstPlayerControllerIterator It = GetWorld()->GetPlayerControllerIterator(); It; ++It)
{
APlayerController* PlayerController = It->Get();
if (PlayerController)
{
PlayerController->SetIgnoreMoveInput(true);
PlayerController->SetIgnoreLookInput(true);
}
}
}
void ABrawlbblesGameMode::EnableAllPlayers()
{
for (FConstPlayerControllerIterator It = GetWorld()->GetPlayerControllerIterator(); It; ++It)
{
APlayerController* PC = It->Get();
if (PC)
{
PC->SetIgnoreMoveInput(false);
PC->SetIgnoreLookInput(false);
UE_LOG(LogTemp, Warning, TEXT("Input re-enabled for: %s"), *PC->GetName());
}
}
}
void ABrawlbblesGameMode::RestartGame()
{
UBrawlbblesGameInstance* GameInstance = GetGameInstance<UBrawlbblesGameInstance>();
if (GameInstance)
{
GameInstance->ResetStoredData();
// Example: Set "Invincible" to 1 (Activate)
GameInstance->SetMusicParameter("Win", 0.0f);
// Assign and play the event
GameInstance->MusicComponent->SetEvent(GameInstance->BackgroundMusicEvent);
GameInstance->MusicComponent->Play();
GameInstance->bIsGameStarted = false;
}
bIsWinUIActive = false;
SwitchLevel();
}
void ABrawlbblesGameMode::UpdateAllScoresInUI()
{
if (!ScoreUI) return; // Ensure the UI exists
UE_LOG(LogTemp, Warning, TEXT("Updating all player scores in UI..."));
// Iterate through all players and update their scores
for (const auto& Elem : PlayerScores)
{
APlayerController* PlayerController = Elem.Key;
int32 Score = Elem.Value;
if (PlayerController)
{
int32 PlayerIndex = PlayerController->GetLocalPlayer()->GetControllerId();
ScoreUI->UpdateScore(PlayerIndex, Score);
}
}
}
void ABrawlbblesGameMode::RestoreLocalPlayers()
{
for (int32 i = 0; i < AmountOfPlayers; i++)
{
FString OutError;
ULocalPlayer* LocalPlayer = GetGameInstance()->CreateLocalPlayer(i, OutError, true);
if (LocalPlayer)
{
APlayerController* NewController = LocalPlayer->GetPlayerController(GetWorld());
if (NewController)
{
NewController->bAutoManageActiveCameraTarget = false;
SpawnPlayer(NewController, i);
}
}
}
}
void ABrawlbblesGameMode::TogglePauseMenu(APlayerController* RequestingPlayer)
{
if (!PauseUIClass) return;
if (bIsPaused)
{
UBrawlbblesGameInstance* GameInstance = GetGameInstance<UBrawlbblesGameInstance>();
if (GameInstance)
{
GameInstance->SetMusicParameter("Pause", 0.0f);
}
// ✅ Unpause the game
if (PauseUI)
{
PauseUI->RemoveFromParent();
PauseUI = nullptr;
}
// ✅ Restore normal game speed
UGameplayStatics::SetGlobalTimeDilation(GetWorld(), 1.0f);
// ✅ Restore input for all players
for (FConstPlayerControllerIterator It = GetWorld()->GetPlayerControllerIterator(); It; ++It)
{
APlayerController* PC = It->Get();
if (PC)
{
PC->bShowMouseCursor = false;
PC->SetIgnoreMoveInput(false);
PC->SetIgnoreLookInput(false);
PC->EnableInput(PC);
}
}
UE_LOG(LogTemp, Warning, TEXT("Game Unpaused!"));
}
else
{
UBrawlbblesGameInstance* GameInstance = GetGameInstance<UBrawlbblesGameInstance>();
if (GameInstance)
{
GameInstance->SetMusicParameter("Pause", 1.0f);
}
// ✅ Slow down time to 0 (freeze the game)
UGameplayStatics::SetGlobalTimeDilation(GetWorld(), 0.0f);
// ✅ Disable movement but keep input active
for (FConstPlayerControllerIterator It = GetWorld()->GetPlayerControllerIterator(); It; ++It)
{
APlayerController* PC = It->Get();
if (PC)
{
PC->bShowMouseCursor = true;
PC->SetIgnoreMoveInput(true);
PC->SetIgnoreLookInput(true);
PC->EnableInput(PC); // ✅ Keep listening for unpause
}
}
// ✅ Show Pause UI
if (!PauseUI)
{
PauseUI = CreateWidget<UUserWidget>(RequestingPlayer, PauseUIClass);
if (PauseUI)
{
PauseUI->AddToViewport();
}
}
UE_LOG(LogTemp, Warning, TEXT("Game Paused!"));
}
bIsPaused = !bIsPaused;
}
void ABrawlbblesGameMode::StartGameAfterDelay()
{
UE_LOG(LogTemp, Warning, TEXT("5 seconds passed! Starting game..."));
// ✅ Remove Controls UI
if (ControlsUI)
{
ControlsUI->RemoveFromParent();
ControlsUI->SetVisibility(ESlateVisibility::Hidden);
ControlsUI = nullptr;
}
}
Player Class :
// Copyright Epic Games, Inc. All Rights Reserved.
#include "BrawlbblesCharacter.h"
#include "BrawlbblesGameInstance.h"
#include "BrawlbblesGameMode.h"
#include "Camera.h"
#include "EngineUtils.h"
#include "Engine/LocalPlayer.h"
#include "Components/CapsuleComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "GameFramework/SpringArmComponent.h"
#include "GameFramework/Controller.h"
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"
#include "FMODBlueprintStatics.h"
#include "FMODEvent.h"
#include "InputActionValue.h"
#include "Projectile.h"
#include "Camera/CameraActor.h"
#include "Components/DecalComponent.h"
#include "Components/Image.h"
#include "Components/ProgressBar.h"
#include "Components/TextBlock.h"
#include "Components/WidgetComponent.h"
#include "Kismet/GameplayStatics.h"
#include "Kismet/KismetMathLibrary.h"
DEFINE_LOG_CATEGORY(LogTemplateCharacter);
//////////////////////////////////////////////////////////////////////////
// ABrawlbblesCharacter
ABrawlbblesCharacter::ABrawlbblesCharacter()
{
// Create attack hitbox
AttackHitbox = CreateDefaultSubobject<USphereComponent>(TEXT("AttackHitbox"));
AttackHitbox->SetupAttachment(GetMesh()); // Attach to the character
AttackHitbox->SetSphereRadius(75.0f); // Set hitbox size
AttackHitbox->SetCollisionEnabled(ECollisionEnabled::NoCollision); // Disable by default
AttackHitbox->SetCollisionObjectType(ECC_WorldDynamic);
AttackHitbox->SetCollisionResponseToAllChannels(ECR_Overlap);
AttackHitbox->SetGenerateOverlapEvents(true);
// Ensure capsule detects item overlaps
GetCapsuleComponent()->SetCollisionResponseToAllChannels(ECR_Ignore);
GetCapsuleComponent()->SetCollisionResponseToChannel(ECC_WorldDynamic, ECR_Overlap);
GetCapsuleComponent()->SetGenerateOverlapEvents(true);
// Bind overlap event
AttackHitbox->OnComponentBeginOverlap.AddDynamic(this, &ABrawlbblesCharacter::OnAttackHitboxOverlap);
AttackHitbox->OnComponentEndOverlap.AddDynamic(this, &ABrawlbblesCharacter::OnAttackHitboxEndOverlap);
// Set size for collision capsule
GetCapsuleComponent()->InitCapsuleSize(42.f, 96.0f);
// Don't rotate when the controller rotates. Let that just affect the camera.
bUseControllerRotationPitch = false;
bUseControllerRotationYaw = false;
bUseControllerRotationRoll = false;
// Configure character movement
GetCharacterMovement()->bOrientRotationToMovement = false; // Character moves in the direction of input...
GetCharacterMovement()->RotationRate = FRotator(0.0f, 500.0f, 0.0f); // ...at this rotation rate
// Note: For faster iteration times these variables, and many more, can be tweaked in the Character Blueprint
// instead of recompiling to adjust them
GetCharacterMovement()->JumpZVelocity = 700.f;
GetCharacterMovement()->MinAnalogWalkSpeed = 20.f;
// Reduce gravity scale for a lighter feel
GetCharacterMovement()->GravityScale = 0.2f;
GetCharacterMovement()->GroundFriction = 3.0f;
// Reduce acceleration for a slower response, making the character feel buoyant
GetCharacterMovement()->MaxAcceleration = 1200.0f;
// Reduce braking deceleration to prevent abrupt stopping
GetCharacterMovement()->BrakingDecelerationWalking = 800.0f;
GetCharacterMovement()->BrakingDecelerationFalling = 200.0f;
// Increase air control for smoother in-air movement
GetCharacterMovement()->AirControl = 2.0f;
// Lower max speed slightly to balance floatiness
GetCharacterMovement()->MaxWalkSpeed = 400.0f;
// Enable flying if you want more control in the air
GetCharacterMovement()->SetMovementMode(MOVE_Flying);
// Create the widget component
PlayerIndicatorWidget = CreateDefaultSubobject<UWidgetComponent>(TEXT("PlayerIndicator"));
PlayerIndicatorWidget->SetupAttachment(RootComponent);
// Position it above the player's head
PlayerIndicatorWidget->SetRelativeLocation(FVector(0.f, 0.f, 120.f));
// Set it to always face the camera
PlayerIndicatorWidget->SetWidgetSpace(EWidgetSpace::Screen);
// Set default size
PlayerIndicatorWidget->SetDrawSize(FVector2D(200.f, 50.f));
// Create the decal component
ShadowDecal = CreateDefaultSubobject<UDecalComponent>(TEXT("ShadowDecal"));
ShadowDecal->SetupAttachment(RootComponent);
// Set the decal size (adjust as needed)
ShadowDecal->DecalSize = FVector(100.0f, 200.0f, 200.0f);
// Ensure the shadow faces downward
ShadowDecal->SetRelativeRotation(FRotator(-90, 0, 0));
ShadowPlane = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("ShadowPlane"));
ShadowPlane->SetupAttachment(RootComponent);
// Disable collision (so it doesn't interfere with physics)
ShadowPlane->SetCollisionEnabled(ECollisionEnabled::NoCollision);
// Ensure the plane is hidden in-game (but still receives decals)
ShadowPlane->SetCastShadow(false);
ShadowPlane->SetHiddenInGame(false);
// Adjust scale to fit character size
ShadowPlane->SetWorldScale3D(FVector(1.5f, 1.5f, 1.0f));
// Create the widget component for the item duration bar
ItemDurationWidget = CreateDefaultSubobject<UWidgetComponent>(TEXT("ItemDurationWidget"));
ItemDurationWidget->SetupAttachment(RootComponent);
// Position it above the player's head, slightly above the player indicator
ItemDurationWidget->SetRelativeLocation(FVector(0.f, 0.f, 150.f));
// Set it to always face the camera
ItemDurationWidget->SetWidgetSpace(EWidgetSpace::Screen);
// Set default size
ItemDurationWidget->SetDrawSize(FVector2D(200.f, 20.f));
// Create the widget component for item icon
ItemIconWidgetComponent = CreateDefaultSubobject<UWidgetComponent>(TEXT("ItemIconWidget"));
ItemIconWidgetComponent->SetupAttachment(RootComponent);
// Position it above the player's head
ItemIconWidgetComponent->SetRelativeLocation(FVector(0.f, 0.f, 180.f));
// Set it to always face the camera
ItemIconWidgetComponent->SetWidgetSpace(EWidgetSpace::Screen);
// Set default size
ItemIconWidgetComponent->SetDrawSize(FVector2D(100.f, 100.f));
// Initially hide it
ItemIconWidgetComponent->SetVisibility(false);
}
void ABrawlbblesCharacter::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
// Get the scale of the player
float ScaleFactor = GetActorScale3D().X; // Assuming uniform scaling
// Adjust movement speed based on scale
GetCharacterMovement()->MaxWalkSpeed = BaseWalkSpeed * ScaleFactor;
if (!bIsRotating && !GetCharacterMovement()->bOrientRotationToMovement)
{
FVector Velocity = GetVelocity();
if (!Velocity.IsNearlyZero()) // Only update rotation if moving
{
FRotator CurrentRotation = GetActorRotation();
FRotator DesiredRotation = Velocity.Rotation();
FRotator NewRotation = FMath::RInterpTo(CurrentRotation, DesiredRotation, DeltaTime, 5.0f);
SetActorRotation(NewRotation);
}
}
// Adjust jump velocity based on scale
GetCharacterMovement()->JumpZVelocity = BaseJumpVelocity * ScaleFactor;
// Adjust air control for smoother aerial movement at larger scales
GetCharacterMovement()->AirControl = BaseAirControl * ScaleFactor;
// Keep PlayerIndicatorWidget at the correct world position
if (PlayerIndicatorWidget)
{
float HeightOffset = GetCapsuleComponent()->GetScaledCapsuleHalfHeight() + 30.f;
FVector NewLocation = GetActorLocation() + FVector(0.f, 0.f, HeightOffset);
PlayerIndicatorWidget->SetWorldLocation(NewLocation);
}
// Keep ItemDurationWidget at the correct position
if (ItemDurationWidget)
{
float HeightOffset = GetCapsuleComponent()->GetScaledCapsuleHalfHeight() + 50.f;
FVector NewLocation = GetActorLocation() + FVector(0.f, 0.f, HeightOffset);
ItemDurationWidget->SetWorldLocation(NewLocation);
}
// Update the item duration UI if the timer is active
if (GetWorld()->GetTimerManager().IsTimerActive(ItemDurationTimerHandle))
{
float RemainingTime = GetWorld()->GetTimerManager().GetTimerRemaining(ItemDurationTimerHandle);
UpdateItemDuration(RemainingTime);
}
if (!ShadowPlane) return;
FVector Start = GetActorLocation();
FVector End = Start - FVector(0, 0, 10000.0f); // Large downward trace
FHitResult Hit;
FCollisionQueryParams CollisionParams;
CollisionParams.AddIgnoredActor(this); // Ignore the player itself
// Perform the raycast
if (GetWorld()->LineTraceSingleByChannel(Hit, Start, End, ECC_Visibility, CollisionParams))
{
// Move the shadow to the hit location
FVector ShadowLocation = Hit.Location + FVector(0, 0, 5.0f); // Small offset to prevent z-fighting
ShadowPlane->SetWorldLocation(ShadowLocation);
// Ensure the shadow remains flat on the surface
FRotator ShadowRotation = FRotationMatrix::MakeFromZ(Hit.Normal).Rotator();
ShadowPlane->SetWorldRotation(FRotator(0.f, ShadowRotation.Yaw, 0.f));
}
else
{
// Hide shadow if no ground detected
ShadowPlane->SetWorldLocation(End);
}
}
void ABrawlbblesCharacter::BeginPlay()
{
Super::BeginPlay();
//Add Input Mapping Context
if (APlayerController* PlayerController = Cast<APlayerController>(Controller))
{
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
{
Subsystem->AddMappingContext(DefaultMappingContext, 0);
}
AActor* MainCamera = UGameplayStatics::GetActorOfClass(GetWorld(), ACamera::StaticClass());
PlayerController->SetViewTarget(MainCamera);
}
TSubclassOf<UUserWidget> PlayerIndicatorClass = LoadObject<UClass>(nullptr, TEXT("/Game/UI/WBP_PlayerIndicator.WBP_PlayerIndicator_C"));
if (PlayerIndicatorClass)
{
PlayerIndicatorWidget->SetWidgetClass(PlayerIndicatorClass);
}
PlayerIndicatorWidget->SetVisibility(true);
// Load Shadow Material
UMaterialInterface* ShadowMaterial = LoadObject<UMaterialInterface>(nullptr, TEXT("/Game/Materials/M_Shadow.M_Shadow"));
if (ShadowMaterial)
{
ShadowDecal->SetDecalMaterial(ShadowMaterial);
}
// Load Shadow Plane Material
UMaterialInterface* ShadowPlaneMaterial = LoadObject<UMaterialInterface>(nullptr, TEXT("/Game/Materials/M_ShadowPlane.M_ShadowPlane"));
if (ShadowPlaneMaterial)
{
ShadowPlane->SetMaterial(0, ShadowPlaneMaterial);
}
// Load Static Mesh for Shadow Plane
UStaticMesh* PlaneMesh = LoadObject<UStaticMesh>(nullptr, TEXT("/Engine/BasicShapes/Plane.Plane"));
if (PlaneMesh)
{
ShadowPlane->SetStaticMesh(PlaneMesh);
}
// Load Item Duration Widget Class
TSubclassOf<UUserWidget> ItemWidgetClass = LoadObject<UClass>(nullptr, TEXT("/Game/UI/WBP_ItemDurationBar.WBP_ItemDurationBar_C"));
if (ItemWidgetClass)
{
ItemDurationWidget->SetWidgetClass(ItemWidgetClass);
}
ItemDurationWidget->SetVisibility(false);
}
void ABrawlbblesCharacter::StartController()
{
//Add Input Mapping Context
if (APlayerController* PlayerController = Cast<APlayerController>(Controller))
{
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
{
Subsystem->AddMappingContext(DefaultMappingContext, 0);
}
AActor* MainCamera = UGameplayStatics::GetActorOfClass(GetWorld(), ACamera::StaticClass());
PlayerController->SetViewTarget(MainCamera);
}
}
void ABrawlbblesCharacter::SetPlayerName(FText NewName)
{
if (PlayerIndicatorWidget)
{
// ✅ Ensure widget is initialized
if (!PlayerIndicatorWidget->GetUserWidgetObject())
{
PlayerIndicatorWidget->InitWidget();
}
// ✅ Ensure widget is valid before accessing text block
if (UUserWidget* Widget = PlayerIndicatorWidget->GetUserWidgetObject())
{
if (UTextBlock* NameText = Cast<UTextBlock>(Widget->GetWidgetFromName(TEXT("PlayerNameText"))))
{
NameText->SetText(NewName);
}
}
}
}
void ABrawlbblesCharacter::SetPlayerTextColor(FLinearColor NewColor)
{
if (UUserWidget* Widget = PlayerIndicatorWidget->GetUserWidgetObject())
{
if (UTextBlock* NameText = Cast<UTextBlock>(Widget->GetWidgetFromName(TEXT("PlayerNameText"))))
{
NameText->SetColorAndOpacity(NewColor);
}
}
}
//////////////////////////////////////////////////////////////////////////
// Input
void ABrawlbblesCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
// Set up action bindings
if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent)) {
// Jumping
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Started, this, &ACharacter::Jump);
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &ACharacter::StopJumping);
// Moving
EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &ABrawlbblesCharacter::Move);
// Rotate
EnhancedInputComponent->BindAction(RotateAction, ETriggerEvent::Triggered, this, &ABrawlbblesCharacter::Rotate);
// Attack
EnhancedInputComponent->BindAction(AttackAction, ETriggerEvent::Triggered, this, &ABrawlbblesCharacter::Attack);
// Item
EnhancedInputComponent->BindAction(ItemAction, ETriggerEvent::Triggered, this, &ABrawlbblesCharacter::Item);
EnhancedInputComponent->BindAction(PauseAction, ETriggerEvent::Triggered, this, &ABrawlbblesCharacter::PauseGame);
}
else
{
UE_LOG(LogTemplateCharacter, Error, TEXT("'%s' Failed to find an Enhanced Input component! This template is built to use the Enhanced Input system. If you intend to use the legacy system, then you will need to update this C++ file."), *GetNameSafe(this));
}
}
inline void ABrawlbblesCharacter::PauseGame()
{
if (ABrawlbblesGameMode* GameMode = Cast<ABrawlbblesGameMode>(UGameplayStatics::GetGameMode(GetWorld())))
{
GameMode->TogglePauseMenu(Cast<APlayerController>(GetController()));
}
}
void ABrawlbblesCharacter::OnAttackHitboxOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
Enemy = Cast<ABrawlbblesCharacter>(OtherActor);
if (Enemy && Enemy != this) // Ensure it's another player
{
bCanDamage = true;
// Change material color to red
/*UMaterialInstanceDynamic* DynMaterial = Enemy->GetMesh()->CreateAndSetMaterialInstanceDynamic(0);
if (DynMaterial)
{
DynMaterial->SetVectorParameterValue(FName("BaseColor"), FLinearColor::Red);
DynMaterial->SetScalarParameterValue(FName("Emissive"), 1.0);
bCanDamage = true;
}*/
// Reset color after a short delay
/*FTimerHandle ResetColorTimer;
GetWorld()->GetTimerManager().SetTimer(ResetColorTimer, [Enemy]()
{
if (Enemy && Enemy->GetMesh())
{
UMaterialInstanceDynamic* ResetMaterial = Enemy->GetMesh()->CreateAndSetMaterialInstanceDynamic(0);
if (ResetMaterial)
{
ResetMaterial->SetVectorParameterValue(FName("BaseColor"), FLinearColor::White);
ResetMaterial->SetScalarParameterValue(FName("Emissive"), 0.0);
}
}
}, 0.5f, false);*/
}
}
void ABrawlbblesCharacter::OnAttackHitboxEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
Enemy = Cast<ABrawlbblesCharacter>(OtherActor);
if (Enemy && Enemy != this) // Ensure it's another player
{
bCanDamage = false;
Enemy = nullptr;
/*
UMaterialInstanceDynamic* ResetMaterial = Enemy->GetMesh()->CreateAndSetMaterialInstanceDynamic(0);
if (ResetMaterial)
{
ResetMaterial->SetVectorParameterValue(FName("BaseColor"), FLinearColor::White);
ResetMaterial->SetScalarParameterValue(FName("Emissive"), 0.0);
bCanDamage = false;
}*/
}
}
void ABrawlbblesCharacter::UpdateShadowPosition()
{
if (!ShadowDecal) return;
// Raycast from the player straight down
FVector Start = GetActorLocation();
FVector End = Start - FVector(0, 0, 100000.0f); // Large downward trace
FHitResult Hit;
FCollisionQueryParams CollisionParams;
CollisionParams.AddIgnoredActor(this); // Ignore the player itself
// Perform the raycast
if (GetWorld()->LineTraceSingleByChannel(Hit, Start, End, ECC_GameTraceChannel1, CollisionParams))
{
// Move the decal to the hit location
FVector DecalLocation = Hit.Location + FVector(0, 0, 10.0f); // Prevent Z-fighting
ShadowDecal->SetWorldLocation(DecalLocation);
ShadowDecal->SetVisibility(true);
// Keep the decal facing straight down
FRotator NewRotation = FRotator(-90.0f, 0.0f, 0.0f);
ShadowDecal->SetWorldRotation(NewRotation);
}
else
{
// If no ground is hit, place it far below
ShadowDecal->SetWorldLocation(End);
ShadowDecal->SetVisibility(true);
}
}
void ABrawlbblesCharacter::DisableFarting()
{
CurrentItemType = EItemType::None; // Remove the fart ability
GetWorld()->GetTimerManager().ClearTimer(FartCooldownTimerHandle);
}
void ABrawlbblesCharacter::Fart()
{
UFMODEvent* FartEvent = LoadObject<UFMODEvent>(nullptr, TEXT("FMODEvent'/Game/FMOD/Events/Fart.Fart'"));
if (FartEvent)
{
UFMODBlueprintStatics::PlayEventAtLocation(GetWorld(), FartEvent, GetActorTransform(), true);
}
LaunchCharacter(GetActorForwardVector() * FartForce, false, false);
// If this is the first fart, start the 2-second timer
if (CurrentItemType == EItemType::Fart)
{
// Ensure the timer is only set once and isn't reset incorrectly
if (!GetWorld()->GetTimerManager().IsTimerActive(FartCooldownTimerHandle))
{
ClearItemDuration();
StartItemDuration(MaxFartTime);
GetWorld()->GetTimerManager().SetTimer(FartCooldownTimerHandle, this, &ABrawlbblesCharacter::DisableFarting, MaxFartTime, false);
}
}
}
void ABrawlbblesCharacter::Milk()
{
if (bIsInvincible) return; // Ignore if already invincible
bIsInvincible = true;
ClearItemDuration();
StartItemDuration(InvincibilityDuration);
// Get the game instance and cast it
UBrawlbblesGameInstance* GameInstance = Cast<UBrawlbblesGameInstance>(UGameplayStatics::GetGameInstance(GetWorld()));
if (GameInstance)
{
// Example: Set "Invincible" to 1 (Activate)
GameInstance->SetMusicParameter("Invincible", 1.0f);
}
// Start invincibility timer
GetWorld()->GetTimerManager().SetTimer(InvincibilityTimerHandle, this, &ABrawlbblesCharacter::DisableInvincibility, InvincibilityDuration, false);
// Start the rainbow effect ticking every frame
GetWorld()->GetTimerManager().SetTimer(RainbowEffectTimerHandle, this, &ABrawlbblesCharacter::UpdateRainbowEffect, RainbowTickRate, true);
}
void ABrawlbblesCharacter::WaterGun()
{
if (!bCanUseWaterGun)
{
bCanUseWaterGun = true;
ClearItemDuration();
StartItemDuration(WaterGunDuration);
// ✅ Always reset the timer on fire
GetWorld()->GetTimerManager().ClearTimer(WaterGunTimerHandle);
GetWorld()->GetTimerManager().SetTimer(WaterGunTimerHandle, this, &ABrawlbblesCharacter::DisableWaterGun, WaterGunDuration, false);
}
if (ProjectileClass)
{
FVector SpawnLocation = GetActorLocation() + GetActorForwardVector() * 200.0f;
FRotator SpawnRotation = GetActorRotation();
FActorSpawnParameters SpawnParams;
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;
UFMODEvent* ShootEvent = LoadObject<UFMODEvent>(nullptr, TEXT("FMODEvent'/Game/FMOD/Events/Shoot.Shoot'"));
if (ShootEvent)
{
UFMODBlueprintStatics::PlayEventAtLocation(GetWorld(), ShootEvent, GetActorTransform(), true);
}
AProjectile* SpawnedProjectile = GetWorld()->SpawnActor<AProjectile>(ProjectileClass, SpawnLocation, SpawnRotation, SpawnParams);
if (SpawnedProjectile)
{
SpawnedProjectile->SetInstigatorCharacter(this); // Set the shooter as the instigator
}
}
}
float ABrawlbblesCharacter::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent,
AController* EventInstigator, AActor* DamageCauser)
{
if (bIsInvincible) return 0.0f; // Ignore damage if invincible
float ActualDamage = Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);
UMaterialInstanceDynamic* DynMaterial = GetMesh()->CreateAndSetMaterialInstanceDynamic(0);
if (DynMaterial)
{
DynMaterial->SetVectorParameterValue(FName("BaseColor"), FLinearColor::Red);
DynMaterial->SetScalarParameterValue(FName("Emissive"), 1.0);
}
// Reset color after a short delay
FTimerHandle ResetColorTimer;
GetWorld()->GetTimerManager().SetTimer(ResetColorTimer, [this]()
{
UMaterialInstanceDynamic* ResetMaterial = GetMesh()->CreateAndSetMaterialInstanceDynamic(0);
if (ResetMaterial)
{
ResetMaterial->SetVectorParameterValue(FName("BaseColor"), FLinearColor::White);
ResetMaterial->SetScalarParameterValue(FName("Emissive"), 0.0);
}
}, 0.5f, false);
// Apply knockback effect
if (DamageCauser)
{
FVector KnockbackDirection = (GetActorLocation() - DamageCauser->GetActorLocation()).GetSafeNormal();
//KnockbackDirection.Z = 0.5f; // Slight vertical lift
float KnockbackForce = 5000.0f; // Adjust force as needed
LaunchCharacter(KnockbackDirection * KnockbackForce, true, true);
}
// Reduce health
Health -= ActualDamage;
// Log damage
UE_LOG(LogTemp, Warning, TEXT("%s took %f damage!"), *GetName(), ActualDamage);
// Check if the character is dead
if (Health <= 0)
{
UFMODEvent* PlayerPopEvent = LoadObject<UFMODEvent>(nullptr, TEXT("FMODEvent'/Game/FMOD/Events/PlayerPop.PlayerPop'"));
if (PlayerPopEvent)
{
UFMODBlueprintStatics::PlayEventAtLocation(GetWorld(), PlayerPopEvent, GetActorTransform(), true);
}
else
{
UE_LOG(LogTemp, Error, TEXT("Failed to load Playerpop FMOD event!"));
}
Die(Cast<APlayerController>(DamageCauser->GetInstigatorController()));
}
return ActualDamage;
}
void ABrawlbblesCharacter::Die(APlayerController* Killer)
{
ABrawlbblesGameMode* GameMode = Cast<ABrawlbblesGameMode>(UGameplayStatics::GetGameMode(GetWorld()));
if (GameMode)
{
GameMode->PlayerEliminated(Killer, Cast<APlayerController>(GetController()));
}
Destroy();
}
void ABrawlbblesCharacter::SetItemIcon(FName ItemName)
{
// Load Player Indicator Widget Class
TSubclassOf<UUserWidget> ItemIconWidgetClass = LoadObject<UClass>(nullptr, TEXT("/Game/UI/WBP_ItemIconWidget.WBP_ItemIconWidget_C"));
if (ItemIconWidgetClass)
{
ItemIconWidgetComponent->SetWidgetClass(ItemIconWidgetClass);
}
if (!ItemIconWidgetComponent) return;
UUserWidget* Widget = ItemIconWidgetComponent->GetUserWidgetObject();
if (!Widget) return;
UImage* IconImage = Cast<UImage>(Widget->GetWidgetFromName(TEXT("ItemIcon")));
if (!IconImage) return;
UTexture2D* IconTexture = nullptr;
if (ItemName == "Milk")
{
IconTexture = MilkTexture;
}
else if (ItemName == "WaterGun")
{
IconTexture = WaterGunTexture;
}
else if (ItemName == "Fart")
{
IconTexture = FartTexture;
}
if (IconImage && IconTexture)
{
IconImage->SetBrushFromTexture(IconTexture);
PlayerIndicatorWidget->SetVisibility(false);
ItemIconWidgetComponent->SetVisibility(true);
}
}
void ABrawlbblesCharacter::DisableAttackHitbox()
{
AttackHitbox->SetCollisionEnabled(ECollisionEnabled::NoCollision);
}
void ABrawlbblesCharacter::DisableWaterGun()
{
bCanUseWaterGun = false;
GetWorld()->GetTimerManager().ClearTimer(WaterGunTimerHandle);
CurrentItemType = EItemType::None;
}
void ABrawlbblesCharacter::UpdateRainbowEffect()
{
if (!GetMesh()) return;
static float Hue = 0.0f;
Hue += 5.0f; // Increase hue each tick
if (Hue >= 360.0f) Hue = 0.0f; // Loop hue value
// Convert HSV to RGB
FLinearColor RainbowColor = FLinearColor::MakeFromHSV8(Hue, 255, 255);
// Apply dynamic material color
UMaterialInstanceDynamic* DynMaterial = GetMesh()->CreateAndSetMaterialInstanceDynamic(0);
if (DynMaterial)
{
DynMaterial->SetVectorParameterValue(FName("BaseColor"), RainbowColor);
}
}
void ABrawlbblesCharacter::DisableInvincibility()
{
bIsInvincible = false;
CurrentItemType = EItemType::None;
// Stop the rainbow effect
GetWorld()->GetTimerManager().ClearTimer(RainbowEffectTimerHandle);
// Get the game instance and cast it
UBrawlbblesGameInstance* GameInstance = Cast<UBrawlbblesGameInstance>(UGameplayStatics::GetGameInstance(GetWorld()));
if (GameInstance)
{
// Example: Set "Invincible" to 1 (Activate)
GameInstance->SetMusicParameter("Invincible", 0.0f);
}
// Reset material color to normal (white)
UMaterialInstanceDynamic* DynMaterial = GetMesh()->CreateAndSetMaterialInstanceDynamic(0);
if (DynMaterial)
{
DynMaterial->SetVectorParameterValue(FName("BaseColor"), FLinearColor::White);
}
}
void ABrawlbblesCharacter::UpdateItemDuration(float RemainingTime)
{
if (UUserWidget* Widget = ItemDurationWidget->GetUserWidgetObject())
{
if (UProgressBar* ProgressBar = Cast<UProgressBar>(Widget->GetWidgetFromName(TEXT("ItemDurationProgressBar"))))
{
float Progress = FMath::Clamp(RemainingTime / MaxItemDuration, 0.0f, 1.0f);
ProgressBar->SetPercent(Progress);
}
}
}
void ABrawlbblesCharacter::Move(const FInputActionValue& Value)
{
const FVector2D MovementVector = Value.Get<FVector2D>();
if (Controller != nullptr)
{
// Get the Camera's Rotation
const FRotator CameraRotation = UGameplayStatics::GetPlayerCameraManager(GetWorld(), 0)->GetTransformComponent()->GetComponentRotation();
// Get Movement Directions based on Camera Rotation
const FVector ForwardDirection = UKismetMathLibrary::GetForwardVector(CameraRotation);
const FVector RightDirection = UKismetMathLibrary::GetRightVector(CameraRotation);
// Compute movement input direction
FVector MovementDirection = (ForwardDirection * MovementVector.Y) + (RightDirection * MovementVector.X);
MovementDirection.Z = 0; // Ensure no vertical influence
if (!MovementDirection.IsNearlyZero()) // Only rotate if there is movement input
{
// Get the desired rotation to face the movement direction
FRotator DesiredRotation = MovementDirection.Rotation();
// Interpolate towards the desired rotation for smooth rotation
FRotator NewRotation = FMath::RInterpTo(GetActorRotation(), DesiredRotation, GetWorld()->GetDeltaSeconds(), 10.0f);
SetActorRotation(NewRotation);
}
// Apply movement
AddMovementInput(ForwardDirection, MovementVector.Y);
AddMovementInput(RightDirection, MovementVector.X);
}
}
void ABrawlbblesCharacter::Rotate(const FInputActionValue& Value)
{
// Get input as a Vector2D (X = Up/Down, Y = Left/Right)
const FVector2D RotationInput = Value.Get<FVector2D>();
if (GetCharacterMovement())
{
// Disable auto-rotation with movement
GetCharacterMovement()->bOrientRotationToMovement = false;
bIsRotating = true;
}
// Apply rotation based on stick input
float YawRotation = RotationInput.X * RotationMultiplier * GetWorld()->GetDeltaSeconds(); // Left/Right
float PitchRotation = RotationInput.Y * RotationMultiplier * GetWorld()->GetDeltaSeconds(); // Up/Down
// Get current rotation
FRotator NewRotation = GetActorRotation();
// Modify yaw (left/right)
NewRotation.Yaw += YawRotation;
// Modify pitch (up/down) and clamp it to prevent flipping
NewRotation.Pitch = FMath::Clamp(NewRotation.Pitch + PitchRotation, -85.0f, 85.0f);
// Apply rotation
SetActorRotation(NewRotation);
// Store this as the last manual rotation
TargetRotation = NewRotation;
}
void ABrawlbblesCharacter::Attack(const FInputActionValue& Value)
{
if (!bCanAttack) return; // If on cooldown, do nothing
bCanAttack = false; // Disable attacking
UFMODEvent* PunchEvent = LoadObject<UFMODEvent>(nullptr, TEXT("FMODEvent'/Game/FMOD/Events/Punch.Punch'"));
if (PunchEvent)
{
UFMODBlueprintStatics::PlayEventAtLocation(GetWorld(), PunchEvent, GetActorTransform(), true);
}
else
{
UE_LOG(LogTemp, Error, TEXT("Failed to load Punch FMOD event!"));
}
// Enable AttackHitbox
AttackHitbox->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
if (AttackAnimMontage)
{
PlayAnimMontage(AttackAnimMontage, 1);
if (bCanDamage)
{
if (Enemy)
{
UGameplayStatics::ApplyDamage(Enemy, AttackDamage, GetController(), this, UDamageType::StaticClass());
}
}
}
// Start timer to disable attack hitbox after 0.5 seconds
GetWorld()->GetTimerManager().SetTimer(AttackHitboxTimer, this, &ABrawlbblesCharacter::DisableAttackHitbox, 0.5f, false);
// Start the cooldown timer for attacking
GetWorld()->GetTimerManager().SetTimer(AttackCooldownTimer, this, &ABrawlbblesCharacter::ResetAttackCooldown, AttackCooldown, false);
}
void ABrawlbblesCharacter::ResetAttackCooldown()
{
bCanAttack = true;
}
void ABrawlbblesCharacter::Item(const FInputActionValue& Value)
{
switch (CurrentItemType)
{
case EItemType::Fart:
Fart();
break;
case EItemType::Milk:
Milk();
break;
case EItemType::WaterGun:
WaterGun();
break;
}
}
void ABrawlbblesCharacter::Jump()
{
Super::Jump();
if (GetCharacterMovement()->IsMovingOnGround())
{
JumpCurrentCount = 0; // Reset jump count when on the ground
State = 0;
}
if (JumpCurrentCount < MaxJumpCount)
{
LaunchCharacter(FVector(0, 0, GetCharacterMovement()->JumpZVelocity), false, true);
JumpCurrentCount++;
// Optional: Add a tiny horizontal push to jumps for a softer feel
AddMovementInput(GetActorForwardVector(), 0.1f);
State = 500;
}
}
void ABrawlbblesCharacter::Landed(const FHitResult& Hit)
{
Super::Landed(Hit);
// Apply a small bounce effect on landing
float BounceStrength = FMath::Clamp(GetCharacterMovement()->Velocity.Z * -0.3f, -100.f, 150.f);
GetCharacterMovement()->Velocity.Z = BounceStrength;
}
void ABrawlbblesCharacter::StartItemDuration(float Duration)
{
MaxItemDuration = Duration;
ToggleItemUI(true); // Hide player indicator, show item bar
GetWorld()->GetTimerManager().SetTimer(ItemDurationTimerHandle, this, &ABrawlbblesCharacter::ClearItemDuration, Duration, false);
}
void ABrawlbblesCharacter::ClearItemDuration()
{
UpdateItemDuration(0);
ToggleItemUI(false); // Show player indicator, hide item bar
GetWorld()->GetTimerManager().ClearTimer(ItemDurationTimerHandle);
}
void ABrawlbblesCharacter::ToggleItemUI(bool bItemActive)
{
if (PlayerIndicatorWidget)
{
PlayerIndicatorWidget->SetVisibility(!bItemActive);
}
if (ItemIconWidgetComponent)
{
ItemIconWidgetComponent->SetVisibility(false);
}
if (ItemDurationWidget)
{
ItemDurationWidget->SetVisibility(bItemActive);
}
}