アクションゲームを作っていると、攻撃が当たった瞬間の「手応え」をどう演出するかが気になってきますよね。
Godot 4 でも AnimationPlayer や Tween、Camera2D のプロパティを直接いじることで、zoom や offset を一時的に変更する実装はできます。
ただ、素直にやろうとすると…
- プレイヤーシーンの中にカメラ用のアニメーションをガッツリ仕込む
- 敵シーンの中にも同じような処理をコピペする
- 「ズーム処理」をプレイヤーや敵のスクリプトに直接書き込んでしまう
といった感じで、カメラ演出のロジックがプレイヤーや敵とベッタリ結合してしまいがちです。
さらに、ズームの強さや時間を調整したいときに、複数のスクリプトを開いて微調整…という地味に面倒な作業も増えます。
そこで今回は、「ヒット時ズーム」を一個のコンポーネントに閉じ込めるアプローチを紹介します。
プレイヤーでも敵でも、動く罠でも、「当たったらズームしたいカメラ」にだけアタッチして使い回せるようにするのが狙いです。
【Godot 4】当たりの爽快感を一瞬で盛る!「HitStopZoom」コンポーネント
今回作る HitStopZoom コンポーネントは、ざっくり言うと:
- Camera2D 専用のコンポーネント
- 「攻撃がヒットした」ときに、任意のターゲット位置へ一瞬ズームインしてから元に戻す
- ズーム倍率・時間・イージングなどを
@exportで Inspector から調整可能 - シグナル/関数を呼ぶだけで発動できる
つまり、「ヒット演出」は全部このコンポーネントに押し込めて、プレイヤーや敵のスクリプトは
「当たったよ → カメラさん、ズームお願い」と依頼するだけにしてしまおう、という設計ですね。
フルコード:HitStopZoom.gd
extends Node
class_name HitStopZoom
## 攻撃ヒット時にカメラをターゲットへ一瞬ズームインさせるコンポーネント
##
## ・Camera2D にアタッチして使う前提
## ・hit_zoom_at(target_global_position) を呼ぶだけで発動
## ・ズーム量、時間、イージングなどは Inspector から調整可能
@export_group("Basic Settings")
## ズーム演出を有効/無効にするフラグ
@export var enabled: bool = true
## ズームイン時の倍率(Camera2D.zoom は 1 が等倍、0.5 で2倍ズーム)
## 例: Vector2(0.6, 0.6) なら、少しだけ寄る感じ
@export var zoom_in_value: Vector2 = Vector2(0.6, 0.6)
## ズームインにかける時間(秒)
@export_range(0.01, 1.0, 0.01)
@export var in_duration: float = 0.08
## ズームアウト(元に戻る)にかける時間(秒)
@export_range(0.01, 1.0, 0.01)
@export var out_duration: float = 0.12
@export_group("Target & Movement")
## ヒット時にカメラの中心をターゲット位置へ寄せるかどうか
## false の場合はズームのみで、位置は動かさない
@export var move_to_target: bool = true
## ターゲット位置へ寄せる強さ(0~1)
## 1.0 = 完全にターゲット位置へ移動、0.5 = 半分だけ寄る
@export_range(0.0, 1.0, 0.05)
@export var move_strength: float = 0.7
## カメラ位置の補正量(画面揺れなどと合わせる場合に調整)
@export var position_offset: Vector2 = Vector2.ZERO
@export_group("Timing & Easing")
## ヒットからズーム開始までの遅延(秒)
@export_range(0.0, 0.5, 0.01)
@export var start_delay: float = 0.0
## ズームイン/アウトのイージング(Tween.EaseType)
## 0: IN, 1: OUT, 2: IN_OUT, 3: OUT_IN
@export_enum("IN", "OUT", "IN_OUT", "OUT_IN")
@export var ease_type: int = 1
## ズームイン/アウトのトランジション(Tween.TransitionType)
## よく使うものだけ列挙
## 0: LINEAR, 1: SINE, 2: QUAD, 3: CUBIC, 4: QUART, 5: QUINT, 6: EXPO, 7: CIRC, 8: BACK, 9: ELASTIC, 10: BOUNCE
@export_enum("LINEAR", "SINE", "QUAD", "CUBIC", "QUART", "QUINT", "EXPO", "CIRC", "BACK", "ELASTIC", "BOUNCE")
@export var transition_type: int = 2
@export_group("Safety")
## 連続ヒット時に、前のズームをキャンセルして上書きするか
## true: 毎回リセットして新しいズームを開始
## false: すでにズーム中なら無視する
@export var override_when_running: bool = true
## デバッグ用:エディタ上でテスト再生できるようにする
@export var debug_test_on_start: bool = false
## 内部状態
var _camera: Camera2D
var _tween: Tween
var _original_zoom: Vector2
var _original_position: Vector2
var _is_running: bool = false
func _ready() -> void:
## 親ノードが Camera2D であることを前提に取得
_camera = get_parent() as Camera2D
if _camera == null:
push_warning("HitStopZoom: Parent is not a Camera2D. This component is intended to be a child of Camera2D.")
enabled = false
return
_original_zoom = _camera.zoom
_original_position = _camera.global_position
if debug_test_on_start:
## シーン再生直後にカメラの現在位置へテストズーム
hit_zoom_at(_camera.global_position)
## 外部から呼ぶメインのAPI
## target_global_position: ヒットしたオブジェクトのグローバル座標
func hit_zoom_at(target_global_position: Vector2) -> void:
if not enabled:
return
## すでにズーム中の扱い
if _is_running and not override_when_running:
return
## すでに動いている Tween があれば止める
if _tween and _tween.is_valid():
_tween.kill()
_is_running = true
## 毎回、現在の zoom / position を基準として扱う
_original_zoom = _camera.zoom
_original_position = _camera.global_position
_tween = create_tween()
_tween.set_parallel(false) # 直列でつなぐ
## 遅延があれば wait
if start_delay > 0.0:
_tween.tween_interval(start_delay)
## ターゲット位置へ寄せる場合
var zoom_in_target_zoom := zoom_in_value
var zoom_in_target_position := _original_position
if move_to_target:
## ターゲット位置に向かって move_strength 分だけ寄せる
var target_pos := _original_position.lerp(target_global_position, move_strength) + position_offset
zoom_in_target_position = target_pos
## イージング設定を Tween に適用するヘルパー
var ease := Tween.EaseType.values()[ease_type]
var trans := Tween.TransitionType.values()[transition_type]
## ズームイン(zoom と position を並列に動かす)
var in_tween := _tween.parallel()
in_tween.tween_property(
_camera,
"zoom",
zoom_in_target_zoom,
in_duration
).set_ease(ease).set_trans(trans)
if move_to_target:
in_tween.tween_property(
_camera,
"global_position",
zoom_in_target_position,
in_duration
).set_ease(ease).set_trans(trans)
## ズームアウト(元の zoom / position に戻す)
var out_tween := _tween.parallel()
out_tween.tween_property(
_camera,
"zoom",
_original_zoom,
out_duration
).set_ease(ease).set_trans(trans)
if move_to_target:
out_tween.tween_property(
_camera,
"global_position",
_original_position,
out_duration
).set_ease(ease).set_trans(trans)
## 完了時にフラグを戻す
_tween.finished.connect(_on_tween_finished)
func _on_tween_finished() -> void:
_is_running = false
## 念のため最終値をきっちり補正しておく
if _camera:
_camera.zoom = _original_zoom
_camera.global_position = _original_position
## 外部から「今の演出を強制終了して元に戻す」ための関数
func reset_now() -> void:
if _tween and _tween.is_valid():
_tween.kill()
if _camera:
_camera.zoom = _original_zoom
_camera.global_position = _original_position
_is_running = false
使い方の手順
ここからは、実際にシーンへ組み込む流れを見ていきましょう。
例として、プレイヤーが敵に攻撃を当てたときにカメラがズームインするケースで説明します。
① コンポーネントスクリプトを用意する
- プロジェクト内に
res://components/camera/HitStopZoom.gdなどのパスで保存します。 - 上記のフルコードをコピペして保存。
- Godot エディタでスクリプトを開き、エラーが出ていないことを確認。
② Camera2D に HitStopZoom をアタッチする
プレイヤーを追従するカメラが既にある想定で、シーン構成はこんな感じにしてみましょう:
Player (CharacterBody2D)
├── Sprite2D
├── CollisionShape2D
└── Camera2D
└── HitStopZoom (Node)
Camera2Dを選択して、右クリック → 「子ノードを追加」 →Nodeを追加- 追加した Node を選択して、「スクリプトをアタッチ」から
HitStopZoom.gdを指定
これで Camera2D の子として HitStopZoom コンポーネントがぶら下がった状態になります。
③ プレイヤーの攻撃判定から呼び出す
プレイヤーのスクリプト側では、「敵に当たった」タイミングで hit_zoom_at() を呼ぶだけです。
プレイヤー側は「カメラがどう動くか」を一切知らなくてOKです。
# Player.gd (抜粋)
extends CharacterBody2D
@onready var hit_stop_zoom: HitStopZoom = $Camera2D/HitStopZoom
func _on_attack_hit(enemy: Node2D) -> void:
# 敵にヒットしたときに呼ばれるとする
if not hit_stop_zoom:
return
# 敵のグローバル座標へズームイン
hit_stop_zoom.hit_zoom_at(enemy.global_position)
# ダメージ処理などはいつも通り
enemy.take_damage(1)
シグナルを使っているなら、Area2D.body_entered や独自の hit シグナルから同様に呼んでOKです。
④ 敵側や別カメラでも使い回す
このコンポーネントは 「Camera2D にアタッチされていること」だけが前提なので、他のシーンでも同じように使い回せます。
例えば、ボス専用カメラや演出用のシネマティックカメラにも同じコンポーネントを付けて:
BossScene (Node2D)
├── Boss (CharacterBody2D)
└── BossCamera (Camera2D)
└── HitStopZoom (Node)
ボスの攻撃がプレイヤーに当たったときに、BossCamera/HitStopZoom へ hit_zoom_at(player.global_position) を投げてやれば、ボス視点のド派手なヒットズームも簡単に作れます。
メリットと応用
コンポーネントとして切り出すことで、いろいろと嬉しいポイントがあります。
- プレイヤーや敵のスクリプトがシンプルになる
「当たったらhit_zoom_at()を呼ぶ」以外のことを考えなくてよくなります。 - カメラ演出のパラメータを一箇所で調整できる
ズーム倍率・時間・イージングなどはコンポーネントの@exportから調整できるので、
レベルデザイナーや演出担当がシーンごとに微調整しやすいです。 - シーン構造がスッキリする
Camera2D の下に「ズーム演出」「シェイク」「ポストエフェクト制御」などのコンポーネントを並べていけば、
継承地獄や深いノード階層に頼らずに、カメラ機能を合成していくスタイルが実現できます。 - 別カメラでもそのまま再利用可能
プレイヤー用・ボス用・リプレイ用など、複数カメラを持つゲームでも、
同じコンポーネントをペタペタ貼るだけで統一した挙動にできます。
「ヒット時ズーム」はゲームによって好みが分かれる演出なので、コンポーネントごと ON/OFF できるのも地味に便利ですね。
改造案:ヒットの強さでズーム量を変える
例えば、「強攻撃のときはより深くズームしたい」という場合、
ダメージ量やノックバック量に応じてズーム量を変える関数を追加するのもアリです。
## ダメージ量に応じてズーム量をスケールさせる例
func hit_zoom_with_power(target_global_position: Vector2, power: float) -> void:
# power: 0.0 ~ 1.0 を想定(1.0 で最大ズーム)
power = clamp(power, 0.0, 1.0)
# 元の zoom_in_value を基準に、さらに寄せる
var base_zoom := zoom_in_value
var extra_zoom_factor := 0.4 # 最大でさらに 40% 寄るイメージ
var scaled_zoom := base_zoom * (1.0 - extra_zoom_factor * power)
var prev_zoom := zoom_in_value
zoom_in_value = scaled_zoom
hit_zoom_at(target_global_position)
zoom_in_value = prev_zoom
プレイヤー側からは:
# 例: 0.0 = 弱攻撃, 1.0 = 超必殺技
hit_stop_zoom.hit_zoom_with_power(enemy.global_position, attack_power)
のように呼べば、攻撃の強さに応じてカメラ演出も盛り上げることができます。
こんな感じで、カメラ演出をどんどんコンポーネント化していくと、
継承に頼らない「合成ベース」の Godot プロジェクトが気持ちよく回るようになります。
ぜひ自分のゲームに合わせて、HitStopZoom をベースに育ててみてください。
