#Fun fire - a real mess of bullet shells

1 messages · Page 1 of 1 (latest)

tidal flame
#

Somewhere in the course, there was the optional challenge to change code, so that the bullet shells do not get immediately destroyed when they hit the ground.
I thought about using just a lifespan timeout to make them disappear after some given time, maybe disabling physics after they stop moving for performance reasons...
Getting to the Automatic Fire chapter inspired me to have more fun implementing a kind of real mess solution, never deleting the bullets, so actually ending up in something like this in the image (please forgive my ugly map, still focused on logic and animations...)
Of course, its not just that. The idea was to go for using instanced static meshes.

Just define in your weapon blueprint properties, if you want to use the instancing or not, and that's it.

I'll add the code (the important points) in some more posts, as Discord limits the input. Also note, that this is based on the point around where Stephen did add the automatic fire, so functions are based on that point in time.

We only need to change code in GameMode, Weapon and Casing classes, as well as add our new class for handling that Instancing.

And yes, this topic leads to quite some more questions. Is it really a valid approach (apart from the fact, if it really makes sense to do that for stupid things like bullets). So for example: performance impact on adding instances one by one? Better add multiple at one time? PreallocateInstanceMemory? Instance Culling?
Well, at least, this was a cool thing to try out, and I'd welcome feedback on issues, especially concerning the topic of our course: Multiplayer

#

===== A new class in town: AInstancedCasing =====
AInstancedCasing is an actor, that gets spawned as soon as a weapon using the keep shells feature is spawned with a new type of Casing Class. It's kind of a singleton, managed in GameMode and holds a UInstancedStaticMeshComponent.

UCLASS()
class BLASTERSHOOTER_API AInstancedCasing : public AActor
{
    GENERATED_BODY()
    
public:    
    AInstancedCasing();
    virtual void Tick(float DeltaTime) override;

    void AddMeshInstance(FTransform Transform);
    void SetStaticMesh(class UStaticMesh* CasingMesh);
    void PrintInstancesDebugInfo();

protected:
    virtual void BeginPlay() override;

private:
    UPROPERTY()
    class UInstancedStaticMeshComponent* ISMComp;

    bool bHasMeshDefined = false;

// Public Getters and Setters
public:
    FORCEINLINE bool hasDefinedMesh(void) const { return bHasMeshDefined; }```

Implementation
```c++
AInstancedCasing::AInstancedCasing()
{
    PrimaryActorTick.bCanEverTick = false;
    bReplicates = true;
    ISMComp = CreateDefaultSubobject<UInstancedStaticMeshComponent>(TEXT("InstancedMeshComp"));
    SetRootComponent(ISMComp);
}

void AInstancedCasing::BeginPlay()
{
    Super::BeginPlay();
}

void AInstancedCasing::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);
}

// Set static mesh - called when a weapon ejects a shell for the first time
void AInstancedCasing::SetStaticMesh(UStaticMesh* CasingMesh)
{
    ISMComp->SetStaticMesh(CasingMesh);
    bHasMeshDefined = true;
}

// Add a new instance when the 'physics' casing is destroyed
void AInstancedCasing::AddMeshInstance(FTransform Transform)
{
    int32 idx = ISMComp->AddInstance(Transform, true);
}

void AInstancedCasing::PrintInstancesDebugInfo()
{
    if (GEngine) GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Blue, FString::Printf(TEXT("InstanceCasing %s: %d"), *GetName(), ISMComp->GetInstanceCount()));
}```
#

===== GameMode =====
Is responsible for spawning these actors. Gamemode keeps a TMap of these, key is the name of the Casing class.
Add 2 simple variables to GameMode:

public:
    AInstancedCasing* GetInstancedCasingActor(FName CasingClass, AWeapon* Weapon);

private:
    TMap<FName, AInstancedCasing*> InstancedCasingActorMap;```

There's only one single function here: the public Getter

```c++
// kind of a singleton
AInstancedCasing* ABlasterGameMode::GetInstancedCasingActor(FName CasingClass, AWeapon* Weapon)
{
    AInstancedCasing** IC = InstancedCasingActorMap.Find(CasingClass);
    if (IC == nullptr)
    {
        UWorld* World = GetWorld();
        if (World)
        {
            AInstancedCasing* InstancedCasingActor;
            InstancedCasingActor = World->SpawnActor<AInstancedCasing>(AInstancedCasing::StaticClass(), FVector(), FRotator());
            if (InstancedCasingActor)
            {
                InstancedCasingActorMap.Add(CasingClass, InstancedCasingActor);
                return InstancedCasingActor;
            }
        }
        return nullptr;
    }
    else return *IC;
}```
#

===== Weapon Class =====
Here, we call that GameMode Getter in BeginPlay() from in the HasAuthority() section, if our weapon is set to use that feature. So in Weapon header, add these 2 variables: the option and the Pointer to that new actor:

private:

// Make bullet shells for this weapon not disappear after LifeSpan. This uses Instanced Static Meshes.
UPROPERTY(EditAnywhere, Category = "Weapon Properties", meta = (DisplayName = "Keep Shells"))
bool bKeepShellsForever = false;

class AInstancedCasing* InstancedCasingActor = nullptr;```

So in the BeginPlay() section with HasAuthority(), add this one

```c++
if (bKeepShellsForever && CasingClass)
{
    ABlasterGameMode* GM = Cast<ABlasterGameMode>(UGameplayStatics::GetGameMode(this));
    if (GM) InstancedCasingActor = GM->GetInstancedCasingActor(FName(*CasingClass->GetName()), this);
}```

Now only need to add this simple part to the Weapon Fire() function to propagate a reference to our InstancedCasingActor to the spawned Casing actor:

```c++
ACasing* Casing = World->SpawnActor<ACasing>(
            CasingClass,
            SocketTransform.GetLocation(),
            SocketTransform.GetRotation().Rotator()        // FQuat, so convert to Rotator
            );
// Herb64 experiment - keep a reference to the InstancedCasingActor
if (bKeepShellsForever && Casing) Casing->SetInstancedCasingActor(InstancedCasingActor);```