14 Commits

32 changed files with 1927 additions and 271 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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

@@ -0,0 +1,156 @@
#include "EnemyProjectile.h"
#include "GameFramework/ProjectileMovementComponent.h"
#include "Components/StaticMeshComponent.h"
#include "SpaceshipPawn.h" // Include the player ship header
#include "Kismet/GameplayStatics.h"
#include "Engine/Engine.h"
AEnemyProjectile::AEnemyProjectile()
{
PrimaryActorTick.bCanEverTick = true;
// Create and setup the projectile mesh
ProjectileMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("ProjectileMesh"));
RootComponent = ProjectileMesh;
ProjectileMesh->SetCollisionProfileName(TEXT("EnemyProjectile"));
// Set up collision
ProjectileMesh->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
ProjectileMesh->SetCollisionObjectType(ECC_WorldDynamic);
ProjectileMesh->SetCollisionResponseToAllChannels(ECR_Ignore);
// Block GameTraceChannel1 (player ship)
ProjectileMesh->SetCollisionResponseToChannel(ECC_GameTraceChannel1, ECR_Block);
// Block world static for environment collisions
ProjectileMesh->SetCollisionResponseToChannel(ECC_WorldStatic, ECR_Block);
// Block pawns for player ship collision
ProjectileMesh->SetCollisionResponseToChannel(ECC_Pawn, ECR_Block);
// Enable overlap events
ProjectileMesh->SetGenerateOverlapEvents(true);
// Bind hit event
ProjectileMesh->OnComponentHit.AddDynamic(this, &AEnemyProjectile::OnHit);
// Bind overlap event for redundant collision detection
ProjectileMesh->OnComponentBeginOverlap.AddDynamic(this, &AEnemyProjectile::OnOverlapBegin);
// Create and setup projectile movement
ProjectileMovement = CreateDefaultSubobject<UProjectileMovementComponent>(TEXT("ProjectileMovement"));
ProjectileMovement->UpdatedComponent = ProjectileMesh;
ProjectileMovement->InitialSpeed = ProjectileSpeed;
ProjectileMovement->MaxSpeed = ProjectileSpeed;
ProjectileMovement->bRotationFollowsVelocity = true;
ProjectileMovement->ProjectileGravityScale = 0.0f;
// Important: Enable sweep to prevent tunneling at high speeds
ProjectileMovement->bSweepCollision = true;
// Set lifetime
InitialLifeSpan = 3.0f;
}
void AEnemyProjectile::BeginPlay()
{
Super::BeginPlay();
// Debug message to verify projectile spawned
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(-1, 1.0f, FColor::Blue, TEXT("Enemy Projectile Spawned"));
}
}
void AEnemyProjectile::OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp,
FVector NormalImpulse, const FHitResult& Hit)
{
// Debug message to verify hit detection
if (GEngine && OtherActor)
{
GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Orange,
FString::Printf(TEXT("Enemy Projectile hit: %s"), *OtherActor->GetName()));
}
if (OtherActor && OtherActor != this)
{
// Check if we hit a player ship
ASpaceshipPawn* Player = Cast<ASpaceshipPawn>(OtherActor);
if (Player)
{
// Apply damage to player
FDamageEvent DamageEvent;
float ActualDamage = Player->TakeDamage(DamageAmount, DamageEvent, nullptr, this);
// Debug message for damage
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Red,
FString::Printf(TEXT("Dealt %.1f damage to player"), ActualDamage));
}
// Spawn impact effect if set
if (ImpactEffect)
{
UGameplayStatics::SpawnEmitterAtLocation(
GetWorld(),
ImpactEffect,
Hit.Location,
Hit.Normal.Rotation()
);
}
}
}
// Destroy the projectile
Destroy();
}
void AEnemyProjectile::OnOverlapBegin(UPrimitiveComponent* OverlappedComp, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex,
bool bFromSweep, const FHitResult& SweepResult)
{
// This is a redundant collision detection method
// Some fast-moving projectiles might miss hit events but catch overlap events
// Debug message to verify overlap detection
if (GEngine && OtherActor)
{
GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Yellow,
FString::Printf(TEXT("Enemy Projectile overlap with: %s"), *OtherActor->GetName()));
}
if (OtherActor && OtherActor != this)
{
// Check if we overlap with a player ship
ASpaceshipPawn* Player = Cast<ASpaceshipPawn>(OtherActor);
if (Player)
{
// Apply damage to player
FDamageEvent DamageEvent;
float ActualDamage = Player->TakeDamage(DamageAmount, DamageEvent, nullptr, this);
// Debug message for damage
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Red,
FString::Printf(TEXT("Overlap - Dealt %.1f damage to player"), ActualDamage));
}
// Spawn impact effect if set
if (ImpactEffect)
{
UGameplayStatics::SpawnEmitterAtLocation(
GetWorld(),
ImpactEffect,
GetActorLocation(),
GetActorRotation()
);
}
// Destroy the projectile
Destroy();
}
}
}

View File

@@ -0,0 +1,43 @@
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "EnemyProjectile.generated.h"
UCLASS()
class MYPROJECT3_API AEnemyProjectile : public AActor
{
GENERATED_BODY()
public:
AEnemyProjectile();
protected:
virtual void BeginPlay() override;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Components")
class UStaticMeshComponent* ProjectileMesh;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Components")
class UProjectileMovementComponent* ProjectileMovement;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Projectile")
float ProjectileSpeed = 2500.0f; // Slightly slower than player projectiles
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Projectile")
float DamageAmount = 10.0f; // Less damage than player projectiles
// Add a particle effect for impact
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Effects")
class UParticleSystem* ImpactEffect;
UFUNCTION()
void OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp,
FVector NormalImpulse, const FHitResult& Hit);
// Add an overlap handler for redundant collision detection
UFUNCTION()
void OnOverlapBegin(UPrimitiveComponent* OverlappedComp, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex,
bool bFromSweep, const FHitResult& SweepResult);
};

View File

@@ -1,5 +1,13 @@
#include "EnemySpaceship.h" #include "EnemySpaceship.h"
#include "Kismet/GameplayStatics.h" #include "Kismet/GameplayStatics.h"
#include "GameFramework/ProjectileMovementComponent.h"
#include "Components/SphereComponent.h"
#include "Engine/World.h"
#include "DrawDebugHelpers.h"
#include "Math/UnrealMathUtility.h"
#include "EnemyProjectile.h"
#include "SpaceShooterGameMode.h"
#include "SpaceshipPawn.h"
AEnemySpaceship::AEnemySpaceship() AEnemySpaceship::AEnemySpaceship()
{ {
@@ -9,6 +17,11 @@ AEnemySpaceship::AEnemySpaceship()
EnemyMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("EnemyMesh")); EnemyMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("EnemyMesh"));
RootComponent = EnemyMesh; RootComponent = EnemyMesh;
// Create projectile spawn point
ProjectileSpawnPoint = CreateDefaultSubobject<USceneComponent>(TEXT("ProjectileSpawnPoint"));
ProjectileSpawnPoint->SetupAttachment(EnemyMesh);
ProjectileSpawnPoint->SetRelativeLocation(FVector(100.0f, 0.0f, 0.0f)); // Forward of the ship
// Disable gravity and physics simulation // Disable gravity and physics simulation
EnemyMesh->SetSimulatePhysics(false); EnemyMesh->SetSimulatePhysics(false);
EnemyMesh->SetEnableGravity(false); EnemyMesh->SetEnableGravity(false);
@@ -18,32 +31,532 @@ AEnemySpaceship::AEnemySpaceship()
EnemyMesh->SetCollisionObjectType(ECC_Pawn); EnemyMesh->SetCollisionObjectType(ECC_Pawn);
EnemyMesh->SetCollisionResponseToAllChannels(ECR_Block); EnemyMesh->SetCollisionResponseToAllChannels(ECR_Block);
EnemyMesh->SetCollisionResponseToChannel(ECC_Pawn, ECR_Ignore); // Ignore other pawns EnemyMesh->SetCollisionResponseToChannel(ECC_Pawn, ECR_Ignore); // Ignore other pawns
EnemyMesh->SetCollisionResponseToChannel(ECC_GameTraceChannel1, ECR_Ignore); // Ignore player 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()
{ {
Super::BeginPlay(); Super::BeginPlay();
// Find the player pawn // Find the player pawn and cast to SpaceshipPawn
PlayerPawn = UGameplayStatics::GetPlayerPawn(GetWorld(), 0); AActor* FoundPlayerPawn = UGameplayStatics::GetPlayerPawn(GetWorld(), 0);
PlayerPawn = Cast<ASpaceshipPawn>(FoundPlayerPawn);
// Initialize behavior state timer
GetWorldTimerManager().SetTimer(BehaviorTimerHandle, this, &AEnemySpaceship::ChangeBehaviorState, BehaviorChangeTime, true);
// Initialize with the ability to fire
bCanFire = true;
// 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) void AEnemySpaceship::Tick(float DeltaTime)
{ {
Super::Tick(DeltaTime); Super::Tick(DeltaTime);
if (!PlayerPawn)
{
AActor* FoundPlayerPawn = UGameplayStatics::GetPlayerPawn(GetWorld(), 0);
PlayerPawn = Cast<ASpaceshipPawn>(FoundPlayerPawn);
if (!PlayerPawn) return;
}
// Calculate distance to player
DistanceToPlayer = FVector::Dist(GetActorLocation(), PlayerPawn->GetActorLocation());
// Update behavior state based on distance
UpdateBehaviorState();
// Execute behavior based on current state
switch (CurrentBehaviorState)
{
case EEnemyBehaviorState::Chase:
PerformChase(DeltaTime);
break;
case EEnemyBehaviorState::Attack:
PerformAttack(DeltaTime);
break;
case EEnemyBehaviorState::Retreat:
PerformRetreat(DeltaTime);
break;
case EEnemyBehaviorState::Strafe:
PerformStrafe(DeltaTime);
break;
case EEnemyBehaviorState::Flank:
PerformFlank(DeltaTime);
break;
}
// Debug state information
if (GEngine)
{
FString StateString;
switch (CurrentBehaviorState)
{
case EEnemyBehaviorState::Chase: StateString = "Chase"; break;
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,
FString::Printf(TEXT("Enemy State: %s | Distance: %.1f"), *StateString, DistanceToPlayer));
}
}
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()
{
if (!PlayerPawn) return;
FVector DesiredPosition = CalculatePositionAwayFromOtherEnemies();
FVector PlayerVelocity = PlayerPawn->GetVelocity();
float PlayerSpeed = PlayerVelocity.Size();
float AggressionRoll = FMath::FRand();
if (DistanceToPlayer < MinDistanceToPlayer)
{
if (AggressionRoll < AggressionFactor * 0.3f)
{
CurrentBehaviorState = EEnemyBehaviorState::Attack;
}
else
{
CurrentBehaviorState = EEnemyBehaviorState::Retreat;
}
}
else if (DistanceToPlayer < AttackRange)
{
// Increased chance of flanking
if (AggressionRoll < AggressionFactor * FlankingFrequency)
{
// 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
{
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
if (FMath::FRand() < StateChangeChance)
{
// Pick a random state
int32 NewState = FMath::RandRange(0, 3);
CurrentBehaviorState = static_cast<EEnemyBehaviorState>(NewState);
// Randomize strafe direction when entering strafe mode
if (CurrentBehaviorState == EEnemyBehaviorState::Strafe)
{
StrafeDirection = FMath::RandBool() ? 1.0f : -1.0f;
}
}
}
void AEnemySpaceship::PerformChase(float DeltaTime)
{
if (PlayerPawn) if (PlayerPawn)
{ {
// Move towards the player // Calculate direction to player
FVector Direction = (PlayerPawn->GetActorLocation() - GetActorLocation()).GetSafeNormal(); FVector DirectionToPlayer = (PlayerPawn->GetActorLocation() - GetActorLocation()).GetSafeNormal();
FVector NewLocation = GetActorLocation() + Direction * MovementSpeed * DeltaTime;
SetActorLocation(NewLocation);
// Face towards the player // Calculate target position
FVector TargetPosition = GetActorLocation() + DirectionToPlayer * MovementSpeed * DeltaTime;
// 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);
// Fire if within range
if (DistanceToPlayer < AttackRange && bCanFire)
{
Fire();
}
}
}
void AEnemySpaceship::PerformAttack(float DeltaTime)
{
if (PlayerPawn)
{
// Face towards the player but don't move forward
FVector Direction = (PlayerPawn->GetActorLocation() - GetActorLocation()).GetSafeNormal();
FRotator NewRotation = Direction.Rotation(); FRotator NewRotation = Direction.Rotation();
SetActorRotation(NewRotation); SetActorRotation(NewRotation);
// Fire if ready
if (bCanFire)
{
Fire();
}
}
}
void AEnemySpaceship::PerformRetreat(float DeltaTime)
{
if (PlayerPawn)
{
// Calculate ideal retreat position
FVector DirectionFromPlayer = (GetActorLocation() - PlayerPawn->GetActorLocation()).GetSafeNormal();
FVector DesiredPosition = PlayerPawn->GetActorLocation() + (DirectionFromPlayer * OptimalCombatDistance);
// 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 in range
if (bCanFire && DistanceToPlayer < AttackRange)
{
Fire();
}
}
}
void AEnemySpaceship::PerformStrafe(float DeltaTime)
{
if (PlayerPawn)
{
// Calculate direction to player
FVector DirectionToPlayer = (PlayerPawn->GetActorLocation() - GetActorLocation()).GetSafeNormal();
// 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;
// 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 TargetRotation = DirectionToPlayer.Rotation();
FRotator NewRotation = FMath::RInterpTo(GetActorRotation(), TargetRotation, DeltaTime, InterpSpeed);
SetActorRotation(NewRotation);
// Fire while strafing if in range
if (bCanFire && DistanceToPlayer < AttackRange)
{
Fire();
}
}
}
void AEnemySpaceship::Fire()
{
if (!ProjectileClass) return;
UWorld* World = GetWorld();
if (World)
{
FVector SpawnLocation = ProjectileSpawnPoint->GetComponentLocation();
FRotator SpawnRotation = GetActorRotation();
// 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;
FActorSpawnParameters SpawnParams;
SpawnParams.Owner = this;
SpawnParams.Instigator = GetInstigator();
AEnemyProjectile* Projectile = World->SpawnActor<AEnemyProjectile>(
ProjectileClass,
SpawnLocation,
SpawnRotation,
SpawnParams
);
// Start fire rate timer
bCanFire = false;
GetWorldTimerManager().SetTimer(FireTimerHandle, this, &AEnemySpaceship::ResetFire, FireRate, false);
}
}
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));
} }
} }
@@ -58,7 +571,21 @@ float AEnemySpaceship::TakeDamage(float DamageAmount, FDamageEvent const& Damage
if (GEngine) if (GEngine)
{ {
GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Red, GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Red,
FString::Printf(TEXT("Enemy Health: %f"), CurrentHealth)); FString::Printf(TEXT("Enemy Hit! Health: %f"), CurrentHealth));
}
// Apply visual damage effects
ApplyDamageFlash();
// When damaged, prefer retreat or strafe behaviors temporarily
if (FMath::RandBool())
{
CurrentBehaviorState = EEnemyBehaviorState::Retreat;
}
else
{
CurrentBehaviorState = EEnemyBehaviorState::Strafe;
StrafeDirection = FMath::RandBool() ? 1.0f : -1.0f;
} }
if (CurrentHealth <= 0) if (CurrentHealth <= 0)
@@ -71,7 +598,26 @@ float AEnemySpaceship::TakeDamage(float DamageAmount, FDamageEvent const& Damage
void AEnemySpaceship::Die() void AEnemySpaceship::Die()
{ {
// Add any death effects here // Play explosion effect
UGameplayStatics::SpawnEmitterAtLocation(
GetWorld(),
nullptr, // Add your explosion effect here
GetActorLocation(),
FRotator::ZeroRotator,
FVector(2.0f)
);
// Play explosion sound
UGameplayStatics::PlaySoundAtLocation(
this,
nullptr, // Add your explosion sound here
GetActorLocation()
);
if (ASpaceShooterGameMode* GameMode = Cast<ASpaceShooterGameMode>(UGameplayStatics::GetGameMode(GetWorld())))
{
GameMode->IncrementKillCount();
}
// Destroy the enemy // Destroy the enemy
Destroy(); Destroy();

View File

@@ -4,6 +4,17 @@
#include "GameFramework/Actor.h" #include "GameFramework/Actor.h"
#include "EnemySpaceship.generated.h" #include "EnemySpaceship.generated.h"
// Define AI behavior states
UENUM(BlueprintType)
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
Flank // Execute flanking maneuver
};
UCLASS() UCLASS()
class MYPROJECT3_API AEnemySpaceship : public AActor class MYPROJECT3_API AEnemySpaceship : public AActor
{ {
@@ -18,8 +29,49 @@ protected:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Components") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Components")
UStaticMeshComponent* EnemyMesh; UStaticMeshComponent* EnemyMesh;
// Projectile spawn points
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat")
USceneComponent* ProjectileSpawnPoint;
// Add interpolation speed for smooth movement
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement")
float MovementSpeed = 300.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 MovementSpeed = 1200.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement")
float FlankingSpeed = 1200.0f; // Even faster during flanking
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement")
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;
@@ -27,6 +79,50 @@ protected:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat")
float CurrentHealth = 100.0f; float CurrentHealth = 100.0f;
// Projectile class to spawn
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat")
TSubclassOf<class AEnemyProjectile> ProjectileClass;
// Firing properties
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat")
float FireRate = 1.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat")
float ProjectileSpeed = 2000.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat")
float ProjectileDamage = 10.0f;
// AI behavior properties
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AI")
EEnemyBehaviorState CurrentBehaviorState = EEnemyBehaviorState::Chase;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AI")
float BehaviorChangeTime = 3.0f;
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: public:
virtual void Tick(float DeltaTime) override; virtual void Tick(float DeltaTime) override;
@@ -35,6 +131,55 @@ public:
class AController* EventInstigator, AActor* DamageCauser) override; class AController* EventInstigator, AActor* DamageCauser) override;
private: private:
AActor* PlayerPawn; // Reference to player
class ASpaceshipPawn* PlayerPawn;
// Distance to player
float DistanceToPlayer;
// Fire control
FTimerHandle FireTimerHandle;
bool bCanFire = true;
// Strafe direction (1 = right, -1 = left)
float StrafeDirection = 1.0f;
// Timer for changing behavior
FTimerHandle BehaviorTimerHandle;
// Helper functions
void Fire();
void ResetFire();
void UpdateBehaviorState();
void ChangeBehaviorState();
void Die(); void Die();
void SmoothMove(const FVector& TargetLocation, float DeltaTime);
// AI behavior implementation functions
void PerformChase(float DeltaTime);
void PerformAttack(float DeltaTime);
void PerformRetreat(float DeltaTime);
void PerformStrafe(float DeltaTime);
void PerformFlank(float DeltaTime);
FVector CalculatePositionAwayFromOtherEnemies();
FVector CalculateFlankPosition();
FVector CurrentVelocity;
FVector TargetVelocity;
FVector LastPosition;
FVector FlankPosition;
float FlankTimer;
bool bIsFlankingRight;
FTimerHandle FlankUpdateTimer;
float LastFlankUpdateTime;
FVector CurrentFlankTarget;
bool bInitializedFlank;
void InitializeDynamicMaterial();
void ApplyDamageFlash();
void ResetDamageFlash();
}; };

View File

@@ -41,6 +41,10 @@ ASpaceShooterGameMode::ASpaceShooterGameMode()
{ {
GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Yellow, TEXT("GameMode Constructor")); 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,29 +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 // Spawn wave of enemies
for (int32 i = 0; i < WaveSize; i++) for (int32 i = 0; i < WaveSize; i++)
{ {
FVector SpawnLocation; // Calculate base spawn position
SpawnLocation.X = PlayerLocation.X + (EdgeDirection.X * 2000.0f) + FVector BaseSpawnPos = PlayerLocation + FVector(BaseDirection.X, BaseDirection.Y, 0) * MinimumSpawnDistance;
(PerpDirection.X * (i - WaveSize / 2) * FormationSpacing);
SpawnLocation.Y = PlayerLocation.Y + (EdgeDirection.Y * 2000.0f) + // Offset along the wave line
(PerpDirection.Y * (i - WaveSize / 2) * FormationSpacing); FVector Offset = FVector(PerpDirection.X, PerpDirection.Y, 0) * (i - WaveSize / 2) * FormationSpacing;
SpawnLocation.Z = PlayerLocation.Z; FVector SpawnLocation = BaseSpawnPos + Offset;
FRotator SpawnRotation = FRotator::ZeroRotator; FRotator SpawnRotation = FRotator::ZeroRotator;
FActorSpawnParameters SpawnParams; FActorSpawnParameters SpawnParams;
@@ -180,7 +182,6 @@ void ASpaceShooterGameMode::SpawnEnemyWave()
} }
} }
// Increase wave counter and possibly switch back to random
CurrentWaveCount++; CurrentWaveCount++;
if (CurrentWaveCount >= 3) if (CurrentWaveCount >= 3)
{ {
@@ -191,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;
@@ -205,8 +214,8 @@ void ASpaceShooterGameMode::SpawnEnemyFormation()
float ApproachAngle = FMath::RandRange(0.0f, 2.0f * PI); float ApproachAngle = FMath::RandRange(0.0f, 2.0f * PI);
FVector2D ApproachDir(FMath::Cos(ApproachAngle), FMath::Sin(ApproachAngle)); FVector2D ApproachDir(FMath::Cos(ApproachAngle), FMath::Sin(ApproachAngle));
// Base spawn position far from player // Base spawn position far from player - increased from 2500 to MinimumSpawnDistance + 500
FVector BaseSpawnPos = PlayerLocation + FVector(ApproachDir.X, ApproachDir.Y, 0) * 2500.0f; FVector BaseSpawnPos = PlayerLocation + FVector(ApproachDir.X, ApproachDir.Y, 0) * (MinimumSpawnDistance + 500.0f);
// Create formation positions // Create formation positions
TArray<FVector> FormationPositions; TArray<FVector> FormationPositions;
@@ -261,13 +270,16 @@ void ASpaceShooterGameMode::SpawnEnemyFormation()
// Spawn enemies at formation positions // Spawn enemies at formation positions
for (const FVector& Position : FormationPositions) for (const FVector& Position : FormationPositions)
{ {
// Ensure the spawn location is far enough from the player
FVector SpawnLocation = EnsureMinimumSpawnDistance(Position, PlayerLocation);
FRotator SpawnRotation = FRotator::ZeroRotator; FRotator SpawnRotation = FRotator::ZeroRotator;
FActorSpawnParameters SpawnParams; FActorSpawnParameters SpawnParams;
SpawnParams.SpawnCollisionHandlingOverride = SpawnParams.SpawnCollisionHandlingOverride =
ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn; ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;
AEnemySpaceship* NewEnemy = World->SpawnActor<AEnemySpaceship>( AEnemySpaceship* NewEnemy = World->SpawnActor<AEnemySpaceship>(
EnemyClass, Position, SpawnRotation, SpawnParams); EnemyClass, SpawnLocation, SpawnRotation, SpawnParams);
if (NewEnemy) if (NewEnemy)
{ {
@@ -278,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()
{ {
@@ -285,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);
@@ -296,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);
@@ -305,9 +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);
FVector SpawnLocation = PlayerLocation + FVector(OffsetDir.X, OffsetDir.Y, 0) * 2000.0f; // Calculate spawn position
FVector SpawnLocation = PlayerLocation + (Direction * (MinimumSpawnDistance + FMath::RandRange(0.0f, 300.0f)));
FRotator SpawnRotation = FRotator::ZeroRotator; FRotator SpawnRotation = FRotator::ZeroRotator;
FActorSpawnParameters SpawnParams; FActorSpawnParameters SpawnParams;
@@ -324,99 +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;
switch (Edge)
{
case 0: // Top edge
RandomPos = FMath::RandRange(ScreenBounds[0].X, ScreenBounds[1].X);
SpawnLocation = FVector(RandomPos, ScreenBounds[0].Y - ScreenSpawnMargin, PlayerLocation.Z);
break;
case 1: // Right edge
RandomPos = FMath::RandRange(ScreenBounds[0].Y, ScreenBounds[1].Y);
SpawnLocation = FVector(ScreenBounds[1].X + ScreenSpawnMargin, RandomPos, PlayerLocation.Z);
break;
case 2: // Bottom edge
RandomPos = FMath::RandRange(ScreenBounds[0].X, ScreenBounds[1].X);
SpawnLocation = FVector(RandomPos, ScreenBounds[1].Y + ScreenSpawnMargin, PlayerLocation.Z);
break;
case 3: // Left edge
RandomPos = FMath::RandRange(ScreenBounds[0].Y, ScreenBounds[1].Y);
SpawnLocation = FVector(ScreenBounds[0].X - ScreenSpawnMargin, RandomPos, PlayerLocation.Z);
break;
}
return SpawnLocation;
}
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;
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
); );
return Zone.Location + SpawnOffset; // Use the minimum spawn distance plus some random additional distance
} float SpawnDistance = MinimumSpawnDistance + FMath::RandRange(0.0f, 500.0f);
}
// Fallback // Calculate the spawn position
return GetScreenEdgeSpawnLocation(); return PlayerLocation + (Direction * SpawnDistance);
} }
void ASpaceShooterGameMode::UpdateDifficulty() void ASpaceShooterGameMode::UpdateDifficulty()
@@ -447,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)
@@ -505,3 +404,65 @@ void ASpaceShooterGameMode::RotateTowardsPlayer(AEnemySpaceship* Enemy, const FV
// Set the enemy's rotation // Set the enemy's rotation
Enemy->SetActorRotation(NewRotation); Enemy->SetActorRotation(NewRotation);
} }
// New function to ensure enemies spawn at least MinimumSpawnDistance away from player
FVector ASpaceShooterGameMode::EnsureMinimumSpawnDistance(const FVector& ProposedLocation, const FVector& PlayerLocation)
{
FVector Direction = ProposedLocation - PlayerLocation;
Direction.Z = 0; // Keep on the same Z plane as the player
float CurrentDistance = Direction.Size();
// If already far enough, return the proposed location
if (CurrentDistance >= MinimumSpawnDistance)
{
return ProposedLocation;
}
// Otherwise, extend the vector to meet the minimum distance
Direction.Normalize();
return PlayerLocation + Direction * MinimumSpawnDistance;
}
void ASpaceShooterGameMode::UpdateGameTimer()
{
if (bIsGameOver) return;
RemainingTime -= 1.0f;
if (RemainingTime <= 0.0f)
{
EndGame();
}
}
void ASpaceShooterGameMode::EndGame()
{
bIsGameOver = true;
// Stop enemy spawning
GetWorldTimerManager().ClearTimer(EnemySpawnTimer);
// Clear existing enemies
TArray<AActor*> FoundEnemies;
UGameplayStatics::GetAllActorsOfClass(GetWorld(), AEnemySpaceship::StaticClass(), FoundEnemies);
for (AActor* Enemy : FoundEnemies)
{
Enemy->Destroy();
}
// Pause the game
UGameplayStatics::SetGamePaused(GetWorld(), true);
}
void ASpaceShooterGameMode::IncrementKillCount()
{
if (!bIsGameOver)
{
KillCount++;
}
}
void ASpaceShooterGameMode::RestartGame()
{
UGameplayStatics::OpenLevel(this, FName(*GetWorld()->GetName()), false);
}

View File

@@ -44,6 +44,13 @@ public:
virtual void StartPlay() override; virtual void 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;
@@ -55,7 +62,10 @@ protected:
int32 MaxEnemies = 10; int32 MaxEnemies = 10;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spawning") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spawning")
float ScreenSpawnMargin = 100.0f; float ScreenSpawnMargin = 300.0f; // Increased from 100.0f
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spawning")
float MinimumSpawnDistance = 4000.0f; // New property to ensure minimum spawn distance
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spawning") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spawning")
TArray<FSpawnZone> SpawnZones; TArray<FSpawnZone> SpawnZones;
@@ -75,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;
@@ -87,10 +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
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

@@ -1,6 +1,7 @@
#include "SpaceshipPawn.h" #include "SpaceshipPawn.h"
#include "GameFramework/SpringArmComponent.h" #include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h" #include "Camera/CameraComponent.h"
#include "Camera/CameraShakeBase.h"
#include "Components/StaticMeshComponent.h" #include "Components/StaticMeshComponent.h"
#include "EnhancedInputComponent.h" #include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h" #include "EnhancedInputSubsystems.h"
@@ -59,6 +60,7 @@ ASpaceshipPawn::ASpaceshipPawn()
ShipMesh->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Block); ShipMesh->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Block);
ShipMesh->SetCollisionResponseToChannel(ECC_GameTraceChannel1, ECollisionResponse::ECR_Ignore); ShipMesh->SetCollisionResponseToChannel(ECC_GameTraceChannel1, ECollisionResponse::ECR_Ignore);
ShipMesh->SetCollisionResponseToChannel(ECC_Pawn, ECollisionResponse::ECR_Ignore); ShipMesh->SetCollisionResponseToChannel(ECC_Pawn, ECollisionResponse::ECR_Ignore);
ShipMesh->SetGenerateOverlapEvents(true);
} }
} }
@@ -104,6 +106,14 @@ void ASpaceshipPawn::BeginPlay()
} }
} }
} }
// Initialize health and shield values
CurrentHealth = MaxHealth;
CurrentShield = MaxShield;
LastDamageTime = 0.0f;
// Initialize the dynamic material
InitializeDynamicMaterial();
} }
void ASpaceshipPawn::Tick(float DeltaTime) void ASpaceshipPawn::Tick(float DeltaTime)
@@ -201,7 +211,7 @@ void ASpaceshipPawn::UpdateArcadeMovement(float DeltaTime)
// Calculate desired velocity (forward/back + strafe) // Calculate desired velocity (forward/back + strafe)
FVector DesiredVelocity = (ForwardVector * CurrentThrottleInput * MaxSpeed) + FVector DesiredVelocity = (ForwardVector * CurrentThrottleInput * MaxSpeed) +
(RightVector * CurrentStrafeInput * (MaxSpeed * 0.7f)); // Strafe is slightly slower (RightVector * CurrentStrafeInput * (MaxSpeed * 0.95f)); // Strafe is slightly slower
// Smoothly interpolate current velocity to desired velocity // Smoothly interpolate current velocity to desired velocity
if (!FMath::IsNearlyZero(CurrentThrottleInput) || !FMath::IsNearlyZero(CurrentStrafeInput)) if (!FMath::IsNearlyZero(CurrentThrottleInput) || !FMath::IsNearlyZero(CurrentStrafeInput))
@@ -395,3 +405,206 @@ void ASpaceshipPawn::ResetFire()
{ {
bCanFire = true; bCanFire = true;
} }
float ASpaceshipPawn::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent,
AController* EventInstigator, AActor* DamageCauser)
{
float DamageToApply = Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);
// Update last damage time
LastDamageTime = GetWorld()->GetTimeSeconds();
// Stop shield recharge and clear timer
GetWorldTimerManager().ClearTimer(ShieldRechargeTimerHandle);
// Apply damage to shield first
if (CurrentShield > 0.0f)
{
if (CurrentShield >= DamageToApply)
{
// Shield absorbs all damage
CurrentShield -= DamageToApply;
DamageToApply = 0.0f;
}
else
{
// Shield absorbs part of the damage
DamageToApply -= CurrentShield;
CurrentShield = 0.0f;
}
}
// Apply remaining damage to health
CurrentHealth -= DamageToApply;
// Debug message
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Red,
FString::Printf(TEXT("Player Hit! Health: %.1f, Shield: %.1f"),
CurrentHealth, CurrentShield));
}
// Apply visual damage effects
ApplyDamageFlash();
// Play impact effect
if (ImpactEffect)
{
UGameplayStatics::SpawnEmitterAttached(
ImpactEffect,
ShipMesh,
NAME_None,
GetActorLocation(),
GetActorRotation(),
EAttachLocation::KeepWorldPosition
);
}
// Play impact sound
if (ImpactSound)
{
UGameplayStatics::PlaySoundAtLocation(
this,
ImpactSound,
GetActorLocation()
);
}
// Start shield recharge timer after delay
GetWorldTimerManager().SetTimer(
ShieldRechargeTimerHandle,
this,
&ASpaceshipPawn::StartShieldRecharge,
ShieldRechargeDelay,
false
);
// Check for death
if (CurrentHealth <= 0.0f)
{
Die();
}
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()
{
// 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,
&ASpaceshipPawn::ResetDamageFlash,
DamageFlashDuration,
false
);
}
}
void ASpaceshipPawn::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));
}
}
void ASpaceshipPawn::StartShieldRecharge()
{
// Start continuous shield recharge
GetWorldTimerManager().SetTimer(
ShieldRechargeTimerHandle,
this,
&ASpaceshipPawn::RechargeShield,
0.1f, // update every 0.1 seconds
true // looping
);
}
void ASpaceshipPawn::RechargeShield()
{
// Recharge shield gradually
if (CurrentShield < MaxShield)
{
CurrentShield = FMath::Min(CurrentShield + (ShieldRechargeRate * 0.1f), MaxShield);
}
else
{
// Shield is full, stop the timer
GetWorldTimerManager().ClearTimer(ShieldRechargeTimerHandle);
}
}
void ASpaceshipPawn::Die()
{
// Play death explosion effect
UGameplayStatics::SpawnEmitterAtLocation(
GetWorld(),
ImpactEffect,
GetActorLocation(),
GetActorRotation(),
FVector(3.0f) // Larger explosion for death
);
// Play death sound
if (ImpactSound)
{
UGameplayStatics::PlaySoundAtLocation(
this,
ImpactSound,
GetActorLocation(),
2.0f // Louder for death
);
}
// Hide the ship mesh
if (ShipMesh)
{
ShipMesh->SetVisibility(false);
ShipMesh->SetCollisionEnabled(ECollisionEnabled::NoCollision);
}
// Disable input
APlayerController* PC = Cast<APlayerController>(GetController());
if (PC)
{
DisableInput(PC);
}
// For now, just log the death
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Red, TEXT("PLAYER DIED!"));
}
}

View File

@@ -25,6 +25,30 @@ public:
virtual void Tick(float DeltaTime) override; virtual void Tick(float DeltaTime) override;
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override; virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
// Override TakeDamage to handle player damage
virtual float TakeDamage(float DamageAmount, struct FDamageEvent const& DamageEvent,
class AController* EventInstigator, AActor* DamageCauser) override;
// Get current health percentage
UFUNCTION(BlueprintCallable, Category = "Combat")
float GetHealthPercentage() const { return CurrentHealth / MaxHealth; }
// Get current shield percentage
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: protected:
virtual void BeginPlay() override; virtual void BeginPlay() override;
@@ -48,12 +72,20 @@ protected:
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input") UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
class UInputAction* MouseLookAction; class UInputAction* MouseLookAction;
// Add a strafe input to allow lateral movement
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
class UInputAction* StrafeAction;
// Input action for shooting
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
class UInputAction* ShootAction;
// Movement Parameters // Movement Parameters
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement")
EShipMovementMode MovementMode = EShipMovementMode::Arcade; EShipMovementMode MovementMode = EShipMovementMode::Arcade;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement")
float MaxSpeed = 2000.0f; float MaxSpeed = 3000.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement")
float Acceleration = 2000.0f; float Acceleration = 2000.0f;
@@ -86,10 +118,6 @@ protected:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement")
float AutoBrakeStrength = 3.0f; float AutoBrakeStrength = 3.0f;
// Add a strafe input to allow lateral movement
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
class UInputAction* StrafeAction;
// Shooting properties // Shooting properties
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat")
TSubclassOf<class ASpaceshipProjectile> ProjectileClass; TSubclassOf<class ASpaceshipProjectile> ProjectileClass;
@@ -100,10 +128,6 @@ protected:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat")
USceneComponent* ProjectileSpawnPoint; USceneComponent* ProjectileSpawnPoint;
// Input action for shooting
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
class UInputAction* ShootAction;
UPROPERTY(EditDefaultsOnly, Category = "UI") UPROPERTY(EditDefaultsOnly, Category = "UI")
TSubclassOf<UUserWidget> CrosshairWidgetClass; TSubclassOf<UUserWidget> CrosshairWidgetClass;
@@ -115,6 +139,51 @@ protected:
void HandleStrafeInput(const FInputActionValue& Value); void HandleStrafeInput(const FInputActionValue& Value);
void HandleMouseLook(const FInputActionValue& Value); void HandleMouseLook(const FInputActionValue& Value);
// Health properties
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat|Stats")
float MaxHealth = 100.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat|Stats")
float CurrentHealth = 100.0f;
// Shield properties
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat|Stats")
float MaxShield = 100.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat|Stats")
float CurrentShield = 100.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat")
float ShieldRechargeRate = 5.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat")
float ShieldRechargeDelay = 3.0f;
// Impact effects
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Effects")
UParticleSystem* ImpactEffect;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Effects")
class USoundBase* ImpactSound;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Effects")
class UCameraShakeBase* DamageShake;
// Damage flash effect for the ship
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Effects")
float DamageFlashDuration = 0.2f;
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: private:
// Movement state // Movement state
float CurrentThrottleInput; float CurrentThrottleInput;
@@ -143,4 +212,20 @@ private:
void UpdateAssistedMovement(float DeltaTime); void UpdateAssistedMovement(float DeltaTime);
void UpdateRealisticMovement(float DeltaTime); void UpdateRealisticMovement(float DeltaTime);
void UpdateShipRotation(float DeltaTime); void UpdateShipRotation(float DeltaTime);
// Damage handling
void ApplyDamageFlash();
void ResetDamageFlash();
FTimerHandle ShieldRechargeTimerHandle;
FTimerHandle DamageFlashTimerHandle;
float LastDamageTime;
// Initialize dynamic material
void InitializeDynamicMaterial();
void StartShieldRecharge();
void RechargeShield();
bool IsDead() const { return CurrentHealth <= 0.0f; }
void Die();
}; };

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();
};

View File

@@ -22,7 +22,7 @@ protected:
class UProjectileMovementComponent* ProjectileMovement; class UProjectileMovementComponent* ProjectileMovement;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Projectile") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Projectile")
float ProjectileSpeed = 3000.0f; float ProjectileSpeed = 3500.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Projectile") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Projectile")
float DamageAmount = 20.0f; float DamageAmount = 20.0f;