Compare commits
11 Commits
39ace4311d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 577205fc12 | |||
| 322f1ac061 | |||
| 0fed8de306 | |||
| 44aee655de | |||
| 7f08808b71 | |||
| f732cdef59 | |||
| c292da805d | |||
| 67ac9d1935 | |||
| 10ab51e938 | |||
| bc03fd5091 | |||
| e2e709bffd |
BIN
Content/Blueprints/BP_EnemyProjectile.uasset
LFS
Normal file
BIN
Content/Blueprints/BP_EnemyProjectile.uasset
LFS
Normal file
Binary file not shown.
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",
|
"Name": "MyProject3",
|
||||||
"Type": "Runtime",
|
"Type": "Runtime",
|
||||||
"LoadingPhase": "Default"
|
"LoadingPhase": "Default",
|
||||||
|
"AdditionalDependencies": [
|
||||||
|
"Engine"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"Plugins": [
|
"Plugins": [
|
||||||
|
|||||||
156
Source/MyProject3/EnemyProjectile.cpp
Normal file
156
Source/MyProject3/EnemyProjectile.cpp
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
Source/MyProject3/EnemyProjectile.h
Normal file
43
Source/MyProject3/EnemyProjectile.h
Normal 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);
|
||||||
|
};
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
};
|
};
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
};
|
};
|
||||||
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);
|
||||||
|
};
|
||||||
@@ -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!"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
};
|
};
|
||||||
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();
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user