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 10

【Day10】タイルをマップに埋め込ませましょう【QAC24】

Last updated at Posted at 2024-12-09

マップにタイル(の情報)を埋め込む

前回の記事でクラスについて触れていきましたが、「次回説明する」と言って省略したアクセス修飾子について触れていきます。

アクセス修飾子

前回の記事でpublicとかprivateとか書いていましたが、これはアクセス修飾子といって、クラスのメンバーに対してアクセス制限をかけるものです。

これはどういうことかというと、コードを見てもらいましょう。

class Person {
  private privateData: string = "ふっふっふ...人間は私が作り上げたのだよ";
  public name: string = "アダム";
  protected family: string[] = ["イブ", "ソーン"];
  public favorite: string = "リンゴ";
  protected protectedData: string = "これが人類にだけ教えてあげる...";
}

class Tanaka extends Person {
  public name: string = "太郎";
  protected family: string[] = ["次郎", "三郎"];
  public favorite: string = "はちみつ";
  private myNumber: number = 123456789;
}

class Yamada extends Person {
  public name: string = "花子";
  protected family: string[] = ["花女", "花男"];
  public favorite: string = "カレー";
  private myNumber: number = 987654321;
}

このようにPersonクラスとそれを継承したTanakaクラス、Yamadaクラスを作成しました。

まず、アクセス修飾子にはprivateprotectedpublicの 3 つがあります。

  • private:そのクラス内でのみアクセス可能
  • protected:そのクラスとそのサブクラス内でのみアクセス可能
  • public:どこからでもアクセス可能

今回、他人に知られてはいけない情報として、privateDatamyNumberprivateで宣言しています。

const adam = new Person();
const taro = new Tanaka();
const hanako = new Yamada();

console.log(adam.privateData); // エラー
console.log(taro.myNumber); // エラー

console.log(taro.privateData); // エラー

このように、privateで宣言されたメンバーには、そのクラス外からアクセスすることができません。
アクセスできるのはそのクラス内なので、それぞれのクラスにshowMyNumberメソッドを追加してみましょう。

...

class Tanaka extends Person {
  public name: string = "太郎";
  protected family: string[] = ["次郎", "三郎"];
  public favorite: string = "はちみつ";
  private myNumber: number = 123456789;

  public showMyNumber() {
    console.log(this.myNumber);
  }
}

...

console.log(taro.showMyNumber()); // 123456789

で、そのpublicとは、どこからでもアクセスできるということです。

console.log(adam.name); // アダム
console.log(taro.name); // 太郎
console.log(hanako.name); // 花子

protectedは、そのクラスとそのサブクラス内でのみアクセス可能ということなので、familyについては、PersonクラスとそのサブクラスであるTanakaクラス、Yamadaクラスでアクセス可能です。

class Person {
  private privateData: string = "ふっふっふ...人間は私が作り上げたのだよ";
  public name: string = "アダム";
  protected family: string[] = ["イブ", "ソーン"];
  public favorite: string = "リンゴ";

  public showFamily() {
    console.log(this.family);
  }

  public showProtectedData() {
    console.log(this.protectedData);
  }
}

console.log(adam.family); // エラー
console.log(taro.family); // エラー
console.log(hanako.family); // エラー

console.log(adam.showFamily()); // ["イブ", "ソーン"]
console.log(taro.showFamily()); // ["次郎", "三郎"]
console.log(hanako.showFamily()); // ["花女", "花男"]

console.log(adam.protectedData); // エラー
console.log(taro.protectedData); // エラー

console.log(adam.showProtectedData()); // これが人類にだけ教えてあげる...
console.log(taro.showProtectedData()); // これが人類にだけ教えてあげる...

このように、外からはアクセスできないが、そのクラス(継承も含む)内であればアクセスできるということです。

...というよくわからない説明がきっとサバイバルガイドだと分かりやすいと思うので貼っておきます。

これらのアクセス修飾子をうまく使い分けることで、プログラムの安全性をいい感じに高められます。

タイルをマップに埋め込む

という訳で埋め込むめに必要な情報は何でしょうか...

まずはtile.ts内のTileSizeFakeTileRealTilefakeTileToRealTileを全てexportしてから、map.tsにインポートしていきます。

src/lib/map.ts
import { TileSize, FakeTile, RealTile, fakeTileToRealTile } from "./tile";

マップ上にタイルを描画するために必要な情報はFakeTile(タイルマップへの画像パスと、そのタイルマップ内でのタイルを示す座標)に加えて、マップのどこに描画するかです。
この 2 の情報をまとめたオブジェクト型MapTileを作成しましょう。

type MapTile = {
  x: number;
  y: number;
  tile: FakeTile;
};

では、GameMapクラスにtilesプロパティを追加しましょう。

abstract class GameMap {
  protected tiles: MapTile[][] = [];
}

今回はMapTile[][]として、2 次元配列でタイルを管理します。
理由は、マップタイルをレイヤー構造で管理するためです。(後で必要になったときに便利。)

まだ、このMapTileFakeTileで、描画はこのままではできません。
そこで、同様にRealTileを含んだDrawableTile型を作成しましょう。

type DrawableTile = {
  x: number;
  y: number;
  tile: RealTile;
};

そして、GameMap内でMapTile[][]DrawableTile[][]に変換するメソッドを追加しましょう。

abstract class GameMap {
  protected tiles: MapTile[][] = [];
  protected realtiles: DrawableTile[][] = [];

  protected generateMapTiles() {
    for (let l = 0; l < this.tiles.length; l++) {
      if (!this.realtiles[l]) this.realtiles[l] = [];
      for (let t = 0; t < this.tiles[l].length; t++) {
        const tile = this.tiles[l][t];
        fakeTileToRealTile(tile.tile).then((realTile) => {
          this.realtiles[l][t] = {
            x: tile.x,
            y: tile.y,
            tile: realTile,
          };
        });
      }
    }
  }
}

こんな感じでしょうか?
forループを使って、MapTile[][]DrawableTile[][]に変換しています。
if (!this.realtiles[l]) this.realtiles[l] = [];は、realtilesの現在のレイヤーが存在しない場合に、新しく配列を作成しています。
そうでないと、存在しない要素にアクセスしようとしてエラーが発生してしまいます。

また、fakeTileToRealTile(tile.tile).then((realTile) => { ... }として、fakeTileToRealTile関数をthenで実行しています。
これは、awaitを使わないでasync関数を実行する方法の 1 つで、そのasync関数が終わったらthen内の処理を実行するというものです。

描画を試すためにメイン処理を書いてみる

今まで手つかずだったindex.htmlmain.tsを書いていきます。

index.html
<!doctype html>
<html lang="ja-JP">
  <head>
    <meta charset="UTF-8" />
    <link rel="stylesheet" href="/src/styles.css" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>qac24-sample</title>
    <script type="module" src="/src/main.ts" defer></script>
  </head>

  <body>
    <!-- メインのゲーム画面を表示するCanvas -->
    <div id="main">
      <canvas id="game" width="960" height="960">
        HTML5 Canvasがサポートされていません。<br>
        OSのアップグレードをご確認ください。
      </canvas>
    </div>
  </body>
</html>
src/main.ts
import { Game } from "./lib/game";

let game: Game;

const main = async () => {
  game = new Game();
};

window.addEventListener("DOMContentLoaded", () => {
  main();
});

出来上がったのがこちらです。

更に、なんか適当なマップを作ってみましょう。

srcmapsフォルダを作成し、その中にsample.tsを作成します。

src/maps/sample.ts
import { GameMap } from "../lib/map";

あ、GameMapの export を忘れていましたね。

src/lib/map.ts
export abstract class GameMap {
  ...
}

GameMapを継承したSampleMapクラスを作成しましょう。
継承はextendsを使います。
なお、継承の際のルールとして、abstractなメソッドやプロパティがある場合は、それらを実装する必要があります。今回はないですね。

src/maps/sample.ts
import { GameMap } from "../lib/map";

export class SampleMap extends GameMap {
  protected tiles = [[
    {
      x: 0,
      y: 0,
      tile: {
        path: "/src/assets/rpg/Graphics/Tilesets/001-Grassland01.png",
        x: 1,
        y: 3,
        width: 1,
        height: 1,
      },
    },
    {
      x: 1,
      y: 0,
      tile: {
        path: "/src/assets/rpg/Graphics/Tilesets/001-Grassland01.png",
        x: 1,
        y: 3,
        width: 1,
        height: 1,
      },
    },
    {
      x: 0,
      y: 1,
      tile: {
        path: "/src/assets/rpg/Graphics/Tilesets/001-Grassland01.png",
        x: 1,
        y: 3,
        width: 1,
        height: 1,
      },
    },
    {
      x: 1,
      y: 1,
      tile: {
        path: "/src/assets/rpg/Graphics/Tilesets/001-Grassland01.png",
        x: 1,
        y: 3,
        width: 1,
        height: 1,
      },
    }
  ]]
}

これで、SampleMapという 4 マスしかないマップが作成されました。

※画像パスについては、お使いの環境に合わせてください。
 今回は Day 7 で解説した通り、RPG Maker XP の RTP を使用しています。

描画するためのdrawメソッドをGameMapに追加しましょう。

src/lib/map.ts
export abstract class GameMap {
  ...

  public draw() {
    const canvas = document.getElementById("game") as HTMLCanvasElement | null;
    if (!canvas) return;
    const ctx = canvas.getContext("2d");
    if (!ctx) return;

    ctx.clearRect(0, 0, canvas.width, canvas.height);

    this.realtiles.forEach((tiles) => {
      tiles.forEach((tile) => {
        tile.tile.draw(
          tile.x * TileSize,
          tile.y * TileSize,
          ctx
        )
      });
    });
  }

  ...
}

これで、SampleMapdrawメソッドを呼び出すことで、マップが描画されるようになりました。

src/main.ts
import { Game } from "./lib/game";
import { SampleMap } from "./maps/sample";

let game: Game;

const main = async () => {
  game = new Game();
  game.changeMap(SampleMap);
  game.map?.generateMapTiles();
  game.map?.draw();
};

window.addEventListener("DOMContentLoaded", () => {
  main();
});

なんかしらん?が出てきましたね。

先ほどのgame.tsの定義内で、mapプロパティをGameMap | nullとしていました。
そのため、mapnullの場合もあります。
その場合、mapのメソッドを呼び出すとnullから存在しないものを呼び出しているとなってエラーが発生してしまいます。

そのため、?をつけることで、mapnullの場合はそのメソッドを呼び出さないようにしています。

そして、現在ではgenerateMapTilesprotectedになっているので、とりあえずお試しするためだけにpublicに変更しておきましょう。

src/lib/map.ts
export abstract class GameMap {
  ...

  public generateMapTiles() {
    ...
  }

  ...
}

では、pnpm tauri devを実行して、確認してみましょう。

...

あれ?
画面が真っ白ですよ?

まとめ

現在まで適切にかけていれば、エラー無く動作すると思います。
しかしながら、エラーはないものの、描画されているはずのタイルが描画されていません。

これは、画像の読み込みが完了していないためです。
いやいや、generateMapTilesfakeTileToRealTilethenで実行しているじゃないかと思うかもしれませんが...これは特殊な処理なのです。

という訳で、次回は非同期処理の話をさらに深めていきます。

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?