2025-01-20

GDevelopを触ってみた感想

2024年に小さな2Dプラットフォームゲームをリリースしましたが、その時の感想を書いていなかったので書き残しておこうと思います。

良かった点

2Dプラットフォームゲームを始めやすい

当初はPhaser.jsなどのライブラリを調べたこともありましたが、自分が使える時間を考えると最初から各種エディタやツールが統合された開発環境を利用した方が良い、と判断しました。
そこでUnity2Dにほんのちょっとだけ触れてみて、アングリーバード風のサンプルゲームを見たりもしたのですが、やりたいことに対してUnityは巨大すぎる印象を受けたので、機能が2Dに限定された開発環境を探すことにしました。
2D向けのゲーム開発環境は、シンプルな代わりに機能が限定的すぎるものや、機能が充実している代わりにゲームプログラミングの深い理解を要するものなどがあり、最終的に2Dプラットフォームゲームが一番作りやすそうなGDevelopに決まりました。
シーンエディタにドラッグ&ドロップで敵キャラやアイテムを配置して、イベントシートに衝突時の処理を書くだけでそれなりにゲームになるので、とっつきやすかったです。

無料のゲーム開発環境の中では比較的情報が多い

WebでGDevelopについて検索すると、公式ドキュメントか他のユーザーの記事かフォーラムのやりとりが何かしら見つかったので、少なくとも2Dプラットフォームゲームの作成を始める際の情報には困りませんでした。
また、開発環境の入力欄の1つ1つに説明文が付いている場合が多いので、特に新しいビヘイビアを利用する際に理解しやすかったです。
作成中のゲームに新しい機能が必要になった時の典型的な流れとしては、例えばキャラクターを点滅させたい場合、まずWebで検索してフォーラムのやりとりがヒットして「Flashビヘイビアというものがある」と分かり、開発環境に戻ってキャラクターにFlashビヘイビアを設定すると、点滅の時間の入力欄と説明文が表示されるので、入力してプレビューで確認する、という順序で解決しました。

作ったゲームを公開する場所がある

昔、Windows向けのミニゲームを作っていた時は、自分のWebサイトにEXEファイルを含めたZipアーカイブを置いていました。シンプルなゲームであれば最初からWebで遊べた方が利用しやすいのですが、当時は古い技術が廃止されたり新しい技術が発展途上だったりと、なかなか手を出しづらい状況でした。
GDevelopはWebアプリ(旧HTML5アプリ)としてゲームをエクスポートできるだけでなく、エクスポート先としてgd.gamesが用意されているので、公開できる場所を探したり自前で公開するためのセットアップ作業をせずに済みました。
もちろんEXEファイルをエクスポートすることも可能で、GDevelopのクラウドサービスでビルドする場合は無料アカウントだと1日1回までですが、自力でElectronをインストールしてビルドする方法もあり、公式ドキュメントに手順が書いてあります。

既存機能で出来ないことがあった時の拡張性が高い

GDevelopも他のゲーム開発環境と同様に拡張機能(エクステンション)をサポートしているため、2D向けの高精度な物理演算やコヨーテタイム(落下中でもジャンプできる猶予時間)などの機能を作成中のゲームに組み込むことができます。多くのビジュアルプログラミング環境と同様に、複雑な処理は外部の拡張機能に任せるのがベストな選択です。
一方で、目的の機能そのものを持つ拡張機能が無かった場合、拡張機能をJavaScriptで自作するか諦めるかの単純な2択にならないのがGDevelopの特徴で、GDevelopのイベントシートは複雑な処理が書けるようにプログラミング言語の要素が一部含まれています(ローカル変数、foreachループやwhileループ、関数呼び出しなど)。デバッグの労力が増大する危険性はありますが、ゲーム内の一部の処理だけ複雑な実装をする必要がある場合に、いきなりGDevelopのJavaScript APIについて調べなくても、対応する余地はあります。

ここまではGDevelopの良かった点ですが、懸念事項も書いておきます。

懸念点

イベントシートの条件分岐の書き方が冗長になりがち

イベントシートで「変数>=0」のイベントに加えて「それ以外(プログラミング言語のelseに相当)」のイベントを用意したい場合、明示的に「変数<0」のイベントを作成する必要があります。条件が複雑な場合はNOT演算子に相当する命令を使うことで省力化できますが、元々の条件が変わる度に「それ以外」のイベントの方も条件の修正が必要になります。
代替案として、ローカル変数でフラグを用意して「変数>=0」の場合にフラグを立てるものとし、次にフラグが立っていない時に実行するイベントを用意すれば疑似的に「それ以外」のイベントとして動作しますが、やや冗長に見えます。
複雑な条件を1つのイベントにまとめて書くと読みにくくなる場合、サブイベントに分けて書くことも可能ですが、サブイベントの階層が深くなるため、後からイベントを追加する時に適切な階層を選択するように注意が必要です。

途中で処理を打ち切りたい場合はフラグが必要になる

前述の「それ以外」イベントの課題と関連しますが、イベントシートにはプログラミング言語のbreakのように処理を打ち切る方法がないため、代わりにフラグを用意して処理をスキップする必要があります。フラグが立った時の処理をサブイベントにすれば、フラグが立っていない時にまとめてスキップさせることが可能になります。

イベントに書いた内容は条件が成立する限り毎フレーム実行される

プレイヤーキャラクターの歩くアニメーションが終わった時に、歩数カウンターを増やしたいとします。もし現在のアニメーション名が"Walk"であることだけを条件に歩数カウンターを増やすイベントを書いた場合、プレイヤーキャラクターのアニメーション名が"Walk"になった直後から毎フレームで歩数カウンターが増加します。この場合、アニメーションが完了したことも条件に追加する必要があります。
イベントが毎フレーム実行される仕様は、イベントシートが大きくなって各イベントに目が届かなくなるほど厄介になります。ひとまとまりの処理をサブイベントにすることで、メインイベントの条件を満たした時だけ実行されるように制御できます。
また、「1回だけ実行」という条件を追加するとイベントが連続して実行されなくなるため便利ですが、意図したタイミングで実行されない場合もあるため、必要に応じてアニメーション終了やタイマーなどの他の条件が使えないか調べてみることも重要です。

ピクセルエディターの挙動が独特

GDevelopのピクセルエディターはレイヤー機能や同色範囲の選択ツールが使えるなど、ゲーム開発環境に付属しているお絵かきソフトとしてはそこそこ高機能なのですが、他のペイントツールと微妙に操作方法が違うので戸惑います。また、画像を複製してから変更した時に他の複製画像まで同じ変更で上書きされてしまうことがあります。
ちょっと調べてみた限りでは、piskelというGDevelopとは全く別の製品が動作しているため、動作に変なところがあってGDevelopチームに指摘しても、piskelの関係者ではないので対応は難しいようです。
最初から画像データの作成は他のペイントツールで行い、アニメーションの作成だけGDevelopで行うことで影響を小さくできますが、統合された開発環境としてのメリットは低下します。
また、ピクセルエディターで画像を作成する際に、早い段階で名前を指定しておかないとNewSprite.png、NewSprite2.png、NewSprite3.png...という具合に自動でファイル名が設定されます。リソースエディタで画像リソースと画像ファイルの関連付けを変えることができるので、画像ファイル名を変更してから画像リソースとの関連付けを再設定することもできますが、次々と作成されるNewSprite.pngに対応するのは大変なので、基本的に放っておくのが一番手間の掛からない対処法です。

イベントシートをデバッグするのが難しい

シーンエディタに様々なオブジェクトを追加したりイベントシートに処理を追加したりと、色々追加しているうちは良いのですが、全てが変な動きをしないようにデバッグするのは難しいです。ある程度の変な動きはあきらめて、自分がゲームの中で重要だと思っている部分に注力した方が効率的かもしれません。
GDevelopのデバッガーは変数の中身を細かく確認できる上に、ゲームを一時停止することもできるので便利ですが、デバッガーで特定のオブジェクトが予期せぬ状態に変わったことは分かっても、その原因を調べるのには苦労します。
同じような管理が必要なオブジェクトはグルーピング機能でまとめて、さらにイベントシートの一部を外部イベントや拡張機能として分離することで、問題の発生個所が見つけやすくなるよう工夫することはできます。


以上のように、ビジュアルプログラミングとイベントシートという特徴に起因した難点はあるものの、プログラミングが必須ではないゲーム開発環境としては必要な機能が一通り揃っており、取り組みやすいソフトであることは間違いないです。

2025-01-13

Godotでシンプルな3マッチパズルゲームを作って公開しました

この年末年始はGodotの勉強のために3マッチパズルを作ってました。パズルの基本部分だけ実装できたので年始の終わりに一度公開しましたが、さらにUIや攻略できるステージを整備してリリースしました。


Simple Match 3-5x6 - フリーゲーム投稿サイト GodotPlayer

https://godotplayer.com/games/syjiro_simple_match3_5x6




[基本的なルール]

  • ピースをマウスドラッグかスワイプで動かして、2つのピースを入れ替えてください。
  • 同じ色のピースをタテかヨコに3個以上並べると消えます。
  • ピースを入れ替える度に、画面上部に表示されている移動回数が減ります。0になるとゲームオーバーです。
  • 同じ色のピースを4個並べると、タテまたはヨコに一直線に消せるピースが出現します。
  • 同じ色のピースを5個以上並べると(L字型やT字型も含む)、同じ色のピースをまとめて消せる虹ピースが出現します。

[ステージの紹介]

  • ENDLESS:500点ごとに移動回数が増加
  • Stage1~5:画面左上に提示されているピースを消す
  • Stage6~10:氷ブロックを消す(氷ブロックは近くのピースが消えると一緒に消える)
  • Stage11~15:上から落ちてくるピースは、斜め下が空いていると斜め下にも落ちる
  • Stage16~20:凍っているピースは1回消すと中身のピースが現れる
  • Stage20~25:水滴を一番下まで落とす
  • Stage26~30:上から落ちてくるピースが6色に増える

3マッチパズルのスマホゲームは広告も含めて大量に目にしてきましたが、いざ自分で作ってみると思った以上に大変でした。ルールが単純なので、少量のコーディングですぐに終わるだろうと甘く見ていましたが、大間違いでした。その辺の苦労はまた別の記事で触れたいと思います。

2024-09-18

GDevelopでプロジェクトのassetsフォルダに未使用の画像ファイルが残る

GDevelopでスプライト用の画像を描いたりアセットを入れたりしていると、プロジェクトのassetsフォルダに未使用のファイルが溜まっていきます。

他の記事の調査で作ったプロジェクトのリソースタブを開いてみると、Stop2~5の4つの画像ファイルが表示されました。

一方で、Windowsのエクスプローラでassetsフォルダを確認してみると、7つの画像ファイルが入っています。未使用の画像ファイルが3つあるということです。

プロジェクトをHTML5形式でエクスポートしてみると、使用中の4つの画像ファイルだけがコピーされていました。


ただし、リソースを整理するつもりで不要なファイルをリソースタブに表示している場合は注意が必要です。

今回のプロジェクトのリソースタブで右クリックメニューの「プロジェクトフォルダ内をスキャン>画像」を実行すると、不要なファイルも含めた7つの画像ファイルが一覧表示されます。

この状態でエクスポートすると、未使用の画像ファイルを含めた7つの画像ファイルがコピーされていました。


このような場合は、リソースタブで右クリックメニューの「未使用のファイルを削除...>画像」を実行すると、一覧から未使用の画像が削除されます。ただし、この操作は一覧に表示されなくなるだけで、assetsフォルダには引き続き画像ファイルが残ります。

未使用の画像ファイルが残るのが気になる場合は、事前に空のフォルダを作った後で、GDevelopのファイルメニューで「名前を付けて保存」で空のフォルダを保存先にすると、未使用のファイルを除いたプロジェクトフォルダの内容がコピーされます。

2024-09-17

GDevelopでテキスト型変数を利用する場合は、初期値を空欄にしない方がよい

GDevelopで変数の種類をテキストにした場合、初期値を空欄にすることができます。
初めてGDevelopに触れた時は、この時点で変数に長さ0の文字列が設定されたものと思っていましたが、どうも思ったような動作にならないので調べてみることにしました。

テキストのシーン変数を追加して、初期値を空欄にします。

プレビューしてデバッガで Scene variables を確認してみると、「"value" : "0"」となっていました。テキスト型変数を利用する場合は、1文字以上のテキストを初期値として入れておいた方がよさそうです。


代わりに、できるだけ早いタイミングで変数に""を設定する方法を考えます。イベントシートでシーン開始時に2個目のシーン変数に""を設定します。

再びプレビューしてデバッガで Scene variables を確認してみると、今度は2個目のシーン変数が「"value" : ""」になりました。


GDevelopの開発環境で変数に特定のシーン名を設定する時はリストから選びたい

GDevelopでシーンを変更する時はリストから既存のシーン名を選択できますが、変数にシーン名を設定する場合はシーン名を直接入力する必要があります。また、シーンに付けた名前を後から変更した場合、GDevelopの開発環境が影響のありそうな所を自動的に新しい名前で置換してくれますが、直接入力したシーン名は変更されません。

拡張機能を自作する案

堅実な方法としては、拡張機能を自作してテキスト型変数にシーン名を保持し、変数に対するアクセスは拡張機能の提供するアクションと式に限定する、といったやり方があります。

概要だけ示すと、例えば SceneManager という拡張機能を作成し、次のシーン名を管理するために NextSceneName という変数を追加します。シーン変数にするかグローバル変数するかは用途に応じて変えます。

SceneManager に対し、 NextSceneName を参照するための NextScene 式と、変更するための SetNextScene アクションを用意します。

イベントシートで次のシーン名を取得したい時は「SceneManager::NextScene()」式を使います。

イベントシートのアクションで次のシーンを変更する時は「その他のアクション」から SceneManager のアクションをクリックすると、リストからシーンを選択できます。

変数にアクセスする手段を制限することで不正なシーン名が入りにくくなり、名称の変更にも対応しやすくなりました(例えば SetNextScene アクションの名前を SetNextSceneName に変えた場合など)。チームで作業したり、拡張機能を不特定多数に配布する場合、このような方法でプログラムの部品を制御することが重要になります。

一方で、製作しようとしている作品の規模によっては、ここまで徹底するのは大げさすぎるかもしれません。

シーン名を渡すだけの式を作成する案

拡張機能でアクションや式のパラメーターを「シーン名(テキスト)」にすると、イベントシートで入力する際にリストからシート名を選択できるようになります。この特徴を利用して「シーン名(テキスト)のパラメーターをそのまま返すだけの式」を作成すれば、必要な時だけリストからシート名を選択できそうです。

拡張機能の式を作成して、パラメーターを「シーン名(テキスト)」にします。


式の内容は戻り値にパラメーターの値を設定するだけです。戻り値もシーン名なので、式のタイプも「シーン名(テキスト)」にします。


イベントシートの条件やアクションで「MyExt::InputSceneName("")」と入力し、カーソルを""の中に移すと、シート名をリストから選択できるようになります。もし後で該当シーンの名前が変わっても、""の中のシーン名は自動的に新しい名前に変わります。


変数に何回か特定のシーン名を設定するだけの場合は、この方法で事足りるように見えます。とはいえ開発が進んでシーン名を扱う箇所が増えていくと、最終的には SceneManager に相当する機能を自作するかアセットストアから導入することになるかもしれません。

もし将来のGDevelopが変数の種類として「シーン名(テキスト)」をサポートするようになった場合、MyExt::InputSceneNameのような機能は単純に無駄になるので、あくまでも現時点の妥協的な手段と考える必要があります。

2024-09-16

20年ぶりにゲームを作って公開しました

GDevelopで作成した短い2DアクションゲームをWebブラウザ向けに公開しました。
4つのステージでゴールに到達するとクリアです。
※スマホ非対応、キーボード入力のみ

Pixelamid Adventure | Play on gd.games
https://gd.games/syjiro/pixelamidadventure 

  • 十字キーの ← または → で移動
  • スペースキー か Z キー でジャンプ
  • X キーで武器を発射

20年以上前にHSP(Hot Soup Processor、Windows向けスクリプト環境)で作ったゲームを公開して以来なので、本当に久しぶりの個人ゲーム制作でした。
キャラクターが自動的に目的地へ歩いたり敵を倒したりするスタート画面を作ったのは初めてかもしれません。
サウンドとBGMはOtoLogic様の音声素材を使用しました。短い期間でゲームに音声を付ける目途が立ったのは、使いやすく音声ごとのテーマが分かりやすい素材の助けあってこそでした。
スプレーの説明を読むために顔を近付けるくらい年齢を感じだした日々の中で、このような貴重な経験ができたことに感謝します。
GDevelopを触ってみての感想などは別の機会に書きたいと思います。

GDevelopの「オブジェクトを生成する」アクションの計算式の中で生成対象のインスタンスの高さを取得できない

タイトルに書いてみると当然の処理結果なのですが、GDevelopは「Ball.Height()」のような式を見ただけでは特定のインスタンスの高さなのかBall全体の初期値としての高さなのかが必ずしも自明ではないため、備忘録として残します。

アクションゲームの製作で、プレイヤーの頭上からボールが落ちてくるギミックを考えます。今回は、プレイヤーが白い箱に接触したら、頭上にボールを表示することにします。


イベントシートで、プレイヤーと白い箱が衝突したことを条件に「オブジェクトを生成する」アクションでボールを生成します。ボールの生成位置のY座標を「Player.Y()-Ball.Height()」と書くことで、プレイヤーの左上座標よりもさらにボールの高さ分だけ高い位置にボールが生成されそうに見えます。


プレビューしてみると、プレイヤーと白い箱が接触した瞬間に、プレイヤーと同じ高さにボールが表示されてしまいました。一方で、同じイベント行の中でボールの生成後に高さを取得することは成功しており、「ボールの高さ=32」と表示されました。




動作確認のために、問題の式を「Player.Y()-32」にすると、プレイヤーの頭上にボールが出現しました。この場限りの対応ならこれで終わりですが、この機能が他のところにコピーされてしまうと、ボールの高さが変わった時に修正の手間が増えるかもしれません。

ボールの高さを直接記入する代わりに変数を使うと、ボールの高さを変えたい時に修正するのが変数だけで済みます。

ボールが一度生成されれば高さを取得できる点に注目して、ボールの生成は人から見えない位置で実行し、後から移動する方法もあります。生成後の移動時のY座標に「Player.Y()-Ball.Height()」と書くと期待通りに動作します。ただしイベントに記入するアクションが常に1つ増えるため、その労力が成果に見合うかは作ろうとしているゲームの仕組みにもよるでしょう。


最後に、変数を使った場合の動作例を載せます。