Combat mechanics, Level design and an intro Cutscene

* Added a cinematic on level start that shows the way ahead
* The HUD and Player character are hidden and input disabled during
the cutscene. Then reenabled after.
* Various level design changes.
* Added a damage multiplier to weapon rarities
that's taken into account.
* Changed stun chance calculation to make it depend on
the damage inflicted and enemy health. To avoid constant stuns
with the SMG.
* Fixed input still working after player death.
* Explosive barrels now make other barrels explode in chain.
This commit is contained in:
charnet3d 2025-01-16 03:23:38 +01:00
parent e4ed6c309a
commit 08aff94716
39 changed files with 177 additions and 93 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
Content/_Game/HUD/ShooterHUDBP.uasset (Stored with Git LFS)

Binary file not shown.

BIN
Content/_Game/Health/HealthBP.uasset (Stored with Git LFS)

Binary file not shown.

BIN
Content/_Game/Sequences/BPI_LevelSequenceLink.uasset (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Content/_Game/Sequences/LevelStart.uasset (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Content/_Game/Sequences/TestLevelSequence.uasset (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

View File

@ -30,7 +30,7 @@ AEnemy::AEnemy() :
bCanHitReact(true),
HitNumberDestroyTime(1.5f),
bStunned(false),
StunChance(.5f),
StunChance(0.8f),
AttackLFast(TEXT("AttackLFast")),
AttackRFast(TEXT("AttackRFast")),
AttackL(TEXT("AttackL")),
@ -235,7 +235,7 @@ void AEnemy::CombatRangeOverlap(UPrimitiveComponent* OverlappedComponent, AActor
if (!EnemyController) return;
EnemyController->GetBlackboardComponent()->SetValueAsBool(TEXT("InAttackRange"), true);
//UE_LOG(LogTemp, Warning, TEXT("overlap player"));
//UE_LOG(LogTemp, Warning, TEXT("overlap player: %d"), static_cast<int>(bInAttackRange));
}
void AEnemy::CombatRangeEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
@ -427,10 +427,13 @@ float AEnemy::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AC
if (bDying) return DamageInflicted;
ShowHealthBar();
// Determine actual Stun Chance based on health and damage inflicted
float ActualStunChance = DamageInflicted / (Health / 10.f) * StunChance;
// Determine whether bullet hit stuns
const float Stunned = FMath::FRandRange(0.f, 1.f);
if (Stunned <= StunChance)
if (Stunned <= ActualStunChance)
{
// Stun the Enemy
PlayHitMontage(FName("HitReactFront"));

View File

@ -15,7 +15,8 @@
// Sets default values
AExplosive::AExplosive() :
Damage(40.f)
Damage(40.f),
Exploded(false)
{
// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;
@ -41,27 +42,53 @@ void AExplosive::Tick(float DeltaTime)
}
void AExplosive::BulletHit_Implementation(FHitResult HitResult, AActor* Shooter, AController* ShooterController)
void AExplosive::Explode(AActor* Shooter, AController* ShooterController)
{
if (ExplosionSound)
{
UGameplayStatics::PlaySoundAtLocation(this, ExplosionSound, GetActorLocation());
}
Exploded = true;
if (ExplosionSound)
{
UGameplayStatics::PlaySoundAtLocation(this, ExplosionSound, GetActorLocation());
}
if (ExplosionParticles)
{
UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), ExplosionParticles, HitResult.Location, FRotator(0.0), true);
}
if (ExplosionParticles)
{
UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), ExplosionParticles, GetActorLocation(), FRotator(0.0), true);
}
// Apply explosive damage
TArray<AActor*> OverlappingActors;
GetOverlappingActors(OverlappingActors, ACharacter::StaticClass());
// Apply explosive damage
TArray<AActor*> OverlappingActors;
GetOverlappingActors(OverlappingActors, ACharacter::StaticClass());
for (auto Actor : OverlappingActors)
{
UGameplayStatics::ApplyDamage(Actor, Damage, ShooterController, Shooter, UDamageType::StaticClass());
}
Destroy();
for (auto Actor : OverlappingActors)
{
UGameplayStatics::ApplyDamage(Actor, Damage, ShooterController, Shooter, UDamageType::StaticClass());
}
// Other explosives
OverlappingActors.Empty();
GetOverlappingActors(OverlappingActors, AExplosive::StaticClass());
for (auto Actor : OverlappingActors)
{
AExplosive* Explosive = Cast<AExplosive>(Actor);
if (Explosive && Explosive != this && !Explosive->Exploded)
UGameplayStatics::ApplyDamage(Actor, Damage, ShooterController, Shooter, UDamageType::StaticClass());
}
Destroy();
}
void AExplosive::BulletHit_Implementation(FHitResult HitResult, AActor* Shooter, AController* ShooterController)
{
Explode(Shooter, ShooterController);
}
float AExplosive::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator,
AActor* DamageCauser)
{
Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);
Explode(DamageCauser, EventInstigator);
return DamageAmount;
}

View File

@ -24,7 +24,8 @@ public:
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
void Explode(AActor* Shooter, AController* ShooterController);
private:
/** Particles to spawn when hit by bullets */
@ -46,9 +47,14 @@ private:
/** Damage amount for explosive */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Combat, meta = (AllowPrivateAccess = true))
float Damage;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Combat, meta = (AllowPrivateAccess = true))
bool Exploded;
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
virtual void BulletHit_Implementation(FHitResult HitResult, AActor* Shooter, AController* ShooterController) override;
virtual float TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) override;
};

View File

@ -210,6 +210,7 @@ void AItem::OnConstruction(const FTransform& Transform)
{
GetItemMesh()->SetCustomDepthStencilValue(RarityRow->CustomDepthStencil);
}
DamageMultiplier = RarityRow->DamageMultiplier;
if (MaterialInstance)
{

View File

@ -35,7 +35,7 @@ enum class EItemRarity : uint8
UENUM(BlueprintType)
enum class EItemState : uint8
{
EIS_Pickup UMETA(DisplayName = "Damaged"),
EIS_Pickup UMETA(DisplayName = "Pickup"),
EIS_EquipInterping UMETA(DisplayName = "EquipInterping"),
EIS_PickedUp UMETA(DisplayName = "PickedUp"),
EIS_Equipped UMETA(DisplayName = "Equipped"),
@ -75,6 +75,9 @@ struct FItemRarityTable : public FTableRowBase
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 CustomDepthStencil;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
float DamageMultiplier;
};
UCLASS()
@ -305,6 +308,10 @@ private:
/** Background icon for the inventory */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Rarity, meta = (AllowPrivateAccess = true))
UTexture2D* IconBackground;
/** Damage multiplier applied on the weapon damage */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Rarity, meta = (AllowPrivateAccess = true))
float DamageMultiplier;
public:
FORCEINLINE UWidgetComponent* GetPickupWidget() const { return PickupWidget; }
@ -345,10 +352,12 @@ public:
FORCEINLINE void SetDynamicMaterialInstance(UMaterialInstanceDynamic* Instance) { DynamicMaterialInstance = Instance; }
FORCEINLINE FLinearColor GetGlowColor() const { return GlowColor; }
FORCEINLINE void SetMaterialIndex(int32 Index) { MaterialIndex = Index; }
FORCEINLINE int32 GetMaterialIndex() const { return MaterialIndex; }
FORCEINLINE float GetDamageMultiplier() const { return DamageMultiplier; }
/** Called from the AShooterCharacter class */
void StartItemCurve(AShooterCharacter* Char, bool bForcePlaySound = false);

View File

@ -61,8 +61,8 @@ AShooterCharacter::AShooterCharacter() :
CameraInterpDistance(150.f),
CameraInterpElevation(55.f),
// Starting ammo amounts
Starting9mmAmmo(85),
StartingARAmmo(120),
Starting9mmAmmo(15),
StartingARAmmo(0),
// Combat variables
CombatState(ECombatState::ECS_Unoccupied),
bCrouching(false),
@ -238,6 +238,12 @@ void AShooterCharacter::Die()
if (bDying) return;
bDying = true;
APlayerController* PC = UGameplayStatics::GetPlayerController(this, 0);
if (PC)
{
DisableInput(PC);
}
if (!DeathMontage) return;
UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
@ -249,11 +255,6 @@ void AShooterCharacter::Die()
void AShooterCharacter::FinishDeath()
{
GetMesh()->bPauseAnims = true;
APlayerController* PC = UGameplayStatics::GetPlayerController(this, 0);
if (PC)
{
DisableInput(PC);
}
}
// Called every frame
@ -691,6 +692,7 @@ void AShooterCharacter::FireWeapon()
if (!WeaponHasAmmo())
{
ReloadWeapon();
return;
}
PlayFireSound();
@ -788,12 +790,12 @@ void AShooterCharacter::SendBullet()
float damageToApply;
if (BeamHitResult.BoneName.ToString() == HitEnemy->GetHeadBone())
{
damageToApply = EquippedWeapon->GetHeadshotDamage();
damageToApply = EquippedWeapon->GetHeadshotDamage() * EquippedWeapon->GetDamageMultiplier();
HitEnemy->ShowHitNumber(damageToApply, BeamHitResult.Location, true);
}
else
{
damageToApply = EquippedWeapon->GetDamage();
damageToApply = EquippedWeapon->GetDamage() * EquippedWeapon->GetDamageMultiplier();
HitEnemy->ShowHitNumber(damageToApply, BeamHitResult.Location, false);
}

View File

@ -21,7 +21,33 @@ void AShooterPlayerController::BeginPlay()
if (HUDOverlay)
{
HUDOverlay->AddToViewport();
HUDOverlay->SetVisibility(ESlateVisibility::Visible);
HUDOverlay->SetVisibility(ESlateVisibility::Hidden);
}
}
}
void AShooterPlayerController::GameHasEnded(AActor* EndGameFocus, bool bIsWinner)
{
Super::GameHasEnded(EndGameFocus, bIsWinner);
HUDOverlay->RemoveFromParent();
/*if (bIsWinner)
{
UUserWidget* WinScreen = CreateWidget(this, WinScreenClass);
if (WinScreen != nullptr)
{
WinScreen->AddToViewport();
}
}
else
{
UUserWidget* LoseScreen = CreateWidget(this, LoseScreenClass);
if (LoseScreen != nullptr)
{
LoseScreen->AddToViewport();
}
}
GetWorldTimerManager().SetTimer(RestartTimer, this, &APlayerController::RestartLevel, RestartDelay);*/
}

View File

@ -17,7 +17,8 @@ class SHOOTER_API AShooterPlayerController : public APlayerController
GENERATED_BODY()
public:
AShooterPlayerController();
virtual void GameHasEnded(class AActor* EndGameFocus = nullptr, bool bIsWinner = false) override;
protected:
virtual void BeginPlay() override;