
Blenderでブロック崩しを作る
本日(2023年6月28日)、Blender 3.6 LTSがリリースされました。
今回はその記念(?)ということで、注目の新機能である「Simulation Nodes」を使ってブロック崩しを作ってみました。
先程リリースされた、Blender 3.6 LTSの新機能を使って、「ブロック崩し」を作ってみました #Blender #b3d #GeometryNodes #GeometryNodesSimulation pic.twitter.com/aG37x563en
— Melville (@MelvilleTw) June 27, 2023
Pythonによるコーディング等は一切使わず、ボールの制御や当たり判定などをすべてGeometry Nodeで行っており、特にSimulation Nodes
(Simulation Zone)が大活躍しています。
Simulation Nodesは前フレームのジオメトリや数値の情報をその次のフレームの計算に持ち込むことができるという非常に強力な機能です。
この記事では具体的にどのようにノードを繋げばブロック崩しが作れるのか、実際に私が組んだ作例を見せながら解説していきます。
全体像
こちらが今回作成したブロック崩しを制御しているGeometry Nodeの全体像です。

一番左の「Build Dynamic Geometry」で、今回Simulation Zoneに流すジオメトリを生成しています。

真ん中の部分が今回の肝であるSimulation Nodes(Simulation Zone)を記述している箇所です。
ここで衝突判定やボールの運動、ブロックの消去、スコア計算などを行っています。

そして最後の箇所はシミュレーションに関わらないジオメトリ(ステージのメッシュなど)の作成や、ゲームクリア/ゲームオーバーの判定などを行います。

シミュレーション前
まずはシミュレーション前段階ですが、ここでは2つのグループがあります。
Build Blocks(ブロックの生成)
Build Ball(ボールの生成)

Build Blocks(ブロックの生成)
ブロックの位置に先にポイントを配置しておき、立方体のメッシュのインスタンスを生成します。

Build Ball(ボールの生成)
ボールのメッシュを生成し、位置を取得しやすいようインスタンス化しておきます。
初期位置はパドルすぐ上の位置になるように位置設定もします。

シミュレーション部分
シミュレーション部分は6つのグループがあります。
Blocks Collision(ブロックとの当たり判定)
Wall Collision(壁との当たり判定)
Paddle Collision(パドルとの当たり判定)
Velocity Manager(当たり判定情報を集約して、速度を変化させる)
Update Blocks(ブロックのジオメトリとスコアを更新する)
Update Ball(ボールの位置を更新する)

Blocks Collision(ブロックとの当たり判定)
ブロックとボールの当たり判定の実装です。

ここでの注意として、ブロックとボールはこの時点ではそれぞれ別のジオメトリであり、ブロックの操作にボールの情報が必要になったり、ボールの操作にブロックの情報が必要になったりと、ジオメトリを跨いで情報を与える必要がある場合は特殊なノードを使用する必要があります。
このグループで具体的に用いているのは「インデックスサンプル」と「属性統計」というノードです。
インデックスサンプルは指定したジオメトリの、指定したドメインの、指定したインデックスの、指定したフィールドの値を取得することができ、取得した値は別のジオメトリにも持ち込むことができます。
今回の場合は「ボールの位置」の情報をブロック側のジオメトリで利用したいわけですが、ボールのジオメトリはインスタンスがひとつしかないので、インスタンスのドメインのインデックス0番を指定することで、ボールの位置などを取得可能です。

属性統計は、指定したジオメトリ全体の代表値を取得できるノードです。
今回は「ボールの位置」の情報をブロック側のジオメトリで利用して全ブロックとの当たり判定を"集計"して、さらにそれをボール側に戻して速度に反映させる必要があります。
実際にこのグループでは、全てのブロックとボールとの当たり判定を、X軸とY軸それぞれについて確認し、属性統計ノードの「最大」を使うことでひとつでも衝突がみつかればフラグが立つ、という仕組みを実現しています。

Wall Collision(壁との当たり判定)
壁との当たり判定の実装です。
こちらはブロックと違って壁の位置情報は静的な値をとるので、順当に当たり判定を行っています。

Paddle Collision(パドルとの当たり判定)
パドルは動くものの、グループ入力から位置を取得できるので、壁と同様に順当に当たり判定を行えます。
パドルに関しては、ブロックや壁と違い、反射する際に速度を軸反転するするのではなく、ボールがパドルにぶつかった位置に応じてボールが反射する向きが変動するよう、新しい速度ベクトルを生成するようにしています。

Velocity Manager(当たり判定情報を集約して、速度を変化させる)
これは前3つのグループで得た当たり判定情報を用いて「velocity」という名前の属性を更新します。

Update Blocks(ブロックのジオメトリとスコアを更新する)
ブロックの当たり判定情報を利用して当たったブロックと当たらなかったブロックにジオメトリを分離します。
当たったブロック側は数を数えてスコア計算に用います。
当たらなかったブロックはそのまま次のフレームに用います。

Update Ball(ボールの位置を更新する)
これは「velocity」属性から位置に速度を足しているだけです。

シミュレーション後
残りは仕上げ工程です。
Judge Game Clear(ゲームクリア判定)
Judge Game Over(ゲームオーバー判定)
Center Text(中央に出るテキストの表示)
Score Text(スコア表示)
Build Stage(ステージの生成)
Build Paddle(パドルの生成)

ブロックとボールはわざわざこのタイミングでマテリアル設定ノードを挟んでいます。本当はメッシュ生成と一緒のタイミングでマテリアル設定するとかで良さそうなのですが、バグなのか、どうやらシミュレーションゾーンを通すとマテリアルの情報が消えてしまうようで、シミュレーションゾーンを抜けてから設定しています。
(バグだと思っているのですが、もし理由が分かる人いたら、教えてください)
2023 6/29 追記 : Qさんのツイートより
どうやら、これ自体はバグではなく仕様なようです。
地味に不便なので改善されてほしいですね!
マテリアルが消えてしまうのは、現時点ではバグではなくて仕様ですのじゃ。
— Q@スタジオぽぷり (@popqjp) June 28, 2023
今の仕組みでは実装がやりづらいから、作り直すまでとりあえずマテリアル無しで!ユーザーが自分で管理して!という状態なので、バグに近い仕様だけど。
おそらく4.0では直っているだろう、というのが自分の楽観的予想…
Judge Game Clear(ゲームクリア判定)
ブロックのジオメトリに含まれるインスタンス数が0(以下)であればクリアという判定をします。

Judge Game Over(ゲームオーバー判定)
インデックスサンプルを用いてボールの座標を取得し、ボールの位置がステージの下端よりも下であればゲームオーバーとします。

Center Text(中央に出るテキストの表示)
前2つのグループを用いて判定した結果を用いて「Game Over…」や「Game Clear!」というテキストを画面中央に表示します。

Score Text(スコア表示)
シミュレーションゾーンが直接出力している「Score」という値を用いて、スコア表示を行います。

Build Stage(ステージの生成)
メッシュブーリアンとかを使ってステージのメッシュを作ります。
マテリアルが単色だと上から見たときに壁がどこかわからなくなってしまうのでz座標に応じて色が変化するよう「color」という属性に色を格納しています。

作った「color」属性はBackground用マテリアルで利用します。

Build Paddle(パドルの生成)
このブロック崩しでは、プレイヤーはCube形状のEmptyの位置を移動させることでパドルを操作する、という"なんちゃってUI"を実現しています。
Cube形状のEmptyのデフォルトサイズは、そのスケールベクトルと、Geometry Node標準のプリミティブ立方体のサイズベクトルとは2倍異なるので、その調整をしてあげることでEmptyの大きさと表示上のメッシュの大きさを合わせることができます。

はじめからメッシュオブジェクトでいいのでは?というのはその通りだと思います。
まあ、メッシュオブジェクトだと編集モードで簡単にサイズ制御できてしまうので、
GNで作ったほうが少しだけ安心かも、ぐらいの温度感です。
入力
このGeometry Nodeは4つの入力を設けていました。
画像のような数値を設定すると、だいたい私と同じ状態になります。
お好みで調整してもおおよそいい感じに動くはず…です。
Playerというオブジェクトは、先述の通りCube形状のエンプティオブジェクトを別途作成し、指定してください。

その他Tipsなど
Geometry Nodeの解説は以上です。
ここからはこれを実際に遊ぶ場合のTipsです。
フレームレートの調整
デフォルトのフレームレートは24fpsなのですが、高くしたほうが当たり判定のエラーが少ないので、例えば60fpsにすると良いと思います。

ゲームの実行
Simulation Nodesを前提としたゲームなので、実行するためには再生状態にする必要があります。
また、最終フレームでゲームが強制終了してしまうので、終了フレームを適当にでかい値(例えば100000など)にしておくと長く遊ぶことができます。

ブロック崩し本体の選択可能状態を無効に
パドルを選択する際にブロック崩し本体のオブジェクトが選択されてしまうと厄介なので、3Dビューポート上のゲーム自体の選択可能状態は無効にしておくとQoLが上がりそうです。


パドルの動かし
標準のキー操作であればBlenderはGキーで移動モードになります。
この状態でXキーを押せばX軸だけの動きになるので、ブロック崩しっぽくなります。
一方で、別にY軸に動かしてもゲーム自体は遊べるので、XY平面を動いても楽しいと思います。
その際、パドルがZ方向に動いてしまうと厄介なので、真上からの視点にしたり、あるいは移動モード中にShift + Zを押してXY平面上に制限した移動にすると良いと思います。
おわりに
今回のこの記事は半分ネタで、もう半分はSimulation Nodesの布教目的です。
これが非常に強力な機能だということが伝わっていれば幸いです。
ただしもちろんこれは別にゲームを作る目的の機能ではありません。
より実用的かつ「シミュレーション」チックな例としては…
例えばこういったものが作れます。
反応拡散系
— Melville (@MelvilleTw) June 17, 2023
Reaction Diffusion#Blender pic.twitter.com/ECN9z42rqt
今回Blender 3.6 LTS正式採用記念ということで記事を書きましたが、Simulation Nodes自体は半年ぐらい前のBlender 3.5 α版から触れるようになっていました。
私もその頃から触っていまして、いくつか同様のネタ記事を書いています。
興味があれば他の作例としてぜひご覧ください。
「BlenderのGeometryNodesでライフゲームを作る」
これはライフゲームを作る記事で、33個のノードだけで作成できます。
Simulation Nodesで作れるミニマムな作例としても参考になるのではないかと思います。
「BlenderのGeometryNodesでゲームを遊べるようにした」
これの記事は今回の記事と趣旨が被るのですが、こちらはGNのブーリアンノードを直接操作する形でのワンボタンゲームになっています。
今回の記事と比べるとビジュアル面でのこだわりが見られます。
「BlenderのGeometryNodesで円周率10000桁を計算する」
これは結構自信作なのですが、Simulation Nodesを使って円周率を計算しよう、という企画になっています。
Simulation Nodesの作例としてはもちろん、円周率を計算機でどのように計算するか?という観点でも興味深い内容に仕上げたつもりですので、こういった数値計算分野にも興味があれば是非ご覧ください。