Hi,
I’m trying to implement a custom FGameplayEffectContext
, plan is to extend the original one with some custom attributes. At some point I realized that gameplay effects are not replicated on client(s) anymore while it is working on the servers client (play as listen server).
Infos:
- I’m following a course/tutorial using UE 5.2 (Win) where it seems to work, while I’m using UE 5.3.1 on MacOS.
- The general setup to use
MyGameplayEffectContext
seems to work as I am able set breakpoints to debug it. - I checked a posting on TheGames.Dev and Lyra but could not find any
Problem/What happens:
The game is a common ARPG (top-down) using GAS with health/mana and other attributes. Health and mana are setup as a gameplay effects that are replicated on start (to fill up to max health/max mana). This effect is not reaching the clients while it works on the server side. So my health/mana globes for all client players are empty while the server player has it filled (unless I use a dedicated server, then all versions have empty globes).
I could trace my problem down but now I’m lost with the messages and what I should do. So I’d really like to ask for some help to solve this issue
#pragma once
#include "GameplayEffectTypes.h"
#include "MyAbilityTypes.generated.h"
USTRUCT(BlueprintType)
struct FMyGameplayEffectContext : public FGameplayEffectContext
{
GENERATED_BODY()
public:
bool IsBlockedHit() const { return bIsBlockedHit; }
bool IsCriticalHit() const { return bIsCriticalHit; }
void SetIsBlockedHit(bool bInIsBlockedHit) { bIsBlockedHit = bInIsBlockedHit; }
void SetIsCriticalHit(bool bInIsCriticalHit) { bIsCriticalHit = bInIsCriticalHit; }
virtual UScriptStruct* GetScriptStruct() const override
{
return FGameplayEffectContext::StaticStruct();
}
/** Creates a copy of this context, used to duplicate for later modifications */
virtual FMyGameplayEffectContext* Duplicate() const override
{
FMyGameplayEffectContext* NewContext = new FMyGameplayEffectContext();
*NewContext = *this;
if (GetHitResult())
{
// Does a deep copy of the hit result
NewContext->AddHitResult(*GetHitResult(), true);
}
return NewContext;
}
virtual bool NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess) override;
protected:
UPROPERTY()
bool bIsBlockedHit = false;
UPROPERTY()
bool bIsCriticalHit = false;
};
template <>
struct TStructOpsTypeTraits<FMyGameplayEffectContext> : public TStructOpsTypeTraitsBase2<FMyGameplayEffectContext>
{
enum
{
WitNetSerializer = true,
WithCopy = true
};
};
#include "MyAbilityTypes.h"
bool FAuraGameplayEffectContext::NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess)
{
uint32 RepBits = 0;
if (Ar.IsSaving())
{
if (bReplicateInstigator && Instigator.IsValid())
{
RepBits |= 1 << 0;
}
if (bReplicateEffectCauser && EffectCauser.IsValid())
{
RepBits |= 1 << 1;
}
if (AbilityCDO.IsValid())
{
RepBits |= 1 << 2;
}
if (bReplicateSourceObject && SourceObject.IsValid())
{
RepBits |= 1 << 3;
}
if (Actors.Num() > 0)
{
RepBits |= 1 << 4;
}
if (HitResult.IsValid())
{
RepBits |= 1 << 5;
}
if (bHasWorldOrigin)
{
RepBits |= 1 << 6;
}
if (bIsBlockedHit)
{
RepBits |= 1 << 7;
}
if (bIsCriticalHit)
{
RepBits |= 1 << 8;
}
}
Ar.SerializeBits(&RepBits, 9);
if (RepBits & (1 << 0))
{
Ar << Instigator;
}
if (RepBits & (1 << 1))
{
Ar << EffectCauser;
}
if (RepBits & (1 << 2))
{
Ar << AbilityCDO;
}
if (RepBits & (1 << 3))
{
Ar << SourceObject;
}
if (RepBits & (1 << 4))
{
SafeNetSerializeTArray_Default<31>(Ar, Actors);
}
if (RepBits & (1 << 5))
{
if (Ar.IsLoading())
{
if (!HitResult.IsValid())
{
HitResult = TSharedPtr<FHitResult>(new FHitResult());
}
}
HitResult->NetSerialize(Ar, Map, bOutSuccess);
}
if (RepBits & (1 << 6))
{
Ar << WorldOrigin;
bHasWorldOrigin = true;
}
else
{
bHasWorldOrigin = false;
}
if (RepBits & (1 << 7))
{
Ar << bIsBlockedHit;
}
if (RepBits & (1 << 8))
{
Ar << bIsCriticalHit;
}
if (Ar.IsLoading())
{
AddInstigator(Instigator.Get(), EffectCauser.Get()); // Just to initialize InstigatorAbilitySystemComponent
}
bOutSuccess = true;
return true;
}
So I started to trace it down and here are my findings:
-
When I reduce
NetSerialize
to use only 7 Bits it works like a charm. I can even swap out original values for my two bools. But whenever I start using the 8th bit it breaks again. Breakpoint works and the function is executed without problems. I guess this is just a strange side effect as I think #2 is really more the issue (but it’s a guess). -
I’m pretty sure that there is an error within the header file and it should read like this:
virtual UScriptStruct* GetScriptStruct() const override
{
return StaticStruct();
}
to return the StaticStruct()
value of the current implementation and not the parent one. So I tried that out, result is a fatal error which breaks PIE and crashed UE Editor:
2023-10-21 11:52:21.736518+0200 UnrealEditor-Mac-DebugGame[77998:20062618] [UE] Fatal error: [File:./../Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Private/GameplayEffectTypes.cpp] [Line: 423]
FGameplayEffectContextHandle::NetSerialize called on data struct MyGameplayEffectContext without a native NetSerialize
I took a look into line 423 and it’s context:
if (ScriptStruct->StructFlags & STRUCT_NetSerializeNative)
{
...
}
else
{
// This won't work since FStructProperty::NetSerializeItem is deprecrated.
// 1) we have to manually crawl through the topmost struct's fields since we don't have a FStructProperty for it (just the UScriptProperty)
// 2) if there are any UStructProperties in the topmost struct's fields, we will assert in FStructProperty::NetSerializeItem.
ABILITY_LOG(Fatal, TEXT("FGameplayEffectContextHandle::NetSerialize called on data struct %s without a native NetSerialize"), *ScriptStruct->GetName());
}
I checked my code against the tutorials (which clearly not using #2 correctly - but it’s working with UE 5.2 for the tutor). Other posts/examples like Lyra and TheGames.Dev Blog (Creating your own Gameplay Effect Context. – The Games Dev) are showing that #2 is initially wrong and should be changed to MyGameplayEffectContext::StaticStruct()
.
When I checked out Lyra I saw that there are some compiler directives for IRIS to map the NetSerialize. But I couldn’t get that to work with copy & paste (adapted the macros of course), because the was a C++ compiler error due to vtables. Anyway, I’m not using IRIS as far as I know (at least there is no setting in MyGame.Build.cs
or the .uproject
file).
I really appreciate any help to solve this issue which is causing me headaches for two full days now