Godot 4でスマホやタブレット向けのゲームを作り始めると、まずぶつかるのが「スワイプ入力どうしよう問題」ですね。
標準の InputEventScreenTouchInputEventScreenDrag を素直に使おうとすると、

  • プレイヤー、UI、カメラなど、あちこちのスクリプトで入力処理を書きがち
  • 上下左右の判定ロジックを毎回コピペしがち
  • ノードを継承して「PlayerWithSwipe」「EnemyWithSwipe」みたいなクラスが増殖しがち

結果として、「どこでスワイプを処理しているのか分からない」「ちょっと仕様を変えたいだけなのに、全部のスクリプトを直す羽目になる」という、よくある泥沼にハマります。

そこで、今回の記事では「継承より合成」の考え方で、どのノードにも後付けできる SwipeDetector コンポーネント を用意してみましょう。
プレイヤーでも、敵でも、UIでも、「スワイプを使いたいノードにコンポーネントを1つアタッチするだけ」で済むようにします。

【Godot 4】指スライドをスマートに分離!「SwipeDetector」コンポーネント

このコンポーネントのゴールはシンプルです。

  • タッチの開始位置と終了位置から、上下左右のスワイプを判定
  • しきい値(最低距離・最短時間など)をパラメータで調整可能
  • 判定結果は シグナル で外部に通知(プレイヤーやUIはそれを受け取るだけ)

つまり、「入力の生データ処理」をこのコンポーネントに閉じ込めて、ゲームロジック側は swipe_detected(direction) みたいなシグナルだけを見ればOK、という構造にします。


フルコード:SwipeDetector.gd


extends Node
class_name SwipeDetector
## タッチパネルのスワイプ方向(上下左右)を検知してシグナルを発行するコンポーネント
##
## ・どのノードにもアタッチ可能(Node継承)
## ・_unhandled_input を使うので、UI で消費されなかったタッチを拾う構成
## ・PCでのテスト用にマウスドラッグにも対応(オプション)

## スワイプが検知されたときに発火するシグナル
## direction は "up" / "down" / "left" / "right" のいずれか
signal swipe_detected(direction: String)

## --- 設定パラメータ(インスペクタで調整可能) ---

@export_range(10.0, 1000.0, 1.0)
var min_swipe_distance: float = 80.0
## スワイプとみなす最小距離(ピクセル)。
## これより短い移動は「タップ」扱いとして無視します。

@export_range(0.0, 1.0, 0.01)
var max_swipe_duration: float = 0.5
## スワイプとして許可する最大時間(秒)。
## これより長いドラッグは「長押し+ドラッグ」とみなし、スワイプとしては無視します。
## 0 の場合は時間制限なし。

@export var enable_diagonal: bool = false
## 斜め方向も許可するか。
## false の場合:X か Y の大きい方にスナップして「上下左右」のどれかに丸めます。
## true の場合:斜め方向も文字列で返す("up_left" など)。※今回はオプション扱い。

@export var accept_mouse_as_touch: bool = true
## PC 上でテストしやすいように、マウス左ボタンのドラッグも
## スクリーンタッチとして扱うかどうか。

@export var debug_print: bool = false
## デバッグ用:true のとき、スワイプ方向や距離を print します。

## --- 内部状態 ---

var _is_dragging: bool = false
var _start_position: Vector2 = Vector2.ZERO
var _start_time: float = 0.0
var _active_index: int = -1
## 使用中のタッチインデックス(マルチタッチのうちどれを追うか)

func _ready() -> void:
    ## 特に初期化は不要ですが、将来的な拡張に備えておきます。
    pass


func _unhandled_input(event: InputEvent) -> void:
    ## UI などで消費されなかった入力イベントをここで拾います。
    ## 「どのノードが入力を処理するか」をゲームロジックから切り離せるのがポイントですね。
    if event is InputEventScreenTouch:
        _handle_screen_touch(event)
    elif event is InputEventScreenDrag:
        _handle_screen_drag(event)
    elif accept_mouse_as_touch and (event is InputEventMouseButton or event is InputEventMouseMotion):
        _handle_mouse_as_touch(event)


## --- タッチイベントの処理 ---

func _handle_screen_touch(event: InputEventScreenTouch) -> void:
    if event.pressed:
        # タッチ開始
        _start_swipe(event.position, event.index)
    else:
        # タッチ終了
        _end_swipe(event.position, event.index)


func _handle_screen_drag(event: InputEventScreenDrag) -> void:
    # ドラッグ中は「今どの指を追っているか」を確認するだけ。
    # 今回は途中経過ではシグナルを出さず、離した瞬間だけ判定します。
    if _is_dragging and event.index == _active_index:
        # 必要ならここで「リアルタイム方向プレビュー」も可能
        pass


## --- マウスをタッチとして扱う処理(PCテスト用) ---

func _handle_mouse_as_touch(event: InputEvent) -> void:
    if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
        if event.pressed:
            _start_swipe(event.position, 0)
        else:
            _end_swipe(event.position, 0)
    elif event is InputEventMouseMotion:
        # ドラッグ中のマウス移動。今回は使わない。
        pass


## --- 共通処理 ---

func _start_swipe(pos: Vector2, index: int) -> void:
    # すでに別の指を追っている場合は無視(単純化のためシングルタッチ想定)
    if _is_dragging:
        return

    _is_dragging = true
    _start_position = pos
    _start_time = Time.get_ticks_msec() / 1000.0
    _active_index = index

    if debug_print:
        print("[SwipeDetector] start at: ", pos, " index: ", index)


func _end_swipe(pos: Vector2, index: int) -> void:
    if not _is_dragging:
        return
    if index != _active_index:
        # 追跡中の指ではない
        return

    _is_dragging = false
    var end_time := Time.get_ticks_msec() / 1000.0
    var duration := end_time - _start_time
    var delta := pos - _start_position
    var distance := delta.length()

    if debug_print:
        print("[SwipeDetector] end at: ", pos, " duration: ", duration, " distance: ", distance)

    # 時間チェック
    if max_swipe_duration > 0.0 and duration > max_swipe_duration:
        if debug_print:
            print("[SwipeDetector] ignored: too slow")
        return

    # 距離チェック
    if distance < min_swipe_distance:
        if debug_print:
            print("[SwipeDetector] ignored: too short")
        return

    var direction := _get_direction_from_delta(delta)
    if direction == "":
        if debug_print:
            print("[SwipeDetector] ignored: direction undetermined")
        return

    if debug_print:
        print("[SwipeDetector] swipe_detected: ", direction)

    swipe_detected.emit(direction)


func _get_direction_from_delta(delta: Vector2) -> String:
    ## 移動ベクトルから方向文字列を決定する。
    ## enable_diagonal のオン/オフで挙動を変えます。
    if delta == Vector2.ZERO:
        return ""

    var x := delta.x
    var y := delta.y

    if enable_diagonal:
        # 8方向対応(up, down, left, right, up_left, up_right, down_left, down_right)
        var horizontal := ""
        var vertical := ""

        if x > 0:
            horizontal = "right"
        elif x < 0:
            horizontal = "left"

        if y > 0:
            vertical = "down"
        elif y < 0:
            vertical = "up"

        if horizontal != "" and vertical != "":
            return vertical + "_" + horizontal
        elif horizontal != "":
            return horizontal
        elif vertical != "":
            return vertical
        else:
            return ""
    else:
        # 4方向のみ。X と Y のどちらが大きいかで決定。
        if abs(x) > abs(y):
            return "right" if x > 0 else "left"
        else:
            return "down" if y > 0 else "up"

使い方の手順

ここからは、実際にゲーム内で使う流れを見ていきましょう。

手順①: コンポーネントスクリプトを用意する

  1. 上の SwipeDetector.gd をコピペして、新規スクリプトとして保存します。
    例: res://components/input/swipe_detector.gd など。
  2. スクリプトを開いた状態で、Godotエディタ右上の「クラス」アイコンから Global Class として登録しておくと、ノード追加時に「SwipeDetector」が出てきて便利です。

手順②: プレイヤーシーンにアタッチする

例として、2Dアクションゲームのプレイヤーにスワイプ操作で「ダッシュ」させるケースを考えます。

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── SwipeDetector (Node)
  1. Player シーンを開く
  2. 子ノードとして Node を追加
  3. その Node に SwipeDetector.gd をアタッチ(もしくは Global Class から直接 SwipeDetector を追加)
  4. インスペクタで以下のように設定
    • min_swipe_distance: 80〜120 くらい(端末解像度に合わせて調整)
    • max_swipe_duration: 0.4〜0.6 あたり(素早いスワイプだけを拾う)
    • enable_diagonal: false(まずは上下左右だけ)
    • accept_mouse_as_touch: true(PCでテストしやすく)

手順③: シグナルをプレイヤースクリプトに接続する

Player 側は、「入力処理」を一切持たずに、「スワイプされた方向に応じて動く」ことだけを考えます。
コンポーネントとの連携は シグナル 経由です。


# Player.gd (例)
extends CharacterBody2D

@export var dash_speed: float = 800.0
@export var dash_duration: float = 0.2

var _dash_velocity: Vector2 = Vector2.ZERO
var _dash_time_left: float = 0.0

func _ready() -> void:
    # 子ノードの SwipeDetector を取得
    var swipe_detector := $SwipeDetector
    swipe_detector.swipe_detected.connect(_on_swipe_detected)


func _physics_process(delta: float) -> void:
    if _dash_time_left > 0.0:
        _dash_time_left -= delta
        velocity = _dash_velocity
    else:
        # 通常移動ロジック(例)
        var input_dir := Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
        velocity = input_dir * 200.0

    move_and_slide()


func _on_swipe_detected(direction: String) -> void:
    # スワイプ方向にダッシュする例
    match direction:
        "up":
            _dash_velocity = Vector2.UP * dash_speed
        "down":
            _dash_velocity = Vector2.DOWN * dash_speed
        "left":
            _dash_velocity = Vector2.LEFT * dash_speed
        "right":
            _dash_velocity = Vector2.RIGHT * dash_speed
        _:
            return

    _dash_time_left = dash_duration

これで、プレイヤーは「方向キーで歩く」「スワイプでダッシュ」という2系統の操作を簡潔に共存させられます。
プレイヤー自身は InputEvent を一切知らず、direction: String だけを相手にすればいい、というのがポイントですね。

手順④: 別の用途にもそのまま再利用する

同じコンポーネントを、例えば「スワイプで動く床」にも使ってみましょう。

MovingPlatform (Node2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── SwipeDetector (Node)

# MovingPlatform.gd (例)
extends Node2D

@export var move_distance: float = 64.0
@export var move_speed: float = 200.0

var _target_position: Vector2
var _moving: bool = false

func _ready() -> void:
    _target_position = global_position
    $SwipeDetector.swipe_detected.connect(_on_swipe_detected)


func _physics_process(delta: float) -> void:
    if _moving:
        var dir := (_target_position - global_position)
        if dir.length() < 1.0:
            global_position = _target_position
            _moving = false
        else:
            global_position += dir.normalized() * move_speed * delta


func _on_swipe_detected(direction: String) -> void:
    match direction:
        "left":
            _target_position = global_position + Vector2.LEFT * move_distance
        "right":
            _target_position = global_position + Vector2.RIGHT * move_distance
        "up":
            _target_position = global_position + Vector2.UP * move_distance
        "down":
            _target_position = global_position + Vector2.DOWN * move_distance
        _:
            return
    _moving = true

同じ SwipeDetector を、プレイヤーにも、動く床にも、UIのメニューにも、どこにでもアタッチできます。
「スワイプの判定ロジック」は1か所に閉じ込めておく。これがコンポーネント指向のうまみですね。


メリットと応用

この SwipeDetector コンポーネントを導入することで、以下のようなメリットがあります。

  • シーン構造がスッキリ
    プレイヤー、敵、ギミックがそれぞれ自分で _input を持たなくて済みます。
    「入力を扱うノード」は SwipeDetector に集約されるので、デバッグもしやすくなります。
  • 継承ツリーを増やさずに機能追加
    PlayerWithSwipe みたいな派生クラスを作らなくても、既存の Player に SwipeDetector を 後付け できます。
    これは「継承より合成」の典型的な利点ですね。
  • 設定パラメータでゲームごとの調整が簡単
    カジュアルゲームなら min_swipe_distance を小さめに、アクションゲームなら max_swipe_duration を厳しめに、など、各シーンごとにインスペクタで調整可能です。
  • テストしやすい
    accept_mouse_as_touch をオンにしておけば、PC 上でマウスドラッグだけでスワイプ操作を確認できます。実機ビルドの回数を減らせるのは地味に嬉しいところです。

改造案:スワイプ開始・終了のシグナルを追加する

「スワイプ中にエフェクトを出したい」「指を離した瞬間に UI を戻したい」といったケースでは、「開始」と「終了」もシグナルで通知したくなります。
そんなときは、次のような関数とシグナルを追加するのが手軽です。


signal swipe_started(start_position: Vector2)
signal swipe_ended(start_position: Vector2, end_position: Vector2)

func _start_swipe(pos: Vector2, index: int) -> void:
    if _is_dragging:
        return
    _is_dragging = true
    _start_position = pos
    _start_time = Time.get_ticks_msec() / 1000.0
    _active_index = index
    swipe_started.emit(_start_position)

func _end_swipe(pos: Vector2, index: int) -> void:
    if not _is_dragging or index != _active_index:
        return
    _is_dragging = false
    swipe_ended.emit(_start_position, pos)
    # (このあとに既存の距離・時間チェックと swipe_detected.emit を続ける)

こうしておけば、例えば UI では swipe_started でボタンを「押し込み」状態にし、swipe_ended で元に戻す、といった演出も簡単に作れます。

入力処理を1つのコンポーネントに閉じ込めておくと、こうした拡張も「1ファイルをいじるだけ」で全体に反映されるのが気持ちいいですね。
ぜひ自分のプロジェクト用にカスタマイズして、Godot 4 のコンポーネント指向開発を楽しんでみてください。