15 Commits

32 changed files with 1179 additions and 359 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
Content/Materials/Material.uasset LFS Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -7,7 +7,10 @@
{
"Name": "MyProject3",
"Type": "Runtime",
"LoadingPhase": "Default"
"LoadingPhase": "Default",
"AdditionalDependencies": [
"Engine"
]
}
],
"Plugins": [

View File

@@ -6,8 +6,7 @@
#include "DrawDebugHelpers.h"
#include "Math/UnrealMathUtility.h"
#include "EnemyProjectile.h"
// Include SpaceshipPawn to access player-specific functionality
#include "SpaceShooterGameMode.h"
#include "SpaceshipPawn.h"
AEnemySpaceship::AEnemySpaceship()
@@ -34,6 +33,10 @@ AEnemySpaceship::AEnemySpaceship()
EnemyMesh->SetCollisionResponseToChannel(ECC_Pawn, ECR_Ignore); // Ignore other pawns
EnemyMesh->SetCollisionResponseToChannel(ECC_GameTraceChannel1, ECR_Block); // Block player (for damage)
EnemyMesh->SetGenerateOverlapEvents(true);
CurrentVelocity = FVector::ZeroVector;
TargetVelocity = FVector::ZeroVector;
LastPosition = FVector::ZeroVector;
}
void AEnemySpaceship::BeginPlay()
@@ -52,20 +55,26 @@ void AEnemySpaceship::BeginPlay()
// Randomize initial strafe direction
StrafeDirection = FMath::RandBool() ? 1.0f : -1.0f;
LastPosition = GetActorLocation();
bInitializedFlank = false;
LastFlankUpdateTime = 0.0f;
CurrentFlankTarget = GetActorLocation();
InitializeDynamicMaterial();
}
// Modify Tick function to include flanking behavior
void AEnemySpaceship::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
if (!PlayerPawn)
{
// Try to find player again if not set
AActor* FoundPlayerPawn = UGameplayStatics::GetPlayerPawn(GetWorld(), 0);
PlayerPawn = Cast<ASpaceshipPawn>(FoundPlayerPawn);
if (!PlayerPawn)
return;
if (!PlayerPawn) return;
}
// Calculate distance to player
@@ -89,6 +98,9 @@ void AEnemySpaceship::Tick(float DeltaTime)
case EEnemyBehaviorState::Strafe:
PerformStrafe(DeltaTime);
break;
case EEnemyBehaviorState::Flank:
PerformFlank(DeltaTime);
break;
}
// Debug state information
@@ -101,6 +113,7 @@ void AEnemySpaceship::Tick(float DeltaTime)
case EEnemyBehaviorState::Attack: StateString = "Attack"; break;
case EEnemyBehaviorState::Retreat: StateString = "Retreat"; break;
case EEnemyBehaviorState::Strafe: StateString = "Strafe"; break;
case EEnemyBehaviorState::Flank: StateString = "Flank"; break;
}
GEngine->AddOnScreenDebugMessage(-1, 0.0f, FColor::Orange,
@@ -108,30 +121,213 @@ void AEnemySpaceship::Tick(float DeltaTime)
}
}
void AEnemySpaceship::SmoothMove(const FVector& TargetLocation, float DeltaTime)
{
FVector CurrentLocation = GetActorLocation();
FVector DirectionToTarget = (TargetLocation - CurrentLocation);
float DistanceToTarget = DirectionToTarget.Size();
// Calculate target velocity
FVector NewTargetVelocity = DirectionToTarget;
if (DistanceToTarget > 1.0f)
{
NewTargetVelocity = DirectionToTarget.GetSafeNormal() * FMath::Min(MovementSpeed, DistanceToTarget / DeltaTime);
}
else
{
NewTargetVelocity = FVector::ZeroVector;
}
// Smoothly interpolate current velocity towards target velocity
CurrentVelocity = FMath::VInterpTo(CurrentVelocity, NewTargetVelocity, DeltaTime, InterpSpeed);
// Clamp acceleration
FVector Acceleration = (CurrentVelocity - (CurrentLocation - LastPosition) / DeltaTime);
if (Acceleration.SizeSquared() > MaxAcceleration * MaxAcceleration)
{
Acceleration = Acceleration.GetSafeNormal() * MaxAcceleration;
CurrentVelocity = ((CurrentLocation - LastPosition) / DeltaTime) + (Acceleration * DeltaTime);
}
// Update position
LastPosition = CurrentLocation;
FVector NewLocation = CurrentLocation + (CurrentVelocity * DeltaTime);
SetActorLocation(NewLocation);
}
void AEnemySpaceship::UpdateBehaviorState()
{
// Override random behavior changes when distance requires specific behavior
if (!PlayerPawn) return;
FVector DesiredPosition = CalculatePositionAwayFromOtherEnemies();
FVector PlayerVelocity = PlayerPawn->GetVelocity();
float PlayerSpeed = PlayerVelocity.Size();
float AggressionRoll = FMath::FRand();
if (DistanceToPlayer < MinDistanceToPlayer)
{
// Too close, retreat or strafe
CurrentBehaviorState = FMath::RandBool() ? EEnemyBehaviorState::Retreat : EEnemyBehaviorState::Strafe;
if (AggressionRoll < AggressionFactor * 0.3f)
{
CurrentBehaviorState = EEnemyBehaviorState::Attack;
}
else
{
CurrentBehaviorState = EEnemyBehaviorState::Retreat;
}
}
else if (DistanceToPlayer < AttackRange)
{
// Within attack range but not too close, either attack or strafe
if (CurrentBehaviorState != EEnemyBehaviorState::Attack &&
CurrentBehaviorState != EEnemyBehaviorState::Strafe)
// Increased chance of flanking
if (AggressionRoll < AggressionFactor * FlankingFrequency)
{
CurrentBehaviorState = FMath::RandBool() ? EEnemyBehaviorState::Attack : EEnemyBehaviorState::Strafe;
// Only change to flank state if we're not already flanking
// This prevents unnecessary state changes that could cause position jumps
if (CurrentBehaviorState != EEnemyBehaviorState::Flank)
{
CurrentBehaviorState = EEnemyBehaviorState::Flank;
bIsFlankingRight = FMath::RandBool();
bInitializedFlank = false; // Will trigger new flank position calculation
LastFlankUpdateTime = GetWorld()->GetTimeSeconds();
}
}
else if (AggressionRoll < AggressionFactor * 0.8f) // Increased from 0.7f
{
CurrentBehaviorState = EEnemyBehaviorState::Attack;
}
else
{
CurrentBehaviorState = EEnemyBehaviorState::Strafe;
StrafeDirection = FMath::RandBool() ? 1.0f : -1.0f;
}
}
else
{
// Too far, chase
CurrentBehaviorState = EEnemyBehaviorState::Chase;
}
}
// Add this new function to handle enemy spacing
FVector AEnemySpaceship::CalculatePositionAwayFromOtherEnemies()
{
FVector AveragePosition = GetActorLocation();
int32 EnemyCount = 0;
// Find all enemy spaceships
TArray<AActor*> FoundEnemies;
UGameplayStatics::GetAllActorsOfClass(GetWorld(), AEnemySpaceship::StaticClass(), FoundEnemies);
for (AActor* Enemy : FoundEnemies)
{
if (Enemy != this)
{
float Distance = FVector::Dist(GetActorLocation(), Enemy->GetActorLocation());
if (Distance < MinDistanceToOtherEnemies)
{
AveragePosition += Enemy->GetActorLocation();
EnemyCount++;
}
}
}
if (EnemyCount > 0)
{
AveragePosition /= (EnemyCount + 1);
// Calculate position away from the cluster
FVector AwayFromCrowd = GetActorLocation() - AveragePosition;
AwayFromCrowd.Normalize();
return GetActorLocation() + (AwayFromCrowd * MinDistanceToOtherEnemies);
}
return GetActorLocation();
}
FVector AEnemySpaceship::CalculateFlankPosition()
{
if (!PlayerPawn) return GetActorLocation();
// Get player's forward vector and velocity
FVector PlayerForward = PlayerPawn->GetActorForwardVector();
FVector PlayerVelocity = PlayerPawn->GetVelocity();
// Use player's velocity direction if they're moving, otherwise use their forward vector
FVector BaseDirection = PlayerVelocity.SizeSquared() > 100.0f ?
PlayerVelocity.GetSafeNormal() : PlayerForward;
// Calculate the flanking angle
float AngleRadians = FMath::DegreesToRadians(FlankAngle * (bIsFlankingRight ? 1.0f : -1.0f));
// Rotate the vector for flanking
FVector FlankDirection = BaseDirection.RotateAngleAxis(AngleRadians, FVector::UpVector);
// Calculate the final position with some variation in distance
float VariedDistance = FlankDistance * FMath::RandRange(0.8f, 1.2f);
return PlayerPawn->GetActorLocation() + (FlankDirection * VariedDistance);
}
void AEnemySpaceship::PerformFlank(float DeltaTime)
{
if (!PlayerPawn) return;
float CurrentTime = GetWorld()->GetTimeSeconds();
// Initialize flank position if needed
if (!bInitializedFlank)
{
CurrentFlankTarget = CalculateFlankPosition();
bInitializedFlank = true;
LastFlankUpdateTime = CurrentTime;
}
// Update flank position periodically
if (CurrentTime - LastFlankUpdateTime >= FlankPositionUpdateInterval)
{
// Smoothly transition to new flank position
FVector NewFlankPosition = CalculateFlankPosition();
CurrentFlankTarget = FMath::VInterpTo(
CurrentFlankTarget,
NewFlankPosition,
DeltaTime,
2.0f // Interpolation speed
);
LastFlankUpdateTime = CurrentTime;
// Occasionally switch flanking direction
if (FMath::FRand() < 0.3f) // 30% chance to switch direction
{
bIsFlankingRight = !bIsFlankingRight;
}
}
// Calculate distance to current flank target
float DistanceToFlankTarget = FVector::Dist(GetActorLocation(), CurrentFlankTarget);
// Calculate movement speed based on distance
float CurrentFlankSpeed = FMath::Min(FlankingSpeed, DistanceToFlankTarget * 2.0f);
// Move towards flank position
FVector DirectionToFlank = (CurrentFlankTarget - GetActorLocation()).GetSafeNormal();
FVector TargetPosition = GetActorLocation() + DirectionToFlank * CurrentFlankSpeed * DeltaTime;
// Use smooth movement
SmoothMove(TargetPosition, DeltaTime);
// Face the player while flanking
FVector DirectionToPlayer = (PlayerPawn->GetActorLocation() - GetActorLocation()).GetSafeNormal();
FRotator TargetRotation = DirectionToPlayer.Rotation();
FRotator NewRotation = FMath::RInterpTo(GetActorRotation(), TargetRotation, DeltaTime, InterpSpeed);
SetActorRotation(NewRotation);
// Fire more frequently while flanking
if (bCanFire && DistanceToPlayer < AttackRange)
{
Fire();
}
}
void AEnemySpaceship::ChangeBehaviorState()
{
// Random chance to change behavior state
@@ -154,18 +350,21 @@ void AEnemySpaceship::PerformChase(float DeltaTime)
if (PlayerPawn)
{
// Calculate direction to player
FVector Direction = (PlayerPawn->GetActorLocation() - GetActorLocation()).GetSafeNormal();
FVector DirectionToPlayer = (PlayerPawn->GetActorLocation() - GetActorLocation()).GetSafeNormal();
// Move towards the player
FVector NewLocation = GetActorLocation() + Direction * MovementSpeed * DeltaTime;
SetActorLocation(NewLocation);
// Calculate target position
FVector TargetPosition = GetActorLocation() + DirectionToPlayer * MovementSpeed * DeltaTime;
// Face towards the player
FRotator NewRotation = Direction.Rotation();
// Use smooth movement
SmoothMove(TargetPosition, DeltaTime);
// Smoothly rotate to face player
FRotator TargetRotation = DirectionToPlayer.Rotation();
FRotator NewRotation = FMath::RInterpTo(GetActorRotation(), TargetRotation, DeltaTime, InterpSpeed);
SetActorRotation(NewRotation);
// If within attack range, fire occasionally
if (DistanceToPlayer < AttackRange && bCanFire && FMath::FRand() < 0.3f)
// Fire if within range
if (DistanceToPlayer < AttackRange && bCanFire)
{
Fire();
}
@@ -193,24 +392,32 @@ void AEnemySpaceship::PerformRetreat(float DeltaTime)
{
if (PlayerPawn)
{
// Move away from player
FVector Direction = (GetActorLocation() - PlayerPawn->GetActorLocation()).GetSafeNormal();
FVector NewLocation = GetActorLocation() + Direction * MovementSpeed * DeltaTime;
SetActorLocation(NewLocation);
// Calculate ideal retreat position
FVector DirectionFromPlayer = (GetActorLocation() - PlayerPawn->GetActorLocation()).GetSafeNormal();
FVector DesiredPosition = PlayerPawn->GetActorLocation() + (DirectionFromPlayer * OptimalCombatDistance);
// Keep facing the player while backing up
FVector FaceDirection = (PlayerPawn->GetActorLocation() - GetActorLocation()).GetSafeNormal();
FRotator NewRotation = FaceDirection.Rotation();
// Consider other enemies
FVector AvoidCrowdingPosition = CalculatePositionAwayFromOtherEnemies();
FVector FinalTargetPosition = FMath::Lerp(DesiredPosition, AvoidCrowdingPosition, 0.3f);
// Use smooth movement
SmoothMove(FinalTargetPosition, DeltaTime);
// Smoothly rotate to face player
FVector DirectionToPlayer = (PlayerPawn->GetActorLocation() - GetActorLocation()).GetSafeNormal();
FRotator TargetRotation = DirectionToPlayer.Rotation();
FRotator NewRotation = FMath::RInterpTo(GetActorRotation(), TargetRotation, DeltaTime, InterpSpeed);
SetActorRotation(NewRotation);
// Fire while retreating
if (bCanFire)
// Fire while retreating if in range
if (bCanFire && DistanceToPlayer < AttackRange)
{
Fire();
}
}
}
void AEnemySpaceship::PerformStrafe(float DeltaTime)
{
if (PlayerPawn)
@@ -218,19 +425,44 @@ void AEnemySpaceship::PerformStrafe(float DeltaTime)
// Calculate direction to player
FVector DirectionToPlayer = (PlayerPawn->GetActorLocation() - GetActorLocation()).GetSafeNormal();
// Calculate strafe direction (perpendicular to direction to player)
// Calculate ideal combat distance
float CurrentDistance = DistanceToPlayer;
float DistanceAdjustment = 0.0f;
if (CurrentDistance < OptimalCombatDistance)
{
DistanceAdjustment = -1.0f; // Move away
}
else if (CurrentDistance > OptimalCombatDistance + 200.0f)
{
DistanceAdjustment = 1.0f; // Move closer
}
// Calculate strafe direction
FVector StrafeVector = FVector::CrossProduct(DirectionToPlayer, FVector::UpVector) * StrafeDirection;
// Move sideways while maintaining distance
FVector NewLocation = GetActorLocation() + StrafeVector * StrafeSpeed * DeltaTime;
SetActorLocation(NewLocation);
// Calculate target position
FVector DistanceAdjustmentVector = DirectionToPlayer * DistanceAdjustment * MovementSpeed * 0.5f;
FVector StrafeMovement = StrafeVector * StrafeSpeed;
FVector DesiredMovement = (StrafeMovement + DistanceAdjustmentVector) * DeltaTime;
// Get position avoiding other enemies
FVector CrowdAvoidancePosition = CalculatePositionAwayFromOtherEnemies();
// Blend between desired movement and crowd avoidance
FVector TargetPosition = GetActorLocation() + DesiredMovement;
FVector FinalTargetPosition = FMath::Lerp(TargetPosition, CrowdAvoidancePosition, 0.3f);
// Use smooth movement
SmoothMove(FinalTargetPosition, DeltaTime);
// Face towards the player
FRotator NewRotation = DirectionToPlayer.Rotation();
FRotator TargetRotation = DirectionToPlayer.Rotation();
FRotator NewRotation = FMath::RInterpTo(GetActorRotation(), TargetRotation, DeltaTime, InterpSpeed);
SetActorRotation(NewRotation);
// Fire while strafing
if (bCanFire)
// Fire while strafing if in range
if (bCanFire && DistanceToPlayer < AttackRange)
{
Fire();
}
@@ -239,12 +471,7 @@ void AEnemySpaceship::PerformStrafe(float DeltaTime)
void AEnemySpaceship::Fire()
{
if (!ProjectileClass)
{
if (GEngine)
GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Red, TEXT("Enemy ProjectileClass not set!"));
return;
}
if (!ProjectileClass) return;
UWorld* World = GetWorld();
if (World)
@@ -252,9 +479,11 @@ void AEnemySpaceship::Fire()
FVector SpawnLocation = ProjectileSpawnPoint->GetComponentLocation();
FRotator SpawnRotation = GetActorRotation();
// Add slight randomness to rotation for less perfect aim
float RandPitch = FMath::RandRange(-5.0f, 5.0f);
float RandYaw = FMath::RandRange(-5.0f, 5.0f);
// Less random spread during flanking for more accurate shots
float SpreadMultiplier = (CurrentBehaviorState == EEnemyBehaviorState::Flank) ? 0.9f : 1.0f;
float RandPitch = FMath::RandRange(-5.0f, 5.0f) * SpreadMultiplier;
float RandYaw = FMath::RandRange(-5.0f, 5.0f) * SpreadMultiplier;
SpawnRotation.Pitch += RandPitch;
SpawnRotation.Yaw += RandYaw;
@@ -262,7 +491,6 @@ void AEnemySpaceship::Fire()
SpawnParams.Owner = this;
SpawnParams.Instigator = GetInstigator();
// Spawn the projectile using EnemyProjectile class
AEnemyProjectile* Projectile = World->SpawnActor<AEnemyProjectile>(
ProjectileClass,
SpawnLocation,
@@ -270,12 +498,6 @@ void AEnemySpaceship::Fire()
SpawnParams
);
if (Projectile)
{
if (GEngine)
GEngine->AddOnScreenDebugMessage(-1, 0.5f, FColor::Yellow, TEXT("Enemy Fired!"));
}
// Start fire rate timer
bCanFire = false;
GetWorldTimerManager().SetTimer(FireTimerHandle, this, &AEnemySpaceship::ResetFire, FireRate, false);
@@ -287,6 +509,57 @@ void AEnemySpaceship::ResetFire()
bCanFire = true;
}
void AEnemySpaceship::InitializeDynamicMaterial()
{
if (EnemyMesh)
{
// Store the original material
OriginalMaterial = EnemyMesh->GetMaterial(0);
if (OriginalMaterial)
{
// Create dynamic material instance only once
DynamicMaterialInstance = UMaterialInstanceDynamic::Create(OriginalMaterial, this);
if (DynamicMaterialInstance)
{
// Apply the dynamic material instance to the mesh
EnemyMesh->SetMaterial(0, DynamicMaterialInstance);
// Initialize flash color to transparent
DynamicMaterialInstance->SetVectorParameterValue("FlashColor", FLinearColor(0.0f, 0.0f, 0.0f, 0.0f));
}
}
}
}
void AEnemySpaceship::ApplyDamageFlash()
{
// Only set the flash color if we have a valid dynamic material instance
if (DynamicMaterialInstance)
{
// Set the flash color
DynamicMaterialInstance->SetVectorParameterValue("FlashColor", DamageFlashColor);
// Set timer to reset the flash
GetWorldTimerManager().SetTimer(
DamageFlashTimerHandle,
this,
&AEnemySpaceship::ResetDamageFlash,
DamageFlashDuration,
false
);
}
}
void AEnemySpaceship::ResetDamageFlash()
{
// Reset the flash color only if we have a valid dynamic material instance
if (DynamicMaterialInstance)
{
DynamicMaterialInstance->SetVectorParameterValue("FlashColor", FLinearColor(0.0f, 0.0f, 0.0f, 0.0f));
}
}
float AEnemySpaceship::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent,
AController* EventInstigator, AActor* DamageCauser)
{
@@ -301,6 +574,9 @@ float AEnemySpaceship::TakeDamage(float DamageAmount, FDamageEvent const& Damage
FString::Printf(TEXT("Enemy Hit! Health: %f"), CurrentHealth));
}
// Apply visual damage effects
ApplyDamageFlash();
// When damaged, prefer retreat or strafe behaviors temporarily
if (FMath::RandBool())
{
@@ -338,6 +614,11 @@ void AEnemySpaceship::Die()
GetActorLocation()
);
if (ASpaceShooterGameMode* GameMode = Cast<ASpaceShooterGameMode>(UGameplayStatics::GetGameMode(GetWorld())))
{
GameMode->IncrementKillCount();
}
// Destroy the enemy
Destroy();
}

View File

@@ -11,7 +11,8 @@ enum class EEnemyBehaviorState : uint8
Chase, // Actively pursue the player
Attack, // Stop and shoot at the player
Retreat, // Move away from player when too close
Strafe // Move sideways while attacking
Strafe, // Move sideways while attacking
Flank // Execute flanking maneuver
};
UCLASS()
@@ -32,17 +33,45 @@ protected:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat")
USceneComponent* ProjectileSpawnPoint;
// Add interpolation speed for smooth movement
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement")
float MovementSpeed = 500.0f;
float InterpSpeed = 3.0f;
// Add max acceleration to prevent sudden movements
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement")
float MaxAcceleration = 2000.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement")
float AttackRange = 1500.0f;
float MovementSpeed = 1200.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement")
float MinDistanceToPlayer = 500.0f;
float FlankingSpeed = 1200.0f; // Even faster during flanking
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement")
float StrafeSpeed = 300.0f;
float FlankAngle = 60.0f; // Angle for flanking maneuver
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement")
float FlankDistance = 2000.0f; // Distance to maintain during flanking
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement")
float FlankPositionUpdateInterval = 1.0f; // How often to update flank position
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement")
float AttackRange = 4000.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement")
float MinDistanceToPlayer = 1500.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement")
float StrafeSpeed = 500.0f;
// Add new property for minimum distance to other enemies
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement")
float MinDistanceToOtherEnemies = 800.0f;
// Add new property for optimal combat distance
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement")
float OptimalCombatDistance = 2500.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat")
float MaxHealth = 100.0f;
@@ -74,6 +103,26 @@ protected:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AI")
float StateChangeChance = 0.3f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AI")
float AggressionFactor = 0.7f; // Higher values make the enemy more likely to choose aggressive actions
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AI")
float FlankingFrequency = 0.6f; // Increased chance of flanking (was 0.4f)
UPROPERTY()
UMaterialInterface* OriginalMaterial;
UPROPERTY()
UMaterialInstanceDynamic* DynamicMaterialInstance;
UPROPERTY(EditAnywhere, Category = "Effects")
FLinearColor DamageFlashColor = FLinearColor(1.0f, 0.0f, 0.0f, 1.0f);
UPROPERTY(EditAnywhere, Category = "Effects")
float DamageFlashDuration = 0.1f;
FTimerHandle DamageFlashTimerHandle;
public:
virtual void Tick(float DeltaTime) override;
@@ -105,9 +154,32 @@ private:
void ChangeBehaviorState();
void Die();
void SmoothMove(const FVector& TargetLocation, float DeltaTime);
// AI behavior implementation functions
void PerformChase(float DeltaTime);
void PerformAttack(float DeltaTime);
void PerformRetreat(float DeltaTime);
void PerformStrafe(float DeltaTime);
void PerformFlank(float DeltaTime);
FVector CalculatePositionAwayFromOtherEnemies();
FVector CalculateFlankPosition();
FVector CurrentVelocity;
FVector TargetVelocity;
FVector LastPosition;
FVector FlankPosition;
float FlankTimer;
bool bIsFlankingRight;
FTimerHandle FlankUpdateTimer;
float LastFlankUpdateTime;
FVector CurrentFlankTarget;
bool bInitializedFlank;
void InitializeDynamicMaterial();
void ApplyDamageFlash();
void ResetDamageFlash();
};

View File

@@ -41,6 +41,10 @@ ASpaceShooterGameMode::ASpaceShooterGameMode()
{
GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Yellow, TEXT("GameMode Constructor"));
}
KillCount = 0;
bIsGameOver = false;
RemainingTime = GameDuration;
}
void ASpaceShooterGameMode::StartPlay()
@@ -60,6 +64,9 @@ void ASpaceShooterGameMode::StartPlay()
{
GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Green, TEXT("GameMode StartPlay"));
}
RemainingTime = GameDuration;
GetWorldTimerManager().SetTimer(GameTimerHandle, this, &ASpaceShooterGameMode::UpdateGameTimer, 1.0f, true);
}
void ASpaceShooterGameMode::Tick(float DeltaTime)
@@ -99,12 +106,11 @@ void ASpaceShooterGameMode::SpawnEnemy()
switch (CurrentPattern)
{
case ESpawnPattern::Random:
// Spawn a single enemy at a random edge location
{
UWorld* World = GetWorld();
if (World && EnemyClass)
{
FVector SpawnLocation = GetScreenEdgeSpawnLocation();
FVector SpawnLocation = GetRandomSpawnLocation();
FRotator SpawnRotation = FRotator::ZeroRotator;
FActorSpawnParameters SpawnParams;
SpawnParams.SpawnCollisionHandlingOverride =
@@ -142,32 +148,25 @@ void ASpaceShooterGameMode::SpawnEnemyWave()
if (!World || !EnemyClass)
return;
// Choose a random direction for the wave
float WaveAngle = FMath::RandRange(0.0f, 2.0f * PI);
FVector2D EdgeDirection(FMath::Cos(WaveAngle), FMath::Sin(WaveAngle));
// Get player location for facing direction
FVector PlayerLocation = GetPlayerLocation();
// Get screen bounds
TArray<FVector2D> ScreenBounds = GetScreenBounds();
float ScreenWidth = ScreenBounds[1].X - ScreenBounds[0].X;
// Choose a random angle for the wave
float WaveAngle = FMath::RandRange(0.0f, 2.0f * PI);
FVector2D BaseDirection(FMath::Cos(WaveAngle), FMath::Sin(WaveAngle));
// Create a line of enemies perpendicular to the direction
FVector2D PerpDirection(-EdgeDirection.Y, EdgeDirection.X);
// Create a perpendicular direction for the wave line
FVector2D PerpDirection(-BaseDirection.Y, BaseDirection.X);
// Spawn wave of enemies - increased distance from 2000 to MinimumSpawnDistance
// Spawn wave of enemies
for (int32 i = 0; i < WaveSize; i++)
{
FVector ProposedLocation;
ProposedLocation.X = PlayerLocation.X + (EdgeDirection.X * MinimumSpawnDistance) +
(PerpDirection.X * (i - WaveSize / 2) * FormationSpacing);
ProposedLocation.Y = PlayerLocation.Y + (EdgeDirection.Y * MinimumSpawnDistance) +
(PerpDirection.Y * (i - WaveSize / 2) * FormationSpacing);
ProposedLocation.Z = PlayerLocation.Z;
// Calculate base spawn position
FVector BaseSpawnPos = PlayerLocation + FVector(BaseDirection.X, BaseDirection.Y, 0) * MinimumSpawnDistance;
// Ensure the spawn location is far enough from the player
FVector SpawnLocation = EnsureMinimumSpawnDistance(ProposedLocation, PlayerLocation);
// Offset along the wave line
FVector Offset = FVector(PerpDirection.X, PerpDirection.Y, 0) * (i - WaveSize / 2) * FormationSpacing;
FVector SpawnLocation = BaseSpawnPos + Offset;
FRotator SpawnRotation = FRotator::ZeroRotator;
FActorSpawnParameters SpawnParams;
@@ -183,7 +182,6 @@ void ASpaceShooterGameMode::SpawnEnemyWave()
}
}
// Increase wave counter and possibly switch back to random
CurrentWaveCount++;
if (CurrentWaveCount >= 3)
{
@@ -194,6 +192,14 @@ void ASpaceShooterGameMode::SpawnEnemyWave()
void ASpaceShooterGameMode::SpawnEnemyFormation()
{
// Count current enemies
TArray<AActor*> FoundEnemies;
UGameplayStatics::GetAllActorsOfClass(GetWorld(), AEnemySpaceship::StaticClass(), FoundEnemies);
// Only spawn if we haven't reached the maximum
if (FoundEnemies.Num() < MaxEnemies)
{
UWorld* World = GetWorld();
if (!World || !EnemyClass)
return;
@@ -284,6 +290,7 @@ void ASpaceShooterGameMode::SpawnEnemyFormation()
// Switch back to random pattern after a formation spawn
CurrentPattern = ESpawnPattern::Random;
}
}
void ASpaceShooterGameMode::SpawnEnemyFlanking()
{
@@ -291,10 +298,9 @@ void ASpaceShooterGameMode::SpawnEnemyFlanking()
if (!World || !EnemyClass)
return;
// Get player location
FVector PlayerLocation = GetPlayerLocation();
// Spawn enemies from multiple sides (usually 2-3 sides)
// Spawn enemies from multiple sides (2-3 sides)
int32 NumSides = FMath::RandRange(2, 3);
float BaseAngle = FMath::RandRange(0.0f, 2.0f * PI);
@@ -302,7 +308,6 @@ void ASpaceShooterGameMode::SpawnEnemyFlanking()
{
// Calculate angle for this side
float Angle = BaseAngle + (Side * (2.0f * PI / NumSides));
FVector2D Direction(FMath::Cos(Angle), FMath::Sin(Angle));
// Spawn 1-2 enemies from this side
int32 NumEnemies = FMath::RandRange(1, 2);
@@ -311,13 +316,10 @@ void ASpaceShooterGameMode::SpawnEnemyFlanking()
{
// Add some variation to the spawn position
float OffsetAngle = Angle + FMath::RandRange(-0.3f, 0.3f);
FVector2D OffsetDir(FMath::Cos(OffsetAngle), FMath::Sin(OffsetAngle));
FVector Direction(FMath::Cos(OffsetAngle), FMath::Sin(OffsetAngle), 0.0f);
// Increased from 2000 to MinimumSpawnDistance
FVector ProposedLocation = PlayerLocation + FVector(OffsetDir.X, OffsetDir.Y, 0) * MinimumSpawnDistance;
// Ensure the spawn location is far enough from the player
FVector SpawnLocation = EnsureMinimumSpawnDistance(ProposedLocation, PlayerLocation);
// Calculate spawn position
FVector SpawnLocation = PlayerLocation + (Direction * (MinimumSpawnDistance + FMath::RandRange(0.0f, 300.0f)));
FRotator SpawnRotation = FRotator::ZeroRotator;
FActorSpawnParameters SpawnParams;
@@ -334,107 +336,28 @@ void ASpaceShooterGameMode::SpawnEnemyFlanking()
}
}
// Return to random spawning
CurrentPattern = ESpawnPattern::Random;
}
FVector ASpaceShooterGameMode::GetScreenEdgeSpawnLocation()
FVector ASpaceShooterGameMode::GetRandomSpawnLocation()
{
FVector PlayerLocation = GetPlayerLocation();
TArray<FVector2D> ScreenBounds = GetScreenBounds();
// Decide which edge to spawn from (0 = top, 1 = right, 2 = bottom, 3 = left)
int32 Edge = FMath::RandRange(0, 3);
FVector SpawnLocation;
float RandomPos;
// Increased margin to spawn farther from screen edges
float ExtendedMargin = ScreenSpawnMargin + 500.0f;
switch (Edge)
{
case 0: // Top edge
RandomPos = FMath::RandRange(ScreenBounds[0].X, ScreenBounds[1].X);
SpawnLocation = FVector(RandomPos, ScreenBounds[0].Y - ExtendedMargin, PlayerLocation.Z);
break;
case 1: // Right edge
RandomPos = FMath::RandRange(ScreenBounds[0].Y, ScreenBounds[1].Y);
SpawnLocation = FVector(ScreenBounds[1].X + ExtendedMargin, RandomPos, PlayerLocation.Z);
break;
case 2: // Bottom edge
RandomPos = FMath::RandRange(ScreenBounds[0].X, ScreenBounds[1].X);
SpawnLocation = FVector(RandomPos, ScreenBounds[1].Y + ExtendedMargin, PlayerLocation.Z);
break;
case 3: // Left edge
RandomPos = FMath::RandRange(ScreenBounds[0].Y, ScreenBounds[1].Y);
SpawnLocation = FVector(ScreenBounds[0].X - ExtendedMargin, RandomPos, PlayerLocation.Z);
break;
}
// Ensure the spawn location is far enough from the player
return EnsureMinimumSpawnDistance(SpawnLocation, PlayerLocation);
}
FVector ASpaceShooterGameMode::GetSpawnZoneLocation()
{
// If no spawn zones are defined, return a screen edge location
if (SpawnZones.Num() == 0)
{
return GetScreenEdgeSpawnLocation();
}
// Filter active spawn zones
TArray<FSpawnZone> ActiveZones;
float TotalWeight = 0.0f;
for (const FSpawnZone& Zone : SpawnZones)
{
if (Zone.bActive)
{
ActiveZones.Add(Zone);
TotalWeight += Zone.SpawnWeight;
}
}
// If no active zones, return screen edge
if (ActiveZones.Num() == 0)
{
return GetScreenEdgeSpawnLocation();
}
// Select a zone based on weight
float RandomWeight = FMath::RandRange(0.0f, TotalWeight);
float WeightSum = 0.0f;
FVector PlayerLocation = GetPlayerLocation();
for (const FSpawnZone& Zone : ActiveZones)
{
WeightSum += Zone.SpawnWeight;
if (RandomWeight <= WeightSum)
{
// Generate random point within this zone's radius
// Generate a random angle in radians
float RandomAngle = FMath::RandRange(0.0f, 2.0f * PI);
float RandomRadius = FMath::RandRange(0.0f, Zone.Radius);
FVector SpawnOffset(
FMath::Cos(RandomAngle) * RandomRadius,
FMath::Sin(RandomAngle) * RandomRadius,
// Create a direction vector from the random angle
FVector Direction(
FMath::Cos(RandomAngle),
FMath::Sin(RandomAngle),
0.0f
);
FVector ProposedLocation = Zone.Location + SpawnOffset;
// Use the minimum spawn distance plus some random additional distance
float SpawnDistance = MinimumSpawnDistance + FMath::RandRange(0.0f, 500.0f);
// Ensure the spawn location is far enough from the player
return EnsureMinimumSpawnDistance(ProposedLocation, PlayerLocation);
}
}
// Fallback
return GetScreenEdgeSpawnLocation();
// Calculate the spawn position
return PlayerLocation + (Direction * SpawnDistance);
}
void ASpaceShooterGameMode::UpdateDifficulty()
@@ -465,48 +388,6 @@ FVector ASpaceShooterGameMode::GetPlayerLocation()
return FVector::ZeroVector;
}
TArray<FVector2D> ASpaceShooterGameMode::GetScreenBounds()
{
TArray<FVector2D> Bounds;
FVector2D ScreenMin, ScreenMax;
// Get player controller for screen info
APlayerController* PlayerController = UGameplayStatics::GetPlayerController(GetWorld(), 0);
if (!PlayerController)
{
// Fallback values if no controller
Bounds.Add(FVector2D(-2000, -2000));
Bounds.Add(FVector2D(2000, 2000));
return Bounds;
}
// Get viewport size
int32 ViewportSizeX, ViewportSizeY;
PlayerController->GetViewportSize(ViewportSizeX, ViewportSizeY);
// Get world location of screen corners
FVector WorldLocation, WorldDirection;
// Top-Left corner
PlayerController->DeprojectScreenPositionToWorld(0, 0, WorldLocation, WorldDirection);
ScreenMin = FVector2D(WorldLocation.X, WorldLocation.Y);
// Bottom-Right corner
PlayerController->DeprojectScreenPositionToWorld(ViewportSizeX, ViewportSizeY, WorldLocation, WorldDirection);
ScreenMax = FVector2D(WorldLocation.X, WorldLocation.Y);
// Add some margin
ScreenMin.X -= 200;
ScreenMin.Y -= 200;
ScreenMax.X += 200;
ScreenMax.Y += 200;
Bounds.Add(ScreenMin);
Bounds.Add(ScreenMax);
return Bounds;
}
void ASpaceShooterGameMode::RotateTowardsPlayer(AEnemySpaceship* Enemy, const FVector& PlayerLocation)
{
if (!Enemy)
@@ -541,3 +422,47 @@ FVector ASpaceShooterGameMode::EnsureMinimumSpawnDistance(const FVector& Propose
Direction.Normalize();
return PlayerLocation + Direction * MinimumSpawnDistance;
}
void ASpaceShooterGameMode::UpdateGameTimer()
{
if (bIsGameOver) return;
RemainingTime -= 1.0f;
if (RemainingTime <= 0.0f)
{
EndGame();
}
}
void ASpaceShooterGameMode::EndGame()
{
bIsGameOver = true;
// Stop enemy spawning
GetWorldTimerManager().ClearTimer(EnemySpawnTimer);
// Clear existing enemies
TArray<AActor*> FoundEnemies;
UGameplayStatics::GetAllActorsOfClass(GetWorld(), AEnemySpaceship::StaticClass(), FoundEnemies);
for (AActor* Enemy : FoundEnemies)
{
Enemy->Destroy();
}
// Pause the game
UGameplayStatics::SetGamePaused(GetWorld(), true);
}
void ASpaceShooterGameMode::IncrementKillCount()
{
if (!bIsGameOver)
{
KillCount++;
}
}
void ASpaceShooterGameMode::RestartGame()
{
UGameplayStatics::OpenLevel(this, FName(*GetWorld()->GetName()), false);
}

View File

@@ -44,6 +44,13 @@ public:
virtual void StartPlay() override;
virtual void Tick(float DeltaTime) override;
void IncrementKillCount();
void RestartGame();
int32 GetKillCount() const { return KillCount; }
float GetRemainingTime() const { return RemainingTime; }
float GetGameDuration() const { return GameDuration; }
bool IsGameOver() const { return bIsGameOver; }
protected:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spawning")
TSubclassOf<class AEnemySpaceship> EnemyClass;
@@ -78,6 +85,18 @@ protected:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spawning|Difficulty")
int32 DifficultyInterval = 30;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Game Rules")
float GameDuration = 30.0f; // 3 minutes default
UPROPERTY(BlueprintReadOnly, Category = "Game Stats")
int32 KillCount;
UPROPERTY(BlueprintReadOnly, Category = "Game Stats")
float RemainingTime;
UPROPERTY(BlueprintReadOnly, Category = "Game State")
bool bIsGameOver;
private:
FTimerHandle EnemySpawnTimer;
FTimerHandle DifficultyTimer;
@@ -90,13 +109,15 @@ private:
void SpawnEnemyWave();
void SpawnEnemyFormation();
void SpawnEnemyFlanking();
FVector GetScreenEdgeSpawnLocation();
FVector GetSpawnZoneLocation();
FVector GetRandomSpawnLocation();
void UpdateDifficulty();
FVector GetPlayerLocation();
TArray<FVector2D> GetScreenBounds();
void RotateTowardsPlayer(AEnemySpaceship* Enemy, const FVector& PlayerLocation);
// New helper method to ensure minimum spawn distance
FVector EnsureMinimumSpawnDistance(const FVector& ProposedLocation, const FVector& PlayerLocation);
void EndGame();
void UpdateGameTimer();
FTimerHandle GameTimerHandle;
};

View File

@@ -0,0 +1,280 @@
#include "SpaceShooterHUD.h"
#include "SpaceShooterGameMode.h"
#include "Engine/Canvas.h"
#include "Engine/Font.h"
#include "SpaceshipPawn.h"
#include "UObject/ConstructorHelpers.h"
ASpaceShooterHUD::ASpaceShooterHUD()
{
// Find and set the default font
static ConstructorHelpers::FObjectFinder<UFont> FontObj(TEXT("/Engine/EngineFonts/RobotoDistanceField"));
TextFont = FontObj.Object;
GameOverStartTime = 0.0f;
}
ASpaceshipPawn* ASpaceShooterHUD::GetPlayerPawn() const
{
APlayerController* PC = GetWorld()->GetFirstPlayerController();
if (PC)
{
return Cast<ASpaceshipPawn>(PC->GetPawn());
}
return nullptr;
}
// Add this new function for drawing the bars
void ASpaceShooterHUD::DrawStatusBars()
{
if (!Canvas) return;
ASpaceshipPawn* PlayerPawn = GetPlayerPawn();
if (!PlayerPawn) return;
// Get the canvas size
const float ScreenWidth = Canvas->SizeX;
const float ScreenHeight = Canvas->SizeY;
// Calculate positions for the bars (top right corner)
float StartX = ScreenWidth - BarWidth - BarPadding;
float HealthY = BarPadding;
float ShieldY = HealthY + BarHeight + BarPadding;
// Get current health and shield values
float HealthPercent = PlayerPawn->GetCurrentHealth() / PlayerPawn->GetMaxHealth();
float ShieldPercent = PlayerPawn->GetCurrentShield() / PlayerPawn->GetMaxShield();
// Clamp values between 0 and 1
HealthPercent = FMath::Clamp(HealthPercent, 0.0f, 1.0f);
ShieldPercent = FMath::Clamp(ShieldPercent, 0.0f, 1.0f);
// Draw health bar background
FCanvasBoxItem HealthBG(FVector2D(StartX, HealthY), FVector2D(BarWidth, BarHeight));
HealthBG.SetColor(BackgroundBarColor);
Canvas->DrawItem(HealthBG);
// Draw health bar fill
FCanvasBoxItem HealthFill(FVector2D(StartX, HealthY), FVector2D(BarWidth * HealthPercent, BarHeight));
HealthFill.SetColor(HealthBarColor);
Canvas->DrawItem(HealthFill);
// Draw shield bar background
FCanvasBoxItem ShieldBG(FVector2D(StartX, ShieldY), FVector2D(BarWidth, BarHeight));
ShieldBG.SetColor(BackgroundBarColor);
Canvas->DrawItem(ShieldBG);
// Draw shield bar fill
FCanvasBoxItem ShieldFill(FVector2D(StartX, ShieldY), FVector2D(BarWidth * ShieldPercent, BarHeight));
ShieldFill.SetColor(ShieldBarColor);
Canvas->DrawItem(ShieldFill);
// Draw text labels
FString HealthText = FString::Printf(TEXT("Health: %.0f%%"), HealthPercent * 100);
FString ShieldText = FString::Printf(TEXT("Shield: %.0f%%"), ShieldPercent * 100);
// Draw text over the bars
DrawText(HealthText, FLinearColor::White, StartX + 5, HealthY + 2, TextFont, 0.8f);
DrawText(ShieldText, FLinearColor::White, StartX + 5, ShieldY + 2, TextFont, 0.8f);
}
void ASpaceShooterHUD::DrawHUD()
{
Super::DrawHUD();
ASpaceShooterGameMode* GameMode = GetGameMode();
if (!GameMode) return;
APlayerController* PC = GetWorld()->GetFirstPlayerController();
bool bIsPaused = PC && PC->IsPaused();
if (!GameMode->IsGameOver() && bIsPaused)
{
DrawPauseScreen();
}
if (!GameMode->IsGameOver() && !bIsPaused)
{
DrawGameplayHUD();
}
if (GameMode->IsGameOver())
{
DrawGameOverScreen();
}
}
void ASpaceShooterHUD::DrawGameplayHUD()
{
ASpaceShooterGameMode* GameMode = GetGameMode();
if (!GameMode) return;
// Draw timer
int32 Minutes = FMath::FloorToInt(GameMode->GetRemainingTime() / 60.0f);
int32 Seconds = FMath::FloorToInt(FMath::Fmod(GameMode->GetRemainingTime(), 60.0f));
FString TimeString = FString::Printf(TEXT("Time: %02d:%02d"), Minutes, Seconds);
DrawText(TimeString, FLinearColor::White, 50, 50, TextFont);
// Draw kill count
FString KillString = FString::Printf(TEXT("Kills: %d"), GameMode->GetKillCount());
DrawText(KillString, FLinearColor::White, 50, 100, TextFont);
// Draw status bars
DrawStatusBars();
}
void ASpaceShooterHUD::DrawPauseScreen()
{
if (!Canvas) return;
// Draw semi-transparent dark overlay (this creates a "blur-like" effect)
FCanvasBoxItem BoxItem(FVector2D(0, 0), FVector2D(Canvas->SizeX, Canvas->SizeY));
BoxItem.SetColor(FLinearColor(0.0f, 0.0f, 0.0f, 0.75f)); // More opaque for better readability
Canvas->DrawItem(BoxItem);
// Draw "GAME PAUSED" text
FString PauseText = TEXT("GAME PAUSED");
float TextWidth, TextHeight;
GetTextSize(PauseText, TextWidth, TextHeight, TextFont, PauseTextScale);
// Center the text
float PosX = (Canvas->SizeX - TextWidth) * 0.5f;
float PosY = (Canvas->SizeY - TextHeight) * 0.5f;
// Draw text shadow for better visibility
DrawText(PauseText,
FLinearColor(0.0f, 0.0f, 0.0f, 1.0f),
PosX + 2.0f,
PosY + 2.0f,
TextFont,
PauseTextScale);
// Draw main text
DrawText(PauseText,
FLinearColor(1.0f, 1.0f, 1.0f, 1.0f),
PosX,
PosY,
TextFont,
PauseTextScale);
// Draw "Press ESC to Resume" text
FString ResumeText = TEXT("Press ESC to Resume");
GetTextSize(ResumeText, TextWidth, TextHeight, TextFont);
DrawText(ResumeText,
FLinearColor(1.0f, 1.0f, 1.0f, 0.8f),
(Canvas->SizeX - TextWidth) * 0.5f,
PosY + TextHeight * 3,
TextFont);
}
void ASpaceShooterHUD::DrawGameOverBackground()
{
if (!Canvas) return;
// Draw a semi-transparent black background
FCanvasBoxItem BoxItem(FVector2D(0, 0), FVector2D(Canvas->SizeX, Canvas->SizeY));
BoxItem.SetColor(FLinearColor(0.0f, 0.0f, 0.0f, 0.7f));
Canvas->DrawItem(BoxItem);
}
void ASpaceShooterHUD::DrawGameOverText(const FString& Text, const FVector2D& Position, const FLinearColor& Color, float Scale)
{
if (!Canvas || !TextFont) return;
float TextWidth, TextHeight;
GetTextSize(Text, TextWidth, TextHeight, TextFont, Scale);
// Draw text shadow
DrawText(Text, FLinearColor(0.0f, 0.0f, 0.0f, Color.A * 0.5f),
Position.X + 3.0f, Position.Y + 3.0f, TextFont, Scale);
// Draw main text
DrawText(Text, Color, Position.X, Position.Y, TextFont, Scale);
}
void ASpaceShooterHUD::DrawGameOverScreen()
{
ASpaceShooterGameMode* GameMode = GetGameMode();
if (!GameMode || !Canvas) return;
// Initialize GameOverStartTime if needed
if (GameOverStartTime == 0.0f)
{
GameOverStartTime = GetWorld()->GetTimeSeconds();
}
// Draw darkened background
DrawGameOverBackground();
const FVector2D ViewportSize(Canvas->SizeX, Canvas->SizeY);
const float CurrentTime = GetWorld()->GetTimeSeconds();
const float TimeSinceGameOver = CurrentTime - GameOverStartTime;
// Calculate pulsating alpha for "GAME OVER" text
float PulsatingAlpha = FMath::Lerp(PulsateMinAlpha, PulsateMaxAlpha,
(FMath::Sin(TimeSinceGameOver * PulsateSpeed) + 1.0f) * 0.5f);
// Calculate vertical positions
float CenterY = ViewportSize.Y * 0.4f; // Slightly above center
float Spacing = 60.0f * GameOverScale;
// Draw "GAME OVER" with pulsating effect
FLinearColor PulsatingColor = GameOverTextColor;
PulsatingColor.A = PulsatingAlpha;
FString GameOverText = TEXT("GAME OVER");
float TextWidth, TextHeight;
GetTextSize(GameOverText, TextWidth, TextHeight, TextFont, GameOverScale);
DrawGameOverText(GameOverText,
FVector2D((ViewportSize.X - TextWidth) * 0.5f, CenterY),
PulsatingColor,
GameOverScale);
// Draw stats with scaling animations
const float StatsScale = 1.5f;
float StatsY = CenterY + Spacing;
// Time Survived
int32 TotalSeconds = FMath::RoundToInt(GameMode->GetGameDuration() - GameMode->GetRemainingTime());
int32 Minutes = TotalSeconds / 60;
int32 Seconds = TotalSeconds % 60;
FString TimeText = FString::Printf(TEXT("Time Survived: %02d:%02d"), Minutes, Seconds);
GetTextSize(TimeText, TextWidth, TextHeight, TextFont, StatsScale);
DrawGameOverText(TimeText,
FVector2D((ViewportSize.X - TextWidth) * 0.5f, StatsY),
StatsTextColor,
StatsScale);
// Kill Count
StatsY += Spacing * 0.7f;
FString KillText = FString::Printf(TEXT("Enemies Destroyed: %d"), GameMode->GetKillCount());
GetTextSize(KillText, TextWidth, TextHeight, TextFont, StatsScale);
DrawGameOverText(KillText,
FVector2D((ViewportSize.X - TextWidth) * 0.5f, StatsY),
StatsTextColor,
StatsScale);
// Calculate final score based on time and kills
int32 FinalScore = GameMode->GetKillCount() * 100 + TotalSeconds * 10;
StatsY += Spacing * 0.7f;
FString ScoreText = FString::Printf(TEXT("Final Score: %d"), FinalScore);
GetTextSize(ScoreText, TextWidth, TextHeight, TextFont, StatsScale);
DrawGameOverText(ScoreText,
FVector2D((ViewportSize.X - TextWidth) * 0.5f, StatsY),
StatsTextColor,
StatsScale);
// Draw restart text with bounce effect
float BounceOffset = FMath::Sin(TimeSinceGameOver * 3.0f) * 5.0f;
StatsY += Spacing * 1.2f;
FString RestartText = TEXT("Press R to Restart");
GetTextSize(RestartText, TextWidth, TextHeight, TextFont, StatsScale);
DrawGameOverText(RestartText,
FVector2D((ViewportSize.X - TextWidth) * 0.5f, StatsY + BounceOffset),
RestartTextColor,
StatsScale);
}
ASpaceShooterGameMode* ASpaceShooterHUD::GetGameMode() const
{
return Cast<ASpaceShooterGameMode>(GetWorld()->GetAuthGameMode());
}

View File

@@ -0,0 +1,76 @@
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/HUD.h"
#include "SpaceShooterHUD.generated.h"
UCLASS()
class MYPROJECT3_API ASpaceShooterHUD : public AHUD
{
GENERATED_BODY()
public:
ASpaceShooterHUD();
virtual void DrawHUD() override;
protected:
UPROPERTY()
UFont* TextFont;
// Bar configuration
UPROPERTY(EditAnywhere, Category = "HUD|Bars")
float BarWidth = 200.0f;
UPROPERTY(EditAnywhere, Category = "HUD|Bars")
float BarHeight = 20.0f;
UPROPERTY(EditAnywhere, Category = "HUD|Bars")
float BarPadding = 10.0f;
UPROPERTY(EditAnywhere, Category = "HUD|Bars")
FLinearColor HealthBarColor = FLinearColor(1.0f, 0.0f, 0.0f, 1.0f); // Red
UPROPERTY(EditAnywhere, Category = "HUD|Bars")
FLinearColor ShieldBarColor = FLinearColor(0.0f, 0.5f, 1.0f, 1.0f); // Light Blue
UPROPERTY(EditAnywhere, Category = "HUD|Bars")
FLinearColor BackgroundBarColor = FLinearColor(0.0f, 0.0f, 0.0f, 0.5f); // Semi-transparent black
// Game Over Screen Configuration
UPROPERTY(EditAnywhere, Category = "HUD|GameOver")
float GameOverScale = 3.0f;
UPROPERTY(EditAnywhere, Category = "HUD|GameOver")
FLinearColor GameOverTextColor = FLinearColor(1.0f, 0.2f, 0.2f, 1.0f); // Bright red
UPROPERTY(EditAnywhere, Category = "HUD|GameOver")
FLinearColor StatsTextColor = FLinearColor(0.9f, 0.9f, 0.9f, 1.0f); // Off-white
UPROPERTY(EditAnywhere, Category = "HUD|GameOver")
FLinearColor RestartTextColor = FLinearColor(1.0f, 0.843f, 0.0f, 1.0f); // Gold
UPROPERTY(EditAnywhere, Category = "HUD|GameOver")
float PulsateSpeed = 2.0f;
UPROPERTY(EditAnywhere, Category = "HUD|GameOver")
float PulsateMinAlpha = 0.7f;
UPROPERTY(EditAnywhere, Category = "HUD|GameOver")
float PulsateMaxAlpha = 1.0f;
UPROPERTY(EditAnywhere, Category = "HUD|Pause")
float PauseTextScale = 2.0f;
private:
void DrawGameplayHUD();
void DrawGameOverScreen();
void DrawStatusBars();
class ASpaceShooterGameMode* GetGameMode() const;
class ASpaceshipPawn* GetPlayerPawn() const;
void DrawPauseScreen();
float GameOverStartTime;
void DrawGameOverBackground();
void DrawGameOverText(const FString& Text, const FVector2D& Position, const FLinearColor& Color, float Scale = 1.0f);
};

View File

@@ -111,6 +111,9 @@ void ASpaceshipPawn::BeginPlay()
CurrentHealth = MaxHealth;
CurrentShield = MaxShield;
LastDamageTime = 0.0f;
// Initialize the dynamic material
InitializeDynamicMaterial();
}
void ASpaceshipPawn::Tick(float DeltaTime)
@@ -486,17 +489,36 @@ float ASpaceshipPawn::TakeDamage(float DamageAmount, FDamageEvent const& DamageE
return DamageToApply;
}
void ASpaceshipPawn::InitializeDynamicMaterial()
{
if (ShipMesh)
{
// Store the original material
OriginalMaterial = ShipMesh->GetMaterial(0);
if (OriginalMaterial)
{
// Create dynamic material instance only once
DynamicMaterialInstance = UMaterialInstanceDynamic::Create(OriginalMaterial, this);
if (DynamicMaterialInstance)
{
// Apply the dynamic material instance to the mesh
ShipMesh->SetMaterial(0, DynamicMaterialInstance);
// Initialize flash color to transparent
DynamicMaterialInstance->SetVectorParameterValue("FlashColor", FLinearColor(0.0f, 0.0f, 0.0f, 0.0f));
}
}
}
}
void ASpaceshipPawn::ApplyDamageFlash()
{
// Apply material flash effect to indicate damage
if (ShipMesh && ShipMesh->GetMaterial(0))
// Only set the flash color if we have a valid dynamic material instance
if (DynamicMaterialInstance)
{
UMaterialInstanceDynamic* DynamicMaterial = UMaterialInstanceDynamic::Create(ShipMesh->GetMaterial(0), this);
if (DynamicMaterial)
{
// Assuming the material has a parameter named "FlashColor"
DynamicMaterial->SetVectorParameterValue("FlashColor", DamageFlashColor);
ShipMesh->SetMaterial(0, DynamicMaterial);
// Set the flash color
DynamicMaterialInstance->SetVectorParameterValue("FlashColor", DamageFlashColor);
// Set timer to reset the flash
GetWorldTimerManager().SetTimer(
@@ -508,19 +530,13 @@ void ASpaceshipPawn::ApplyDamageFlash()
);
}
}
}
void ASpaceshipPawn::ResetDamageFlash()
{
// Reset material flash effect
if (ShipMesh && ShipMesh->GetMaterial(0))
// Reset the flash color only if we have a valid dynamic material instance
if (DynamicMaterialInstance)
{
UMaterialInstanceDynamic* DynamicMaterial = Cast<UMaterialInstanceDynamic>(ShipMesh->GetMaterial(0));
if (DynamicMaterial)
{
// Reset the flash color (assuming transparent black means no flash)
DynamicMaterial->SetVectorParameterValue("FlashColor", FLinearColor(0.0f, 0.0f, 0.0f, 0.0f));
}
DynamicMaterialInstance->SetVectorParameterValue("FlashColor", FLinearColor(0.0f, 0.0f, 0.0f, 0.0f));
}
}
@@ -586,10 +602,6 @@ void ASpaceshipPawn::Die()
DisableInput(PC);
}
// You could either restart the level after a delay or show a game over screen here
// For example:
// GetWorldTimerManager().SetTimer(RestartTimerHandle, this, &ASpaceshipPawn::RestartLevel, 3.0f, false);
// For now, just log the death
if (GEngine)
{

View File

@@ -37,6 +37,18 @@ public:
UFUNCTION(BlueprintCallable, Category = "Combat")
float GetShieldPercentage() const { return CurrentShield / MaxShield; }
UFUNCTION(BlueprintCallable, Category = "Stats")
float GetCurrentHealth() const { return CurrentHealth; }
UFUNCTION(BlueprintCallable, Category = "Stats")
float GetMaxHealth() const { return MaxHealth; }
UFUNCTION(BlueprintCallable, Category = "Stats")
float GetCurrentShield() const { return CurrentShield; }
UFUNCTION(BlueprintCallable, Category = "Stats")
float GetMaxShield() const { return MaxShield; }
protected:
virtual void BeginPlay() override;
@@ -128,17 +140,17 @@ protected:
void HandleMouseLook(const FInputActionValue& Value);
// Health properties
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat")
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat|Stats")
float MaxHealth = 100.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat")
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat|Stats")
float CurrentHealth = 100.0f;
// Shield properties
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat")
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat|Stats")
float MaxShield = 100.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat")
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat|Stats")
float CurrentShield = 100.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat")
@@ -164,6 +176,14 @@ protected:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Effects")
FLinearColor DamageFlashColor = FLinearColor(1.0f, 0.0f, 0.0f, 0.5f);
// Store the original material
UPROPERTY()
UMaterialInterface* OriginalMaterial;
// Store the dynamic material instance
UPROPERTY()
UMaterialInstanceDynamic* DynamicMaterialInstance;
private:
// Movement state
float CurrentThrottleInput;
@@ -201,6 +221,9 @@ private:
FTimerHandle DamageFlashTimerHandle;
float LastDamageTime;
// Initialize dynamic material
void InitializeDynamicMaterial();
void StartShieldRecharge();
void RechargeShield();
bool IsDead() const { return CurrentHealth <= 0.0f; }

View File

@@ -0,0 +1,61 @@
#include "SpaceshipPlayerController.h"
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"
#include "Components/PostProcessComponent.h"
#include "Materials/MaterialInstanceDynamic.h"
#include "SpaceShooterGameMode.h"
ASpaceshipPlayerController::ASpaceshipPlayerController()
{
// Don't automatically show mouse cursor
bShowMouseCursor = false;
}
void ASpaceshipPlayerController::BeginPlay()
{
Super::BeginPlay();
// Add Input Mapping Context
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(GetLocalPlayer()))
{
if (DefaultMappingContext)
{
Subsystem->AddMappingContext(DefaultMappingContext, 0);
}
}
}
void ASpaceshipPlayerController::SetupInputComponent()
{
Super::SetupInputComponent();
if (UEnhancedInputComponent* EnhancedInputComponent = CastChecked<UEnhancedInputComponent>(InputComponent))
{
// Bind the pause action
if (PauseAction)
{
EnhancedInputComponent->BindAction(PauseAction, ETriggerEvent::Started,
this, &ASpaceshipPlayerController::HandlePauseAction);
}
}
}
void ASpaceshipPlayerController::HandlePauseAction()
{
// Don't allow pausing if game is over
if (ASpaceShooterGameMode* GameMode = Cast<ASpaceShooterGameMode>(GetWorld()->GetAuthGameMode()))
{
if (GameMode->IsGameOver())
{
return;
}
}
bool bIsPaused = IsPaused();
SetPause(!bIsPaused);
// Show cursor only when paused, hide when resuming
bShowMouseCursor = !bIsPaused;
// To fix resume not working, don't change input mode from game only to UI only since in UI mode the key mapping for pause won't work
}

View File

@@ -0,0 +1,33 @@
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "SpaceshipPlayerController.generated.h"
UCLASS()
class MYPROJECT3_API ASpaceshipPlayerController : public APlayerController
{
GENERATED_BODY()
public:
ASpaceshipPlayerController();
protected:
virtual void BeginPlay() override;
virtual void SetupInputComponent() override;
// Input Action for pausing
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
class UInputAction* PauseAction;
// Input Mapping Context
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
class UInputMappingContext* DefaultMappingContext;
private:
// Post process component for blur effect
UPROPERTY()
class UPostProcessComponent* BlurPostProcess;
void HandlePauseAction();
};