TUT Advent Calendar 2024の15日目の記事です!
技科大生による記事です。別に技科大とは関係ないです()
昨日14日の記事は、『「広報サポーター」としての活動を振り返って』でした。
いつも楽しいツイートをされてる技科大のTwitterアカウントの舞台裏が見られて面白かったです!
1. はじめに
2023年9月、Unityがインストール数に応じた利用料金の変更(Unity Runtime Fee)を発表しました。この発表は世界中のゲーム開発者から大きな反発を呼びました。
そして、Unityから乗り換える先として筆頭候補の1つとなったのがGodotです。
あまりポジティブなきっかけではありませんが、それからGodotは初心者・インディー向けゲームエンジンとして紹介されることが多くなってきました。
2024年9月、UnityはRuntime Feeを撤回しました。そんな今、他のゲームエンジンの利用者は、Godotに挑戦するにも決定打に欠けると感じる方も多いのでは?
この記事では、Godotをこよなく愛する中級者が、Godotの素晴らしさを布教することを目的としています。
まずGodotのいいとこを列挙し、それからGodotで小さいゲームを作ります。また、自分が初心者の頃に知りたかったこともまとめました。
注意事項:正しい情報はご自身でご確認ください。
2. Godotとは?
Godotは、「ゴドー」と読みます。(ゴードットだと思ったでしょ、仲間です🤝)
Godotは現在Githubで、OSSとして公開されています。
Godotはもともとクローズドソースでしたが、2014年にオープンソース化されました。
参考
2024年12月15日現在、Godotは完全に無料で利用することができます。
Webサイトを覗いて、ダウンロードボタンをポチるだけです。
(Godotのアイコンを元にしたキャラクターGobot君。かわいい。License)
使用例
とにかく、Meの作りたいゲームは作れるのかい?という声が聞こえるので、有名な使用例を上げておきます。
- ソニック・カラーズ:アルティメット
-
Unrailed 2:バック・オン・トラック
ソース:https://godotengine.org/article/dev-snapshot-godot-4-4-dev-4/ - Robot Detour (体験版ありで、おすすめ)
※ソニックとUnrailed2は、シーン・ノードの仕組みは使わずにRenderingServer(後述)のみ使っていると思われます。
いいとこ
とにかく軽い
起動の速さは、一度体験してみてほしい程です。Godotは一線を画しています。
Godotのサイトで落としたzipファイルを解凍すると、100MBくらいのexeが出てきます。インストールいらず。
起動は新規プロジェクトなら数秒、大きいプロジェクトでも、20秒以上かかることはめったにありません。
またGodotエディタは、Godotエンジンを使って作られています。Godotエンジンが軽くなるほど、Godotエディタも軽くなるというわけです。
シンプルな設計
Godotは比較的新しいゲームエンジンで、よりシンプルな設計になっているように思います。(それ故に、なれるのに時間がかかる場合もあるかもしれません。)
例えば、Godotでは1つのNode(UnityのGameObject)を親ノードとするノードツリーを、一つの「Scene(シーン)」としてファイルに保存することができます。
また、1つ1つの画面をSceneとして保存しておき、画面遷移のときは画面に表示するSceneを切り替えます。
つまりGodotでは、SceneをPrehubとしても使ってくださいねということです。シンプル~~
Godotは、Addonで追加できるような機能をあえて実装しない方針を取っています。とにかくシンプルに纏まったゲームエンジンです。
複数言語に対応
いろいろ選べて嬉しい。
-
GDScript
Pythonに似た独自言語で、最も簡潔に書ける。まだ最適化不足で、C++で書くよりは遅い。
Godotエディタ内でオートコンプリート付きのGDScriptエディタが使える。
完全ではないが静的型付けにも対応している。(型ヒントとかではなく、ちゃんと実行速度に影響する。)オススメ。 -
C#
静的な型システムを使える。
インデントでブロックを作るのに耐えられない人にはオススメ。 - C++, Rust, など(GDExtension)
使おうと思えば使える。特に重い処理だけC++で書くなどしたほうが良いと思う。
拡張機能
Godotは、Assetとして多くのプラグインが公開されています。(GodotエディタがGodotで作られているので、そのプラグインもGodotで作れるのです)
Godot4.xでは、3.xにあったVisualScriptingの機能がなくなっていますが、
有志のプラグイン(Orchestrator, BlockCode, Hengoなど)によって、ノードをつなぐタイプやブロックを積むタイプのVisualScriptingを使うことができます。
オープンソース
中身がどうなっているのか、実際にコードを見て確かめることが出来ます。また、見つけたバグを自分で修正したり、新しい機能を追加することも出来ます。
ついでに、太っ腹なことに、無料です。
gitで管理しやすい
シーン含めほとんどのデータがテキストで管理されているので、gitと相性がいいです。
よくないとこ
まず、他のゲームエンジンと比べ、機能が限られています。
例えばUnreal Engineに比べてグラフィックは劣ります。また、Assetの豊富さはUnityには敵いません。
日本語ドキュメントは公式のものがある程度使えますが、ところどころ英語です。わりと忍耐強さが必要になる時もあります。
さらに、Godotは破壊的な変更を加えるバージョンアップがたまにあります。すぐにネットの情報が古くなるので、公式ドキュメントを読む力が必要です。
3. 小さいゲームを作る
基礎的なプログラミングの知識と検索力が必要です。
とりあえず動く小さいゲームをお見せします。
「クリックでボールが出現し、2つの同じ大きさのボールがくっつくと、1段階大きいボールになる」というゲームです。
(なぜかボールが果物に見えてくる...)
Ball.gdを作成し、BallシーンのRigidBody2Dにアタッチします。
#Ball.gd
#解説のために、詳細なコメントを加えています。
extends RigidBody2D
class_name Ball
# _ball_sizeは、最小が0で、9まで大きくなる。9の次は0になる。
var _ball_size: int = 0
const MAX_SIZE = 10 # 後から変更できない変数(定数)
func _calc_scale(size: int) -> Vector2:
return Vector2(1, 1) * (0.5 + size * 0.2)
func _set_scale(_scale: Vector2):
for child in get_children():
child.scale = _scale
func _next_ball_size(size: int) -> int:
return (size + 1) % MAX_SIZE
# 別のスクリプトから呼ぶとき、最初に実行してもらう関数
func initialize(ball_size: int):
_ball_size = ball_size
_set_scale(_calc_scale(_ball_size))
func _on_body_entered(body: Node): # 何かとぶつかったとき
# ぶつかった相手がBallのときだけ通す
if not is_instance_of(body, Ball):
return
# 自分のインスタンスIDのほうが大きいときだけ通す
if get_instance_id() < body.get_instance_id():
return
# _ball_sizeが等しいときだけ通す
if _ball_size != body._ball_size:
return
_ball_size = _next_ball_size(_ball_size) # 次の_ball_sizeに更新
position = (body.position + position) / 2 # 自分と相手の中点に移動
_set_scale(_calc_scale(_ball_size)) # 自分の大きさを変更
body.queue_free() # 衝突相手を削除
インスペクタで、RigidBody2Dのノードのmax_contacts_reportedを1、ContactMonitorをオンにします。
RididBody2Dのノードから、自分自身に対して「body_entered」シグナルを繋ぎます。
次に、このようなStageシーンを作ります。
(名前がStageのノードは、Node2Dです。)
3つのCollisionShape2Dを使って、器を作ってください。
CollisionShape2Dを作り、ShapeをRectangleShape2Dに変更すると、長方形のコリジョンが出現します。
1つのCollisionShape2Dを複製すると、1つのCollisionShape2Dの変更が3つとも全てに反映されてしまうかもしれません。そのときは、変更したいCollisionShape2DのShapeを右クリックし、「ユニーク化」をしてください。
とりあえず動けばいいので、デバッグからコリジョン形状を表示をオンにしてください。
Stage.gdを作成し、ノードStageにアタッチします。
#Stage.gd
extends Node2D
@onready var ball_scene = preload("res://Ball.tscn")
# 毎フレーム呼ばれる関数
func _process(_delta):
if Input.is_action_just_pressed("click"):
var new_ball: Ball = ball_scene.instantiate()
new_ball.initialize(randi_range(1, 3))
new_ball.position = get_viewport().get_mouse_position()
add_child(new_ball)
インプットマップで、"click"イベントを登録してください。登録するキーは左クリックです。メインシーンをStage.tscnとして起動してください。
できた~
例のくだものゲームが、基礎の部分はこんなに簡単に作れちゃいます。
コメントと空白を除くと、たった33行です。プログラミングが不慣れでも、1行ずつ見ていけば必ず理解できます!
4. 初心者の頃知りたかったこと
Godotがどんなふうに動いているのかを中心に、初心者の頃の僕が知りたかったことを解説します。
4章は、他の章に比べて前提知識が多いです。よく分からなかったら飛ばしてください。
画面に描画される仕組み
Godotは、起動するとツリー(SceneTree)を一つだけ生成します。
このツリーは、get_tree()
で取得できます。
SceneTreeのルートノードはViewportで、その子ノードとしてメインシーンのツリーが追加されます。
SceneTreeのルートノードはget_tree().root
で取得できて、中身を確認するとViewportが入っています。
Viewportには、そのViewportの子のツリーの状態を見て画像を生成します。
例えば、
SceneTree
Viewport
|-Stage(Node2D)
|-Ball0(RigidBody2D)
|- ...
|-Ball1(RigidBody2D)
|-...
このようなツリーがあれば、Viewportは「Ball0のpositionが(0, 0)で、Stageのpositionが(1,1)だから...」といった感じで確認し、それに応じて画面に表示するための画像を生成します。
起動時に自動追加されるルートノードのViewport以外に、自分で別のViewportを作ることもできます。自分でViewportを作った場合、それ単体では画面に画像を表示できません。適当なTextureに作ったViewportを指定することで表示できます。
(...本当は、画像を生成しているのはViewportではなくRenderingServerです。RenderingServerは、描画に関するOSごとの違いを吸収するための低レベルな関数を備えるオブジェクトです。)
継承
例えば、RigidBody2Dは、 RigidBody2D < PhysicsBody2D < CollisionObject2D < Node2D < CanvasItem < Node < Object
という継承の関係になっています。
「継承」と言うからには当然ですが、RigidBody2Dは、継承元のObject~PhysicsBody2Dの全ての関数・属性を使うことができます。
これは、Godotのユニークな部分です。
Unityの場合は、GameObjectにアタッチされたコンポーネントの一つであるTransformクラスに位置を表す属性がありますが、
Godotの場合は、Node2D, Node3Dを継承する全てのノードが位置を表す属性positionを持っています。
クラスが持つネイティブ関数は基本オーバーライドできません。
例外として、_ready()
関数や_process()
関数などのバーチャル関数はオーバーライドすることができます。
設定でオーバーライド出来るようにすることはできます。
ただし、自分で作ったスクリプトから呼び出すときだけ関数の挙動が変わります。Godotエンジンが呼び出したときの挙動は変えられません。
アタッチ
GDScriptなどのスクリプトをノードにアタッチすると、ノードの機能を「拡張」することができます。
アタッチできるのはノードのクラスか、その基底クラスのスクリプトのみです。
一つのノードに一つまでアタッチできます。
シーンの継承
ややこしいことに、Godotにおける「継承」は2つの意味があります。
一つは、クラスを継承するときの「継承」。もう一つは、シーンを継承するときの「継承」です。
シーンを継承するとはどういう意味かと言うと、あるシーンを元に、別のシーンを作るという意味です。
例えば、Weaponシーンを作った後、Weaponシーンを継承してSwordシーンを作ることができます。
WeaponシーンのルートノードにWeaponクラスのスクリプトをアタッチして、
SwordシーンのルートノードにWeaponクラスを継承したSwordクラスのスクリプトをアタッチすることもできます。
(アタッチしないこともできます。)
継承シーンを作った後に継承元のシーンを変更すると、継承シーンにも反映されます。ただし、継承シーンで変更した属性や追加したノードは変更されません。
クラスの継承とだいたい同じと思っておけばOKです。
シグナル
独自解釈が含まれます
GDScriptには、オブジェクト同士の密結合を避けるために、シグナルという仕組みが用意されています。
シグナルはクラスの一種で、関数をSignal.connect(Callable)
で登録しておくと、Signal.emit()
でその関数を実行できます。
例えば敵全体にダメージを与えるとき、味方側のスクリプトから敵が持つ関数を呼び出すことは、可能です。
しかしそれは、「味方側のスクリプトが敵側の持つ関数に依存している」ということになります。こういった依存の関係がコードに点在すると、バグの原因が特定しづらくなったり、プログラムを追いづらくなったりします。
そこで、別オブジェクトへの依存を、シグナルに集約させます。(オブザーバーパターンといいます。)
味方のスクリプトにattacked
シグナルを定義し、敵一体ずつの_on_attacked()
関数に接続します。
味方の関数attack()
関数の中でattacked.emit()
を実行すると、敵全体の_on_attacked()関数が実行されます。
(オブザーバーパターンの文脈では、attack
がPublisher, attacked
がObservable、_on_attacked
がSubscriberです。)
参考:https://zenn.dev/morinokami/books/learning-patterns-1/viewer/observer-pattern
非同期処理 await
GDScriptは、すべての関数を非同期関数として扱います。
普通の言語で置き換えるなら、すべての関数にasyncが付いているようなもの、ということです。
await を含む関数をawaitなしで実行すると、関数内のawaitの行を 待たずに 次の行に進みます。
6行目で"0"を出力し、awaitなしでtest関数を実行しています。
直感的には、その後17行目で"1"、18行目で1秒待ってから19行目で"2"を出力しそうなものです。
しかし、先ほど説明したように、awaitを含む関数をawaitなしで実行すると、関数内のawaitが終わるのを 待たずに 次の行へ進みます。
その結果、17行目で"1"を出力した後、18行目で1秒待たずに8行目に進みます。
"3"を出力し、10行目で5秒待ちます。
その間に18行目の実行が終わり、19行目に移って"2"を出力し、10行目に戻ります。
その後は直感的に進むので説明は省きます。
ちなみに、await Signal とするとシグナルの発火を待機できます。
5. まとめ
いかがでしたか?Godotを使ってみたくなりましたか?
敷居が低く、実績もできつつあります。知れば知るほど好きになるGodot。ぜひ一度触ってみてほしいです。
また、ノードやシグナルなどの独特な部分に躓いたら、きっとこの記事が役に立ちます。
公式ドキュメントにも、英語ですが丁寧に解説があります。困ったときはそちらもご確認ください。
参考になれば嬉しいです!
間違い等ありましたら、コメントしていただると助かります。
明日 16日の記事は、ひじきさんの「強い人類と人間関係」です。
全く内容が想像つかない...楽しみです!!
2024/12/14 公開