アクションゲームを作っていると、攻撃が当たった瞬間の「手応え」をどう演出するかが気になってきますよね。
Godot 4 でも AnimationPlayer や Tween、Camera2D のプロパティを直接いじることで、zoomoffset を一時的に変更する実装はできます。

ただ、素直にやろうとすると…

  • プレイヤーシーンの中にカメラ用のアニメーションをガッツリ仕込む
  • 敵シーンの中にも同じような処理をコピペする
  • 「ズーム処理」をプレイヤーや敵のスクリプトに直接書き込んでしまう

といった感じで、カメラ演出のロジックがプレイヤーや敵とベッタリ結合してしまいがちです。
さらに、ズームの強さや時間を調整したいときに、複数のスクリプトを開いて微調整…という地味に面倒な作業も増えます。

そこで今回は、「ヒット時ズーム」を一個のコンポーネントに閉じ込めるアプローチを紹介します。
プレイヤーでも敵でも、動く罠でも、「当たったらズームしたいカメラ」にだけアタッチして使い回せるようにするのが狙いです。

【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

使い方の手順

ここからは、実際にシーンへ組み込む流れを見ていきましょう。
例として、プレイヤーが敵に攻撃を当てたときにカメラがズームインするケースで説明します。

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

  1. プロジェクト内に res://components/camera/HitStopZoom.gd などのパスで保存します。
  2. 上記のフルコードをコピペして保存。
  3. 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/HitStopZoomhit_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 をベースに育ててみてください。