ミレー
前回はサボって申し訳ない。
今回もサボります。
タイルとは?
RPG のマップは、タイルという小さな画像が何枚も連続して配置されることで描画されます。
草原であれば、草のタイル画像を何枚も並べて描画します。
タイルセット
んでそのタイル画像が集まった画像がタイルセットと呼ばれています。
基本はこのタイルセットを素材として用意することで、マップを作成します。
マップだけではなく、スマホゲームなんかでも使われていましたね。
そして、このタイルセット素材なのですが、フリー素材として提供されていることもあります。
調べてみると、様々な方が Booth などで提供されています。
3 分タイルセットクッキング
というわけで 3 分でタイルセットを用意しましょう ♪
まずは RPG Maker XP のインストールフォルダを開きます(?????)
突然の RPG Maker XP?
RPG Maker XP というのは、RPG Maker (RPG ツクール) シリーズの作品のひとつで、少し前に Steam で無料配布されていました。
無料素材を探すのが面倒なので、今回はここから素材を拝借して使います。
ちなみに、RPG Maker の素材については、RPG Maker 以外でも使えるということが規約に書かれています
詳しくは規約を確認してくださいね。
それでは、RPG Maker XP などがない人は、無料素材を探してみてください。
素材の種別は特に問いません。
ただ、今回はタイルサイズは 32x32 とさせていただきます。
今回はsrc/assets/rpg
の中に RPG Maker XP の RTP を置いておきました。
タイルを管理するコード
今、main.ts
がある場所にlib
というフォルダを作りましょう。
その中にtile.ts
を作成します。
const TileSize = 32;
まずはタイルサイズを定義しておきましょう。
先の記事で Canvas に画像を描画する方法を紹介しましたが、そのImage
クラス(正確にはHTMLImageElement
)をタイルに保管します。
...そんなオブジェクト...の型を定義しましょう。
type Tile = {
image: HTMLImageElement;
x: number;
y: number;
width: number;
height: number;
};
このオブジェクトは、タイルセットの画像と、そのうちの対象となるタイルの特定の座標から特定のサイズを保管するものです。
これだけだとちょっとまだ頼りないので、さらに Canvas に描画する関数も追加しましょう。
Canvas に描画するために必要なものは、Canvas の Context と、描画位置、描画する画像、その画像のうちどの部分を描画するかの情報です。
const drawTile = (
tile: Tile,
x: number,
y: number,
ctx: CanvasRenderingContext2D,
width?: number,
height?: number
) => {
ctx.drawImage(
tile.image,
tile.x,
tile.y,
tile.width,
tile.height,
x,
y,
width,
height
);
};
まぁ安直に書いたらこうなりますよね。
これを Tile オブジェクトに追加しましょう。
width
とheight
なのですが、これはキャンバスに描画する際のサイズです。
通常、画像サイズと同じで良いので、省略できるようにします。
(引き伸ばしなどをしなければならない場合は別)
省略できる引数は?
を付けることで実現できます。
そのために、「タイルセットへの画像パス」、「タイルの位置・サイズ」から Tile オブジェクトを生成する関数を作ります。
const getTile = (
path: string,
x: number,
y: number,
width: number,
height: number
): Tile => {
const image = new Image();
image.src = imagePath;
return { image, x, y, width, height };
};
これでは、画像の読み込みが終わる前にオブジェクトを返しており、実際に描画が出来るかどうかの保証がありません。
そこでimage.decode()
を使います。
これは、画像の読み込みが終わるまで待機するメソッドです。
const getTile = (
path: string,
x: number,
y: number,
width: number,
height: number
): Tile => {
const image = new Image();
image.src = imagePath;
image.decode();
return { image, x, y, width, height };
};
と、一般に考えたらこうなりますが、decode()
というのは非同期処理なのです。
非同期処理!?
2 年前のアドベントカレンダーではマジで軽くしか触れませんでしたが、それは私が Python の非同期処理に対しての知見が浅かったからです。
TypeScript の非同期処理は、Python よりも比較的簡単です。
まず概要として、非同期処理というのは主に I/O 処理などで、どうしても待たなければならない処理がある場合に使用されます。
例えば、あるファイルをダウンロードしていある間は、I/O が処理を行って、CPU は完了まで待機しているような状態です。
その間に、CPU に他の仕事をさせてあげれば、効率が良くなるよね~というものが非同期処理の考え方...です!多分。
const getTile = async (
path: string,
x: number,
y: number,
width: number,
height: number
): Promise<Tile> => {
const image = new Image();
image.src = imagePath;
await image.decode();
return { image, x, y, width, height };
};
非同期処理に対応した関数を非同期で使うには、await
というキーワードを使います。
ただ、await
を使うには、そのスコープがasync
である必要があります。
そのためにgetTile
もasync
関数に変更しました。
(そのasync
関数が非同期処理に対応した関数なので、これを実行するにはさらにasync
内で実行しなけらばならず...の無限ループなのです)
また、async
関数の返り値はPromise<T>
型になります。
は?T って何?ってなると思いますが、これはジェネリクスという考え方で...今回はPromise<Tile>
が、Tile
型を返すPromise
であることを覚えておいてください。
async
関数をawait
を付けずにそのまま実行すると、本当の値(今回ならTile
)ではなくPromise
が返ってきます。
また、await
を付けないと、処理完了まで止まることはなく、どんどん次の処理へと進んでいってしまいます。
...こんなもんで分かったかな?
Tile に今の関数をうめこもう
type Tile = {
image: HTMLImageElement;
x: number;
y: number;
width: number;
height: number;
draw: (
x: number,
y: number,
ctx: CanvasRenderingContext2D,
width?: number,
height?: number
) => void;
};
const getTile = async (
path: string,
x: number,
y: number,
width: number,
height: number
): Promise<Tile> => {
const image = new Image();
image.src = imagePath;
const tileInfo = {
x,
y,
width,
height,
};
await image.decode();
return {
image,
...tileInfo,
draw: (
x: number,
y: number,
ctx: CanvasRenderingContext2D,
width: number = tileInfo.width,
height: number = tileInfo.height
) => {
ctx.drawImage(
image,
tileInfo.x,
tileInfo.y,
tileInfo.width,
tileInfo.height,
x,
y,
width,
height
);
},
};
};
まず、Tile
にdraw
メソッドを追加しました。
ここには実装は書けず、関数の型のみを書くことが出来ます。
実際のgetTile
の中身ですが、まずtileInfo
というオブジェクトを作ります。
これは、Tile
オブジェクトに含まれるx
、y
、width
、height
をまとめたものです。
これを...tileInfo
で展開してTile
オブジェクトに追加します。
Tile
はimage
、x
、y
、width
、height
、draw
を持つオブジェクトになります。
と、このようにオブジェクトを作成することで、色々な処理を行うことが出来ます。
これは、オブジェクト指向の考え方に少しだけ近いようなもの...ですかね多分。
class
を使わなかった理由は、なんとなくたくさんの数のクラスをインスタンス化するとボトルネックになるかなと思ったからです。
次回以降は、このTile
オブジェクトをさらに拡張していき、より使いやすいものにしていきます。
あとがき
うわ~計画狂っちゃったよ!!
この記事でこんなに書くとは思わなかった!!
明日の分のネタ無くなっちゃったよ!!