Implement enemy shooting and scale down for more arcade game feel
This commit is contained in:
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.
Binary file not shown.
BIN
Content/Main.umap
LFS
BIN
Content/Main.umap
LFS
Binary file not shown.
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,14 @@
|
|||||||
#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 SpaceshipPawn to access player-specific functionality
|
||||||
|
#include "SpaceshipPawn.h"
|
||||||
|
|
||||||
AEnemySpaceship::AEnemySpaceship()
|
AEnemySpaceship::AEnemySpaceship()
|
||||||
{
|
{
|
||||||
@@ -9,6 +18,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,7 +32,7 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,26 +40,252 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
void AEnemySpaceship::Tick(float DeltaTime)
|
void AEnemySpaceship::Tick(float DeltaTime)
|
||||||
{
|
{
|
||||||
Super::Tick(DeltaTime);
|
Super::Tick(DeltaTime);
|
||||||
|
|
||||||
|
if (!PlayerPawn)
|
||||||
|
{
|
||||||
|
// Try to find player again if not set
|
||||||
|
AActor* FoundPlayerPawn = UGameplayStatics::GetPlayerPawn(GetWorld(), 0);
|
||||||
|
PlayerPawn = Cast<ASpaceshipPawn>(FoundPlayerPawn);
|
||||||
|
|
||||||
|
if (!PlayerPawn)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
GEngine->AddOnScreenDebugMessage(-1, 0.0f, FColor::Orange,
|
||||||
|
FString::Printf(TEXT("Enemy State: %s | Distance: %.1f"), *StateString, DistanceToPlayer));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AEnemySpaceship::UpdateBehaviorState()
|
||||||
|
{
|
||||||
|
// Override random behavior changes when distance requires specific behavior
|
||||||
|
if (DistanceToPlayer < MinDistanceToPlayer)
|
||||||
|
{
|
||||||
|
// Too close, retreat or strafe
|
||||||
|
CurrentBehaviorState = FMath::RandBool() ? EEnemyBehaviorState::Retreat : EEnemyBehaviorState::Strafe;
|
||||||
|
}
|
||||||
|
else if (DistanceToPlayer < AttackRange)
|
||||||
|
{
|
||||||
|
// Within attack range but not too close, either attack or strafe
|
||||||
|
if (CurrentBehaviorState != EEnemyBehaviorState::Attack &&
|
||||||
|
CurrentBehaviorState != EEnemyBehaviorState::Strafe)
|
||||||
|
{
|
||||||
|
CurrentBehaviorState = FMath::RandBool() ? EEnemyBehaviorState::Attack : EEnemyBehaviorState::Strafe;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Too far, chase
|
||||||
|
CurrentBehaviorState = EEnemyBehaviorState::Chase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 Direction = (PlayerPawn->GetActorLocation() - GetActorLocation()).GetSafeNormal();
|
||||||
|
|
||||||
|
// Move towards the player
|
||||||
FVector NewLocation = GetActorLocation() + Direction * MovementSpeed * DeltaTime;
|
FVector NewLocation = GetActorLocation() + Direction * MovementSpeed * DeltaTime;
|
||||||
SetActorLocation(NewLocation);
|
SetActorLocation(NewLocation);
|
||||||
|
|
||||||
// Face towards the player
|
// Face towards the player
|
||||||
FRotator NewRotation = Direction.Rotation();
|
FRotator NewRotation = Direction.Rotation();
|
||||||
SetActorRotation(NewRotation);
|
SetActorRotation(NewRotation);
|
||||||
|
|
||||||
|
// If within attack range, fire occasionally
|
||||||
|
if (DistanceToPlayer < AttackRange && bCanFire && FMath::FRand() < 0.3f)
|
||||||
|
{
|
||||||
|
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();
|
||||||
|
SetActorRotation(NewRotation);
|
||||||
|
|
||||||
|
// Fire if ready
|
||||||
|
if (bCanFire)
|
||||||
|
{
|
||||||
|
Fire();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AEnemySpaceship::PerformRetreat(float DeltaTime)
|
||||||
|
{
|
||||||
|
if (PlayerPawn)
|
||||||
|
{
|
||||||
|
// Move away from player
|
||||||
|
FVector Direction = (GetActorLocation() - PlayerPawn->GetActorLocation()).GetSafeNormal();
|
||||||
|
FVector NewLocation = GetActorLocation() + Direction * MovementSpeed * DeltaTime;
|
||||||
|
SetActorLocation(NewLocation);
|
||||||
|
|
||||||
|
// Keep facing the player while backing up
|
||||||
|
FVector FaceDirection = (PlayerPawn->GetActorLocation() - GetActorLocation()).GetSafeNormal();
|
||||||
|
FRotator NewRotation = FaceDirection.Rotation();
|
||||||
|
SetActorRotation(NewRotation);
|
||||||
|
|
||||||
|
// Fire while retreating
|
||||||
|
if (bCanFire)
|
||||||
|
{
|
||||||
|
Fire();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AEnemySpaceship::PerformStrafe(float DeltaTime)
|
||||||
|
{
|
||||||
|
if (PlayerPawn)
|
||||||
|
{
|
||||||
|
// Calculate direction to player
|
||||||
|
FVector DirectionToPlayer = (PlayerPawn->GetActorLocation() - GetActorLocation()).GetSafeNormal();
|
||||||
|
|
||||||
|
// Calculate strafe direction (perpendicular to direction to player)
|
||||||
|
FVector StrafeVector = FVector::CrossProduct(DirectionToPlayer, FVector::UpVector) * StrafeDirection;
|
||||||
|
|
||||||
|
// Move sideways while maintaining distance
|
||||||
|
FVector NewLocation = GetActorLocation() + StrafeVector * StrafeSpeed * DeltaTime;
|
||||||
|
SetActorLocation(NewLocation);
|
||||||
|
|
||||||
|
// Face towards the player
|
||||||
|
FRotator NewRotation = DirectionToPlayer.Rotation();
|
||||||
|
SetActorRotation(NewRotation);
|
||||||
|
|
||||||
|
// Fire while strafing
|
||||||
|
if (bCanFire)
|
||||||
|
{
|
||||||
|
Fire();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AEnemySpaceship::Fire()
|
||||||
|
{
|
||||||
|
if (!ProjectileClass)
|
||||||
|
{
|
||||||
|
if (GEngine)
|
||||||
|
GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Red, TEXT("Enemy ProjectileClass not set!"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UWorld* World = GetWorld();
|
||||||
|
if (World)
|
||||||
|
{
|
||||||
|
FVector SpawnLocation = ProjectileSpawnPoint->GetComponentLocation();
|
||||||
|
FRotator SpawnRotation = GetActorRotation();
|
||||||
|
|
||||||
|
// Add slight randomness to rotation for less perfect aim
|
||||||
|
float RandPitch = FMath::RandRange(-5.0f, 5.0f);
|
||||||
|
float RandYaw = FMath::RandRange(-5.0f, 5.0f);
|
||||||
|
SpawnRotation.Pitch += RandPitch;
|
||||||
|
SpawnRotation.Yaw += RandYaw;
|
||||||
|
|
||||||
|
FActorSpawnParameters SpawnParams;
|
||||||
|
SpawnParams.Owner = this;
|
||||||
|
SpawnParams.Instigator = GetInstigator();
|
||||||
|
|
||||||
|
// Spawn the projectile using EnemyProjectile class
|
||||||
|
AEnemyProjectile* Projectile = World->SpawnActor<AEnemyProjectile>(
|
||||||
|
ProjectileClass,
|
||||||
|
SpawnLocation,
|
||||||
|
SpawnRotation,
|
||||||
|
SpawnParams
|
||||||
|
);
|
||||||
|
|
||||||
|
if (Projectile)
|
||||||
|
{
|
||||||
|
if (GEngine)
|
||||||
|
GEngine->AddOnScreenDebugMessage(-1, 0.5f, FColor::Yellow, TEXT("Enemy Fired!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start fire rate timer
|
||||||
|
bCanFire = false;
|
||||||
|
GetWorldTimerManager().SetTimer(FireTimerHandle, this, &AEnemySpaceship::ResetFire, FireRate, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AEnemySpaceship::ResetFire()
|
||||||
|
{
|
||||||
|
bCanFire = true;
|
||||||
|
}
|
||||||
|
|
||||||
float AEnemySpaceship::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent,
|
float AEnemySpaceship::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent,
|
||||||
AController* EventInstigator, AActor* DamageCauser)
|
AController* EventInstigator, AActor* DamageCauser)
|
||||||
@@ -58,7 +298,18 @@ 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));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 +322,21 @@ 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()
|
||||||
|
);
|
||||||
|
|
||||||
// Destroy the enemy
|
// Destroy the enemy
|
||||||
Destroy();
|
Destroy();
|
||||||
|
|||||||
@@ -4,6 +4,16 @@
|
|||||||
#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
|
||||||
|
};
|
||||||
|
|
||||||
UCLASS()
|
UCLASS()
|
||||||
class MYPROJECT3_API AEnemySpaceship : public AActor
|
class MYPROJECT3_API AEnemySpaceship : public AActor
|
||||||
{
|
{
|
||||||
@@ -18,8 +28,21 @@ 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;
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement")
|
||||||
float MovementSpeed = 300.0f;
|
float MovementSpeed = 500.0f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement")
|
||||||
|
float AttackRange = 1500.0f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement")
|
||||||
|
float MinDistanceToPlayer = 500.0f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement")
|
||||||
|
float StrafeSpeed = 300.0f;
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat")
|
||||||
float MaxHealth = 100.0f;
|
float MaxHealth = 100.0f;
|
||||||
@@ -27,6 +50,30 @@ 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;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
virtual void Tick(float DeltaTime) override;
|
virtual void Tick(float DeltaTime) override;
|
||||||
|
|
||||||
@@ -35,6 +82,32 @@ 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();
|
||||||
|
|
||||||
|
// AI behavior implementation functions
|
||||||
|
void PerformChase(float DeltaTime);
|
||||||
|
void PerformAttack(float DeltaTime);
|
||||||
|
void PerformRetreat(float DeltaTime);
|
||||||
|
void PerformStrafe(float DeltaTime);
|
||||||
};
|
};
|
||||||
@@ -156,15 +156,18 @@ void ASpaceShooterGameMode::SpawnEnemyWave()
|
|||||||
// Create a line of enemies perpendicular to the direction
|
// Create a line of enemies perpendicular to the direction
|
||||||
FVector2D PerpDirection(-EdgeDirection.Y, EdgeDirection.X);
|
FVector2D PerpDirection(-EdgeDirection.Y, EdgeDirection.X);
|
||||||
|
|
||||||
// Spawn wave of enemies
|
// Spawn wave of enemies - increased distance from 2000 to MinimumSpawnDistance
|
||||||
for (int32 i = 0; i < WaveSize; i++)
|
for (int32 i = 0; i < WaveSize; i++)
|
||||||
{
|
{
|
||||||
FVector SpawnLocation;
|
FVector ProposedLocation;
|
||||||
SpawnLocation.X = PlayerLocation.X + (EdgeDirection.X * 2000.0f) +
|
ProposedLocation.X = PlayerLocation.X + (EdgeDirection.X * MinimumSpawnDistance) +
|
||||||
(PerpDirection.X * (i - WaveSize / 2) * FormationSpacing);
|
(PerpDirection.X * (i - WaveSize / 2) * FormationSpacing);
|
||||||
SpawnLocation.Y = PlayerLocation.Y + (EdgeDirection.Y * 2000.0f) +
|
ProposedLocation.Y = PlayerLocation.Y + (EdgeDirection.Y * MinimumSpawnDistance) +
|
||||||
(PerpDirection.Y * (i - WaveSize / 2) * FormationSpacing);
|
(PerpDirection.Y * (i - WaveSize / 2) * FormationSpacing);
|
||||||
SpawnLocation.Z = PlayerLocation.Z;
|
ProposedLocation.Z = PlayerLocation.Z;
|
||||||
|
|
||||||
|
// Ensure the spawn location is far enough from the player
|
||||||
|
FVector SpawnLocation = EnsureMinimumSpawnDistance(ProposedLocation, PlayerLocation);
|
||||||
|
|
||||||
FRotator SpawnRotation = FRotator::ZeroRotator;
|
FRotator SpawnRotation = FRotator::ZeroRotator;
|
||||||
FActorSpawnParameters SpawnParams;
|
FActorSpawnParameters SpawnParams;
|
||||||
@@ -205,8 +208,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 +264,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)
|
||||||
{
|
{
|
||||||
@@ -307,7 +313,11 @@ void ASpaceShooterGameMode::SpawnEnemyFlanking()
|
|||||||
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));
|
FVector2D OffsetDir(FMath::Cos(OffsetAngle), FMath::Sin(OffsetAngle));
|
||||||
|
|
||||||
FVector SpawnLocation = PlayerLocation + FVector(OffsetDir.X, OffsetDir.Y, 0) * 2000.0f;
|
// Increased from 2000 to MinimumSpawnDistance
|
||||||
|
FVector ProposedLocation = PlayerLocation + FVector(OffsetDir.X, OffsetDir.Y, 0) * MinimumSpawnDistance;
|
||||||
|
|
||||||
|
// Ensure the spawn location is far enough from the player
|
||||||
|
FVector SpawnLocation = EnsureMinimumSpawnDistance(ProposedLocation, PlayerLocation);
|
||||||
|
|
||||||
FRotator SpawnRotation = FRotator::ZeroRotator;
|
FRotator SpawnRotation = FRotator::ZeroRotator;
|
||||||
FActorSpawnParameters SpawnParams;
|
FActorSpawnParameters SpawnParams;
|
||||||
@@ -339,30 +349,34 @@ FVector ASpaceShooterGameMode::GetScreenEdgeSpawnLocation()
|
|||||||
FVector SpawnLocation;
|
FVector SpawnLocation;
|
||||||
float RandomPos;
|
float RandomPos;
|
||||||
|
|
||||||
|
// Increased margin to spawn farther from screen edges
|
||||||
|
float ExtendedMargin = ScreenSpawnMargin + 500.0f;
|
||||||
|
|
||||||
switch (Edge)
|
switch (Edge)
|
||||||
{
|
{
|
||||||
case 0: // Top edge
|
case 0: // Top edge
|
||||||
RandomPos = FMath::RandRange(ScreenBounds[0].X, ScreenBounds[1].X);
|
RandomPos = FMath::RandRange(ScreenBounds[0].X, ScreenBounds[1].X);
|
||||||
SpawnLocation = FVector(RandomPos, ScreenBounds[0].Y - ScreenSpawnMargin, PlayerLocation.Z);
|
SpawnLocation = FVector(RandomPos, ScreenBounds[0].Y - ExtendedMargin, PlayerLocation.Z);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 1: // Right edge
|
case 1: // Right edge
|
||||||
RandomPos = FMath::RandRange(ScreenBounds[0].Y, ScreenBounds[1].Y);
|
RandomPos = FMath::RandRange(ScreenBounds[0].Y, ScreenBounds[1].Y);
|
||||||
SpawnLocation = FVector(ScreenBounds[1].X + ScreenSpawnMargin, RandomPos, PlayerLocation.Z);
|
SpawnLocation = FVector(ScreenBounds[1].X + ExtendedMargin, RandomPos, PlayerLocation.Z);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 2: // Bottom edge
|
case 2: // Bottom edge
|
||||||
RandomPos = FMath::RandRange(ScreenBounds[0].X, ScreenBounds[1].X);
|
RandomPos = FMath::RandRange(ScreenBounds[0].X, ScreenBounds[1].X);
|
||||||
SpawnLocation = FVector(RandomPos, ScreenBounds[1].Y + ScreenSpawnMargin, PlayerLocation.Z);
|
SpawnLocation = FVector(RandomPos, ScreenBounds[1].Y + ExtendedMargin, PlayerLocation.Z);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 3: // Left edge
|
case 3: // Left edge
|
||||||
RandomPos = FMath::RandRange(ScreenBounds[0].Y, ScreenBounds[1].Y);
|
RandomPos = FMath::RandRange(ScreenBounds[0].Y, ScreenBounds[1].Y);
|
||||||
SpawnLocation = FVector(ScreenBounds[0].X - ScreenSpawnMargin, RandomPos, PlayerLocation.Z);
|
SpawnLocation = FVector(ScreenBounds[0].X - ExtendedMargin, RandomPos, PlayerLocation.Z);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return SpawnLocation;
|
// Ensure the spawn location is far enough from the player
|
||||||
|
return EnsureMinimumSpawnDistance(SpawnLocation, PlayerLocation);
|
||||||
}
|
}
|
||||||
|
|
||||||
FVector ASpaceShooterGameMode::GetSpawnZoneLocation()
|
FVector ASpaceShooterGameMode::GetSpawnZoneLocation()
|
||||||
@@ -395,6 +409,7 @@ FVector ASpaceShooterGameMode::GetSpawnZoneLocation()
|
|||||||
// Select a zone based on weight
|
// Select a zone based on weight
|
||||||
float RandomWeight = FMath::RandRange(0.0f, TotalWeight);
|
float RandomWeight = FMath::RandRange(0.0f, TotalWeight);
|
||||||
float WeightSum = 0.0f;
|
float WeightSum = 0.0f;
|
||||||
|
FVector PlayerLocation = GetPlayerLocation();
|
||||||
|
|
||||||
for (const FSpawnZone& Zone : ActiveZones)
|
for (const FSpawnZone& Zone : ActiveZones)
|
||||||
{
|
{
|
||||||
@@ -411,7 +426,10 @@ FVector ASpaceShooterGameMode::GetSpawnZoneLocation()
|
|||||||
0.0f
|
0.0f
|
||||||
);
|
);
|
||||||
|
|
||||||
return Zone.Location + SpawnOffset;
|
FVector ProposedLocation = Zone.Location + SpawnOffset;
|
||||||
|
|
||||||
|
// Ensure the spawn location is far enough from the player
|
||||||
|
return EnsureMinimumSpawnDistance(ProposedLocation, PlayerLocation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -505,3 +523,21 @@ 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;
|
||||||
|
}
|
||||||
@@ -55,7 +55,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;
|
||||||
@@ -93,4 +96,7 @@ private:
|
|||||||
FVector GetPlayerLocation();
|
FVector GetPlayerLocation();
|
||||||
TArray<FVector2D> GetScreenBounds();
|
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);
|
||||||
};
|
};
|
||||||
@@ -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,11 @@ void ASpaceshipPawn::BeginPlay()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize health and shield values
|
||||||
|
CurrentHealth = MaxHealth;
|
||||||
|
CurrentShield = MaxShield;
|
||||||
|
LastDamageTime = 0.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ASpaceshipPawn::Tick(float DeltaTime)
|
void ASpaceshipPawn::Tick(float DeltaTime)
|
||||||
@@ -201,7 +208,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 +402,197 @@ 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::ApplyDamageFlash()
|
||||||
|
{
|
||||||
|
// Apply material flash effect to indicate damage
|
||||||
|
if (ShipMesh && ShipMesh->GetMaterial(0))
|
||||||
|
{
|
||||||
|
UMaterialInstanceDynamic* DynamicMaterial = UMaterialInstanceDynamic::Create(ShipMesh->GetMaterial(0), this);
|
||||||
|
if (DynamicMaterial)
|
||||||
|
{
|
||||||
|
// Assuming the material has a parameter named "FlashColor"
|
||||||
|
DynamicMaterial->SetVectorParameterValue("FlashColor", DamageFlashColor);
|
||||||
|
ShipMesh->SetMaterial(0, DynamicMaterial);
|
||||||
|
|
||||||
|
// Set timer to reset the flash
|
||||||
|
GetWorldTimerManager().SetTimer(
|
||||||
|
DamageFlashTimerHandle,
|
||||||
|
this,
|
||||||
|
&ASpaceshipPawn::ResetDamageFlash,
|
||||||
|
DamageFlashDuration,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ASpaceshipPawn::ResetDamageFlash()
|
||||||
|
{
|
||||||
|
// Reset material flash effect
|
||||||
|
if (ShipMesh && ShipMesh->GetMaterial(0))
|
||||||
|
{
|
||||||
|
UMaterialInstanceDynamic* DynamicMaterial = Cast<UMaterialInstanceDynamic>(ShipMesh->GetMaterial(0));
|
||||||
|
if (DynamicMaterial)
|
||||||
|
{
|
||||||
|
// Reset the flash color (assuming transparent black means no flash)
|
||||||
|
DynamicMaterial->SetVectorParameterValue("FlashColor", FLinearColor(0.0f, 0.0f, 0.0f, 0.0f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// You could either restart the level after a delay or show a game over screen here
|
||||||
|
// For example:
|
||||||
|
// GetWorldTimerManager().SetTimer(RestartTimerHandle, this, &ASpaceshipPawn::RestartLevel, 3.0f, false);
|
||||||
|
|
||||||
|
// For now, just log the death
|
||||||
|
if (GEngine)
|
||||||
|
{
|
||||||
|
GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Red, TEXT("PLAYER DIED!"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,18 @@ 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; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual void BeginPlay() override;
|
virtual void BeginPlay() override;
|
||||||
|
|
||||||
@@ -48,12 +60,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 +106,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 +116,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 +127,43 @@ 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")
|
||||||
|
float MaxHealth = 100.0f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat")
|
||||||
|
float CurrentHealth = 100.0f;
|
||||||
|
|
||||||
|
// Shield properties
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat")
|
||||||
|
float MaxShield = 100.0f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat")
|
||||||
|
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);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Movement state
|
// Movement state
|
||||||
float CurrentThrottleInput;
|
float CurrentThrottleInput;
|
||||||
@@ -143,4 +192,17 @@ 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;
|
||||||
|
|
||||||
|
void StartShieldRecharge();
|
||||||
|
void RechargeShield();
|
||||||
|
bool IsDead() const { return CurrentHealth <= 0.0f; }
|
||||||
|
void Die();
|
||||||
};
|
};
|
||||||
@@ -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