Compare commits
8 Commits
10ab51e938
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 577205fc12 | |||
| 322f1ac061 | |||
| 0fed8de306 | |||
| 44aee655de | |||
| 7f08808b71 | |||
| f732cdef59 | |||
| c292da805d | |||
| 67ac9d1935 |
Binary file not shown.
Binary file not shown.
BIN
Content/Blueprints/BP_SpaceShooterHUD.uasset
LFS
Normal file
BIN
Content/Blueprints/BP_SpaceShooterHUD.uasset
LFS
Normal file
Binary file not shown.
Binary file not shown.
BIN
Content/Blueprints/BP_SpaceshipPlayerController.uasset
LFS
Normal file
BIN
Content/Blueprints/BP_SpaceshipPlayerController.uasset
LFS
Normal file
Binary file not shown.
Binary file not shown.
BIN
Content/Input/IA_Pause.uasset
LFS
BIN
Content/Input/IA_Pause.uasset
LFS
Binary file not shown.
BIN
Content/Main.umap
LFS
BIN
Content/Main.umap
LFS
Binary file not shown.
BIN
Content/Materials/M_StarSparrow_Red1.uasset
LFS
Normal file
BIN
Content/Materials/M_StarSparrow_Red1.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Materials/Material.uasset
LFS
Normal file
BIN
Content/Materials/Material.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Materials/Material_001.uasset
LFS
Normal file
BIN
Content/Materials/Material_001.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Materials/Material_002.uasset
LFS
Normal file
BIN
Content/Materials/Material_002.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Materials/xwingCanopy.uasset
LFS
Normal file
BIN
Content/Materials/xwingCanopy.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Meshes/spaceship_lowpoly.uasset
LFS
Normal file
BIN
Content/Meshes/spaceship_lowpoly.uasset
LFS
Normal file
Binary file not shown.
@@ -7,7 +7,10 @@
|
||||
{
|
||||
"Name": "MyProject3",
|
||||
"Type": "Runtime",
|
||||
"LoadingPhase": "Default"
|
||||
"LoadingPhase": "Default",
|
||||
"AdditionalDependencies": [
|
||||
"Engine"
|
||||
]
|
||||
}
|
||||
],
|
||||
"Plugins": [
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
280
Source/MyProject3/SpaceShooterHUD.cpp
Normal file
280
Source/MyProject3/SpaceShooterHUD.cpp
Normal 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());
|
||||
}
|
||||
76
Source/MyProject3/SpaceShooterHUD.h
Normal file
76
Source/MyProject3/SpaceShooterHUD.h
Normal 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);
|
||||
};
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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; }
|
||||
|
||||
61
Source/MyProject3/SpaceshipPlayerController.cpp
Normal file
61
Source/MyProject3/SpaceshipPlayerController.cpp
Normal 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
|
||||
}
|
||||
33
Source/MyProject3/SpaceshipPlayerController.h
Normal file
33
Source/MyProject3/SpaceshipPlayerController.h
Normal 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();
|
||||
};
|
||||
Reference in New Issue
Block a user