ドット絵が描けましたので、いよいよそのドット絵をWASM-4で描画してみましょう。画像を出さないことにはゲーム開発が始まった感じがしません。
WASM-4で画像を読み込む
WASM-4はWASMの1モジュールにゲームのすべてを収めなければならず、ファイルシステムにアクセスすることはできません。外部ファイルの画像を読み込むようなことはできませんから、画像をコードに変換してゲームに組み込みます。
WASM-4にはそのためのコマンドラインツール w4 png2src
が備わっており、PNG画像を各種のプログラミング言語のソースコードへと変換することができます。Rustの場合は --rust
オプションを付けて次のように実行します。
$ w4 png2src --rust player.png
こうすると、次のようなソースコードが標準出力に吐き出されます。
// player
const PLAYER_WIDTH: u32 = 8;
const PLAYER_HEIGHT: u32 = 16;
const PLAYER_FLAGS: u32 = BLIT_2BPP;
const PLAYER: [u8; 32] = [ 0x00,0x00, ... ,0x04,0x10 ];
ここでは、PLAYER
という32バイトの配列に画像データが入っているわけです。1ピクセルが2ビットで、この画像の大きさは8x16ピクセルなので、2bit * 8 * 16 = 156bit = 32bytes
というわけです。これをコピペして適当なソースファイルに貼り付ければいいわけです。
テンプレートファイルを使う
画像はたくさんあるので、私は画像ごとに個別のクレートにわけるようにしています。しかし、上記の素のコマンドで生成されるコードでは単なるconst
の変数に値が入っているので、クレートの外部からこのデータにアクセスできません。そんなわけでpub const
にしたいです。また、画像の幅や高さ、ピクセルデータがバラバラだとuse
が面倒なので、画像の幅や高さの情報もまとめた画像を表すデータ型 Image
として取り扱いたいです。そんなときは、テンプレートファイルで生成する内容をカスタマイズできます。テンプレートファイルはmustacheです。私は次のように書きました。
{{#sprites}}
// {{name}}
use crate::wasm4::*;
use crate::image::Image;
const {{rustName}}_WIDTH: u32 = {{width}};
const {{rustName}}_HEIGHT: u32 = {{height}};
const {{rustName}}_FLAGS: u32 = {{flagsHumanReadable}};
const {{rustName}}: [u8; {{length}}] = [ {{bytes}} ];
pub const {{rustName}}_IMAGE: Image = Image {
width: {{rustName}}_WIDTH,
height: {{rustName}}_HEIGHT,
flags: {{rustName}}_FLAGS,
data: &{{rustName}},
};
{{/sprites}}
シェルスクリプトでなるべく自動化する
また、実際のゲーム開発では一度描いたドット絵を修正することはよくあります。そのたびに毎回コピーアンドペーストするのは面倒なので、コマンド一発ですべての画像に対してw4 png2src
を実行するシェルスクリプトで自動化することにしました。こんな感じです。
#!/bin/bash
for filename in ./image/*.png; do
base=$(basename "$filename")
name=${base%.*}
out="./src/image/${name}.rs"
w4 png2src --rust "$filename" --template template.mustache > "$out"
done
これで、画像を変更したり追加しても、シェルスクリプト一発でクレート化できます。
画像を描画する
あとはこの画像データをblit
関数で描画するだけです。私の場合は、先述の Image
構造体に draw
メソッドを生やして、そこで描画するようにしました。
impl Image {
pub fn draw(&self, x: i32, y: i32, flags: u32) {
wasm4::blit(self.data, x, y, self.width, self.height, self.flags | flags);
}
}
ちなみに、気をつけないといけないのは、描画色はまた別で指定しなければならないことです。画像の各ピクセルは0
〜3
のインデックスを持っていますが、これがそのままバッファに書き込まれるわけではありません。DRAW_COLORS
ポインタの先にには、この0
から3
のインデックスの値が、カラーパレットのどの値として描き込まれるかの対応関係が入っています。この描画色の設定をしておかないと、意図したとおりに描画されません。次のような関数を用意しておくと便利です。
pub fn set_draw_color(idx: u16) {
unsafe { *wasm4::DRAW_COLORS = idx }
}
画像を描画するときは、次のようにset_draw_color
を呼び出してから描画を行います。
set_draw_color(0x3210);
image.draw(x, y, 0);
ここでは、
- 画像のインデックス
0
は0
(透明色)、 - 画像のインデックス
1
はカラーパレットの1
- 画像のインデックス
2
はカラーパレットの2
- 画像のインデックス
3
はカラーパレットの3
となるように指定していて、つまり画像のインデックス0
のピクセルは透明色なので実際には描画されません。画像のピクセルは0
〜3
の4つの値を取りますが、描画色は0
(透明色)とカラーパレットの1
〜4
までで合計5つの値をとるので、その対応関係をコード上で指定しなければならないわけです。ちょっと面倒ですね……。
次回予告
このあたりでざっとWASM-4のAPI全体を眺めてみます。とはいっても関数12個しかないので簡単です。