Godotを使ってみよう

はじめまして。
入社1年と半年くらいの者です。
まだ知らないことが多かったり締め切りが次々にやってきたりと、毎日目が回りそうになりながら過ごしています。

さて、今回はOSSのゲームエンジン「Godot」(ごどー)の使い方を見ていきます。  
お仕事とはまったく関係ありません。  
作るものは3DゲームでのHello World!である(と私が思っている)Roll a ballです。  
ボールを操作してフィールドに散らばるコインを集める感じのやつです。

Godotを触るのは初めてだったので、AIの先生方に作ってもらって「オォ!」みたいな記事にしようと思っていたのですが、なかなか難しかったのであきらめました。

インストール ~ プロジェクトを作る

この辺は特に迷わないと思うので飛ばしていきます。
公式サイトからアーカイブをダウンロードして解凍するだけ。
中身の実行ファイルがGodotです。

プロジェクトの作成もまず迷わないでしょう。

  1. 「+New Project」して適当な保存場所を選ぶ
  2. プロジェクト名を指定して「Create Folder」を押す
  3. 「Create & Edit」を押す
  4. エディタが開く

分かりやすいですね。
プロジェクト名は「Roll_a_ball」としました。

画面上のオブジェクトを作る

ここからが本題です。
見た感じ、Godtではプレイヤーや地面などオブジェクトのまとまり1つ分がシーンという扱いのようです。
ゲーム > シーン > 基本的なオブジェクト  のようなメンタルモデルなんでしょうかね。
あんまりよく分かってないですが、雰囲気で進めていきます。
お仕事で作ってるわけではないので怖いものなしです。

地面

まずは地面を作ります。
右上の「3D Scene」を押して「Node 3D」を追加します。
これは何も入っていない、何も設定されていない空っぽのハコです。

このハコに地面としての特徴を足していきます。
Node3Dのコンテキストメニューから「Add Child Node」を選択し、「Create New Node」のダイアログで「MeshInstance3D」を指定します。

Meshって付いてるやつは見た目など、モノのカタチ系のあれですね。たぶん。

そして、InspectorでMeshに「New PlaneMesh」を指定します。

これで「Node 3D」を一枚の平面として視認できるようになりました。

次に実体をつけていきます。
今は目には見えているものの、他のオブジェクトと接触することはありません。
プレイヤーがこの平面に着地することができない状態なのです。

画面上部の「Mesh」から「Create Trimesh Static Body」を選びます。

「StaticBody3D」と「CollisionShape3D」というものが追加されましたね。

これで他のオブジェクトが接地できるようになりました。
Collisionと付いてるものは衝突判定を持つ何かですね。おそらく。  

地面はいったん出来上がりです。
「Ctrl+S」を押すとシーンの保存ダイアログが出ますので、「Plane.tscn」として保存しておきます。  

本当はプレイヤーが地面の端から奈落に転落しないよう四方に壁を作ったりしたいところですが、余白が足りませんので割愛です。

プレイヤー

次にプレイヤーを作ります。

シーン

まずはプレイヤーのシーンを作成します。
「res://」のコンテキストメニューから「Create New > Scene…」を選択します。

「Create New Scene」のダイアログが出ますので、以下を指定して「OK」します。

  • Root Type: RidgitBody3D
  • Scene Name: Player

Rigidと付いているものは硬い実体で物理的な影響を受けるものみたいですね。
加速とか摩擦とか重力とかのあれです。

これでPlayerシーンができました。

次はPlayerのモデルです。
モデルはBlenderなどで作成したモデルデータを取り込んだりアセットを使ったりするのですが、今回のプレイヤーキャラクターはただの球体ですので、組み込みの基本オブジェクトを使うだけです。

地面のときと同じく、Playerのコンテキストメニューから「Add Child Node」を選択して、「MeshInstance3D」を追加します。
そして、MeshInstance3Dの設定で「Mesh」に「New SphereMesh」を指定します。

エディタ上で球が見えるようになりましたね。

衝突判定

Playerも地面やコインと触れ合いたいのでCollisionを付けます。
Playerのコンテキストメニューから「Add Child Node」を選択して、「CollisionShape3D」を追加します。  
そしてCollisionにもSphereの形を与えます。
CollisionShape3Dの設定で「Shape」に「New SphereShape3D」を指定します。
そこにあるモノのカタチが実際に見えている形と同じとは限らないのです。不思議ですね。

あと、ちょっと面倒なのでさぼるためにナゾの設定を1つ追加します。
Playerの設定で「Lock Rotation」というチェックをOnにしておいてください。

とりあえずプレイヤーもこれで出来上がりです。
「Ctrl+S」でシーンを保存します。

コイン

なんだか慣れてきましたね。よし。
シーンを作って、形(Mesh)をつけて、衝突判定(Collision)をつける、です。
そして出来上がったものがこちら。

後から撮ったキャプチャなのでFileSystemに余計なものが映り込んでいますがお気になさらず。。

Meshには「New CylinderMesh」というものを使っています。
ただ、そのままだと厚みがあるのと、寝そべってしまっていますので、以下のようにTransformを設定してコインが立っているような見た目にします。

  • Scale y: 0.1  (平べったくなります)
  • Rotation x: 90  (直立します)

Transformというのはモノのカタチを変えたりするやつです。
設定内容に見られる通り、大きさ、回転、位置などを変えることができます。

くっつけてみる

出来上がったものたちを地面のシーンでくっつけてみましょう。

地面のシーンを開きます。
開いたらPlayer.tscnのコンテキストメニューから「Instantiate」を選択します。

Planeのシーンの中にPlayerを配置できました。
しかし、だいぶ世界が狭いですね。。
地面を広げます。
PlaneのMeshInstance3Dを選択し、TransformでScaleのxとzを30に変更します。

広くなりましたね。
これでいったん動かしてみましょう。
右上の再生ボタンを押すとゲームをデバッグ実行できます。
何か聞かれますが「Select Current」で大丈夫です。  

虚無。

カメラ

カメラを忘れていました。
ゲームの中の世界はカメラを通して見ることになるので、カメラがないと何も見えません。
今回はプレイヤーである球に追従する感じにします。

まずPlayerのシーンを開きます。
Playerのコンテキストメニューから「Add Child Node」を選択して、「Camera3D」を追加します。  

カメラが追加されました。
エディタ上で紫の枠が向いている方向がカメラに映る範囲を示しています。
上にくっついてる△は何でしょうね。「こっちを上にしてるよ」の記号かな?

ただ、今はカメラが球の中心にめり込んでいるのでプレイするときに見づらいです。
(エディタ上の「Preview」をチェックすると、このカメラを通して世界を見ることができます)  

カメラを球の後ろ上方に移動させ、軽く見下ろすように角度を調整してみましょう。
カメラのTransformで以下のように設定します。

  • Position (x, y, z): (0, 2, 2)
  • Rotation (x, y, z): (-20, 0, 0)

もう一度くっつける

地面のシーンに戻って再生してみましょう。

まだ何か変ですね。。
世界がほとんど灰色なのと、どうやら落下しているようです。

灰色を直す

一面灰色なのは、光がないために世界が暗黒に包まれてしまっているという状態です。

地面のシーンで、エディタ上部の縦の「…」(表現しづらい)から「Add Sun to Scene」を選択します。エディタ上だと分かりづらいですが、これで太陽が追加されます。
もう一度縦の「…」を開き、「Add Environment to Scene」を選択します。
これで青空が見えるようになります。

これで光ができたので良しとします。

落下を直す

落下しているのは、Playerが地面にめり込んでいるために接地判定が起きていない、という状態です。
これは直すの簡単そうですね。
PlayerのTransformでPosition yを1にしてあげます。
これでPlayerが少し上に配置されるようになり、地面に着地できるようになります。

これで実行してみましょう。

よさそうですね。
ついでにCoinもいくつかInstantiateしておきます。

スクリプト

あと少しです。
ここまでで見た目はなんとかなりましたが、まだPlayerを動かすことができません。
オブジェクトにスクリプトをくっつけることで動かせるようにしていきます。
Godotには組み込みの言語としてGDScriptというものがありますので、今回はこれを使ってみます。

プレイヤー側

とりあえず、矢印キーで動けるようにしましょう。
マウス操作で向きを変えられるようにもしたいですね。

Playerのシーンを開き、Playerのコンテキストメニューから「Attach Script」を選択します。
何か聞かれますが、「Create」で大丈夫です。

エディタが開きますので↓のスクリプトをコピペします。
(シンタクスハイライトがないと見づらいな。。)

extends RigidBody3D

# 他のスクリプトからPlayerを参照できるようにします。
class_name Player

# y軸方向に対する回転を加えるためのオブジェクトです。
# 最初、直接Playerに回転を加えたらなんかうまくいかんかったので間にこれを挟んでます。
@onready var yaw := $Yaw

# y軸に対する回転方向の入力値を保持しておきます。
var yaw_input := 0.0

# Called when the node enters the scene tree for the first time.
func _ready():
    # マウスカーソルをゲーム内にキャプチャします。
    # キャプチャすることでマウスの入力値が扱えるようになります。
    Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)

# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
    move_player(delta)
    rotate_player(delta)
    reset_mouse_capture_at_ECS()

# なんか組み込みのメソッド。よくわからん。
func _unhandled_input(event: InputEvent) -> void:
    # マウスのx軸方向の移動をPlayerの回転として利用するために保存します。
    # 入力値は大きすぎるので1000分の1してます。
    if event is InputEventMouseMotion:
        if Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
            yaw_input = - event.relative.x * 0.001

# 移動
func move_player(delta) -> void:
    # 矢印キーによる左右の入力をx軸(左右方向)、上下をz軸(前後方向)にマッピングします。
    # これがPlayerを動かす力の元になります。
    var input := Vector3.ZERO
    input.x = Input.get_axis("ui_left", "ui_right")
    input.z = Input.get_axis("ui_up", "ui_down")

    # マッピングした力をRigidBodyに加えます。
    #    $Yaw.basis: 現在のYawオブジェクトのベクトルに対して力を加えます。
    #               (Yawの背中を押すように力を加える、と言えばよいか)
    #    input: 前後左右の入力値を反映した力を加えます。
    #    1000.0: しかし、このRigidBodyを動かすにはinputの値だけでは足りないので、1000倍します。
    #    delta: 前回のフレーム描画からの経過秒数です。たぶん。
    #            ゲームを実行する機器の性能によって1秒当たり何回画面を描画できるか(fps)が変わります。
    #            前回描画からの経過時間を乗算することで、経過した時間分の移動に必要な値が求まり、
    #            どの機器であっても適切な移動後の位置に描画することができるようになります。
    apply_central_force($Yaw.basis * input * 1000.0 * delta)

# 振り向き
func rotate_player(delta) -> void:
    # カメラにマウスカーソル移動分の回転を加えます。
    # 回転を加えたままにするとグルグル回ってしまうので、すぐに0.0を入れて次のフレームで止まるようにします。
    yaw.rotate_y(yaw_input)
    yaw_input = 0.0

# ESCキーを押すことでマウスカーソルのキャプチャを解除できるようにします。
# ゲームウィンドウを閉じるときに必要になります。
func reset_mouse_capture_at_ECS() -> void:
    if Input.is_action_just_pressed("ui_cancel"):
        Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)    

# 他のオブジェクトとの当たり判定です。
# コインと接触した場合はそのコインを取り除きます。
func _on_body_entered(body):
    print(body.name)
    if body is Coin:
        body.queue_free()

コイン側

次にCoinのシーンを開き、同様にスクリプトを追加します。
こちらは「Coin.gd」になります。

pythonextends RigidBody3D

# Playerとの接触判定に使うために名前が必要になります。
# オブジェクトの名前の方だけで何とかならんものか。。
class_name Coin

イベントの通知設定

イベントの発生を監視し、スクリプトに通知する設定を追加します。
キーボード入力などは入力機器からのイベントなのでデフォルトで監視していますが、ゲームオブジェクト同士の接触といったイベントは個別に設定してあげる必要があるようです。

まずは、Playerの設定で以下を設定します。

  • Max Contacts Reported: 1
  • Connect Monitor: On

これはイベントの発生を監視対象にするかどうか(たぶん)と、Max~は何でしょうねこれ。
とりあえず、ここが0だったりOffだったりするとオブジェクト同士が接触しても通知が行われず、イベント処理のためのメソッドが呼ばれません。

次に、イベントとイベント発生時に呼ばれるメソッドを紐づけます。
Playerの設定のNodeタブを開き、「RigidBody3D > body_entered(body: Node)」のコンテキストメニューから「Connect…」を選択します。  

何か聞かれますが、そのままOKで大丈夫です。  

これで、イベント発生時に先ほどコピペしたスクリプトの_on_body_enteredが呼び出されるようになります。  

やったか

これで動くようになったはずですが、どうでしょうか。。

やり切った。よかった。
お疲れさまでした。

このままではまだゲーム性のカケラもないですが、手始めとしてはイイ感じなのではないでしょうか。
ここから、

  • 全てのコインを回収するまでのタイムアタックにしたり、
  • 次のコイン取得までの所要時間に応じて得点のレートが上がるスコアアタックにしたり、
  • エネミーを追加したり、
  • 自機を白黒反転できるようにして同じ色のコインに接触したら得点、違う色なら1ミス(いいゲームですね!)にしたり、

のようにいろいろ付け加えていくこともできます。

最後に

今回はGodotを使ってRoll a ballを作ってみました。
普段はお仕事で業務アプリ的なものを作っていますが、ゲームは業務アプリとは違ったパラダイムに触れることになるので新鮮です。
性能面がシビアで10fpsを割り込んでしまいまともに動かせない、昔別れてそれきりだった三角関数や微分積分と偶然再会(そして勉強し直し)、といったいろいろなイベントもあります。
ということで、気分転換にゲーム作り、いかがでしょうか。

みなさまの過ごす日々が実り多いものでありますように。