Bevy 0.6 に対応2022/01/13

Rust + Bevyで作成したゲーム「くるんくる~ぱ」を、Bevy 0.6に対応させました。

ブラウザ等、環境によっては動かないかもしれません。

開発プロジェクト(GitHub

ガイドに従えば0.6への移行自体はそんなに難しくはありませんが、0.5で作成した前回の記事で挙げられた点が結構改修されていますので、その辺りを主に並べてみます。


スプライトを半透明にする

SpriteSheetBundleと同じような感じで、SpriteBundleでもカラーのパラメータがSpriteに含まれるようになった。
これで透明度を変えるときでも、Assets<ColorMaterial>を持ち歩かなくて済む。

スプライトの加算合成

AlphaModeなんてのがあったから、これは?と思って見てみたが、どうも2Dのスプライトに使うものでもないらしい。そもそも、ブレンドモードに加算合成が無い……

需要はありそうなもんだが、何か見落としているのだろうか?

テキスト描画

Text2dBundleTransformが、テキストの拡大率や回転角に影響するように変更された。
紹介ページでは、サイズを変えたきゃ「フォントサイズ」を変えることを勧められているけど。

Web対応

標準でWeb対応されて、特にプラグインを入れることなくコマンドオプションだけでWeb用にコンパイルできる。
そのためなのか、それとは別の話なのかはわからないが、Web版のテストにはwasm-server-runnerを使うようになっている。

いくつか注意点があって、wasm-server-runnerではこちらの用意したhtmlを介さないからなのかWindowDescriptorでcanvasを設定すると動かなかった。少なくともデバッグビルドでは設定を外す必要がある。

また同様に、小細工ができないので音が鳴らない。

wasm-server-runnerの設定かなんかでやりようがあるのかもしれないが、致命的な問題でもなかったのでほっといている。

最後に

コンパイル・デバッグの方法の変更でちょっと手間取ったりもしましたが、プログラム自体のBeby0.6への対応は難しくありませんでした。カプセル化されたクラスで変更点を吸収してしまうようなオブジェクト指向型のエンジンに比べると、修正箇所が多いような気もしますが。

スプライトの半透明(カラー)設定なんかは、0.6ですっきりした形になって扱いやすくなりました。むしろ、0.6を待ってからゲームを作り始めていればあんな苦労は……



Rust + Bevyでゲーム作成2021/12/22

何か必要に迫られてというわけではないけれど、
 Rustあたりは触っておいた方が良いかなと、
 ゲームエンジンBevyを使用してミニゲームを作ってみました。

くるんくる~ぱ

くるくるアクションパズル くるんくる~ぱ

ブラウザ等、環境によっては動かないかもしれません。

開発プロジェクト(GitHub


Bevyの基本的な使い方は紹介ページもありますし日本語でもいろいろな所で解説されていますが、実際にゲームを作るとなるとそれ以外にも必要な事が出てきたり様々な問題にぶつかったりもします。
今回の経験から、そういった点をいくつか挙げてみます。


スプライトを半透明にする

 アルファ値(を含めたカラー)を設定するには、ColorMaterialのcolor:Colorを変更する。
 同じ大きさのスプライトを複数まとめたSpriteSheetBundleを使った場合にはTextureAtlasSpriteを持ってきて、そのメンバのcolorを変更すれば良い。
 ただし SpriteBundleでは materialがHandle<ColorMaterial>なので、ResMut<Assets<ColorMaterial>も持ってきてそこからget_mutでColorMaterialを取り出す必要がある。
 SpriteBundleSpriteSheetBundleも似たようなものだと思うのだが、何でやり方が異なるのだろうか?

ちなみにSpriteBundleでもスプライト作成時に設定するのなら、ResMut<Assets<ColorMaterial>>にaddする前に変更してしまえば良い。


スプライトの加算合成

 加算合成を含めた描画モードの変更の仕方だが、結局わからなかった。
 このゲームでは省いても何とかなったけど、エフェクトなど他のゲームではよく使われる手法なのでやり方がわからないと困りそう。


テキスト描画

 テキストの描画はText2dBundleでエンティティを生成

は良いとして、
transformのscaleを変更しても、拡大率が変わらなかった。
何か使い方を間違えているのか、そういうものなのか……


場面の切替え

 タイトル画面からゲームへなど場面を設定して移行する機能で、他のエンジンでは"シーン"などと呼ばれることもあるが、Bevyでは"State"となっている。
 SystemSetで各Stateごとにシステムを登録することによって、動作を切り替えることができるようになっている。

切り替え時には画面も変わるということでそのStateで生成したエンティティなんかを削除してまっさらにして欲しいものだが、それは自前で行う必要がある。
 エンティティをすべてクリアするとか、ある時点の状態に戻すとかそういう機能があれば便利なのだが、そんなのは見つからなかったのでこのゲームでは

State開始時に存在しているエンティティのidを記録しておいて、終了時にそれ以外のエンティティを全部削除

するようにしている。あまりカタギじゃないやり方なので真似しない方が良いかも。
 そのState中で作られるエンティティに目印としての構造体を insertしておくという手もあるが、面倒だし付け忘れても動作はしてしまうのが怖いところ。

また、あるStateから同じStateに直接切り替えることはできない。
 GAME OVERの後、同じゲームStateを最初からやり直したかったのだが、一旦ダミーのStateに切り替えてからすぐにゲームのStateに戻すようにしている。めんどくさいことに。

あとフレーム処理絡みで問題があるのだが、それは次の項で。


フレーム処理

 1秒間に60回とか30回とか、決まった間隔で処理を行う機能。
 ここにあるように、フレーム処理させたいシステムの前に  .with_run_criteria(FixedTimestep::step( 間隔 ))
を差し込むだけ。簡単。

なのだが
 一つ問題があって、これを差し込んだ後のシステムは Stateを無視して実行される(っぽい)。
 他のStateの実行中に、そのStateの初期化部分も通らず繰り返し部分が実行されてしまうので当然エラーになる(むしろエラーで止まってくれて助かった)。

そのために、with_run_criteriaにchainで

 現在そのStateの実行中か

という判定を挟んで対処している。なんなんだ一体。


Web対応

 チートブックには2つのやり方が示されているが、bevy_webgl2_app_templateを使う方を選択。Webに対応するというよりは、Webに対応されたひな型を元にプロジェクトを作るという感じ。

ただ、ブラウザで実行させてみるとちょっと画面が荒い。なぜかサイズが1.2倍に拡大されている。どこかに設定でもあるのかと探してみたが見つからなかったので、プログラム側で対処してみた。

具体的には、

・ウィンドウ生成時のサイズを1.2分の1
 ・カメラのスケールを1.2に
 ・マウスカーソルの座標を1.2倍に

の3ヵ所。


オーディオ再生

 WebではBevyの標準のサウンドシステムは使えないらしいので、ここに従ってbevy_kira_audioを組み込んでみた。
 featuresに"mp3"を含めるとコンパイルエラーが出るけど、どうせoggしか使っていないので他は削除して対処。

ブラウザで実行してみると音が出ない。どうも、Webページではユーザーのアクションがあるまで音を鳴らしてはいけないという規則のせいらしい。
 画面をクリックしても音は出なかったが、おそらくクリック前にオーディオの初期化をしているとかそんな感じなんだろう。よくわからないが。

よくわからないまま、Googleのページのコードをhtmlに張り付けて、無事に音が出るようになった。


この記事について

 いろいろと問題点やその対処法を挙げてみましたが、いかんせんRust・Bevyの初心者が見よう見まねとその場しのぎでゲームを作っただけなので、もっとシンプルでスタンダードな手法があるとか、そもそも何か勘違いしているとか怪しい点もあるかと思います。

この記事はこれからBevyで何かを作ろうとする方ではなく、よくBevyをわかっている方に初心者がつまずく点ということで参考にしてもらえるとありがたいです。




Akashic Engine プログラミングでの注意点2020/04/04

「今日からはじめるニコ生ゲーム制作」
なんて放送もあって、新たに Akashic Engineを使ってみようという方も出てきそうなこの頃。

しかし放送でやっていたように画像を差し替えるくらいなら良いのですが、プログラムを組んでゲームを作るとなるといろいろ注意しなければいけない点も出てきます。
一応 Akashic Engineで何本かゲームを作ってきた身として、そのあたりをいくつか挙げてみたいと思います。

なお、サンプルコードは TypeScriptで書かれています。
JavaScriptの方は型宣言等、読み飛ばしちゃってください。


1.マルチタッチ

PCで開発してマウスでテストプレイしていると気づきにくいのですが、Akashic Engineはマルチタッチに対応しています。画面の2点以上を同時にタッチすると、それぞれのポイントのイベントが発生します。

pointDownでタッチを見るだけなら、気にする必要はありません。
pointMovepointUpで移動したとき・離したときのイベントを使うときは、引数のメンバpointerIdでそれぞれ区別・対応するようにしましょう。
一緒にすると、スマホだけの裏技なんてものができてしまうかもしれません。


2.シーンオブジェクトのポイントイベント

エンティティ毎のポイントイベントとは別に、シーンオブジェクトのpointDownCaptureでシーン全体のポイントイベントを取得することができます。
タッチした座標は引数のメンバpointで得られるのですが、touchableなエンティティの上をクリックした場合、その値が画面上の座標ではなくエンティティ上での相対座標になってしまいます。

例えば (100, 100) - (200, 200)の座標・大きさでtouchableなエンティティがあったときに画面上の座標(120, 120)をクリックすると、取得される値は(20, 20)になってしまいます。

画面上の座標を得るには、メンバtargetを見てエンティティの行列で変換する必要があります。
scene.pointDownCapture.add((ev: g.PointDownEvent) =>
{
    let pos: g.CommonOffset;
    if ( ev.target ) {
        pos = ev.target.getMatrix().multiplyPoint(ev.point);
    }
    else {
        pos = ev.point;
    }



3.アンカーポイント

エンティティはアンカーポイントによって表示する座標の原点や拡大・縮小・回転の位置を指定することができます。
原点を指定するようなものなのですが、appendで追加した子エンティティの相対座標に親のアンカーポイントは影響しません。

例えば次のように、
const   parent: g.FilledRect = new g.FilledRect(
{
    scene: scene,
    cssColor: "#ff0000",
    x: 100,
    y: 100,
    width:  64,
    height: 64,
    anchorX: 0.5,
    anchorY: 0.5,
});
scene.append(parent);

const   child: g.FilledRect = new g.FilledRect(
{
    scene: scene,
    cssColor: "#0000ff",
    x: 100,
    y: 100,
    width:  32,
    height: 32,
    anchorX: 0.5,
    anchorY: 0.5,
});
scene.append(child);

中心をアンカーポイントとした2つのエンティティを、それぞれ同じ座標(100, 100)に表示すると

Akashic Engine アンカーポイントの例

2つは同じ座標を中心として表示されます。当然ながら。

const   child = new g.FilledRect(
{
    scene: scene,
    cssColor: "#0000ff",
    x: 0,
    y: 0,
    width:  32,
    height: 32,
    anchorX: 0.5,
    anchorY: 0.5,
});
parent.append(child);

そこで座標が同じなんだから相対位置は(0, 0)だろうと、青い方の位置を x: 0, y: 0にして 赤い方に appendしてみると

Akashic Engine アンカーポイントの例

左上にずれます。
アンカーポイントに依らず、相対座標は親エンティティの左上を原点としているようです。

中心を合わせるには、
    x: 64/2 + 0,
    y: 64/2 + 0,
親エンティティのサイズを考慮する必要があります。

    x: parent.width*parent.anchorX + 0,
    y: parent.height*parent.anchorY + 0,
汎用的に書くと、こんな感じです。



というわけで、自分が Akashic Engineを使う上で実際にぶつかった点を3つ程挙げてみました。

あとはラベルやフォントなんかで気になる点もあるのですが、よくわからないままその場しのぎで対応してたりするので、ここでは割愛致します。


Akashic Engine 知られざる機能2019/11/28

アツマールAPIの話をしたので、Akashic Engineの方も少し。

Akashic Engineの知られざる機能
……というより、知っておくと便利な機能
……というか、このくらい知らせておいた方が良いんじゃないの?という機能を2つほど挙げてみます。


1.グローバルアセット
いわゆる常駐データです。
通常アセットはシーン(g.Scene)毎に読み替えられますが、グローバルアセットに指定されたアセットはずっと常駐しています。
そのため、シーンに依らない共通データをシーンが変わるたびに読み直す必要がありません。

グローバルアセットの指定は game.jsonでおこなわれ、アセット定義の"global"を trueにすることでグローバルアセットとして扱われます。
グローバルアセットはアプリ開始時に自動的に読み込まれ、アクセスするときは
 g.game.assets["data"]のように g.Gameから参照されます。

詳しくは、game.json の仕様で。


2.スプライトへの描画

みんなでいしかり ステージ選択

「みんなで いしかり」のステージ選択画面は、各ステージの配置画像がボタンとなって並んでいます。つまりステージを直接画面に描画するのではなく、一旦ボタン用のスプライトに描画してから、そのボタンを画面に描画しているわけです。

このスプライトの作成には、エンティティ(g.E)からスプライト(g.Sprite)を作成する関数である

g.Util.createSpriteFromE(scene: g.Scene, e: g.E, camera?: g.Camera): g.Sprite

を使用しています。
元になるのはエンティティということでスプライトでもラベルでも良く、また他のエンティティを子として appendすることもできます。

注意点としては、使用したエンティティは自分で destroy()する必要があるということです。
……なんて断言はできませんが、たぶんそういうことになってるんじゃないかと思います。少なくとも destroyして悪いことはないはず……

関数の詳しい使い方は、リファレンスで。



というわけで、おそらくあまり知られていない Akashic Engineの機能を挙げてみました。
……が、使いどころが限られる「スプライトへの描画」はともかく、「グローバルアセット」なんかはもっと周知させておくべきなんじゃないでしょうか?
データの読み込み頻度は、アプリの快適さに関わってくるものですし……

前述の通り game.json の仕様に記載はありますが、なかなかそこまでは読まんわー。



「みんなで いしかり」でのアツマールAPIの使い方2019/11/25

反射パズル みんなで いしかりでは、ユーザーがオリジナルステージを作成・公開することができます。
ステージデータの管理などアツマールAPIの機能を使っているのですが、その具体的な方法を以下に挙げてみます。


1.ステージデータの取得
公開されたステージでのデータは、制作したユーザーの共有セーブに保存されています。
それらの取得のために必要な共有セーブを持つユーザーのリストは、ランキング機能であるスコアボードを利用しています。

こういう場合ユーザー情報APIで最近プレイしたユーザーの情報を取得して使うのが一般的なようですが、このゲームでのステージエディットは少しハードルが高いこともありステージを公開しているユーザーが押し出されてしまう可能性があります。
ステージを公開した時だけプレイヤー間通信の有効化を行うという手もありますが、一旦公開したステージを非公開にして共有データが無くなる可能性も考慮して、スコアボードを使うことにしました。

ステージ作成側は、
・ステージ公開時
・起動時
にスコアボードに日にちをスコアとして登録します。起動時にも登録しているのは、新しいステージを作成しなくても埋もれてしまわないようにするためです。

そして作成ステージをプレイする側は、scroreboards.getRecordsでユーザーのリストを取得します。
日にち(1970/1/1からの日数)をスコアとしているので新しいほど上位にきますが、リストにさえ入っていれば順位は処理に関係ありません。


2.作成したステージをTwitterで紹介
APIの使い方自体は、特に変わったことはありません。
screenshot.setTweetMessageで、クエリ文字列を設定して、screenshot.displayModalでユーザーがツイート、
そのリンクで起動した側はquery[key]でステージ情報を得る
という処理になっています。

クエリ文字列には、ステージのデータと作成したユーザーの情報が丸々入っています。
ステージ情報をデータベース等で一括管理していれば通し番号でもやり取りすれば良いのですが、データ自体が個々のユーザーの共有セーブにあり変更されたり無くなったりすることもあるので、パラメータの文字列だけでステージを再現できるようにしました。


3.ステージ作者情報の表示
そういうAPIがあったので、せっかくだから使ってみました。
ステージを作ってくださったユーザー様の宣伝に、少しでもなってくれればというところでもあります。


4.できなかったこと
ユーザーシグナルあたりを使って、オリジナルステージを遊んだ人の数やクリアした人の数を制作者に伝えるようにしてみたかったのですが、取りこぼしがあったり制作者が定期的に起動する必要があったりするのであきらめました。値が不正確では、あまり意味もありませんので。

何かしらのフィードバックがあった方がステージの作り甲斐もあるのですが、そこはTwitterでのステージ紹介を使ってなんとか……