#include "EnemySpaceship.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() { PrimaryActorTick.bCanEverTick = true; // Create and setup the enemy mesh EnemyMesh = CreateDefaultSubobject(TEXT("EnemyMesh")); RootComponent = EnemyMesh; // Create projectile spawn point ProjectileSpawnPoint = CreateDefaultSubobject(TEXT("ProjectileSpawnPoint")); ProjectileSpawnPoint->SetupAttachment(EnemyMesh); ProjectileSpawnPoint->SetRelativeLocation(FVector(100.0f, 0.0f, 0.0f)); // Forward of the ship // Disable gravity and physics simulation EnemyMesh->SetSimulatePhysics(false); EnemyMesh->SetEnableGravity(false); // Set up collision for the enemy mesh EnemyMesh->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics); EnemyMesh->SetCollisionObjectType(ECC_Pawn); EnemyMesh->SetCollisionResponseToAllChannels(ECR_Block); EnemyMesh->SetCollisionResponseToChannel(ECC_Pawn, ECR_Ignore); // Ignore other pawns EnemyMesh->SetCollisionResponseToChannel(ECC_GameTraceChannel1, ECR_Block); // Block player (for damage) EnemyMesh->SetGenerateOverlapEvents(true); CurrentVelocity = FVector::ZeroVector; TargetVelocity = FVector::ZeroVector; LastPosition = FVector::ZeroVector; } void AEnemySpaceship::BeginPlay() { Super::BeginPlay(); // Find the player pawn and cast to SpaceshipPawn AActor* FoundPlayerPawn = UGameplayStatics::GetPlayerPawn(GetWorld(), 0); PlayerPawn = Cast(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) { Super::Tick(DeltaTime); if (!PlayerPawn) { AActor* FoundPlayerPawn = UGameplayStatics::GetPlayerPawn(GetWorld(), 0); PlayerPawn = Cast(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 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(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) { // Calculate direction to player FVector DirectionToPlayer = (PlayerPawn->GetActorLocation() - GetActorLocation()).GetSafeNormal(); // 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(); 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( 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)); } } float AEnemySpaceship::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) { float DamageToApply = Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser); CurrentHealth -= DamageToApply; // Debug message if (GEngine) { GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Red, 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) { Die(); } return DamageToApply; } void AEnemySpaceship::Die() { // 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(UGameplayStatics::GetGameMode(GetWorld()))) { GameMode->IncrementKillCount(); } // Destroy the enemy Destroy(); }