こんにちは。トイロジックプログラマの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コンポーネントとの繋ぎ込み

作成したSplineMetadataSplineコンポーネントに持たせ、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クラスの作成

ISplineMetadataDetailsTSharedFromThisを継承したクラスを作成します。

インタフェースの仮想関数をオーバーライドし、選択中の頂点のインデックスを保持するメンバ変数を定義します。

エディタの詳細パネルにチェックボックスを表示してフラグの値を変更するため、チェックボックスに必要な関数とメンバ変数も定義します。


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との繋ぎ込み

これで詳細パネルに追加する要素の定義と処理の実装ができました。

最後に、作成したSplineMetadataDetailsSplineMetadataとの繋ぎ込みを行います。

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型の変数を追加してエディタ上から数値を入力して操作可能にする」というような事も可能なので、色々な活用方法を試してみてください。
 

著者紹介 I.A
プログラマーとして複数のゲームタイトルに携わった後、2024年にトイロジック入社。『FOAMSTARS』で一部キャラクターのスキルやシステムの実装を担当。

趣味は旅行で、最近は特に城郭巡りが楽しい模様。