0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Tauriでエンジンからゲームを作ってみるAdvent Calendar 2024

Day 7

【Day7】落ち穂と素材は拾った方がいい。【QAC24】

Last updated at Posted at 2024-12-06

ミレー

前回はサボって申し訳ない。
今回もサボります。

タイルとは?

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 オブジェクトに追加しましょう。

widthheightなのですが、これはキャンバスに描画する際のサイズです。
通常、画像サイズと同じで良いので、省略できるようにします。
(引き伸ばしなどをしなければならない場合は別)
省略できる引数は?を付けることで実現できます。

そのために、「タイルセットへの画像パス」、「タイルの位置・サイズ」から 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である必要があります。
そのためにgetTileasync関数に変更しました。
(その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
      );
    },
  };
};

まず、Tiledrawメソッドを追加しました。
ここには実装は書けず、関数の型のみを書くことが出来ます。

実際のgetTileの中身ですが、まずtileInfoというオブジェクトを作ります。
これは、Tileオブジェクトに含まれるxywidthheightをまとめたものです。
これを...tileInfoで展開してTileオブジェクトに追加します。
Tileimagexywidthheightdrawを持つオブジェクトになります。

と、このようにオブジェクトを作成することで、色々な処理を行うことが出来ます。
これは、オブジェクト指向の考え方に少しだけ近いようなもの...ですかね多分。

classを使わなかった理由は、なんとなくたくさんの数のクラスをインスタンス化するとボトルネックになるかなと思ったからです。

次回以降は、このTileオブジェクトをさらに拡張していき、より使いやすいものにしていきます。

あとがき

うわ~計画狂っちゃったよ!!
この記事でこんなに書くとは思わなかった!!
明日の分のネタ無くなっちゃったよ!!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?