UE5でUI操作をゲームパッド・マウス両対応にする完全ガイド

UE5でUI操作をゲームパッド・マウス両対応にする完全ガイド

ゲーム開発において、UIの操作対応は極めて重要です。特にコンソールゲームやマルチプラットフォーム対応を目指す場合、ゲームパッド操作は必須要件です。しかし、PCでのマウス操作も無視できません。本記事では、UE5(Unreal Engine 5)でUIをゲームパッド・マウスの両方に対応させる方法を、ブループリント・C++の両面から徹底解説します。

目次

第1章:基礎知識とアーキテクチャ

UE5のUIシステム概要

UE5のUIシステムは、以下の3つの主要コンポーネントで構成されています。

【UMGシステム概要図】

User Interface
├─ UMG(Unreal Motion Graphics)
│  ├─ Widget Blueprint
│  ├─ Canvas Panel
│  └─ Input System
├─ Slate(低レベルUI)
│  └─ C++による直接操作
└─ Enhanced Input System(推奨、5.0+)
   ├─ Input Mapping Context
   ├─ Input Action
   └─ Input Modifiers/Processors

UE5 5.1以降での推奨アプローチ

UE5.1より前は、従来のSetupPlayerInputComponent()を使用していました。しかし、UE5.1以降は「Enhanced Input System」(EIS)の使用が推奨されています。

機能従来型Enhanced Input System
柔軟性
デバッグ性困難容易
マップ管理手動自動
モバイル対応限定的優秀
公式推奨度✗ 非推奨✓ 推奨

本記事では、Enhanced Input Systemを中心に解説します。

入力の優先度(Priority)

UE5では、以下の優先度順で入力が処理されます。

【入力処理の優先度】

優先度1:Modal Widget(ポップアップなど)
    ↓
優先度2:Focused Widget(フォーカス中のボタンなど)
    ↓
優先度3:Game Viewport(ゲーム画面)
    ↓
優先度4:Other Widgets(その他ウィジェット)

※モーダルウィジェットが開いていると、
 それより下の優先度の入力はブロックされる

第2章:基本設定とプロジェクト構成

ステップ1:プロジェクト設定

1-1. Enhanced Input Systemの有効化

【手順】

1. Edit → Project Settings → Search "Enhanced"
2. 「Enhanced Input」プラグインが有効になっているか確認
   ✓ Enabled であることを確認
   
3. 再度 Project Settings を開く
   Edit → Project Settings → Input

【確認項目】

□ Default Player Input Class:
   DefaultPlayerInput → EnhancedPlayerInput に変更

□ Default Input Component Class:
   DefaultInputComponent → EnhancedInputComponent に変更

□ 設定保存:Ctrl + S

1-2. フォルダ構成の推奨レイアウト

【プロジェクト構成】

Content/
├─ Input/
│  ├─ IA_Navigate.uasset(方向キー入力)
│  ├─ IA_Click.uasset(マウスクリック)
│  ├─ IA_Submit.uasset(決定ボタン)
│  ├─ IA_Cancel.uasset(キャンセルボタン)
│  └─ IMC_UIDefault.uasset(UI用マッピング)
│
├─ UI/
│  ├─ Widgets/
│  │  ├─ WBP_MainMenu.uasset
│  │  ├─ WBP_PauseMenu.uasset
│  │  └─ WBP_InGameUI.uasset
│  │
│  └─ Styles/
│     ├─ BP_UIFocusStyle.uasset
│     └─ DT_ButtonSettings.uasset(データテーブル)
│
└─ Characters/
   └─ BP_PlayerCharacter.uasset

第3章:Enhanced Input Systemの詳細設定

ステップ2:Input Actionの作成

Input Actionは、「プレイヤーの意図」を表現するアセットです。キーボードのどのキーが押されたかではなく、「ナビゲート」「クリック」などの意図を定義します。

3-1. 基本的なInput Actionの作成例

【Create Blank Input Action】

【IA_Navigate】
├─ Value Type:Axis2D(2軸、方向キー用)
├─ Mapping:
│  ├─ Keyboard:Arrow Keys
│  ├─ Keyboard:WASD
│  ├─ Gamepad:Left Stick
│  └─ Gamepad:D-Pad
└─ Modifiers:
   └─ Dead Zone(デッドゾーン設定:0.2)

【IA_Click】
├─ Value Type:Digital(1ボタン、オン/オフ)
├─ Mapping:
│  ├─ Mouse:Left Mouse Button
│  └─ Gamepad:Gamepad Face Button Bottom(A ボタン)
└─ Modifiers:なし

【IA_Submit】
├─ Value Type:Digital
├─ Mapping:
│  ├─ Keyboard:Enter
│  ├─ Gamepad:Gamepad Face Button Bottom
│  └─ Gamepad:Gamepad Face Button Right(X ボタン)
└─ Modifiers:なし

【IA_Cancel】
├─ Value Type:Digital
├─ Mapping:
│  ├─ Keyboard:Escape
│  ├─ Gamepad:Gamepad Face Button Right(B ボタン)
│  └─ Gamepad:Gamepad Special Right
└─ Modifiers:なし

3-2. ゲームパッド各ボタンの対応

UE5では、ゲームパッドのボタンが標準的に割り当てられています。

【ゲームパッド標準ボタン配置】

Face Buttons:
├─ Face Button Bottom = A ボタン(Xbox)/ ✕ ボタン(PlayStation)
├─ Face Button Right = B ボタン(Xbox)/ ◯ ボタン(PlayStation)
├─ Face Button Left = X ボタン(Xbox)/ □ ボタン(PlayStation)
└─ Face Button Top = Y ボタン(Xbox)/ △ ボタン(PlayStation)

Shoulder Buttons:
├─ Left Shoulder = LB/L1
├─ Right Shoulder = RB/R1
├─ Left Trigger = LT/L2
└─ Right Trigger = RT/R2

Stick Input:
├─ Left Stick = 左スティック(アナログ)
├─ Left Stick Click = 左スティック押込
├─ Right Stick = 右スティック(アナログ)
└─ Right Stick Click = 右スティック押込

Special Buttons:
├─ Special Left = Menu button(Options/Menu)
├─ Special Right = View button(Back/Select)
└─ Gamepad Center Button = スタートボタン

D-Pad:
├─ D-Pad Up
├─ D-Pad Down
├─ D-Pad Left
└─ D-Pad Right

ステップ3:Input Mapping Contextの作成

Input Mapping Context(IMC)は、Input Actionをどのキー/ボタンに割り当てるかを定義します。

【IMC_UIDefault の設定手順】

1. Create Mapping Context
   Name:IMC_UIDefault
   
2. Mapping を追加
   ├─ IA_Navigate(方向移動用)
   │  ├─ Mapping 1: Keyboard Arrow Keys
   │  ├─ Mapping 2: Keyboard WASD
   │  ├─ Mapping 3: Gamepad Left Stick
   │  └─ Mapping 4: Gamepad D-Pad
   │
   ├─ IA_Click(クリック用)
   │  ├─ Mapping 1: Mouse Left Button
   │  └─ Mapping 2: Gamepad Face Button Bottom
   │
   ├─ IA_Submit(決定用)
   │  ├─ Mapping 1: Keyboard Enter
   │  └─ Mapping 2: Gamepad Face Button Bottom
   │
   └─ IA_Cancel(キャンセル用)
      ├─ Mapping 1: Keyboard Escape
      └─ Mapping 2: Gamepad Face Button Right

3. Modifiers と Processors(フィルタリング)

ステップ4:Modifiers と Processors

Modifiersは入力値を変換し、Processorsは入力フローを制御します。

4-1. よく使用するModifiers

【推奨 Modifiers リスト】

1. Dead Zone Modifier(デッドゾーン)
   用途:アナログスティック入力時にジッター・ノイズ除去
   設定:
   ├─ Lower Threshold:0.1(値がこれ以下なら入力と見なさない)
   ├─ Upper Threshold:0.9(飽和ポイント)
   └─ Type:Radial(XY平面上での距離)

2. Scalar Modifier(スケーリング)
   用途:入力感度の調整
   設定:
   ├─ Scalar:1.5(150%の感度)
   └─ 用例:マウス感度が低すぎる場合に使用

3. Negate Modifier(反転)
   用途:入力方向の反転
   設定:なし
   用例:Y軸反転(Invert Mouse Y)

4. Swizzle Axis Modifier(軸の交換)
   用途:XYZの軸を入れ替える
   設定:
   ├─ Order:YXZ(例:YとXを交換)
   用例:スティック入力で上下・左右を反転

5. Smooth Value Modifier(スムージング)
   用途:アナログ入力の滑らかさ向上
   設定:
   ├─ Rising Speed:2.0(立ち上がり速度)
   ├─ Falling Speed:2.0(立ち下がり速度)
   └─ 用例:キャラクター移動時にぎこちなさを軽減

4-2. Processors

【推奨 Processors リスト】

1. Input Value Debug Processor
   用途:入力値の確認(デバッグ用)
   
2. Input Debounce Processor
   用途:入力のチャタリング除去
   設定:
   └─ Duration:0.1秒(この時間内の重複入力を無視)

第4章:ブループリントでのUI操作実装

ステップ5:プレイヤーコントローラーの設定

【BP_PlayerController の設定】

1. Event BeginPlay で IMC を追加
   ├─ Add Input Mapping Context
   │  ├─ Input Mapping Context:IMC_UIDefault
   │  └─ Priority:0
   │
   └─ Enable Input(HUD/UI用)
      └─ Pawn:Self

2. Input Setup Complete
   ├─ 入力システムの初期化完了を通知
   └─ UI 初期化と同期

ステップ6:UI Widget への入力バインディング

UMG Widgetでの入力受け取りは、「Focus」の概念が重要です。

6-1. Focus システムの理解

【フォーカスシステム】

Focused Widget(フォーカス中):
├─ キーボード・ゲームパッド入力を受け取る
├─ ビジュアル的に「選択状態」を表示(ハイライト等)
└─ 通常1つのウィジェットのみフォーカス可

Focus Navigation:
├─ 方向キー/スティック でフォーカス移動
├─ Tab キー(次へ)/ Shift+Tab(前へ)
└─ マウスでクリック = フォーカス移動

6-2. ボタンウィジェットへの入力実装

以下、シンプルなポーズメニューの例です。

【WBP_PauseMenu(ブループリント)の実装】

【Event Construct】
├─ Create Widget:WBP_PauseMenuContent
├─ Add to Viewport
├─ Set Focus
│  └─ Target Widget:Button_Resume(再開ボタンにフォーカス)
└─ Set Input Mode
   └─ Input Mode:Game and UI
      (ゲーム入力とUI入力の両立)

【ボタンの On Clicked イベント】
Button_Resume:
├─ Remove from Parent(メニューを閉じる)
└─ Set Input Mode:Game Only
   (UI入力を無効化、ゲーム入力のみ有効)

Button_Settings:
├─ Create Widget:WBP_SettingsMenu
├─ Add to Viewport
└─ Set Focus:Button_OK

Button_Exit:
├─ Open Level:MainMenu
└─ Flush Level Streaming

【マウス入力対応】
- Button On Hovered イベント:
  └─ Set Focus(マウス移動でボタンハイライト)
  
- Button On Unhovered イベント:
  └─ Clear Focus(マウスが離れたらハイライト解除)

6-3. カスタムボタンの作成(ゲームパッド対応)

デフォルトのButtonウィジェットは基本的ですが、ゲームパッド対応を強化するためにカスタマイズできます。

【WBP_CustomButton(カスタムボタン)】

【構成要素】

Visual Layer:
├─ Image(Background)
│  ├─ Normal State:Color = Gray
│  ├─ Hovered State:Color = White
│  └─ Pressed State:Color = Yellow
│
├─ Text(Label)
│  └─ 表示テキスト
│
└─ Image(Border)
   └─ フォーカス時の枠線(ゲームパッド用)

Variables:
├─ bIsFocused:是否フォーカス状態
├─ bIsPressed:是否押下状態
└─ OnButtonClicked Delegate

【フォーカス対応の実装】

On Focus Received:
├─ Set Brush:Border → Visibility = Visible
├─ Play Sound:SelectSound.uasset
└─ bIsFocused = true

On Focus Lost:
├─ Set Brush:Border → Visibility = Hidden
└─ bIsFocused = false

On Mouse Enter:
├─ Set Focus
└─ On Focus Received を実行

On Mouse Leave:
├─ Clear Focus
└─ On Focus Lost を実行

ステップ7:ゲームパッド入力の直接処理

より高度なUI制御が必要な場合、Input Actionを直接受け取ることができます。

【WBP_AdvancedMenu での直接入力処理】

【プレイヤーコントローラーで実装】

Setup Input Component:
├─ Bind Action:IA_Navigate
│  ├─ Event:Triggered
│  ├─ Callback:On Navigate Input
│  │  ├─ 入力値(Axis2D)を取得
│  │  ├─ if(Y > 0.5)→ Move Up
│  │  ├─ if(Y < -0.5)→ Move Down
│  │  ├─ if(X > 0.5)→ Move Right
│  │  └─ if(X < -0.5)→ Move Left
│  │
│  ├─ Event:Completed
│  └─ Callback:On Navigate Input Complete
│     └─ フォーカス移動完了処理
│
├─ Bind Action:IA_Submit
│  ├─ Event:Triggered
│  └─ Callback:On Submit
│     ├─ Focused Widget の On Clicked を実行
│     └─ または委譲を呼び出し
│
└─ Bind Action:IA_Cancel
   ├─ Event:Triggered
   └─ Callback:On Cancel
      ├─ メニューを閉じる
      └─ 親メニューに戻る

第5章:C++での詳細実装

ステップ8:PlayerController C++ コード

8-1. 基本的なセットアップ

cpp

// MyPlayerController.h
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "InputActionValue.h"
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"
#include "MyPlayerController.generated.h"

class UEnhancedInputComponent;
class UEnhancedInputLocalPlayerSubsystem;
class UInputMappingContext;
class UInputAction;

UCLASS()
class YOURPROJECT_API AMyPlayerController : public APlayerController
{
    GENERATED_BODY()

public:
    AMyPlayerController();

protected:
    // Enhanced Input System
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
    class UInputMappingContext* UIInputMappingContext;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
    class UInputAction* NavigateAction;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
    class UInputAction* ClickAction;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
    class UInputAction* SubmitAction;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
    class UInputAction* CancelAction;

    virtual void BeginPlay() override;
    virtual void SetupInput(class UEnhancedInputComponent* EnhancedInputComponent);

    // Input callbacks
    void OnNavigateInput(const FInputActionValue& Value);
    void OnClickInput(const FInputActionValue& Value);
    void OnSubmitInput(const FInputActionValue& Value);
    void OnCancelInput(const FInputActionValue& Value);

public:
    virtual void SetupPlayerInputComponent(
        class UInputComponent* PlayerInputComponent) override;
};

cpp

// MyPlayerController.cpp
#include "MyPlayerController.h"
#include "InputActionValue.h"
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"
#include "InputActionValue.h"

AMyPlayerController::AMyPlayerController()
{
    bShowMouseCursor = true;
    DefaultMouseCursor = EMouseCursor::Default;
    bEnableClickEvents = true;
    bEnableTouchEvents = true;
}

void AMyPlayerController::BeginPlay()
{
    Super::BeginPlay();

    // Enhanced Input System のサブシステムを取得
    if (UEnhancedInputLocalPlayerSubsystem* Subsystem =
            GetLocalPlayer()->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>())
    {
        // IMC を追加
        Subsystem->AddMappingContext(UIInputMappingContext, 0);
    }

    // UI入力モード設定
    FInputModeGameAndUI InputMode;
    InputMode.SetWidgetToFocus(nullptr);
    SetInputMode(InputMode);
}

void AMyPlayerController::SetupPlayerInputComponent(
    UInputComponent* PlayerInputComponent)
{
    Super::SetupPlayerInputComponent(PlayerInputComponent);

    if (UEnhancedInputComponent* EnhancedInputComponent =
            CastChecked<UEnhancedInputComponent>(PlayerInputComponent))
    {
        // Navigate Action バインド
        EnhancedInputComponent->BindAction(
            NavigateAction,
            ETriggerEvent::Triggered,
            this,
            &AMyPlayerController::OnNavigateInput);

        // Click Action バインド
        EnhancedInputComponent->BindAction(
            ClickAction,
            ETriggerEvent::Triggered,
            this,
            &AMyPlayerController::OnClickInput);

        // Submit Action バインド
        EnhancedInputComponent->BindAction(
            SubmitAction,
            ETriggerEvent::Triggered,
            this,
            &AMyPlayerController::OnSubmitInput);

        // Cancel Action バインド
        EnhancedInputComponent->BindAction(
            CancelAction,
            ETriggerEvent::Triggered,
            this,
            &AMyPlayerController::OnCancelInput);
    }
}

void AMyPlayerController::OnNavigateInput(const FInputActionValue& Value)
{
    // Axis2D値を取得(-1.0 ~ 1.0)
    FVector2D NavValue = Value.Get<FVector2D>();

    // フォーカス移動処理
    if (NavValue.Y > 0.5f)
    {
        // 上キー / スティック上
        OnNavigateUp();
    }
    else if (NavValue.Y < -0.5f)
    {
        // 下キー / スティック下
        OnNavigateDown();
    }

    if (NavValue.X > 0.5f)
    {
        // 右キー / スティック右
        OnNavigateRight();
    }
    else if (NavValue.X < -0.5f)
    {
        // 左キー / スティック左
        OnNavigateLeft();
    }
}

void AMyPlayerController::OnClickInput(const FInputActionValue& Value)
{
    // マウスクリック または ゲームパッドAボタン
    if (Value.Get<bool>())
    {
        // クリック処理
        OnUIClicked();
    }
}

void AMyPlayerController::OnSubmitInput(const FInputActionValue& Value)
{
    if (Value.Get<bool>())
    {
        // 決定ボタン処理(Enterキー またはゲームパッドボタン)
        OnSubmit();
    }
}

void AMyPlayerController::OnCancelInput(const FInputActionValue& Value)
{
    if (Value.Get<bool>())
    {
        // キャンセル処理(Escapeキー またはゲームパッドキャンセル)
        OnCancel();
    }
}

8-2. UI Widget の C++ コード

cpp

// MyUIWidget.h
#pragma once

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "Blueprint/WidgetNavigation.h"
#include "MyUIWidget.generated.h"

class UButton;
class UTextBlock;

DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnButtonClicked);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnNavigationEvent, 
    int32, FromIndex, int32, ToIndex);

UCLASS()
class YOURPROJECT_API UMyUIWidget : public UUserWidget
{
    GENERATED_BODY()

protected:
    UPROPERTY(meta = (BindWidget))
    class UButton* Button_Resume;

    UPROPERTY(meta = (BindWidget))
    class UButton* Button_Settings;

    UPROPERTY(meta = (BindWidget))
    class UButton* Button_Exit;

    UPROPERTY(BlueprintReadWrite, Category = "UI")
    int32 CurrentFocusIndex;

    UPROPERTY(BlueprintReadWrite, Category = "UI")
    TArray<UButton*> ButtonArray;

    virtual void NativeConstruct() override;
    virtual void NativeDestruct() override;

    // ボタンイベント
    UFUNCTION()
    void OnResumeClicked();

    UFUNCTION()
    void OnSettingsClicked();

    UFUNCTION()
    void OnExitClicked();

    // フォーカス管理
    UFUNCTION(BlueprintCallable, Category = "UI")
    void MoveNavigationUp();

    UFUNCTION(BlueprintCallable, Category = "UI")
    void MoveNavigationDown();

    UFUNCTION(BlueprintCallable, Category = "UI")
    void MoveNavigationLeft();

    UFUNCTION(BlueprintCallable, Category = "UI")
    void MoveNavigationRight();

    UFUNCTION(BlueprintCallable, Category = "UI")
    void SetFocusToButton(int32 ButtonIndex);

    UFUNCTION(BlueprintCallable, Category = "UI")
    void ActivateCurrentButton();

public:
    UPROPERTY(BlueprintAssignable, Category = "UI")
    FOnNavigationEvent OnNavigationEvent;
};

cpp

// MyUIWidget.cpp
#include "MyUIWidget.h"
#include "Components/Button.h"
#include "Components/TextBlock.h"
#include "Kismet/GameplayStatics.h"

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

    // ボタンイベントをバインド
    if (Button_Resume)
    {
        Button_Resume->OnClicked.AddDynamic(this, &UMyUIWidget::OnResumeClicked);
    }
    if (Button_Settings)
    {
        Button_Settings->OnClicked.AddDynamic(this, &UMyUIWidget::OnSettingsClicked);
    }
    if (Button_Exit)
    {
        Button_Exit->OnClicked.AddDynamic(this, &UMyUIWidget::OnExitClicked);
    }

    // ボタン配列に格納
    ButtonArray.Empty();
    ButtonArray.Add(Button_Resume);
    ButtonArray.Add(Button_Settings);
    ButtonArray.Add(Button_Exit);

    // 初期フォーカスを設定
    CurrentFocusIndex = 0;
    SetFocusToButton(0);
}

void UMyUIWidget::NativeDestruct()
{
    Super::NativeDestruct();

    // バインドを削除
    if (Button_Resume)
    {
        Button_Resume->OnClicked.RemoveDynamic(this, &UMyUIWidget::OnResumeClicked);
    }
    if (Button_Settings)
    {
        Button_Settings->OnClicked.RemoveDynamic(this, &UMyUIWidget::OnSettingsClicked);
    }
    if (Button_Exit)
    {
        Button_Exit->OnClicked.RemoveDynamic(this, &UMyUIWidget::OnExitClicked);
    }
}

void UMyUIWidget::OnResumeClicked()
{
    // 再開処理
    UGameplayStatics::SetGamePaused(GetWorld(), false);
    RemoveFromParent();
}

void UMyUIWidget::OnSettingsClicked()
{
    // 設定メニューを開く
    UE_LOG(LogTemp, Warning, TEXT("Settings menu opened"));
}

void UMyUIWidget::OnExitClicked()
{
    // メインメニューに戻る
    UGameplayStatics::OpenLevel(GetWorld(), FName("MainMenu"));
}

void UMyUIWidget::MoveNavigationUp()
{
    int32 NewIndex = CurrentFocusIndex - 1;
    if (NewIndex < 0) NewIndex = ButtonArray.Num() - 1;

    SetFocusToButton(NewIndex);
}

void UMyUIWidget::MoveNavigationDown()
{
    int32 NewIndex = CurrentFocusIndex + 1;
    if (NewIndex >= ButtonArray.Num()) NewIndex = 0;

    SetFocusToButton(NewIndex);
}

void UMyUIWidget::SetFocusToButton(int32 ButtonIndex)
{
    if (ButtonIndex < 0 || ButtonIndex >= ButtonArray.Num())
        return;

    int32 PrevIndex = CurrentFocusIndex;
    CurrentFocusIndex = ButtonIndex;

    if (ButtonArray[ButtonIndex])
    {
        ButtonArray[ButtonIndex]->SetKeyboardFocus();
    }

    OnNavigationEvent.Broadcast(PrevIndex, CurrentFocusIndex);
}

void UMyUIWidget::ActivateCurrentButton()
{
    if (CurrentFocusIndex >= 0 && CurrentFocusIndex < ButtonArray.Num())
    {
        if (ButtonArray[CurrentFocusIndex])
        {
            ButtonArray[CurrentFocusIndex]->OnClicked.Broadcast();
        }
    }
}

第6章:マウス入力の高度なハンドリング

ステップ9:マウスカーソルと相互作用

【マウス入力の実装パターン】

パターン1:クリック・ホバー時のビジュアル反応
├─ On Mouse Enter → ボタン明るくする
├─ On Mouse Leave → ボタン暗くする
└─ On Clicked → アクティベート

パターン2:マウス位置からのヒット判定
├─ Get Mouse Position in Viewport
├─ Viewport to Screen Space
├─ Screen Space to World Space
└─ Line Trace で UI ウィジェット検出

パターン3:マウス速度に基づいた処理
├─ Get Last Mouse Position
├─ Calculate Velocity
└─ Spawn Drag Effect(ドラッグ表現)

9-1. ブループリントでの実装

【WBP_MouseAwareButton】

On Mouse Move:
├─ Get Mouse Position in Viewport
├─ Calculate Distance to Button Center
├─ If Distance < Threshold:
│  └─ Set Button Color to Highlight
└─ Else:
   └─ Set Button Color to Normal

On Mouse Down:
├─ Play Press Animation
├─ Play Sound Effect
└─ Store Start Position

On Mouse Up:
├─ If Still Over Button:
│  ├─ Trigger Click Event
│  └─ Play Release Animation
└─ Else:
   └─ Cancel Click

9-2. C++ での詳細実装

cpp

// MyMouseAwareButton.h
#pragma once

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "Components/Button.h"
#include "MyMouseAwareButton.generated.h"

UCLASS()
class YOURPROJECT_API UMyMouseAwareButton : public UButton
{
    GENERATED_BODY()

protected:
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Mouse")
    float HoverDistanceThreshold;

    UPROPERTY(BlueprintReadWrite, Category = "Mouse")
    FVector2D LastMousePosition;

    UPROPERTY(BlueprintReadWrite, Category = "Mouse")
    bool bIsMouseOver;

    virtual void NativeConstruct() override;
    virtual void NativeTick(const FGeometry& MyGeometry, 
                           float InDeltaTime) override;

    UFUNCTION()
    void OnMouseEnter(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent);

    UFUNCTION()
    void OnMouseLeave(const FPointerEvent& MouseEvent);

    UFUNCTION()
    void OnMouseMove(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent);

public:
    UFUNCTION(BlueprintCallable, Category = "Mouse")
    float GetDistanceToMouse(const FGeometry& MyGeometry) const;
};

cpp

// MyMouseAwareButton.cpp
#include "MyMouseAwareButton.h"
#include "Kismet/GameplayStatics.h"

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

    HoverDistanceThreshold = 50.0f;
    bIsMouseOver = false;

    OnMouseEnterEvent.AddDynamic(this, &UMyMouseAwareButton::OnMouseEnter);
    OnMouseLeaveEvent.AddDynamic(this, &UMyMouseAwareButton::OnMouseLeave);
    OnMouseMoveEvent.AddDynamic(this, &UMyMouseAwareButton::OnMouseMove);
}

void UMyMouseAwareButton::NativeTick(const FGeometry& MyGeometry, 
                                     float InDeltaTime)
{
    Super::NativeTick(MyGeometry, InDeltaTime);

    // マウス位置の更新(フレームごと)
    if (bIsMouseOver)
    {
        APlayerController* PC = GetOwningPlayer();
        if (PC)
        {
            FVector2D MousePosition;
            PC->GetMousePosition(MousePosition.X, MousePosition.Y);
            LastMousePosition = MousePosition;
        }
    }
}

void UMyMouseAwareButton::OnMouseEnter(const FGeometry& MyGeometry, 
                                       const FPointerEvent& MouseEvent)
{
    bIsMouseOver = true;

    // フォーカス設定
    if (!HasKeyboardFocus())
    {
        SetKeyboardFocus();
    }

    // ビジュアル反応(色変更など)
    // SetColorAndOpacity(FLinearColor::Yellow);
}

void UMyMouseAwareButton::OnMouseLeave(const FPointerEvent& MouseEvent)
{
    bIsMouseOver = false;

    // フォーカス解除
    if (HasKeyboardFocus())
    {
        ClearKeyboardFocus();
    }

    // ビジュアル戻す
    // SetColorAndOpacity(FLinearColor::White);
}

void UMyMouseAwareButton::OnMouseMove(const FGeometry& MyGeometry, 
                                      const FPointerEvent& MouseEvent)
{
    LastMousePosition = MouseEvent.GetScreenSpacePosition();
}

float UMyMouseAwareButton::GetDistanceToMouse(const FGeometry& MyGeometry) const
{
    FVector2D ButtonCenter = MyGeometry.GetAbsoluteSize() / 2.0f;
    FVector2D ButtonScreenPosition = MyGeometry.GetAbsolutePosition() + ButtonCenter;

    return FVector2D::Distance(ButtonScreenPosition, LastMousePosition);
}

第7章:ゲームパッド入力の高度なハンドリング

ステップ10:ゲームパッド振動とフィードバック

ゲームパッド操作では、振動フィードバック(ハプティクス)がユーザーエクスペリエンスを大幅に向上させます。

cpp

// GamepadFeedback.cpp
#include "GameFramework/PlayerController.h"
#include "GenericPlatform/GenericApplication.h"

void AMyPlayerController::PlayGamepadVibration(float Intensity, float Duration)
{
    if (!IsLocalController())
        return;

    FVector2D VibeAmount(Intensity, Intensity);
    
    // ゲームパッド振動を再生
    FSlateApplication::Get().OnControllerAnalogValueChanged(
        FGamepadKeyNames::LeftMotor,
        VibeAmount.X,
        0
    );

    // 振動を一定時間後に停止
    GetWorld()->GetTimerManager().SetTimer(
        VibeTimerHandle,
        [this]()
        {
            FVector2D StopVibe(0.0f, 0.0f);
            FSlateApplication::Get().OnControllerAnalogValueChanged(
                FGamepadKeyNames::LeftMotor,
                0.0f,
                0
            );
        },
        Duration,
        false
    );
}

ステップ11:複数ゲームパッド対応

複数のプレイヤーをサポートする場合、各プレイヤーの入力を適切に管理する必要があります。

cpp

// MultiplayerController.cpp

void AMyGameMode::StartMatch()
{
    // Player 1
    APlayerController* PC1 = GetWorld()->GetFirstPlayerController();
    if (PC1)
    {
        // 各プレイヤーに別々のInputMappingContextを割り当て
        if (UEnhancedInputLocalPlayerSubsystem* Subsystem1 =
                PC1->GetLocalPlayer()->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>())
        {
            Subsystem1->AddMappingContext(Player1IMC, 0);
        }
    }

    // Player 2 以降も同様
}

第8章:共通のトラブルシューティング

問題1:ゲームパッドが反応しない

【原因と対処】

原因1:Input Mapping Context が追加されていない
対処:
├─ SetupPlayerInputComponent() で必ず AddMappingContext() を呼ぶ
└─ プロジェクト設定で Enhanced Input が有効か確認

原因2:Input Action が正しく割り当てられていない
対処:
├─ Mapping タブで、Gamepad mapping が存在するか確認
├─ Dead Zone が大きすぎないか確認(推奨0.1~0.3)
└─ Input Action の Type が適切か確認

原因3:ゲームパッドが認識されていない
対処:
├─ Windows: Settings → Devices → Bluetooth & other devices で確認
├─ macOS: System Preferences → Bluetooth で確認
├─ Linux: jstest-gtk などでテスト
└─ ドライバの再インストール

問題2:マウスとゲームパッドが競合する

【原因と対処】

原因:両入力が同時にUIを操作しようとしている
対処:
├─ SetInputMode() で入力モードを明確に設定
│  ├─ Game and UI:両方対応
│  ├─ Game Only:ゲーム入力のみ
│  └─ UI Only:UI入力のみ
│
├─ マウスフォーカスとゲームパッドフォーカスを分離
│  └─ デバイス種別ごとに異なるフォーカスUIを使用
│
└─ Input Consume(入力を消費)を適切に設定
   └─ マウスホバー時はゲームパッド移動を無視

問題3:フォーカス移動がぎこちない

【原因と対処】

原因1:Navigation設定が不足
対処:
├─ Panel 設定で Navigation を明示的に設定
├─ Focus Navigation Rules:
│  ├─ Up Navigation
│  ├─ Down Navigation
│  ├─ Left Navigation
│  └─ Right Navigation
└─ 一方向ナビゲーションに設定

原因2:Dead Zone が小さすぎる
対処:
├─ Dead Zone Modifier のパラメータを調整
├─ Lower Threshold:0.1~0.3(推奨0.2)
└─ 感度テストを複数ゲームパッド型式で実施

原因3:ナビゲーション遅延
対処:
├─ Smooth Value Modifier を追加
├─ Rising Speed を低めに設定(1.0~2.0)
└─ Processors で Debounce を有効

問題4:マウス入力が反応しない

【原因と対処】

原因1:bShowMouseCursor が false
対処:
├─ APlayerController::BeginPlay() で設定
└─ bShowMouseCursor = true;

原因2:Input Mode が正しく設定されていない
対処:
├─ SetInputMode(FInputModeGameAndUI());
└─ または SetInputMode(FInputModeUIOnly());

原因3:UI が Canvas の背後にある
対処:
├─ Z-Order(レイヤー)順序を確認
├─ Canvas Panel の後ろに配置されていないか
└─ Add to Viewport の Priority パラメータを確認

第9章:ベストプラクティス

ガイドライン1:入力の責任分離

【良い設計パターン】

PlayerController:
├─ 入力受け取り
├─ 入力イベント発火
└─ UI への委譲

UI Widget:
├─ 入力イベント処理
├─ ビジュアル反応
└─ ゲーム状態変更要求

Game Logic:
├─ ゲーム状態管理
├─ UI への通知
└─ 入力への直接反応なし

ガイドライン2:フォーカス管理の一元化

cpp

// 推奨パターン
class AMyHUD : public AHUD
{
    UPROPERTY()
    UMyUIWidget* CurrentUIWidget;

public:
    void SetFocusedWidget(UMyUIWidget* NewWidget)
    {
        if (CurrentUIWidget)
        {
            CurrentUIWidget->OnFocusLost();
        }
        
        CurrentUIWidget = NewWidget;
        if (CurrentUIWidget)
        {
            CurrentUIWidget->OnFocusGained();
        }
    }
};

ガイドライン3:プラットフォーム別の入力設定

【推奨フォルダ構成】

Input/
├─ Common/
│  ├─ IA_Navigate
│  ├─ IA_Click
│  └─ IMC_UIDefault
│
├─ PC/
│  ├─ IA_Navigate_PC(キーボード重視)
│  └─ IMC_UIPlatformPC
│
├─ Console/
│  ├─ IA_Navigate_Console(ゲームパッド重視)
│  └─ IMC_UIPlatformConsole
│
└─ Mobile/
   ├─ IA_Navigate_Mobile(タッチ入力)
   └─ IMC_UIPlatformMobile

ガイドライン4:デバッグ補助機能

cpp

// 推奨デバッグ実装
void AMyPlayerController::PrintInputDebugInfo()
{
    if (UEnhancedInputComponent* EIC = 
        Cast<UEnhancedInputComponent>(InputComponent))
    {
        const TArray<FEnhancedInputActionValueBinding>& Bindings = 
            EIC->GetActionBindings();

        for (const auto& Binding : Bindings)
        {
            UE_LOG(LogTemp, Warning, 
                TEXT("Action: %s, Value: %f"),
                *Binding.GetAction()->GetName(),
                Binding.GetValue().Get<float>()
            );
        }
    }
}

// コンソールコマンドとして登録
FConsoleCommandWithoutArgsDelegate PrintInputDebugDelegate(
    FConsoleCommandWithoutArgsDelegate::CreateUObject(
        PlayerController,
        &AMyPlayerController::PrintInputDebugInfo
    )
);
IConsoleManager::Get().RegisterConsoleCommand(
    TEXT("PrintInputDebug"),
    TEXT("Print input debug information"),
    PrintInputDebugDelegate
);

第10章:実装チェックリスト

ゲームパッド・マウス両対応のUI実装に向けたチェックリストです。

準備フェーズ

□ UE5.1以上のバージョンを使用
□ Enhanced Input System が有効化されている
□ DefaultPlayerInputClass が EnhancedPlayerInput に設定
□ DefaultInputComponentClass が EnhancedInputComponent に設定

Input Action 準備

□ IA_Navigate 作成(Axis2D型)
  □ Keyboard: Arrow Keys
  □ Keyboard: WASD
  □ Gamepad: Left Stick
  □ Gamepad: D-Pad
  □ Dead Zone Modifier: 0.2

□ IA_Click 作成(Digital型)
  □ Mouse: Left Button
  □ Gamepad: Face Button Bottom

□ IA_Submit 作成(Digital型)
  □ Keyboard: Enter
  □ Gamepad: Face Button Bottom/Right

□ IA_Cancel 作成(Digital型)
  □ Keyboard: Escape
  □ Gamepad: Face Button Right

Input Mapping Context 準備

□ IMC_UIDefault 作成
□ 上記4つの Input Action をマッピング
□ 各 Action に複数の Mapping 追加
□ Modifiers と Processors を必要に応じて追加

PlayerController 実装

□ BeginPlay() で AddMappingContext() を呼び出し
□ SetupPlayerInputComponent() で全 Action をバインド
□ SetInputMode() で入力モード設定
□ bShowMouseCursor = true でマウス表示

UI Widget 実装

□ OnFocusReceived イベント実装
□ OnFocusLost イベント実装
□ OnMouseEnter/OnMouseLeave 実装
□ ゲームパッド用フォーカスビジュアル実装
□ マウス用ホバービジュアル実装
□ ナビゲーション設定(Up/Down/Left/Right)

テスト

□ マウスキーボードでの操作テスト
  □ マウスクリック
  □ キーボード矢印キー
  □ Tab キーナビゲーション
  
□ ゲームパッドでの操作テスト
  □ 左スティック移動
  □ D-Pad移動
  □ A ボタン(決定)
  □ B ボタン(キャンセル)

□ 複合操作テスト
  □ マウスホバー + ゲームパッド移動
  □ キーボード入力 + ゲームパッド入力
  □ 入力が競合しないか確認

□ パフォーマンステスト
  □ フレームレート低下がないか
  □ 入力レイテンシが許容値以下か

まとめ

UE5でのゲームパッド・マウス両対応UI実装は、以下の重要なポイントを押さえることで実現できます:

要点1:Enhanced Input System を採用

UE5.1以降では Enhanced Input System が標準です。従来の SetupPlayerInputComponent だけでは不十分です。Input Action と Input Mapping Context を正しく構成することが基本です。

要点2:フォーカスシステムを理解する

フォーカスはゲームパッド入力の生命線です。正確なフォーカス移動とビジュアルフィードバックが、ゲームパッド操作性を決定します。

要点3:マウス入力とゲームパッド入力の分離

両入力が競合しないよう、以下を明確にします:

  • マウス入力:直接クリック、ホバー効果
  • ゲームパッド入力:フォーカス移動、ボタン実行

要点4:プラットフォーム別の最適化

PC・コンソール・モバイルでは最適な入力方法が異なります。プラットフォームごとに Input Mapping Context を分けることで、各プラットフォームで最高のUXを提供できます。

要点5:継続的なテストと最適化

デッドゾーン、感度、ナビゲーション遅延など、細かな調整がユーザーエクスペリエンスに大きく影響します。複数のゲームパッド型式・マウス環境でテストすることが重要です。


参考資料・リンク集

公式ドキュメント

コミュニティリソース

関連ツール

  • Enhanced Input Debugger:エディタプラグインで入力値をリアルタイム監視
  • Gamepad Tester:複数ゲームパッドの入力をテスト
  • Input Visualizer:UI入力の流れを可視化

このガイドが、あなたのゲーム開発における入力システム構築に役立つことを願っています。Happy Coding!

よかったらシェアしてね!
目次