Yes! This way the lever doesnât need direct knowledge of the train. Instead, it simply announces its action (like being switched), and any interested object (like the train) that is listening can respond accordingly.
Yes. In the BP_Train add a âChild Actor Componentâ; this component is designed to hold an instance of another actor (BP_Lever in this case). Then Set the Child Actor Class to BP_Lever in the properties of the Child Actor Component. Now you can position the lever relative to the train directly in the Blueprint editor.
The line between c++ and bp is a bit blurry. I aim to get all base functionality in c++ and use bp to mostly inherit, set cosmetics, references, instance specifics, etc. For the levers specifically I would add the child components, binds and any other code needed in c++ then expose a TSubclassOf to BP to set the child class and its tansform in BP.
IMO the lever needs to be an actor because it requieres more components for it to work. Components commonly used to make code modular, the lever serves that purpose but it requieres more than what a AC can offer.
Yes.
MyTrain.h
// .h
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Components")
TObjectPtr<UStaticMeshComponent> BodyComponent = nullptr;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="LeverComponents")
TObjectPtr<UChildActorComponent> LeverA = nullptr;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "LeverComponents")
TObjectPtr<UChildActorComponent> LeverB = nullptr;
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category="Levers")
TSubclassOf<class ALeverBase> LeverAClass;
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category="Levers")
TSubclassOf<class ALeverBase> LeverBClass;
// This will send it directly to blueprint.
UFUNCTION(BlueprintImplementableEvent, meta = (DisplayName = "LeverATriggered"))
void LeverATriggered();
UFUNCTION(BlueprintImplementableEvent, meta = (DisplayName = "LeverBTriggered"))
void LeverBTriggered();
private:
#if WITH_EDITOR
virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;
#endif
MyTrain.cpp
AMyTrain::MyTrain()
{
BodyComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("StaticMeshComponent"));
SetRootComponent(BodyComponent);
LeverA = CreateDefaultSubobject<UChildActorComponent>("ChildActorComponent0");
LeverB = CreateDefaultSubobject<UChildActorComponent>("ChildActorComponent1");
LeverA->SetupAttachment(RootComponent);
LeverB->SetupAttachment(RootComponent);
}
void AMyTrain::BeginPlay()
{
Super::BeginPlay();
if (ALeverBase* tmp = Cast<ALeverBase> (LeverA->GetChildActor()))
{
tmp->Triggered.AddDynamic(this, &AMyTrain::LeverATriggered);
}
if (ALeverBase* tmp = Cast<ALeverBase> (LeverB->GetChildActor()))
{
tmp->Triggered.AddDynamic(this, &AMyTrain::LeverBTriggered);
}
}
// ...
// PostEditChangeProperty() will catch any changes in this actor so we can have the update in editor.
#if WITH_EDITOR
void AMyTrain::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
{
FName PropertyName = (PropertyChangedEvent.Property != nullptr)
? PropertyChangedEvent.Property->GetFName() : NAME_None;
if (PropertyName == GET_MEMBER_NAME_CHECKED(AMyTrain, LeverAClass))
{
LeverA->SetChildActorClass(LeverAClass.Get());
}
if (PropertyName == GET_MEMBER_NAME_CHECKED(AMyTrain, LeverBClass))
{
LeverB->SetChildActorClass(LeverBClass.Get());
}
Super::PostEditChangeProperty(PropertyChangedEvent);
}
#endif
LeverBase.h
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FTriggerDelegate);
UCLASS(Abstract, Blueprintable)
class ALeverBase : public AActor
{
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Components")
TObjectPtr<class USphereComponent> CollisionComponent = nullptr;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Components")
TObjectPtr<UStaticMeshComponent> StaticMeshComponentA = nullptr;
// BlueprintCallable allows it to be called in BP.
UPROPERTY(BlueprintCallable, Category = "Test")
FTriggerDelegate Triggered;
}
LeverBase.cpp
ALeverBase::ALeverBase()
{
PrimaryActorTick.bCanEverTick = false;
CollisionComponent = CreateDefaultSubobject<USphereComponent>(TEXT("SphereComponent0"));
SetRootComponent(CollisionComponent);
CollisionComponent->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Block);
StaticMeshComponentA = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("StaticMeshComponent0"));
StaticMeshComponentA->SetupAttachment(RootComponent);
}
Now in the lever child bp you can set up what ever interface or condition to call the delegate (or do it in c++) and set components defaults. Heres with a bp interface i already had in a test level:
In my âMyTrainâ bp:
