@JonathanDorman: Sure, I am using Owen from Content Examples and used roughly the following steps and code. I hope I didn’t forget anything crucial! You might want to omit the replication part and implement it as a standalone game, maybe.
- Start with a Minimal Code project
- Add code to project: extend from Actor, let’s name it ControlledRagdoll
- Migrate Owen from the Content Examples demo to your project
- Create a blueprint from the migrated Owen asset, reparent it to ControlledRagdoll, and place it in the world.
- In the settings tab of the blueprinted Owen, tick “Replicates” and “Replicate Movement”
- Crank up ConfiguredInternetSpeed, ConfiguredLanSpeed etc.
- (In world settings, disable gravity)
In YourProject.Build.cs, add PhysX stuff to the following to get direct access to PhysX:
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "PhysX" });
PublicIncludePaths.AddRange(new string[] { "Engine/Source/ThirdParty/PhysX/PhysX-3.3/include" });
(see How to include the header file from a plugin - Plugins - Unreal Engine Forums)
Then, add the following to ControlledRagdoll.h:
(By the way, if someone knows how to replicate 3rd party data structs without this kind of a workaround, then please share! Thanks!)
#include <PxTransform.h>
USTRUCT()
struct FBoneState {
GENERATED_USTRUCT_BODY()
private:
UPROPERTY()
TArray<int8> TransformData;
public:
FBoneState()
{
TransformData.SetNumZeroed( sizeof(physx::PxTransform) );
}
physx::PxTransform & GetPxTransform()
{
return reinterpret_cast<physx::PxTransform &>(TransformData[0]);
}
};
UCLASS()
class RAGDOLLCONTROLLER45_API AControlledRagdoll : public AActor
{
GENERATED_UCLASS_BODY()
protected:
/** The SkeletalMeshComponent of the actor to be controlled. */
USkeletalMeshComponent * SkeletalMeshComponent;
/** Data for all bodies of the SkeletalMeshComponent, for server-to-client pose replication. */
UPROPERTY( EditAnywhere, BlueprintReadWrite, Replicated, Category = RagdollController )
TArray<FBoneState> BoneStates;
/** Store pose into the replicated BoneStates array. */
void sendPose();
/** Apply replicated pose from the BoneStates array. */
void receivePose();
public:
virtual void PostInitializeComponents() override;
virtual void Tick( float deltaSeconds ) override;
};
And to ControlledRagdoll.cpp:
#include <Net/UnrealNetwork.h>
#include <extensions/PxD6Joint.h>
#include <PxRigidBody.h>
#include <PxRigidDynamic.h>
#include <PxTransform.h>
void AControlledRagdoll::GetLifetimeReplicatedProps( TArray<FLifetimeProperty> & OutLifetimeProps ) const
{
Super::GetLifetimeReplicatedProps( OutLifetimeProps );
DOREPLIFETIME( AControlledRagdoll, BoneStates );
}
AControlledRagdoll::AControlledRagdoll(const class FPostConstructInitializeProperties& PCIP)
: Super(PCIP)
{
// enable ticking
PrimaryActorTick.bCanEverTick = true;
}
void AControlledRagdoll::PostInitializeComponents()
{
Super::PostInitializeComponents();
/* init SkeletalMeshComponent: scan all components and choose the first USkeletalMeshComponent */
// get all components of the right type
TArray<USkeletalMeshComponent*> comps;
GetComponents( comps );
// if at least one found, choose the first one
if( comps.Num() >= 1 )
{
this->SkeletalMeshComponent = comps[0];
// warn about multiple SkeletalMeshComponents
if( comps.Num() > 1 )
{
UE_LOG( LogTemp, Warning, TEXT( "(%s) Multiple USkeletalMeshComponents found! Using the first one." ), TEXT( __FUNCTION__ ) );
}
}
else
{
UE_LOG( LogTemp, Error, TEXT( "(%s) No USkeletalMeshComponents found!" ), TEXT( __FUNCTION__ ) );
}
if( this->Role >= ROLE_Authority )
{
/* We are standalone or a server */
//...
}
else
{
/* We are a network client, ... */
// Set the skeletal mesh to kinematic mode, so as to track exactly the pose stream received from the server (we do not want any local physics interactions or simulation)
if( this->SkeletalMeshComponent )
{
// cannot do this, as UE appears to be using internally a different the coordinate system for kinematic skeletons!
//this->SkeletalMeshComponent->SetSimulatePhysics( false );
}
else
{
UE_LOG( LogTemp, Error, TEXT( "(%s) Failed to switch SkeletalMeshComponent to kinematic mode on a network client!" ), TEXT( __FUNCTION__ ) );
}
}
}
void AControlledRagdoll::Tick( float deltaSeconds )
{
Super::Tick( deltaSeconds );
// If network client, then apply the received pose from the server and return
if( this->Role < ROLE_Authority )
{
receivePose();
return;
}
/* We are standalone or a server */
// Store pose so that it can be replicated to client(s)
sendPose();
//...
}
void AControlledRagdoll::sendPose()
{
// check the number of bones and resize the BoneStates array
int numBodies = this->SkeletalMeshComponent->Bodies.Num();
this->BoneStates.SetNum( numBodies );
// loop through bones and write out state data
for( int body = 0; body < numBodies; ++body )
{
physx::PxRigidDynamic * pxBody = this->SkeletalMeshComponent->Bodies[body]->GetPxRigidDynamic();
if( !pxBody )
{
UE_LOG( LogTemp, Error, TEXT( "(%s) GetPxRididDynamic() failed for body %d!" ), TEXT( __FUNCTION__ ), body );
return;
}
// Replicate pose, skip velocities
this->BoneStates[body].GetPxTransform() = pxBody->getGlobalPose();
}
}
void AControlledRagdoll::receivePose()
{
int numBodies = this->BoneStates.Num();
// Verify that the skeletal meshes have the same number of bones (for example, one might not be initialized yet)
if( numBodies != this->SkeletalMeshComponent->Bodies.Num() )
{
UE_LOG( LogTemp, Error, TEXT( "(%s) Number of bones do not match. Cannot replicate pose!" ), TEXT( __FUNCTION__ ) );
return;
}
// (omitted: verify binary compatibility of the pose data)
// Loop through bones and apply received replication data to each
for( int body = 0; body < numBodies; ++body )
{
physx::PxRigidDynamic * pxBody = this->SkeletalMeshComponent->Bodies[body]->GetPxRigidDynamic();
if( !pxBody )
{
UE_LOG( LogTemp, Error, TEXT( "(%s) GetPxRididDynamic() failed for body %d!" ), TEXT( __FUNCTION__ ), body );
return;
}
// Replicate pose, skip velocities
pxBody->setGlobalPose( this->BoneStates[body].GetPxTransform() );
}
}
ps. There was a bug with reparenting at least in 4.4.3: “Save All” does/did not save reparenting information, instead you have to manually save the asset. Maybe you can file a bug report for this if this is still true with 4.5 and you think this is not too minor to be filed?