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 9

【Day9】Mapを作り始めよう【QAC24】

Last updated at Posted at 2024-12-08

やった!オブジェクト指向プログラミングだよ!!!

/src/lib/map.ts を作成しましょう。

map.ts
class GameMap {}

GameMap というクラスを作りました。
(間違っても Map という名前のクラスを作るんじゃあないぞ!)

クラスというのは、オブジェクト指向プログラミングにおいて非常に重要な概念です。

クラスは、オブジェクトの設計図のようなもので、オブジェクトを作るための型を定義します。(by Copilot)

クラスを使うことで、カプセル化や継承なんかの概念を使うことができます。

まぁまずは Class についてもっとわかりやすく説明するところから始めましょう。

クラスとは?

課題
分かりやすく説明するために、今からあなたにはこの世界の神様になってもらいます。
さぁ!早く TypeScript で生き物を創造してください!

えっと...猫と犬と人間のクラスを作るか...

THE_WORLD.ts
class Cat {
  public name: string = "ねこ";
  public legs: number = 4;
  private foods: string[] = ["さかな", "ねずみ"];
  private reward: string = "またたび";

  public meow() {
    console.log("にゃー");
  }

  public sleep() {
    console.log("ぐー");
    this.energy += 1;
  }

  public walk() {
    position += 1;
  }

  public feed(food: string) {
    if (this.foods.includes(food)) {
      console.log("おいしい!");
    } else {
      console.log("おいしくない...");
    }
  }
}

class Dog {
  public name: string = "いぬ";
  public legs: number = 4;
  private foods: string[] = ["にく", "どっぐふーど"];
  private reward: string = "ほね";

  public bow() {
    console.log("わんわん");
  }

  public sleep() {
    console.log("ぐー");
    this.energy += 1;
  }

  public walk() {
    position += 1;
  }

  public feed(food: string) {
    if (this.foods.includes(food)) {
      console.log("おいしい!");
    } else {
      console.log("おいしくない...");
    }
  }
}

class Human {
  public name: string = "ひと";
  public legs: number = 2;
  private foods: string[] = ["かんぜんえいようしょく", "やさい"];
  private reward: string = "ゆうきゅう";

  public quote() {
    console.log("我々は神が想像した生き物の中で最も偉大なものである。");
  }

  public sleep() {
    console.log("我々は決して眠る事は許されない。");
  }

  public walk() {
    position += 1;
  }

  public feed(food: string) {
    if (this.foods.includes(food)) {
      console.log("大変喜ばしい事である。我は主に最大限の感謝を示す。");
    } else {
      console.log("主が提供してくれた物であるため、どのような物でも我は喜んで頂こう。");
    }
  }
}

privatepublicについては次回の記事でお話します。

こんな感じ?

あーあ。
確かに間違ってはいません。
いや...人間の表現がなんかあれなんだけど...

これら全てのクラスは大まかに同じような構造を持っています。
それだったら、Animalといったクラスを作って、そこから継承すれば、それぞれのクラスの共通部分をまとめたり、それぞれのクラスにおける互換性を持たせたりもできます。

THE_WORLD.ts
abstract class Animal {
  public abstract name: string;
  public abstract legs: number;
  protected abstract foods: string[];
  protected abstract reward: string;

  protected abstract message: string;

  public quote() {
    console.log(this.message);
  }

  public abstract sleep(): void;

  public walk() {
    if (this.legs > 0) {
      position += 1;
    } else {
      throw new Error("足がないので歩けません。");
    }
  }

  public feed(food: string) {
    if (this.foods.includes(food)) {
      console.log("おいしい!");
    } else {
      console.log("おいしくない...");
    }
  }
}

これで、全ての生き物が継承するべきAnimalクラスができました。
さらにここに猫、犬、人間を追加してみましょう。

THE_WORLD.ts
...

class Cat extends Animal {
  public name: string = "ねこ";
  public legs: number = 4;
  protected foods: string[] = ["さかな", "ねずみ"];
  protected reward: string = "またたび";

  protected message: string = "にゃー";

  public sleep() {
    console.log("ぐー");
    this.energy += 1;
  }
}

class Dog extends Animal {
  public name: string = "いぬ";
  public legs: number = 4;
  protected foods: string[] = ["にく", "どっぐふーど"];
  protected reward: string = "ほね";

  protected message: string = "わんわん";

  public sleep() {
    console.log("ぐー");
    this.energy += 1;
  }
}

class Human extends Animal {
  public name: string = "ひと";
  public legs: number = 2;
  protected foods: string[] = ["かんぜんえいようしょく", "やさい"];
  protected reward: string = "ゆうきゅう";

  protected message: string = "我々は神が想像した生き物の中で最も偉大なものである。";

  public sleep() {
    console.log("我々は決して眠る事は許されない。");
  }
}

このようにextendsを使って、Animalクラスを継承することで、それぞれのクラスに共通する部分をまとめることができます。
その際、abstractを使って、Animalクラスでは定義しなかったメソッドやプロパティについては、それぞれのクラスで実装しなければなりません。

さらに言うと、Animalクラスはnew Animal()としてインスタンスを生成することができません。
これは、Animalクラスが抽象クラスであり、「こんな感じの設計図を作ればいいと思うよ」という先輩からのアドバイスみたいなものだからです。

ちなみにこのクラスを使うためにはnewを使ってインスタンス化する必要があります。

const cat = new Cat();
const dog = new Dog();

こうすることで、それぞれのクラスのインスタンスを生成することができます。
インスタンス化とは、設計図から実際のオブジェクトを作ることです。

以上これがクラスの概要です。

じゃあマップを作ってよ

言われなくても作ります。

まずは、さっきのもぬけの殻のGameMapクラスにをabstractにしたうえで、必要そうなものを考えていきましょう。

map.ts
abstract class GameMap {}

はい、今回の記事ではこれが全てです。

マップを呼び出す処理を書きたい

後はタイル情報の埋め込みと、キャラクター移動処理、コリジョンの設定などなどなのですが、そこらへんは別の記事になってしまいます。
でも少しづつ現実味を帯びていますよね。

余った時間で、ゲームを管理するためのクラスGameを作っていきましょう。
これは、1 ゲームにつき 1 つだけ存在するべきクラスとなります。

src/lib/game.ts
class Game {}

このGameには、現在のマップと、マップ切り替えメソッドを追加したいですね。
という訳で、先ほど作ったGameMapが型となるmapプロパティを追加しましょう。

src/lib/game.ts
class Game {
  public map: GameMap | null = null;

  public changeMap(map: GameMap) {
    this.map = map;
  }
}

しかし、おおよそこれではエラーが出るかと思います。
なぜなら、GameMapというものがどこにあるのか、game.tsがそれを知らないからです。
なので、インポートしてあげましょう。

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

class Game {
  public map: GameMap | null = null;

  public changeMap(map: GameMap) {
    this.map = map;
  }
}

これで、GameクラスがGameMapクラスを認識できるようになりま...あれ?
と、TypeScript でこのようにモジュール化したりするときに、忘れてはいけないことがあります。
それはexportです。
指定したものを、別のファイルなどで使用できるようにするには、exportを使ってエクスポートする必要があります。

map.tsGameMapクラスにexportを追加しましょう。

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

これで、GameクラスがGameMapクラスを認識できるようになりました。

が、GameMapは抽象クラスで、インスタンス化されることはありません。
なのにchangeMapメソッドでGameMapを引数に取っているのはおかしいですね。
この「GameMapが引数」なのを「GameMapの継承クラスを引数」にしましょう。

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

class Game {
  public map: GameMap | null = null;

  public changeMap<T extends GameMap>(map: T) {
    this.map = map;
  }
}

これで、GameMapの継承クラスを引数に取ることができるようになりました。
extendsというのは、クラスの継承の意味合いで、実際にGameMapを継承した別のクラスを定義するときはclass NewGameMap extends GameMap {}のように書きます。

で、Tです。
ちょっと前の記事でPromise<T>も出てきましたね。

ジェネリクスとは

ジェネリクスを用いると、型の安全性とコードの共通化を両立することができます。
ジェネリクス (generics) | TypeScript 入門 『サバイバル TypeScript』

ジェネリクスは、型を概念的に扱うような感じで...うん...

じゃあ例えば引数として受け取った値をそのまま返す関数returnSuruzeを作ってみましょう。

const returnSuruze = (value) => {
  return value;
};

この関数の処理がどんなのなのかは多分見ればわかると思います。
まぁ、今扱ってるのは TypeScript なので型を指定してあげましょう。

const returnSuruze = (value: string): string => {
  return value;
};

はい、これで文字列を引数にして、文字列を返す関数になりました。
でもこれだと数字とかだとエラーが出ますね。

const returnSuruze = (value: string | number): string | number => {
  return value;
};

型1 | 型2みたいな表記は Union Types といって、型1または型2のどちらかを受け取ることができるようになります。
つまりこれは、文字列か数字を受け取って、文字列か数字を返す関数になります。
なんかいい感じに見えますよね。

const returnSuruze = (value: string | number): string | number => {
  return value;
};

console.log(returnSuruze(2) + 4);

はい、returnSuruzeに数字を渡して、それがそのまま返ってきてその数値に 4 を足して...え?エラー?

演算子 '+' を型 'string | number' および 'number' に適用することはできません。ts(2365)

なぜなら、returnSuruzeが返すのはstringnumberのどちらかで、それは決定しないからです。

これじゃあ不便ですよね?

え?こうすればいいじゃないかって?

const returnSuruzeString = (value: string): string => {
  return value;
};

const returnSuruzeNumber = (value: number): number => {
  return value;
};

console.log(returnSuruzeNumber(2) + 4);

なるほど。
これならエラーは起きませんね。

でも、これだと型の数だけ関数を作らないといけないですよね。
オブジェクトなども含めると少なくとも無限に関数を作らなければなりません。

そこで、ジェネリクスを使います。

const returnSuruze = <T>(value: T): T => {
  return value;
};

console.log(returnSuruze(2) + 4);

<T>というので、Tという型を作り上げます。現在、この型は何でも受け入れます。
そして、引数valueと返り値の両方にTを指定しています。
そのため、引数valueと返り値は同じ型になります。
そのため、例で 2 というnumber型を渡しているので、返り値の型もnumberになる...というわけなのです。

先ほどは何でも受け入れると言いましたが、Tに制約を加えることもできます。

const returnSuruze = <T extends string | number>(value: T): T => {
  return value;
};

これで、Tstringnumberのどちらかになります。
それでもvalue: string | numberの時とは異なり、返り値は引数valueと同じ型であることが保証されているため、エラーは起きません。

このTは、自ら関数呼び出し時に指定することもできます。

console.log(returnSuruze<string>("Hello, World!"));

このように、関数名<ジェネリクス型>(引数)という形で、いつもの関数呼び出しに少し手を加えるだけで、ジェネリクス指定も出来ます。

ちなみに、Typeの頭文字をとって良くTと書かれますが、他の文字でも構いません。UとかKとかVとか...。1 文字じゃなくても。

これでGameMapの継承型を受け付けてる...?

そうなんですが、現在のコードでは「インスタンス化されたGameMapを継承したクラス」が引数となります。
わざわざインスタンス化してから引数に渡してやるのは面倒ですよね。
(プログラミング的思考:「インスタンス化」という共通項目があるならそれを関数に落とし込むべきである)

なので、インスタンス化していない状態のGameMapを引数に取るようにしましょう。

class Game {
  public map: GameMap | null = null;

  public changeMap<T extends GameMap>(map: new () => T) {
    this.map = new map();
  }
}

引数がnew () => Tとなっています。
これは、「インスタンス化したらT型になるクラス」というような意味合いです。

これで、GameMapのインスタンス化前継承クラスを引数に取ることができるようになりました。

このGameクラスは絶対外で使うので、exportしておきましょう。

export class Game {
  public map: GameMap | null = null;

  public changeMap<T extends GameMap>(map: new () => T) {
    this.map = new map();
  }
}

まとめ

これでオブジェクト指向プログラミングの基本的な概念と、昨今流行のジェネリクスについて学べましたね。
最近の多くの言語にはジェネリクスが搭載されているので、ぜひ使いこなせるようになっておきましょうね~

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?