この記事は夏のブログリレー2019の11日目の記事です。夏休みの約四分の一が終了しました。私は悲しいです。
進捗どうですか?w
2022/01/07 記事の一部に変更を施すとともに、現時点でのGodotのバージョンで再現したものをGitHubにアップロードしました。今振り返ると汚い部分もあるコードですが、興味のある方は参照してください。
https://github.com/FourmiSushi/kakomuyatu
変更内容:「ゲーム画面のスクリプトの作成」において、square_sizeを定義している部分のコードが抜けていたのを修正しました。
Godot Engine
皆さんはゲームをどうやって作りますか?
私は、ゲームを作らないのでよくわかんないです。
ミニゲーム的なものをゲームライブラリ(PIXI.js、Pygame等)を使って作ることは時々ありますが、ある程度凝ったゲームを作ってみたことはありませんでした。
私はむずかしいことを考えるのが苦手なので、パーティクルやらなんやらの凝った感じの処理もするならゲームエンジンかなと思って調べてみたわけです。
Unity?なんか馴染めない!UnrealEngine?だったらUnityにするかな、、、GameMaker?お金が無い、、、わけじゃないけどあんまり余裕が無い!
といった感じで優柔不断っぷりを余すことなく発揮した果てにたどり着いたのがこの、Godot Engineというわけです。
Godotは2014年に登場したオープンソースのC++製ゲームエンジンで、現在最新バージョンは3.1.1となっています。GitHubのスター数は2万4千ほどで、JavaScriptの有名なゲームライブラリであるPIXI.jsにも迫るほどです。
特徴
複数OS対応
Windows、macOSはもちろん、Linux、なんとかBSD、Haikuなどにも対応していると公式に明記されています。作品は更にブラウザ、スマートフォンなどにも対応させることができます。
オープンソース
オープンソースじゃなくてもわたしは構いませんがなんかうれしい
2D/3D対応
うれしい。
独自言語・独自エディタ
むしろ言語やエディタで迷わなくて済む。最高ですね
GDScriptという、Pythonに似たスクリプト言語で書くことができます。
C#で書くこともできるとかなんとか。多少の手順を踏めばNimやらDやらのような言語でもかけるらしいが試してません。ゆるしてね
「ノード」と「シーン」による直感的な制作
(個人の感想です。)
インストール
インストールは公式サイトからバイナリを落とす、パッケージマネージャで入れるなど様々な方法でできますが、SteamからインストールできるのでSteamからインストールしました。
作られた作品
こちらのサイトから、Godotで作られた作品の一部を見ることができます。
数はそこまで多くないですが、ハイクオリティな作品もあり、Godotがゲーム制作における現実的な選択肢であることがわかります。
アセットストア
Godot Asset Library
UnityなどにあるようなアセットストアがGodotにも存在します。しかしながらその数は346と少なく、これは他の有名ゲームエンジンに劣っている点だといえるでしょう。
充実したチュートリアル
Godotには公式のチュートリアルが多くあります。動画ではなくテキストでのチュートリアルであるため自分のペースで読むことができるのは私としては嬉しいです。
数の多さだけでなく、そのすべてが最新版に対応していることからコミュニティの努力がうかがえます。
つくってみた
ちょっとしたゲームを作成した流れを下に書いておくので、興味を持った方はながめてみてください。
今回作ったもの
四角形を囲み、爆発させていくゲームです。
製作期間は1日(<24h)くらいでしょうか。
フォントは8:51:22 pmさんの851テガキカクットを使用させていただきました。
音ズレが訪れていたり重かったりしますがここから遊べます
感想
そこまで時間をかけていない割にはそれらしい演出を追加したりすることができてゲームエンジンはすごいなあと感じました。
また、ノードを組み合わせることで機能を実現していくのはやはりわかりやすいように感じます。Unityを普段使っている方にも感想を聞いてみたいです。
Unityとの比較に関して言えば、2D制作における長さの単位がデフォルトでピクセル単位なのはかなり嬉しいように感じました。
手順
やったことを淡々と書いていきますが、字面だけではわかりにくそうなところには適宜説明をいれておくので参考にする場合は参考にしてください。
プロジェクトの作成
サンプルプロジェクトをいくつか見たあと、プロジェクトを作成しました。OpenGL ES 3.0で。
プロジェクトの設定
解像度などもろもろを「プロジェクト/プロジェクト設定」から設定します。
Display->Window->SizeからWidth/Height: 640/360、Test Width/Height: 1280/720
Display->Window->StretchからMode: viewport(拡大時にぼやけなくなります)、Aspect: keep
Rendering->Quality->2dからUse Pixel Snap: オン(図形の回転にアンチエイリアスがかからなくなります。今回意味があるかは確かめてないです。)
ボタンの割当
今回はシンプルなゲームなので、わざわざする必要もないのですが。
プロジェクト設定の「インプットマップ」タブから"action_main"を追加し、マウス左ボタンを割り当てます。
囲むやつの作成
矩形選択のようにして画面内の四角形を囲むやつを作ります。
画面上方の「2D」をクリックして2Dモードにしたあと、
Area2Dをルートに持つカスタムノードを作成します。Area2DをCaptureAreaという名前に変え、子ノードにCollisionPolygon2DとPolygon2Dを作成し、Polygon2DのColorを半透明な白にします。
囲むやつのスクリプトの作成
CaptureAreaのインスペクタの下方から新規スクリプトを作成します。
テンプレートはNo Comentsで。
中身を以下のように変更します。
extends Area2D
var capture_start = Vector2()
# 初期化処理
func _ready():
visible = false # 何もしていないときは見えない
# 物理演算が関係する、毎フレームごとの処理は_physics_processに書く
func _physics_process(delta):
# 必要な入力情報を用意する
var just_pressed = Input.is_action_just_pressed("action_main")
var pressing = Input.is_action_pressed("action_main")
var just_released = Input.is_action_just_released("action_main")
var mouse_pos = get_viewport().get_mouse_position()
if just_pressed:
capture_start = mouse_pos # 矩形の始点
visible = true # クリックすると見えるようになる
if pressing:
# 左ボタンが押下されている間はCollisionPolygon2DとPolygon2Dを常に変更する
var poly = [
capture_start,
Vector2(capture_start.x, mouse_pos.y),
mouse_pos,
Vector2(mouse_pos.x, capture_start.y)
]
$CollisionPolygon2D.polygon = poly
$Polygon2D.polygon = poly
if just_released:
# はなされるとみえるようになり、ポリゴンはリセットされる
visible = false
$CollisionPolygon2D.polygon = []
$Polygon2D.polygon = []
画面右上の「シーンを実行」から保存・実行してみましょう。
しっかりと動いているのが確認できます。
四角形の作成
新しいシーンを作成し、ルートノードはRigidBody2Dに、名前をSquareにします。
子ノードにCollisionShape2DとPolygon2Dを作成します。
SquareについてRigidBody2D->Gravity Scale: 8、PhysicsBody2D->Collision->Mask: 左上の色が暗くなるようにクリック(四角形同士がぶつからなくなります)
CollisionShape2DについてCollisionShape2D->Shape: 新規RectangleShape2D
四角形は3種類作成しますが、そのパラメーターの違いを今回はシーンの違いで管理します。
Squareシーンを保存し、複製してそれぞれSquareLarge、SquareMedium、SquareSmallと名前を変えます。
CollisionShape2D->Shape->RectangleShape2D->ExtentsをLargeから順に20、10、5と変えます。(X、Y両方とも)
Polygon2DはCollisionShape2Dに沿うようにして四角形を描きます。色はお好みで。
四角形のスクリプトの作成・CaptureAreaとの連携
Square.gdを作成し、それぞれのSquareに割り当てます。ファイルシステムからドラッグアンドドロップで割り当てることができます。
CaptureAreaでマウスボタンがはなされた瞬間に、四角形には分裂、または消滅してもらう必要があります。シーンやノード間の連携には、signalというものを使うことができます。
signalを受け取った場合に実行する関数を用意しておけば、柔軟な連携が可能になります。
まずはCaptureArea.gdに変更を加えていきます。
extends Area2D
の下にsignal captured
、if just_released
の中にemit_signal("captured")
と書きます。これによってマウスボタンがはなされた瞬間にsignalが発生します。しかしながら、signalを受け取るノードを指定していないのでこの段階では何も起こりません。
CaptureAreaのインスペクタタブの右の「ノード」タブから、body_enteredとbody_exitedのsignalをダブルクリックし、そのまま接続します。
これはArea2DであるCaptureArea内にPhysicsBody2Dが入った、出たときに出るsignalで、自分のsignalを受け取ったときに実行する関数を自動で作成してくれます。追加された関数の中身を以下のように変更します。
func _on_CaptureArea_body_entered(body):
connect("captured", body, "_on_captured") # 左からsignal名、接続対象、実行する関数
func _on_CaptureArea_body_exited(body):
disconnect("captured", body, "_on_captured")
これにより、CaptureArea内のPhysicsBody2Dがcapturedのsignalを受け取るようになります。
extends RigidBody2D
# exportの変数はGUIエディタからいじることができる
export (PackedScene) var child_square # 一つ小さい四角形のシーン
var child_spread = 128
var torque_range = 128
var screen_size = Vector2()
func _ready():
randomize() # 乱数をランダムにするやつ
screen_size = get_viewport_rect().size # 画面サイズを取得
func _on_captured():
var parent = get_parent() # 親ノード
if child_square != null:
for i in range(4):
var child = child_square.instance() # シーンからノードを作成
child.position = position
child.add_torque(rand_range(-1 * torque_range, torque_range)) # 回転を加える
var force = Vector2(rand_range(-1 * child_spread, child_spread) , rand_range(-1 * child_spread, 0)) # ある程度ランダムな方向に飛ばす
child.apply_central_impulse(force) # 撃力
parent.add_child(child)
queue_free() # 自身を消去
func _physics_process(delta):
# 画面外に行ったときの処理
if position.y > screen_size.y:
queue_free()
if position.x < 0 or position.x > screen_size.x:
queue_free()
GUIエディタから、各Squareに自分より一つ小さいSquareをChild Squareに割り当てます。Smallには何も割り当てません。
テスト用にシーンを作成し、動作を確かめてみます。
動いていることがわかりました。
ゲーム画面の作成
いままでに作ったCaptureAreaとSquareを使って、ゲームのメイン部分を作成していきます。
新たにNode2Dをルートに持つシーンを作り、Node2DをGameという名前に変えます。
横方向へのSquareが出てしまうのをある程度防止するため、壁を作成します。
壁になるように画面外横に2つStaticBody2Dを作り、CollisionPolygon2Dを子に持たせます。
新しい四角形を生み出すためにTimerを作り、SpawnTimerとします。
SpawnTimerのWait Timeを2にします。
さらに、CaptureArea.tscnをドラッグアンドドロップします。
ゲーム画面のスクリプトの作成
この画面では、画面の下にSquareが落ちた際に、再生成して上に打ち上げます。弾性衝突を用いることも考えましたが、回転などが加わり十分な高さに打ち上がらないので今回は使いませんでした。
また、SpawnTimerが0になるごとに新しいSquareを生成する機能も必要です。
Gameノードにスクリプトを作成して割り当てます。
SpawnTimerのtimeout signalとGameノードを接続、Square.gdとGame.gdをそれぞれ以下のように変更します。
extends RigidBody2D
signal square_fell(body) # 追加 引数つきsignal
# exportの変数はGUIエディタからいじることができる
export (PackedScene) var child_square # 一つ小さい四角形のシーン
export (String) var square_size # 追加 四角形のサイズを表す Stringでなくてもintなどでもよい
......
func _ready():
connect("square_fell", get_parent(), "_on_square_fell")
......
func _physics_process(delta):
# 画面外に行ったときの処理
if position.y > screen_size.y:
emit_signal("square_fell", self) # 追加
queue_free()
if position.x < 0 or position.x > screen_size.x:
queue_free()
extends Node2D
var SquareLarge = preload("res://SquareLarge.tscn") # 生成用に読み込んでおく
var SquareMedium = preload("res://SquareMedium.tscn")
var SquareSmall = preload("res://SquareSmall.tscn")
var screen_size = Vector2()
func _ready():
screen_size = get_viewport_rect().size
$SpawnTimer.start()
func _on_SpawnTimer_timeout():
# 2秒ごとに新しいSquareを生成
var newSquare = SquareLarge.instance()
newSquare.position = Vector2(screen_size.x / 2, screen_size.y)
newSquare.add_torque(128)
newSquare.apply_central_impulse(Vector2(0, -640))
add_child(newSquare)
func _on_square_fell(body):
# 下に落ちたら再生成
var newSquare
match body.square_size:
"Large":
newSquare = SquareLarge.instance()
"Medium":
newSquare = SquareMedium.instance()
"Small":
newSquare = SquareSmall.instance()
newSquare.position = Vector2(body.position.x, screen_size.y)
newSquare.angular_velocity = body.angular_velocity
newSquare.apply_central_impulse(Vector2(body.linear_velocity.x, -640))
add_child(newSquare)
各SquareのSquare Sizeを変更したら、実行してみましょう。
タイトル画面やリザルト画面の作成
長くなりそうなので割愛しますが、Labelノードで文字を描画しつつ、get_tree().change_scene_to(scene)
などを用いることによってタイトル画面とリザルト画面を作成します。
演出の追加
これで一通り遊ぶことができるようになりましたが、まだ画面が味気ないと思います。そこで、以下のような演出を追加しようと思います。
- 囲む途中は時間が遅くなる
- 囲むと爆発のようなエフェクトが出る
- 四角形を薄く光らせる
時間を遅くする
時間を遅くするのはかなりかんたんに実装することができます。
CaptureArea.gdのif just_pressed:
の中にEngine.set_time_scale(0.2)
、if just_released:
の中にEngine.set_time_scale(1)
と書きます。
これだけで、時間を遅くする演出が実装できました。
エフェクトを作る
Particles2Dをルートに持つシーンを作成し、以下のようにパラメータを変更します。
Amount: 200
Time->LifeTime: 0.8
->One Shot: オン
->Explosiveness: 0.8
ProcessMaterial->Material: 新規 ParticlesMaterial
新しく作成したParticlesMaterialsのパラメータを以下のように変更します。
Emission Shape->Shape: Sphere(Sphere Radius 2)
Spread->Spread: 180
Initial Velocity->Velocity: 128
->Velocity Random: 0.5
Linear Accel->Accel: 100
->Accel Random: 0.5
->Accel Curve: いい感じになる曲線
Scale->Scale: 5
->Scale Random: 1
->Scale Curve: いい感じになる曲線
Color->Color Ramp: GradientTexture(黄色→半透明赤のGradientを作成)
Explosionという名前で保存し、_on_captured
で四角形と同座標に生成するようにコードを書きます。
四角形を薄く光らせる
四角形を薄く光らせることで、なんかすごそうな雰囲気を出します。
まずは各シーンにPolygon2Dで背景を作ります。暗めの色にしました。
WorldEnvironmentを作成し、
Background->Mode: Canvas
Glow->Enabled: オン
->Intensity: 1
->Blend Mode: Additive
とします。
光らせたいもののPolygon2Dノードなどを
CanvasItem->Material->Material: 新規 CanvasItemMaterial
としてBlend ModeをAddとします。
これで一通り演出の追加が終わりました。
そこまで時間がかかっていない割にはそれなりにそれっぽいものができたように思います。
あとは2秒でつくった(ことにした)SEを追加して完成です。(は????)
まとめ
伝わったかどうかは知りませんが、Godotは魅力的なゲームエンジンです。
夏休みの間に、挑戦してはいかがでしょうか?
私は進捗ダメです。