#Some ideas for discussion on SSR

1 messages · Page 1 of 1 (latest)

tribal mantle
#

I just take a breath and sit back, rethinking the really great stuff we are learning here. Stephen, as you said, there's not a lot of really valuable content available on the net, so again thank you so much for this course. I admit, I'm overwhelmed by the huge amount of details, that get added to the code, so while understanding the concepts, these details require taking that lean back and review.

As always happens to me, in almost each chapter, some ideas come into my mind immediately, and I'd like to share some of these for this important topic for some discussion, because I might be completely off here and would appreciate feedback.
Here we go with my thoughts:
Do we really need to capture Frame Packages on each Tick if for example running at 60fps? Why not simply add a configurable interval of for example 10 captures per second. Remember, that we do an interpolation.

The advantage in the first place is less processing overhead for the capture process itself and less memory required for the LinkedList, that does not grow that fast.

But also: in this case, why use a Linked list at all? We have a given time MaxRecordTime AND a given interval - so the data size is fixed (+-1 maybe) - means, that we can just use a simple TArray allocated in advance whichcould be iterated in a round-robin way.
So this is a fixed allocated structure in memory, so not requiring any dynamic allocations once created. Even the for sure highly optimized linked list cannot be more efficient than this, I believe.

At least I implemented this one (not yet the array version, but the fixed timer interval) It basically works, but Idid not yet test this extensively.

#

And the second really adventurous idea:

The character already has a skeletal mesh and a Physics Asset - so that's the one all collisions in game are run against. Basic Idea here: instead of defining these collision box components (which actually do hit performance when opening the blueprint and cause additional work to create) - why not take advantage of these already existing collision shapes?

Basic idea: capture the state of the current collision bodies instead of these Boxes, and maybe spawn a skelmesh at the location with this current setup captured when doing the SSR validation?

#

Just a silly dump of some code snippets from my experiments so far to approach that topic

void ABlasterCharacter::Herb64ExperimentForSSR()
{

    USkeletalMeshComponent* MeshComponent = GetMesh();
    if (MeshComponent)
    {
        TArray<FName> BoneNames;
        MeshComponent->GetBoneNames(BoneNames);
        for (FName BoneName : BoneNames)
        {
            // FTransform GetBoneTransform( int32 BoneIndex, const FTransform& LocalToWorld ) const;
            int32 BoneIndex = MeshComponent->GetBoneIndex(BoneName);
            FTransform BoneXForm = MeshComponent->GetBoneTransform(BoneIndex);
            //if (GEngine) GEngine->AddOnScreenDebugMessage(-1, 15.f, FColor::Yellow, FString::Printf(TEXT("Bone: %s"), *BoneName.ToString()));
        }

        UPhysicsAsset* PhysAsset = MeshComponent->GetPhysicsAsset();
        if (PhysAsset)
        {
            for (USkeletalBodySetup* Setup : PhysAsset->SkeletalBodySetups)
            {
                int32 ElementCount = Setup->AggGeom.GetElementCount();
                for (int32 i = 0; i < ElementCount; i++)
                {
                    // Weird type name Sphyl for the Capsule type
                    // https://forums.unrealengine.com/t/why-does-unreal-use-the-word-sphyl-what-does-this-word-from/102999
                    EAggCollisionShape::Type ElementType = Setup->AggGeom.GetElement(i)->GetShapeType();

                    // no pretty print possible - enum is not an UEnum...
                    if (GEngine) GEngine->AddOnScreenDebugMessage(-1, 15.f, FColor::Yellow, FString::Printf(TEXT("Element Type: %d"), ElementType));
                }
            }
            //PhysAsset->GetCollisionMesh()
        }
        // Getting array of transforms?? 
        TArray<FTransform> XF = GetMesh()->GetBoneSpaceTransforms();

        //MeshComponent->SetRefPoseOverride(XF);   // could be useful?
        //GetMesh()->RefreshBoneTransforms();
        //GetMesh()->GetBoneTransform()
    }
}```
#

So we can get the transforms, but how could we spawn an actor and apply all these transforms just for the collision tests? And, of course, without causing a performance hit compared to the approache with the collision boxes. The idea behind is less work in defining the hitboxes and getting a more generic approach.

near mirage
#

I independently had the same thought and investigated this a little. I was able to generate my BoxComponents using the physics asset. I've currently encountered two issues. The first is that physics components can have several capsules per bone. For example in my physics asset, the backpack bone has a blanket capsule and a backpack capsule. We can fix by allowing multiple box extents per bone by making the name unique. The second issue is I need to find where the magic scale is set. Everything is 2x too big (solved - see below). Here's my experimental code so far and I pasted the resulting collision boxes in an image below:

void ABlasterCharacter::CreateCollisionBoxes()
{
    UPhysicsAsset* PhysicsAsset = GetMesh()->GetPhysicsAsset();
    if (PhysicsAsset)
    {
        for (USkeletalBodySetup* BodySetup : PhysicsAsset->SkeletalBodySetups)
        {
            FName BoneName = BodySetup->BoneName;

            // Capsule - Cylinder with spherical top/bottom or sphyinder.
            for (const FKSphylElem& Capsule : BodySetup->AggGeom.SphylElems)
            {
                float CylinderLength = Capsule.Length;
                float Radius = Capsule.Radius;

                // Everything is 2x too big.  Where do I get scaler?
                FVector BoxExtent = FVector( 2*Radius, 2*Radius, CylinderLength + 2*Radius) / 2;
                UBoxComponent* BoxComponent = NewObject<UBoxComponent>(this);
                BoxComponent->SetupAttachment(GetMesh(), BoneName);
                BoxComponent->RegisterComponent();
                BoxComponent->SetCollisionEnabled(ECollisionEnabled::NoCollision);
                BoxComponent->SetRelativeLocation(Capsule.Center);
                BoxComponent->SetRelativeRotation(Capsule.Rotation);
                BoxComponent->SetBoxExtent(BoxExtent);
                HitCollisionBoxes.Add(BoneName, BoxComponent);
            }
        }
    }
}
#

I only added capsule support in my experiment. A more complete solution would support other physics shapes such as spheres and boxes.

#

This is pretty promising though. Way easier than manually adding and sizing collision boxs to your character and the result matches your physics asset.

#

For reference, here's my physics asset. Note the blanket collision box is missing in my result because it is attached to the backpack bone rather than the blanket bone. Easily fixed.

near mirage
#

I'm a dummy. Extent is measured from the center of the box. That's why everything was 2x too big!

#

I think this experiment was a success! I'll clean up the code and post as a separate topic so it's easier to find.

jovial tree
#

You two like bro UE C+ gangsters, keep it up 👍

tribal mantle
#

In the meantime, I did a rework on the proposed approach by @near mirage - giving up the idea of having any replay of Character Animation State or whatever - this one really seems to lead to a dead end. But instead of turning everything into boxes, this one simply recreates the original shapes (Box, Sphere and Capsule). So we handle generic Shapes in SSR as they appear in the original Physics Asset. This seems to be more accurate. More to follow..

tribal mantle
#

Going on here as promised. Discord seems to behave always different if adding content: sometimes it complains on >2000 characters and does not allow to add content at all (without Nitro), one time it added some (not properly C++ formatted) fragment for download - but most likely I'm just too dumb to properly use it correctly. So need to find some better way to share my code (apart from Github)
But anyway, trying again: one very if not most important part in my implementation in addition to be potentially more precise to @near mirage implementation is to add the Unreal Component Tags feature. I'm using 2 of them, added during the CollisionShape construction - the code I unfortunately cannot not share due to the Discord limits for now...

  1. BoneName
  2. ShapeType (BOX, SPHERE or CAPSULE) - the only ones supported as Collision Shapes
    These Tags extremely simplify the code for SSR Hit Confirmation - there's no need to double traces for early exit to detect a HeadShot any more. We now can even react to a Leg Shot or whatever you want in a different way, by just checking the hit bone name Tag. So instead of returning the bool for headshot, we could return a more detailed info on the shot. Here's a short enough example for the HitScan Weapon... ok too long, but sometimes in discord, waiting some minutes helps... discord needs some time to separate posts, as I believe???
tribal mantle
#

OK, now the code here after waiting: a much more simplified version for Confirm Hit using the Tags. This is for the HitScan Weapon type - and while this even includes some of my debug code it is much more streamlined c++ FServerSideRewindResult ULagCompensationComponent::ConfirmHit(const FFramePackage& Package, ABlasterCharacter* HitCharacter, const FVector_NetQuantize& TraceStart, const FVector_NetQuantize& HitLocation) { if (HitCharacter == nullptr) return FServerSideRewindResult(); FFramePackage CurrentFrame; CacheSSRCollisionShapes(HitCharacter, CurrentFrame); MoveSSRCollisionShapes(HitCharacter, Package); PrepareSSRHitshapeCollision(HitCharacter); FHitResult ConfirmHitResult; const FVector TraceEnd = TraceStart + (HitLocation - TraceStart) * 1.25f; bool bIsHeadShot = false; bool bHit = false; UWorld* World = GetWorld(); if (World) { World->LineTraceSingleByChannel(ConfirmHitResult, TraceStart, TraceEnd, ECC_SSRHitShape); bHit = ConfirmHitResult.bBlockingHit; FName BoneName = FName("n/a"); FName ShapeType = FName("n/a"); if (bHit && ConfirmHitResult.Component.IsValid() && GetShapeComponentTags(ConfirmHitResult.Component.Get(), BoneName, ShapeType)) { bIsHeadShot = BoneName == "head"; if (bDebugShowHitShapes) { HLog::Info(TEXT("Hit bone: %s, shape type: %s"), *BoneName.ToString(), *ShapeType.ToString()); DrawDebugHitResult(ConfirmHitResult.Component.Get()); } } } ResetSSRCollisionShapes(HitCharacter, CurrentFrame); return FServerSideRewindResult{ bHit, bIsHeadShot }; }
Yes, this involved quite some refactoring of the LagCompensationComponent - code is too long to share - but i believe you get the idea. Feel free to ask for details,

tribal mantle
#

What I forgot to mention: using that Tags feature should even be useable if not implementing all the generic hit shapes stuff and sticking to the default manually created boxes - not 100% sure, but...

tribal mantle
#

Some code from the refactored LagCompensation Component - reusing the same vector for all 3 possible hit shape types is important for this. The FramePackage now includes ShapeInformations```c++
/**

  • Generic SSR collision shapes instead of having boxes only
  • The ShapeExtent Vector is used to store information for all 3 types of Collision
  • shape components - same semantic can be easily stored this way
    • Box - x,y,z - full vector required to determine extent
    • Capsule - x = HalfHeight, y = Radius
    • Sphere - x = radius
      */
      USTRUCT(BlueprintType)
      struct FShapeInformation
      {
      GENERATED_BODY()

      UPROPERTY()
      ESSRShapeType ShapeType = ESSRShapeType::CST_Unknown;

      UPROPERTY()
      FVector Location = FVector(EForceInit::ForceInitToZero);

      UPROPERTY()
      FRotator Rotation = FRotator(EForceInit::ForceInitToZero);

      UPROPERTY()
      FVector ShapeExtent = FVector(EForceInit::ForceInitToZero);
      };

USTRUCT(BlueprintType)
struct FFramePackage
{
GENERATED_BODY()

UPROPERTY()
float Time = 0;

UPROPERTY()
TMap<FName, FShapeInformation> HitShapeInfo;

UPROPERTY()
ABlasterCharacter* Character = nullptr;

};

near mirage
#

It's really cool that you implemented the exact shapes. Well done! However something to consider is whether this work is warranted or advisable. That might depend on your game. There can be substantial error in the SSR calculations (depending of velocity) because the ping is just an estimate and typically varies quite a bit. A few milliseconds off can result in a substantial difference in rewind positions. For this reason it might make sense to actually inflate the dimensions of the SSR targets rather than strive to more accurately mirror their shapes. Using a bounding rectangles rather than exact shapes automatically extends the bounds a bit. Further inflating these rectangles will reduce the chance of valid hits being discarded. Just food for thought. I think your implementation is very nice though!

tribal mantle
# near mirage It's really cool that you implemented the exact shapes. Well done! However som...

Thank you for your kind words. Yes, in fact I also did already think about adding some safety factor to the shape dimensions as a configurable setting. Did not yet do this, currently trying to get the whole animation stuff reworked - based on your excellent idea to use the orientation warping. Diving into new dimensions of the anim bp, studying lyra and multithreaded animation etc.. super cools stuff.