Godot 4でシューティングやアクションゲームを作っていると、「散弾(ショットガンみたいな扇状の弾)」を撃ちたくなる場面って多いですよね。
でも素直に実装しようとすると、プレイヤーや敵キャラのスクリプトの中に

  • 弾を何発出すか
  • 角度をどうずらすか
  • どのシーンをインスタンスするか
  • クールタイムや入力処理

…といったロジックが全部入りになって、どんどんスクリプトが肥大化していきます。
さらに「敵の散弾」「プレイヤーの散弾」「動く砲台の散弾」みたいにパターンが増えると、継承でなんとかしようとしても、最終的には if 文だらけの巨大クラスになりがちです。

そこでこの記事では、「散弾を撃つ」という機能だけを切り出した、コンポーネント指向の ShotgunSpawner を用意して、
プレイヤーにも敵にも砲台にも、ポン付けで再利用できる形にしてしまいましょう。

【Godot 4】扇状にバラ撒け!「ShotgunSpawner」コンポーネント

以下のコンポーネントは、

  • 指定した弾シーンを
  • 指定した発射数・角度で扇状に
  • 指定した方向を基準に発射

するための、汎用ショットガン発射コンポーネントです。
プレイヤー・敵・ギミックなど、どのノードにもアタッチできるように、単独の Node として実装します。

フルコード:ShotgunSpawner.gd


extends Node
class_name ShotgunSpawner
## ShotgunSpawner
## 一度に複数の弾を、角度をずらして扇状に発射するコンポーネント。
##
## 親ノード(プレイヤー、敵、砲台など)にアタッチして使います。
## 「発射方向」「弾のシーン」「弾速」「発射数」「扇の角度」などを
## エディタ上から柔軟に調整できるようにしてあります。

@export_group("Bullet Settings")
## 発射する弾のシーン(PackedScene)
## 例: Bullet.tscn(Rigidbody2D / Area2D など)
@export var bullet_scene: PackedScene

## 弾の初速(ピクセル/秒)。弾シーン側で速度を持っている場合は 0 にして無視してもOK。
@export var bullet_speed: float = 500.0

## 発射位置のオフセット(ShotgunSpawner のローカル座標)
## 例えば銃口の位置に合わせたい場合に調整します。
@export var spawn_offset: Vector2 = Vector2.ZERO

@export_group("Spread Settings")
## 一度に発射する弾の数
@export_range(1, 64, 1)
var bullet_count: int = 5

## 扇状の合計角度(度数法)。中心から左右に半分ずつ広がります。
## 例: 60度なら、中心から -30度 ~ +30度 に散弾をばら撒きます。
@export_range(0.0, 360.0, 1.0)
var spread_angle_deg: float = 60.0

## 中心の発射方向(度数法)。0度は右向き、90度は下向き(Godotの座標系)。
## 通常は親ノードの向きから計算して渡すのがおすすめです。
@export var base_angle_deg: float = 0.0

@export_group("Fire Control")
## 発射クールタイム(秒)。0なら連射制限なし。
@export_range(0.0, 10.0, 0.01)
var cooldown: float = 0.2

## ショットガンを自動で撃つかどうか(例: タレットや敵用)
@export var auto_fire: bool = false

## 自動発射時のターゲット方向(世界座標)。
## ターゲットがいない場合は base_angle_deg を使います。
@export var auto_target: Node2D

## デバッグ用に発射方向のラインを表示するかどうか
@export var debug_draw: bool = false

## 内部状態:クールタイムタイマー
var _cooldown_timer: float = 0.0


func _process(delta: float) -> void:
    # クールタイムを進める
    if _cooldown_timer > 0.0:
        _cooldown_timer -= delta

    # 自動発射モード
    if auto_fire:
        # ターゲットが設定されていれば、その方向に向けて撃つ
        if auto_target and is_instance_valid(auto_target):
            var dir: Vector2 = (auto_target.global_position - global_position).normalized()
            base_angle_deg = rad_to_deg(dir.angle())
        # 発射を試みる(クールタイムを考慮)
        fire()


func can_fire() -> bool:
    ## 今撃てる状態かどうかを返します。
    if cooldown > 0.0 and _cooldown_timer > 0.0:
        return false
    if not bullet_scene:
        push_warning("ShotgunSpawner: bullet_scene が設定されていません。")
        return false
    return true


func fire(custom_base_angle_deg: float = NAN) -> void:
    ## 散弾を発射します。
    ## custom_base_angle_deg を指定すると、その角度を中心に扇状に発射します。
    ## 指定しない場合は base_angle_deg を使用します。
    if not can_fire():
        return

    var center_angle_deg: float = base_angle_deg
    if not is_nan(custom_base_angle_deg):
        center_angle_deg = custom_base_angle_deg

    # 弾数が1発なら、単純に1方向に撃つだけ
    if bullet_count <= 1 or spread_angle_deg == 0.0:
        _spawn_single_bullet(center_angle_deg)
    else:
        _spawn_spread_bullets(center_angle_deg)

    # クールタイムをリセット
    _cooldown_timer = cooldown


func _spawn_single_bullet(angle_deg: float) -> void:
    ## 単発弾を一発だけ撃つ
    var bullet: Node2D = bullet_scene.instantiate()
    # ShotgunSpawner の親と同じツリーに出したいので、ルートではなく親の親に入れる
    var parent_for_bullet: Node = get_parent() if get_parent() else get_tree().current_scene
    parent_for_bullet.add_child(bullet)

    # 発射位置(ShotgunSpawner の位置 + ローカルオフセットを回転させたもの)
    var angle_rad: float = deg_to_rad(angle_deg)
    var offset_rotated: Vector2 = spawn_offset.rotated(angle_rad)
    bullet.global_position = global_position + offset_rotated

    # 方向ベクトル
    var dir: Vector2 = Vector2.RIGHT.rotated(angle_rad)

    # 弾側に速度プロパティがあれば設定を試みる
    if bullet.has_variable("velocity"):
        bullet.set("velocity", dir * bullet_speed)
    elif bullet.has_method("set_velocity"):
        bullet.call("set_velocity", dir * bullet_speed)

    # RigidBody2D なら初速として linear_velocity を設定
    if bullet is RigidBody2D:
        (bullet as RigidBody2D).linear_velocity = dir * bullet_speed

    # 見た目の向きも発射方向に合わせたい場合
    if bullet is Node2D:
        (bullet as Node2D).rotation = angle_rad

    if debug_draw:
        _debug_draw_ray(bullet.global_position, dir)


func _spawn_spread_bullets(center_angle_deg: float) -> void:
    ## 複数弾を扇状に発射する
    var half_spread: float = spread_angle_deg * 0.5

    # 弾が2発以上のときの角度ステップ
    # 例: spread_angle_deg = 60, bullet_count = 5
    #   -> -30, -15, 0, 15, 30 のように配置
    var step: float = 0.0
    if bullet_count > 1:
        step = spread_angle_deg / float(bullet_count - 1)

    for i in bullet_count:
        var t: float = float(i) / float(max(bullet_count - 1, 1))
        var angle_offset_deg: float = -half_spread + spread_angle_deg * t
        var angle_deg: float = center_angle_deg + angle_offset_deg
        _spawn_single_bullet(angle_deg)


func _debug_draw_ray(origin: Vector2, dir: Vector2, length: float = 32.0) -> void:
    ## シンプルなデバッグ描画(エディタ上では見えません)
    # ここでは簡易的に、Line2D を一瞬生成してすぐ消す形にしています。
    var line := Line2D.new()
    line.width = 1.0
    line.default_color = Color.YELLOW
    line.points = PackedVector2Array([origin, origin + dir.normalized() * length])
    get_tree().current_scene.add_child(line)
    # 0.1秒後に削除
    line.call_deferred("queue_free")

使い方の手順

ここでは、代表的な3パターンを例に使い方を見ていきます。

  1. プレイヤーのショットガン攻撃
  2. 敵の扇状弾幕
  3. 固定砲台(Turret)の自動散弾

手順①:弾シーン(Bullet.tscn)を用意する

まずは「弾」そのものを表すシーンを作ります。シンプルな例として、Area2D ベースの弾を想定します。

Bullet (Area2D)
 ├── CollisionShape2D
 └── Sprite2D

Bullet.gd の中身は最低限こんな感じでOKです(velocity だけ持つシンプルな弾):


extends Area2D

@export var life_time: float = 2.0
var velocity: Vector2 = Vector2.ZERO

func _process(delta: float) -> void:
    position += velocity * delta
    life_time -= delta
    if life_time <= 0.0:
        queue_free()

この velocity プロパティに、ShotgunSpawner 側から速度ベクトルを流し込みます。

手順②:プレイヤーに ShotgunSpawner をアタッチ

次にプレイヤーシーンにコンポーネントを追加します。

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── ShotgunSpawner (Node)

プレイヤーのスクリプト(Player.gd)は、入力に応じて ShotgunSpawner.fire() を呼ぶだけです。


extends CharacterBody2D

@onready var shotgun: ShotgunSpawner = $ShotgunSpawner

func _process(delta: float) -> void:
    # マウスカーソル方向に向けて撃つ例
    if Input.is_action_pressed("shoot"):
        var mouse_pos: Vector2 = get_global_mouse_position()
        var dir: Vector2 = (mouse_pos - global_position).normalized()
        var angle_deg: float = rad_to_deg(dir.angle())
        shotgun.fire(angle_deg)

ShotgunSpawner ノードのインスペクタ設定例:

  • bullet_scene: res://Bullet.tscn
  • bullet_speed: 800
  • bullet_count: 7
  • spread_angle_deg: 45
  • cooldown: 0.15
  • spawn_offset: (16, 0) など、銃口位置

これで、プレイヤーのスクリプト側は「どの程度ばら撒くか」を一切知らず、
「とにかくこの方向に散弾撃って」とコンポーネントに丸投げできます。

手順③:敵に扇状弾幕を持たせる

敵キャラにも全く同じコンポーネントをアタッチできます。

Enemy (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── ShotgunSpawner (Node)

敵のスクリプト(Enemy.gd)では、例えばプレイヤー方向に一定間隔で散弾を撃つ処理を簡潔に書けます。


extends CharacterBody2D

@export var player: Node2D
@onready var shotgun: ShotgunSpawner = $ShotgunSpawner

func _process(delta: float) -> void:
    if not player or not is_instance_valid(player):
        return

    var dir: Vector2 = (player.global_position - global_position).normalized()
    var angle_deg: float = rad_to_deg(dir.angle())
    shotgun.fire(angle_deg)

ShotgunSpawner 側の設定を変えるだけで、

  • 弾数 3 + 角度 20° → 軽い三連ショット
  • 弾数 15 + 角度 120° → 画面を覆う広範囲弾幕

…といったバリエーションを 敵ごとに簡単に切り替えられます

手順④:固定砲台(Turret)の自動散弾

最後に、自動で散弾を撃ち続ける固定砲台を作ってみましょう。

Turret (Node2D)
 ├── Sprite2D
 └── ShotgunSpawner (Node)

Turret.gd はなんと空でもOKです。ロジックはすべて ShotgunSpawner に任せます。

ShotgunSpawner の設定:

  • auto_fire: ON
  • bullet_scene: res://Bullet.tscn
  • bullet_speed: 600
  • bullet_count: 8
  • spread_angle_deg: 90
  • cooldown: 0.5
  • auto_target: プレイヤーノード(任意)

auto_target を設定しておけば、常にプレイヤー方向に向けて扇状に発射してくれます。
設定しなければ base_angle_deg を中心に固定方向へばら撒く「固定砲台」になります。

メリットと応用

ShotgunSpawner コンポーネントを使うことで、

  • 「散弾を撃つ」という機能を、プレイヤー・敵・ギミックから完全に切り離せる
  • ノード階層はシンプルなまま、コンポーネントを付け外しするだけで挙動を変えられる
  • 発射数・角度・クールタイムなどは 継承や条件分岐ではなくインスペクタの数値で調整できる
  • 「プレイヤー専用の散弾クラス」「敵専用の散弾クラス」みたいな継承ツリーの肥大化を防げる

といったメリットがあります。
レベルデザインの段階でも、「この敵だけ弾数を増やそう」「この砲台は角度を広げよう」といった変更が、コードを一切触らずにできます。

さらに、弾シーン側も velocity というシンプルなインターフェイスにしておけば、

  • まっすぐ進む弾
  • 途中で加速する弾
  • 波打つ弾

などを、弾シーンだけ差し替えて再利用できます。
「弾の動き」と「弾のばら撒き方」をコンポーネントで分離できるのが気持ちいいところですね。

改造案:ショットガンを「回転させながら」撃つ

例えばボス戦で、「回転しながら散弾をばら撒く」パターンを作りたい場合は、
ShotgunSpawner にこんなメソッドを追加するだけで実現できます。


func spin_fire(spin_speed_deg: float, delta: float) -> void:
    ## 毎フレーム base_angle_deg を回転させながら散弾を発射する
    base_angle_deg = wrapf(base_angle_deg + spin_speed_deg * delta, 0.0, 360.0)
    fire()

これをボスの _process から呼べば、


func _process(delta: float) -> void:
    $ShotgunSpawner.spin_fire(120.0, delta) # 1秒で120度回転しながら発射

のように、ぐるぐる回転する散弾攻撃がすぐに実装できます。
このように、コンポーネントとして分離しておくと、攻撃パターンのバリエーション追加がとても楽になりますね。