マップにタイル(の情報)を埋め込む
前回の記事でクラスについて触れていきましたが、「次回説明する」と言って省略したアクセス修飾子について触れていきます。
アクセス修飾子
前回の記事で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
クラスを作成しました。
まず、アクセス修飾子にはprivate
、protected
、public
の 3 つがあります。
-
private
:そのクラス内でのみアクセス可能 -
protected
:そのクラスとそのサブクラス内でのみアクセス可能 -
public
:どこからでもアクセス可能
今回、他人に知られてはいけない情報として、privateData
とmyNumber
をprivate
で宣言しています。
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
内のTileSize
、FakeTile
、RealTile
、fakeTileToRealTile
を全てexport
してから、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 次元配列でタイルを管理します。
理由は、マップタイルをレイヤー構造で管理するためです。(後で必要になったときに便利。)
まだ、このMapTile
はFakeTile
で、描画はこのままではできません。
そこで、同様に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.html
とmain.ts
を書いていきます。
<!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>
import { Game } from "./lib/game";
let game: Game;
const main = async () => {
game = new Game();
};
window.addEventListener("DOMContentLoaded", () => {
main();
});
出来上がったのがこちらです。
更に、なんか適当なマップを作ってみましょう。
src
にmaps
フォルダを作成し、その中にsample.ts
を作成します。
import { GameMap } from "../lib/map";
あ、GameMap
の export を忘れていましたね。
export abstract class GameMap {
...
}
GameMap
を継承したSampleMap
クラスを作成しましょう。
継承はextends
を使います。
なお、継承の際のルールとして、abstract
なメソッドやプロパティがある場合は、それらを実装する必要があります。今回はないですね。
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
に追加しましょう。
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
)
});
});
}
...
}
これで、SampleMap
のdraw
メソッドを呼び出すことで、マップが描画されるようになりました。
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
としていました。
そのため、map
がnull
の場合もあります。
その場合、map
のメソッドを呼び出すとnull
から存在しないものを呼び出しているとなってエラーが発生してしまいます。
そのため、?
をつけることで、map
がnull
の場合はそのメソッドを呼び出さないようにしています。
そして、現在ではgenerateMapTiles
がprotected
になっているので、とりあえずお試しするためだけにpublic
に変更しておきましょう。
export abstract class GameMap {
...
public generateMapTiles() {
...
}
...
}
では、pnpm tauri dev
を実行して、確認してみましょう。
...
あれ?
画面が真っ白ですよ?
まとめ
現在まで適切にかけていれば、エラー無く動作すると思います。
しかしながら、エラーはないものの、描画されているはずのタイルが描画されていません。
これは、画像の読み込みが完了していないためです。
いやいや、generateMapTiles
でfakeTileToRealTile
をthen
で実行しているじゃないかと思うかもしれませんが...これは特殊な処理なのです。
という訳で、次回は非同期処理の話をさらに深めていきます。