Godot 4でスマホやタブレット向けのゲームを作り始めると、まずぶつかるのが「スワイプ入力どうしよう問題」ですね。
標準の InputEventScreenTouch や InputEventScreenDrag を素直に使おうとすると、
- プレイヤー、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"
使い方の手順
ここからは、実際にゲーム内で使う流れを見ていきましょう。
手順①: コンポーネントスクリプトを用意する
- 上の
SwipeDetector.gdをコピペして、新規スクリプトとして保存します。
例:res://components/input/swipe_detector.gdなど。 - スクリプトを開いた状態で、Godotエディタ右上の「クラス」アイコンから Global Class として登録しておくと、ノード追加時に「SwipeDetector」が出てきて便利です。
手順②: プレイヤーシーンにアタッチする
例として、2Dアクションゲームのプレイヤーにスワイプ操作で「ダッシュ」させるケースを考えます。
Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── SwipeDetector (Node)
- Player シーンを開く
- 子ノードとして Node を追加
- その Node に
SwipeDetector.gdをアタッチ(もしくは Global Class から直接 SwipeDetector を追加) - インスペクタで以下のように設定
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 のコンポーネント指向開発を楽しんでみてください。
