问题描述:毕业设计中所遇到的疑难杂症。
玩家角色类上保存着大小恒为2的结构体数组,结构体数组中含有指针类型的变量,代码如下。
结构体数组:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
TArray<FSlotInfo> WeaponSlots;
结构体:
USTRUCT(BlueprintType)
struct FSlotInfo
{
GENERATED_USTRUCT_BODY()
public:
UPROPERTY(BlueprintReadWrite)
int32 WeaponID;
UPROPERTY(BlueprintReadWrite)
ACDWeaponBase* SlotWeaponPtr;
UPROPERTY(BlueprintReadWrite)
FName SocketName;
UPROPERTY(BlueprintReadWrite)
bool bIsUsed;
FSlotInfo(FName soltName)
{
WeaponID=-1;
SlotWeaponPtr=nullptr;
this->SocketName=soltName;
bIsUsed=false;
WeaponID=-1;
}
FSlotInfo()
{
}
bool operator==(const FSlotInfo OtherSlot) {
return this->WeaponID==OtherSlot.WeaponID;
}
};
解释:此结构体数组对应玩家的武器插槽(受到动画资源限制,导致角色初始就携带一把初始武器,大小写死恒为2)。此前,未增加武器指针时,只保存武器ID,会出现角色只能拾取不同的武器这个Bug。后续为了支持角色拾取相同的武器,在结构体中增加武器指针属性,用于区分武器实例。
报错信息:
LogOutputDevice: Error: Ensure condition failed: bIsValidObjectReference [File:D:/Build/++UE4/Sync/Engine/Source/Runtime/CoreUObject/Private/UObject/UObjectGlobals.cpp] [Line: 3453] Invalid object referenced by the PersistentFrame: 0x00007ffecfd84db1 (Blueprint object: Blueprint /Game/Blueprint/Character/BP_Michelle.BP_Michelle, ReferencingProperty: ObjectProperty /Script/FrontLine.SlotInfo:SlotWeaponPtr, Instance: BP_Michelle_C /Game/Blueprint/Game/Map/UEDPIE_0_KillAll.KillAll:PersistentLevel.BP_Michelle_C_0, Address: 0x0000010981295e0 0) - If you have a reliable repro for this, please contact the development team with it.
问题分析和解决方案:
这个Bug复现率极高,但又不是每次运行都会出现。且出现后,会直接杀死UE进程。
通过程序崩溃信息,找出报错位置:
if (!ensureMsgf(bIsValidObjectReference
, TEXT("Invalid object referenced by the PersistentFrame: 0x%016llx (Blueprint object: %s, ReferencingProperty: %s, Instance: %s, Address: 0x%016llx) - If you have a reliable repro for this, please contact the development team with it.")
, (int64)(PTRINT)Object
, *GetBlueprintObjectNameLambda(GetSerializingObject())
, GetSerializedProperty() ? *GetSerializedProperty()->GetFullName() : TEXT("NULL")
, GetSerializedDataContainer() ? *GetSerializedDataContainer()->GetFullName() : TEXT("NULL")
, (int64)(PTRINT)&Object))
{
// clear the property value (it's garbage)... the ubergraph-frame
// has just lost a reference to whatever it was attempting to hold onto
Object = nullptr;
}
发现程序卡死在了这个Ensure,再根据Ensure的参数,找到参数的含义:
protected:
virtual FArchive& operator<<(UObject*& Object) override
{
#if !(UE_BUILD_TEST || UE_BUILD_SHIPPING)
const bool bIsValidObjectReference = (Object == nullptr || Object->IsValidLowLevelFast());
定位到大致问题,因为UE4的垃圾回收机制,会回收无用的标记过的无用变量,而判断无用变量的条件根据上述代码所示就是是否为空指针。
分析到这里,就不难发现导致程序崩溃的原因。角色身上未拾取武器时,武器插槽数组中的指针均为nullptr,但是我的想法是一直保留数组中的空指针,因为游戏中玩家可以反复拾取和丢弃武器。但是UE4的GC机制会判断武器插槽中指针为空时,就把内存回收掉了,导致我下一次使用不了该指针变量。
解决方案:
首先声明,我的解决方案很粗糙,因为要赶毕设的时间。作下简单分享,因为角色具有初始武器,且该武器不能被丢弃,因此玩家身上一直保存着该武器的有效指针。所以我的做法时,在玩家未拾取武器或丢弃武器时,不将武器插槽数组的指针置空,而是指向角色的初始武器,这样UE4的GC机制就会跳过该指针变量。等角色拾取武器时,再将指针重新赋值。这样做后,并没有发现此类的崩溃错误。代码如下:
// 如果丢的是武器插槽1的武器
for (int i = 0; i < WeaponSlots.Num(); i++)
{
if (WeaponSlots[i].SlotWeaponPtr == DropWeapon && WeaponSlots[i].SlotWeaponPtr)
{
// 先将当前武器插槽空余出来
WeaponSlots[i].bIsUsed = false;
WeaponSlots[i].WeaponID = -1;
WeaponSlots[i].SlotWeaponPtr = PistolWeapon;
// 如果丢的武器插槽1的武器,将武器2移动到武器插槽1,空出插槽2以便拾取武器
if (i == 0)
{
if(WeaponSlots[i + 1].bIsUsed)
{
WeaponSlots[i].bIsUsed = WeaponSlots[i + 1].bIsUsed;
WeaponSlots[i].WeaponID = WeaponSlots[i + 1].WeaponID;
WeaponSlots[i].SlotWeaponPtr = WeaponSlots[i + 1].SlotWeaponPtr;
WeaponSlots[i].SlotWeaponPtr->AttachToComponent(GetMesh(),
FAttachmentTransformRules::SnapToTargetNotIncludingScale,
WeaponSlots[i].SocketName);
WeaponSlots[i + 1].bIsUsed = false;
WeaponSlots[i + 1].WeaponID = -1;
WeaponSlots[i + 1].SlotWeaponPtr = PistolWeapon;
}
}
}
}
总结:上述解决方案只是为了图快,但并不建议。时间充裕的话,可以重构武器系统。或者在武器基类上定义一个静态变量用作武器的计数,每次实例化武器的时候,将该计数值加一,并保存为区分相同武器不同实例的ID,在角色武器插槽数组中保存该ID,也可区分。
(以上只是一些简单分享)