#152 Implementing Net Serialize

35 messages · Page 1 of 1 (latest)

cerulean atlas
#

I have to admit that I’m finding this section a little confusing. Below was my challenge solution to serialize myself. My changes are worth noting and I have some questions listed below.

bool FAuraGameplayEffectContext::NetSerialize(FArchive& Ar, UPackageMap* Map, bool& bOutSuccess)
{
    // Serialize the base class
    Super::NetSerialize(Ar, Map, bOutSuccess);

    enum RepFlag
    {
       REP_IsBlockedHit,
       REP_IsCriticalHit,
       REP_MAX
    };
    
    uint8 RepBits = 0;
    if (Ar.IsSaving())
    {
       if (bIsBlockedHit)
       {
          RepBits |= 1 << REP_IsBlockedHit;
       }
       if (bIsCriticalHit)
       {
          RepBits |= 1 << REP_IsCriticalHit;
       }
    }
    Ar.SerializeBits(&RepBits, REP_MAX);

    if (Ar.IsLoading())
    {
       bIsBlockedHit  = RepBits & (1 << REP_IsBlockedHit);
       bIsCriticalHit = RepBits & (1 << REP_IsCriticalHit);
    }
    
    bOutSuccess = true;
    return true;
}

Note that I call the base class rather than duplicate that code, and handle loading differently. I prefer not to duplicate base code if I can avoid it so this seems better. The same number of bytes serialized either way as we now have 9 bits to serialize. Any reason not to do this?

Also looking at Stephen’s code I see him both save/loading the packed bits like I do, and later serializing the Booleans again as follows:

if (RepBits & (1 << 7))
{
    Ar << bIsBlockedHit;
}
if (RepBits & (1 << 8))
{
    Ar << bIsCriticalHit;
}

Isn’t this just serializing the crit and blocked bools again for both load and save if the bit is set? Unnecessarily bloating the net serialize structure by two bools? See the load technique I used instead.

cerulean atlas
#

I put a breakpoint in IsLoading for multiplayer and it never gets called when I launch projectiles at enemies! I guess that make sense since we calculate damage on the server and RPC to the client via ShowDamageNumber. So where is this whole exercise in custom net replication used? Interestingly, it is called when hitting Server Aura from a Client Aura. Another point of confusion is why do we need to customize NetSerialize at all, rather than just mark member variables as replicated and use built-in replication? I high level explanation of why we do this would definately reduce my confusion.

#

One final thing that just seems wrong is adding BlockedHit and CriticalHit globally to all effect specs. In a world where you want to minimize network traffic, it seems wasteful to serialize unused information for all effects rather than the damage effect. Even if the added info is just a few booleans that control what is actually serialized.

cerulean atlas
#

So, in summary, here are my questions:

  1. Is there any reason not to call super to serialize (other than possible less tight packing of bools in some cases)? Seems like a better coding practice.
  2. Ar << bIsBlockedHit seems unnecessary (see my solution). Is this just a bug?
  3. The damage effect context just runs on the server. So why do we need to implement NetSerialize at all? Seems to work fine without it because we later RPC our booleans to the locally controlled client via ShowDamageNumber when we actually display the popup. Perhaps this is just for illustration purposes?
  4. Why is NetSerialize used at all rather than just mark member variables as remote? Why does Unreal have two mechanisms here? When will this object be serialized - when is it really necessary to implement this? I know the glib answer is when we replicate, but I was surprised that this was only called at startup, and when targeting another Aura character.
  5. Adding BlockedHit and CriticalHit to all gameplay effect contexts globally in UAuraAbilitySystemGlobals::AllocGameplayEffectContext seems wrong as it unnecessarily bloats the amount of data serialized for all effect contexts. I know that we are only adding bits that control what is actually serialized but shouldn’t this be applied more surgically to just where we need it? This design doesn't scale well as you add more and more data. The whole effect context thing seems weird to me as a dumping place for this data rather than putting it on the effect spec on a class-by-class basis seems anti-object-oriented. I know this is Epic's design, but seems like a bit of a wart. Makes me wonder if we could put this data on the effect spec instead?
cerulean atlas
#

After digging a bit more, I see that Stephen was faithfully following the example from an Ironwood Games developer post which has the same issues. https://www.thegames.dev/?p=62. While in general, GAS is very well implemented, this area still seems a little rough to me.

cerulean atlas
#

Ok, I think I now know enough to answer most of my questions.

First of all, I now have a better understanding of the parts of an effect. I’m a visual thinker, so a block diagram of this in the course would have helped me.

GamePlay Effect – These are immutable components that provide data for the effect. Analogous to a mesh on a character. It is not changed at runtime.

GamePlay Effect Spec – This is the runtime version of the Gameplay Effect. They hold a reference to the gameplay effect and an instance of a context and can be modified at runtime.

GamePlay Effect Context – General project wide structure that can be used by the effect spec to hold miscellaneous data. This object is defined globally for various kinds of data and instanced for each effect. Each data type has an associate bool that controls whether the data part is active. It also handles network replication for these data types.

cerulean atlas
#

• A.SerializeBits will either read or write the bits depending on whether we are loading and saving. So there is no reason to use the << operation to serialize the Boolean again. We can use the flag directly as I did in my solution. I believe that this was an error in the sample code Stephen referenced.

• Calling Super in NetSerialize works and is a good practice to protect against changes to the base class. Also minimizes code duplication. I believe that this is an improvement over the sample code in spite of the fact that the bools are packed into two separate blocks. In our case the total bytes serialized is the same as we need one more byte to hold both of our bools.

• There was no need to implement NetSerialize for our case of BlockHit and CriticalHit because our damage effect only runs on the server and is not serialized. However, implementing this function provides a more complete solution for the future and is therefore a good practice.

• It is still a mystery to me as to why the context exists at all rather than put the data on the effect spec and replicate it directly from there. Also a mystery why the lower-level NetSerialize is used here rather than the standard variable replication. [Edit] Actually maybe it makes sense. Perhaps so the serialization strategy can be bulk controlled for all properties for an effect level specification?

lusty elm
lusty elm
# lusty elm Thank you DotTheEyes,I hava same confusion, and you have helped me a lot.

I think I can answer one of your questions. You can comment on this line of code:

// OnGameplayEffectAppliedDelegateToSelf.AddUObject(this, &UAuraAbilitySystemComponent::ClientEffectApplied);

This way, when the "server Aura" attacks the "client Aura", NetSerialize will not be called. This is because ClientEffectApplied is a Client RPC function.

My understanding is that when ClientEffectApplied is executed, which happens during RPC (Server => Client) communication, NetSerialize will be executed as well.

#

#include "AuraAbilityTypes.h"

bool FAuraGameplayEffectContext::NetSerialize(FArchive& Ar, UPackageMap* Map, bool& bOutSuccess)
{
    uint8 RepBits = 0;
    if (Ar.IsSaving())
    {
        if (bIsBlockedHit)
        {
            RepBits |= 1 << 0;
        }
        if (bIsCriticalHit)
        {
            RepBits |= 1 << 1;
        }
    }
    
    Ar.SerializeBits(&RepBits, 2);
    if (RepBits & 1 << 0)
    {
        Ar << bIsBlockedHit;
    }
    if (RepBits & 1<< 1)
    {
        Ar << bIsBlockedHit;
    }

    return Super::NetSerialize(Ar, Map, bOutSuccess);
}

This is my custom NetSerialize. Even if I comment them all out, my Firebolt Ability still works well on both Dedicated Server and Listen server. So I am confused about the purpose of this NetSerialize override. My understanding is that the custom NetSerialize is meant to reduce the traffic bandwidth when handling RPC transmission of FStructs.

The course introduced the purpose of NetSerialize at this time point, which has left me confused (maybe it will be needed in the future, but I haven't started that part of the course yet).

From my debugging results, "GE Spec apply to target" happens on the Server and is replicated to the Clients, which is correct. So it doesn't go through RPC, and thus our custom FAuraGameplayEffectContext::NetSerialize is not executed. It only runs once when the client connects to the server.

Therefore, I am really puzzled about implementing the custom NetSerialize here.

neon path
#

Debug this line : return Super::NetSerialize(Ar, Map, bOutSuccess);
You will find it calling every effect apply to an ASC

#

Actually the effect replication

#

The data in Server's ram is not readable in Client. So the data (effect) must convert to something that can transfer through the network. such as "01010101" send to Clint by cable. And convert back to data in Ram. That's the replication. It base on serialize

#

Ar<<bIsBlockedHit; Convert the bool to something like "01010101" and send or read in this process

#

So that you really need is
Ar << bIsBlockedHit;
Ar << bIsCriticalHit;
return Super::NetSerialize(Ar, Map, bOutSuccess);

neon path
lusty elm
# neon path Debug this line : return Super::NetSerialize(Ar, Map, bOutSuccess); You will fin...

Oh, Maybe I didn't make it clear. My debugging results show that serialization doesn't execute every time the character successfully attacks. It only executes once at the very beginning. Surprisingly, even if this serialization method is not implemented, the program still works fine, and the client can still receive critical hit data. This is very strange, indicating that the data is directly sent to the client through replication, while Net serialization only executes during RPCs.

neon path
lusty elm
#

No, it works both on Dedicated Server and Listen server, see my previous comments

#

RPC and Replication is different in my test cases.

#

Replication will not execute our custom NetSerialize, But RPC always.

lusty elm
neon path
#

It shouldn't working. I don't understand

#

Do you have this open?
template<>
struct TStructOpsTypeTraits< FAuraGameplayEffectContext > : public TStructOpsTypeTraitsBase2< FAuraGameplayEffectContext >
{
enum
{
WithNetSerializer = true,
WithCopy = true // Necessary so that TSharedPtr<FHitResult> Data is copied around
};
};

lusty elm
#

yes, I we are same

neon path
#

That's strange. Hope someone else can help you

lusty elm
cerulean atlas
#

I answer that above. Critical hit and blocked hit only run on the server so NetSerialize is not actually used in this case. We manually rpc outside the effect to the controlling client to show the popup text. I detail all my findings in my sixth/seventh post above.

cerulean atlas
neon path
cerulean atlas
shrewd shell
shrewd shell
cerulean atlas