Announcement

Collapse
No announcement yet.

Struggling with UDestructibleComponent, asynchrony, TFunction<void()> and void* (PhysX)

Collapse
X
 
  • Filter
  • Time
  • Show
Clear All
new posts

    Struggling with UDestructibleComponent, asynchrony, TFunction<void()> and void* (PhysX)

    Hello,

    this was in terms of programming far the most complicated thing I've ever made and I used some concepts I never used before. So far it works, but I wonder if I could have made it clearer and more performant.
    If I understand it correctly, the physics engine and thus also APEX destruction runs asynchrone. So I have to take care of locks and on some times need to wait for the physics engine to finish a task.
    I want to fully destroy a destructible mesh (make every chunk of it movable) and then do something with the chunks. This means that after applying damage to it, I have to wait for an event before I can add forces to the chunks. Some investigation on the source code shows me that I can't subscribe to this event directly without changing the engine's source code, but the UDestructibleComponent has a

    Code:
    /** Callback from physics system to notify the actor that it has been damaged */
    void OnDamageEvent(const nvidia::apex::DamageEventReportData& InDamageEvent);
    I can't override it, but inside this function calls

    Code:
    /** Trigger any fracture effects after a damage event is received */
    virtual void SpawnFractureEffectsFromDamageEvent(const nvidia::apex::DamageEventReportData& InDamageEvent);
    which I can override. So this is the function where I can receive the damage event of the physics engine.
    The question is: How do it knows what code to execute after it has received a damage event? I apply damage to the destructible mesh, after some time the event fires and SpawnFractureEffectsFromDamageEvent is called. It would be cool to save the information what code to execute after the event has been fired inside the const nvidia::apex:amageEventReportData& InDamageEvent. Fortunately the class DestructibleActor the has the function

    Code:
    virtual void applyRadiusDamage(float damage, float momentum, const PxVec3& position, float radius, bool falloff, void* damageUserData = NULL) = 0;
    Note the last parameter, void* damageUserData. With this I could pass the information respectively the code.
    So I made a

    Code:
    struct FDestructibleDamageUserData
    {
    EDestructibleDamageType DamageType;
    TFunction<void()> CallbackFunction;
    };
    
    // ...
    
    // Cache of user data that is passed to the physics engine by applyDamage or applyRadiusDamage.
    TArray<TSharedPtr<FDestructibleDamageUserData>> DamageUserDataCache;
    
    // Removes a FDestructibleDamageUserData pointer from the DamageUserDataCache
    bool RemoveFromDamageUserDataCache(FDestructibleDamageUserData* DamageUserData);
    
    
    // Creates a TSharedPtr with DamageUserData and adds it to the cache. Returns a raw pointer to the data.
    FDestructibleDamageUserData* MakeDamageUserData(const FDestructibleDamageUserData& DamageUserData);
    
    //...
    
    bool UExtendedDestructibleComponent::RemoveFromDamageUserDataCache(FDestructibleDamageUserData* DamageUserData)
    {
    for (int32 i = 0; i < DamageUserDataCache.Num(); ++i)
    {
    if (DamageUserDataCache[i].Get() == DamageUserData)
    {
    DamageUserDataCache.RemoveAtSwap(i);
    return true;
    }
    }
    
    return false;
    }
    
    FDestructibleDamageUserData* UExtendedDestructibleComponent::MakeDamageUserData(
    const FDestructibleDamageUserData& DamageUserData)
    {
    TSharedPtr<FDestructibleDamageUserData> NewDamageUserData = MakeShared<FDestructibleDamageUserData>(DamageUserData);
    DamageUserDataCache.Emplace(NewDamageUserData);
    return NewDamageUserData.Get();
    }
    I overloaded the UDestructibleComponent::ApplyRadiusDamage with

    Code:
    void UExtendedDestructibleComponent::ApplyRadiusDamage(float BaseDamage, const FVector& HurtOrigin, float DamageRadius,
    float ImpulseStrength, bool bFullDamage, TFunction<void()> CallbackFunction)
    {
    if (ApexDestructibleActor != NULL)
    {
    // Create user data
    FDestructibleDamageUserData DamageUserData;
    DamageUserData.DamageType = EDestructibleDamageType::RadiusDamage;
    DamageUserData.CallbackFunction = CallbackFunction;
    FDestructibleDamageUserData* DamageUserDataPtr = MakeDamageUserData(DamageUserData);
    
    // Transfer damage information to the APEX NxDestructibleActor interface
    ApexDestructibleActor->applyRadiusDamage(BaseDamage, ImpulseStrength, U2PVector( HurtOrigin ), DamageRadius, bFullDamage ? false : true, DamageUserDataPtr);
    }
    }
    So this member function creates my user data with a TFunction<void()>, damages the component (applyRadiusDamage) and passes a pointer to my user data, which is stored as TSharedPtr in a TArray.

    In the UExtendedDestructibleComponent::SpawnFractureEffectsFromDamageEvent function I receive the damage event from the physics engine with my user data and the function to call:

    Code:
    void UExtendedDestructibleComponent::SpawnFractureEffectsFromDamageEvent(
    const nvidia::apex::DamageEventReportData& InDamageEvent)
    {
    Super::SpawnFractureEffectsFromDamageEvent(InDamageEvent);
    
    // Has the event user data?
    if (InDamageEvent.appliedDamageUserData)
    {
    FDestructibleDamageUserData* DamageUserData = static_cast<FDestructibleDamageUserData*>(InDamageEvent.appliedDamageUserData);
    
    // If there is a valid callback function inside the user data, call it
    if (DamageUserData->CallbackFunction)
    {
    DamageUserData->CallbackFunction();
    }
    
    // Remove the user data from the cache
    check(RemoveFromDamageUserDataCache(DamageUserData));
    }
    }
    It executes the CallbackFunction and removes the DamageUserData, as it is not used again.
    Now I can make a function

    Code:
    void UExtendedDestructibleComponent::ExecuteAfterFracturedCompletely(TFunction<void()> CallbackFunction)
    {
    ApplyRadiusDamage(FLT_MAX, GetComponentLocation(), FLT_MAX, 0.0f, true, CallbackFunction);
    }
    Now I can call it this way:

    Code:
    ExecuteAfterFracturedCompletely([=]
    {
    // ...
    });
    If I call ExecuteAfterFracturedCompletely, it makes the following:

    1. Create damage user data with the Lambda-function inside
    2. Apply damage to the destructible mesh and pass the damage user data to the physics engine
    3. Receive the event from the physics engine in SpawnFractureEffectsFromDamageEvent, check if user data respectively a Lambda is included in the damage user data.
    4. If there is a valid Lambda function in the user data, call it.
    5. Destroy the damage user data as it is no longer used.

    So, basically it makes what I want and it seems to work.

    My questions are:

    1. Is there an easier way to do this without changing the engine source code?
    2. Is it safe?
    3. In ExecuteAfterFracturedCompletely, can I safely pass variables by reference ([&])?
    4. Where can I use TFunctionRef instead of TFunction to make it a bit more performant?

    I am still struggling with understanding the physics engine and especially the UDestructibleComponent.
    I appreciate every feedback.

    Thanks in advance,

    Thilo
Working...
X