Starting the Anim Toolkit off nice and simple with a ControlRig selector.
This is implemented as a Slate Editor Plugin for convenience.
I split my requirements into “Minimal” and “Production-Ready”. This allows me to know the minimum viable feature set for both my own testing and beta reviews as well as what is required for it to be ready for use in a production.
For now I’ll be implementing the minimum features for the prototype phase.
Listing the production requirements now is great because it gives either myself or future developers a quick reference point later down the line.
I don’t actually have a client here, so “Production-Ready Requirements” is myself emulating a client request list, if I were designing this for a client I would not add anything they did not request here without discussion!
Minimal Requirements:
- Display Available ControlRigs with a name based on what is in the World Outliner
- Selecting a ControlRig will activate it in the viewport
- DoubleClicking a ControlRig will focus the camera to the bound actor
Production-Ready Requirements
- Adding/Removing/Updating ControlRigs will reflect instantly in the ListView
- Visibility toggle icon mapped to show/hide the actor
- Change text colour based on visibility
- Storable Colour groups
- Filter field (For large scenes)
- Right click menu:
– Find Asset in Content Browser
– Open Blueprint
– Select all controls
Implementation:
In the future I’ll only share important snippets, but as this widget is so small and simple I’ve just dumped the whole thing here less includes and namespaces.
Fortunately between source code and some posts on the unreal forum I’ve put this together fairly effortlessly.
First thing I had to do is shift ControlRigEditMode.h to the Public folder so I could use it in my code.
Next for this to work I need two new utility methods to get and set ControlRigs
TArray<UControlRig*> GetAvailableControlRigs()
{
// Get loaded sequences (Will refine this later)
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(FName("AssetRegistry"));
IAssetRegistry& AssetRegistry = AssetRegistryModule.Get();
TArray<FAssetData> Sequences;
AssetRegistry.GetAssetsByClass(ULevelSequence::StaticClass()->GetFName(), Sequences);
TArray<UControlRig*> ControlRigs;
for (FAssetData SequenceAsset : Sequences) {
// Filter for level sequences
if ( ULevelSequence* LevelSequence = Cast<ULevelSequence>(SequenceAsset.GetAsset()) ) {
// Loop over tracks
for (const FMovieSceneBinding& Binding : LevelSequence->MovieScene->GetBindings() ) {
// Is this a control rig track?
UMovieSceneControlRigParameterTrack* Track = Cast<UMovieSceneControlRigParameterTrack>(
LevelSequence->MovieScene->FindTrack(UMovieSceneControlRigParameterTrack::StaticClass(),
Binding.GetObjectGuid(), NAME_None));
// Then add it to the list
if ( Track && Track->GetControlRig() )
ControlRigs.Add( Track->GetControlRig() );
}
}
}
return ControlRigs;
}
void SetActiveControlRig( UControlRig* ControlRig ) {
// failsafe
if ( !ControlRig ) return;
// Ensure we are in control rig edit mode
FControlRigEditMode* ControlRigEditMode = static_cast<FControlRigEditMode*>(GLevelEditorModeTools().GetActiveMode(FControlRigEditMode::ModeName));
if ( !ControlRigEditMode ) {
GLevelEditorModeTools().ActivateMode(FControlRigEditMode::ModeName);
ControlRigEditMode = static_cast<FControlRigEditMode*>(GLevelEditorModeTools().GetActiveMode(FControlRigEditMode::ModeName));
}
// setObjects to set single control rig... why not.
ControlRigEditMode->SetObjects( ControlRig, nullptr, nullptr );
}
Next is the ListView data, as the text needs to refer to assigned memory I have wrapped this in a mini data class. I will refactor this later once I have a more solid direction for my data flow.
class FControlRigData
{
public:
FControlRigData( UControlRig* InControlRig )
: ControlRigPtr( InControlRig ) {
AActor* Actor = InControlRig->GetObjectBinding()->GetHostingActor();
FString Name;
if ( Actor )
DisplayName = FText::FromString( Actor->GetName() );
else if ( InControlRig )
DisplayName = FText::FromString( InControlRig->GetName() );
else
DisplayName = LOCTEXT("Invalid", "Invalid");
}
UControlRig* GetControlRig() const {
if ( ControlRigPtr.IsValid() )
return ControlRigPtr.Get();
return nullptr;
}
const FText& GetDisplayName() { return DisplayName; }
private:
FText DisplayName;
TWeakObjectPtr<UControlRig> ControlRigPtr;
};
My List view is very simple at this stage, as everything is private we can switch the data around later on at will.
// Header Definition
/// Forward Decl
class FControlRigData;
/* SControlRigSelector
Simple Panel for listing available ControlRigs
*/
class SControlRigSelector : public SCompoundWidget
{
public:
SLATE_BEGIN_ARGS(SControlRigSelector) {}
SLATE_END_ARGS()
/// Construc the widget
/// @param Args: arguments
void Construct(const FArguments& Args);
/// Clear the view and add any available control rigs
void Refresh();
private:
/// Contruct a row for each control rig
/// @param ControlRig: ptr to control rig data
/// @param OwnerTable: data table
/// @return: row widget
TSharedRef<ITableRow> ConstructRow(TSharedPtr<FControlRigData> ControlRig, const TSharedRef<STableViewBase>& OwnerTable);
/// On Selection change callback, selects the control rig
/// @param ControlRig: Control rig to select
/// @param SelectInfo: UNUSED
void SelectionChanged(TSharedPtr<FControlRigData> ControlRig, ESelectInfo::Type SelectInfo);
/// On Double click, focus in on control rig
/// @param ControlRig: Rig to focus on
void DoubleClicked(TSharedPtr<FControlRigData> ControlRig);
/// Private Data
TArray<TSharedPtr<FControlRigData>> ControlRigs;
TSharedPtr < SListView<TSharedPtr<FControlRigData>> > ListView;
};
// Implementation
void SControlRigSelector::Construct(const FArguments& Args)
{
const FText NameText = LOCTEXT("Name", "Name");
ChildSlot
.VAlign(VAlign_Fill)
.HAlign(HAlign_Fill)
[
SNew(SScrollBox)
+ SScrollBox::Slot()
[
SAssignNew(ListView, SListView<TSharedPtr<FControlRigData>>)
.ItemHeight(24)
.ListItemsSource(&ControlRigs)
.OnGenerateRow(this, &SControlRigSelector::ConstructRow)
.OnSelectionChanged(this, &SControlRigSelector::SelectionChanged)
.OnMouseButtonDoubleClick(this, &SControlRigSelector::DoubleClicked)
.HeaderRow(
SNew(SHeaderRow)
+SHeaderRow::Column("ControlRig")
[
SNew(SBorder)
.Padding(5)
.Content()
[
SNew(STextBlock)
.Text(NameText)
]
]
)
]
];
Refresh();
}
void SControlRigSelector::SelectionChanged(TSharedPtr<FControlRigData> ControlRig, ESelectInfo::Type SelectInfo)
{
SetActiveControlRig(ControlRig->GetControlRig());
}
void SControlRigSelector::DoubleClicked(TSharedPtr<FControlRigData> ControlRig)
{
if ( ControlRig->GetControlRig() ) {
AActor* Actor = ControlRig->GetControlRig()->GetObjectBinding()->GetHostingActor();
if ( Actor ) {
TArray<AActor*> Actors;
Actors.Add( Actor );
TArray<UPrimitiveComponent*> SelectedComponents;
GEditor->MoveViewportCamerasToActor(Actors, SelectedComponents, true);
}
}
}
void SControlRigSelector::Refresh()
{
ControlRigs.Reset();
for( auto ControlRig : GetAvailableControlRigs() )
ControlRigs.Add( MakeShareable( new FControlRigData( ControlRig ) ) );
ListView->RequestListRefresh();
}
TSharedRef<ITableRow> SControlRigSelector::ConstructRow(TSharedPtr<FControlRigData> ControlRig, const TSharedRef<STableViewBase>& OwnerTable)
{
return
SNew(STableRow< TSharedRef<FText> >, OwnerTable)
[
SNew(STextBlock).Text(ControlRig->GetDisplayName())
];
}
Final Result:
Closing thoughts
Unreal is certainly a paradigm shift to what I’m used to, but fortunately Slate is similar enough to QtQuick that I can muddle through without much hassle. It certainly makes me realize how spoilt I’ve been with Qt’s great documentation.
The main issue I keep facing is object ownership and memory management; It’s not always clear when a widget takes a copy of a string and when it takes a reference until it crashes.
I’d also like to re-iterate that this series is not a tutorial, it’s just some insights into a personal project of mine. I’d recommend raising questions over on the unreal forum as I have very limited free time.
Next on my list is a ControlRig Property Editor which will likely be posted in the next two weeks.
Regards,
Mr MinimalEffort