Experiments With Chaos Physics - Using C++

Introduction

The page PhysicsUsingBlueprints describes the creation of a simple physics example implementing a trebuchet using blueprints.

This page describes implementing the same thing using c++.

Trebuchet C++ Class

Create the class using the Tools | New C++ Class menu option. Derive the class from the Actor base class and call it "Trebuchet". The editor will automatically prefix the class name with "A" so the actual c++ class name will be "ATrebuchet".

Note that this will work even if the project was created as a blueprint project - adding a c++ class will convert the project to a c++ project.

ATrebuchet Header File

This header file looks like this:

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Trebuchet.generated.h"

class USphereComponent;
class UPhysicsConstraintComponent;

UCLASS()
class PHYSICS_003_API ATrebuchet : public AActor
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	ATrebuchet();

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

public:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;

public:
	UPROPERTY(VisibleAnywhere) UStaticMeshComponent* Arm;
	UPROPERTY(VisibleAnywhere) UStaticMeshComponent* Base;
	UPROPERTY(VisibleAnywhere) UStaticMeshComponent* Ramp;
	UPROPERTY(VisibleAnywhere) UStaticMeshComponent* Weight;
	UPROPERTY(VisibleAnywhere) USphereComponent* Ball;
	UPROPERTY(VisibleAnywhere) UPhysicsConstraintComponent* ArmBaseConstraint;
	UPROPERTY(VisibleAnywhere) UPhysicsConstraintComponent* ArmWeightConstraint;
	UPROPERTY(VisibleAnywhere) UPhysicsConstraintComponent* CableConstraint;

private:
	bool bConstraintBroken = false;
};

In addition to the generated code we have added:

  • forward declarations for USphereComponent and UPhysicsConstraintComponent classes
  • member variables to hold the static mesh components which form the body of the trebuchet
  • member variables for the physics constraints
  • a member variable to track whether the cable constraint has been broken when the projectile is fired

ATrebuchet CPP File

The constructor for the ATrebuchet class is shown here:

ATrebuchet::ATrebuchet()
{
  PrimaryActorTick.bCanEverTick = true;

  static ConstructorHelpers::FObjectFinder<UStaticMesh> 
      ArmMeshAsset(TEXT("StaticMesh'/Game/Meshes/SM_Arm.SM_Arm'"));
  static ConstructorHelpers::FObjectFinder<UStaticMesh> 
      BaseMeshAsset(TEXT("StaticMesh'/Game/Meshes/SM_Base.SM_Base'"));
  static ConstructorHelpers::FObjectFinder<UStaticMesh> 
      RampMeshAsset(TEXT("StaticMesh'/Game/Meshes/SM_Ramp.SM_Ramp'"));
  static ConstructorHelpers::FObjectFinder<UStaticMesh> 
      WeightMeshAsset(TEXT("StaticMesh'/Game/Meshes/SM_Weight.SM_Weight'"));

  Base = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Base"));
  Base->SetStaticMesh(BaseMeshAsset.Object);
  Base->SetMobility(EComponentMobility::Static);
  Base->SetCollisionProfileName(TEXT("BlockAll"));
  RootComponent = Base;

  Ramp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Ramp"));
  Ramp->SetStaticMesh(RampMeshAsset.Object);
  Ramp->SetMobility(EComponentMobility::Static);
  Ramp->SetCollisionProfileName(TEXT("BlockAll"));
  Ramp->SetRelativeLocation(FVector( 0.0, 410.0, 40.0 ));
  Ramp->AttachToComponent(Base, FAttachmentTransformRules::KeepRelativeTransform );

  Arm = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Arm"));
  Arm->SetStaticMesh(ArmMeshAsset.Object);
  Arm->SetMobility(EComponentMobility::Movable);
  Arm->SetRelativeLocation(FVector( 20.000000,  57.132445,  694.646682));
  Arm->SetRelativeRotation(FRotator(0.000000, 0.000000, 40.000000));
  Arm->SetMassOverrideInKg(NAME_None, 505);
  Arm->SetSimulatePhysics(true);
  Arm->SetEnableGravity(true);
  Arm->SetCollisionProfileName(TEXT("PhysicsActor"));
  Arm->AttachToComponent(Base, FAttachmentTransformRules::KeepRelativeTransform);

  Weight = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Weight"));
  Weight->SetStaticMesh(WeightMeshAsset.Object);
  Weight->SetMobility(EComponentMobility::Movable);
  Weight->SetRelativeLocation(FVector( 10.000000, -165.000000, 640.000000));
  Weight->SetSimulatePhysics(true);
  Weight->SetMassOverrideInKg(NAME_None,4506);
  Weight->SetEnableGravity(true);
  Weight->SetCollisionProfileName(TEXT("PhysicsActor"));
  Weight->AttachToComponent(Base, FAttachmentTransformRules::KeepRelativeTransform);

  Ball = CreateDefaultSubobject<USphereComponent>(TEXT("Ball"));
  Ball->SetMobility(EComponentMobility::Movable);
  Ball->SetRelativeLocation(FVector(0.000000, 190.000000, 140.000000));
  Ball->SetSimulatePhysics(true);
  Ball->SetMassOverrideInKg(NAME_None,15);
  Ball->SetEnableGravity(true);
  Ball->SetCollisionProfileName(TEXT("PhysicsActor"));
  Ball->SetHiddenInGame(false);
  Ball->AttachToComponent(Base, FAttachmentTransformRules::KeepRelativeTransform);

  ArmBaseConstraint = CreateDefaultSubobject< UPhysicsConstraintComponent >(TEXT("ArmBaseConstraint"));
  ArmBaseConstraint->SetRelativeLocation(FVector(10.000000, 0.000000, 740.000000));
  ArmBaseConstraint->SetConstrainedComponents( Base, TEXT(""), Arm, TEXT("") );
  ArmBaseConstraint->SetLinearXLimit(ELinearConstraintMotion::LCM_Locked, 0);
  ArmBaseConstraint->SetLinearYLimit(ELinearConstraintMotion::LCM_Locked, 0);
  ArmBaseConstraint->SetLinearZLimit(ELinearConstraintMotion::LCM_Locked, 0);
  ArmBaseConstraint->SetAngularSwing1Limit(EAngularConstraintMotion::ACM_Locked, 0 );
  ArmBaseConstraint->SetAngularSwing2Limit(EAngularConstraintMotion::ACM_Locked, 0 );
  ArmBaseConstraint->SetAngularTwistLimit(EAngularConstraintMotion::ACM_Free, 0);
  ArmBaseConstraint->SetDisableCollision(true);
  ArmBaseConstraint->AttachToComponent(Base, FAttachmentTransformRules::KeepRelativeTransform);

  ArmWeightConstraint = CreateDefaultSubobject< UPhysicsConstraintComponent >(TEXT("ArmWeightConstraint"));;
  ArmWeightConstraint->SetRelativeLocation(FVector( 15.000000, -168.000000, 883.000000));
  ArmWeightConstraint->SetConstrainedComponents(Arm, TEXT(""), Weight, TEXT(""));
  ArmWeightConstraint->SetLinearXLimit(ELinearConstraintMotion::LCM_Locked, 0);
  ArmWeightConstraint->SetLinearYLimit(ELinearConstraintMotion::LCM_Locked, 0);
  ArmWeightConstraint->SetLinearZLimit(ELinearConstraintMotion::LCM_Locked, 0);
  ArmWeightConstraint->SetAngularSwing1Limit(EAngularConstraintMotion::ACM_Locked,0);
  ArmWeightConstraint->SetAngularSwing2Limit(EAngularConstraintMotion::ACM_Locked,0);
  ArmWeightConstraint->SetAngularTwistLimit(EAngularConstraintMotion::ACM_Free,0);
  ArmWeightConstraint->SetDisableCollision(true);
  ArmWeightConstraint->AttachToComponent(Base, FAttachmentTransformRules::KeepRelativeTransform);

  CableConstraint = CreateDefaultSubobject< UPhysicsConstraintComponent >(TEXT("CableConstraint"));;
  CableConstraint->SetRelativeLocation(FVector( 14.000000, 634.000000, 210.000000));
  CableConstraint->SetConstrainedComponents(Arm, TEXT(""), Ball, TEXT(""));
  CableConstraint->SetLinearXLimit(ELinearConstraintMotion::LCM_Locked, 0);
  CableConstraint->SetLinearYLimit(ELinearConstraintMotion::LCM_Locked, 0);
  CableConstraint->SetLinearZLimit(ELinearConstraintMotion::LCM_Locked, 0);
  CableConstraint->SetAngularSwing1Limit(EAngularConstraintMotion::ACM_Free,0);
  CableConstraint->SetAngularSwing2Limit(EAngularConstraintMotion::ACM_Free,0);
  CableConstraint->SetAngularTwistLimit(EAngularConstraintMotion::ACM_Free,0);
  CableConstraint->SetDisableCollision(true);
  CableConstraint->AttachToComponent(Base, FAttachmentTransformRules::KeepRelativeTransform);
}

Static Mesh Loading

For each static mesh we have an object to locate and load it:

static ConstructorHelpers::FObjectFinder<UStaticMesh> 
    ArmMeshAsset(TEXT("StaticMesh'/Game/Meshes/SM_Arm.SM_Arm'"));

This works for a simple example, in a larger project you would not do this because:

  • it forces the mesh assets to be loaded even if they are not in use
  • hard coding the asset path is brittle and hard to maintain

For most of the static mesh components we set properties like this:

Weight = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Weight"));
Weight->SetStaticMesh(WeightMeshAsset.Object);
Weight->SetMobility(EComponentMobility::Movable);
Weight->SetRelativeLocation(FVector( 10.000000, -165.000000, 640.000000));
Weight->SetSimulatePhysics(true);
Weight->SetMassOverrideInKg(NAME_None,4506);
Weight->SetEnableGravity(true);
Weight->SetCollisionProfileName(TEXT("PhysicsActor"));
Weight->AttachToComponent(Base, FAttachmentTransformRules::KeepRelativeTransform);

The location and rotation values are simply copied from the blueprint project. This is enough to show physics working from c++.

Properties and Defaults

Note that more properties required setting from c++ than when using the editor. When using the editor, changing the Mobility property to "Movable" also changes the Collision Preset value to "PhysicsActor". In c++ we need to manually set both properties like this:

Weight->SetMobility(EComponentMobility::Movable);
Weight->SetCollisionProfileName(TEXT("PhysicsActor"));

Also note the defaults for components are different depending on how they are created. A USphere component has the Hidden In Game property set to false when created in the blueprint editor and true when created from c++, so we need to explcitly set it in c++:

Ball->SetHiddenInGame(false);

Cable Release Code

This image shows the blueprint which breaks the Physics Constraint once the projectile is travelling in the right direction and at an angle of <= 45 degrees:

This is converted into the c++ code shown here:

void ATrebuchet::Tick(float DeltaTime)
{
  check(Ball);
  check(CableConstraint);

  Super::Tick(DeltaTime);

  if (!bConstraintBroken )
  {
    const FVector Velocity = Ball->GetComponentVelocity();

    // assume we firing down X axis, 
    if (Velocity.X > 0)
    {
      const float TrajectoryDegress = 
         FMath::RadiansToDegrees( FMath::Atan(Velocity.Z / Velocity.X) );
      if (TrajectoryDegress < 45)
      {
        CableConstraint->BreakConstraint();
        bConstraintBroken = true;
      }
    }
  }
}