UE5 Common UI お試し ③ デフォルトナビゲーション (Default Back Action)

上記の続きです。

前回デフォルトナビゲーションのうち、Default Click Action しか触れられなかったので Default Back Action を試します。

設定に関するコード

前回も貼りましたが再掲します。

/* Derive from this class to store the Input data. It is referenced in the Common Input Settings, found in the project settings UI. */
UCLASS(Abstract, Blueprintable, ClassGroup = Input, meta = (Category = "Common Input"))
class COMMONINPUT_API UCommonUIInputData : public UObject
{
    GENERATED_BODY()

public:
    virtual bool NeedsLoadForServer() const override;

public:
    UPROPERTY(EditDefaultsOnly, Category = "Properties", meta = (RowType = CommonInputActionDataBase))
    FDataTableRowHandle DefaultClickAction;

    UPROPERTY(EditDefaultsOnly, Category = "Properties", meta = (RowType = CommonInputActionDataBase))
    FDataTableRowHandle DefaultBackAction;
};

また、これらを取得するコードは UCommonInputSettings にあります。

UCLASS(config = Game, defaultconfig)
class COMMONINPUT_API UCommonInputSettings : public UDeveloperSettings
{
    FDataTableRowHandle GetDefaultClickAction() const;
    FDataTableRowHandle GetDefaultBackAction() const;

    // 全体的に省略

GetDefaultBackAction 利用箇所

void UCommonActivatableWidget::NativeConstruct()
{
    Super::NativeConstruct();

    if (bIsBackHandler)
    {
        // 💡 Default Back Action 時に HandleBackAction が呼び出される
        FBindUIActionArgs BindArgs(ICommonInputModule::GetSettings().GetDefaultBackAction(), FSimpleDelegate::CreateUObject(this, &UCommonActivatableWidget::HandleBackAction));
        BindArgs.bDisplayInActionBar = bIsBackActionDisplayedInActionBar;

        DefaultBackActionHandle = RegisterUIActionBinding(BindArgs);
    }

    if (bAutoActivate)
    {
        UE_LOG(LogCommonUI, Verbose, TEXT("[%s] auto-activated"), *GetName());
        ActivateWidget();
    }
}

上記の一箇所しかありません。

UCommonActivatableWidget は、最初の記事で WBP_Menu を作成する際に UserWidget の代わりに親に指定したものです。

NativeConstruct で呼び出されていますが、bIsBackHandler の場合のみのようです。

上記のプロパティになりますが、もちろん WBP_Menu にも存在しています。以下は定義です。

/** 
 * The base for widgets that are capable of being "activated" and "deactivated" during their lifetime without being otherwise modified or destroyed. 
 *
 * This is generally desired for one or more of the following purposes:
 * - This widget can turn on/off without being removed from the hierarchy (or otherwise reconstructing the underlying SWidgets), so Construct/Destruct are insufficient
 * - 💡 You'd like to be able to "go back" from this widget, whether that means back a breadcrumb, closing a modal, or something else. This is built-in here.
 * - This widget's place in the hierarchy is such that it defines a meaningful node-point in the tree of activatable widgets through which input is routed to all widgets.
 *
 * By default, an activatable widget:
 * - Is not automatically activated upon construction
 * - 💡 Does not register to receive back actions (or any other actions, for that matter)
 * - 💡 If classified as a back handler, is automatically deactivated (but not destroyed) when it receives a back action
 * 
 * Note that removing an activatable widget from the UI (i.e. triggering Destruct()) will always deactivate it, even if the UWidget is not destroyed.
 * Re-constructing the underlying SWidget will only result in re-activation if auto-activate is enabled.
 *
 * TODO: ADD MORE INFO ON INPUTS
 */
UCLASS(meta = (DisableNativeTick))
class COMMONUI_API UCommonActivatableWidget : public UCommonUserWidget
{
    // 略
    
    /** True to receive "Back" actions automatically. Custom back handler behavior can be provided, default is to deactivate. */
    UPROPERTY(EditAnywhere, Category = Back)
    bool bIsBackHandler = false;

    /** True to receive "Back" actions automatically. Custom back handler behavior can be provided, default is to deactivate. */
    UPROPERTY(EditAnywhere, Category = Back)
    bool bIsBackActionDisplayedInActionBar = false;

    /** Handle to default back action, if bound */
    FUIActionBindingHandle DefaultBackActionHandle;

上記でクラスのコメントに強調アイコンを付けましたが、UCommonActivatableWidget としても back に関しては主要な機能として説明されています。

戻ると、bIsBackHandler は Editor から設定可能な bool 値になっています。
bIsBackActionDisplayedInActionBar に関しては今回はスキップします。

DefaultBackActionHandle に関しては RegisterUIActionBinding の戻り値を受け取り、Deactivate のために利用しているだけです。実際のアクションの登録は RegisterUIActionBinding で行われています。

Back Action を受け取った時

すでに定義コードのコメントで書かれていますが、以下の実際の処理コードでも分かる通り DeactivateWidget が呼び出されています。(BP 側で OnHandleBackAction が設定されている場合は呼び出されないようです)

bool UCommonActivatableWidget::NativeOnHandleBackAction()
{
    //@todo DanH: This isn't actually fleshed out enough - we need to figure out whether we want to let back be a normal action or a special one routed through all activatables that lets them conditionally handle it
    if (bIsBackHandler)
    {
        if (!BP_OnHandleBackAction())
        {
            // Default behavior is unconditional deactivation
            UE_LOG(LogCommonUI, Verbose, TEXT("[%s] handled back with default implementation. Deactivating immediately."), *GetName());
            DeactivateWidget();
        }
        return true;
    }
    return false;
}

void UCommonActivatableWidget::HandleBackAction()
{
    NativeOnHandleBackAction();
}

DeactivateWidget

動作を確認するにあたり、CommonActivatableWidget の Deactivate がどのようなことをするのか見ておきます。

void UCommonActivatableWidget::DeactivateWidget()
{
    if (bIsActive)
    {
        InternalProcessDeactivation();
    }
}

void UCommonActivatableWidget::InternalProcessDeactivation()
{
    UE_LOG(LogCommonUI, Verbose, TEXT("[%s] -> Deactivated"), *GetName());

    bIsActive = false;
    NativeOnDeactivated();
}

void UCommonActivatableWidget::NativeOnDeactivated()
{
    if (ensure(!bIsActive))
    {
        if (bSetVisibilityOnDeactivated)
        {
            SetVisibility(DeactivatedVisibility);
            UE_LOG(LogCommonUI, Verbose, TEXT("[%s] set visibility to [%d] on deactivation"), *GetName(), *StaticEnum<ESlateVisibility>()->GetDisplayValueAsText(DeactivatedVisibility).ToString());
        }

        // Cancel any holds that were active
        ClearActiveHoldInputs();

        BP_OnDeactivated();
        OnDeactivated().Broadcast();
        BP_OnWidgetDeactivated.Broadcast();
    }
}

まずは bIsActivefalse にし、その後 bSetVisibilityOnDeactivatedtrue なら SetVisibility(DeactivatedVisibility) を行います。
ClearActiveHoldInputs に関しては HoldInputs をまだ確認していないので詳しくは見ませんが、それをクリアするということかと思います。

その後は BP 側のイベントを流したり、OnDeactivated を流したりといった感じです。

bSetVisibilityOnDeactivated / DeactivatedVisibility

// UCommonActivatableWidget.h

    UPROPERTY(EditAnywhere, Category = Activation, meta = (InlineEditConditionToggle = "DeactivatedVisibility"))
    bool bSetVisibilityOnDeactivated = false;

    UPROPERTY(EditAnywhere, Category = Activation, meta = (EditCondition = "bSetVisibilityOnDeactivated"))
    ESlateVisibility DeactivatedVisibility = ESlateVisibility::Collapsed;

こちらも設定項目ですね。IsBackHandler を確認する時にも同じ箇所のスクショを貼りましたが、今回は上記のプロパティの箇所に枠を付けています。

ひとまず Hidden することにして消してしまいます。

実行する

ひとまずこれまでに作った NavigationBack を指定します。

何も起きない?

消えませんでした。この Widget 内部で NavigationBack のイベントを受け取るボタンが存在しているのでそこでクリックが消費されると Back までいかないのかもしれません。

異なるボタンを割り当てる

上記のようにここまで利用していないボタンを割り当てました。

すると、Keyborad、Gamepad 両方のボタンで消えることが確認できました。

まとめ

Default Click Action はボタンのホバー時にアイコンを表示するだけだったのに対して Default Back Action はアイコン表示はなく、入力イベントを待ち受け Deacitivate を行うというものでした。

ゲームのユーザーとしては使うボタンは少ない方がいいので、こういう default で指定された入力をうまく使って UI も設計できればいいのだろうと思いました。