
こんにちは。トイロジックプログラマのI.Aです。本記事では、UE5のSplineコンポーネント
の拡張についてお話したいと思います。
アクションゲームでは、一定の経路に沿って移動を行うアクターが頻繁に登場します。この際、「この角に来たら少し立ち止まる挙動を入れたい」など、経路上の特定の地点で何かをさせたいという場面はよくあります。
該当地点に専用のアクターを置くなどの手法も考えられますが、今回はSplineコンポーネント
の頂点を利用する手法を紹介します。なお、今回使用するUEのバージョンは5.5.4です。
Splineコンポーネント
を使った移動については、過去のトイログ記事でも紹介されていますのでご参照ください。
24卒採用連動企画「C++でスプライン移動コンポーネントを作ってみた」 – トイロジック技術開発ブログ「トイログ」
目次
独自のフラグを追加する
マップに配置されたスプラインの頂点をクリックすると、詳細パネルにSelected Points
という項目が表示されます。このように、スプラインは各頂点ごとにインデックス、位置、曲線の接線方向といった情報を持っています。
ここに独自のフラグを1つ追加してみようと思います。
UE5には、スプラインに追加の情報を持たせるSplineMetadata
という機能が用意されており、こちらを使用して実装します。
SplineMetadataクラスの作成
まず、USplineMetadata
を継承したクラスを作成します。
継承元の仮想関数をオーバーライドし、追加するフラグをメンバ変数に定義します。
型がTArrayになっているのは、スプラインの頂点の数だけフラグが必要となるためです。
UCLASS()
class UMySplineMetadata : public USplineMetadata
{
GENERATED_BODY()
public:
UMySplineMetadata();
virtual void InsertPoint(int32 Index, float t, bool bClosedLoop) override;
virtual void UpdatePoint(int32 Index, float t, bool bClosedLoop) override;
virtual void AddPoint(float InputKey) override;
virtual void RemovePoint(int32 Index) override;
virtual void DuplicatePoint(int32 Index) override;
virtual void CopyPoint(const USplineMetadata* FromSplineMetadata, int32 FromIndex, int32 ToIndex) override;
virtual void Reset(int32 NumPoints) override;
virtual void Fixup(int32 NumPoints, USplineComponent* SplineComp) override;
// 追加で持たせるフラグ
UPROPERTY(EditInstanceOnly)
TArray<bool> StopPointFlags;
};
関数の処理実装
次に、オーバーライドした関数の処理を実装します。
各関数は、Splineコンポーネント
に対して特定の頂点操作を行った際に呼び出されるものです。
それぞれの操作に合わせて配列の要素数や値が正しく設定されるようにします。
Modify()関数
は、そのオブジェクトに変更が加わったことをエディタ側に知らせるために呼び出します。
// 挿入
void UMySplineMetadata::InsertPoint(int32 Index, float t, bool bClosedLoop)
{
if (Index >= StopPointFlags.Num())
{
AddPoint(static_cast<float>(Index));
}
else
{
StopPointFlags.Insert(false, Index);
}
Modify();
}
// 更新
void UMySplineMetadata::UpdatePoint(int32 Index, float t, bool bClosedLoop)
{
Modify();
}
// 末尾への追加
void UMySplineMetadata::AddPoint(float InputKey)
{
StopPointFlags.Emplace(false);
Modify();
}
// 削除
void UMySplineMetadata::RemovePoint(int32 Index)
{
StopPointFlags.RemoveAt(Index);
Modify();
}
// 末尾への複製
void UMySplineMetadata::DuplicatePoint(int32 Index)
{
StopPointFlags.Emplace(StopPointFlags[Index]);
Modify();
}
// 他のスプラインからのコピー
void UMySplineMetadata::CopyPoint(const USplineMetadata* FromSplineMetadata, int32 FromIndex, int32 ToIndex)
{
if(const UMySplineMetadata* FromMetadata = Cast<UMySplineMetadata>(FromSplineMetadata))
{
StopPointFlags[ToIndex] = FromMetadata->StopPointFlags[FromIndex];
Modify();
}
}
// リセット
void UMySplineMetadata::Reset(int32 NumPoints)
{
StopPointFlags.Reset(NumPoints);
Modify();
}
// 配列個数の整合性確保
void UMySplineMetadata::Fixup(int32 NumPoints, USplineComponent* SplineComp)
{
if (StopPointFlags.Num() > NumPoints)
{
StopPointFlags.RemoveAt(NumPoints, StopPointFlags.Num() - NumPoints);
Modify();
}
while(StopPointFlags.Num() < NumPoints)
{
StopPointFlags.Emplace(false);
Modify();
}
}
Splineコンポーネントとの繋ぎ込み
作成したSplineMetadata
はSplineコンポーネント
に持たせ、GetSplinePointsMetadata()関数
でそのSplineMetadata
を返すようにオーバーライドします。
これで、スプラインの頂点操作時にSplineMetadata
側の各関数が呼び出されるようになります。
UCLASS()
class UMySplineComponent : public USplineComponent
{
GENERATED_BODY()
public:
UMySplineComponent();
// 作成したSplineMetadataを返すようにオーバーライド
virtual USplineMetadata* GetSplinePointsMetadata() override { return SplineMetadata; }
virtual const USplineMetadata* GetSplinePointsMetadata() const override { return SplineMetadata; }
protected:
UPROPERTY()
TObjectPtr<UMySplineMetadata> SplineMetadata = nullptr;
};
// コンストラクタでSplineMetadataのインスタンスを作成するようにします
UMySplineComponent::UMySplineComponent()
{
SplineMetadata = CreateDefaultSubobject<UMySplineMetadata>(TEXT("SplineMetadata"));
}
追加したフラグをエディタ上で変更可能にする
SplineMetadata
を使用して独自のフラグを持たせましたが、このままではエディタ上から値を変更することができません。
そのためには、SplineMetadata
用に詳細パネルのカスタマイズを追加する必要があります。
こちらは、UE5に用意されているSplineMetadataDetails
を使って作成できます。
SplineMetadataDetailsクラスの作成
ISplineMetadataDetails
とTSharedFromThis
を継承したクラスを作成します。
インタフェースの仮想関数をオーバーライドし、選択中の頂点のインデックスを保持するメンバ変数を定義します。
エディタの詳細パネルにチェックボックスを表示してフラグの値を変更するため、チェックボックスに必要な関数とメンバ変数も定義します。
class FMySplineMetadataDetails : public ISplineMetadataDetails, public TSharedFromThis<FMySplineMetadataDetails>
{
public:
virtual ~FMySplineMetadataDetails() {}
virtual FName GetName() const override { return TEXT("MySplineMetadata"); }
virtual FText GetDisplayName() const override { return FText::FromString(TEXT("AdditionalData")); }
virtual void Update(USplineComponent* InSplineComponent, const TSet<int32>& InSelectedKeys) override;
virtual void GenerateChildContent(IDetailGroup& InGroup) override;
protected:
// フラグの値からチェックボックスの状態を返す関数
ECheckBoxState GetStopPointFlag() const;
// チェックボックスが操作された時に呼ばれる関数
void OnStopPointFlagChanged(ECheckBoxState NewState);
TObjectPtr<UMySplineComponent> SplineComp = nullptr;
// 選択中の頂点のインデックスを保持するための変数
TSet<int32> SelectedKeys;
// エディタに表示中のフラグの値を保持するための変数
TOptional<bool> StopPointFlag = false;
};
表示中のフラグの値保持にTOptional<bool>
という型を使用していますが、これはtrue/false以外に「未セット」という状態を持つことができます。
後述のチェックボックスの状態に合わせるためには、こちらの方が便利です。
Update関数の処理実装
Update()関数
では、エディタ上で選択中の頂点のインデックスを受け取り、SplineMetadata
側の該当するインデックスのフラグの値に応じて、Details
側の保持するフラグの値を変更します。
ここでのポイントは、頂点はエディタ上で複数選択が可能なため、選択された頂点同士でフラグの値が同じにならない場合があるという事です。
そのような場合は、チェックボックスを「Undetermined(未定)」の状態にするため、Details側のフラグを「未セット」の状態にします。
void FMySplineMetadataDetails::Update(USplineComponent* InSplineComponent, const TSet<int32>& InSelectedKeys)
{
SplineComp = Cast<UMySplineComponent>(InSplineComponent);
SelectedKeys = InSelectedKeys;
StopPointFlag.Reset();
if (SplineComp)
{
if (UMySplineMetadata* Metadata = Cast<UMySplineMetadata>(SplineComp->GetSplinePointsMetadata()))
{
// 選択されている全ての頂点のMetadata側のフラグをチェックする
for (int32 Index : SelectedKeys)
{
if (Metadata->StopPointFlags.IsValidIndex(Index))
{
if (!StopPointFlag.IsSet())
{
StopPointFlag = Metadata->StopPointFlags[Index];
}
else if (StopPointFlag.GetValue() != Metadata->StopPointFlags[Index])
{
// 選択された複数の頂点で異なる値がセットされていた場合、フラグの状態を未セットにする
StopPointFlag.Reset();
break;
}
}
else
{
// Metadata側とSplineComponent側でフラグ数の不整合があった場合に修正する関数呼び出す
Metadata->Fixup(SplineComp->GetNumberOfSplinePoints(), SplineComp);
StopPointFlag = Metadata->StopPointFlags[Index];
}
}
}
}
}
エディタ側の詳細パネルの要素を追加
GenerateChildContent()関数
では、エディタ側の詳細パネルの要素を追加します。
詳細パネルの要素は、Slateの構文で記載する必要があります。
項目名部分としてSTextBlock
を、チェックボックス部分としてSCheckBox
を追加し、SCheckBox
には2種類のコールバック関数を登録します。
void FMySplineMetadataDetails::GenerateChildContent(IDetailGroup& InGroup)
{
// パネルの項目名部分
InGroup.AddWidgetRow()
.Visibility(EVisibility::Visible)
.NameContent()
.HAlign(HAlign_Left)
.VAlign(VAlign_Center)
[
SNew(STextBlock)
.Text(FText::FromString(TEXT("IsStopPoint")))
.Font(IDetailLayoutBuilder::GetDetailFont())
]
// パネルのチェックボックス部分
.ValueContent()
[
SNew(SCheckBox)
.IsChecked(this, &FMySplineMetadataDetails::GetStopPointFlag) // フラグの値に応じたチェックボックスの状態を返す関数を登録
.OnCheckStateChanged(this, &FMySplineMetadataDetails::OnStopPointFlagChanged) // チェックボックスが操作された時に呼ばれる関数を登録
];
}
IsChecked
に登録したコールバック関数では、Details
側が持つフラグの値に応じたチェックボックスの状態を返すようにします。
OnCheckStateChanged
に登録したコールバック関数では、操作後のチェックボックスの状態に応じてSplineMetadata
側のフラグの値をセットするようにします。
// フラグの値に応じたチェックボックスの状態を返す
ECheckBoxState FMySplineMetadataDetails::GetStopPointFlag() const
{
// フラグの値が
// true → ECheckBoxState::Checked
// false → ECheckBoxState::Unchecked
// 未セット → ECheckBoxState::Undetermined
if (StopPointFlag.IsSet())
{
return StopPointFlag.GetValue() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked;
}
else
{
return ECheckBoxState::Undetermined;
}
}
// チェックボックスが操作された時に呼ばれる
void FMySplineMetadataDetails::OnStopPointFlagChanged(ECheckBoxState NewState)
{
// 操作後のチェックボックスの状態に応じてSplineMetadata側のフラグの値を変更する
if (UMySplineMetadata* Metadata = Cast<UMySplineMetadata>(SplineComp->GetSplinePointsMetadata()))
{
for (int32 Index : SelectedKeys)
{
if (Metadata->StopPointFlags.IsValidIndex(Index))
{
// チェックボックスがCheckedになった時はboolの値はtrue、それ以外はfalse
Metadata->StopPointFlags[Index] = NewState == ECheckBoxState::Checked;
}
Metadata->Modify();
}
}
}
作成したSplineMetadataDetailsとSplineMetadataとの繋ぎ込み
これで詳細パネルに追加する要素の定義と処理の実装ができました。
最後に、作成したSplineMetadataDetails
とSplineMetadata
との繋ぎ込みを行います。
USplineMetadataDetailsFactoryBase
を継承したクラスを作成し、Create()関数
でSplineMetadataDetailsクラス
のシェアードポインタを、GetMetadataClass()関数
でSplineMetadataクラス
のインスタンスを返すようにオーバーライドします。
class UMySplineMetadataDetailsFactory : public USplineMetadataDetailsFactoryBase
{
GENERATED_UCLASS_BODY()
public:
virtual TSharedPtr<ISplineMetadataDetails> Create() override { return MakeShared<FMySplineMetadataDetails>(); }
virtual UClass* GetMetadataClass() const override { return UMySplineMetadata::StaticClass(); }
};
完成!
以上で実装は完了です。
拡張したスプラインをエディタ上に配置して頂点を選択すると、詳細パネルに追加したフラグを変更するチェックボックスの項目が追加されており、値を操作可能になっています。
ちなみに、チェックボックスの項目名はSTextBlock
に設定したTextの文字列(ここではIsStopPoint)になり、その親のセクション名はSplineMetadataDetailsクラス
のGetDisplayName()関数
で返される文字列(ここではAdditionalData)になります。
追加したフラグの値を取得する
Splineコンポーネントに持たせたSplineMetadata
を参照することで、追加したフラグの値を取得することが可能です。
フラグの配列のインデックス指定には、頂点のInputKey
の値を使用する形になります。
// Actorから一番近い頂点のIsStopPointの値を取得する例
float NearestInputKey = TargetSpline->FindInputKeyClosestToWorldLocation(GetActorLocation());
int32 TargetPointIndex = static_cast<int32>(NearestInputKey);
if(UMySplineMetadata* SplineMetadata = Cast<UMySplineMetadata>(TargetSpline->GetSplinePointsMetadata()))
{
bool bIsStopPoint = SplineMetadata->StopPointFlags.IsValidIndex(TargetPointIndex) ? SplineMetadata->StopPointFlags[TargetPointIndex] : false;
}
最後に
以上、コード部分が長くなってしまいましたが、スプライン曲線の頂点に独自のフラグを追加する方法についてお話しました。
今回はbool型の変数を追加してチェックボックスで操作するというやり方でしたが、SplineMetadata
にはbool型以外の変数を持たせることもでき、詳細パネルにはチェックボックス以外の要素を追加することもできます。
「float型の変数を追加してエディタ上から数値を入力して操作可能にする」というような事も可能なので、色々な活用方法を試してみてください。