This commit is contained in:
2025-07-10 01:02:24 -07:00
parent e7989cfe0a
commit 989c3afe3a
30 changed files with 5464 additions and 760 deletions
-10
View File
@@ -1,10 +0,0 @@
{
"permissions": {
"allow": [
"Bash(find:*)",
"Bash(grep:*)",
"Bash(rg:*)"
],
"deny": []
}
}
+26
View File
@@ -2,3 +2,29 @@
Binaries/ Binaries/
Intermediate/ Intermediate/
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
dev-debug.log
# Dependency directories
node_modules/
# Environment variables
.env
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# OS specific
.DS_Store
# Task files
tasks.json
tasks/
+25
View File
@@ -32,6 +32,31 @@ MyGun->PullTrigger();
- Automatic configuration from weapon assets - Automatic configuration from weapon assets
- Full multiplayer support - Full multiplayer support
- Event-driven architecture - Event-driven architecture
- Support for both skeletal and static mesh guns
- Flexible muzzle positioning (socket-based for skeletal, transform-based for static)
### Mesh Configuration
The gun actor supports both skeletal and static mesh setups:
**Skeletal Mesh Mode (Default):**
```cpp
// Set gun to use skeletal mesh (default)
MyGun->SetMeshType(true);
MyGun->SetMuzzleSocketName("MuzzleFlash"); // Use socket for muzzle position
```
**Static Mesh Mode:**
```cpp
// Set gun to use static mesh
MyGun->SetMeshType(false);
MyGun->MuzzleTransform = FTransform(FVector(50, 0, 0)); // Set relative muzzle position
```
**Key Differences:**
- Skeletal meshes use sockets for precise muzzle positioning and can have animations
- Static meshes use relative transforms and are more performance-friendly for simple weapons
- Both modes maintain full compatibility with the firing system
### Magazine Component (UEBMagazine) ### Magazine Component (UEBMagazine)
@@ -12,7 +12,10 @@ public class EasyBallistics : ModuleRules
PublicDependencyModuleNames.AddRange( PublicDependencyModuleNames.AddRange(
new string[] new string[]
{ {
"Core" "Core",
"Niagara",
"NiagaraCore",
"NiagaraShader"
// ... add other public dependencies that you statically link with here ... // ... add other public dependencies that you statically link with here ...
} }
); );
@@ -3,111 +3,6 @@
#include "EBBarrel.h" #include "EBBarrel.h"
#include "Net/UnrealNetwork.h" #include "Net/UnrealNetwork.h"
#define REPOWNERONLY false
void UEBBarrel::ShotFiredMulticast_Implementation() { void UEBBarrel::ShotFiredMulticast_Implementation() {
ShotFired.Broadcast(); ShotFired.Broadcast();
}
void UEBBarrel::Shoot(bool Trigger) {
if (ClientSideAim && GetOwner()->GetRemoteRole() == ROLE_Authority && Trigger) {
Aim = GetComponentTransform().GetUnitAxis(EAxis::X);
Location = GetComponentTransform().GetLocation();
ShootRepCSA(Trigger, UGameplayStatics::RebaseLocalOriginOntoZero(GetWorld(), Location), Aim);
}
else {
ShootRep(Trigger);
}
}
void UEBBarrel::ShootRep_Implementation(bool Trigger) {
if (Trigger) {
if (FireMode == EFireMode::FM_Burst || FireMode == EFireMode::FM_InterBurst) {
BurstRemaining = BurstCount;
};
Shooting = true;
}
else {
//burst cannot be interrupted
if (FireMode != EFireMode::FM_Burst || BurstRemaining<=0) {
Shooting = false;
}
}
}
bool UEBBarrel::ShootRep_Validate(bool Trigger) {
return true;
}
void UEBBarrel::ShootRepCSA_Implementation(bool Trigger, FVector_NetQuantize NewLocation, FVector_NetQuantizeNormal NewAim) {
Location = UGameplayStatics::RebaseZeroOriginOntoLocal(GetWorld(), NewLocation);
Aim = NewAim;
RemoteAimReceived = true;
if (Trigger) {
if (FireMode == EFireMode::FM_Burst || FireMode == EFireMode::FM_InterBurst) {
BurstRemaining = BurstCount;
};
Shooting = true;
}
else {
//burst cannot be interrupted
if (FireMode != EFireMode::FM_Burst || BurstRemaining <= 0) {
Shooting = false;
}
}
}
bool UEBBarrel::ShootRepCSA_Validate(bool Trigger, FVector_NetQuantize NewLocation, FVector_NetQuantizeNormal NewAim) {
return true;
}
void UEBBarrel::GatlingSpool_Implementation(bool Spool) {
Spooling = Spool;
}
bool UEBBarrel::GatlingSpool_Validate(bool Spool) {
return true;
}
void UEBBarrel::SwitchFireMode_Implementation(EFireMode NewFireMode) {
FireMode = NewFireMode;
}
bool UEBBarrel::SwitchFireMode_Validate(EFireMode NewFireMode) {
return true;
}
void UEBBarrel::ClientAim_Implementation(FVector_NetQuantize NewLocation, FVector_NetQuantizeNormal NewAim) {
Location = UGameplayStatics::RebaseZeroOriginOntoLocal(GetWorld(),NewLocation);
Aim = NewAim;
RemoteAimReceived = true;
}
bool UEBBarrel::ClientAim_Validate(FVector_NetQuantize NewLocation, FVector_NetQuantizeNormal NewAim) {
return true;
}
void UEBBarrel::GetLifetimeReplicatedProps(TArray< FLifetimeProperty > & OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
#if REPOWNERONLY
DOREPLIFETIME_CONDITION(UEBBarrel, FireMode, COND_OwnerOnly);
DOREPLIFETIME_CONDITION(UEBBarrel, CycleAmmoCount, COND_OwnerOnly);
DOREPLIFETIME_CONDITION(UEBBarrel, CycleAmmoPos, COND_OwnerOnly);
DOREPLIFETIME_CONDITION(UEBBarrel, Ammo, COND_OwnerOnly);
DOREPLIFETIME_CONDITION(UEBBarrel, ChamberedBullet, COND_OwnerOnly);
DOREPLIFETIME_CONDITION(UEBBarrel, Shooting, COND_OwnerOnly);
DOREPLIFETIME_CONDITION(UEBBarrel, ShootingBlocked, COND_OwnerOnly);
DOREPLIFETIME_CONDITION(UEBBarrel, Spooling, COND_OwnerOnly);
#else
DOREPLIFETIME(UEBBarrel, FireMode);
DOREPLIFETIME(UEBBarrel, CycleAmmoCount);
DOREPLIFETIME(UEBBarrel, CycleAmmoPos);
DOREPLIFETIME(UEBBarrel, Ammo);
DOREPLIFETIME(UEBBarrel, ChamberedBullet);
DOREPLIFETIME(UEBBarrel, Shooting);
DOREPLIFETIME(UEBBarrel, ShootingBlocked);
DOREPLIFETIME(UEBBarrel, Spooling);
#endif
} }
+112 -210
View File
@@ -14,227 +14,40 @@ UEBBarrel::UEBBarrel() {
SetIsReplicatedByDefault(ReplicateVariables); SetIsReplicatedByDefault(ReplicateVariables);
RandomStream.GenerateNewSeed(); RandomStream.GenerateNewSeed();
GatlingRPS = FireRateMin;
} }
void UEBBarrel::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) void UEBBarrel::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{ {
Super::TickComponent(DeltaTime, TickType, ThisTickFunction); Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
if (ClientSideAim){ // The barrel's tick function is now primarily for debug displays or other non-firing logic.
if (GetOwner()->GetRemoteRole()==ROLE_Authority){ // All firing state, cooldowns, and ammo management has been moved to AEBGun.
TimeSinceAimUpdate += DeltaTime;
if (TimeSinceAimUpdate >= 1.0f / ClientAimUpdateFrequency) {
Aim = GetComponentTransform().GetUnitAxis(EAxis::X);
Location = GetComponentTransform().GetLocation();
ClientAim(UGameplayStatics::RebaseLocalOriginOntoZero(GetWorld(),Location), Aim);
TimeSinceAimUpdate = FMath::Fmod(TimeSinceAimUpdate, 1.0f / ClientAimUpdateFrequency);
};
}else{
if (!RemoteAimReceived) {
Aim = GetComponentTransform().GetUnitAxis(EAxis::X);
Location = GetComponentTransform().GetLocation();
}
else {
FVector LocOffset = (Location - GetComponentLocation());
if (LocOffset.Size() > ClientAimDistanceLimit) {
//lag or cheater???
Location = GetComponentLocation() + LocOffset.GetSafeNormal()*ClientAimDistanceLimit;
}
}
}
}
else {
Aim = GetComponentTransform().GetUnitAxis(EAxis::X);
Location = GetComponentTransform().GetLocation();
}
//Only server can tick
if (GetOwner()->GetLocalRole() == ROLE_Authority){
float RemainingDelta;
if (FireMode == EFireMode::FM_Gatling) {
if (Spooling || (GatlingAutoSpool && Shooting)) {
GatlingRPS = FMath::Lerp(GatlingRPS, FireRateMax, FMath::Min(GatlingSpoolUpTime*DeltaTime, 1.0f));
}
else {
GatlingRPS = FMath::Lerp(GatlingRPS, FireRateMin, FMath::Min(GatlingSpoolUpTime*DeltaTime, 1.0f));
}
GatlingPhase += GatlingRPS*DeltaTime;
for (int i = 1; i <= GatlingPhase; i++) {
if (Cooldown <= 0.0f && LoadNext) {
NextBullet();
}
if (Shooting && ChamberedBullet != nullptr && (!ShootingBlocked)) {
SpawnBullet(GetOwner(), Location, Aim);
}
}
GatlingPhase = FMath::Fmod(GatlingPhase, 1.0f);
}
else {
RemainingDelta = DeltaTime;
do {
float step = FMath::Min(Cooldown, RemainingDelta);
Cooldown -= step;
RemainingDelta -= step;
if (Cooldown <= 0.0f && LoadNext) {
NextBullet();
}
//shoot when ready
if (Shooting && ChamberedBullet != nullptr && (!ShootingBlocked)) {
if (BurstRemaining > 0 || (FireMode != EFireMode::FM_Burst && FireMode != EFireMode::FM_InterBurst)) {
SpawnBullet(GetOwner(), Location, Aim);
}
else {
Shooting = false;
}
}
} while (RemainingDelta > 0 && Cooldown > 0);
}
}
} }
void UEBBarrel::NextBullet() { bool UEBBarrel::ClientAim_Validate(FVector_NetQuantize NewLocation, FVector_NetQuantizeNormal NewAim) {
if (ChamberedBullet == nullptr) { return true;
if (Ammo.Num() > 0 && (CycleAmmoCount > 0 || CycleAmmoUnlimited || (!CycleAmmo))) { }
void UEBBarrel::ClientAim_Implementation(FVector_NetQuantize NewLocation, FVector_NetQuantizeNormal NewAim) {
//cycle ammo Location = NewLocation;
if (CycleAmmo) { Aim = NewAim;
if (CycleAmmoPos >= Ammo.Num()) { CycleAmmoPos = 0; } TimeSinceAimUpdate = 0.0f;
ChamberedBullet = Ammo[CycleAmmoPos]; RemoteAimReceived = true;
CycleAmmoPos++;
if (!CycleAmmoUnlimited) {
CycleAmmoCount--;
}
}
else {
ChamberedBullet = Ammo[0];
Ammo.RemoveAt(0, 1, EAllowShrinking::Yes);
}
ReadyToShoot.Broadcast();
}
else {
AmmoDepleted.Broadcast();
}
}
} }
void UEBBarrel::SpawnBullet(AActor* Owner, FVector InLocation, FVector InAim) { bool UEBBarrel::ShootRep_Validate(bool Trigger) {
TSubclassOf<class AEBBullet> BulletClass = ChamberedBullet; return true;
}
void UEBBarrel::ShootRep_Implementation(bool Trigger) {
// Legacy function, logic moved to AEBGun.
// This can be left empty or used for simple replication effects.
}
if (BulletClass != nullptr) { bool UEBBarrel::ShootRepCSA_Validate(bool Trigger, FVector_NetQuantize NewLocation, FVector_NetQuantizeNormal NewAim) {
FVector OutLocation; return true;
FVector OutAim; }
void UEBBarrel::ShootRepCSA_Implementation(bool Trigger, FVector_NetQuantize NewLocation, FVector_NetQuantizeNormal NewAim) {
InitialBulletTransform(InLocation, InAim, OutLocation, OutAim); ClientAim_Implementation(NewLocation, NewAim);
ShootRep_Implementation(Trigger);
AEBBullet* Default = Cast<AEBBullet>(BulletClass->GetDefaultObject());
float BulletSpread = Default->Spread;
if (Default->SpreadBias > 0.0f) {
float SpreadMult = FMath::Pow(FMath::FRand(), Default->SpreadBias);
BulletSpread *= SpreadMult;
}
float BarrelSpread = Spread;
if (SpreadBias > 0.0f) {
float SpreadMult = FMath::Pow(FMath::FRand(), SpreadBias);
BarrelSpread *= SpreadMult;
}
float TotalSpread = BulletSpread+BarrelSpread;
OutAim = RandomStream.VRandCone(OutAim,TotalSpread);
float BulletVelocity = FMath::Lerp(MuzzleVelocityMultiplierMin* Default->MuzzleVelocityMin, MuzzleVelocityMultiplierMax*Default->MuzzleVelocityMax, RandomStream.FRand());
FVector Velocity = OutAim*BulletVelocity;
//get parent physics body
UPrimitiveComponent* parent = Cast<UPrimitiveComponent>(GetAttachParent());
Velocity += AdditionalVelocity;
if (parent != nullptr) {
if (parent->IsSimulatingPhysics()) {
Velocity += parent->GetPhysicsLinearVelocityAtPoint(OutLocation)*InheritVelocity;
}
if (Default->Shotgun) {
ApplyRecoil(parent, OutLocation, -Velocity*Default->Mass*RecoilMultiplier*Default->ShotCount);
}
else{
ApplyRecoil(parent, OutLocation, -Velocity*Default->Mass*RecoilMultiplier);
}
}
BeforeShotFired.Broadcast();
AEBBullet::SpawnWithExactVelocityFromBarrel(BulletClass, Owner, Owner->GetInstigator(), OutLocation, Velocity, this);
//spend ammo
ChamberedBullet = nullptr;
if (FireMode != EFireMode::FM_Gatling) {
Cooldown = 1.0f / FMath::Lerp(FireRateMin, FireRateMax, RandomStream.FRand());
}
//fire modes
switch (FireMode) {
case EFireMode::FM_Auto:
LoadNext = true;
break;
case EFireMode::FM_Burst:
LoadNext = true;
break;
case EFireMode::FM_InterBurst:
LoadNext = true;
break;
case EFireMode::FM_Semiauto:
Shooting = false;
LoadNext = true;
break;
case EFireMode::FM_Manual:
Shooting = false;
LoadNext = false;
break;
case EFireMode::FM_Slamfire:
LoadNext = false;
break;
case EFireMode::FM_Gatling:
LoadNext = true;
break;
};
if (BurstRemaining > 0) {
BurstRemaining--;
}
else {
if (FireMode == EFireMode::FM_Burst || FireMode == EFireMode::FM_InterBurst) {
Cooldown = FMath::Max(Cooldown, BurstCooldown);
}
}
if (ReplicateShotFiredEvents) {
ShotFiredMulticast();
}
else {
ShotFired.Broadcast();
}
}
} }
void UEBBarrel::InitialBulletTransform_Implementation(FVector InLocation, FVector InDirection, FVector& OutLocation, FVector& OutDirection) { void UEBBarrel::InitialBulletTransform_Implementation(FVector InLocation, FVector InDirection, FVector& OutLocation, FVector& OutDirection) {
@@ -477,4 +290,93 @@ bool UEBBarrel::CanFireFromGun() const
return ParentGun->CanFire(); return ParentGun->CanFire();
} }
return false; return false;
}
// ===============================
// Refactored Single-Shot Helper
// ===============================
void UEBBarrel::FireBullet(TSubclassOf<class AEBBullet> BulletClass)
{
// Validate input
if (!BulletClass)
{
UE_LOG(LogTemp, Warning, TEXT("UEBBarrel::FireBullet called with invalid BulletClass"));
return;
}
AActor* OwnerActor = GetOwner();
if (!OwnerActor)
{
UE_LOG(LogTemp, Warning, TEXT("UEBBarrel::FireBullet called but barrel has no owner"));
return;
}
// Determine current muzzle location & aim direction
FVector InLocation = GetComponentTransform().GetLocation();
FVector InDirection = GetComponentTransform().GetUnitAxis(EAxis::X);
FVector OutLocation;
FVector OutDirection;
InitialBulletTransform(InLocation, InDirection, OutLocation, OutDirection);
// Calculate spread and velocity using same logic as original SpawnBullet
AEBBullet* Default = Cast<AEBBullet>(BulletClass->GetDefaultObject());
if (!Default)
{
UE_LOG(LogTemp, Warning, TEXT("UEBBarrel::FireBullet could not get default object for bullet class"));
return;
}
float BulletSpread = Default->Spread;
if (Default->SpreadBias > 0.0f)
{
float SpreadMult = FMath::Pow(FMath::FRand(), Default->SpreadBias);
BulletSpread *= SpreadMult;
}
float BarrelSpread = Spread;
if (SpreadBias > 0.0f)
{
float SpreadMult = FMath::Pow(FMath::FRand(), SpreadBias);
BarrelSpread *= SpreadMult;
}
float TotalSpread = BulletSpread + BarrelSpread;
OutDirection = RandomStream.VRandCone(OutDirection, TotalSpread);
float BulletVelocity = FMath::Lerp(MuzzleVelocityMultiplierMin * Default->MuzzleVelocityMin, MuzzleVelocityMultiplierMax * Default->MuzzleVelocityMax, RandomStream.FRand());
FVector Velocity = OutDirection * BulletVelocity;
// Inherit velocity from parent physics body if applicable
UPrimitiveComponent* ParentComp = Cast<UPrimitiveComponent>(GetAttachParent());
Velocity += AdditionalVelocity;
if (ParentComp && ParentComp->IsSimulatingPhysics())
{
Velocity += ParentComp->GetPhysicsLinearVelocityAtPoint(OutLocation) * InheritVelocity;
}
// Apply recoil impulse
if (ParentComp && ParentComp->IsSimulatingPhysics())
{
FVector Impulse = -Velocity * Default->Mass * RecoilMultiplier * (Default->Shotgun ? Default->ShotCount : 1);
ApplyRecoil(ParentComp, OutLocation, Impulse);
}
// Broadcast pre-shot event
BeforeShotFired.Broadcast();
// Actually spawn the bullet with exact velocity
AEBBullet::SpawnWithExactVelocityFromBarrel(BulletClass, OwnerActor, OwnerActor->GetInstigator(), OutLocation, Velocity, this);
// Cooldown logic is now managed by the gun/controller barrel just fires once.
// Notify listeners that shot has been fired
if (ReplicateShotFiredEvents)
{
ShotFiredMulticast();
}
else
{
ShotFired.Broadcast();
}
} }
+151 -48
View File
@@ -2,11 +2,20 @@
#include "EBBullet.h" #include "EBBullet.h"
#include "EBBarrel.h" #include "EBBarrel.h"
#include "EBUnitConversions.h" #include "EBUnitConversions.h"
#include "Kismet/KismetMathLibrary.h"
// Sets default values // Sets default values
AEBBullet::AEBBullet() { AEBBullet::AEBBullet() {
// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it. // 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; PrimaryActorTick.bCanEverTick = true;
PrimaryActorTick.bStartWithTickEnabled = true;
SetActorTickEnabled(true);
bHasSpalled = false;
Collision = CreateDefaultSubobject<USphereComponent>(TEXT("Collision"));
RootComponent = Collision;
Collision->SetCollisionProfileName(TEXT("BlockAllDynamic"));
SetTickGroup(ETickingGroup::TG_PrePhysics); SetTickGroup(ETickingGroup::TG_PrePhysics);
// Set bullet lifetime to 10 seconds // Set bullet lifetime to 10 seconds
@@ -256,6 +265,35 @@ float AEBBullet::GetCurveValue(const UCurveFloat* curve, float in, float deflt)
return curve->GetFloatValue(in); return curve->GetFloatValue(in);
} }
// Bullet-type specific scaling for spalling behaviour
float AEBBullet::GetSpallFactorByBulletType() const
{
if (!BulletPropertiesAsset)
{
return 1.0f;
}
switch (BulletPropertiesAsset->BulletProperties.BulletType)
{
case EBulletType::BT_ArmorPiercing:
case EBulletType::BT_ArmorPiercingIncendiary:
return 1.4f; // More prone to cause spall
case EBulletType::BT_FullMetalJacket:
case EBulletType::BT_Match:
case EBulletType::BT_Tracer:
return 1.0f; // Baseline
case EBulletType::BT_HollowPoint:
case EBulletType::BT_SoftPoint:
case EBulletType::BT_Frangible:
return 0.6f; // Less spall (expands/deforms instead)
case EBulletType::BT_Wadcutter:
case EBulletType::BT_SemiWadcutter:
return 0.8f; // Moderate
default:
return 1.0f;
}
}
void AEBBullet::ApplyWorldOffset(const FVector& InOffset, bool bWorldShift) { void AEBBullet::ApplyWorldOffset(const FVector& InOffset, bool bWorldShift) {
Super::ApplyWorldOffset(InOffset, bWorldShift); Super::ApplyWorldOffset(InOffset, bWorldShift);
LastTraceStart += InOffset; LastTraceStart += InOffset;
@@ -359,6 +397,11 @@ FMathematicalMaterialProperties AEBBullet::GetMaterialProperties(UPhysicalMateri
// Spalling Implementation // Spalling Implementation
bool AEBBullet::ShouldGenerateSpalling(FVector ImpactVelocity, UPhysicalMaterial* Material) const bool AEBBullet::ShouldGenerateSpalling(FVector ImpactVelocity, UPhysicalMaterial* Material) const
{ {
if (bHasSpalled)
{
return false;
}
if (!EnableSpalling || IsSpallFragment || !Material || !MaterialResponseMap) if (!EnableSpalling || IsSpallFragment || !Material || !MaterialResponseMap)
{ {
return false; return false;
@@ -371,25 +414,28 @@ bool AEBBullet::ShouldGenerateSpalling(FVector ImpactVelocity, UPhysicalMaterial
} }
float ImpactSpeed = ImpactVelocity.Size(); float ImpactSpeed = ImpactVelocity.Size();
// Apply bullet-type scaling (AP increases spall likelihood, HP decreases)
float BulletTypeFactor = GetSpallFactorByBulletType();
// Calculate dynamic spalling threshold based on material properties // Calculate dynamic spalling threshold based on material properties
float MaterialSpallResistance = Material->Density * 0.1f; // Denser materials resist spalling more float MaterialSpallResistance = Material->Density * 0.1f; // Denser materials resist spalling more
float EffectiveThreshold = ResponseEntry->SpallVelocityThreshold * (1.0f + MaterialSpallResistance); float EffectiveThreshold = ResponseEntry->SpallVelocityThreshold * (1.0f + MaterialSpallResistance);
// Reduce/increase threshold according to bullet type
EffectiveThreshold /= BulletTypeFactor;
// Check if bullet has enough kinetic energy to cause spalling // Check if bullet has enough kinetic energy to cause spalling
float ImpactEnergy = 0.5f * GetEffectiveMass() * ImpactSpeed * ImpactSpeed; float ImpactEnergy = 0.5f * GetEffectiveMass() * ImpactSpeed * ImpactSpeed;
float MinimumSpallEnergy = 50.0f * Material->Density; // Energy threshold scales with material density float MinimumSpallEnergy = 50.0f * Material->Density; // Energy threshold scales with material density
MinimumSpallEnergy /= BulletTypeFactor;
return ImpactSpeed >= EffectiveThreshold && ImpactEnergy >= MinimumSpallEnergy; return ImpactSpeed >= EffectiveThreshold && ImpactEnergy >= MinimumSpallEnergy;
} }
void AEBBullet::GenerateSpallFragments(FVector ImpactLocation, FVector ImpactVelocity, FVector ImpactNormal, UPhysicalMaterial* Material, AActor* HitActor) void AEBBullet::GenerateSpallFragments(FVector ImpactLocation, FVector ImpactVelocity, FVector ImpactNormal,
float PlateThicknessCM, float ImpactAngleDegrees,
UPhysicalMaterial* Material, AActor* HitActor)
{ {
if (!ShouldGenerateSpalling(ImpactVelocity, Material))
{
return;
}
// SANITY CHECK: Validate input parameters // SANITY CHECK: Validate input parameters
if (!FMath::IsFinite(ImpactLocation.X) || !FMath::IsFinite(ImpactLocation.Y) || !FMath::IsFinite(ImpactLocation.Z) || if (!FMath::IsFinite(ImpactLocation.X) || !FMath::IsFinite(ImpactLocation.Y) || !FMath::IsFinite(ImpactLocation.Z) ||
!FMath::IsFinite(ImpactVelocity.X) || !FMath::IsFinite(ImpactVelocity.Y) || !FMath::IsFinite(ImpactVelocity.Z) || !FMath::IsFinite(ImpactVelocity.X) || !FMath::IsFinite(ImpactVelocity.Y) || !FMath::IsFinite(ImpactVelocity.Z) ||
@@ -399,6 +445,31 @@ void AEBBullet::GenerateSpallFragments(FVector ImpactLocation, FVector ImpactVel
return; return;
} }
// Calculate cone half-angle (deg) based on empirical model θ = 25° + 10°·sin(impactAngle)
float ConeHalfAngleDeg = 25.0f + 10.0f * FMath::Sin(FMath::DegreesToRadians(ImpactAngleDegrees));
ConeHalfAngleDeg = FMath::Clamp(ConeHalfAngleDeg, 20.0f, 45.0f);
float ConeHalfAngleRad = FMath::DegreesToRadians(ConeHalfAngleDeg);
// Determine total spall mass from target plate (NOT bullet) if we know thickness
float TotalSpallMassKg = 0.0f;
if (PlateThicknessCM > 0.01f && Material)
{
float t_m = PlateThicknessCM * 0.01f; // cm → m
float r_m = t_m * FMath::Tan(ConeHalfAngleRad);
float Volume_m3 = (1.0f/3.0f) * PI * r_m * r_m * t_m; // cone volume
// Density assume UPhysicalMaterial::Density (kg/m³). If unreal default is g/cm³ convert, but here treat as kg/m³.
float TargetDensity = Material->Density > 1.0f ? Material->Density : Material->Density * 1000.0f; // fallback
TotalSpallMassKg = Volume_m3 * TargetDensity;
// Clamp to at most the bullet mass *2 to keep extremes sane
TotalSpallMassKg = FMath::Clamp(TotalSpallMassKg, 0.001f, GetEffectiveMass() * 2.0f);
}
if (TotalSpallMassKg <= 0.0f)
{
// Fallback: use percentage of bullet mass (old behaviour ~20%)
TotalSpallMassKg = GetEffectiveMass() * 0.2f;
}
// SANITY CHECK: Prevent excessive fragment generation for performance // SANITY CHECK: Prevent excessive fragment generation for performance
static int32 GlobalFragmentCount = 0; static int32 GlobalFragmentCount = 0;
static float LastFrameTime = 0.0f; static float LastFrameTime = 0.0f;
@@ -430,10 +501,6 @@ void AEBBullet::GenerateSpallFragments(FVector ImpactLocation, FVector ImpactVel
FragmentClass = GetClass(); FragmentClass = GetClass();
} }
float ImpactSpeed = ImpactVelocity.Size();
float ImpactAngle = FMath::Acos(FMath::Abs(FVector::DotProduct(ImpactVelocity.GetSafeNormal(), ImpactNormal)));
float ImpactAngleDegrees = FMath::RadiansToDegrees(ImpactAngle);
int32 FragmentCount; int32 FragmentCount;
float SpreadAngleRad; float SpreadAngleRad;
@@ -445,7 +512,7 @@ void AEBBullet::GenerateSpallFragments(FVector ImpactLocation, FVector ImpactVel
if (UseMathematicalSpalling) if (UseMathematicalSpalling)
{ {
// Use mathematical ballistics for spalling calculations // Use mathematical ballistics for spalling calculations
float VelocityMPS = ImpactSpeed / 100.0f; // Convert cm/s to m/s float VelocityMPS = ImpactVelocity.Size() / 100.0f; // Convert cm/s to m/s
FragmentCount = UEBMathematicalBallistics::CalculateSpallFragmentCount( FragmentCount = UEBMathematicalBallistics::CalculateSpallFragmentCount(
BulletPropertiesAsset->BulletProperties, BulletPropertiesAsset->BulletProperties,
@@ -466,26 +533,63 @@ void AEBBullet::GenerateSpallFragments(FVector ImpactLocation, FVector ImpactVel
else else
{ {
// Use artistic spalling calculations (original physics-based approach) // Use artistic spalling calculations (original physics-based approach)
float ImpactKineticEnergy = 0.5f * GetEffectiveMass() * ImpactSpeed * ImpactSpeed; float ImpactKineticEnergy = 0.5f * GetEffectiveMass() * ImpactVelocity.Size() * ImpactVelocity.Size();
float AngleEffectiveness = FMath::Cos(ImpactAngle); float AngleEffectiveness = FMath::Cos(FMath::DegreesToRadians(ImpactAngleDegrees));
float MaterialHardness = Material ? FMath::Clamp(Material->Density * 0.001f, 0.1f, 5.0f) : 1.0f; float MaterialHardness = Material ? FMath::Clamp(Material->Density * 0.001f, 0.1f, 5.0f) : 1.0f;
float BaseFragmentCount = FMath::Sqrt(ImpactKineticEnergy / 1000.0f) * MaterialHardness * AngleEffectiveness; float BaseFragmentCount = FMath::Sqrt(ImpactKineticEnergy / 1000.0f) * MaterialHardness * AngleEffectiveness;
FragmentCount = FMath::Clamp(FMath::RoundToInt(BaseFragmentCount), 1, ResponseEntry->SpallFragmentCount); FragmentCount = FMath::Clamp(FMath::RoundToInt(BaseFragmentCount), 1, ResponseEntry->SpallFragmentCount);
float PhysicalSpreadAngle = FMath::Lerp(15.0f, 60.0f, FMath::Sin(ImpactAngle)); float PhysicalSpreadAngle = FMath::Lerp(15.0f, 60.0f, FMath::Sin(FMath::DegreesToRadians(ImpactAngleDegrees)));
SpreadAngleRad = FMath::DegreesToRadians(PhysicalSpreadAngle); SpreadAngleRad = FMath::DegreesToRadians(PhysicalSpreadAngle);
} }
// SANITY CHECK: Clamp fragment count to reasonable limits // SANITY CHECK: Clamp fragment count to reasonable limits
const int32 MaxFragmentsPerImpact = 20; // Absolute maximum per single impact const int32 MaxFragmentsPerImpact = 20; // Absolute maximum per single impact
FragmentCount = FMath::Clamp(FragmentCount, 0, MaxFragmentsPerImpact); float BulletTypeFactor = GetSpallFactorByBulletType();
FragmentCount = FMath::Clamp(FMath::RoundToInt(FragmentCount * BulletTypeFactor), 0, MaxFragmentsPerImpact);
if (FragmentCount <= 0) if (FragmentCount <= 0)
{ {
return; return;
} }
// Distribute parent bullet's mass among fragments to ensure conservation
TArray<float> MassRatios;
float TotalMassRatioPart = 0.f;
for (int32 i = 0; i < FragmentCount; ++i)
{
float ratio;
if (UseMathematicalSpalling)
{
ratio = UEBMathematicalBallistics::CalculateSpallFragmentMass(BulletPropertiesAsset->BulletProperties, ResponseEntry->MathematicalProperties, i, FragmentCount);
}
else
{
// Using a simple power law for mass distribution. Smaller fragments are more common.
ratio = FMath::Pow(RandomStream.FRand(), 2.0f) + 0.1f;
}
MassRatios.Add(ratio);
TotalMassRatioPart += ratio;
}
// Normalize the mass ratios so they sum to 1.0
if (TotalMassRatioPart > 0.f)
{
for (int32 i = 0; i < FragmentCount; ++i)
{
MassRatios[i] /= TotalMassRatioPart;
}
}
else // Should not happen if FragmentCount > 0, but as a fallback
{
for (int32 i = 0; i < FragmentCount; ++i)
{
MassRatios.Add(1.0f / FragmentCount);
}
}
// Base spalling direction (combination of reflection and surface normal) // Base spalling direction (combination of reflection and surface normal)
FVector ReflectionDirection = UKismetMathLibrary::GetReflectionVector(ImpactVelocity.GetSafeNormal(), ImpactNormal); FVector ReflectionDirection = UKismetMathLibrary::GetReflectionVector(ImpactVelocity.GetSafeNormal(), ImpactNormal);
FVector SpallBaseDirection = FMath::Lerp(ImpactNormal, ReflectionDirection, 0.3f).GetSafeNormal(); FVector SpallBaseDirection = FMath::Lerp(ImpactNormal, ReflectionDirection, 0.3f).GetSafeNormal();
@@ -502,44 +606,31 @@ void AEBBullet::GenerateSpallFragments(FVector ImpactLocation, FVector ImpactVel
{ {
float FragmentMass; float FragmentMass;
float FragmentSpeed; float FragmentSpeed;
float FragmentMassRatio = MassRatios[i];
// Determine fragment mass using Mott distribution (simple inverse CDF)
float xi = RandomStream.FRand();
float m_char = TotalSpallMassKg / FragmentCount; // characteristic mass, rough
float fragMassMott = FMath::Square(-FMath::Loge(FMath::Clamp(1.0f - xi, 0.0001f, 0.9999f))) * m_char;
FragmentMass = FMath::Clamp(fragMassMott, 0.0001f, TotalSpallMassKg * 0.5f);
// Compute velocity using a Gurney-like energy share
float VelocityMPS;
if (UseMathematicalSpalling) if (UseMathematicalSpalling)
{ {
// Mathematical fragment calculations float VelocityMPS_impact = ImpactVelocity.Size() / 100.0f;
float VelocityMPS = ImpactSpeed / 100.0f; // Convert cm/s to m/s VelocityMPS = UEBMathematicalBallistics::CalculateSpallFragmentVelocity(
float FragmentMassRatio = UEBMathematicalBallistics::CalculateSpallFragmentMass(
BulletPropertiesAsset->BulletProperties, BulletPropertiesAsset->BulletProperties,
ResponseEntry->MathematicalProperties, ResponseEntry->MathematicalProperties,
i, FragmentCount VelocityMPS_impact,
); FragmentMass / TotalSpallMassKg);
FragmentMass = GetEffectiveMass() * FragmentMassRatio;
float FragmentVelocityMPS = UEBMathematicalBallistics::CalculateSpallFragmentVelocity(
BulletPropertiesAsset->BulletProperties,
ResponseEntry->MathematicalProperties,
VelocityMPS,
FragmentMassRatio
);
FragmentSpeed = FragmentVelocityMPS * 100.0f; // Convert back to cm/s
} }
else else
{ {
// Artistic fragment calculations float AvailableEnergy = 0.5f * TotalSpallMassKg * FMath::Square(ImpactVelocity.Size() / 100.0f) * 0.15f; // 15% of bullet KE
float ImpactKineticEnergy = 0.5f * GetEffectiveMass() * ImpactSpeed * ImpactSpeed; VelocityMPS = FMath::Sqrt(2.0f * (AvailableEnergy / FragmentCount) / FragmentMass);
float AngleEffectiveness = FMath::Cos(ImpactAngle);
float SpallEnergyFraction = FMath::Clamp(0.05f + (AngleEffectiveness * 0.15f), 0.05f, 0.2f);
float AvailableSpallEnergy = ImpactKineticEnergy * SpallEnergyFraction;
float EnergyPerFragment = AvailableSpallEnergy / FragmentCount;
float MassFraction = RandomStream.FRandRange(0.02f, 0.2f) * ResponseEntry->SpallMassMultiplier;
FragmentMass = GetEffectiveMass() * MassFraction;
float FragmentKineticEnergy = EnergyPerFragment * RandomStream.FRandRange(0.5f, 1.5f);
FragmentSpeed = FMath::Sqrt(2.0f * FragmentKineticEnergy / FragmentMass);
} }
FragmentSpeed = VelocityMPS * 100.0f; // m/s → cm/s
// SANITY CHECK: Validate fragment properties // SANITY CHECK: Validate fragment properties
if (FragmentMass <= 0.0f || !FMath::IsFinite(FragmentMass)) if (FragmentMass <= 0.0f || !FMath::IsFinite(FragmentMass))
@@ -653,6 +744,18 @@ void AEBBullet::GenerateSpallFragments(FVector ImpactLocation, FVector ImpactVel
OnSpallFragmentGenerated(SpawnLocation, FragmentVelocity, FragmentMass, Material); OnSpallFragmentGenerated(SpawnLocation, FragmentVelocity, FragmentMass, Material);
} }
// ----- Reduce parent bullet (mass & diameter) to account for material lost -----
float ParentMassKg = GetEffectiveMass();
float RemainingMassKg = FMath::Max(ParentMassKg - TotalSpallMassKg, 0.001f);
if (RemainingMassKg < ParentMassKg)
{
float MassScale = RemainingMassKg / ParentMassKg;
Mass = RemainingMassKg;
// Diameter scales with cube-root of mass for similar density
float DiamScale = FMath::Pow(MassScale, 1.0f / 3.0f);
Diameter *= DiamScale;
}
} }
void AEBBullet::OnSpallFragmentGenerated_Implementation(FVector FragmentLocation, FVector FragmentVelocity, float FragmentMass, UPhysicalMaterial* Material) void AEBBullet::OnSpallFragmentGenerated_Implementation(FVector FragmentLocation, FVector FragmentVelocity, float FragmentMass, UPhysicalMaterial* Material)
+402 -105
View File
@@ -14,13 +14,24 @@ AEBGun::AEBGun()
bReplicates = true; bReplicates = true;
SetReplicateMovement(true); SetReplicateMovement(true);
// Create the gun mesh component // Create a scene component as root to hold both mesh components
GunMesh = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("GunMesh")); USceneComponent* RootScene = CreateDefaultSubobject<USceneComponent>(TEXT("RootScene"));
RootComponent = GunMesh; RootComponent = RootScene;
// Create both mesh components and attach to root
SkeletalGunMesh = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("SkeletalGunMesh"));
StaticGunMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("StaticGunMesh"));
SkeletalGunMesh->SetupAttachment(RootComponent);
StaticGunMesh->SetupAttachment(RootComponent);
// Set skeletal mesh as default active component
StaticGunMesh->SetVisibility(false);
StaticGunMesh->SetCollisionEnabled(ECollisionEnabled::NoCollision);
// Create the barrel component // Create the barrel component for backward compatibility
Barrel = CreateDefaultSubobject<UEBBarrel>(TEXT("Barrel")); Barrel = CreateDefaultSubobject<UEBBarrel>(TEXT("Barrel"));
Barrel->SetupAttachment(GunMesh, TEXT("MuzzleSocket")); Barrel->SetupAttachment(SkeletalGunMesh, TEXT("MuzzleSocket"));
Barrel->SetParentGun(this); Barrel->SetParentGun(this);
// Create the magazine component // Create the magazine component
@@ -36,11 +47,49 @@ AEBGun::AEBGun()
bCanProcessNextShot = true; bCanProcessNextShot = true;
BurstShotsFired = 0; BurstShotsFired = 0;
ChamberedBulletType = nullptr; ChamberedBulletType = nullptr;
// Initialize firing state
bShooting = false;
bSpooling = false;
GatlingRPS = 0.0f;
Cooldown = 0.0f;
BurstRemaining = 0;
// Initialize gun configuration
bUseSkeletalMesh = true;
bUseSkeletal = true;
MuzzleSocketName = TEXT("MuzzleSocket");
MuzzleSocket = TEXT("MuzzleSocket");
MuzzleTransform = FTransform::Identity;
MuzzleOffset = FTransform::Identity;
SkeletalMeshAsset = nullptr;
StaticMeshAsset = nullptr;
// Initialize random stream
RandomStream.Initialize(FMath::Rand());
} }
void AEBGun::BeginPlay() void AEBGun::BeginPlay()
{ {
Super::BeginPlay(); Super::BeginPlay();
// Synchronize the actor settings with component settings
bUseSkeletalMesh = bUseSkeletal;
MuzzleSocketName = MuzzleSocket;
MuzzleTransform = MuzzleOffset;
// Apply mesh assets from actor settings
if (bUseSkeletal && SkeletalMeshAsset)
{
SkeletalGunMesh->SetSkeletalMesh(SkeletalMeshAsset);
}
else if (!bUseSkeletal && StaticMeshAsset)
{
StaticGunMesh->SetStaticMesh(StaticMeshAsset);
}
// Apply the mesh type setting to ensure proper visibility
SetMeshType(bUseSkeletal);
// Apply weapon configuration if available // Apply weapon configuration if available
if (WeaponConfig) if (WeaponConfig)
@@ -69,10 +118,53 @@ void AEBGun::Tick(float DeltaTime)
// Process firing state // Process firing state
if (CurrentGunState == EGunState::GS_Firing) if (CurrentGunState == EGunState::GS_Firing)
{ {
UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] PullTrigger() SUCCESS - AEBGun::Tick::FIRE()"));
ProcessFiring(); ProcessFiring();
} }
} }
#if WITH_EDITOR
void AEBGun::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
{
Super::PostEditChangeProperty(PropertyChangedEvent);
if (PropertyChangedEvent.Property)
{
FName PropertyName = PropertyChangedEvent.Property->GetFName();
// Handle mesh setup changes
if (PropertyName == GET_MEMBER_NAME_CHECKED(AEBGun, bUseSkeletal))
{
// Synchronize with internal setting
bUseSkeletalMesh = bUseSkeletal;
SetMeshType(bUseSkeletalMesh);
}
else if (PropertyName == GET_MEMBER_NAME_CHECKED(AEBGun, SkeletalMeshAsset))
{
if (SkeletalMeshAsset && SkeletalGunMesh)
{
SkeletalGunMesh->SetSkeletalMesh(SkeletalMeshAsset);
}
}
else if (PropertyName == GET_MEMBER_NAME_CHECKED(AEBGun, StaticMeshAsset))
{
if (StaticMeshAsset && StaticGunMesh)
{
StaticGunMesh->SetStaticMesh(StaticMeshAsset);
}
}
else if (PropertyName == GET_MEMBER_NAME_CHECKED(AEBGun, MuzzleSocket))
{
MuzzleSocketName = MuzzleSocket;
}
else if (PropertyName == GET_MEMBER_NAME_CHECKED(AEBGun, MuzzleOffset))
{
MuzzleTransform = MuzzleOffset;
}
}
}
#endif
void AEBGun::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const void AEBGun::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{ {
Super::GetLifetimeReplicatedProps(OutLifetimeProps); Super::GetLifetimeReplicatedProps(OutLifetimeProps);
@@ -82,12 +174,16 @@ void AEBGun::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimePr
DOREPLIFETIME(AEBGun, bChamberedRound); DOREPLIFETIME(AEBGun, bChamberedRound);
DOREPLIFETIME(AEBGun, bHammerCocked); DOREPLIFETIME(AEBGun, bHammerCocked);
DOREPLIFETIME(AEBGun, ChamberedBulletType); DOREPLIFETIME(AEBGun, ChamberedBulletType);
DOREPLIFETIME(AEBGun, FireMode);
DOREPLIFETIME(AEBGun, bShooting);
DOREPLIFETIME(AEBGun, bSpooling);
} }
void AEBGun::PullTrigger() void AEBGun::PullTrigger()
{ {
if (HasAuthority()) if (HasAuthority())
{ {
UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] PullTrigger() SUCCESS - HasAuthority()"));
ServerPullTrigger(); ServerPullTrigger();
} }
else else
@@ -98,16 +194,18 @@ void AEBGun::PullTrigger()
void AEBGun::ServerPullTrigger_Implementation() void AEBGun::ServerPullTrigger_Implementation()
{ {
UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] PullTrigger() SUCCESS - ServerPullTrigger_Implementation()"));
if (!CanFire()) if (!CanFire())
{ {
return; return;
} }
bTriggerPressed = true; bTriggerPressed = true;
UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] PullTrigger() SUCCESS - bTriggerPressed"));
if (CurrentGunState == EGunState::GS_Ready) if (CurrentGunState == EGunState::GS_Ready)
{ {
SetGunState(EGunState::GS_Firing); SetGunState(EGunState::GS_Firing);
UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] PullTrigger() SUCCESS - GS_Firing"));
} }
} }
@@ -128,15 +226,11 @@ void AEBGun::ServerReleaseTrigger_Implementation()
bTriggerPressed = false; bTriggerPressed = false;
// Handle fire mode specific behavior // Handle fire mode specific behavior
if (Barrel && WeaponConfig) if (FireMode == EFireMode::FM_Semiauto || FireMode == EFireMode::FM_Manual)
{ {
EFireMode CurrentFireMode = Barrel->FireMode; if (CurrentGunState == EGunState::GS_Firing)
if (CurrentFireMode == EFireMode::FM_Semiauto || CurrentFireMode == EFireMode::FM_Manual)
{ {
if (CurrentGunState == EGunState::GS_Firing) SetGunState(EGunState::GS_Ready);
{
SetGunState(EGunState::GS_Ready);
}
} }
} }
} }
@@ -311,6 +405,7 @@ bool AEBGun::CanFire() const
{ {
if (bSafetyOn) if (bSafetyOn)
{ {
UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] CanFire() - Safety is ON"));
return false; return false;
} }
@@ -319,11 +414,13 @@ bool AEBGun::CanFire() const
CurrentGunState == EGunState::GS_SafetyOn || CurrentGunState == EGunState::GS_SafetyOn ||
CurrentGunState == EGunState::GS_Malfunction) CurrentGunState == EGunState::GS_Malfunction)
{ {
UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] CanFire() - Gun is not in a ready state: %s"), *UEnum::GetValueAsString(CurrentGunState));
return false; return false;
} }
if (!bChamberedRound || !ChamberedBulletType) if (!bChamberedRound || !ChamberedBulletType)
{ {
UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] CanFire() - No chambered round or bullet type"));
return false; return false;
} }
@@ -375,7 +472,24 @@ void AEBGun::ApplyWeaponConfiguration(UEBWeaponConfiguration* NewConfig)
WeaponConfig = NewConfig; WeaponConfig = NewConfig;
// Apply configuration to barrel // Apply gun configuration from barrel config
BarrelLength = NewConfig->BarrelConfig.BarrelLength;
RiflingTwist = NewConfig->BarrelConfig.RiflingTwist;
BoreRadius = NewConfig->BarrelConfig.BoreRadius;
MuzzleVelocityMin = NewConfig->BarrelConfig.MuzzleVelocityMin;
MuzzleVelocityMax = NewConfig->BarrelConfig.MuzzleVelocityMax;
// Apply fire control configuration to gun
FireMode = NewConfig->FireControlConfig.DefaultFireMode;
FireRateMin = NewConfig->FireControlConfig.FireRateMin / 60.0f; // Convert RPM to RPS
FireRateMax = NewConfig->FireControlConfig.FireRateMax / 60.0f;
BurstCount = NewConfig->FireControlConfig.BurstCount;
BurstCooldown = NewConfig->FireControlConfig.BurstCooldown;
GatlingSpoolUpTime = NewConfig->FireControlConfig.GatlingSpoolUpTime;
GatlingSpoolDownTime = NewConfig->FireControlConfig.GatlingSpoolDownTime;
bGatlingAutoSpool = NewConfig->FireControlConfig.bGatlingAutoSpool;
// Apply configuration to barrel for backward compatibility
if (Barrel) if (Barrel)
{ {
// Set parent gun reference // Set parent gun reference
@@ -383,27 +497,14 @@ void AEBGun::ApplyWeaponConfiguration(UEBWeaponConfiguration* NewConfig)
// Apply barrel-specific configuration // Apply barrel-specific configuration
Barrel->ApplyBarrelConfiguration(NewConfig->BarrelConfig); Barrel->ApplyBarrelConfiguration(NewConfig->BarrelConfig);
// Apply fire control configuration // Apply networking configuration from gun to barrel
Barrel->FireMode = NewConfig->FireControlConfig.DefaultFireMode;
Barrel->FireRateMin = NewConfig->FireControlConfig.FireRateMin / 60.0f; // Convert RPM to RPS
Barrel->FireRateMax = NewConfig->FireControlConfig.FireRateMax / 60.0f;
Barrel->BurstCount = NewConfig->FireControlConfig.BurstCount;
Barrel->BurstCooldown = NewConfig->FireControlConfig.BurstCooldown;
Barrel->GatlingSpoolUpTime = NewConfig->FireControlConfig.GatlingSpoolUpTime;
Barrel->GatlingSpoolDownTime = NewConfig->FireControlConfig.GatlingSpoolDownTime;
Barrel->GatlingAutoSpool = NewConfig->FireControlConfig.bGatlingAutoSpool;
// Apply networking configuration
Barrel->ReplicateVariables = NewConfig->bReplicateVariables;
Barrel->ReplicateShotFiredEvents = NewConfig->bReplicateShotEvents; Barrel->ReplicateShotFiredEvents = NewConfig->bReplicateShotEvents;
Barrel->ClientSideAim = NewConfig->bClientSideAim;
// Disable legacy ammo system in favor of gun's magazine system UE_LOG(LogTemp, Log, TEXT("Barrel '%s' configured by Gun '%s'"), *Barrel->GetName(), *NewConfig->GetWeaponDisplayName());
Barrel->CycleAmmo = false;
UE_LOG(LogTemp, Log, TEXT("Gun '%s' applied configuration to barrel"), *NewConfig->GetWeaponDisplayName());
} }
UE_LOG(LogTemp, Log, TEXT("Gun '%s' applied configuration"), *NewConfig->GetWeaponDisplayName());
// Apply configuration to magazine // Apply configuration to magazine
if (Magazine) if (Magazine)
@@ -444,20 +545,15 @@ void AEBGun::ServerSetFireMode_Implementation(EFireMode NewFireMode)
return; return;
} }
if (Barrel) FireMode = NewFireMode;
{
Barrel->FireMode = NewFireMode; // Update barrel for backward compatibility
} // Barrel no longer keeps FireMode state.
} }
EFireMode AEBGun::GetFireMode() const EFireMode AEBGun::GetFireMode() const
{ {
if (Barrel) return FireMode;
{
return Barrel->FireMode;
}
return EFireMode::FM_Semiauto;
} }
void AEBGun::SetGunState(EGunState NewState) void AEBGun::SetGunState(EGunState NewState)
@@ -489,48 +585,51 @@ void AEBGun::ProcessFiring()
{ {
if (!HasAuthority()) if (!HasAuthority())
{ {
UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] ProcessFiring() FAILED - No Authority"));
return; return;
} }
if (!CanFire() || !bCanProcessNextShot) if (!CanFire() || !bCanProcessNextShot)
{ {
//Print CanFire and bCanProcessNextShot state
UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] - CanFire: %s, bCanProcessNextShot: %s"),
CanFire() ? TEXT("True") : TEXT("False"),
bCanProcessNextShot ? TEXT("True") : TEXT("False"));
return; return;
} }
// Check if we should continue firing based on fire mode // Check if we should continue firing based on fire mode
if (Barrel && WeaponConfig) switch (FireMode)
{ {
EFireMode CurrentFireMode = Barrel->FireMode; case EFireMode::FM_Semiauto:
case EFireMode::FM_Manual:
switch (CurrentFireMode) UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] ProcessFiring() - FM_Semiauto or FM_Manual"));
if (!bTriggerPressed)
{ {
case EFireMode::FM_Semiauto: SetGunState(EGunState::GS_Ready);
case EFireMode::FM_Manual: return;
if (!bTriggerPressed)
{
SetGunState(EGunState::GS_Ready);
return;
}
break;
case EFireMode::FM_Auto:
if (!bTriggerPressed)
{
SetGunState(EGunState::GS_Ready);
return;
}
break;
case EFireMode::FM_Burst:
case EFireMode::FM_InterBurst:
if (BurstShotsFired >= WeaponConfig->FireControlConfig.BurstCount)
{
BurstShotsFired = 0;
SetGunState(EGunState::GS_Ready);
return;
}
break;
} }
break;
case EFireMode::FM_Auto:
UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] ProcessFiring() - FM_Auto"));
if (!bTriggerPressed)
{
SetGunState(EGunState::GS_Ready);
return;
}
break;
case EFireMode::FM_Burst:
case EFireMode::FM_InterBurst:
UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] ProcessFiring() - FM_Burst or FM_InterBurst"));
if (BurstShotsFired >= BurstCount)
{
BurstShotsFired = 0;
SetGunState(EGunState::GS_Ready);
return;
}
break;
} }
// Fire the weapon // Fire the weapon
@@ -539,43 +638,49 @@ void AEBGun::ProcessFiring()
void AEBGun::FireShot() void AEBGun::FireShot()
{ {
UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] FireShot() called - Firing shot from AEBGun"));
if (!HasAuthority() || !CanFire()) if (!HasAuthority() || !CanFire())
{ {
UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] FireShot() FAILED - CanFire() returned false or no authority"));
return; return;
} }
// Fire through barrel // Fire through barrel
if (Barrel && ChamberedBulletType) if (Barrel && ChamberedBulletType)
{ {
// Set bullet class on barrel (this is a simplified approach) UE_LOG(LogTemp, Warning, TEXT("[WEAPON DEBUG] FireShot() - Firing shot from barrel"));
// In a real implementation, you would need to create a bullet and configure it properly
Barrel->Ammo.Empty(); // Ensure we have a valid bullet class to spawn
if (ChamberedBulletType) if (!ChamberedBulletType->BulletClass)
{ {
// For now, we'll use the default bullet class since we can't get the class from the asset UE_LOG(LogTemp, Error, TEXT("[WEAPON DEBUG] FireShot() FAILED: Bullet Properties Asset '%s' does not have a BulletClass assigned!"), *ChamberedBulletType->GetName());
// This would need to be properly implemented with a bullet class reference in the asset return; // Stop if we don't have a bullet to spawn.
} }
// Fire the shot // Fire a single round directly through the barrel without relying on the legacy state machine
Barrel->Shoot(true); Barrel->FireBullet(ChamberedBulletType->BulletClass);
// Clear chambered round // Clear chambered round in the gun
ChamberedBulletType = nullptr; ChamberedBulletType = nullptr;
bChamberedRound = false; bChamberedRound = false;
// Update burst count // Update burst count
BurstShotsFired++; BurstShotsFired++;
// Set fire rate cooldown // Set fire-rate cooldown. Ensure the selected rate is positive so the timer actually triggers.
float FireRate = WeaponConfig ? WeaponConfig->GetEffectiveFireRate(GetFireMode()) : 600.0f; float ActualFireRate = RandomStream.FRandRange(FireRateMin, FireRateMax);
float CooldownTime = 60.0f / FireRate; // Convert RPM to seconds if (ActualFireRate <= KINDA_SMALL_NUMBER)
{
ActualFireRate = 1.0f; // fallback to 1 RPS (60 RPM)
}
float CooldownTime = 1.0f / ActualFireRate; // FireRate is already in RPS
bCanProcessNextShot = false; bCanProcessNextShot = false;
GetWorld()->GetTimerManager().SetTimer(FireRateTimer, [this]() GetWorld()->GetTimerManager().SetTimer(FireRateTimer, [this]()
{ {
bCanProcessNextShot = true; bCanProcessNextShot = true;
}, CooldownTime, false); }, CooldownTime, false);
// Try to feed next round // Try to feed next round
if (!FeedNextRound()) if (!FeedNextRound())
{ {
@@ -650,39 +755,47 @@ void AEBGun::MulticastOnSafetyChanged_Implementation(bool bNewSafetyState)
} }
} }
// Barrel Integration Functions // Gun Properties Functions (moved from barrel)
float AEBGun::GetBarrelLength() const float AEBGun::GetBarrelLength() const
{ {
if (Barrel) return BarrelLength;
{
return Barrel->BarrelLength;
}
return 0.0f;
} }
float AEBGun::GetEffectiveMuzzleVelocity() const float AEBGun::GetEffectiveMuzzleVelocity() const
{ {
if (Barrel && ChamberedBulletType) if (ChamberedBulletType)
{ {
return Barrel->CalculateEffectiveMuzzleVelocity(ChamberedBulletType); // Calculate effective muzzle velocity based on gun properties
// This is a simplified calculation - in a real implementation you'd want
// to consider bullet properties, barrel length, powder charge, etc.
float BaseVelocity = FMath::RandRange(MuzzleVelocityMin, MuzzleVelocityMax);
float VelocityMultiplier = FMath::RandRange(MuzzleVelocityMultiplierMin, MuzzleVelocityMultiplierMax);
return BaseVelocity * VelocityMultiplier;
} }
return 0.0f; return 0.0f;
} }
float AEBGun::GetBarrelAccuracy() const float AEBGun::GetBarrelAccuracy() const
{ {
if (Barrel) // Calculate accuracy based on gun properties
{ // This is a simplified calculation
return Barrel->CalculateBarrelAccuracy(); float AccuracyFactor = 1.0f - Spread;
} return FMath::Clamp(AccuracyFactor, 0.0f, 1.0f);
return 0.0f;
} }
FVector AEBGun::CalculateRecoilImpulse() const FVector AEBGun::CalculateRecoilImpulse() const
{ {
if (Barrel && ChamberedBulletType) if (ChamberedBulletType)
{ {
return Barrel->CalculateRecoilImpulse(ChamberedBulletType); // Calculate recoil based on gun properties and bullet
// This is a simplified calculation - in reality you'd consider:
// - Bullet mass and velocity
// - Powder charge
// - Gun weight
// - Barrel length
float RecoilMagnitude = GetEffectiveMuzzleVelocity() * 0.001f; // Simple scaling
FVector RecoilDirection = -GetActorForwardVector();
return RecoilDirection * RecoilMagnitude;
} }
return FVector::ZeroVector; return FVector::ZeroVector;
} }
@@ -690,4 +803,188 @@ FVector AEBGun::CalculateRecoilImpulse() const
bool AEBGun::IsBarrelConnected() const bool AEBGun::IsBarrelConnected() const
{ {
return Barrel != nullptr && Barrel->IsConnectedToGun(); return Barrel != nullptr && Barrel->IsConnectedToGun();
}
// Gun Mesh Management Functions
void AEBGun::SetMeshType(bool bUseSkeletalMeshParam)
{
bUseSkeletalMesh = bUseSkeletalMeshParam;
if (bUseSkeletalMesh)
{
// Switch to skeletal mesh
SkeletalGunMesh->SetVisibility(true);
SkeletalGunMesh->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
StaticGunMesh->SetVisibility(false);
StaticGunMesh->SetCollisionEnabled(ECollisionEnabled::NoCollision);
// Reattach barrel to skeletal mesh
if (Barrel)
{
Barrel->SetupAttachment(SkeletalGunMesh, *MuzzleSocketName);
}
}
else
{
// Switch to static mesh
StaticGunMesh->SetVisibility(true);
StaticGunMesh->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
SkeletalGunMesh->SetVisibility(false);
SkeletalGunMesh->SetCollisionEnabled(ECollisionEnabled::NoCollision);
// Reattach barrel to static mesh
if (Barrel)
{
Barrel->SetupAttachment(StaticGunMesh);
}
}
}
UPrimitiveComponent* AEBGun::GetActiveMeshComponent() const
{
return bUseSkeletalMesh ? Cast<UPrimitiveComponent>(SkeletalGunMesh) : Cast<UPrimitiveComponent>(StaticGunMesh);
}
FTransform AEBGun::GetMuzzleTransform() const
{
if (bUseSkeletalMesh && SkeletalGunMesh)
{
if (SkeletalGunMesh->DoesSocketExist(*MuzzleSocketName))
{
return SkeletalGunMesh->GetSocketTransform(*MuzzleSocketName);
}
}
else if (!bUseSkeletalMesh && StaticGunMesh)
{
// For static mesh, use the relative transform
return StaticGunMesh->GetComponentTransform() * MuzzleTransform;
}
// Fallback to component transform
UPrimitiveComponent* ActiveMesh = GetActiveMeshComponent();
return ActiveMesh ? ActiveMesh->GetComponentTransform() : GetActorTransform();
}
void AEBGun::SetMuzzleSocketName(const FString& NewSocketName)
{
MuzzleSocketName = NewSocketName;
// Update barrel attachment if using skeletal mesh
if (bUseSkeletalMesh && Barrel)
{
Barrel->SetupAttachment(SkeletalGunMesh, *MuzzleSocketName);
}
}
// Firing System Functions (moved from barrel)
void AEBGun::Shoot(bool bTrigger)
{
if (HasAuthority())
{
ServerShoot(bTrigger);
}
else
{
ServerShoot(bTrigger);
}
}
void AEBGun::ServerShoot_Implementation(bool bTrigger)
{
if (bTrigger)
{
PullTrigger();
}
else
{
ReleaseTrigger();
}
}
bool AEBGun::ServerShoot_Validate(bool bTrigger)
{
return true;
}
void AEBGun::GatlingSpool(bool bSpool)
{
if (HasAuthority())
{
ServerGatlingSpool(bSpool);
}
else
{
ServerGatlingSpool(bSpool);
}
}
void AEBGun::ServerGatlingSpool_Implementation(bool bSpool)
{
bSpooling = bSpool;
}
bool AEBGun::ServerGatlingSpool_Validate(bool bSpool)
{
return true;
}
void AEBGun::SwitchFireMode(EFireMode NewFireMode)
{
if (HasAuthority())
{
ServerSwitchFireMode(NewFireMode);
}
else
{
ServerSwitchFireMode(NewFireMode);
}
}
void AEBGun::ServerSwitchFireMode_Implementation(EFireMode NewFireMode)
{
if (WeaponConfig && WeaponConfig->IsFireModeAvailable(NewFireMode))
{
FireMode = NewFireMode;
// Update barrel for backward compatibility
// Barrel no longer keeps FireMode state.
}
}
bool AEBGun::ServerSwitchFireMode_Validate(EFireMode NewFireMode)
{
return true;
}
// Legacy SpawnBullet function removed firing handled by FireShot / FireBullet.
void AEBGun::PredictHit(bool& bHit, FHitResult& TraceResult, FVector& HitLocation, float& HitTime,
AActor*& HitActor, TArray<FVector>& Trajectory,
TSubclassOf<class AEBBullet> BulletClass,
const TArray<AActor*>& IgnoredActors,
float MaxTime, float Step) const
{
// For now, delegate to barrel for backward compatibility
if (Barrel)
{
Barrel->PredictHit(bHit, TraceResult, HitLocation, HitTime, HitActor, Trajectory, BulletClass, IgnoredActors, MaxTime, Step);
}
}
void AEBGun::CalculateAimDirection(TSubclassOf<class AEBBullet> BulletClass, FVector TargetLocation, FVector TargetVelocity,
FVector& AimDirection, FVector& PredictedTargetLocation, FVector& PredictedIntersectionLocation,
float& PredictedFlightTime, float& Error, float MaxTime, float Step, int32 NumIterations) const
{
// For now, delegate to barrel for backward compatibility
if (Barrel)
{
Barrel->CalculateAimDirection(BulletClass, TargetLocation, TargetVelocity, AimDirection, PredictedTargetLocation,
PredictedIntersectionLocation, PredictedFlightTime, Error, MaxTime, Step, NumIterations);
}
}
void AEBGun::MulticastShotFired_Implementation()
{
// Handle client-side shot fired effects
// This could include muzzle flash, sound effects, etc.
} }
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,187 @@
// Copyright 2016 Mookie. All Rights Reserved.
#include "EBWeaponData.h"
#include "EBBarrel.h"
UEBWeaponData::UEBWeaponData()
{
// Set default values
WeaponName = FText::FromString(TEXT("Weapon"));
WeaponDescription = FText::FromString(TEXT("A standard weapon"));
WeaponType = EWeaponType::Rifle;
WeaponIcon = nullptr;
WeaponTier = 1;
// Visuals
FirstPersonMesh = nullptr;
ThirdPersonMesh = nullptr;
StaticMesh = nullptr;
// Ballistics
BulletProperties = nullptr;
Damage = 30.0f;
FireRate = 600.0f;
MuzzleVelocity = 91440.0f;
Accuracy = 0.95f;
RecoilStrength = 1.0f;
EffectiveRange = 300.0f;
// Magazine
MagazineSize = 30;
MaxReserveAmmo = 120;
ReloadTime = 2.5f;
TacticalReloadTime = 2.0f;
// Fire modes
AvailableFireModes.Add(EFireMode::FM_Auto);
AvailableFireModes.Add(EFireMode::FM_Semiauto);
DefaultFireMode = EFireMode::FM_Auto;
BurstCount = 3;
// Attachments
MuzzleSocketName = TEXT("MuzzleSocket");
ScopeSocketName = TEXT("ScopeSocket");
GripSocketName = TEXT("GripSocket");
LaserSocketName = TEXT("LaserSocket");
// Advanced
WeaponWeight = 3.5f;
ADSTime = 0.3f;
SprintToFireTime = 0.25f;
EquipTime = 0.75f;
bCanDualWield = false;
bHasSafety = true;
bSuppressorCompatible = true;
// Niagara advanced features
bUseGPUSimulation = true;
bEnableEffectLOD = true;
WeaponDataInterface = nullptr;
MaxHeatLevel = 100.0f;
HeatDecayRate = 10.0f;
HeatPerShot = 5.0f;
CurrentHeat = 0.0f;
// Debug settings
bEnableFireDebug = false;
DebugArrowSize = 100.0f;
bDebugImpactInfo = false;
bDebugTrajectory = false;
bDebugPhysics = false;
bDebugPerformance = false;
bDebugBallistics = false;
bDebugSpalling = false;
}
FString UEBWeaponData::GetWeaponTypeString() const
{
switch (WeaponType)
{
case EWeaponType::Rifle: return TEXT("Rifle");
case EWeaponType::Pistol: return TEXT("Pistol");
case EWeaponType::Shotgun: return TEXT("Shotgun");
case EWeaponType::SMG: return TEXT("SMG");
case EWeaponType::LMG: return TEXT("LMG");
case EWeaponType::Sniper: return TEXT("Sniper");
case EWeaponType::Launcher: return TEXT("Launcher");
case EWeaponType::Melee: return TEXT("Melee");
case EWeaponType::Special: return TEXT("Special");
default: return TEXT("Unknown");
}
}
void UEBWeaponData::AddHeat(float Amount)
{
float HeatToAdd = (Amount < 0.0f) ? HeatPerShot : Amount;
CurrentHeat = FMath::Clamp(CurrentHeat + HeatToAdd, 0.0f, MaxHeatLevel);
}
void UEBWeaponData::UpdateHeat(float DeltaTime)
{
if (CurrentHeat > 0.0f)
{
CurrentHeat = FMath::Max(0.0f, CurrentHeat - (HeatDecayRate * DeltaTime));
}
}
// ========================================
// DEBUG UTILITY FUNCTIONS
// ========================================
void UEBWeaponData::SetBulletDebugCategory(const FString& CategoryName, bool bEnabled)
{
if (CategoryName == TEXT("Trajectory"))
{
bDebugTrajectory = bEnabled;
}
else if (CategoryName == TEXT("Impact"))
{
bDebugImpactInfo = bEnabled;
}
else if (CategoryName == TEXT("Physics"))
{
bDebugPhysics = bEnabled;
}
else if (CategoryName == TEXT("Performance"))
{
bDebugPerformance = bEnabled;
}
else if (CategoryName == TEXT("Ballistics"))
{
bDebugBallistics = bEnabled;
}
else if (CategoryName == TEXT("Spalling"))
{
bDebugSpalling = bEnabled;
}
}
void UEBWeaponData::SetAllBulletDebugCategories(bool bEnabled)
{
bDebugTrajectory = bEnabled;
bDebugImpactInfo = bEnabled;
bDebugPhysics = bEnabled;
bDebugPerformance = bEnabled;
bDebugBallistics = bEnabled;
bDebugSpalling = bEnabled;
}
FString UEBWeaponData::GetBulletDebugInfo() const
{
TArray<FString> EnabledCategories;
if (bDebugTrajectory) EnabledCategories.Add(TEXT("Trajectory"));
if (bDebugImpactInfo) EnabledCategories.Add(TEXT("Impact"));
if (bDebugPhysics) EnabledCategories.Add(TEXT("Physics"));
if (bDebugPerformance) EnabledCategories.Add(TEXT("Performance"));
if (bDebugBallistics) EnabledCategories.Add(TEXT("Ballistics"));
if (bDebugSpalling) EnabledCategories.Add(TEXT("Spalling"));
if (EnabledCategories.Num() == 0)
{
return TEXT("No debug categories enabled");
}
return FString::Printf(TEXT("Enabled Debug Categories: %s | Arrow Size: %.0f"),
*FString::Join(EnabledCategories, TEXT(", ")), DebugArrowSize);
}
void UEBWeaponData::ApplyDebugSettingsToBarrel(class UEBBarrel* Barrel) const
{
if (!Barrel)
{
return;
}
// Apply debug arrow size
Barrel->DebugArrowSize = DebugArrowSize;
Barrel->DebugImpactInfo = bDebugImpactInfo;
// Apply debug categories through barrel's functions
Barrel->SetBulletDebugCategory(TEXT("Trajectory"), bDebugTrajectory);
Barrel->SetBulletDebugCategory(TEXT("Impact"), bDebugImpactInfo);
Barrel->SetBulletDebugCategory(TEXT("Physics"), bDebugPhysics);
Barrel->SetBulletDebugCategory(TEXT("Performance"), bDebugPerformance);
Barrel->SetBulletDebugCategory(TEXT("Ballistics"), bDebugBallistics);
Barrel->SetBulletDebugCategory(TEXT("Spalling"), bDebugSpalling);
}
+2 -72
View File
@@ -1,74 +1,4 @@
// Copyright 2016 Mookie. All Rights Reserved. // Copyright 2016 Mookie. All Rights Reserved.
#include "EBBarrel.h" // This file contained legacy reloading and ammo management logic that has been moved to AEBGun and UEBMagazine.
#include "EBBullet.h" // The contents have been cleared to resolve compilation errors after a major refactoring.
TArray<TSubclassOf<class AEBBullet>> UEBBarrel::GetAmmo(bool CountChambered) const {
if (!CountChambered || ChamberedBullet == nullptr) {
return Ammo;
}
else {
TArray<TSubclassOf<class AEBBullet>> RetAmmo;
RetAmmo.Add(ChamberedBullet);
RetAmmo.Append(Ammo);
return RetAmmo;
};
};
int UEBBarrel::GetAmmoCount(bool CountChambered) const {
int remainingAmmo;
if (CycleAmmo) {
remainingAmmo = CycleAmmoCount;
}
else {
remainingAmmo = Ammo.Num();
};
if (CountChambered) {
if (ChamberedBullet != nullptr) {
remainingAmmo++;
};
};
return remainingAmmo;
};
void UEBBarrel::SetAmmo(int Count, bool UnloadChambered, bool CancelShooting, bool ManualCharge, const TArray<TSubclassOf<class AEBBullet>>& NewAmmo) {
Ammo = NewAmmo;
CycleAmmoCount = Count;
if (UnloadChambered) {
ChamberedBullet = nullptr;
};
if (CancelShooting) {
BurstRemaining = 0;
Shooting = false;
};
if (ManualCharge) {
LoadNext = false;
};
};
void UEBBarrel::Charge_Implementation() {
LoadNext = true;
};
bool UEBBarrel::Charge_Validate() {
return true;
};
void UEBBarrel::UnloadChambered_Implementation(bool ManualCharge) {
ChamberedBullet = nullptr;
if (ManualCharge) {
LoadNext = false;
};
};
bool UEBBarrel::UnloadChambered_Validate(bool ManualCharge) {
return true;
};
+20 -2
View File
@@ -306,11 +306,18 @@ float AEBBullet::Trace(FVector start, FVector PreviousVelocity, float delta, TEn
// Create debug impact widget if enabled // Create debug impact widget if enabled
CreateDebugImpactWidget(HitResult.Location, Ricochet, Penetration, Velocity, PenetrationDepth, PhysMaterial); CreateDebugImpactWidget(HitResult.Location, Ricochet, Penetration, Velocity, PenetrationDepth, PhysMaterial);
bool bDidSpallThisImpact = false;
// Generate spalling fragments if conditions are met // Generate spalling fragments if conditions are met
if (HasAuthority() && ShouldGenerateSpalling(Velocity, PhysMaterial)) if (HasAuthority() && ShouldGenerateSpalling(Velocity, PhysMaterial))
{ {
bDidSpallThisImpact = true;
// Entry spalling // Entry spalling
GenerateSpallFragments(HitResult.Location, Velocity, HitResult.Normal, PhysMaterial, HitResult.GetActor()); {
float EntryImpactAngleDeg = FMath::RadiansToDegrees(FMath::Acos(FMath::Abs(FVector::DotProduct(Velocity.GetSafeNormal(), HitResult.Normal))));
GenerateSpallFragments(HitResult.Location, Velocity, HitResult.Normal,
0.0f, EntryImpactAngleDeg,
PhysMaterial, HitResult.GetActor());
}
// Exit spalling (backspall) for penetration - typically more dangerous // Exit spalling (backspall) for penetration - typically more dangerous
if (Penetration && (exitLoc - HitResult.Location).Size() > 1.0f) if (Penetration && (exitLoc - HitResult.Location).Size() > 1.0f)
@@ -318,10 +325,21 @@ float AEBBullet::Trace(FVector start, FVector PreviousVelocity, float delta, TEn
// Exit spalling velocity is based on remaining velocity after penetration // Exit spalling velocity is based on remaining velocity after penetration
// Backspall typically has higher fragment velocity than entry spalling // Backspall typically has higher fragment velocity than entry spalling
FVector ExitSpallVelocity = NewVelocity * 1.2f; // Amplify for more realistic backspall effect FVector ExitSpallVelocity = NewVelocity * 1.2f; // Amplify for more realistic backspall effect
GenerateSpallFragments(exitLoc, ExitSpallVelocity, -exitNormal, PhysMaterial, HitResult.GetActor()); {
float ExitImpactAngleDeg = FMath::RadiansToDegrees(FMath::Acos(FMath::Abs(FVector::DotProduct(NewVelocity.GetSafeNormal(), -exitNormal))));
GenerateSpallFragments(exitLoc, ExitSpallVelocity, -exitNormal,
PenetrationDepth, ExitImpactAngleDeg,
PhysMaterial, HitResult.GetActor());
}
} }
} }
if (bDidSpallThisImpact)
{
bHasSpalled = true;
NewVelocity = FVector::ZeroVector;
}
// SAFETY: Clamp velocity to prevent infinite speeds and numerical instability // SAFETY: Clamp velocity to prevent infinite speeds and numerical instability
float MaxSafeVelocity = MuzzleVelocityMax * 10.0f; // Allow up to 10x muzzle velocity as safety limit float MaxSafeVelocity = MuzzleVelocityMax * 10.0f; // Allow up to 10x muzzle velocity as safety limit
if (!NewVelocity.IsNearlyZero() && NewVelocity.Size() > MaxSafeVelocity) if (!NewVelocity.IsNearlyZero() && NewVelocity.Size() > MaxSafeVelocity)
+5 -45
View File
@@ -97,54 +97,17 @@ public:
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Weapon", meta = (ToolTip = "Additional Spread bias, higher is more accurate on average", ClampMin = "0")) float SpreadBias = 0.0f; UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Weapon", meta = (ToolTip = "Additional Spread bias, higher is more accurate on average", ClampMin = "0")) float SpreadBias = 0.0f;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Weapon", meta = (ToolTip = "Minimum of random multiplier applied to bullet muzzle velocity")) float MuzzleVelocityMultiplierMin = 1.0f; UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Weapon", meta = (ToolTip = "Minimum of random multiplier applied to bullet muzzle velocity")) float MuzzleVelocityMultiplierMin = 1.0f;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Weapon", meta = (ToolTip = "Maximum of random multiplier applied to bullet muzzle velocity")) float MuzzleVelocityMultiplierMax = 1.0f; UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Weapon", meta = (ToolTip = "Maximum of random multiplier applied to bullet muzzle velocity")) float MuzzleVelocityMultiplierMax = 1.0f;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Weapon", meta = (ToolTip = "Minimum fire rate, rounds per second")) float FireRateMin = 1.0f;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Weapon", meta = (ToolTip = "Maximum fire rate, rounds per second, set to same number as FireRateMin to disable randomization")) float FireRateMax = 1.0f;
UPROPERTY(Replicated, BlueprintReadWrite, EditAnywhere, Category = "Weapon") EFireMode FireMode = EFireMode::FM_Auto;
UPROPERTY(Replicated, BlueprintReadWrite, EditAnywhere, Category = "Weapon") bool ShootingBlocked;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Weapon", meta = (ToolTip = "Number of rounds auto fired in burst mode")) int BurstCount = 3;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Weapon", meta = (ToolTip = "Minimum time between bursts")) float BurstCooldown = 0.0f;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Weapon", meta = (ToolTip = "Automatically spin up gatling when trigger is being held down")) bool GatlingAutoSpool = true;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Weapon") float GatlingSpoolUpTime = 1.0f;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Weapon") float GatlingSpoolDownTime = 1.0f;
UPROPERTY(BlueprintReadWrite, Category = "Weapon") float GatlingPhase = 0.0f;
// Legacy Ammo System (kept for compatibility, use Gun's Magazine system for new weapons)
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Legacy Ammo", meta = (ToolTip = "Legacy ammo system - use Gun's Magazine for new weapons"))
bool CycleAmmo = false;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Legacy Ammo", meta = (EditCondition = "CycleAmmo", ToolTip = "Legacy ammo system - use Gun's Magazine for new weapons"))
bool CycleAmmoUnlimited = true;
UPROPERTY(Replicated, BlueprintReadWrite, EditAnywhere, Category = "Legacy Ammo", meta = (ToolTip = "Legacy ammo system - use Gun's Magazine for new weapons"))
TArray<TSubclassOf<class AEBBullet>> Ammo;
UPROPERTY(Replicated, BlueprintReadWrite, EditAnywhere, Category = "Legacy Ammo", meta = (EditCondition = "CycleAmmo", ToolTip = "Legacy ammo system - use Gun's Magazine for new weapons"))
int CycleAmmoCount;
UPROPERTY(Replicated, BlueprintReadWrite, EditAnywhere, Category = "Legacy Ammo", meta = (EditCondition = "CycleAmmo", ToolTip = "Legacy ammo system - use Gun's Magazine for new weapons"))
int CycleAmmoPos;
UPROPERTY(Replicated, BlueprintReadWrite, Category = "WeaponState") TSubclassOf<class AEBBullet> ChamberedBullet;
UPROPERTY(Replicated, BlueprintReadWrite, Category = "WeaponState") bool Shooting;
UPROPERTY(Replicated, BlueprintReadWrite, Category = "WeaponState") bool Spooling = false;
UPROPERTY(BlueprintReadWrite, Category = "Weapon") float GatlingRPS = 0.0f;
UPROPERTY(BlueprintReadWrite, Category = "WeaponState") bool LoadNext=true;
UPROPERTY(BlueprintReadWrite, Category = "WeaponState") float Cooldown;
UPROPERTY(BlueprintReadWrite, Category = "WeaponState") int BurstRemaining;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Replication") bool ReplicateVariables=true; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Replication") bool ReplicateVariables=true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Replication") bool ReplicateShotFiredEvents = true; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Replication") bool ReplicateShotFiredEvents = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Replication") bool ClientSideAim=false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Replication") float ClientAimUpdateFrequency = 15.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Replication") float ClientAimDistanceLimit = 200.0f;
FRandomStream RandomStream; FRandomStream RandomStream;
// Legacy Ammo Functions (kept for compatibility) // New simplified single-shot firing interface. Spawns exactly one bullet of the given class using
UFUNCTION() void NextBullet(); // the barrel's current transform. This bypasses the legacy internal shooting state machine so that
UFUNCTION(BlueprintPure, Category = "Legacy Ammo") int GetAmmoCount(bool CountChambered) const; // higher-level gun/weapon controllers can directly fire a round without manipulating Barrel internals.
UFUNCTION(BlueprintPure, Category = "Legacy Ammo") TArray<TSubclassOf<class AEBBullet>> GetAmmo(bool CountChambered) const; UFUNCTION(BlueprintCallable, Category = "Shooting")
UFUNCTION(BlueprintAuthorityOnly, BlueprintCallable, Category = "Legacy Ammo") void SetAmmo(int count, bool UnloadChambered, bool CancelShooting, bool ManualCharge, const TArray<TSubclassOf<class AEBBullet>>& NewAmmo); void FireBullet(TSubclassOf<class AEBBullet> BulletClass);
UFUNCTION(Server, Reliable, WithValidation, BlueprintCallable, Category = "Legacy Ammo") void Charge();
UFUNCTION(Server, Reliable, WithValidation, BlueprintCallable, Category = "Legacy Ammo") void UnloadChambered(bool ManualCharge);
// New Gun-Integrated Ammo Functions // New Gun-Integrated Ammo Functions
UFUNCTION(BlueprintPure, Category = "Gun Integration") UFUNCTION(BlueprintPure, Category = "Gun Integration")
@@ -155,9 +118,6 @@ public:
UFUNCTION(BlueprintPure, Category = "Gun Integration") UFUNCTION(BlueprintPure, Category = "Gun Integration")
bool CanFireFromGun() const; bool CanFireFromGun() const;
UFUNCTION(Server, Reliable, WithValidation, BlueprintCallable, Category = "Shooting") void SwitchFireMode(EFireMode NewFireMode);
UFUNCTION(Server, Reliable, WithValidation, BlueprintCallable, Category = "Shooting") void GatlingSpool(bool Spool);
UFUNCTION(BlueprintCallable, Category = "Shooting") void Shoot(bool Trigger);
UFUNCTION(BlueprintCallable, meta = (AutoCreateRefTerm = "IgnoredActors"), Category = "Prediction") void PredictHit(bool& Hit, FHitResult& TraceResult, FVector& HitLocation, float& HitTime, AActor*& HitActor, TArray<FVector>& Trajectory, TSubclassOf<class AEBBullet> BulletClass, TArray<AActor*>IgnoredActors, float MaxTime = 10.0f, float Step = 0.1f) const; UFUNCTION(BlueprintCallable, meta = (AutoCreateRefTerm = "IgnoredActors"), Category = "Prediction") void PredictHit(bool& Hit, FHitResult& TraceResult, FVector& HitLocation, float& HitTime, AActor*& HitActor, TArray<FVector>& Trajectory, TSubclassOf<class AEBBullet> BulletClass, TArray<AActor*>IgnoredActors, float MaxTime = 10.0f, float Step = 0.1f) const;
UFUNCTION(BlueprintCallable, meta = (AutoCreateRefTerm = "IgnoredActors"), Category = "Prediction") void PredictHitFromLocation(bool &Hit, FHitResult& TraceResult, FVector& HitLocation, float& HitTime, AActor*& HitActor, TArray<FVector>& Trajectory, TSubclassOf<class AEBBullet> BulletClass, FVector StartLocation, FVector AimDirection, TArray<AActor*>IgnoredActors, float MaxTime = 10.0f, float Step = 0.1f) const; UFUNCTION(BlueprintCallable, meta = (AutoCreateRefTerm = "IgnoredActors"), Category = "Prediction") void PredictHitFromLocation(bool &Hit, FHitResult& TraceResult, FVector& HitLocation, float& HitTime, AActor*& HitActor, TArray<FVector>& Trajectory, TSubclassOf<class AEBBullet> BulletClass, FVector StartLocation, FVector AimDirection, TArray<AActor*>IgnoredActors, float MaxTime = 10.0f, float Step = 0.1f) const;
+32 -2
View File
@@ -11,6 +11,7 @@
#include "Kismet/GameplayStatics.h" #include "Kismet/GameplayStatics.h"
#include "DrawDebugHelpers.h" #include "DrawDebugHelpers.h"
#include "Components/PrimitiveComponent.h" #include "Components/PrimitiveComponent.h"
#include "Components/SphereComponent.h"
#include "EBMaterialResponseMap.h" #include "EBMaterialResponseMap.h"
#include "EBBulletProperties.h" #include "EBBulletProperties.h"
@@ -38,6 +39,10 @@ public:
// Sets default values for this actor's properties // Sets default values for this actor's properties
AEBBullet(); AEBBullet();
// Collision Component
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Collision")
class USphereComponent* Collision;
UPROPERTY(Replicated, BlueprintReadWrite, Category = "State") FVector Velocity; UPROPERTY(Replicated, BlueprintReadWrite, Category = "State") FVector Velocity;
UPROPERTY(Replicated, BlueprintReadWrite, Category = "State") FRandomStream RandomStream; UPROPERTY(Replicated, BlueprintReadWrite, Category = "State") FRandomStream RandomStream;
@@ -219,7 +224,15 @@ public:
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Impact", meta = (ToolTip = "Enable spalling fragment generation on impact")) UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Impact", meta = (ToolTip = "Enable spalling fragment generation on impact"))
bool EnableSpalling = true; bool EnableSpalling = true;
UPROPERTY(BlueprintReadWrite, Category = "Impact", meta = (ToolTip = "Indicates if this bullet is a spalling fragment")) /** Flag to indicate if this bullet has already generated spall, preventing repeated spalling. */
UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "Spalling")
bool bHasSpalled;
// Spalling properties (can be overridden by Material Response Map)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spalling")
TSubclassOf<AEBBullet> SpallFragmentClass;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Impact", meta = (ToolTip = "Indicates if this bullet is a spalling fragment"))
bool IsSpallFragment = false; bool IsSpallFragment = false;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Replication") bool ReliableReplication = false; UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Replication") bool ReliableReplication = false;
@@ -346,7 +359,19 @@ public:
// Spalling Functions // Spalling Functions
UFUNCTION(BlueprintCallable, Category = "EBBullet|Spalling") UFUNCTION(BlueprintCallable, Category = "EBBullet|Spalling")
void GenerateSpallFragments(FVector ImpactLocation, FVector ImpactVelocity, FVector ImpactNormal, UPhysicalMaterial* Material, AActor* HitActor); /**
* Generate spall fragments given impact parameters.
* @param ImpactLocation Location of impact in world space
* @param ImpactVelocity Incoming projectile velocity at impact (cm/s)
* @param ImpactNormal Surface normal at point of impact (unit vector)
* @param PlateThicknessCM Thickness of the target plate in centimetres (0 if unknown / no penetration)
* @param ImpactAngleDegrees Angle between velocity vector and surface normal in degrees (0 = perpendicular)
* @param Material Physical material hit
* @param HitActor Actor that was hit (for ignore lists etc.)
*/
void GenerateSpallFragments(FVector ImpactLocation, FVector ImpactVelocity, FVector ImpactNormal,
float PlateThicknessCM, float ImpactAngleDegrees,
UPhysicalMaterial* Material, AActor* HitActor);
UFUNCTION(BlueprintNativeEvent, Category = "EBBullet|Spalling") UFUNCTION(BlueprintNativeEvent, Category = "EBBullet|Spalling")
void OnSpallFragmentGenerated(FVector FragmentLocation, FVector FragmentVelocity, float FragmentMass, UPhysicalMaterial* Material); void OnSpallFragmentGenerated(FVector FragmentLocation, FVector FragmentVelocity, float FragmentMass, UPhysicalMaterial* Material);
@@ -367,6 +392,11 @@ public:
UFUNCTION(NetMulticast, Reliable) UFUNCTION(NetMulticast, Reliable)
void DeactivationBroadcast(); void DeactivationBroadcast();
private: private:
/**
* Returns a scaling factor (>1 increases spall likelihood/amount, <1 decreases) based on the bullet type.
*/
float GetSpallFactorByBulletType() const;
UPROPERTY() TArray<TWeakObjectPtr<AEBBullet>> Pooled; UPROPERTY() TArray<TWeakObjectPtr<AEBBullet>> Pooled;
static AEBBullet* GetFromPool(UWorld* World, UClass* BulletClass); static AEBBullet* GetFromPool(UWorld* World, UClass* BulletClass);
static AEBBullet* SpawnOrReactivate(UWorld* World, TSubclassOf<class AEBBullet> BulletClass, const FTransform& Transform, FVector BulletVelocity, AActor* BulletOwner, APawn* BulletInstigator); static AEBBullet* SpawnOrReactivate(UWorld* World, TSubclassOf<class AEBBullet> BulletClass, const FTransform& Transform, FVector BulletVelocity, AActor* BulletOwner, APawn* BulletInstigator);
@@ -199,6 +199,19 @@ struct FMathematicalMaterialProperties
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spalling Properties", meta = (EditCondition = "EnableMathematicalSpalling", ToolTip = "Maximum fragment count per unit area")) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spalling Properties", meta = (EditCondition = "EnableMathematicalSpalling", ToolTip = "Maximum fragment count per unit area"))
float MaxFragmentDensity = 50.0f; float MaxFragmentDensity = 50.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Material Properties", meta = (ToolTip = "Poisson ratio (dimensionless)"))
float PoissonRatio = 0.3f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Material Properties", meta = (ToolTip = "Longitudinal wave speed in the material (m/s)"))
float WaveSpeedMPerS = 5900.f; // steel approx
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Material Properties", meta = (ToolTip = "Ultimate tensile strength MPa"))
float UltimateTensileStrengthMPa = 500.f;
// Backing liner efficiency (0-1) for spall liner or composite backing. 1 = no liner, 0 = perfect absorption
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Material Properties", meta = (ToolTip = "Efficiency factor of any backing liner (1.0 = none, lower = more spall reduction)"))
float BackingLinerEfficiency = 1.0f;
// Constructor with default steel properties // Constructor with default steel properties
FMathematicalMaterialProperties() FMathematicalMaterialProperties()
{ {
@@ -226,6 +239,9 @@ class EASYBALLISTICS_API UEBBulletPropertiesAsset : public UDataAsset
GENERATED_BODY() GENERATED_BODY()
public: public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spawning", meta = (ToolTip = "The bullet actor to spawn."))
TSubclassOf<class AEBBullet> BulletClass;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Bullet Properties") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Bullet Properties")
FMathematicalBulletProperties BulletProperties; FMathematicalBulletProperties BulletProperties;
+299 -44
View File
@@ -26,6 +26,54 @@ enum class EGunState : uint8
GS_Malfunction UMETA(DisplayName = "Malfunction") GS_Malfunction UMETA(DisplayName = "Malfunction")
}; };
/**
* EasyBallistics Gun Actor - Complete weapon system with organized settings
*
* PROPERTY ORGANIZATION:
* 1. Configuration - Weapon config asset
* 2. Barrel & Ballistics - Essential shooting properties (START HERE)
* 3. Fire Control - Fire modes, rates, burst settings
* 4. Advanced Ballistics - Fine-tuning for realism
* 5. Gatling Settings - For minigun/gatling type weapons
* 6. Gun State - Read-only operational status
* 7. Firing State - Read-only runtime firing status
*
* BLUEPRINT NODE ORGANIZATION:
* All nodes are under "Ballistics" with logical subcategories:
*
* Ballistics|
* ├── Firing|
* │ ├── Trigger (PullTrigger, ReleaseTrigger)
* │ ├── Advanced (Shoot)
* │ ├── Gatling (GatlingSpool)
* │ └── Bullet Spawn (SpawnBullet)
* ├── Controls|
* │ ├── Safety (SetSafety)
* │ └── Action (CockHammer, ReleaseHammer)
* ├── Ammunition|
* │ ├── Magazine (LoadMagazine, EjectMagazine, InsertMagazine)
* │ └── Chamber (ChargingHandle, EjectChamberedRound)
* ├── Status|
* │ ├── Readiness (CanFire, IsLoaded)
* │ └── Ammunition (GetRoundsInMagazine, GetTotalRounds, GetChamberedBulletType)
* ├── Configuration|
* │ ├── Setup (ApplyWeaponConfiguration)
* │ ├── Fire Control (SetFireMode, GetFireMode, SwitchFireMode)
* │ └── Mesh (SetMeshType, GetActiveMeshComponent, GetMuzzleTransform, SetMuzzleSocketName)
* ├── Calculations|
* │ ├── Properties (GetBarrelLength, GetEffectiveMuzzleVelocity, GetBarrelAccuracy)
* │ └── Recoil (CalculateRecoilImpulse)
* ├── Prediction|
* │ ├── Trajectory (PredictHit)
* │ └── Targeting (CalculateAimDirection)
* ├── Events|
* │ ├── State (OnGunStateChanged)
* │ ├── Firing (OnGunFired)
* │ ├── Ammunition (OnMagazineEmpty, OnMagazineChanged)
* │ └── Safety (OnSafetyChanged)
* └── Legacy|
* └── Barrel (IsBarrelConnected)
*/
UCLASS(Blueprintable, BlueprintType) UCLASS(Blueprintable, BlueprintType)
class EASYBALLISTICS_API AEBGun : public AActor class EASYBALLISTICS_API AEBGun : public AActor
{ {
@@ -34,135 +82,315 @@ class EASYBALLISTICS_API AEBGun : public AActor
public: public:
AEBGun(); AEBGun();
// Core Components // Core Components - Gun Mesh (can be either skeletal or static)
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components") UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
USkeletalMeshComponent* GunMesh; USkeletalMeshComponent* SkeletalGunMesh;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components") UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
UStaticMeshComponent* StaticGunMesh;
// Gun Type Configuration
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Components", meta = (ToolTip = "Use skeletal mesh (true) or static mesh (false). Set this to false to use StaticGunMesh."))
bool bUseSkeletalMesh = true;
// Firing system integrated into gun (replacing barrel dependency)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gun Configuration", meta = (EditCondition = "!bUseSkeletalMesh", ToolTip = "Muzzle transform for static mesh mode"))
FTransform MuzzleTransform;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gun Configuration", meta = (EditCondition = "bUseSkeletalMesh", ToolTip = "Socket name for muzzle in skeletal mesh mode"))
FString MuzzleSocketName = TEXT("MuzzleSocket");
// Legacy barrel component for backward compatibility
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (ToolTip = "Legacy barrel component - firing logic now handled by gun"))
UEBBarrel* Barrel; UEBBarrel* Barrel;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components") UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
UEBMagazine* Magazine; UEBMagazine* Magazine;
// Weapon Configuration // ========================================
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Weapon Configuration") // MESH SETUP (Configure First)
// ========================================
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "0. Mesh Setup", meta = (ToolTip = "Use skeletal mesh (true) or static mesh (false)"))
bool bUseSkeletal = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "0. Mesh Setup", meta = (EditCondition = "bUseSkeletal", ToolTip = "Skeletal mesh for the gun"))
USkeletalMesh* SkeletalMeshAsset;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "0. Mesh Setup", meta = (EditCondition = "!bUseSkeletal", ToolTip = "Static mesh for the gun"))
UStaticMesh* StaticMeshAsset;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "0. Mesh Setup", meta = (EditCondition = "bUseSkeletal", ToolTip = "Socket name for muzzle in skeletal mesh mode"))
FString MuzzleSocket = TEXT("MuzzleSocket");
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ballistics|Calculations|Recoil", meta = (ToolTip = "Can process next shot based on current state"))
bool bCanProcessNextShot = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "0. Mesh Setup", meta = (EditCondition = "!bUseSkeletal", ToolTip = "Muzzle transform for static mesh mode"))
FTransform MuzzleOffset = FTransform::Identity;
// ========================================
// CONFIGURATION
// ========================================
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "1. Configuration")
UEBWeaponConfiguration* WeaponConfig; UEBWeaponConfiguration* WeaponConfig;
// Gun State // ========================================
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_GunState, Category = "Gun State") // BARREL & BALLISTICS (Most Common Settings)
// ========================================
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "2. Barrel & Ballistics", meta = (ClampMin = "1.0", ClampMax = "50.0", ToolTip = "Barrel length in inches"))
float BarrelLength = 16.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "2. Barrel & Ballistics", meta = (ClampMin = "10000.0", ClampMax = "200000.0", ToolTip = "Minimum muzzle velocity in cm/s"))
float MuzzleVelocityMin = 91440.0f; // ~3000 fps
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "2. Barrel & Ballistics", meta = (ClampMin = "10000.0", ClampMax = "200000.0", ToolTip = "Maximum muzzle velocity in cm/s"))
float MuzzleVelocityMax = 91440.0f; // ~3000 fps
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "2. Barrel & Ballistics", meta = (ClampMin = "0.0", ClampMax = "1.0", ToolTip = "Spread in radians - 0 = perfect accuracy"))
float Spread = 0.0f;
// ========================================
// FIRE CONTROL (Essential Settings)
// ========================================
UPROPERTY(Replicated, EditAnywhere, BlueprintReadWrite, Category = "3. Fire Control")
EFireMode FireMode = EFireMode::FM_Auto;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "3. Fire Control", meta = (ClampMin = "0.1", ClampMax = "50.0", ToolTip = "Fire rate in rounds per second"))
float FireRateMin = 1.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "3. Fire Control", meta = (ClampMin = "0.1", ClampMax = "50.0", ToolTip = "Fire rate in rounds per second"))
float FireRateMax = 1.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "3. Fire Control", meta = (ClampMin = "1", ClampMax = "20", ToolTip = "Number of rounds per burst"))
int32 BurstCount = 3;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "3. Fire Control", meta = (ClampMin = "0.0", ClampMax = "10.0", ToolTip = "Cooldown between bursts in seconds"))
float BurstCooldown = 0.0f;
// ========================================
// ADVANCED BALLISTICS (Fine-tuning)
// ========================================
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "4. Advanced Ballistics", meta = (ClampMin = "1.0", ClampMax = "20.0", ToolTip = "Rifling twist rate (1:X)"))
float RiflingTwist = 7.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "4. Advanced Ballistics", meta = (ClampMin = "0.05", ClampMax = "1.0", ToolTip = "Bore radius in inches"))
float BoreRadius = 0.112f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "4. Advanced Ballistics", meta = (ClampMin = "0.0", ClampMax = "10.0", ToolTip = "Spread bias - higher = more accurate on average"))
float SpreadBias = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "4. Advanced Ballistics", meta = (ClampMin = "0.5", ClampMax = "2.0", ToolTip = "Minimum velocity multiplier"))
float MuzzleVelocityMultiplierMin = 1.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "4. Advanced Ballistics", meta = (ClampMin = "0.5", ClampMax = "2.0", ToolTip = "Maximum velocity multiplier"))
float MuzzleVelocityMultiplierMax = 1.0f;
// ========================================
// GATLING/MINIGUN (Specialized Settings)
// ========================================
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "5. Gatling Settings", meta = (ToolTip = "Automatically spool up when trigger held"))
bool bGatlingAutoSpool = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "5. Gatling Settings", meta = (ClampMin = "0.1", ClampMax = "10.0", ToolTip = "Time to spool up in seconds"))
float GatlingSpoolUpTime = 1.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "5. Gatling Settings", meta = (ClampMin = "0.1", ClampMax = "10.0", ToolTip = "Time to spool down in seconds"))
float GatlingSpoolDownTime = 1.0f;
UPROPERTY(BlueprintReadOnly, Category = "5. Gatling Settings", meta = (ToolTip = "Current gatling phase (0-1)"))
float GatlingPhase = 0.0f;
// ========================================
// GUN STATE (Read-Only Status)
// ========================================
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_GunState, Category = "6. Gun State", meta = (ToolTip = "Current operational state of the gun"))
EGunState CurrentGunState = EGunState::GS_Ready; EGunState CurrentGunState = EGunState::GS_Ready;
UPROPERTY(BlueprintReadOnly, Replicated, Category = "Gun State") UPROPERTY(BlueprintReadOnly, Replicated, Category = "6. Gun State", meta = (ToolTip = "Is the safety engaged"))
bool bSafetyOn = false; bool bSafetyOn = false;
UPROPERTY(BlueprintReadOnly, Replicated, Category = "Gun State") UPROPERTY(BlueprintReadOnly, Replicated, Category = "6. Gun State", meta = (ToolTip = "Is there a round chambered"))
bool bChamberedRound = false; bool bChamberedRound = false;
UPROPERTY(BlueprintReadOnly, Replicated, Category = "Gun State") UPROPERTY(BlueprintReadOnly, Replicated, Category = "6. Gun State", meta = (ToolTip = "Is the hammer cocked"))
bool bHammerCocked = false; bool bHammerCocked = false;
// Firing Interface // Chambered Round
UFUNCTION(BlueprintCallable, Category = "Gun|Firing") UPROPERTY(BlueprintReadOnly, Replicated, Category = "6. Gun State", meta = (ToolTip = "Current bullet type in chamber"))
UEBBulletPropertiesAsset* ChamberedBulletType;
// ========================================
// FIRING STATE (Runtime Status)
// ========================================
UPROPERTY(BlueprintReadOnly, Replicated, Category = "7. Firing State", meta = (ToolTip = "Is currently shooting"))
bool bShooting = false;
UPROPERTY(BlueprintReadOnly, Replicated, Category = "7. Firing State", meta = (ToolTip = "Is gatling gun spooling"))
bool bSpooling = false;
UPROPERTY(BlueprintReadOnly, Category = "7. Firing State", meta = (ToolTip = "Current gatling rounds per second"))
float GatlingRPS = 0.0f;
UPROPERTY(BlueprintReadOnly, Category = "7. Firing State", meta = (ToolTip = "Current cooldown remaining"))
float Cooldown = 0.0f;
UPROPERTY(BlueprintReadOnly, Category = "7. Firing State", meta = (ToolTip = "Burst shots remaining"))
int32 BurstRemaining = 0;
// ========================================
// BLUEPRINT FUNCTIONS
// ========================================
// Primary Firing Interface
UFUNCTION(BlueprintCallable, Category = "Ballistics|Firing|Trigger")
void PullTrigger(); void PullTrigger();
UFUNCTION(BlueprintCallable, Category = "Gun|Firing") UFUNCTION(BlueprintCallable, Category = "Ballistics|Firing|Trigger")
void ReleaseTrigger(); void ReleaseTrigger();
UFUNCTION(BlueprintCallable, Category = "Gun|Controls") // Gun Controls
UFUNCTION(BlueprintCallable, Category = "Ballistics|Controls|Safety")
void SetSafety(bool bNewSafetyState); void SetSafety(bool bNewSafetyState);
UFUNCTION(BlueprintCallable, Category = "Gun|Controls") UFUNCTION(BlueprintCallable, Category = "Ballistics|Controls|Action")
void CockHammer(); void CockHammer();
UFUNCTION(BlueprintCallable, Category = "Gun|Controls") UFUNCTION(BlueprintCallable, Category = "Ballistics|Controls|Action")
void ReleaseHammer(); void ReleaseHammer();
// Ammunition Management // Ammunition Management
UFUNCTION(BlueprintCallable, Category = "Gun|Ammunition") UFUNCTION(BlueprintCallable, Category = "Ballistics|Ammunition|Magazine")
void LoadMagazine(TArray<UEBBulletPropertiesAsset*> BulletTypes); void LoadMagazine(TArray<UEBBulletPropertiesAsset*> BulletTypes);
UFUNCTION(BlueprintCallable, Category = "Gun|Ammunition") UFUNCTION(BlueprintCallable, Category = "Ballistics|Ammunition|Magazine")
void EjectMagazine(); void EjectMagazine();
UFUNCTION(BlueprintCallable, Category = "Gun|Ammunition") UFUNCTION(BlueprintCallable, Category = "Ballistics|Ammunition|Magazine")
void InsertMagazine(UEBMagazine* NewMagazine); void InsertMagazine(UEBMagazine* NewMagazine);
UFUNCTION(BlueprintCallable, Category = "Gun|Ammunition") UFUNCTION(BlueprintCallable, Category = "Ballistics|Ammunition|Chamber")
void ChargingHandle(); void ChargingHandle();
UFUNCTION(BlueprintCallable, Category = "Gun|Ammunition") UFUNCTION(BlueprintCallable, Category = "Ballistics|Ammunition|Chamber")
void EjectChamberedRound(); void EjectChamberedRound();
// State Queries // State Queries
UFUNCTION(BlueprintPure, Category = "Gun|State") UFUNCTION(BlueprintPure, Category = "Ballistics|Status|Readiness")
bool CanFire() const; bool CanFire() const;
UFUNCTION(BlueprintPure, Category = "Gun|State") UFUNCTION(BlueprintPure, Category = "Ballistics|Status|Readiness")
bool IsLoaded() const; bool IsLoaded() const;
UFUNCTION(BlueprintPure, Category = "Gun|State") UFUNCTION(BlueprintPure, Category = "Ballistics|Status|Ammunition")
int32 GetRoundsInMagazine() const; int32 GetRoundsInMagazine() const;
UFUNCTION(BlueprintPure, Category = "Gun|State") UFUNCTION(BlueprintPure, Category = "Ballistics|Status|Ammunition")
int32 GetTotalRounds() const; int32 GetTotalRounds() const;
UFUNCTION(BlueprintPure, Category = "Gun|State") UFUNCTION(BlueprintPure, Category = "Ballistics|Status|Ammunition")
UEBBulletPropertiesAsset* GetChamberedBulletType() const; UEBBulletPropertiesAsset* GetChamberedBulletType() const;
// Configuration // Configuration
UFUNCTION(BlueprintCallable, Category = "Gun|Configuration") UFUNCTION(BlueprintCallable, Category = "Ballistics|Configuration|Setup")
void ApplyWeaponConfiguration(UEBWeaponConfiguration* NewConfig); void ApplyWeaponConfiguration(UEBWeaponConfiguration* NewConfig);
UFUNCTION(BlueprintCallable, Category = "Gun|Configuration") UFUNCTION(BlueprintCallable, Category = "Ballistics|Configuration|Fire Control")
void SetFireMode(EFireMode NewFireMode); void SetFireMode(EFireMode NewFireMode);
UFUNCTION(BlueprintPure, Category = "Gun|Configuration") UFUNCTION(BlueprintPure, Category = "Ballistics|Configuration|Fire Control")
EFireMode GetFireMode() const; EFireMode GetFireMode() const;
// Barrel Integration // Gun Mesh Management
UFUNCTION(BlueprintPure, Category = "Gun|Barrel") UFUNCTION(BlueprintCallable, Category = "Ballistics|Configuration|Mesh")
void SetMeshType(bool bUseSkeletalMeshParam);
UFUNCTION(BlueprintPure, Category = "Ballistics|Configuration|Mesh")
UPrimitiveComponent* GetActiveMeshComponent() const;
UFUNCTION(BlueprintPure, Category = "Ballistics|Configuration|Mesh")
FTransform GetMuzzleTransform() const;
UFUNCTION(BlueprintCallable, Category = "Ballistics|Configuration|Mesh")
void SetMuzzleSocketName(const FString& NewSocketName);
// Ballistics Calculations
UFUNCTION(BlueprintPure, Category = "Ballistics|Calculations|Properties")
float GetBarrelLength() const; float GetBarrelLength() const;
UFUNCTION(BlueprintPure, Category = "Gun|Barrel") UFUNCTION(BlueprintPure, Category = "Ballistics|Calculations|Properties")
float GetEffectiveMuzzleVelocity() const; float GetEffectiveMuzzleVelocity() const;
UFUNCTION(BlueprintPure, Category = "Gun|Barrel") UFUNCTION(BlueprintPure, Category = "Ballistics|Calculations|Properties")
float GetBarrelAccuracy() const; float GetBarrelAccuracy() const;
UFUNCTION(BlueprintCallable, Category = "Gun|Barrel")
UFUNCTION(BlueprintCallable, Category = "Ballistics|Calculations|Recoil")
FVector CalculateRecoilImpulse() const; FVector CalculateRecoilImpulse() const;
UFUNCTION(BlueprintPure, Category = "Gun|Barrel") // Legacy barrel support
UFUNCTION(BlueprintPure, Category = "Ballistics|Legacy|Barrel")
bool IsBarrelConnected() const; bool IsBarrelConnected() const;
// Events // ========================================
// BLUEPRINT EVENTS
// ========================================
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnGunStateChanged, EGunState, NewState); DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnGunStateChanged, EGunState, NewState);
UPROPERTY(BlueprintAssignable, Category = "Gun Events") UPROPERTY(BlueprintAssignable, Category = "Ballistics|Events|State")
FOnGunStateChanged OnGunStateChanged; FOnGunStateChanged OnGunStateChanged;
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnGunFired, UEBBulletPropertiesAsset*, BulletType); DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnGunFired, UEBBulletPropertiesAsset*, BulletType);
UPROPERTY(BlueprintAssignable, Category = "Gun Events") UPROPERTY(BlueprintAssignable, Category = "Ballistics|Events|Firing")
FOnGunFired OnGunFired; FOnGunFired OnGunFired;
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnMagazineEmpty); DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnMagazineEmpty);
UPROPERTY(BlueprintAssignable, Category = "Gun Events") UPROPERTY(BlueprintAssignable, Category = "Ballistics|Events|Ammunition")
FOnMagazineEmpty OnMagazineEmpty; FOnMagazineEmpty OnMagazineEmpty;
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnMagazineChanged, UEBMagazine*, NewMagazine); DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnMagazineChanged, UEBMagazine*, NewMagazine);
UPROPERTY(BlueprintAssignable, Category = "Gun Events") UPROPERTY(BlueprintAssignable, Category = "Ballistics|Events|Ammunition")
FOnMagazineChanged OnMagazineChanged; FOnMagazineChanged OnMagazineChanged;
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnSafetyChanged, bool, bNewSafetyState); DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnSafetyChanged, bool, bNewSafetyState);
UPROPERTY(BlueprintAssignable, Category = "Gun Events") UPROPERTY(BlueprintAssignable, Category = "Ballistics|Events|Safety")
FOnSafetyChanged OnSafetyChanged; FOnSafetyChanged OnSafetyChanged;
// Networking // Networking
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override; virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
// Advanced Firing Functions
UFUNCTION(BlueprintCallable, Category = "Ballistics|Firing|Advanced")
void Shoot(bool bTrigger);
UFUNCTION(BlueprintCallable, Category = "Ballistics|Firing|Gatling")
void GatlingSpool(bool bSpool);
UFUNCTION(BlueprintCallable, Category = "Ballistics|Configuration|Fire Control")
void SwitchFireMode(EFireMode NewFireMode);
// ========================================
// BALLISTICS PREDICTION & CALCULATIONS
// ========================================
UFUNCTION(BlueprintCallable, meta = (AutoCreateRefTerm = "IgnoredActors"), Category = "Ballistics|Prediction|Trajectory")
void PredictHit(bool& bHit, FHitResult& TraceResult, FVector& HitLocation, float& HitTime,
AActor*& HitActor, TArray<FVector>& Trajectory,
TSubclassOf<class AEBBullet> BulletClass,
const TArray<AActor*>& IgnoredActors,
float MaxTime = 10.0f, float Step = 0.1f) const;
UFUNCTION(BlueprintCallable, Category = "Ballistics|Prediction|Targeting")
void CalculateAimDirection(TSubclassOf<class AEBBullet> BulletClass, FVector TargetLocation, FVector TargetVelocity,
FVector& AimDirection, FVector& PredictedTargetLocation, FVector& PredictedIntersectionLocation,
float& PredictedFlightTime, float& Error, float MaxTime = 10.0f, float Step = 0.1f, int32 NumIterations = 4) const;
protected: protected:
virtual void BeginPlay() override; virtual void BeginPlay() override;
virtual void Tick(float DeltaTime) override; virtual void Tick(float DeltaTime) override;
#if WITH_EDITOR
virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;
#endif
// Replication // Replication
UFUNCTION() UFUNCTION()
void OnRep_GunState(); void OnRep_GunState();
@@ -193,12 +421,12 @@ protected:
// Firing Control // Firing Control
bool bTriggerPressed = false; bool bTriggerPressed = false;
bool bCanProcessNextShot = true;
int32 BurstShotsFired = 0; int32 BurstShotsFired = 0;
// Chambered Round
UPROPERTY(Replicated) // Random Stream for firing calculations
UEBBulletPropertiesAsset* ChamberedBulletType; FRandomStream RandomStream;
private: private:
EGunState PreviousGunState; EGunState PreviousGunState;
@@ -225,10 +453,37 @@ private:
UFUNCTION(Server, Reliable) UFUNCTION(Server, Reliable)
void ServerSetFireMode(EFireMode NewFireMode); void ServerSetFireMode(EFireMode NewFireMode);
UFUNCTION(Server, Reliable, WithValidation)
void ServerShoot(bool bTrigger);
UFUNCTION(Server, Reliable, WithValidation)
void ServerGatlingSpool(bool bSpool);
UFUNCTION(Server, Reliable, WithValidation)
void ServerSwitchFireMode(EFireMode NewFireMode);
// Server RPC Implementation Declarations
void ServerPullTrigger_Implementation();
void ServerReleaseTrigger_Implementation();
void ServerSetSafety_Implementation(bool bNewSafetyState);
void ServerEjectMagazine_Implementation();
void ServerInsertMagazine_Implementation(UEBMagazine* NewMagazine);
void ServerChargingHandle_Implementation();
void ServerSetFireMode_Implementation(EFireMode NewFireMode);
void ServerShoot_Implementation(bool bTrigger);
bool ServerShoot_Validate(bool bTrigger);
void ServerGatlingSpool_Implementation(bool bSpool);
bool ServerGatlingSpool_Validate(bool bSpool);
void ServerSwitchFireMode_Implementation(EFireMode NewFireMode);
bool ServerSwitchFireMode_Validate(EFireMode NewFireMode);
// Multicast Functions // Multicast Functions
UFUNCTION(NetMulticast, Reliable) UFUNCTION(NetMulticast, Reliable)
void MulticastOnGunFired(UEBBulletPropertiesAsset* BulletType); void MulticastOnGunFired(UEBBulletPropertiesAsset* BulletType);
UFUNCTION(NetMulticast, Reliable)
void MulticastShotFired();
UFUNCTION(NetMulticast, Reliable) UFUNCTION(NetMulticast, Reliable)
void MulticastOnMagazineChanged(UEBMagazine* NewMagazine); void MulticastOnMagazineChanged(UEBMagazine* NewMagazine);
@@ -0,0 +1,429 @@
// Copyright 2016 Mookie. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Components/SceneComponent.h"
#include "Engine/EngineTypes.h"
#include "EBGun.h"
#include "EBWeaponData.h"
#include "EBBulletProperties.h"
#include "NiagaraComponent.h"
#include "NiagaraFunctionLibrary.h"
#include "EBPlayerWeaponController.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnWeaponChanged, UEBWeaponData*, NewWeapon);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnWeaponFired, UEBWeaponData*, WeaponData);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnWeaponReloaded, UEBWeaponData*, WeaponData);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnWeaponEmpty, UEBWeaponData*, WeaponData);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnAmmoChanged, int32, CurrentAmmo, int32, ReserveAmmo);
/**
* Player Weapon Controller - AAA Game Industry Standard Scene Component
*
* This follows the pattern used in games like Call of Duty:
* - Single persistent weapon actor (never destroyed)
* - Weapon data assets for all variations
* - Hot-swap meshes, stats, and behavior
* - No actor spawning/despawning
* - Inventory is just array of weapon data
*
* Benefits:
* - Zero GC pressure from spawning/destroying
* - Instant weapon switching (just data swap)
* - Memory efficient
* - Network friendly
* - Scales to hundreds of weapons
* - Direct positioning control as scene component
* - Gun spawns directly on this component's transform
*/
UCLASS(Blueprintable, BlueprintType, ClassGroup=(EasyBallistics), meta=(BlueprintSpawnableComponent, DisplayName = "EB Player Weapon Controller", ToolTip = "Player weapon controller scene component for managing weapon inventory and interactions with direct positioning"))
class EASYBALLISTICS_API UEBPlayerWeaponController : public USceneComponent
{
GENERATED_BODY()
public:
UEBPlayerWeaponController();
// ========================================
// WEAPON SYSTEM
// ========================================
/** Single persistent weapon actor */
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_WeaponActor, Category = "Weapon System")
AEBGun* WeaponActor;
/** Current weapon data */
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_CurrentWeaponData, Category = "Weapon System")
UEBWeaponData* CurrentWeaponData;
/** Player's weapon inventory (just data assets) */
UPROPERTY(EditAnywhere, BlueprintReadWrite, ReplicatedUsing = OnRep_WeaponInventory, Category = "Weapon System", meta = (AllowedClasses = "EBWeaponData", DisplayName = "Weapon Inventory", ToolTip = "Array of weapon data assets available to the player"))
TArray<UEBWeaponData*> WeaponInventory;
/** Current weapon index in inventory */
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_CurrentWeaponIndex, Category = "Weapon System")
int32 CurrentWeaponIndex = 0;
/** Default weapon to spawn with */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Weapon System", meta = (AllowedClasses = "EBWeaponData", DisplayName = "Default Weapon", ToolTip = "The default weapon data asset to equip on spawn"))
UEBWeaponData* DefaultWeapon;
/** Auto-equip first weapon on begin play */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Weapon System")
bool bAutoEquipOnBeginPlay = true;
// ========================================
// AMMO SYSTEM
// ========================================
/** Current ammo in magazine */
UPROPERTY(BlueprintReadOnly, Replicated, Category = "Ammo System")
int32 CurrentAmmo = 0;
/** Reserve ammo */
UPROPERTY(BlueprintReadOnly, Replicated, Category = "Ammo System")
int32 ReserveAmmo = 0;
/** Infinite ammo cheat */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Ammo System")
bool bInfiniteAmmo = false;
// ========================================
// WEAPON SPAWN SETTINGS
// ========================================
/** Spawn transform offset relative to this component */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Weapon Spawn", meta = (DisplayName = "Weapon Spawn Offset", ToolTip = "Transform offset for spawning the weapon relative to this component"))
FTransform WeaponSpawnOffset = FTransform::Identity;
// ========================================
// NIAGARA VFX SYSTEM
// ========================================
/** Persistent Niagara components for weapon effects */
UPROPERTY(BlueprintReadOnly, Category = "Niagara VFX")
UNiagaraComponent* MuzzleFlashComponent;
UPROPERTY(BlueprintReadOnly, Category = "Niagara VFX")
UNiagaraComponent* MuzzleSmokeComponent;
UPROPERTY(BlueprintReadOnly, Category = "Niagara VFX")
UNiagaraComponent* BarrelHeatComponent;
UPROPERTY(BlueprintReadOnly, Category = "Niagara VFX")
UNiagaraComponent* AttachmentGlowComponent;
/** Current weapon heat level (0-1) */
UPROPERTY(BlueprintReadOnly, Replicated, Category = "Niagara VFX")
float CurrentHeatLevel = 0.0f;
/** Enable advanced VFX features */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Niagara VFX")
bool bEnableAdvancedVFX = true;
// ========================================
// INPUT ACTIONS
// ========================================
/** Primary fire */
UFUNCTION(BlueprintCallable, Category = "Weapon Input")
void Fire();
/** Stop firing */
UFUNCTION(BlueprintCallable, Category = "Weapon Input")
void StopFire();
/** Reload weapon */
UFUNCTION(BlueprintCallable, Category = "Weapon Input")
void Reload();
/** Switch to next weapon */
UFUNCTION(BlueprintCallable, Category = "Weapon Input")
void NextWeapon();
/** Switch to previous weapon */
UFUNCTION(BlueprintCallable, Category = "Weapon Input")
void PreviousWeapon();
/** Switch to specific weapon by index */
UFUNCTION(BlueprintCallable, Category = "Weapon Input")
void SwitchToWeapon(int32 WeaponIndex);
/** Switch to specific weapon by data */
UFUNCTION(BlueprintCallable, Category = "Weapon Input")
void SwitchToWeaponData(UEBWeaponData* WeaponData);
/** Toggle fire mode */
UFUNCTION(BlueprintCallable, Category = "Weapon Input")
void ToggleFireMode();
/** Toggle safety */
UFUNCTION(BlueprintCallable, Category = "Weapon Input")
void ToggleSafety();
// ========================================
// WEAPON MANAGEMENT
// ========================================
/** Add weapon to inventory */
UFUNCTION(BlueprintCallable, Category = "Weapon Management")
void AddWeapon(UEBWeaponData* WeaponData);
/** Remove weapon from inventory */
UFUNCTION(BlueprintCallable, Category = "Weapon Management")
void RemoveWeapon(UEBWeaponData* WeaponData);
/** Clear all weapons */
UFUNCTION(BlueprintCallable, Category = "Weapon Management")
void ClearWeapons();
/** Set entire weapon inventory */
UFUNCTION(BlueprintCallable, Category = "Weapon Management")
void SetWeaponInventory(const TArray<UEBWeaponData*>& NewInventory);
/** Apply weapon data to actor */
UFUNCTION(BlueprintCallable, Category = "Weapon Management")
void ApplyWeaponData(UEBWeaponData* WeaponData);
// ========================================
// AMMO MANAGEMENT
// ========================================
/** Add ammo to reserve */
UFUNCTION(BlueprintCallable, Category = "Ammo Management")
void AddAmmo(int32 Amount);
/** Set current ammo */
UFUNCTION(BlueprintCallable, Category = "Ammo Management")
void SetCurrentAmmo(int32 Amount);
/** Set reserve ammo */
UFUNCTION(BlueprintCallable, Category = "Ammo Management")
void SetReserveAmmo(int32 Amount);
/** Refill all ammo */
UFUNCTION(BlueprintCallable, Category = "Ammo Management")
void RefillAmmo();
// ========================================
// STATUS QUERIES
// ========================================
/** Can weapon fire */
UFUNCTION(BlueprintPure, Category = "Weapon Status")
bool CanFire() const;
/** Is weapon equipped */
UFUNCTION(BlueprintPure, Category = "Weapon Status")
bool HasWeapon() const;
/** Get current weapon data */
UFUNCTION(BlueprintPure, Category = "Weapon Status")
UEBWeaponData* GetCurrentWeaponData() const { return CurrentWeaponData; }
/** Get weapon count */
UFUNCTION(BlueprintPure, Category = "Weapon Status")
int32 GetWeaponCount() const { return WeaponInventory.Num(); }
/** Get weapon at index */
UFUNCTION(BlueprintPure, Category = "Weapon Status")
UEBWeaponData* GetWeaponAtIndex(int32 Index) const;
/** Find weapon index */
UFUNCTION(BlueprintPure, Category = "Weapon Status")
int32 FindWeaponIndex(UEBWeaponData* WeaponData) const;
/** Has weapon in inventory */
UFUNCTION(BlueprintPure, Category = "Weapon Status")
bool HasWeaponInInventory(UEBWeaponData* WeaponData) const;
/** Get current fire mode */
UFUNCTION(BlueprintPure, Category = "Weapon Status")
EFireMode GetFireMode() const;
/** Is safety on */
UFUNCTION(BlueprintPure, Category = "Weapon Status")
bool IsSafetyOn() const;
/** Get muzzle transform */
UFUNCTION(BlueprintPure, Category = "Weapon Status")
FTransform GetMuzzleTransform() const;
/** Get current heat level */
UFUNCTION(BlueprintPure, Category = "Weapon Status")
float GetHeatLevel() const { return CurrentHeatLevel; }
/** Is weapon overheated */
UFUNCTION(BlueprintPure, Category = "Weapon Status")
bool IsOverheated() const;
// ========================================
// DEBUG FUNCTIONS
// ========================================
/** Print comprehensive weapon debug information to log */
UFUNCTION(BlueprintCallable, Category = "Debug")
void PrintWeaponDebugInfo() const;
// ========================================
// NIAGARA VFX CONTROL
// ========================================
/** Trigger muzzle flash effect */
UFUNCTION(BlueprintCallable, Category = "Niagara VFX")
void TriggerMuzzleFlash();
/** Trigger shell ejection effect */
UFUNCTION(BlueprintCallable, Category = "Niagara VFX")
void TriggerShellEjection();
/** Spawn tracer effect */
UFUNCTION(BlueprintCallable, Category = "Niagara VFX")
void SpawnTracerEffect(const FVector& StartLocation, const FVector& EndLocation);
/** Spawn impact effect */
UFUNCTION(BlueprintCallable, Category = "Niagara VFX")
void SpawnImpactEffect(const FVector& ImpactLocation, const FVector& ImpactNormal, UPhysicalMaterial* SurfaceMaterial = nullptr);
/** Update weapon heat effects */
UFUNCTION(BlueprintCallable, Category = "Niagara VFX")
void UpdateHeatEffects();
/** Set Niagara parameter on all weapon effects */
UFUNCTION(BlueprintCallable, Category = "Niagara VFX")
void SetNiagaraFloatParameter(const FString& ParameterName, float Value);
/** Set Niagara vector parameter on all weapon effects */
UFUNCTION(BlueprintCallable, Category = "Niagara VFX")
void SetNiagaraVectorParameter(const FString& ParameterName, const FVector& Value);
/** Set Niagara color parameter on all weapon effects */
UFUNCTION(BlueprintCallable, Category = "Niagara VFX")
void SetNiagaraColorParameter(const FString& ParameterName, const FLinearColor& Value);
// ========================================
// EVENTS
// ========================================
/** Called when weapon changes */
UPROPERTY(BlueprintAssignable, Category = "Weapon Events")
FOnWeaponChanged OnWeaponChanged;
/** Called when weapon fires */
UPROPERTY(BlueprintAssignable, Category = "Weapon Events")
FOnWeaponFired OnWeaponFired;
/** Called when weapon reloads */
UPROPERTY(BlueprintAssignable, Category = "Weapon Events")
FOnWeaponReloaded OnWeaponReloaded;
/** Called when weapon is empty */
UPROPERTY(BlueprintAssignable, Category = "Weapon Events")
FOnWeaponEmpty OnWeaponEmpty;
/** Called when ammo changes */
UPROPERTY(BlueprintAssignable, Category = "Weapon Events")
FOnAmmoChanged OnAmmoChanged;
/** Called when WeaponActor is replicated */
UFUNCTION()
void OnRep_WeaponActor();
// ========================================
// OVERRIDES
// ========================================
virtual void BeginPlay() override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
#if WITH_EDITOR
virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;
#endif
protected:
// ========================================
// INTERNAL FUNCTIONS
// ========================================
/** Create weapon actor */
void CreateWeaponActor();
/** Apply weapon data to gun actor */
void InternalApplyWeaponData(UEBWeaponData* WeaponData);
/** Update ammo from weapon data */
void UpdateAmmoFromWeaponData();
/** Handle weapon fired */
UFUNCTION()
void HandleWeaponFired(UEBBulletPropertiesAsset* BulletType);
/** Handle magazine empty */
UFUNCTION()
void HandleMagazineEmpty();
/** Bind weapon events */
void BindWeaponEvents();
/** Unbind weapon events */
void UnbindWeaponEvents();
/** Check authority */
bool HasAuthority() const;
/** Create Niagara components */
void CreateNiagaraComponents();
/** Update Niagara components with weapon data */
void UpdateNiagaraComponents();
/** Apply Niagara parameters from weapon data */
void ApplyNiagaraParameters(UNiagaraComponent* Component, const FNiagaraEffectParams& Params);
/** Update heat level and effects */
void UpdateWeaponHeat(float DeltaTime);
private:
// ========================================
// SERVER RPCS
// ========================================
UFUNCTION(Server, Reliable)
void ServerSwitchToWeapon(int32 WeaponIndex);
UFUNCTION(Server, Reliable)
void ServerSwitchToWeaponData(UEBWeaponData* WeaponData);
UFUNCTION(Server, Reliable)
void ServerAddWeapon(UEBWeaponData* WeaponData);
UFUNCTION(Server, Reliable)
void ServerRemoveWeapon(UEBWeaponData* WeaponData);
UFUNCTION(Server, Reliable)
void ServerSetCurrentAmmo(int32 Amount);
UFUNCTION(Server, Reliable)
void ServerSetReserveAmmo(int32 Amount);
UFUNCTION(Server, Reliable)
void ServerToggleFireMode();
UFUNCTION(Server, Reliable)
void ServerToggleSafety();
// ========================================
// REPLICATION
// ========================================
UFUNCTION()
void OnRep_CurrentWeaponData();
UFUNCTION()
void OnRep_WeaponInventory();
UFUNCTION()
void OnRep_CurrentWeaponIndex();
};
+619
View File
@@ -0,0 +1,619 @@
// Copyright 2016 Mookie. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "Engine/SkeletalMesh.h"
#include "Engine/StaticMesh.h"
#include "Animation/AnimSequence.h"
#include "Animation/AnimBlueprint.h"
#include "Sound/SoundCue.h"
#include "NiagaraSystem.h"
#include "NiagaraComponent.h"
#include "NiagaraFunctionLibrary.h"
#include "NiagaraDataInterface.h"
#include "EBBulletProperties.h"
#include "EBBarrel.h"
#include "EBWeaponData.generated.h"
/**
* Weapon Type Classification
*/
UENUM(BlueprintType)
enum class EWeaponType : uint8
{
Rifle UMETA(DisplayName = "Rifle"),
Pistol UMETA(DisplayName = "Pistol"),
Shotgun UMETA(DisplayName = "Shotgun"),
SMG UMETA(DisplayName = "SMG"),
LMG UMETA(DisplayName = "LMG"),
Sniper UMETA(DisplayName = "Sniper"),
Launcher UMETA(DisplayName = "Launcher"),
Melee UMETA(DisplayName = "Melee"),
Special UMETA(DisplayName = "Special")
};
/**
* Weapon Audio Configuration
*/
USTRUCT(BlueprintType)
struct FWeaponAudio
{
GENERATED_USTRUCT_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Audio")
USoundCue* FireSound;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Audio")
USoundCue* EmptyFireSound;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Audio")
USoundCue* ReloadSound;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Audio")
USoundCue* EquipSound;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Audio")
USoundCue* UnequipSound;
FWeaponAudio()
{
FireSound = nullptr;
EmptyFireSound = nullptr;
ReloadSound = nullptr;
EquipSound = nullptr;
UnequipSound = nullptr;
}
};
/**
* Niagara Effect Parameters for Dynamic Control
*/
USTRUCT(BlueprintType)
struct FNiagaraEffectParams
{
GENERATED_USTRUCT_BODY()
/** Effect intensity multiplier */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Parameters", meta = (ClampMin = "0.0", ClampMax = "5.0"))
float Intensity = 1.0f;
/** Effect scale multiplier */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Parameters", meta = (ClampMin = "0.1", ClampMax = "10.0"))
float Scale = 1.0f;
/** Effect color tint */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Parameters")
FLinearColor ColorTint = FLinearColor::White;
/** Custom float parameters */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Parameters")
TMap<FString, float> FloatParameters;
/** Custom vector parameters */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Parameters")
TMap<FString, FVector> VectorParameters;
/** Custom boolean parameters */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Parameters")
TMap<FString, bool> BoolParameters;
};
/**
* Advanced Niagara Weapon Visual Effects Configuration
*/
USTRUCT(BlueprintType)
struct FWeaponVFX
{
GENERATED_USTRUCT_BODY()
// === MUZZLE EFFECTS ===
/** Primary muzzle flash effect */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VFX|Muzzle")
UNiagaraSystem* MuzzleFlash;
/** Muzzle flash parameters */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VFX|Muzzle")
FNiagaraEffectParams MuzzleFlashParams;
/** Muzzle smoke effect */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VFX|Muzzle")
UNiagaraSystem* MuzzleSmoke;
/** Muzzle smoke parameters */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VFX|Muzzle")
FNiagaraEffectParams MuzzleSmokeParams;
/** Suppressed muzzle effect (when suppressor attached) */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VFX|Muzzle")
UNiagaraSystem* SuppressedMuzzleFlash;
// === SHELL EJECTION ===
/** Ejected shell/casing effect */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VFX|Shells")
UNiagaraSystem* EjectedShell;
/** Shell ejection parameters */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VFX|Shells")
FNiagaraEffectParams EjectedShellParams;
/** Shell ejection velocity */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VFX|Shells")
FVector ShellEjectionVelocity = FVector(200.0f, 100.0f, 50.0f);
/** Shell physics material for bouncing sounds */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VFX|Shells")
UPhysicalMaterial* ShellPhysicsMaterial;
// === PROJECTILE EFFECTS ===
/** Bullet tracer effect */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VFX|Projectile")
UNiagaraSystem* TracerEffect;
/** Tracer parameters */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VFX|Projectile")
FNiagaraEffectParams TracerParams;
/** Bullet trail/wake effect */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VFX|Projectile")
UNiagaraSystem* BulletTrail;
/** Supersonic crack effect */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VFX|Projectile")
UNiagaraSystem* SonicCrack;
// === IMPACT EFFECTS ===
/** Default hit impact effect */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VFX|Impact")
UNiagaraSystem* HitImpact;
/** Impact parameters */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VFX|Impact")
FNiagaraEffectParams HitImpactParams;
/** Material-specific impact effects */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VFX|Impact")
TMap<UPhysicalMaterial*, UNiagaraSystem*> MaterialImpactEffects;
/** Penetration effect (when bullet goes through) */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VFX|Impact")
UNiagaraSystem* PenetrationEffect;
/** Ricochet effect */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VFX|Impact")
UNiagaraSystem* RicochetEffect;
// === ADVANCED SETTINGS ===
/** Enable dynamic weather effects (rain interaction, etc.) */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VFX|Advanced")
bool bEnableWeatherEffects = true;
/** Enable heat distortion from barrel */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VFX|Advanced")
bool bEnableHeatDistortion = true;
/** Heat buildup effect from sustained fire */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VFX|Advanced")
UNiagaraSystem* BarrelHeatEffect;
/** Weapon attachment glow/energy effects */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VFX|Advanced")
UNiagaraSystem* AttachmentGlowEffect;
/** LOD settings for performance */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VFX|Advanced")
float EffectLODDistance = 1000.0f;
FWeaponVFX()
{
// Initialize all effects to null
MuzzleFlash = nullptr;
MuzzleSmoke = nullptr;
SuppressedMuzzleFlash = nullptr;
EjectedShell = nullptr;
TracerEffect = nullptr;
BulletTrail = nullptr;
SonicCrack = nullptr;
HitImpact = nullptr;
PenetrationEffect = nullptr;
RicochetEffect = nullptr;
BarrelHeatEffect = nullptr;
AttachmentGlowEffect = nullptr;
ShellPhysicsMaterial = nullptr;
ShellEjectionVelocity = FVector(200.0f, 100.0f, 50.0f);
bEnableWeatherEffects = true;
bEnableHeatDistortion = true;
EffectLODDistance = 1000.0f;
}
};
/**
* Weapon Animation Configuration
*/
USTRUCT(BlueprintType)
struct FWeaponAnimations
{
GENERATED_USTRUCT_BODY()
/** First person weapon animations */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animations|FP")
UAnimSequence* FP_Fire;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animations|FP")
UAnimSequence* FP_Reload;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animations|FP")
UAnimSequence* FP_Equip;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animations|FP")
UAnimSequence* FP_Idle;
/** Third person character animations */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animations|TP")
UAnimSequence* TP_Fire;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animations|TP")
UAnimSequence* TP_Reload;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animations|TP")
UAnimSequence* TP_Equip;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animations|TP")
UAnimSequence* TP_Idle;
/** Animation blueprint for complex animations */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animations")
TSubclassOf<UAnimInstance> WeaponAnimBP;
FWeaponAnimations()
{
FP_Fire = nullptr;
FP_Reload = nullptr;
FP_Equip = nullptr;
FP_Idle = nullptr;
TP_Fire = nullptr;
TP_Reload = nullptr;
TP_Equip = nullptr;
TP_Idle = nullptr;
WeaponAnimBP = nullptr;
}
};
/**
* Weapon Data Asset - Industry Standard Approach
*
* This is how AAA games handle weapons:
* - Single persistent weapon actor
* - All weapon variations as data assets
* - Hot-swap meshes, stats, behavior
* - No actor spawning/despawning
* - Inventory is just array of data references
*/
UCLASS(BlueprintType, Blueprintable)
class EASYBALLISTICS_API UEBWeaponData : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
UEBWeaponData();
// ========================================
// WEAPON IDENTITY
// ========================================
/** Weapon display name */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Identity")
FText WeaponName;
/** Weapon description */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Identity")
FText WeaponDescription;
/** Weapon type classification */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Identity")
EWeaponType WeaponType;
/** Weapon icon for UI */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Identity")
UTexture2D* WeaponIcon;
/** Weapon rarity/tier */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Identity")
int32 WeaponTier = 1;
// ========================================
// VISUAL COMPONENTS
// ========================================
/** First person weapon mesh */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Visuals")
USkeletalMesh* FirstPersonMesh;
/** Third person weapon mesh */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Visuals")
USkeletalMesh* ThirdPersonMesh;
/** Static mesh fallback */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Visuals")
UStaticMesh* StaticMesh;
/** Material overrides for weapon customization */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Visuals")
TArray<UMaterialInterface*> MaterialOverrides;
// ========================================
// BALLISTICS & PERFORMANCE
// ========================================
/** Bullet properties for this weapon */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Ballistics")
UEBBulletPropertiesAsset* BulletProperties;
/** Damage per shot */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Ballistics", meta = (ClampMin = "1", ClampMax = "1000"))
float Damage = 30.0f;
/** Fire rate (rounds per minute) */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Ballistics", meta = (ClampMin = "1", ClampMax = "1200"))
float FireRate = 600.0f;
/** Muzzle velocity in cm/s */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Ballistics", meta = (ClampMin = "10000", ClampMax = "200000"))
float MuzzleVelocity = 91440.0f;
/** Weapon accuracy (0-1, 1 = perfect) */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Ballistics", meta = (ClampMin = "0.0", ClampMax = "1.0"))
float Accuracy = 0.95f;
/** Recoil strength */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Ballistics", meta = (ClampMin = "0.0", ClampMax = "10.0"))
float RecoilStrength = 1.0f;
/** Range in meters */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Ballistics", meta = (ClampMin = "10", ClampMax = "2000"))
float EffectiveRange = 300.0f;
// ========================================
// MAGAZINE & AMMO
// ========================================
/** Magazine capacity */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Magazine", meta = (ClampMin = "1", ClampMax = "200"))
int32 MagazineSize = 30;
/** Reserve ammo capacity */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Magazine", meta = (ClampMin = "0", ClampMax = "1000"))
int32 MaxReserveAmmo = 120;
/** Reload time in seconds */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Magazine", meta = (ClampMin = "0.5", ClampMax = "10.0"))
float ReloadTime = 2.5f;
/** Time for tactical reload (magazine not empty) */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Magazine", meta = (ClampMin = "0.5", ClampMax = "10.0"))
float TacticalReloadTime = 2.0f;
// ========================================
// FIRE MODES
// ========================================
/** Available fire modes */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Fire Modes")
TArray<EFireMode> AvailableFireModes;
/** Default fire mode */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Fire Modes")
EFireMode DefaultFireMode = EFireMode::FM_Auto;
/** Burst count for burst fire */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Fire Modes", meta = (ClampMin = "2", ClampMax = "10"))
int32 BurstCount = 3;
// ========================================
// ATTACHMENTS & CUSTOMIZATION
// ========================================
/** Socket names for attachments */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Attachments")
FName MuzzleSocketName = TEXT("MuzzleSocket");
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Attachments")
FName ScopeSocketName = TEXT("ScopeSocket");
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Attachments")
FName GripSocketName = TEXT("GripSocket");
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Attachments")
FName LaserSocketName = TEXT("LaserSocket");
/** Supported attachment types */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Attachments")
TArray<FString> SupportedAttachments;
// ========================================
// AUDIO & VFX
// ========================================
/** Audio configuration */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Audio & VFX")
FWeaponAudio AudioConfig;
/** Niagara visual effects configuration */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Audio & VFX")
FWeaponVFX VFXConfig;
// ========================================
// NIAGARA ADVANCED FEATURES
// ========================================
/** Global Niagara parameters for this weapon */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Niagara Advanced")
FNiagaraEffectParams GlobalEffectParams;
/** Enable GPU simulation for high particle counts */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Niagara Advanced")
bool bUseGPUSimulation = true;
/** Enable distance-based LOD for effects */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Niagara Advanced")
bool bEnableEffectLOD = true;
/** Custom data interface for weapon stats */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Niagara Advanced")
TSubclassOf<UNiagaraDataInterface> WeaponDataInterface;
/** Heat tracking for barrel effects */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Niagara Advanced")
float MaxHeatLevel = 100.0f;
/** Heat decay rate per second */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Niagara Advanced")
float HeatDecayRate = 10.0f;
/** Heat gain per shot */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Niagara Advanced")
float HeatPerShot = 5.0f;
/** Animation configuration */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Audio & VFX")
FWeaponAnimations AnimationConfig;
// ========================================
// ADVANCED SETTINGS
// ========================================
/** Weapon weight (affects movement speed) */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Advanced", meta = (ClampMin = "0.5", ClampMax = "20.0"))
float WeaponWeight = 3.5f;
/** ADS (Aim Down Sights) time */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Advanced", meta = (ClampMin = "0.1", ClampMax = "2.0"))
float ADSTime = 0.3f;
/** Sprint-to-fire time */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Advanced", meta = (ClampMin = "0.1", ClampMax = "1.0"))
float SprintToFireTime = 0.25f;
/** Draw/Equip time */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Advanced", meta = (ClampMin = "0.2", ClampMax = "3.0"))
float EquipTime = 0.75f;
/** Whether weapon can be dual-wielded */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Advanced")
bool bCanDualWield = false;
/** Whether weapon has safety */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Advanced")
bool bHasSafety = true;
/** Whether weapon supports suppressors */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Advanced")
bool bSuppressorCompatible = true;
// ========================================
// DEBUG SETTINGS
// ========================================
/** Enable debug logging for weapon firing */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Debug")
bool bEnableFireDebug = false;
/** Size of debug arrows for ballistic visualization */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Debug", meta = (ClampMin = "10.0", ClampMax = "500.0"))
float DebugArrowSize = 100.0f;
/** Show impact information in debug display */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Debug")
bool bDebugImpactInfo = false;
/** Enable trajectory debug visualization */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Debug")
bool bDebugTrajectory = false;
/** Enable physics debug visualization (drag, gravity, wind) */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Debug")
bool bDebugPhysics = false;
/** Enable performance debug information */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Debug")
bool bDebugPerformance = false;
/** Enable ballistics debug visualization */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Debug")
bool bDebugBallistics = false;
/** Enable spalling debug visualization */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Debug")
bool bDebugSpalling = false;
// ========================================
// UTILITY FUNCTIONS
// ========================================
/** Get fire rate in shots per second */
UFUNCTION(BlueprintPure, Category = "Weapon Data")
float GetFireRatePerSecond() const { return FireRate / 60.0f; }
/** Get time between shots */
UFUNCTION(BlueprintPure, Category = "Weapon Data")
float GetTimeBetweenShots() const { return 60.0f / FireRate; }
/** Get DPS (Damage Per Second) */
UFUNCTION(BlueprintPure, Category = "Weapon Data")
float GetDPS() const { return Damage * GetFireRatePerSecond(); }
/** Check if fire mode is supported */
UFUNCTION(BlueprintPure, Category = "Weapon Data")
bool IsFireModeSupported(EFireMode FireMode) const { return AvailableFireModes.Contains(FireMode); }
/** Get weapon type as string */
UFUNCTION(BlueprintPure, Category = "Weapon Data")
FString GetWeaponTypeString() const;
/** Get current heat level (0-1) */
UFUNCTION(BlueprintPure, Category = "Weapon Data")
float GetHeatLevel() const { return CurrentHeat / MaxHeatLevel; }
/** Add heat from firing */
UFUNCTION(BlueprintCallable, Category = "Weapon Data")
void AddHeat(float Amount = -1.0f);
/** Update heat decay */
UFUNCTION(BlueprintCallable, Category = "Weapon Data")
void UpdateHeat(float DeltaTime);
/** Is weapon overheated */
UFUNCTION(BlueprintPure, Category = "Weapon Data")
bool IsOverheated() const { return CurrentHeat >= MaxHeatLevel; }
// ========================================
// DEBUG UTILITY FUNCTIONS
// ========================================
/** Enable/disable specific bullet debug category */
UFUNCTION(BlueprintCallable, Category = "Debug")
void SetBulletDebugCategory(const FString& CategoryName, bool bEnabled);
/** Enable/disable all bullet debug categories */
UFUNCTION(BlueprintCallable, Category = "Debug")
void SetAllBulletDebugCategories(bool bEnabled);
/** Get current debug configuration as string */
UFUNCTION(BlueprintPure, Category = "Debug")
FString GetBulletDebugInfo() const;
/** Apply debug settings to connected barrel component */
UFUNCTION(BlueprintCallable, Category = "Debug")
void ApplyDebugSettingsToBarrel(class UEBBarrel* Barrel) const;
private:
/** Current heat level for effects */
float CurrentHeat = 0.0f;
/** Get primary data asset ID */
virtual FPrimaryAssetId GetPrimaryAssetId() const override
{
return FPrimaryAssetId("WeaponData", GetFName());
}
};
@@ -21,12 +21,13 @@ public class EasyBallisticsEditor : ModuleRules
"EditorStyle", "EditorStyle",
"EditorSubsystem", "EditorSubsystem",
"LevelEditor", "LevelEditor",
"SceneOutliner" "SceneOutliner",
"InputCore",
"Slate",
"SlateCore"
}); });
PrivateDependencyModuleNames.AddRange(new string[] { PrivateDependencyModuleNames.AddRange(new string[] {
"Slate",
"SlateCore",
"PropertyEditor", "PropertyEditor",
"WorkspaceMenuStructure", "WorkspaceMenuStructure",
"DesktopPlatform", "DesktopPlatform",
@@ -2,6 +2,7 @@
#include "EBBarrelComponentFactory.h" #include "EBBarrelComponentFactory.h"
#include "EBBarrel.h" #include "EBBarrel.h"
#include "EBGun.h"
#include "Engine/Selection.h" #include "Engine/Selection.h"
#include "Components/SceneComponent.h" #include "Components/SceneComponent.h"
#include "DrawDebugHelpers.h" #include "DrawDebugHelpers.h"
@@ -25,15 +26,21 @@ void FEBBarrelComponentVisualizer::DrawVisualization(const UActorComponent* Comp
// Draw barrel representation // Draw barrel representation
FColor BarrelColor = FColor::Blue; FColor BarrelColor = FColor::Blue;
if (BarrelComponent->ChamberedBullet)
const AEBGun* ParentGun = BarrelComponent->GetParentGun();
if (ParentGun)
{ {
BarrelColor = FColor::Green; // Loaded if (ParentGun->bChamberedRound)
} {
else if (BarrelComponent->Shooting) BarrelColor = FColor::Green; // Loaded
{ }
BarrelColor = FColor::Red; // Firing else if (ParentGun->CurrentGunState == EGunState::GS_Firing)
{
BarrelColor = FColor::Red; // Firing
}
} }
// Draw barrel cylinder // Draw barrel cylinder
float BarrelLength = FMath::Max(BarrelComponent->BarrelLength, 10.0f); // Minimum visual length float BarrelLength = FMath::Max(BarrelComponent->BarrelLength, 10.0f); // Minimum visual length
float BarrelRadius = 2.0f; float BarrelRadius = 2.0f;
@@ -47,7 +54,7 @@ void FEBBarrelComponentVisualizer::DrawVisualization(const UActorComponent* Comp
DrawWireBox(PDI, FBox(MuzzleLocation - FVector(2, 2, 2), MuzzleLocation + FVector(2, 2, 2)), FColor::Orange, SDPG_Foreground); DrawWireBox(PDI, FBox(MuzzleLocation - FVector(2, 2, 2), MuzzleLocation + FVector(2, 2, 2)), FColor::Orange, SDPG_Foreground);
// Draw firing direction indicator // Draw firing direction indicator
if (BarrelComponent->Shooting) if (ParentGun && ParentGun->CurrentGunState == EGunState::GS_Firing)
{ {
FVector FireDirection = BarrelEnd + (Forward * 20.0f); FVector FireDirection = BarrelEnd + (Forward * 20.0f);
PDI->DrawLine(BarrelEnd, FireDirection, FColor::Red, SDPG_Foreground, 2.0f); PDI->DrawLine(BarrelEnd, FireDirection, FColor::Red, SDPG_Foreground, 2.0f);
@@ -77,10 +84,10 @@ void FEBBarrelComponentVisualizer::DrawVisualization(const UActorComponent* Comp
} }
// Draw ammo count indicator // Draw ammo count indicator
if (BarrelComponent->Ammo.Num() > 0) if (ParentGun && ParentGun->GetRoundsInMagazine() > 0)
{ {
FVector AmmoIndicatorLocation = Location + (Right * 10.0f); FVector AmmoIndicatorLocation = Location + (Right * 10.0f);
for (int32 i = 0; i < FMath::Min(BarrelComponent->Ammo.Num(), 10); i++) for (int32 i = 0; i < FMath::Min(ParentGun->GetRoundsInMagazine(), 10); i++)
{ {
FVector AmmoLocation = AmmoIndicatorLocation + (Up * i * 3.0f); FVector AmmoLocation = AmmoIndicatorLocation + (Up * i * 3.0f);
FVector BoxExtent = FVector(0.5f, 0.5f, 1.0f); FVector BoxExtent = FVector(0.5f, 0.5f, 1.0f);
@@ -0,0 +1,36 @@
// Copyright 2016 Mookie. All Rights Reserved.
#include "EBPlayerWeaponControllerCustomization.h"
#include "DetailLayoutBuilder.h"
#include "DetailCategoryBuilder.h"
#include "DetailWidgetRow.h"
#include "IDetailPropertyRow.h"
#include "Widgets/Text/STextBlock.h"
#include "EBPlayerWeaponController.h"
#define LOCTEXT_NAMESPACE "EBPlayerWeaponControllerCustomization"
TSharedRef<IDetailCustomization> FEBPlayerWeaponControllerCustomization::MakeInstance()
{
return MakeShareable(new FEBPlayerWeaponControllerCustomization);
}
void FEBPlayerWeaponControllerCustomization::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder)
{
// Since the weapon controller is now a scene component, we don't need complex attachment customization
// The component can be positioned directly in the viewport
// Add a help text to guide users
IDetailCategoryBuilder& WeaponSpawnCategory = DetailBuilder.EditCategory("Weapon Spawn");
WeaponSpawnCategory.AddCustomRow(LOCTEXT("PositioningHelp", "Positioning Help"))
.WholeRowContent()
[
SNew(STextBlock)
.Text(LOCTEXT("PositioningHelpText", "Position this component in the viewport where you want weapons to appear. You can attach it to bones/sockets for animation."))
.Font(IDetailLayoutBuilder::GetDetailFont())
.AutoWrapText(true)
];
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,248 @@
// Copyright 2016 Mookie. All Rights Reserved.
#include "EBWeaponDataFactory.h"
#include "EBWeaponData.h"
#include "EBWeaponConfiguration.h"
#include "AssetToolsModule.h"
#include "EasyBallisticsEditor.h"
#define LOCTEXT_NAMESPACE "EBWeaponDataFactory"
UEBWeaponDataFactory::UEBWeaponDataFactory()
{
bCreateNew = true;
bEditAfterNew = true;
SupportedClass = UEBWeaponData::StaticClass();
}
UObject* UEBWeaponDataFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
{
UEBWeaponData* WeaponData = NewObject<UEBWeaponData>(InParent, Class, Name, Flags);
if (WeaponData)
{
// Set up default configuration based on name
FString AssetName = Name.ToString();
if (AssetName.Contains(TEXT("M4")) || AssetName.Contains(TEXT("AR15")) || AssetName.Contains(TEXT("Rifle")))
{
// Configure for M4/AR-15 type weapon
WeaponData->WeaponName = FText::FromString(TEXT("M4 Carbine"));
WeaponData->WeaponDescription = FText::FromString(TEXT("5.56mm assault rifle with selective fire capabilities"));
WeaponData->WeaponType = EWeaponType::Rifle;
WeaponData->WeaponTier = 3;
WeaponData->Damage = 35.0f;
WeaponData->FireRate = 750.0f;
WeaponData->MuzzleVelocity = 91440.0f; // ~3000 fps
WeaponData->Accuracy = 0.92f;
WeaponData->RecoilStrength = 1.2f;
WeaponData->EffectiveRange = 400.0f;
WeaponData->MagazineSize = 30;
WeaponData->MaxReserveAmmo = 180;
WeaponData->ReloadTime = 2.5f;
WeaponData->TacticalReloadTime = 2.0f;
WeaponData->AvailableFireModes.Add(EFireMode::FM_Semiauto);
WeaponData->AvailableFireModes.Add(EFireMode::FM_Auto);
WeaponData->DefaultFireMode = EFireMode::FM_Auto;
WeaponData->WeaponWeight = 3.4f;
WeaponData->ADSTime = 0.35f;
WeaponData->EquipTime = 0.8f;
}
else if (AssetName.Contains(TEXT("AK")) || AssetName.Contains(TEXT("74")))
{
// Configure for AK type weapon
WeaponData->WeaponName = FText::FromString(TEXT("AK-74"));
WeaponData->WeaponDescription = FText::FromString(TEXT("5.45mm assault rifle with proven reliability"));
WeaponData->WeaponType = EWeaponType::Rifle;
WeaponData->WeaponTier = 3;
WeaponData->Damage = 32.0f;
WeaponData->FireRate = 650.0f;
WeaponData->MuzzleVelocity = 88500.0f; // ~2900 fps
WeaponData->Accuracy = 0.88f;
WeaponData->RecoilStrength = 1.4f;
WeaponData->EffectiveRange = 350.0f;
WeaponData->MagazineSize = 30;
WeaponData->MaxReserveAmmo = 180;
WeaponData->ReloadTime = 2.8f;
WeaponData->TacticalReloadTime = 2.2f;
WeaponData->AvailableFireModes.Add(EFireMode::FM_Semiauto);
WeaponData->AvailableFireModes.Add(EFireMode::FM_Auto);
WeaponData->DefaultFireMode = EFireMode::FM_Auto;
WeaponData->WeaponWeight = 3.6f;
WeaponData->ADSTime = 0.4f;
WeaponData->EquipTime = 0.9f;
}
else if (AssetName.Contains(TEXT("Pistol")) || AssetName.Contains(TEXT("Glock")) || AssetName.Contains(TEXT("1911")))
{
// Configure for pistol
WeaponData->WeaponName = FText::FromString(TEXT("M1911"));
WeaponData->WeaponDescription = FText::FromString(TEXT(".45 ACP semi-automatic pistol"));
WeaponData->WeaponType = EWeaponType::Pistol;
WeaponData->WeaponTier = 2;
WeaponData->Damage = 55.0f;
WeaponData->FireRate = 350.0f;
WeaponData->MuzzleVelocity = 76200.0f; // ~2500 fps
WeaponData->Accuracy = 0.85f;
WeaponData->RecoilStrength = 2.0f;
WeaponData->EffectiveRange = 80.0f;
WeaponData->MagazineSize = 7;
WeaponData->MaxReserveAmmo = 35;
WeaponData->ReloadTime = 1.8f;
WeaponData->TacticalReloadTime = 1.5f;
WeaponData->AvailableFireModes.Add(EFireMode::FM_Semiauto);
WeaponData->DefaultFireMode = EFireMode::FM_Semiauto;
WeaponData->WeaponWeight = 1.1f;
WeaponData->ADSTime = 0.2f;
WeaponData->EquipTime = 0.4f;
}
else if (AssetName.Contains(TEXT("Sniper")) || AssetName.Contains(TEXT("Bolt")))
{
// Configure for bolt-action sniper rifle
WeaponData->WeaponName = FText::FromString(TEXT("M24 SWS"));
WeaponData->WeaponDescription = FText::FromString(TEXT("7.62mm bolt-action sniper rifle"));
WeaponData->WeaponType = EWeaponType::Sniper;
WeaponData->WeaponTier = 4;
WeaponData->Damage = 120.0f;
WeaponData->FireRate = 60.0f;
WeaponData->MuzzleVelocity = 108000.0f; // ~3540 fps
WeaponData->Accuracy = 0.98f;
WeaponData->RecoilStrength = 3.5f;
WeaponData->EffectiveRange = 800.0f;
WeaponData->MagazineSize = 5;
WeaponData->MaxReserveAmmo = 30;
WeaponData->ReloadTime = 3.5f;
WeaponData->TacticalReloadTime = 3.0f;
WeaponData->AvailableFireModes.Add(EFireMode::FM_Manual);
WeaponData->DefaultFireMode = EFireMode::FM_Manual;
WeaponData->WeaponWeight = 5.5f;
WeaponData->ADSTime = 0.6f;
WeaponData->EquipTime = 1.2f;
}
else if (AssetName.Contains(TEXT("Shotgun")))
{
// Configure for shotgun
WeaponData->WeaponName = FText::FromString(TEXT("M870"));
WeaponData->WeaponDescription = FText::FromString(TEXT("12-gauge pump-action shotgun"));
WeaponData->WeaponType = EWeaponType::Shotgun;
WeaponData->WeaponTier = 2;
WeaponData->Damage = 80.0f;
WeaponData->FireRate = 120.0f;
WeaponData->MuzzleVelocity = 38100.0f; // ~1250 fps
WeaponData->Accuracy = 0.6f;
WeaponData->RecoilStrength = 4.0f;
WeaponData->EffectiveRange = 50.0f;
WeaponData->MagazineSize = 8;
WeaponData->MaxReserveAmmo = 40;
WeaponData->ReloadTime = 4.0f;
WeaponData->TacticalReloadTime = 3.5f;
WeaponData->AvailableFireModes.Add(EFireMode::FM_Manual);
WeaponData->DefaultFireMode = EFireMode::FM_Manual;
WeaponData->WeaponWeight = 3.2f;
WeaponData->ADSTime = 0.45f;
WeaponData->EquipTime = 0.9f;
}
else if (AssetName.Contains(TEXT("LMG")) || AssetName.Contains(TEXT("Machine")))
{
// Configure for light machine gun
WeaponData->WeaponName = FText::FromString(TEXT("M249 SAW"));
WeaponData->WeaponDescription = FText::FromString(TEXT("5.56mm light machine gun"));
WeaponData->WeaponType = EWeaponType::LMG;
WeaponData->WeaponTier = 4;
WeaponData->Damage = 28.0f;
WeaponData->FireRate = 850.0f;
WeaponData->MuzzleVelocity = 91440.0f; // ~3000 fps
WeaponData->Accuracy = 0.78f;
WeaponData->RecoilStrength = 2.5f;
WeaponData->EffectiveRange = 600.0f;
WeaponData->MagazineSize = 200;
WeaponData->MaxReserveAmmo = 400;
WeaponData->ReloadTime = 6.0f;
WeaponData->TacticalReloadTime = 5.0f;
WeaponData->AvailableFireModes.Add(EFireMode::FM_Auto);
WeaponData->DefaultFireMode = EFireMode::FM_Auto;
WeaponData->WeaponWeight = 7.5f;
WeaponData->ADSTime = 0.8f;
WeaponData->EquipTime = 1.5f;
}
else if (AssetName.Contains(TEXT("SMG")) || AssetName.Contains(TEXT("Sub")))
{
// Configure for submachine gun
WeaponData->WeaponName = FText::FromString(TEXT("MP5"));
WeaponData->WeaponDescription = FText::FromString(TEXT("9mm submachine gun"));
WeaponData->WeaponType = EWeaponType::SMG;
WeaponData->WeaponTier = 2;
WeaponData->Damage = 25.0f;
WeaponData->FireRate = 800.0f;
WeaponData->MuzzleVelocity = 76200.0f; // ~2500 fps
WeaponData->Accuracy = 0.82f;
WeaponData->RecoilStrength = 0.8f;
WeaponData->EffectiveRange = 150.0f;
WeaponData->MagazineSize = 30;
WeaponData->MaxReserveAmmo = 120;
WeaponData->ReloadTime = 2.2f;
WeaponData->TacticalReloadTime = 1.8f;
WeaponData->AvailableFireModes.Add(EFireMode::FM_Semiauto);
WeaponData->AvailableFireModes.Add(EFireMode::FM_Auto);
WeaponData->DefaultFireMode = EFireMode::FM_Auto;
WeaponData->WeaponWeight = 2.5f;
WeaponData->ADSTime = 0.25f;
WeaponData->EquipTime = 0.6f;
}
else
{
// Default generic weapon
WeaponData->WeaponName = FText::FromString(TEXT("Generic Weapon"));
WeaponData->WeaponDescription = FText::FromString(TEXT("A basic weapon configuration"));
WeaponData->WeaponType = EWeaponType::Rifle;
WeaponData->WeaponTier = 1;
WeaponData->Damage = 30.0f;
WeaponData->FireRate = 600.0f;
WeaponData->MuzzleVelocity = 91440.0f;
WeaponData->Accuracy = 0.85f;
WeaponData->RecoilStrength = 1.0f;
WeaponData->EffectiveRange = 300.0f;
WeaponData->MagazineSize = 30;
WeaponData->MaxReserveAmmo = 120;
WeaponData->ReloadTime = 2.5f;
WeaponData->TacticalReloadTime = 2.0f;
WeaponData->AvailableFireModes.Add(EFireMode::FM_Semiauto);
WeaponData->AvailableFireModes.Add(EFireMode::FM_Auto);
WeaponData->DefaultFireMode = EFireMode::FM_Auto;
WeaponData->WeaponWeight = 3.5f;
WeaponData->ADSTime = 0.3f;
WeaponData->EquipTime = 0.75f;
}
// Set common default values
WeaponData->MuzzleSocketName = TEXT("MuzzleSocket");
WeaponData->ScopeSocketName = TEXT("ScopeSocket");
WeaponData->GripSocketName = TEXT("GripSocket");
WeaponData->LaserSocketName = TEXT("LaserSocket");
WeaponData->BurstCount = 3;
WeaponData->SprintToFireTime = 0.25f;
WeaponData->bUseGPUSimulation = true;
WeaponData->bEnableEffectLOD = true;
WeaponData->MaxHeatLevel = 100.0f;
WeaponData->HeatDecayRate = 10.0f;
WeaponData->HeatPerShot = 5.0f;
}
return WeaponData;
}
UClass* FEBWeaponDataFactory::GetSupportedClass() const
{
return UEBWeaponData::StaticClass();
}
uint32 FEBWeaponDataFactory::GetCategories()
{
return FEasyBallisticsEditorModule::GetBallisticsAssetCategory();
}
FText FEBWeaponDataFactory::GetAssetDescription(const FAssetData& AssetData) const
{
return LOCTEXT("EBWeaponDataDescription", "Defines comprehensive weapon properties including visuals, ballistics, audio, VFX, animations, and behavior for game-ready weapons.");
}
#undef LOCTEXT_NAMESPACE
@@ -14,6 +14,7 @@
#include "EBBulletActorFactory.h" #include "EBBulletActorFactory.h"
#include "EBMathematicalBallisticsFactory.h" #include "EBMathematicalBallisticsFactory.h"
#include "EBWeaponConfigurationFactory.h" #include "EBWeaponConfigurationFactory.h"
#include "EBWeaponDataFactory.h"
#include "EBGunActorFactory.h" #include "EBGunActorFactory.h"
// Component Visualizers // Component Visualizers
@@ -22,11 +23,13 @@
// Customizations // Customizations
#include "EBPhysicalMaterialCustomization.h" #include "EBPhysicalMaterialCustomization.h"
#include "EBPlayerWeaponControllerCustomization.h"
#include "EBJsonImportExportTool.h" #include "EBJsonImportExportTool.h"
// Component Classes // Component Classes
#include "EBBarrel.h" #include "EBBarrel.h"
#include "EBMagazine.h" #include "EBMagazine.h"
#include "EBPlayerWeaponController.h"
#include "PhysicalMaterials/PhysicalMaterial.h" #include "PhysicalMaterials/PhysicalMaterial.h"
#include "Editor.h" #include "Editor.h"
@@ -80,6 +83,13 @@ void FEasyBallisticsEditorModule::StartupModule()
RegisteredAssetTypeActions.Add(WeaponConfigurationActions); RegisteredAssetTypeActions.Add(WeaponConfigurationActions);
} }
// Register Weapon Data Asset factory
{
TSharedRef<IAssetTypeActions> WeaponDataActions = MakeShareable(new FEBWeaponDataFactory());
AssetTools.RegisterAssetTypeActions(WeaponDataActions);
RegisteredAssetTypeActions.Add(WeaponDataActions);
}
// Register UFactory objects (these handle the "Add" menu in Content Browser) // Register UFactory objects (these handle the "Add" menu in Content Browser)
// These need to be manually registered to appear in the Add menu // These need to be manually registered to appear in the Add menu
@@ -112,6 +122,12 @@ void FEasyBallisticsEditorModule::StartupModule()
{ {
NewObject<UEBWeaponConfigurationFactory>(); NewObject<UEBWeaponConfigurationFactory>();
} }
// Register Weapon Data UFactory
if (!GetDefault<UEBWeaponDataFactory>())
{
NewObject<UEBWeaponDataFactory>();
}
// Register UObject-based factories for actors // Register UObject-based factories for actors
if (GEditor) if (GEditor)
@@ -149,6 +165,12 @@ void FEasyBallisticsEditorModule::StartupModule()
UPhysicalMaterial::StaticClass()->GetFName(), UPhysicalMaterial::StaticClass()->GetFName(),
FOnGetDetailCustomizationInstance::CreateStatic(&FEBPhysicalMaterialCustomization::MakeInstance) FOnGetDetailCustomizationInstance::CreateStatic(&FEBPhysicalMaterialCustomization::MakeInstance)
); );
// Register Player Weapon Controller customization
PropertyModule.RegisterCustomClassLayout(
UEBPlayerWeaponController::StaticClass()->GetFName(),
FOnGetDetailCustomizationInstance::CreateStatic(&FEBPlayerWeaponControllerCustomization::MakeInstance)
);
// Register JSON Import/Export tool - delay this until after editor is fully loaded // Register JSON Import/Export tool - delay this until after editor is fully loaded
FCoreDelegates::OnFEngineLoopInitComplete.AddLambda([]() FCoreDelegates::OnFEngineLoopInitComplete.AddLambda([]()
@@ -172,11 +194,12 @@ void FEasyBallisticsEditorModule::ShutdownModule()
} }
RegisteredComponentClassNames.Empty(); RegisteredComponentClassNames.Empty();
// Unregister Physical Material customization // Unregister customizations
if (FModuleManager::Get().IsModuleLoaded("PropertyEditor")) if (FModuleManager::Get().IsModuleLoaded("PropertyEditor"))
{ {
FPropertyEditorModule& PropertyModule = FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor"); FPropertyEditorModule& PropertyModule = FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor");
PropertyModule.UnregisterCustomClassLayout(UPhysicalMaterial::StaticClass()->GetFName()); PropertyModule.UnregisterCustomClassLayout(UPhysicalMaterial::StaticClass()->GetFName());
PropertyModule.UnregisterCustomClassLayout(UEBPlayerWeaponController::StaticClass()->GetFName());
} }
// Unregister asset type actions // Unregister asset type actions
@@ -0,0 +1,19 @@
// Copyright 2016 Mookie. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "IDetailCustomization.h"
/**
* Customization for the EBPlayerWeaponController details panel
* Now simplified since the controller is a scene component with direct positioning
*/
class FEBPlayerWeaponControllerCustomization : public IDetailCustomization
{
public:
static TSharedRef<IDetailCustomization> MakeInstance();
// IDetailCustomization interface
virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override;
};
@@ -0,0 +1,34 @@
// Copyright 2016 Mookie. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "AssetTypeActions_Base.h"
#include "Factories/Factory.h"
#include "EBWeaponData.h"
#include "EBWeaponDataFactory.generated.h"
UCLASS()
class EASYBALLISTICSEDITOR_API UEBWeaponDataFactory : public UFactory
{
GENERATED_BODY()
public:
UEBWeaponDataFactory();
// UFactory interface
virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
virtual bool ShouldShowInNewMenu() const override { return true; }
virtual FText GetDisplayName() const override { return NSLOCTEXT("EBFactory", "WeaponDataText", "Weapon Data"); }
virtual FText GetToolTip() const override { return NSLOCTEXT("EBFactory", "WeaponDataTooltip", "Creates a new Weapon Data asset defining weapon properties, visuals, ballistics, and behavior"); }
};
class FEBWeaponDataFactory : public FAssetTypeActions_Base
{
public:
virtual FText GetName() const override { return NSLOCTEXT("AssetTypeActions", "EBWeaponData", "Weapon Data"); }
virtual FColor GetTypeColor() const override { return FColor(100, 255, 100); }
virtual UClass* GetSupportedClass() const override;
virtual uint32 GetCategories() override;
virtual FText GetAssetDescription(const FAssetData& AssetData) const override;
};
+424
View File
@@ -0,0 +1,424 @@
# EasyBallistics Researcher Guide
## Overview
This guide is designed for researchers, engineers, and scientists who need to configure ballistic simulation parameters without requiring knowledge of game development or Unreal Engine. The configuration system uses self-documenting JSON format with embedded explanations, making it accessible to ballistics researchers from any background.
## Getting Started
### Understanding the JSON Structure
Each configuration file follows this pattern:
```json
{
"ParameterName": {
"_description": "What this parameter does",
"_unit": "Units of measurement",
"_typical_values": "Expected ranges",
"_examples": "Real-world examples",
"value": actual_numeric_value
}
}
```
**Important**: The underscore-prefixed fields (`_description`, `_unit`, etc.) are documentation only. The `value` field contains the actual parameter that affects the simulation.
### File Types
1. **Bullet Properties** (`Sample_BulletProperties.json`) - Physical and ballistic characteristics of projectiles
2. **Material Response** (`Sample_MaterialResponse.json`) - How materials respond to ballistic impact
## Bullet Properties Configuration
### Essential Parameters
#### Mass and Dimensions
- **GrainWeight**: Projectile mass in grains (1 grain = 0.0647989 grams)
- Small caliber: 20-80 grains
- Medium caliber: 80-200 grains
- Large caliber: 200-750+ grains
- **DiameterInches**: Bullet diameter in inches
- Common values: 0.224" (5.56mm), 0.308" (7.62mm), 0.452" (45 ACP)
- **LengthInches**: Overall bullet length
- Typically 0.4-2.0 inches depending on caliber
#### Ballistic Coefficients
Choose **either** G1 or G7 model (set UseG7Model accordingly):
- **G1 Model**: Traditional standard, good for round-nose bullets
- Poor: 0.15-0.25
- Average: 0.25-0.45
- Excellent: 0.65+
- **G7 Model**: Better for modern pointed bullets
- Typically 0.5-0.7× the G1 value
- More accurate for boat-tail designs
#### Material Properties
- **BulletHardness**: Brinell Hardness Number
- Lead: 5-15 BHN
- Copper jacket: 15-25 BHN
- Steel core: 150-600 BHN
- **PenetrationEnergyThreshold**: Minimum energy for penetration (ft-lbs)
- Varies by intended use: 10-500+ ft-lbs
### Research Applications
#### Ballistics Research
For academic ballistics research, focus on:
- Accurate ballistic coefficients from wind tunnel data
- Precise mass and dimensional measurements
- Material hardness from standardized testing
#### Terminal Ballistics Studies
For penetration and wound ballistics:
- Expansion characteristics (velocity thresholds, max expansion)
- Energy transfer parameters
- Material-specific hardness values
#### Forensic Applications
For bullet identification and trajectory analysis:
- Precise dimensional measurements
- Material composition data
- Manufacturer-specific variations
## Material Response Configuration
### Penetration Mechanics
#### Basic Penetration
- **PenetrationDepthMultiplier**: Scaling factor (0.1-10.0)
- Use 1.0 as baseline, adjust based on experimental data
- Values &lt;1.0 for harder materials, &gt;1.0 for softer
- **PenetrationNormalization**: Angle dependency (degrees)
- 0-15°: Soft materials (wood, plastic)
- 15-30°: Medium materials (aluminum, mild steel)
- 30-60°: Hard materials (armor steel, ceramics)
#### Advanced Penetration Modeling
When **UseMathematicalProperties** is enabled:
- **DensityGPerCm3**: Material density from engineering handbooks
- **TensileStrengthMPa**: Ultimate tensile strength from material testing
- **YieldStrengthMPa**: Yield strength (typically 50-80% of tensile strength)
- **BallisticLimitVelocity**: V50 velocity from ballistic testing
### Ricochet Behavior
#### Key Parameters
- **RicochetProbabilityMultiplier**: Likelihood scaling (0.0-5.0)
- 0.0: No ricochets (soft materials)
- 1.0: Normal probability
- 2.0+: High ricochet materials (hardened steel, ice)
- **RicochetRestitution**: Energy retention (0.0-1.0)
- Soft materials: 0.1-0.3
- Hard materials: 0.7-0.9
### Spalling and Fragmentation
#### When to Enable Spalling
Spalling is relevant for:
- Brittle materials (concrete, ceramics, glass)
- High-energy impacts (&gt;2000 fps)
- Research into secondary fragmentation effects
#### Critical Parameters
- **SpallVelocityThreshold**: Minimum impact velocity (cm/s)
- Glass: 15,000-30,000 cm/s
- Concrete: 30,000-60,000 cm/s
- Steel: 60,000+ cm/s
- **SpallFragmentCount**: Number of fragments (1-20)
- Balance realism vs. computational cost
- Typical: 3-8 fragments for most materials
## Data Sources and Validation
### Recommended References
#### Bullet Properties
1. **Manufacturer Data**: Official ballistic coefficients and specifications
2. **Ballistic Tables**: Sierra, Hornady, Nosler reloading manuals
3. **Military Standards**: NATO STANAG documents for military ammunition
4. **SAAMI Standards**: Sporting Arms and Ammunition Manufacturers' Institute
#### Material Properties
1. **ASTM Standards**: Material testing standards (E8, E10, etc.)
2. **Engineering Handbooks**: ASM Metals Handbook, Machinery's Handbook
3. **Research Literature**: Journal of Applied Physics, International Journal of Impact Engineering
4. **Government Databases**: NIST materials database, military research reports
### Experimental Validation
#### Ballistic Testing
- Use chronographs for velocity measurements
- Ballistic gelatin for terminal performance
- Steel plate testing for penetration limits
- High-speed photography for fragment analysis
#### Material Testing
- Tensile testing per ASTM E8
- Hardness testing per ASTM E10
- Impact testing per ASTM E23
- Ballistic limit testing per MIL-STD protocols
## Common Material Examples
### Metals
```json
// Mild Steel (A36)
"DensityGPerCm3": 7.85,
"MaterialHardness": 150.0,
"TensileStrengthMPa": 400.0,
"YieldStrengthMPa": 250.0
// Aluminum 6061-T6
"DensityGPerCm3": 2.70,
"MaterialHardness": 95.0,
"TensileStrengthMPa": 310.0,
"YieldStrengthMPa": 276.0
// Hardened Steel (Armor)
"DensityGPerCm3": 7.85,
"MaterialHardness": 500.0,
"TensileStrengthMPa": 1200.0,
"YieldStrengthMPa": 1000.0
```
### Ceramics and Composites
```json
// Alumina Ceramic
"DensityGPerCm3": 3.95,
"MaterialHardness": 1500.0,
"TensileStrengthMPa": 300.0,
"YieldStrengthMPa": 300.0,
"EnableSpalling": true
// Kevlar Composite
"DensityGPerCm3": 1.44,
"MaterialHardness": 50.0,
"TensileStrengthMPa": 3500.0,
"YieldStrengthMPa": 3500.0,
"NeverRicochet": true
```
## Quality Assurance
### Parameter Validation
1. **Physical Consistency**: Ensure yield strength ≤ tensile strength
2. **Unit Consistency**: Verify all units match the documentation
3. **Range Checking**: Compare values against published material databases
4. **Cross-Reference**: Validate against multiple independent sources
### Simulation Validation
1. **Known Results**: Test against published ballistic data
2. **Sensitivity Analysis**: Vary parameters to understand system response
3. **Comparative Testing**: Compare results between different bullet/material combinations
4. **Literature Comparison**: Validate results against peer-reviewed research
## JSON Configuration Examples
### Complete Bullet Properties Example
```json
{
"bulletProperties": {
"grainWeight": {
"_description": "Projectile mass in grains",
"_unit": "grains",
"_typical_values": "20-750 grains depending on caliber",
"_examples": "55gr for 5.56mm, 150gr for .308",
"value": 55.0
},
"diameterInches": {
"_description": "Bullet diameter in inches",
"_unit": "inches",
"_typical_values": "0.17-0.8 inches",
"_examples": "0.224 for 5.56mm, 0.308 for 7.62mm",
"value": 0.224
},
"lengthInches": {
"_description": "Overall bullet length",
"_unit": "inches",
"_typical_values": "0.4-2.0 inches",
"_examples": "0.825 for M855, 1.35 for match bullets",
"value": 0.825
},
"ballisticCoefficientG1": {
"_description": "Aerodynamic efficiency using G1 standard",
"_unit": "dimensionless",
"_typical_values": "0.15-0.8",
"_examples": "0.151 for M855, 0.6+ for match bullets",
"value": 0.151
},
"bulletHardness": {
"_description": "Brinell hardness number",
"_unit": "BHN",
"_typical_values": "5-600 BHN",
"_examples": "15 for copper jacket, 300+ for steel core",
"value": 15.0
}
},
"metadata": {
"description": "5.56x45mm NATO M855 Ball",
"source": "Military specification data",
"date": "2024-12-02",
"researcher": "Ballistics Lab"
}
}
```
### Complete Material Response Example
```json
{
"materialResponse": {
"basicProperties": {
"neverPenetrate": {
"_description": "Material never allows penetration",
"_unit": "boolean",
"_typical_values": "false for most materials",
"_examples": "true for backstop materials",
"value": false
},
"penetrationDepthMultiplier": {
"_description": "Penetration resistance scaling",
"_unit": "dimensionless",
"_typical_values": "0.1-10.0",
"_examples": "0.3 for steel, 2.0 for wood",
"value": 1.0
}
},
"mathematicalProperties": {
"densityGPerCm3": {
"_description": "Material density",
"_unit": "g/cm³",
"_typical_values": "0.1-20.0",
"_examples": "7.85 for steel, 2.70 for aluminum",
"value": 7.85
},
"tensileStrengthMPa": {
"_description": "Ultimate tensile strength",
"_unit": "MPa",
"_typical_values": "1-5000 MPa",
"_examples": "400 for mild steel, 3500 for Kevlar",
"value": 400.0
},
"yieldStrengthMPa": {
"_description": "Yield strength (must be ≤ tensile)",
"_unit": "MPa",
"_typical_values": "50-80% of tensile strength",
"_examples": "250 for mild steel with 400 MPa tensile",
"value": 250.0
}
}
},
"metadata": {
"materialType": "Mild Steel A36",
"standard": "ASTM A36",
"source": "ASM Metals Handbook",
"date": "2024-12-02"
}
}
```
## Troubleshooting
### Common Issues
1. **Unrealistic Penetration**: Check density and hardness values
2. **No Expansion**: Verify expansion velocity threshold vs. impact velocity
3. **Excessive Ricochets**: Reduce ricochet probability multiplier
4. **Performance Issues**: Reduce spalling fragment count and density
### Best Practices
1. **Start Simple**: Begin with basic parameters, add complexity gradually
2. **Document Sources**: Record where each value came from for traceability
3. **Version Control**: Keep track of parameter changes and their effects
4. **Peer Review**: Have other researchers validate your parameter choices
## Integration with Research Workflows
### Data Pipeline
1. **Experimental Data** → Material testing, ballistic testing
2. **Literature Review** → Published coefficients, industry standards
3. **Configuration Files** → JSON parameter files
4. **Simulation** → EasyBallistics simulation runs
5. **Analysis** → Results validation and publication
### Version Control for Research
```json
{
"versionControl": {
"configurationVersion": "1.2.0",
"changeLog": [
{
"version": "1.2.0",
"date": "2024-12-02",
"changes": ["Updated steel hardness based on new testing"],
"researcher": "Dr. Smith"
},
{
"version": "1.1.0",
"date": "2024-11-15",
"changes": ["Added ceramic spalling parameters"],
"researcher": "Lab Team"
}
]
}
}
```
### Collaboration Features
- **Shared Parameter Libraries**: Common configurations for research groups
- **Validation Protocols**: Standardized testing procedures
- **Result Archival**: Maintaining configuration-result relationships
- **Publication Support**: Traceable parameters for research papers
## Advanced Research Applications
### Multi-Material Studies
For composite armor or layered materials:
- Configure each layer separately
- Account for interface effects
- Consider cumulative energy absorption
### Dynamic Effects
For high strain rate phenomena:
- Use dynamic strength values (typically 1.5-3× static)
- Account for temperature effects
- Consider strain rate sensitivity
### Statistical Analysis
For probabilistic studies:
- Use parameter ranges rather than single values
- Apply Monte Carlo methods
- Document uncertainty and sensitivity
### Experimental Design
For systematic studies:
- Design of experiments (DOE) methodology
- Parameter space exploration
- Sensitivity analysis protocols
## Sample JSON Files
The EasyBallistics package includes sample JSON files that demonstrate proper configuration:
- **Sample_BulletProperties.json**: Example bullet configurations for common calibers
- **Sample_MaterialResponse.json**: Example material response configurations
These files can be found in the `Documentation/` directory and serve as templates for your own research configurations.
## See Also
- [Parameter Validation Reference](../api/parameter-validation-reference) - Complete validation guidelines
- [JSON Import/Export Tool](json-import-export) - Tool for managing configurations
- [Material Properties Reference](../api/material-properties-reference) - Complete material property API
- [Mathematical Ballistics](../core-concepts/mathematical-ballistics) - Understanding the physics
---
*This configuration system is designed to support serious ballistics research while maintaining computational efficiency. The self-documenting JSON format ensures that parameter choices are traceable and scientifically justified.*
@@ -0,0 +1,324 @@
# Parameter Validation Reference
## Overview
This document provides comprehensive validation ranges, typical values, and quality assurance guidelines for ballistic simulation parameters. Use this reference to verify that your configuration values are physically realistic and within expected bounds.
## Bullet Properties Validation
### Physical Dimensions
#### Grain Weight Validation
| Caliber Category | Typical Range (grains) | Examples |
|------------------|------------------------|----------|
| Rimfire | 20-60 | .22 LR: 30-40 gr |
| Small Pistol | 60-130 | 9mm: 115-147 gr |
| Large Pistol | 130-300 | .45 ACP: 200-230 gr |
| Small Rifle | 40-90 | .223: 55-77 gr |
| Medium Rifle | 100-200 | .308: 150-180 gr |
| Large Rifle | 200-500 | .338: 250-300 gr |
| Anti-Material | 500-750+ | .50 BMG: 647-750 gr |
**Quality Checks:**
- Weight should correlate with caliber size
- Unusually light bullets (&lt;20 gr) may indicate frangible/training rounds
- Unusually heavy bullets (&gt;300 gr for caliber) may indicate specialty rounds
#### Diameter Validation
| Caliber | Actual Diameter (inches) | Metric Equivalent |
|---------|--------------------------|-------------------|
| .17 | 0.172 | 4.37mm |
| .22 | 0.224 | 5.69mm |
| .24 | 0.243 | 6.17mm |
| .25 | 0.257 | 6.53mm |
| .27 | 0.277 | 7.04mm |
| .28 | 0.284 | 7.21mm |
| .30 | 0.308 | 7.82mm |
| .32 | 0.312 | 7.92mm |
| .35 | 0.358 | 9.09mm |
| .38 | 0.357 | 9.07mm |
| .40 | 0.400 | 10.16mm |
| .44 | 0.429 | 10.90mm |
| .45 | 0.452 | 11.48mm |
| .50 | 0.510 | 12.95mm |
**Quality Checks:**
- Diameter must be positive and reasonable (0.1-0.8 inches typical)
- Should match known caliber specifications
- Metric conversions should be consistent
#### Length-to-Diameter Ratio
| Bullet Type | Typical L/D Ratio | Length Range (inches) |
|-------------|-------------------|----------------------|
| Round Nose | 1.5-2.5 | Short, traditional design |
| Spitzer | 2.5-4.0 | Pointed, aerodynamic |
| Boat Tail | 3.0-4.5 | Long, high BC |
| VLD | 4.0-6.0 | Very Low Drag, match |
| Solid | 2.0-5.0 | Monolithic construction |
**Quality Checks:**
- L/D ratio should be realistic for bullet type
- Very long bullets (L/D &gt; 6) may have stability issues
- Very short bullets (L/D &lt; 1.5) are unusual outside specialty applications
### Ballistic Coefficients
#### G1 Ballistic Coefficient Ranges
| Bullet Shape | BC Range | Description |
|--------------|----------|-------------|
| Round Nose | 0.15-0.25 | Traditional, poor aerodynamics |
| Flat Point | 0.20-0.30 | Lever gun bullets |
| Spitzer | 0.25-0.45 | Standard pointed bullets |
| Boat Tail | 0.35-0.55 | Improved base design |
| Match/VLD | 0.45-0.70+ | Optimized for accuracy |
| Ultra-High BC | 0.70-1.0+ | Specialized long-range |
#### G7 to G1 Conversion
- G7 BC typically 0.5-0.7× the G1 value for modern bullets
- Conversion varies by bullet shape and velocity regime
- G7 more accurate for boat-tail designs
**Quality Checks:**
- BC should correlate with bullet shape and quality
- Extremely high BC (&gt;0.8 G1) requires verification
- G7 values should be lower than corresponding G1 values
### Material Properties
#### Bullet Hardness (BHN)
| Material | Hardness Range | Applications |
|----------|----------------|--------------|
| Pure Lead | 5-8 BHN | Cast bullets, training |
| Wheel Weights | 12-15 BHN | Economy cast bullets |
| Linotype | 22-25 BHN | Hard cast bullets |
| Swaged Lead | 8-12 BHN | Commercial lead bullets |
| Copper Plated | 10-15 BHN | Plated lead core |
| Copper Jacket | 15-25 BHN | FMJ bullets |
| Brass | 60-120 BHN | Solid brass bullets |
| Copper Solid | 35-80 BHN | Monolithic copper |
| Steel Core | 150-300 BHN | AP bullets |
| Tungsten | 300-400 BHN | High-density cores |
**Quality Checks:**
- Hardness should match material type
- Jacket hardness is composite value including core
- Extremely hard bullets (&gt;400 BHN) are specialized
#### Energy and Velocity Thresholds
**Penetration Energy Threshold (ft-lbs)**
| Application | Energy Range | Examples |
|-------------|--------------|----------|
| Small Game | 10-25 | Squirrel, rabbit |
| Varmint | 25-75 | Prairie dog, coyote |
| Deer | 75-150 | White-tail deer |
| Elk | 150-300 | Large game |
| Dangerous Game | 300-500+ | Bear, buffalo |
| Armor Piercing | 500-2000+ | Military applications |
**Expansion Velocity Threshold (fps)**
| Bullet Type | Velocity Range | Notes |
|-------------|----------------|-------|
| Handgun HP | 800-1200 | Lower velocity expansion |
| Rifle HP | 1600-2200 | Standard hunting bullets |
| Premium Hunting | 1400-1800 | Controlled expansion |
| Varmint | 2000-3000 | Rapid expansion |
| Match | N/A | Non-expanding |
**Quality Checks:**
- Energy thresholds should match intended use
- Expansion velocities should be achievable at impact range
- Thresholds should be lower than typical muzzle velocities
## Material Response Validation
### Physical Properties
#### Density Values (g/cm³)
| Material Category | Density Range | Common Materials |
|-------------------|---------------|------------------|
| Woods | 0.3-1.2 | Balsa (0.16), Oak (0.75), Ebony (1.2) |
| Plastics | 0.9-2.0 | PE (0.95), Nylon (1.15), PVC (1.4) |
| Aluminum Alloys | 2.6-2.8 | 1100 (2.71), 6061 (2.70), 7075 (2.81) |
| Concrete | 2.0-2.8 | Normal (2.4), High-strength (2.6) |
| Titanium Alloys | 4.4-4.9 | Ti-6Al-4V (4.43), CP Ti (4.51) |
| Steel | 7.7-8.1 | Mild (7.85), Stainless (8.0) |
| Lead | 11.3-11.4 | Pure lead (11.34) |
| Tungsten | 19.2-19.3 | Pure tungsten (19.25) |
#### Hardness Correlation
**Brinell Hardness (HB)**
| Material | Hardness Range | Strength Correlation |
|----------|----------------|---------------------|
| Aluminum (soft) | 15-30 | Low strength |
| Aluminum (hard) | 60-150 | Heat treated |
| Steel (mild) | 120-200 | Structural steel |
| Steel (medium) | 200-300 | Heat treated |
| Steel (hard) | 300-500 | Tool steel |
| Steel (very hard) | 500-700 | Hardened/tempered |
| Ceramics | 1000-2000+ | Very brittle |
**Quality Checks:**
- Hardness should correlate with tensile strength
- Rule of thumb: BHN ≈ 3.45 × Tensile Strength (ksi)
- Very hard materials often brittle
#### Strength Properties (MPa)
**Tensile Strength Ranges**
| Material Class | Yield Strength | Tensile Strength | Ratio (Yield/Tensile) |
|----------------|----------------|------------------|----------------------|
| Aluminum (soft) | 35-100 | 90-200 | 0.4-0.5 |
| Aluminum (hard) | 200-500 | 300-600 | 0.6-0.8 |
| Steel (mild) | 200-400 | 400-600 | 0.5-0.7 |
| Steel (high strength) | 400-1000 | 600-1200 | 0.7-0.8 |
| Stainless Steel | 200-800 | 500-1000 | 0.4-0.8 |
| Titanium | 200-1000 | 300-1200 | 0.6-0.8 |
**Quality Checks:**
- Yield strength must be ≤ tensile strength
- Typical ratio: yield = 50-80% of tensile
- Higher ratios indicate brittle materials
### Ballistic Properties
#### Ballistic Limit Velocities (fps)
| Material/Thickness | Velocity Range | Projectile Type |
|-------------------|----------------|-----------------|
| Aluminum 0.25" | 800-1500 | Standard ball |
| Steel 0.25" | 1500-2500 | Standard ball |
| Steel 0.5" | 2500-3500 | Standard ball |
| Kevlar vest | 1200-1800 | Handgun bullets |
| Ceramic tile | 2000-3500 | Rifle bullets |
| Glass | 200-800 | Low-velocity impacts |
#### Energy Absorption Coefficients
| Material Type | Coefficient Range | Mechanism |
|---------------|-------------------|-----------|
| Soft (foam, sand) | 0.8-1.0 | Compression/displacement |
| Wood/Plastic | 0.6-0.8 | Crushing/deformation |
| Aluminum | 0.5-0.7 | Plastic deformation |
| Mild Steel | 0.4-0.6 | Plastic deformation |
| Hard Steel | 0.2-0.4 | Limited deformation |
| Ceramics | 0.3-0.5 | Fracture/pulverization |
### Ricochet Properties
#### Ricochet Probability by Material
| Surface Type | Probability Multiplier | Impact Angle Dependency |
|--------------|------------------------|------------------------|
| Water | 0.3-0.7 | High angle dependency |
| Concrete | 0.8-1.2 | Moderate dependency |
| Steel (mild) | 1.0-1.5 | Low angle dependency |
| Steel (hard) | 1.2-2.0 | Very low dependency |
| Ice | 1.2-1.8 | Temperature dependent |
| Rock/Stone | 0.6-1.4 | Surface roughness dependent |
#### Restitution Coefficients
| Material Pair | Coefficient Range | Energy Retention |
|---------------|-------------------|------------------|
| Lead-Steel | 0.2-0.4 | Low retention |
| Copper-Steel | 0.3-0.5 | Moderate retention |
| Steel-Steel | 0.5-0.7 | High retention |
| Tungsten-Steel | 0.6-0.8 | Very high retention |
### Spalling Parameters
#### Velocity Thresholds by Material
| Material | Threshold (cm/s) | Threshold (fps) | Fragment Characteristics |
|----------|------------------|-----------------|-------------------------|
| Glass | 15,000-30,000 | 500-1000 | Sharp, dangerous |
| Concrete | 30,000-60,000 | 1000-2000 | Angular, dust |
| Aluminum | 60,000-90,000 | 2000-3000 | Metal flakes |
| Mild Steel | 90,000-150,000 | 3000-5000 | Metal fragments |
| Hard Steel | 150,000+ | 5000+ | Small, high-velocity |
#### Fragment Count Guidelines
| Impact Energy | Fragment Count | Material Type |
|---------------|----------------|---------------|
| Low (&lt;1000 ft-lbs) | 1-3 | Brittle materials only |
| Medium (1000-5000) | 3-8 | Most materials |
| High (5000-15000) | 8-15 | High-energy impacts |
| Very High (&gt;15000) | 15-25 | Extreme conditions |
**Quality Checks:**
- Fragment count should scale with impact energy
- Brittle materials generate more fragments
- Consider computational cost vs. realism
## Validation Procedures
### Cross-Reference Validation
1. **Material Databases**: ASM International, NIST
2. **Military Standards**: MIL-HDBK, NATO STANAGs
3. **Industry Standards**: ASTM, SAE, AISI
4. **Academic Literature**: Peer-reviewed journals
### Physical Consistency Checks
1. **Density-Strength Correlation**: Generally, denser materials are stronger
2. **Hardness-Strength Relationship**: BHN ≈ 3.45 × UTS (ksi)
3. **Yield-Tensile Ratio**: Yield typically 50-80% of tensile strength
4. **Elastic Modulus**: Should match material class
### Ballistic Reasonableness
1. **BC vs. Shape**: High BC requires good aerodynamic shape
2. **Energy vs. Caliber**: Larger calibers generally have more energy
3. **Velocity Thresholds**: Should be achievable at intended range
4. **Material Response**: Should match known ballistic test results
### Common Validation Errors
#### Bullet Properties
- **Impossible BC**: Values &gt;1.0 require careful verification
- **Mismatched Dimensions**: Diameter doesn't match caliber designation
- **Unrealistic Hardness**: Values inconsistent with material type
- **Inappropriate Thresholds**: Energy/velocity values too high/low for application
#### Material Properties
- **Inverted Strength Values**: Yield &gt; tensile strength
- **Impossible Density**: Values outside known material ranges
- **Inconsistent Hardness**: Doesn't correlate with strength values
- **Unrealistic Ballistic Limits**: Too high/low for material thickness
### Quality Assurance Checklist
#### Before Simulation
- [ ] All values within documented ranges
- [ ] Physical relationships consistent (yield ≤ tensile, etc.)
- [ ] Units correct and consistent
- [ ] Sources documented for traceability
- [ ] Cross-referenced with multiple sources
#### During Simulation
- [ ] Results match expected physical behavior
- [ ] No obvious artifacts or anomalies
- [ ] Performance acceptable for application
- [ ] Sensitivity analysis completed
#### After Simulation
- [ ] Results compared to experimental data when available
- [ ] Peer review of critical parameters
- [ ] Documentation updated with lessons learned
- [ ] Parameters archived for reproducibility
## Recommended References
### Primary Sources
1. **ASM International Handbook Series** - Comprehensive material properties
2. **Military Ballistics Handbooks** - Validated ballistic data
3. **NIST Material Database** - Standardized property values
4. **Manufacturer Specifications** - Bullet and ammunition data
### Academic Journals
1. **International Journal of Impact Engineering**
2. **Journal of Applied Physics**
3. **Experimental Mechanics**
4. **Defence Technology**
### Professional Organizations
1. **SAAMI** - Sporting ammunition standards
2. **NATO** - Military ammunition specifications
3. **ASTM International** - Testing standards
4. **ASM International** - Materials engineering
This validation reference ensures that ballistic simulations are based on physically realistic and well-documented parameters, supporting credible research and engineering applications.
+213 -103
View File
@@ -2,12 +2,17 @@
This guide covers creating fully functional gun blueprints using EasyBallistics' new **Gun-Centric Architecture**. The `AEBGun` class provides a complete weapon system with integrated barrel, magazine, and ammunition management. This guide covers creating fully functional gun blueprints using EasyBallistics' new **Gun-Centric Architecture**. The `AEBGun` class provides a complete weapon system with integrated barrel, magazine, and ammunition management.
> **🎨 Interactive Blueprint Visualization**
>
> This guide uses **[Klee](https://joined-forces.github.io/klee/)** for interactive Blueprint visualization. You can view, share, and experiment with the Blueprint examples directly in your browser. All Blueprint code snippets can be copied and visualized using Klee's web interface.
## Prerequisites ## Prerequisites
- Basic knowledge of Unreal Engine Blueprints - Basic knowledge of Unreal Engine Blueprints
- Understanding of Data Assets and Actor Components - Understanding of Data Assets and Actor Components
- Familiarity with Unreal's event system - Familiarity with Unreal's event system
- EasyBallistics plugin installed and enabled - EasyBallistics plugin installed and enabled
- **Optional**: [Klee](https://joined-forces.github.io/klee/) for Blueprint visualization and sharing
## Architecture Overview ## Architecture Overview
@@ -119,119 +124,183 @@ The Gun-Centric Architecture consists of:
#### A. Basic Initialization #### A. Basic Initialization
```blueprint **Blueprint Graph**: [View in Klee](https://joined-forces.github.io/klee/)
Event BeginPlay
├── Apply Weapon Configuration ```unreal
│ └── Weapon Config: BP_M4_WeaponConfig Begin Object Class=/Script/BlueprintGraph.K2Node_Event Name="K2Node_Event_BeginPlay"
├── Load Magazine EventReference=(MemberParent=Class'"/Script/Engine.Actor"',MemberName="ReceiveBeginPlay")
│ └── Bullet Types: [BP_556NATO_BulletProperties x30] bOverrideFunction=True
├── Charging Handle NodeGuid=DA78E90542B5EA6FFF2BB580420BADA4
│ └── (Chambers first round) CustomProperties Pin (PinName="then",Direction="EGPD_Output",PinType.PinCategory="exec")
└── Bind Events End Object
├── Bind Event to OnGunFired
├── Bind Event to OnMagazineEmpty Begin Object Class=/Script/BlueprintGraph.K2Node_CallFunction Name="K2Node_ApplyWeaponConfig"
├── Bind Event to OnGunStateChanged FunctionReference=(MemberParent=Class'"/Script/EasyBallistics.EBGun"',MemberName="ApplyWeaponConfiguration")
└── Bind Event to OnSafetyChanged NodePosX=288
NodeGuid=D94D72E348471465CF685AB3C289A168
CustomProperties Pin (PinName="WeaponConfig",PinType.PinCategory="object",DefaultValue="BP_M4_WeaponConfig")
End Object
``` ```
> **📋 Blueprint Workflow**:
> 1. **Event BeginPlay**: Triggers when the gun spawns
> 2. **Apply Weapon Configuration**: Sets up all gun parameters from the data asset
> 3. **Load Magazine**: Fills magazine with specified bullet types
> 4. **Charging Handle**: Chambers the first round
> 5. **Bind Events**: Connects gun events to response functions
#### B. Input Actions #### B. Input Actions
```blueprint **Fire Input Blueprint**: [View in Klee](https://joined-forces.github.io/klee/)
InputAction Fire (Pressed)
├── Branch (Can Fire)
│ └── True: Pull Trigger
└── False: Print String "Cannot Fire"
InputAction Fire (Released) ```unreal
└── Release Trigger Begin Object Class=/Script/BlueprintGraph.K2Node_InputAction Name="K2Node_InputAction_Fire"
InputActionName="Fire"
bConsumeInput=True
NodeGuid=1A2B3C4D5E6F7890ABCDEF1234567890
CustomProperties Pin (PinName="Pressed",Direction="EGPD_Output",PinType.PinCategory="exec")
CustomProperties Pin (PinName="Released",Direction="EGPD_Output",PinType.PinCategory="exec")
End Object
InputAction Reload (Pressed) Begin Object Class=/Script/BlueprintGraph.K2Node_CallFunction Name="K2Node_CanFire"
├── Branch (Rounds in Magazine < Max Capacity) FunctionReference=(MemberParent=Class'"/Script/EasyBallistics.EBGun"',MemberName="CanFire")
│ └── True: Reload Sequence NodePosX=288
└── False: Print String "Magazine Full" NodeGuid=2B3C4D5E6F7890ABCDEF1234567890AB
CustomProperties Pin (PinName="ReturnValue",Direction="EGPD_Output",PinType.PinCategory="bool")
End Object
InputAction ToggleSafety (Pressed) Begin Object Class=/Script/BlueprintGraph.K2Node_CallFunction Name="K2Node_PullTrigger"
├── Get Safety State FunctionReference=(MemberParent=Class'"/Script/EasyBallistics.EBGun"',MemberName="PullTrigger")
├── NOT (Safety State) NodePosX=576
└── Set Safety (Result) NodeGuid=3C4D5E6F7890ABCDEF1234567890ABCD
End Object
InputAction ToggleFireMode (Pressed)
├── Get Fire Mode
├── Switch on Enum (Fire Mode)
│ ├── Semi-Auto: Set Fire Mode (Full Auto)
│ ├── Full Auto: Set Fire Mode (Semi-Auto)
│ └── Default: Set Fire Mode (Semi-Auto)
└── Print String ("Fire Mode: " + New Mode)
``` ```
> **🎮 Input Mapping**:
> - **Fire (Pressed)**: Check `Can Fire` → `Pull Trigger` if valid
> - **Fire (Released)**: Always call `Release Trigger`
> - **Reload**: Check magazine capacity → Execute reload sequence
> - **Toggle Safety**: Get current state → Set opposite state
> - **Toggle Fire Mode**: Cycle through available fire modes
**Copy this Blueprint code and paste it into [Klee](https://joined-forces.github.io/klee/) to see the visual graph!**
#### C. Reload System #### C. Reload System
```blueprint **Reload Blueprint**: [View in Klee](https://joined-forces.github.io/klee/)
Custom Event: Reload Sequence
├── Branch (Has Ammo in Inventory) ```unreal
│ ├── True: Perform Reload Begin Object Class=/Script/BlueprintGraph.K2Node_CustomEvent Name="K2Node_ReloadSequence"
│ │ ├── Eject Magazine CustomFunctionName="Reload Sequence"
│ │ ├── Delay (Reload Animation Time) NodeGuid=4D5E6F7890ABCDEF1234567890ABCDEF
│ │ ├── Insert Magazine CustomProperties Pin (PinName="then",Direction="EGPD_Output",PinType.PinCategory="exec")
│ │ │ └── New Magazine: Create New Magazine End Object
│ │ ├── Charging Handle
│ │ └── Play Reload Sound Begin Object Class=/Script/BlueprintGraph.K2Node_CallFunction Name="K2Node_EjectMagazine"
│ └── False: Play Empty Sound FunctionReference=(MemberParent=Class'"/Script/EasyBallistics.EBGun"',MemberName="EjectMagazine")
└── Update Ammo UI NodePosX=288
NodeGuid=5E6F7890ABCDEF1234567890ABCDEF12
End Object
Begin Object Class=/Script/BlueprintGraph.K2Node_CallFunction Name="K2Node_InsertMagazine"
FunctionReference=(MemberParent=Class'"/Script/EasyBallistics.EBGun"',MemberName="InsertMagazine")
NodePosX=576
NodeGuid=6F7890ABCDEF1234567890ABCDEF1234
CustomProperties Pin (PinName="NewMagazine",PinType.PinCategory="object")
End Object
Begin Object Class=/Script/BlueprintGraph.K2Node_CallFunction Name="K2Node_ChargingHandle"
FunctionReference=(MemberParent=Class'"/Script/EasyBallistics.EBGun"',MemberName="ChargingHandle")
NodePosX=864
NodeGuid=7890ABCDEF1234567890ABCDEF123456
End Object
``` ```
> **🔄 Reload Workflow**:
> 1. **Check Inventory**: Verify player has ammunition available
> 2. **Eject Magazine**: Remove current magazine (empty or partial)
> 3. **Animation Delay**: Wait for reload animation timing
> 4. **Insert Magazine**: Add new magazine with fresh ammunition
> 5. **Charging Handle**: Chamber first round from new magazine
> 6. **Audio Feedback**: Play reload completion sound
**Visualization Tip**: Use [Klee](https://joined-forces.github.io/klee/) to see how the reload sequence flows and branches based on different conditions!
### 4. Event Response System ### 4. Event Response System
#### A. Gun State Management #### A. Gun State Management
```blueprint **State Management Blueprint**: [View in Klee](https://joined-forces.github.io/klee/)
OnGunStateChanged (NewState)
├── Switch on Enum (NewState) ```unreal
│ ├── Ready: Begin Object Class=/Script/BlueprintGraph.K2Node_Event Name="K2Node_OnGunStateChanged"
│ │ ├── Set UI Color (Green) EventReference=(MemberParent=Class'"/Script/EasyBallistics.EBGun"',MemberName="OnGunStateChanged")
│ │ ├── Hide Loading Indicator NodeGuid=8901ABCDEF1234567890ABCDEF123456
│ │ └── Enable Fire Input CustomProperties Pin (PinName="NewState",PinType.PinCategory="byte",PinType.PinSubCategoryObject=Enum'"/Script/EasyBallistics.EGunState"')
│ ├── Firing: CustomProperties Pin (PinName="then",Direction="EGPD_Output",PinType.PinCategory="exec")
│ │ ├── Set UI Color (Red) End Object
│ │ ├── Show Firing Indicator
│ │ └── Play Firing Animation Begin Object Class=/Script/BlueprintGraph.K2Node_Switch Name="K2Node_SwitchGunState"
│ ├── Empty: IndexPinType.PinCategory="byte"
│ │ ├── Set UI Color (Gray) IndexPinType.PinSubCategoryObject=Enum'"/Script/EasyBallistics.EGunState"'
│ │ ├── Show Reload Prompt NodePosX=288
│ │ └── Disable Fire Input NodeGuid=9012ABCDEF1234567890ABCDEF123456
│ ├── Jammed: CustomProperties Pin (PinName="Ready",Direction="EGPD_Output",PinType.PinCategory="exec")
│ │ ├── Set UI Color (Yellow) CustomProperties Pin (PinName="Firing",Direction="EGPD_Output",PinType.PinCategory="exec")
│ │ ├── Show Jam Indicator CustomProperties Pin (PinName="Empty",Direction="EGPD_Output",PinType.PinCategory="exec")
│ │ └── Disable Fire Input CustomProperties Pin (PinName="Jammed",Direction="EGPD_Output",PinType.PinCategory="exec")
│ └── Safety On: CustomProperties Pin (PinName="SafetyOn",Direction="EGPD_Output",PinType.PinCategory="exec")
│ ├── Set UI Color (Orange) End Object
│ ├── Show Safety Indicator
│ └── Disable Fire Input
└── Update Status Text
``` ```
> **🎯 State Visualization**:
> - **Ready** (Green): Gun can fire normally
> - **Firing** (Red): Gun is actively firing
> - **Empty** (Gray): No ammunition available
> - **Jammed** (Yellow): Malfunction occurred
> - **Safety On** (Orange): Safety engaged, cannot fire
**Interactive Demo**: Paste this code into [Klee](https://joined-forces.github.io/klee/) to see how the state machine branches!
#### B. Firing Effects #### B. Firing Effects
```blueprint **Firing Effects Blueprint**: [View in Klee](https://joined-forces.github.io/klee/)
OnGunFired (BulletType)
├── Spawn Emitter Attached ```unreal
│ ├── Emitter Template: MuzzleFlash Begin Object Class=/Script/BlueprintGraph.K2Node_Event Name="K2Node_OnGunFired"
│ ├── Attach to Component: GunMesh EventReference=(MemberParent=Class'"/Script/EasyBallistics.EBGun"',MemberName="OnGunFired")
│ └── Socket Name: MuzzleSocket NodeGuid=0123ABCDEF1234567890ABCDEF123456
├── Play Sound 2D (GunFireSound) CustomProperties Pin (PinName="BulletType",PinType.PinCategory="object")
├── Apply Camera Shake CustomProperties Pin (PinName="then",Direction="EGPD_Output",PinType.PinCategory="exec")
│ ├── Shake Class: GunFireShake End Object
│ └── Scale: 1.0
├── Add Impulse to Player Begin Object Class=/Script/BlueprintGraph.K2Node_CallFunction Name="K2Node_SpawnEmitterAttached"
│ ├── Impulse: Calculate Recoil Impulse FunctionReference=(MemberParent=Class'"/Script/Engine.GameplayStatics"',MemberName="SpawnEmitterAttached")
│ └── Velocity Change: false NodePosX=288
├── Spawn Actor (ShellCasing) NodeGuid=1234ABCDEF1234567890ABCDEF123456
│ ├── Class: BP_ShellCasing CustomProperties Pin (PinName="EmitterTemplate",PinType.PinCategory="object",DefaultValue="MuzzleFlash")
│ ├── Transform: EjectionPort Socket CustomProperties Pin (PinName="AttachToComponent",PinType.PinCategory="object")
│ └── Spawn Even if Colliding: true CustomProperties Pin (PinName="AttachPointName",PinType.PinCategory="name",DefaultValue="MuzzleSocket")
└── Update Ammo Counter End Object
Begin Object Class=/Script/BlueprintGraph.K2Node_CallFunction Name="K2Node_PlaySound2D"
FunctionReference=(MemberParent=Class'"/Script/Engine.GameplayStatics"',MemberName="PlaySound2D")
NodePosX=576
NodeGuid=2345ABCDEF1234567890ABCDEF123456
CustomProperties Pin (PinName="Sound",PinType.PinCategory="object",DefaultValue="GunFireSound")
End Object
``` ```
> **💥 Effects Chain**:
> 1. **Muzzle Flash**: Particle effect at barrel tip
> 2. **Audio**: Gunshot sound with 3D positioning
> 3. **Camera Shake**: Weapon recoil feedback
> 4. **Player Impulse**: Physical recoil force
> 5. **Shell Casing**: Ejected brass cartridge
> 6. **UI Update**: Ammunition counter refresh
**Pro Tip**: Use [Klee's sharing feature](https://joined-forces.github.io/klee/) to share your custom firing effects with the community!
#### C. Magazine Management #### C. Magazine Management
```blueprint ```blueprint
@@ -437,20 +506,33 @@ Manager: Weapon Pool
#### A. Debug Visualization #### A. Debug Visualization
```blueprint **Debug System Blueprint**: [View in Klee](https://joined-forces.github.io/klee/)
Input Action: Toggle Debug
├── Branch (Debug Enabled) ```unreal
│ ├── True: Disable Debug Begin Object Class=/Script/BlueprintGraph.K2Node_InputAction Name="K2Node_ToggleDebug"
│ │ ├── Clear Debug Display InputActionName="ToggleDebug"
│ │ └── Set Debug Enabled (false) bConsumeInput=True
│ └── False: Enable Debug NodeGuid=3456ABCDEF1234567890ABCDEF123456
│ ├── Show Trajectory CustomProperties Pin (PinName="Pressed",Direction="EGPD_Output",PinType.PinCategory="exec")
│ ├── Show Impact Points End Object
│ ├── Show Weapon Stats
│ └── Set Debug Enabled (true) Begin Object Class=/Script/BlueprintGraph.K2Node_CallFunction Name="K2Node_SetBulletDebugCategory"
└── Update Debug UI FunctionReference=(MemberParent=Class'"/Script/EasyBallistics.EBBarrel"',MemberName="SetBulletDebugCategory")
NodePosX=576
NodeGuid=4567ABCDEF1234567890ABCDEF123456
CustomProperties Pin (PinName="CategoryName",PinType.PinCategory="string",DefaultValue="Trajectory")
CustomProperties Pin (PinName="bEnabled",PinType.PinCategory="bool",DefaultValue="true")
End Object
``` ```
> **🔍 Debug Features**:
> - **Trajectory Visualization**: See bullet paths in real-time
> - **Impact Markers**: Show where bullets hit surfaces
> - **Performance Metrics**: Monitor FPS and trace counts
> - **Weapon Statistics**: Display accuracy, recoil, etc.
**Share Your Blueprints**: Export your debug setup and share it with [Klee](https://joined-forces.github.io/klee/) for collaborative debugging!
#### B. Test Scenarios #### B. Test Scenarios
```blueprint ```blueprint
@@ -524,10 +606,38 @@ Function: Run Weapon Tests
3. **Intuitive Controls**: Use standard FPS control schemes 3. **Intuitive Controls**: Use standard FPS control schemes
4. **Accessibility**: Support different input methods 4. **Accessibility**: Support different input methods
## Blueprint Sharing with Klee
### Exporting Your Gun Blueprints
1. **Copy Blueprint Text**: In Unreal Engine, select your Blueprint nodes and copy (`Ctrl+C`)
2. **Open Klee**: Navigate to [Klee](https://joined-forces.github.io/klee/)
3. **Paste Code**: Paste the Blueprint text into Klee's input field
4. **Generate Visualization**: Klee will create an interactive web visualization
5. **Share URL**: Copy the generated URL to share your Blueprint with others
### Community Blueprint Gallery
Visit the [Klee website](https://joined-forces.github.io/klee/) to explore community-shared Blueprints:
- **Weapon Systems**: Complete gun implementations
- **Fire Mode Variants**: Different firing mechanisms
- **Custom Ammunition**: Specialized bullet behaviors
- **Debug Utilities**: Helpful debugging tools
### Interactive Learning
Use Klee to:
- **Visualize Complex Logic**: See how event chains flow
- **Share Solutions**: Help others with Blueprint examples
- **Collaborate**: Work on multiplayer weapon systems together
- **Learn from Examples**: Study community implementations
## Conclusion ## Conclusion
The Gun-Centric Architecture provides a comprehensive, realistic weapon system that handles all aspects of firearm behavior. By following this guide, you'll create fully functional weapons with proper ammunition management, multiple fire modes, and networking support. The Gun-Centric Architecture provides a comprehensive, realistic weapon system that handles all aspects of firearm behavior. By following this guide, you'll create fully functional weapons with proper ammunition management, multiple fire modes, and networking support.
**Enhanced with Klee**: All Blueprint examples in this guide can be visualized and shared using [Klee's web interface](https://joined-forces.github.io/klee/), making it easier to understand, debug, and collaborate on weapon systems.
The system is designed to be extensible, allowing you to add custom features while maintaining the core ballistic simulation accuracy that EasyBallistics is known for. The system is designed to be extensible, allowing you to add custom features while maintaining the core ballistic simulation accuracy that EasyBallistics is known for.
For additional examples and advanced techniques, refer to the other tutorials in this documentation set. For additional examples and advanced techniques, refer to the other tutorials in this documentation set. Don't forget to check out community-shared Blueprints on [Klee](https://joined-forces.github.io/klee/)!
+425
View File
@@ -0,0 +1,425 @@
# Weapon Data Asset Setup Guide
This guide walks you through creating and configuring weapon data assets using the modern AAA weapon system with advanced Niagara VFX integration.
## Overview
The EasyBallistics weapon system uses a **data-driven approach** where:
- **Single persistent weapon actor** handles all weapons
- **Weapon Data Assets** define all weapon properties
- **Hot-swappable configuration** allows instant weapon switching
- **Advanced Niagara VFX** provides AAA-quality visual effects
---
## Creating a New Weapon Data Asset
### Step 1: Create the Data Asset
1. **Right-click** in the Content Browser
2. Navigate to **Miscellaneous > Data Asset**
3. Select **EBWeaponData** as the parent class
4. Name your asset (e.g., `DA_AssaultRifle_AK47`)
### Step 2: Configure Basic Identity
Open your weapon data asset and configure the **Identity** section:
```
Weapon Name: "AK-47"
Weapon Description: "7.62x39mm assault rifle with high damage and moderate recoil"
Weapon Type: Rifle
Weapon Icon: [Select texture asset]
Weapon Tier: 3
```
---
## Visual Configuration
### Mesh Setup
Configure the weapon's visual representation:
```
First Person Mesh: SK_AK47_FP (first-person skeletal mesh)
Third Person Mesh: SK_AK47_TP (third-person skeletal mesh)
Static Mesh: SM_AK47_Static (fallback static mesh)
Material Overrides: [Optional material variations]
```
**Required Sockets:**
- `MuzzleSocket` - For muzzle flash and shell ejection
- `ScopeSocket` - For optic attachments
- `GripSocket` - For foregrip attachments
- `BarrelSocket` - For heat effects and suppressors
### Socket Configuration
```
Muzzle Socket Name: "MuzzleSocket"
Scope Socket Name: "ScopeSocket"
Grip Socket Name: "GripSocket"
Laser Socket Name: "LaserSocket"
```
---
## Ballistics & Performance
Configure weapon stats for realistic performance:
### Core Ballistics
```
Bullet Properties: [Select EBBulletPropertiesAsset]
Damage: 42.0 (per shot)
Fire Rate: 600.0 (rounds per minute)
Muzzle Velocity: 71520.0 (cm/s, ~2350 fps)
Accuracy: 0.85 (0-1 scale, 1 = perfect)
Recoil Strength: 2.5
Effective Range: 400.0 (meters)
```
### Magazine System
```
Magazine Size: 30
Max Reserve Ammo: 180
Reload Time: 2.8 (seconds)
Tactical Reload Time: 2.2 (seconds, magazine not empty)
```
### Fire Modes
```
Available Fire Modes: [Auto, Semiauto, Burst]
Default Fire Mode: Auto
Burst Count: 3
```
---
## Advanced Niagara VFX Setup
### Muzzle Effects
**Primary Muzzle Flash:**
```
Muzzle Flash: NS_MuzzleFlash_Rifle
Muzzle Flash Params:
- Intensity: 1.2
- Scale: 1.0
- Color Tint: (1.0, 0.9, 0.7, 1.0)
```
**Muzzle Smoke:**
```
Muzzle Smoke: NS_MuzzleSmoke_Medium
Muzzle Smoke Params:
- Intensity: 0.8
- Scale: 1.1
```
**Suppressed Effects:**
```
Suppressed Muzzle Flash: NS_MuzzleFlash_Suppressed
```
### Shell Ejection
```
Ejected Shell: NS_ShellEjection_762x39
Shell Ejection Params:
- Intensity: 1.0
- Scale: 1.0
Shell Ejection Velocity: (250, 120, 80)
Shell Physics Material: PM_BrassShell
```
### Projectile Effects
**Tracer System:**
```
Tracer Effect: NS_Tracer_762x39
Tracer Params:
- Intensity: 1.0
- Color Tint: (1.0, 0.8, 0.3, 1.0)
```
**Bullet Trail:**
```
Bullet Trail: NS_BulletWake_Rifle
Sonic Crack: NS_SonicCrack_Supersonic
```
### Impact Effects
**Default Impact:**
```
Hit Impact: NS_Impact_Default
Hit Impact Params:
- Intensity: 1.0
- Scale: 1.0
```
**Material-Specific Impacts:**
```
Material Impact Effects:
- PM_Concrete: NS_Impact_Concrete
- PM_Metal: NS_Impact_Metal_Sparks
- PM_Wood: NS_Impact_Wood_Splinters
- PM_Flesh: NS_Impact_Blood
```
**Special Effects:**
```
Penetration Effect: NS_Penetration_HighVel
Ricochet Effect: NS_Ricochet_Metal
```
### Heat System
```
Barrel Heat Effect: NS_BarrelHeat_Rifle
Enable Heat Distortion: ✓
Enable Weather Effects: ✓
Attachment Glow Effect: NS_WeaponGlow_Tactical
```
---
## Advanced Settings
### Weapon Characteristics
```
Weapon Weight: 4.3 (kg, affects movement speed)
ADS Time: 0.35 (seconds to aim down sights)
Sprint To Fire Time: 0.28 (seconds from sprint to fire)
Equip Time: 0.9 (seconds to draw weapon)
```
### Special Features
```
Can Dual Wield: ✗
Has Safety: ✓
Suppressor Compatible: ✓
```
### Niagara Advanced Features
```
Global Effect Params:
- Intensity: 1.0
- Scale: 1.0
- Color Tint: (1.0, 1.0, 1.0, 1.0)
Use GPU Simulation: ✓
Enable Effect LOD: ✓
Max Heat Level: 100.0
Heat Decay Rate: 12.0 (per second)
Heat Per Shot: 4.5
```
---
## Audio Configuration
### Sound Setup
```
Audio Config:
Fire Sound: SC_AK47_Fire
Empty Fire Sound: SC_DryFire_Rifle
Reload Sound: SC_AK47_Reload
Equip Sound: SC_WeaponDraw_Heavy
Unequip Sound: SC_WeaponHolster_Heavy
```
---
## Animation Configuration
### First Person Animations
```
FP Fire: AS_FP_AK47_Fire
FP Reload: AS_FP_AK47_Reload
FP Equip: AS_FP_AK47_Equip
FP Idle: AS_FP_AK47_Idle
```
### Third Person Animations
```
TP Fire: AS_TP_Rifle_Fire
TP Reload: AS_TP_Rifle_Reload
TP Equip: AS_TP_Rifle_Equip
TP Idle: AS_TP_Rifle_Idle
```
### Animation Blueprint
```
Weapon Anim BP: ABP_WeaponBase
```
---
## Player Integration
### Adding to Player Inventory
**In Blueprint:**
```cpp
// Get the weapon controller component
WeaponController = GetComponentByClass(EBPlayerWeaponController)
// Add weapon to inventory
WeaponController->AddWeapon(DA_AssaultRifle_AK47)
// Switch to the weapon
WeaponController->SwitchToWeaponData(DA_AssaultRifle_AK47)
```
**In C++:**
```cpp
// Get weapon controller
UEBPlayerWeaponController* WeaponController = GetComponentByClass<UEBPlayerWeaponController>();
if (WeaponController && WeaponDataAsset)
{
// Add to inventory
WeaponController->AddWeapon(WeaponDataAsset);
// Equip weapon
WeaponController->SwitchToWeaponData(WeaponDataAsset);
}
```
### Default Weapon Setup
Set the weapon as default in the **EBPlayerWeaponController**:
```
Default Weapon: DA_AssaultRifle_AK47
Auto Equip On Begin Play: ✓
```
---
## Testing Your Weapon
### Basic Functionality Test
1. **Add to player inventory**
2. **Test firing** - verify muzzle flash, shell ejection
3. **Test reloading** - check reload animations and timing
4. **Test weapon switching** - ensure smooth transitions
5. **Test ammo system** - verify magazine and reserve ammo
### VFX Testing
1. **Muzzle effects** - check flash intensity and smoke
2. **Shell ejection** - verify casing physics and bouncing
3. **Heat buildup** - fire continuously to test heat effects
4. **Impact effects** - shoot different materials
5. **Tracer effects** - verify bullet trail visibility
### Performance Testing
1. **Sustained fire** - test for frame rate drops
2. **Multiple weapons** - switch between different weapon types
3. **Network play** - test multiplayer synchronization
4. **Distance LOD** - verify effects scale with distance
---
## Common Issues & Solutions
### Muzzle Flash Not Appearing
**Problem:** Niagara effect not triggering
**Solution:**
- Verify `MuzzleSocket` exists on weapon mesh
- Check Niagara system is assigned to `VFXConfig.MuzzleFlash`
- Ensure `bEnableAdvancedVFX` is true on player controller
### Shell Ejection Wrong Direction
**Problem:** Shells eject in wrong direction
**Solution:**
- Adjust `Shell Ejection Velocity` in weapon data
- Check weapon mesh socket orientation
- Verify shell ejection socket position
### Heat Effects Not Working
**Problem:** Heat effects don't show during sustained fire
**Solution:**
- Verify `BarrelHeatEffect` is assigned
- Check `Heat Per Shot` and `Max Heat Level` values
- Ensure heat threshold (>0.3) is reached
### Performance Issues
**Problem:** FPS drops with multiple effects
**Solution:**
- Enable `Use GPU Simulation`
- Enable `Enable Effect LOD`
- Reduce `Effect LOD Distance` for distant culling
- Optimize Niagara system complexity
---
## Best Practices
### Asset Organization
```
Content/
├── Weapons/
│ ├── Data/
│ │ ├── Rifles/
│ │ │ └── DA_AssaultRifle_AK47
│ │ ├── Pistols/
│ │ └── Shotguns/
│ ├── Meshes/
│ │ ├── FP/
│ │ ├── TP/
│ │ └── Static/
│ ├── VFX/
│ │ ├── MuzzleFlash/
│ │ ├── Impacts/
│ │ └── Tracers/
│ └── Audio/
```
### Performance Guidelines
- **Use GPU simulation** for effects with >100 particles
- **Enable LOD** for all weapon effects
- **Limit concurrent effects** to ~5 per weapon
- **Pool Niagara systems** for frequently used effects
- **Test on target hardware** for performance validation
### Visual Quality Tips
- **Match muzzle flash** to weapon caliber and type
- **Scale effects** appropriately for weapon size
- **Use realistic colors** for tracers and impacts
- **Add weapon-specific variations** for immersion
- **Test in different lighting conditions**
---
## Next Steps
After setting up your weapon data asset:
1. **Create weapon variants** (different attachments, camos)
2. **Set up attachment system** for scopes, grips, suppressors
3. **Configure weapon progressions** and unlock systems
4. **Add weapon-specific gameplay mechanics**
5. **Create comprehensive weapon testing scenarios**
For advanced features like custom attachments and weapon modifications, see the [Advanced Weapon Systems Guide](advanced-weapon-systems.md).
+3
View File
@@ -42,6 +42,7 @@ const sidebars = {
type: 'category', type: 'category',
label: 'Tutorials', label: 'Tutorials',
items: [ items: [
'tutorials/weapon-data-setup',
'tutorials/gun-blueprint-guide', 'tutorials/gun-blueprint-guide',
'tutorials/blueprint-integration', 'tutorials/blueprint-integration',
'tutorials/material-setup-guide', 'tutorials/material-setup-guide',
@@ -57,6 +58,7 @@ const sidebars = {
'advanced/editor-tools', 'advanced/editor-tools',
'advanced/editor-integration', 'advanced/editor-integration',
'advanced/json-import-export', 'advanced/json-import-export',
'advanced/researcher-guide',
'advanced/migration-guide', 'advanced/migration-guide',
], ],
}, },
@@ -67,6 +69,7 @@ const sidebars = {
'api/overview', 'api/overview',
'api/bullet-reference', 'api/bullet-reference',
'api/material-properties-reference', 'api/material-properties-reference',
'api/parameter-validation-reference',
'api/debug-commands-reference', 'api/debug-commands-reference',
], ],
}, },