4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Godot 4.x】3D脱出ゲームを作ってみる②〜FPSプレイヤーの実装(バーチャルジョイスティック+スワイプ操作)〜

4
Last updated at Posted at 2026-07-02

はじめに

この記事は、Godot 4.xでスマホ横向き向けFPS視点の3D脱出ゲームを作る連載の第2回です。

前回までの記事はこちら:

この記事でやること

部屋を配置して、バーチャルなジョイスティックでFPS視点で移動、画面右側をスワイプすることで視点を回転、までの実装となります

  • bed_scene1.tscn を使った寝室シーンの配置
  • CharacterBody3D + Camera3D によるFPSプレイヤーの作成
  • 左側バーチャルジョイスティックで移動
  • 右側スワイプで視点回転

スクリーンショット 2026-07-02 13.50.52.png

想定環境

項目 内容
OS macOS(Apple Silicon)
Godot 4.7 stable
Claude Code 最新版
godot-mcp セットアップ済み

0. claudeにプランを立ててもらう

3D脱出ゲームを作ってみる①、の続きから

プロンプト:
実装を始めます

質問:
3D脱出ゲームのカメラ操作スタイルはどちらにしますか?

  • 固定視点(ポイント&クリック):プレイヤーは動かず、クリックで視点を切り替える昔ながらの脱出ゲームスタイル。実装がシンプルで記事向き
  • 自由移動(FPS/TPS):WASDやクリックで部屋を歩き回れる。より没入感があるが実装が複雑

回答:
自由移動(FPS/TPS):WASDやクリックで部屋を歩き回れる。より没入感があるが実装が複雑

プロンプト:
それでは、FPSプレイヤーの実装CharacterBody3D + Camera3D、バーチャルジョイスティック+スワイプで操作、を実装します

1. 寝室シーンの配置

前回インポートした Low Poly Bedroom Asset Packbed_scene1.tscn を使います。このシーンにはベッド・ドレッサー・ランプなどが一式まとめて配置されています。

1-1. メインシーンの準備

scenes/main.tscn を開き、以下のノード構成を作ります。

Main (Node3D)
├── WorldEnvironment        // 前回作成済み
├── DirectionalLight3D      // 前回作成済み
├── Camera3D                // 前回作成済み
├── BedRoom                 // (bed_scene1.tscnのインスタンス)
└── Player                  // (CharacterBody3D) ← 今回作成

1-2. bed_scene1.tscn のインスタンス化

前回、ベットを配置していたら削除しておいてください

  1. FileSystemパネルで assets/models/bedroom/Scenes/bed_scene1.tscn を探す
  2. シーンにドラッグ&ドロップ、またはメニューから シーン > インスタンスを追加
  3. ノード名を BedRoom にリネーム
  4. Transformの Position・Rotation を調整して部屋の中心に配置

スクリーンショット 2026-07-02 12.20.42.png

今回、下記のようにしました
スクリーンショット 2026-07-02 12.24.29.png

bed_scene1.tscn にはコリジョン(当たり判定)が含まれていない場合があります。その場合は各家具に StaticBody3D + CollisionShape3D を手動で追加するか、後述のプレイヤー実装後に動作確認しながら対応してください。


2. FPSプレイヤーシーンの作成

プレイヤーは独立したシーンとして作成し、メインシーンにインスタンスとして追加します。

2-1. 新規シーンの作成

  1. シーン > 新規シーン
  2. ルートノードを生成 > その他のノードで、[Character]で検索して CharacterBody3D を選択
  3. ノード名を Player にリネーム
  4. scenes/player.tscn として保存

スクリーンショット 2026-07-02 12.27.00.png

スクリーンショット 2026-07-02 12.29.08.png

2-2. ノード構成

Player (CharacterBody3D)
├── CollisionShape3D     # プレイヤーのコリジョン(カプセル形状)
├── Camera3D             # FPS視点カメラ
└── RayCast3D            # インタラクション用(④で使用)

CollisionShape3D の設定

  • Shape プロパティで「新規 CapsuleShape3D」を選択
  • Radius: 0.4、Height: 1.8(人の体型に近いサイズ)

Camera3D の設定

  • Position を (0, 0.8, 0) に設定(カプセルの上部=目線の高さ)
  • CurrentOn にする(このカメラをメインカメラとして使用)

claude code + mcp でプロンプトに上記を投げると作ってもらえます


3. プレイヤースクリプトの実装

Player ノードに GDScript を追加します。

3-1. スクリプトの全体像

extends CharacterBody3D

# 移動速度
const SPEED = 4.0
const GRAVITY = -9.8

# スワイプ視点回転の感度
const LOOK_SENSITIVITY = 0.005

@onready var camera = $Camera3D

# スワイプ追跡用
var _look_touch_index: int = -1
var _look_last_pos: Vector2 = Vector2.ZERO

func _ready():
    # マウスカーソルを非表示(エディタでのテスト用)
    # スマホでは不要だが、デスクトップテスト時に有効
    # Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
    pass

func _physics_process(delta):
    # 重力
    if not is_on_floor():
        velocity.y += GRAVITY * delta

    # 移動入力(バーチャルジョイスティックから受け取る)
    var move_dir = Vector2.ZERO
    move_dir.x = Input.get_axis("move_left", "move_right")
    move_dir.y = Input.get_axis("move_forward", "move_back")

    if move_dir != Vector2.ZERO:
        var direction = (transform.basis * Vector3(move_dir.x, 0, move_dir.y)).normalized()
        velocity.x = direction.x * SPEED
        velocity.z = direction.z * SPEED
    else:
        velocity.x = move_toward(velocity.x, 0, SPEED)
        velocity.z = move_toward(velocity.z, 0, SPEED)

    move_and_slide()

func _input(event):
    # スワイプ(右側タッチ)で視点回転
    if event is InputEventScreenTouch:
        _handle_touch(event)
    elif event is InputEventScreenDrag:
        _handle_drag(event)

func _handle_touch(event: InputEventScreenTouch):
    var screen_center_x = get_viewport().get_visible_rect().size.x / 2.0
    if event.pressed and event.position.x > screen_center_x:
        # 画面右半分のタッチ開始 → 視点操作として追跡
        _look_touch_index = event.index
        _look_last_pos = event.position
    elif not event.pressed and event.index == _look_touch_index:
        _look_touch_index = -1

func _handle_drag(event: InputEventScreenDrag):
    if event.index != _look_touch_index:
        return
    var delta = event.position - _look_last_pos
    _look_last_pos = event.position

    # 左右:プレイヤー本体をY軸回転
    rotate_y(-delta.x * LOOK_SENSITIVITY)
    # 上下:カメラのX軸回転(上下限あり)
    camera.rotation.x = clamp(
        camera.rotation.x - delta.y * LOOK_SENSITIVITY,
        deg_to_rad(-80),
        deg_to_rad(80)
    )

Input.get_axis("move_left", "move_right") はInputMapのアクション名です。次のステップでバーチャルジョイスティックのアクションを設定します。

ここも、このスクリプトをclaude code + mcp に投げてplayer.gdを作成してもらいました
「Player ノードに GDScript を追加します。」の一文を添えておくと安心です

スクリーンショット 2026-07-02 12.37.53.png


4. InputMapの設定

バーチャルジョイスティックから移動方向を受け取るため、プロジェクト設定 > インプットマップ にアクションを追加します。

  1. プロジェクト > プロジェクト設定 > インプットマップ タブを開く
  2. 「アクションを追加」で以下の4つを追加(キー割り当ては任意)
アクション名 キー(PC テスト用)
move_forward W
move_back S
move_left A
move_right D

「新しいアクションを追加」
スクリーンショット 2026-07-02 12.43.47.png

追加したアクションの右側の「+」をクリックして、「W」を入力
スクリーンショット 2026-07-02 12.45.57.png

最終的に
スクリーンショット 2026-07-02 12.48.44.png

スマホ実機ではバーチャルジョイスティックから Input.action_press() でこれらのアクションを呼び出します。PCでのテスト時はWASDキーで代用できます。


5. バーチャルジョイスティックの実装

画面左側に表示するバーチャルジョイスティックをUIシーンとして作成します。

5-1. UIシーンの作成

  1. シーン > 新規シーン
  2. ルートノードに Control を選択、名前を VirtualJoystick にリネーム
  3. scenes/ui/virtual_joystick.tscn として保存

スクリーンショット 2026-07-02 12.50.00.png

スクリーンショット 2026-07-02 12.50.54.png

5-2. ノード構成

VirtualJoystick (Control)
├── Base (TextureRect)   # ジョイスティックの台座
└── Stick (TextureRect)  # 動くスティック部分

Control の設定

  • アンカープリセット を「全画面」に設定
  • マウスフィルターIgnore に設定(右側スワイプを妨げないため)

Base(台座)の設定

  • ColorRectを使用
  • Position: (80, 画面高さ - 200) 程度(左下に配置)
  • Size: (160, 160)
  • Color: Color(1, 1, 1, 0.3)(半透明の白)

Stick(スティック)の設定

  • ColorRectを使用
  • Base の中心に重なるように配置
  • Size: (80, 80)
  • Color: Color(1, 1, 1, 0.6)(やや濃い半透明の白)

こちらもclaude code + mcp のプロンプトに投げて作成してもらうと楽です

今回は ColorRect で代用していますが、後から円形テクスチャの TextureRect に差し替えることで見た目をブラッシュアップできます。

スクリーンショット 2026-07-02 12.59.35.png

5-3. ジョイスティックスクリプト

VirtualJoystick に以下のスクリプトを追加します。

extends Control

@onready var base = $Base
@onready var stick = $Stick

var _touch_index: int = -1
var _base_pos: Vector2 = Vector2.ZERO
const MAX_DISTANCE = 60.0

func _ready():
    _base_pos = base.position + base.size / 2

func _input(event):
    if event is InputEventScreenTouch:
        _on_touch(event)
    elif event is InputEventScreenDrag:
        _on_drag(event)

func _on_touch(event: InputEventScreenTouch):
    var screen_center_x = get_viewport().get_visible_rect().size.x / 2.0
    if event.pressed and event.position.x < screen_center_x:
        # 画面左半分のタッチ → ジョイスティック操作
        _touch_index = event.index
    elif not event.pressed and event.index == _touch_index:
        _touch_index = -1
        _reset_stick()

func _on_drag(event: InputEventScreenDrag):
    if event.index != _touch_index:
        return

    var offset = event.position - _base_pos
    if offset.length() > MAX_DISTANCE:
        offset = offset.normalized() * MAX_DISTANCE

    stick.position = _base_pos - stick.size / 2 + offset

    # 移動アクションを入力に反映
    var ratio = offset / MAX_DISTANCE
    if ratio.x < -0.3:
        Input.action_press("move_left")
    else:
        Input.action_release("move_left")
    if ratio.x > 0.3:
        Input.action_press("move_right")
    else:
        Input.action_release("move_right")
    if ratio.y < -0.3:
        Input.action_press("move_forward")
    else:
        Input.action_release("move_forward")
    if ratio.y > 0.3:
        Input.action_press("move_back")
    else:
        Input.action_release("move_back")

func _reset_stick():
    stick.position = _base_pos - stick.size / 2
    Input.action_release("move_left")
    Input.action_release("move_right")
    Input.action_release("move_forward")
    Input.action_release("move_back")

Claudeのクレジット・セッションを節約したい場合は自身で作成しても良いと思います

スクリーンショット 2026-07-02 13.00.19.png


6. メインシーンへの組み込み

作成した PlayerVirtualJoystick をメインシーンに追加します。

6-1. Player の追加

  1. scenes/main.tscn を開く
  2. シーンパネルの Main ノードを選択した状態で、チェーンアイコン(🔗)「子シーンをインスタンス化」をクリック
  3. scenes/player.tscn を選択して「開く」
  4. Position を部屋の入口付近に設定(例:(0, 1.0, 2.0)

スクリーンショット 2026-07-02 13.06.48.png

Positionを調整したい場合は3Dモードが楽です
スクリーンショット 2026-07-02 13.08.14.png

スクリーンショット 2026-07-02 13.08.58.png

チェーンアイコンはシーンパネル上部のツールバーにあります。または Main ノードを右クリック →「子シーンをインスタンス化」でも同じ操作ができます。

6-2. VirtualJoystick の追加

UIはCanvasLayerを介して追加します。

Main (Node3D)
├── WorldEnvironment
├── DirectionalLight3D
├── BedRoom
├── Player
└── CanvasLayer          ← 追加
    └── VirtualJoystick  ← インスタンスを追加
  1. MainCanvasLayer ノードを追加
  2. CanvasLayer を選択した状態でチェーンアイコン(🔗)をクリックし、scenes/ui/virtual_joystick.tscn を選択

スクリーンショット 2026-07-02 13.10.33.png

スクリーンショット 2026-07-02 13.11.11.png

スクリーンショット 2026-07-02 13.11.57.png

CanvasLayer を使うことで、UIが3Dカメラの動きに影響されず画面上に固定表示されます。


7. 動作確認

PCでタッチ操作をテストする場合:
プロジェクト > プロジェクト設定 > 入力デバイス > ポインティング > マウスからタッチをエミュレートON にしてください。これを有効にするとマウス操作がタッチイベントとして扱われ、ジョイスティックと右側スワイプが動作するようになります。

スクリーンショット 2026-07-02 13.18.38.png

プロジェクトを実行し、以下を確認します。

  • 寝室(bed_scene1.tscn)が表示される
  • 左下のバーチャルジョイスティックで移動できる
  • 画面右側のスワイプで視点が回転する
  • 上下視点が -80° 〜 +80° にクランプされている
  • 家具に当たって止まる(コリジョンが効いている)

マウスやキーボードが反応しない場合は、実行ウィンドウの左上の「入力」をONにしてみてください

スクリーンショット 2026-07-02 13.42.55.png

実機確認はAndroid/iOS向けにエクスポートが必要です。エディタ上でのテストはWASDキー+マウスドラッグで代用できます。

iOS実機確認方法はこちら


成果物

スタート!
スクリーンショット 2026-07-02 13.50.32.png

バーチャルジョイスティックやキーボードのWASDキーでプレイヤー視点でカメラが動きます
スクリーンショット 2026-07-02 13.50.52.png

右側の領域をドラッグすると視点を切り替えられます
スクリーンショット 2026-07-02 13.51.10.png

まとめ

今回は以下を実装しました。

  • bed_scene1.tscn をメインシーンに配置
  • CharacterBody3D + Camera3D でFPSプレイヤーを作成
  • 画面右側スワイプで視点回転(上下クランプあり)
  • 画面左側バーチャルジョイスティックで移動
  • CanvasLayer でUIを3D空間上に固定表示

連載リンク

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?