I am running into an issue where a spawned gun actor that initially was childed to an enemy character, then dropped, and picked up by the character is not visible when equipped. What’s strange is that it is positioned correctly in world and I can fire and the bullet trace shows correctly but the mesh is just invisible.
As a workaround I’ve currently just re-spawned the gun and copied the old gun state and equipped the newly spawned actor and to the player everything works perfectly but this issue is bothering me as I think I am missing something important about spawned actors.
I already checked the obvious things in the editor. When the bug happens I pause the game and find the invisible gun actor in the world outliner. All the visible check boxes on the actor and gun skeletal mesh are checked and all the hidden ones are unchecked. It’s identical from what I can tell from another gun I have in my inventory that I equip and is visible. I’ve come to two conclusions: 1) There is a bug in the engine with this behavior or 2) I am toggling something incorrectly when moving the gun actor from the enemy to the player.
Here is some relevant code
- Dropping the Gun.
When an enemy is killed, I enable physics on the gun actor mesh (Created a physics mesh for the shooter rifle skeletal mesh and the military asset pack I found on the Epic Marketplace already had a physics mesh on their guns), and detach the gun from the enemy. Everything looks perfect as enemy death animation plays and gun is dropped to the floor
I toggle physics and collisions on/off on the AGun class with these public member functions
void AGun::EnablePhysics()
{
Mesh->SetSimulatePhysics(true);
Mesh->SetEnableGravity(true);
Mesh->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
Mesh->SetCollisionObjectType(ECollisionChannel::ECC_WorldDynamic);
Mesh->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Block);
Mesh->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Overlap);
Mesh->SetCollisionResponseToChannel(ECollisionChannel::ECC_Camera, ECollisionResponse::ECR_Ignore);
}
void AGun::DisablePhysics()
{
Mesh->SetSimulatePhysics(false);
Mesh->SetEnableGravity(false);
Mesh->SetCollisionEnabled(ECollisionEnabled::NoCollision);
Mesh->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Ignore);
}
A private member function triggered from AShooterCharacter on death:
void AShooterCharacter::DropGun(AGun* Gun)
{
AGun* CurrentGun = GetCurrentGun();
if (!Guns.Remove(Gun))
{
UE_LOG(LogTemp, Warning, TEXT("%s: DropGun - Gun=%s not in inventory"), *GetName(), (Gun ? *Gun->GetName() : TEXT("NULL")));
return;
}
Gun->SetActorHiddenInGame(false);
Gun->DetachFromActor(FDetachmentTransformRules::KeepWorldTransform);
Gun->SetOwner(nullptr);
Gun->EnablePhysics();
if (Gun == CurrentGun)
{
CurrentGunIndex = -1;
}
else
{
CurrentGunIndex = Guns.IndexOfByKey(CurrentGun);
}
ItemDropBehavior::DropGun(Gun);
}
The ItemDropBehavior namespaced free function is a helper that wraps the gun in an Item actor class AGunItem so I can handle the pickup behavior. Showing the relevant code here:
FActorSpawnParameters SpawnParams;
AGunItem* GunItem = World->SpawnActor<AGunItem>(GunItemClass, Gun->GetActorLocation(), Gun->GetActorRotation(), SpawnParams);
if (!GunItem)
{
UE_LOG(LogTemp, Warning, TEXT("ItemDropBehavior::DropGun - Unable to spawn AGunItem for Gun %s"), *Gun->GetName());
return;
}
GunItem->SetGun(Gun);
GunItem::SetGun
void AGunItem::SetGun(AGun* TheGun)
{
UE_LOG(LogTemp, Display, TEXT("%s: SetGun - %s"), *GetName(), (TheGun ? *TheGun->GetName() : TEXT("NULL")));
this->Gun = TheGun;
AttachToComponent(TheGun->GetRootComponent(), FAttachmentTransformRules::SnapToTargetIncludingScale);
}
When the character picks up the gun, it triggers a sound and moves it into their inventory. That part always works fine. It’s when I try to equip that the gun doesn’t show. Equipping with a gun that was freshly spawn displays fine which is the hack I implemented:
Note here I am using the C++17 if initializer syntax. I have another post on how you can enable this in the Unreal build C# file.
bool AShooterCharacter::PickupGun(AGun* Gun)
{
// FIXME: When picking up a gun owned by another actor, it is never visible on the new character so need to respawn it even though all properties look right
if (!Gun)
{
return false;
}
if(auto SpawnedGun = SpawnAndAddGun(Gun->GetClass()); SpawnedGun)
{
SpawnedGun->InitializeWith(*Gun);
Gun->Destroy();
return true;
}
return false;
}
bool AShooterCharacter::DoPickupGun(AGun* Gun)
{
if (!Gun)
{
UE_LOG(LogTemp, Display, TEXT("%s: Cannot pick up NULL Gun"), *GetName());
return false;
}
// can only have 1 gun of a given type
if (!CanAddGun(Gun->GetClass()))
{
UE_LOG(LogTemp, Display, TEXT("%s: Cannot pick up Gun=%s as already has gun of type %s"), *GetName(), *Gun->GetName(), *Gun->GetClass()->GetName());
return false;
}
//Gun->DetachFromActor(FDetachmentTransformRules::KeepWorldTransform);
Gun->SetActorHiddenInGame(true);
Gun->DisablePhysics();
Guns.Add(Gun);
return true;
}
What I want to do is just call DoPickupGun directly and not have to respawn it. The commented code about detaching was my attempt at clean up which has no effect on the bug I am seeing. I am unclear if you child an actor to another actor if it will automatically remove the root component mesh from the other actor. I would think so since the scene graph is more of a scene tree and you can’t logically have more than 1 parent in a tree structure. I took a look at the source code briefly but didn’t want to dig too much into it yet…
And finally here is my equip private function. Essentially there is a public member function that increments or decrements the current weapon index in a previous/next fashion from the mouse wheel or the D-pad currently on gamepad but it delegates the meat of the functionality to this private member function. The player state is something I am still working on and don’t yet fully understand but know it will be important when transitioning state between levels. I will probably be back here later when I start on that functionality…
void AShooterCharacter::DoEquipGun(AGun* Gun)
{
if (!Gun)
{
return;
}
Gun->SetActorHiddenInGame(false);
GetMesh()->HideBoneByName(OriginalMeshWeaponBoneName, EPhysBodyOp::PBO_None);
Gun->AttachToComponent(GetMesh(), FAttachmentTransformRules::KeepRelativeTransform, GunSocketBoneName);
// ownership comes up in multiplayer and damage
Gun->SetOwner(this);
// TODO: Scale for AI actors based on difficulty
Gun->ScaleReloadTime(1.0f);
InitializeGunPlayerState(Gun);
}