Godotで「ワープポータル」を実装しようとすると、ついこういう構成にしがちですよね。
- Playerシーンの中に「PortalArea」を子ノードとしてベタ書き
- プレイヤー側のスクリプトに「ポータル判定」ロジックを直書き
- 敵や動く床など、他のオブジェクトにもワープを持たせたくなってコピペ地獄
さらに、
- 「このポータルはどこに飛ぶんだっけ?」がシーンを開かないと分からない
- ペア関係をコードでベタ書きしてしまい、レベルデザイナーが触りにくい
- プレイヤーだけワープできる、敵だけワープできる…など条件が複雑化
こういう「継承+深いノード階層+スクリプト肥大化」を避けるために、今回はどのノードにもポン付けできる「TeleportPortal コンポーネント」として実装してみましょう。
合成(Composition)で「ワープ機能」を後付けできるようにしておくと、プレイヤーでも敵でも動く床でも、同じ仕組みを使い回せてスッキリします。
【Godot 4】ペアをつなぐ瞬間移動ゲート!「TeleportPortal」コンポーネント
このコンポーネントは、Area2D をベースにしたポータルです。
コンポーネントとして TeleportPortal をシーンに追加し、ペア先のポータルをインスペクターで紐づけるだけで、入ったオブジェクトをペアの位置へ瞬時に移動させます。
- プレイヤー
- 敵キャラ
- 動く床やギミック
など、「位置を持っているノード」ならなんでもワープ対象にできます。
フルコード:TeleportPortal.gd
extends Area2D
class_name TeleportPortal
##
## TeleportPortal コンポーネント
## - この Area2D に入ったノードを、ペアになっている別の TeleportPortal の位置へ瞬時に移動させる
## - 「どのノードがワープ対象か」はグループまたはカスタム条件で制御可能
##
@export_group("Portal Settings")
## ペア先のポータル。インスペクターでドラッグ&ドロップして設定します。
@export var paired_portal: TeleportPortal : set = set_paired_portal
## ワープ対象にしたいノードが所属しているグループ名。
## 例: "player", "enemy", "movable" など。
## 空文字の場合は「すべてのボディ/エリア」を対象とします。
@export var target_group: StringName = &"player"
## ワープさせるときに、ペア先ポータルの位置から少しだけオフセットする距離(ピクセル)。
## キャラクターがポータルと完全に重ならないようにするためのものです。
@export var exit_offset: Vector2 = Vector2(16, 0)
## ワープ後に「どの方向にオフセットするか」。
## 例: Vector2.RIGHT, Vector2.LEFT, Vector2.UP など。
## ペアポータルの向きに応じて変えたい場合は、回転を見て調整するのもありです。
@export var exit_direction: Vector2 = Vector2.RIGHT
@export_group("Cooldown / Safety")
## 同じポータルに連続で入ったときに「行ったり来たり無限ループ」にならないように、
## ワープ後に再度ワープ可能になるまでのクールダウン時間(秒)。
@export_range(0.0, 10.0, 0.05, "or_greater") var cooldown_time: float = 0.4
## true の場合、ワープ対象のノードに「_last_teleport_time」メタデータを付与して、
## 連続ワープを防ぎます(ポータルをまたいだ無限往復対策)。
@export var use_metadata_cooldown: bool = true
## このポータル全体に対するクールダウン。
## 直前に誰かをワープさせた場合、一定時間は誰もワープさせないようにします。
@export var portal_cooldown_time: float = 0.0
@export_group("Debug")
## デバッグ用。true にすると、ワープ時にログを出力します。
@export var debug_log: bool = false
## 内部状態:最後にこのポータルが誰かをワープさせた時間(秒)
var _last_portal_teleport_time: float = -1000.0
func _ready() -> void:
## Area2D のシグナルを接続しておきます。
body_entered.connect(_on_body_entered)
area_entered.connect(_on_area_entered)
if debug_log:
if paired_portal == null:
push_warning("[TeleportPortal] paired_portal が設定されていません。ワープは行われません。")
else:
print("[TeleportPortal] Ready. Paired with: ", paired_portal.name)
func set_paired_portal(value: TeleportPortal) -> void:
paired_portal = value
if debug_log and paired_portal:
print("[TeleportPortal] Paired portal set to: ", paired_portal.name)
## --- 共通処理: ボディ / エリアが入ったときに呼ばれる ---
func _on_body_entered(body: Node) -> void:
_try_teleport(body)
func _on_area_entered(area: Area2D) -> void:
_try_teleport(area)
## 実際のワープ処理
func _try_teleport(target: Node) -> void:
if paired_portal == null:
if debug_log:
push_warning("[TeleportPortal] paired_portal が未設定のため、ワープをスキップしました。")
return
# ポータル側のクールダウンチェック
var now := Time.get_ticks_msec() / 1000.0
if portal_cooldown_time > 0.0 and now - _last_portal_teleport_time < portal_cooldown_time:
if debug_log:
print("[TeleportPortal] Portal cooldown active. Skip teleport for: ", target)
return
# 対象ノードがグループ条件を満たすかチェック
if target_group != &"" and not target.is_in_group(target_group):
# 対象グループが設定されている場合、それ以外は無視します。
return
# 位置を持っているノードだけを対象にしたいので、Node2D かそのサブクラスかを確認
if not target is Node2D:
if debug_log:
push_warning("[TeleportPortal] Target is not Node2D. Cannot teleport: %s" % [target])
return
# メタデータによる個別クールダウンチェック
if use_metadata_cooldown:
var last_time := target.get_meta_or_default("_last_teleport_time", -1000.0)
if typeof(last_time) == TYPE_FLOAT and now - float(last_time) < cooldown_time:
if debug_log:
print("[TeleportPortal] Target cooldown active. Skip teleport for: ", target)
return
# 実際のワープ位置を計算
var node2d := target as Node2D
var exit_dir := exit_direction.normalized()
if exit_dir == Vector2.ZERO:
exit_dir = Vector2.RIGHT
var exit_pos := paired_portal.global_position + exit_dir * exit_offset.length()
# ワープを実行
node2d.global_position = exit_pos
# キャラクター系(CharacterBody2D / RigidBody2D 等)の速度をリセットしたい場合は、
# ここでカスタム処理を入れてもOKです。
_reset_velocity_if_applicable(target)
# クールダウン情報を更新
_last_portal_teleport_time = now
if use_metadata_cooldown:
target.set_meta("_last_teleport_time", now)
if debug_log:
print("[TeleportPortal] Teleported ", target, " to ", exit_pos, " via ", paired_portal.name)
## CharacterBody2D や RigidBody2D をワープさせるとき、
## 直前の速度を引きずると不自然な場合があるので、必要に応じて速度をゼロにします。
func _reset_velocity_if_applicable(target: Node) -> void:
if target is CharacterBody2D:
var body := target as CharacterBody2D
body.velocity = Vector2.ZERO
elif target is RigidBody2D:
var rigid := target as RigidBody2D
rigid.linear_velocity = Vector2.ZERO
rigid.angular_velocity = 0.0
## 便利関数: スクリプトから明示的にワープを発動したい場合に使えます。
## 例: ボタンを押したときだけワープさせたい、など。
func teleport_node(target: Node) -> void:
_try_teleport(target)
使い方の手順
ここでは、プレイヤーがポータルに入ると別の場所にワープする例で説明します。
手順①:TeleportPortal コンポーネントをシーンに追加
- 新規シーンで
Area2Dを作成し、名前をTeleportPortalに変更。 - この
Area2Dに上記のTeleportPortal.gdをアタッチ。 - 子ノードとして
CollisionShape2DとSprite2Dなどを追加し、ポータルの見た目と当たり判定を作ります。 - このシーンを
TeleportPortal.tscnとして保存。
シーン構成例:
TeleportPortal (Area2D) ├── CollisionShape2D # 円形などでポータルの範囲を定義 └── Sprite2D # ポータルの見た目(アニメーションでもOK)
手順②:ポータルを2つ配置してペアにする
- メインステージ(例:
Level01.tscn)を開く。 - シーンツリーに
TeleportPortal.tscnを2つドラッグ&ドロップして配置。- 例:
Portal_AとPortal_Bという名前にする。
- 例:
Portal_Aを選択し、インスペクターのpaired_portalにPortal_Bをドラッグ。- 同様に、
Portal_Bのpaired_portalにPortal_Aをドラッグ。
ステージ全体のシーン構成図例:
Level01 (Node2D)
├── Player (CharacterBody2D)
│ ├── Sprite2D
│ ├── CollisionShape2D
│ └── PlayerController (Script) # 既存のプレイヤー制御スクリプト
├── Portal_A (TeleportPortal) # TeleportPortal.tscn のインスタンス
│ ├── CollisionShape2D
│ └── Sprite2D
└── Portal_B (TeleportPortal)
├── CollisionShape2D
└── Sprite2D
プレイヤー側のスクリプトは一切いじらずに、ポータル側のコンポーネントだけでワープ機能を完結させているのがポイントです。
手順③:プレイヤーを「ワープ対象グループ」に入れる
- Player ノードを選択。
- インスペクター上部の「Node」タブ → 「Groups」タブを開く。
playerというグループ名を追加。- TeleportPortal の
target_groupにplayerを設定(デフォルトがplayerなので変えなくてもOK)。
これで、Player だけがポータルに反応してワープするようになります。
敵もワープさせたい場合は enemy グループを作って敵を所属させ、ポータル側の target_group を enemy に変えた別インスタンスを置けばOKです。
手順④:実行してテスト
- ゲームを実行し、プレイヤーを
Portal_Aに重ねてみましょう。 - プレイヤーが
Portal_Bの近くに瞬間移動していれば成功です。 - もし「行ったり来たり」を繰り返す場合は、
cooldown_timeやportal_cooldown_timeを少し大きめにしてみてください。
敵キャラでも同じように使えます。例:
Enemy (CharacterBody2D)
├── Sprite2D
├── CollisionShape2D
├── EnemyAI (Script)
└── TeleportPortal (Area2D) # 敵専用のポータルを子として持たせる例
├── CollisionShape2D
└── Sprite2D
この場合は、Enemy を enemy グループに入れて、ポータルの target_group を enemy にしておくと、敵専用ワープゲートとして動かせます。
メリットと応用
この TeleportPortal コンポーネントを使うと、
- プレイヤーや敵のスクリプトに「ポータル処理」を書かなくてよい
- それぞれの役割(移動ロジック、AI、ワープ)は別コンポーネントで管理できます。
- レベルデザイナーがインスペクターからペア関係を直感的に編集できる
- コードを触らずに「このポータルはあっちへ飛ぶ」を変えられます。
- どのノードにも後付けできる
- プレイヤー、敵、動く床、ギミックなど、位置を持つものなら何でもワープ可能。
- シーン構造がフラットになる
- 「Playerの中にPortalArea、その中にさらに…」のような深いネストを避けられます。
特に「継承ベースで PlayerWithPortal, EnemyWithPortal みたいな派生シーンを増やす」パターンと比べると、TeleportPortal をポン付けするだけで済むのはかなり快適です。
応用アイデア:方向を引き継ぐワープ
例えば、「ポータルに入った方向をそのまま出口側に反映したい」場合、
以下のような関数で exit_direction を自動計算するのもアリです。
## 改造案:入口に入ってきた向きから出口方向を決める
func compute_exit_direction_from_entry(target: Node2D) -> Vector2:
# ターゲットとこのポータルの位置関係から「入ってきた方向」を推定
var dir := (global_position - target.global_position).normalized()
# そのまま使うと逆向きなので、反転させて出口方向にする
return -dir
この関数を _try_teleport() 内で使えば、
「左から入ったら右へ抜ける」「下から入ったら上へ抜ける」といった、よりゲームっぽいポータル挙動も簡単に実現できます。
こんな感じで、ワープという「機能」をコンポーネントとして切り出しておくと、
あとから「速度を引き継ぐ」「向きを変える」「エフェクトを出す」などの拡張も、
継承地獄に陥らずにサクッと追加していけますね。Composition でいきましょう。
