9 Commits

25 changed files with 1162 additions and 351 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.

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", "Name": "MyProject3",
"Type": "Runtime", "Type": "Runtime",
"LoadingPhase": "Default" "LoadingPhase": "Default",
"AdditionalDependencies": [
"Engine"
]
} }
], ],
"Plugins": [ "Plugins": [

View File

@@ -6,8 +6,7 @@
#include "DrawDebugHelpers.h" #include "DrawDebugHelpers.h"
#include "Math/UnrealMathUtility.h" #include "Math/UnrealMathUtility.h"
#include "EnemyProjectile.h" #include "EnemyProjectile.h"
#include "SpaceShooterGameMode.h"
// Include SpaceshipPawn to access player-specific functionality
#include "SpaceshipPawn.h" #include "SpaceshipPawn.h"
AEnemySpaceship::AEnemySpaceship() AEnemySpaceship::AEnemySpaceship()
@@ -34,6 +33,10 @@ AEnemySpaceship::AEnemySpaceship()
EnemyMesh->SetCollisionResponseToChannel(ECC_Pawn, ECR_Ignore); // Ignore other pawns EnemyMesh->SetCollisionResponseToChannel(ECC_Pawn, ECR_Ignore); // Ignore other pawns
EnemyMesh->SetCollisionResponseToChannel(ECC_GameTraceChannel1, ECR_Block); // Block player (for damage) EnemyMesh->SetCollisionResponseToChannel(ECC_GameTraceChannel1, ECR_Block); // Block player (for damage)
EnemyMesh->SetGenerateOverlapEvents(true); EnemyMesh->SetGenerateOverlapEvents(true);
CurrentVelocity = FVector::ZeroVector;
TargetVelocity = FVector::ZeroVector;
LastPosition = FVector::ZeroVector;
} }
void AEnemySpaceship::BeginPlay() void AEnemySpaceship::BeginPlay()
@@ -52,20 +55,26 @@ void AEnemySpaceship::BeginPlay()
// Randomize initial strafe direction // Randomize initial strafe direction
StrafeDirection = FMath::RandBool() ? 1.0f : -1.0f; 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) void AEnemySpaceship::Tick(float DeltaTime)
{ {
Super::Tick(DeltaTime); Super::Tick(DeltaTime);
if (!PlayerPawn) if (!PlayerPawn)
{ {
// Try to find player again if not set
AActor* FoundPlayerPawn = UGameplayStatics::GetPlayerPawn(GetWorld(), 0); AActor* FoundPlayerPawn = UGameplayStatics::GetPlayerPawn(GetWorld(), 0);
PlayerPawn = Cast<ASpaceshipPawn>(FoundPlayerPawn); PlayerPawn = Cast<ASpaceshipPawn>(FoundPlayerPawn);
if (!PlayerPawn) return;
if (!PlayerPawn)
return;
} }
// Calculate distance to player // Calculate distance to player
@@ -89,6 +98,9 @@ void AEnemySpaceship::Tick(float DeltaTime)
case EEnemyBehaviorState::Strafe: case EEnemyBehaviorState::Strafe:
PerformStrafe(DeltaTime); PerformStrafe(DeltaTime);
break; break;
case EEnemyBehaviorState::Flank:
PerformFlank(DeltaTime);
break;
} }
// Debug state information // Debug state information
@@ -101,6 +113,7 @@ void AEnemySpaceship::Tick(float DeltaTime)
case EEnemyBehaviorState::Attack: StateString = "Attack"; break; case EEnemyBehaviorState::Attack: StateString = "Attack"; break;
case EEnemyBehaviorState::Retreat: StateString = "Retreat"; break; case EEnemyBehaviorState::Retreat: StateString = "Retreat"; break;
case EEnemyBehaviorState::Strafe: StateString = "Strafe"; break; case EEnemyBehaviorState::Strafe: StateString = "Strafe"; break;
case EEnemyBehaviorState::Flank: StateString = "Flank"; break;
} }
GEngine->AddOnScreenDebugMessage(-1, 0.0f, FColor::Orange, 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() 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) if (DistanceToPlayer < MinDistanceToPlayer)
{ {
// Too close, retreat or strafe if (AggressionRoll < AggressionFactor * 0.3f)
CurrentBehaviorState = FMath::RandBool() ? EEnemyBehaviorState::Retreat : EEnemyBehaviorState::Strafe; {
CurrentBehaviorState = EEnemyBehaviorState::Attack;
}
else
{
CurrentBehaviorState = EEnemyBehaviorState::Retreat;
}
} }
else if (DistanceToPlayer < AttackRange) else if (DistanceToPlayer < AttackRange)
{ {
// Within attack range but not too close, either attack or strafe // Increased chance of flanking
if (CurrentBehaviorState != EEnemyBehaviorState::Attack && if (AggressionRoll < AggressionFactor * FlankingFrequency)
CurrentBehaviorState != EEnemyBehaviorState::Strafe)
{ {
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 else
{ {
// Too far, chase
CurrentBehaviorState = EEnemyBehaviorState::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() void AEnemySpaceship::ChangeBehaviorState()
{ {
// Random chance to change behavior state // Random chance to change behavior state
@@ -154,18 +350,21 @@ void AEnemySpaceship::PerformChase(float DeltaTime)
if (PlayerPawn) if (PlayerPawn)
{ {
// Calculate direction to player // Calculate direction to player
FVector Direction = (PlayerPawn->GetActorLocation() - GetActorLocation()).GetSafeNormal(); FVector DirectionToPlayer = (PlayerPawn->GetActorLocation() - GetActorLocation()).GetSafeNormal();
// Move towards the player // Calculate target position
FVector NewLocation = GetActorLocation() + Direction * MovementSpeed * DeltaTime; FVector TargetPosition = GetActorLocation() + DirectionToPlayer * MovementSpeed * DeltaTime;
SetActorLocation(NewLocation);
// Face towards the player // Use smooth movement
FRotator NewRotation = Direction.Rotation(); SmoothMove(TargetPosition, DeltaTime);
// Smoothly rotate to face player
FRotator TargetRotation = DirectionToPlayer.Rotation();
FRotator NewRotation = FMath::RInterpTo(GetActorRotation(), TargetRotation, DeltaTime, InterpSpeed);
SetActorRotation(NewRotation); SetActorRotation(NewRotation);
// If within attack range, fire occasionally // Fire if within range
if (DistanceToPlayer < AttackRange && bCanFire && FMath::FRand() < 0.3f) if (DistanceToPlayer < AttackRange && bCanFire)
{ {
Fire(); Fire();
} }
@@ -193,24 +392,32 @@ void AEnemySpaceship::PerformRetreat(float DeltaTime)
{ {
if (PlayerPawn) if (PlayerPawn)
{ {
// Move away from player // Calculate ideal retreat position
FVector Direction = (GetActorLocation() - PlayerPawn->GetActorLocation()).GetSafeNormal(); FVector DirectionFromPlayer = (GetActorLocation() - PlayerPawn->GetActorLocation()).GetSafeNormal();
FVector NewLocation = GetActorLocation() + Direction * MovementSpeed * DeltaTime; FVector DesiredPosition = PlayerPawn->GetActorLocation() + (DirectionFromPlayer * OptimalCombatDistance);
SetActorLocation(NewLocation);
// Keep facing the player while backing up // Consider other enemies
FVector FaceDirection = (PlayerPawn->GetActorLocation() - GetActorLocation()).GetSafeNormal(); FVector AvoidCrowdingPosition = CalculatePositionAwayFromOtherEnemies();
FRotator NewRotation = FaceDirection.Rotation(); 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); SetActorRotation(NewRotation);
// Fire while retreating // Fire while retreating if in range
if (bCanFire) if (bCanFire && DistanceToPlayer < AttackRange)
{ {
Fire(); Fire();
} }
} }
} }
void AEnemySpaceship::PerformStrafe(float DeltaTime) void AEnemySpaceship::PerformStrafe(float DeltaTime)
{ {
if (PlayerPawn) if (PlayerPawn)
@@ -218,19 +425,44 @@ void AEnemySpaceship::PerformStrafe(float DeltaTime)
// Calculate direction to player // Calculate direction to player
FVector DirectionToPlayer = (PlayerPawn->GetActorLocation() - GetActorLocation()).GetSafeNormal(); 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; FVector StrafeVector = FVector::CrossProduct(DirectionToPlayer, FVector::UpVector) * StrafeDirection;
// Move sideways while maintaining distance // Calculate target position
FVector NewLocation = GetActorLocation() + StrafeVector * StrafeSpeed * DeltaTime; FVector DistanceAdjustmentVector = DirectionToPlayer * DistanceAdjustment * MovementSpeed * 0.5f;
SetActorLocation(NewLocation); 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 // Face towards the player
FRotator NewRotation = DirectionToPlayer.Rotation(); FRotator TargetRotation = DirectionToPlayer.Rotation();
FRotator NewRotation = FMath::RInterpTo(GetActorRotation(), TargetRotation, DeltaTime, InterpSpeed);
SetActorRotation(NewRotation); SetActorRotation(NewRotation);
// Fire while strafing // Fire while strafing if in range
if (bCanFire) if (bCanFire && DistanceToPlayer < AttackRange)
{ {
Fire(); Fire();
} }
@@ -239,12 +471,7 @@ void AEnemySpaceship::PerformStrafe(float DeltaTime)
void AEnemySpaceship::Fire() void AEnemySpaceship::Fire()
{ {
if (!ProjectileClass) if (!ProjectileClass) return;
{
if (GEngine)
GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Red, TEXT("Enemy ProjectileClass not set!"));
return;
}
UWorld* World = GetWorld(); UWorld* World = GetWorld();
if (World) if (World)
@@ -252,9 +479,11 @@ void AEnemySpaceship::Fire()
FVector SpawnLocation = ProjectileSpawnPoint->GetComponentLocation(); FVector SpawnLocation = ProjectileSpawnPoint->GetComponentLocation();
FRotator SpawnRotation = GetActorRotation(); FRotator SpawnRotation = GetActorRotation();
// Add slight randomness to rotation for less perfect aim // Less random spread during flanking for more accurate shots
float RandPitch = FMath::RandRange(-5.0f, 5.0f); float SpreadMultiplier = (CurrentBehaviorState == EEnemyBehaviorState::Flank) ? 0.9f : 1.0f;
float RandYaw = FMath::RandRange(-5.0f, 5.0f); float RandPitch = FMath::RandRange(-5.0f, 5.0f) * SpreadMultiplier;
float RandYaw = FMath::RandRange(-5.0f, 5.0f) * SpreadMultiplier;
SpawnRotation.Pitch += RandPitch; SpawnRotation.Pitch += RandPitch;
SpawnRotation.Yaw += RandYaw; SpawnRotation.Yaw += RandYaw;
@@ -262,7 +491,6 @@ void AEnemySpaceship::Fire()
SpawnParams.Owner = this; SpawnParams.Owner = this;
SpawnParams.Instigator = GetInstigator(); SpawnParams.Instigator = GetInstigator();
// Spawn the projectile using EnemyProjectile class
AEnemyProjectile* Projectile = World->SpawnActor<AEnemyProjectile>( AEnemyProjectile* Projectile = World->SpawnActor<AEnemyProjectile>(
ProjectileClass, ProjectileClass,
SpawnLocation, SpawnLocation,
@@ -270,12 +498,6 @@ void AEnemySpaceship::Fire()
SpawnParams SpawnParams
); );
if (Projectile)
{
if (GEngine)
GEngine->AddOnScreenDebugMessage(-1, 0.5f, FColor::Yellow, TEXT("Enemy Fired!"));
}
// Start fire rate timer // Start fire rate timer
bCanFire = false; bCanFire = false;
GetWorldTimerManager().SetTimer(FireTimerHandle, this, &AEnemySpaceship::ResetFire, FireRate, false); GetWorldTimerManager().SetTimer(FireTimerHandle, this, &AEnemySpaceship::ResetFire, FireRate, false);
@@ -287,6 +509,57 @@ void AEnemySpaceship::ResetFire()
bCanFire = true; 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, float AEnemySpaceship::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent,
AController* EventInstigator, AActor* DamageCauser) AController* EventInstigator, AActor* DamageCauser)
{ {
@@ -301,6 +574,9 @@ float AEnemySpaceship::TakeDamage(float DamageAmount, FDamageEvent const& Damage
FString::Printf(TEXT("Enemy Hit! Health: %f"), CurrentHealth)); FString::Printf(TEXT("Enemy Hit! Health: %f"), CurrentHealth));
} }
// Apply visual damage effects
ApplyDamageFlash();
// When damaged, prefer retreat or strafe behaviors temporarily // When damaged, prefer retreat or strafe behaviors temporarily
if (FMath::RandBool()) if (FMath::RandBool())
{ {
@@ -338,6 +614,11 @@ void AEnemySpaceship::Die()
GetActorLocation() GetActorLocation()
); );
if (ASpaceShooterGameMode* GameMode = Cast<ASpaceShooterGameMode>(UGameplayStatics::GetGameMode(GetWorld())))
{
GameMode->IncrementKillCount();
}
// Destroy the enemy // Destroy the enemy
Destroy(); Destroy();
} }

View File

@@ -11,7 +11,8 @@ enum class EEnemyBehaviorState : uint8
Chase, // Actively pursue the player Chase, // Actively pursue the player
Attack, // Stop and shoot at the player Attack, // Stop and shoot at the player
Retreat, // Move away from player when too close Retreat, // Move away from player when too close
Strafe // Move sideways while attacking Strafe, // Move sideways while attacking
Flank // Execute flanking maneuver
}; };
UCLASS() UCLASS()
@@ -32,17 +33,45 @@ protected:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat")
USceneComponent* ProjectileSpawnPoint; USceneComponent* ProjectileSpawnPoint;
// Add interpolation speed for smooth movement
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "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") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement")
float AttackRange = 1500.0f; float MovementSpeed = 1200.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement")
float MinDistanceToPlayer = 500.0f; float FlankingSpeed = 1200.0f; // Even faster during flanking
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement") 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") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat")
float MaxHealth = 100.0f; float MaxHealth = 100.0f;
@@ -74,6 +103,26 @@ protected:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AI") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AI")
float StateChangeChance = 0.3f; 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: public:
virtual void Tick(float DeltaTime) override; virtual void Tick(float DeltaTime) override;
@@ -105,9 +154,32 @@ private:
void ChangeBehaviorState(); void ChangeBehaviorState();
void Die(); void Die();
void SmoothMove(const FVector& TargetLocation, float DeltaTime);
// AI behavior implementation functions // AI behavior implementation functions
void PerformChase(float DeltaTime); void PerformChase(float DeltaTime);
void PerformAttack(float DeltaTime); void PerformAttack(float DeltaTime);
void PerformRetreat(float DeltaTime); void PerformRetreat(float DeltaTime);
void PerformStrafe(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")); GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Yellow, TEXT("GameMode Constructor"));
} }
KillCount = 0;
bIsGameOver = false;
RemainingTime = GameDuration;
} }
void ASpaceShooterGameMode::StartPlay() void ASpaceShooterGameMode::StartPlay()
@@ -60,6 +64,9 @@ void ASpaceShooterGameMode::StartPlay()
{ {
GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Green, TEXT("GameMode 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) void ASpaceShooterGameMode::Tick(float DeltaTime)
@@ -99,12 +106,11 @@ void ASpaceShooterGameMode::SpawnEnemy()
switch (CurrentPattern) switch (CurrentPattern)
{ {
case ESpawnPattern::Random: case ESpawnPattern::Random:
// Spawn a single enemy at a random edge location
{ {
UWorld* World = GetWorld(); UWorld* World = GetWorld();
if (World && EnemyClass) if (World && EnemyClass)
{ {
FVector SpawnLocation = GetScreenEdgeSpawnLocation(); FVector SpawnLocation = GetRandomSpawnLocation();
FRotator SpawnRotation = FRotator::ZeroRotator; FRotator SpawnRotation = FRotator::ZeroRotator;
FActorSpawnParameters SpawnParams; FActorSpawnParameters SpawnParams;
SpawnParams.SpawnCollisionHandlingOverride = SpawnParams.SpawnCollisionHandlingOverride =
@@ -142,32 +148,25 @@ void ASpaceShooterGameMode::SpawnEnemyWave()
if (!World || !EnemyClass) if (!World || !EnemyClass)
return; 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 // Get player location for facing direction
FVector PlayerLocation = GetPlayerLocation(); FVector PlayerLocation = GetPlayerLocation();
// Get screen bounds // Choose a random angle for the wave
TArray<FVector2D> ScreenBounds = GetScreenBounds(); float WaveAngle = FMath::RandRange(0.0f, 2.0f * PI);
float ScreenWidth = ScreenBounds[1].X - ScreenBounds[0].X; FVector2D BaseDirection(FMath::Cos(WaveAngle), FMath::Sin(WaveAngle));
// Create a line of enemies perpendicular to the direction // Create a perpendicular direction for the wave line
FVector2D PerpDirection(-EdgeDirection.Y, EdgeDirection.X); 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++) for (int32 i = 0; i < WaveSize; i++)
{ {
FVector ProposedLocation; // Calculate base spawn position
ProposedLocation.X = PlayerLocation.X + (EdgeDirection.X * MinimumSpawnDistance) + FVector BaseSpawnPos = PlayerLocation + FVector(BaseDirection.X, BaseDirection.Y, 0) * MinimumSpawnDistance;
(PerpDirection.X * (i - WaveSize / 2) * FormationSpacing);
ProposedLocation.Y = PlayerLocation.Y + (EdgeDirection.Y * MinimumSpawnDistance) +
(PerpDirection.Y * (i - WaveSize / 2) * FormationSpacing);
ProposedLocation.Z = PlayerLocation.Z;
// Ensure the spawn location is far enough from the player // Offset along the wave line
FVector SpawnLocation = EnsureMinimumSpawnDistance(ProposedLocation, PlayerLocation); FVector Offset = FVector(PerpDirection.X, PerpDirection.Y, 0) * (i - WaveSize / 2) * FormationSpacing;
FVector SpawnLocation = BaseSpawnPos + Offset;
FRotator SpawnRotation = FRotator::ZeroRotator; FRotator SpawnRotation = FRotator::ZeroRotator;
FActorSpawnParameters SpawnParams; FActorSpawnParameters SpawnParams;
@@ -183,7 +182,6 @@ void ASpaceShooterGameMode::SpawnEnemyWave()
} }
} }
// Increase wave counter and possibly switch back to random
CurrentWaveCount++; CurrentWaveCount++;
if (CurrentWaveCount >= 3) if (CurrentWaveCount >= 3)
{ {
@@ -194,6 +192,14 @@ void ASpaceShooterGameMode::SpawnEnemyWave()
void ASpaceShooterGameMode::SpawnEnemyFormation() 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(); UWorld* World = GetWorld();
if (!World || !EnemyClass) if (!World || !EnemyClass)
return; return;
@@ -284,6 +290,7 @@ void ASpaceShooterGameMode::SpawnEnemyFormation()
// Switch back to random pattern after a formation spawn // Switch back to random pattern after a formation spawn
CurrentPattern = ESpawnPattern::Random; CurrentPattern = ESpawnPattern::Random;
} }
}
void ASpaceShooterGameMode::SpawnEnemyFlanking() void ASpaceShooterGameMode::SpawnEnemyFlanking()
{ {
@@ -291,10 +298,9 @@ void ASpaceShooterGameMode::SpawnEnemyFlanking()
if (!World || !EnemyClass) if (!World || !EnemyClass)
return; return;
// Get player location
FVector PlayerLocation = GetPlayerLocation(); 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); int32 NumSides = FMath::RandRange(2, 3);
float BaseAngle = FMath::RandRange(0.0f, 2.0f * PI); float BaseAngle = FMath::RandRange(0.0f, 2.0f * PI);
@@ -302,7 +308,6 @@ void ASpaceShooterGameMode::SpawnEnemyFlanking()
{ {
// Calculate angle for this side // Calculate angle for this side
float Angle = BaseAngle + (Side * (2.0f * PI / NumSides)); float Angle = BaseAngle + (Side * (2.0f * PI / NumSides));
FVector2D Direction(FMath::Cos(Angle), FMath::Sin(Angle));
// Spawn 1-2 enemies from this side // Spawn 1-2 enemies from this side
int32 NumEnemies = FMath::RandRange(1, 2); int32 NumEnemies = FMath::RandRange(1, 2);
@@ -311,13 +316,10 @@ void ASpaceShooterGameMode::SpawnEnemyFlanking()
{ {
// Add some variation to the spawn position // Add some variation to the spawn position
float OffsetAngle = Angle + FMath::RandRange(-0.3f, 0.3f); 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 // Calculate spawn position
FVector ProposedLocation = PlayerLocation + FVector(OffsetDir.X, OffsetDir.Y, 0) * MinimumSpawnDistance; FVector SpawnLocation = PlayerLocation + (Direction * (MinimumSpawnDistance + FMath::RandRange(0.0f, 300.0f)));
// Ensure the spawn location is far enough from the player
FVector SpawnLocation = EnsureMinimumSpawnDistance(ProposedLocation, PlayerLocation);
FRotator SpawnRotation = FRotator::ZeroRotator; FRotator SpawnRotation = FRotator::ZeroRotator;
FActorSpawnParameters SpawnParams; FActorSpawnParameters SpawnParams;
@@ -334,107 +336,28 @@ void ASpaceShooterGameMode::SpawnEnemyFlanking()
} }
} }
// Return to random spawning
CurrentPattern = ESpawnPattern::Random; CurrentPattern = ESpawnPattern::Random;
} }
FVector ASpaceShooterGameMode::GetScreenEdgeSpawnLocation() FVector ASpaceShooterGameMode::GetRandomSpawnLocation()
{ {
FVector PlayerLocation = GetPlayerLocation(); FVector PlayerLocation = GetPlayerLocation();
TArray<FVector2D> ScreenBounds = GetScreenBounds();
// Decide which edge to spawn from (0 = top, 1 = right, 2 = bottom, 3 = left) // Generate a random angle in radians
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
float RandomAngle = FMath::RandRange(0.0f, 2.0f * PI); float RandomAngle = FMath::RandRange(0.0f, 2.0f * PI);
float RandomRadius = FMath::RandRange(0.0f, Zone.Radius);
FVector SpawnOffset( // Create a direction vector from the random angle
FMath::Cos(RandomAngle) * RandomRadius, FVector Direction(
FMath::Sin(RandomAngle) * RandomRadius, FMath::Cos(RandomAngle),
FMath::Sin(RandomAngle),
0.0f 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 // Calculate the spawn position
return EnsureMinimumSpawnDistance(ProposedLocation, PlayerLocation); return PlayerLocation + (Direction * SpawnDistance);
}
}
// Fallback
return GetScreenEdgeSpawnLocation();
} }
void ASpaceShooterGameMode::UpdateDifficulty() void ASpaceShooterGameMode::UpdateDifficulty()
@@ -465,48 +388,6 @@ FVector ASpaceShooterGameMode::GetPlayerLocation()
return FVector::ZeroVector; 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) void ASpaceShooterGameMode::RotateTowardsPlayer(AEnemySpaceship* Enemy, const FVector& PlayerLocation)
{ {
if (!Enemy) if (!Enemy)
@@ -541,3 +422,47 @@ FVector ASpaceShooterGameMode::EnsureMinimumSpawnDistance(const FVector& Propose
Direction.Normalize(); Direction.Normalize();
return PlayerLocation + Direction * MinimumSpawnDistance; 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 StartPlay() override;
virtual void Tick(float DeltaTime) 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: protected:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spawning") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spawning")
TSubclassOf<class AEnemySpaceship> EnemyClass; TSubclassOf<class AEnemySpaceship> EnemyClass;
@@ -78,6 +85,18 @@ protected:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spawning|Difficulty") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spawning|Difficulty")
int32 DifficultyInterval = 30; 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: private:
FTimerHandle EnemySpawnTimer; FTimerHandle EnemySpawnTimer;
FTimerHandle DifficultyTimer; FTimerHandle DifficultyTimer;
@@ -90,13 +109,15 @@ private:
void SpawnEnemyWave(); void SpawnEnemyWave();
void SpawnEnemyFormation(); void SpawnEnemyFormation();
void SpawnEnemyFlanking(); void SpawnEnemyFlanking();
FVector GetScreenEdgeSpawnLocation(); FVector GetRandomSpawnLocation();
FVector GetSpawnZoneLocation();
void UpdateDifficulty(); void UpdateDifficulty();
FVector GetPlayerLocation(); FVector GetPlayerLocation();
TArray<FVector2D> GetScreenBounds();
void RotateTowardsPlayer(AEnemySpaceship* Enemy, const FVector& PlayerLocation); void RotateTowardsPlayer(AEnemySpaceship* Enemy, const FVector& PlayerLocation);
// New helper method to ensure minimum spawn distance // New helper method to ensure minimum spawn distance
FVector EnsureMinimumSpawnDistance(const FVector& ProposedLocation, const FVector& PlayerLocation); 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; CurrentHealth = MaxHealth;
CurrentShield = MaxShield; CurrentShield = MaxShield;
LastDamageTime = 0.0f; LastDamageTime = 0.0f;
// Initialize the dynamic material
InitializeDynamicMaterial();
} }
void ASpaceshipPawn::Tick(float DeltaTime) void ASpaceshipPawn::Tick(float DeltaTime)
@@ -486,17 +489,36 @@ float ASpaceshipPawn::TakeDamage(float DamageAmount, FDamageEvent const& DamageE
return DamageToApply; 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() void ASpaceshipPawn::ApplyDamageFlash()
{ {
// Apply material flash effect to indicate damage // Only set the flash color if we have a valid dynamic material instance
if (ShipMesh && ShipMesh->GetMaterial(0)) if (DynamicMaterialInstance)
{ {
UMaterialInstanceDynamic* DynamicMaterial = UMaterialInstanceDynamic::Create(ShipMesh->GetMaterial(0), this); // Set the flash color
if (DynamicMaterial) DynamicMaterialInstance->SetVectorParameterValue("FlashColor", DamageFlashColor);
{
// Assuming the material has a parameter named "FlashColor"
DynamicMaterial->SetVectorParameterValue("FlashColor", DamageFlashColor);
ShipMesh->SetMaterial(0, DynamicMaterial);
// Set timer to reset the flash // Set timer to reset the flash
GetWorldTimerManager().SetTimer( GetWorldTimerManager().SetTimer(
@@ -508,19 +530,13 @@ void ASpaceshipPawn::ApplyDamageFlash()
); );
} }
} }
}
void ASpaceshipPawn::ResetDamageFlash() void ASpaceshipPawn::ResetDamageFlash()
{ {
// Reset material flash effect // Reset the flash color only if we have a valid dynamic material instance
if (ShipMesh && ShipMesh->GetMaterial(0)) if (DynamicMaterialInstance)
{ {
UMaterialInstanceDynamic* DynamicMaterial = Cast<UMaterialInstanceDynamic>(ShipMesh->GetMaterial(0)); DynamicMaterialInstance->SetVectorParameterValue("FlashColor", FLinearColor(0.0f, 0.0f, 0.0f, 0.0f));
if (DynamicMaterial)
{
// Reset the flash color (assuming transparent black means no flash)
DynamicMaterial->SetVectorParameterValue("FlashColor", FLinearColor(0.0f, 0.0f, 0.0f, 0.0f));
}
} }
} }
@@ -586,10 +602,6 @@ void ASpaceshipPawn::Die()
DisableInput(PC); 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 // For now, just log the death
if (GEngine) if (GEngine)
{ {

View File

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