📖

オブジェクト指向とゲーム開発

に公開

はじめに

こんにちは。
最近一段と冷え込み、加湿器が猛威を振るう季節となりましたが皆様いかがお過ごしでしょうか。

今年の4月にグループ会社にエンジニアとして新卒入社し、10月からSepteni Japan株式会社に出向中の佐藤大河と申します。最近のトレンドはコシヒカリでございます。

今回は入社時研修で学び、配属先の業務でも実際にお世話になっている「オブジェクト指向」について自分の中での整理も兼ねてこの場をお借りしたいと思います。

また、ただオブジェクト指向について語るのでは何だか味気ないため、今回は、オブジェクト指向と相性が良く、かつ私が個人的に学生時代にハマり、プログラミングを好きになるきっかけでもあった「ゲーム開発」と結びつけてまとめていきます。

オブジェクト指向

まずオブジェクト指向とは簡単に説明すると、オブジェクト(物) を主役としてプログラミングする設計・実装手法の考え方です。
適切なオブジェクト指向でデザインされたコードは、再利用性拡張性保守性などの点で非常に優れており、ゲーム開発だけでなく様々な場面で効果を発揮してくれます。

ゲーム開発

ここでまずゲーム開発の特徴を簡単に紹介いたします。
ゲームの世界にはプレイヤーなどのキャラクターをはじめ、建物やアイテムといったオブジェクト、ゲームロジックやサウンド、エフェクトやUI、などなど非常に多くの要素からなり、これらが複雑に絡み合います。
また特に現代のゲームは買い切り型とは違い、定期的なアップデートが入ることでバグの修正から新しいコンテンツの追加など、リリース後も様々な要素が増えていきます。

そのため、開発において拡張性と保守性などを保つために適切な設計が必要不可欠になります。
そこで登場するのが今回の主役「オブジェクト指向」です。

オブジェクト指向とゲーム開発

ゲームにおけるオブジェクト

ゲームの画面を想像してみてください。
あなたの目の前にいる踏めばぺちゃんこになる変なきのこも、あなたが持っているパンも、そこらへんに生えてる草までも全てが「物」です。画面いっぱいに「物」しかありません。
これらをゲーム世界に生み出していくにあたってあらゆる要素をオブジェクトとしてプログラムしていくオブジェクト指向は、ゲーム開発にとって我々の強い味方となってくれます。

オブジェクト指向の4つの要素

オブジェクト指向には、基本となる要素が主に4つ存在します。
それぞれオブジェクト指向を知るためには不可欠な要素なので、ゲーム開発の中でそれがどのような活躍をするのか交えながら紹介していきます。

1. カプセル化

データとそのデータを操作するメソッド(関数)を一つの「クラス」にまとめ、外部から直接アクセスできないようにすること

ゲームにおいてキャラクターは様々な性質を持ちます。
体力やレベルといった身体の状態や、歩く、攻撃するなどの動きの情報を外の世界から守り、意図しない変更から守りたいという時に活躍するのがカプセル化です。
もし守られた性質を見たり変更したいという場合は、以下のコードのようにそれ専用の出入り口となるメソッドを通すなどのルールを決めることで意図しない変更から守ってくれます。

Character.py
class Character: 
    def __init__(self, hp, level): 
        self.__hp = hp # キャラクターの体力を表すプライベート変数 
        self.__level = level # キャラクターのレベルを表すプライベート変数

    # プライベート変数への安全なアクセス
    def get_level(self): 
        return self.__level

    def set_level(self, level):
        self.__level = level

    # レベルアップに関する複雑な処理など
    def level_up(self):
        self.__level += 1
        self.__hp += 5

こちら正直初めは私も面倒だと思ってました。
オブジェクトの情報を見るためだけになんでわざわざそれ専用のメソッドを作らなきゃいけないんだと。
しかし結果これを軽視しパラメータへの直接参照を多用しまくった結果、ゲーム開始直後に主人公が死ぬクソゲーが出来上がりました。
それからはすっかり私はカプセル化さんの舎弟です。いつもお世話になっております。

2. 継承

既存のクラス(親クラス)の特性を引き継いで新しいクラス(子クラス)を作成すること

あなたは人間です。人間の可能性は無限大です。
あなたは戦士や魔法使いなど様々な職業に就職できますし、魔王を倒せば英雄になれます。もしかしたら魔王になれるかもしれません。
このようにある職業(人間)の能力を引き継いだ他の新たな職業(戦士、魔法使い、魔王etc...)を定義、作成したい場合この継承が非常に役に立ってくれます。
以下にご飯を食べて「美味しい」と喋る人間を作成し、その特性を引き継いだ剣を振り回す戦士と魔法を使う魔法使いを作成しました。

Human.py
class Human: # 親クラスとなる人間
    def eat(self): 
        print("yummy.")
Warrior.py
class Warrior(Human): # 人間の特性を継承する戦士 
    def cut(self): 
        print("I swing my sword.")
Wizard.py
class Wizard(Human): # 人間の特性を継承する魔法使い
    def magic(self): 
        print("I use magic.")

戦士も魔法使いもどちらも人間のため、ご飯を食べると「美味しい」と喋ります。
本来であれば戦士、魔法使いそれぞれにも「ご飯を食べる」という特性を付与するところですが、継承により一箇所のみの実装で実現しています。

3. ポリモーフィズム

同じインターフェース(メソッド名など)で異なる振る舞いを実現する仕組み

キャラクターは攻撃ができます。
突然ですがあなたにはきのこの友達ができました。
あなたは彼と共に旅に出て敵と喧嘩することになりました。
あなたはおそらく人間なので、お手持ちの剣を振り回したり相手に悪口を言って精神的ダメージを与えたりと色々できるでしょう。
しかし隣のきのこはどうでしょう。
きのこなので手がないので剣は振れませんし、驚くことに口もないので悪口すら言えません。その代わり彼には胞子を飛ばすなどあなたにはできない手段で攻撃できます。アイデンティティです。
このように、この「攻撃する」という共通の行動に対して、キャラクターごとにバリエーションを与えるために、ポリモーフィズムが活躍します。
以下は「攻撃する」という共通の行動をもったキャラクターである人間ときのこの実装です。

Character.py
class Character: 
    def attack(self):
        pass
Human.py
class Human(Character): 
    def attack(self):
        return "I swing my sword!"
Mushroom.py
class Mushroom(Character): 
    def attack(self):
        return "I fly spores!" 

同じ「攻撃する」という行動が、キャラクターによって「剣を振り回す」と「胞子を飛ばす」に2つの具体的な行動に変わっていることがわかります。
これにはメソッドのオーバーライドというかっこいい手法が鍵となるのですが、眠くなってくるのでここでの詳細な説明は割愛させていただきます。

4. 抽象化

具体的な詳細を隠し、重要な部分だけを公開する仕組み
抽象化により、「何をするか」に焦点を当て、「どう実現するか」を隠すことで、コードを扱いやすくします。

ゲームの世界には大量のアイテムが出てきます。
銃や剣から物干し竿まで、戦闘に使う武器だけでも無限のバリエーションが考えられます。
様々な種類がありますが、武器という運命を背負う彼らには逃れられない共通点があります。
ゲームによりますが、例えば武器の攻撃力やレアリティなどのパラメータなどや、壊れる、修繕するといった振る舞いなどです。
以下は武器に共通する特性を持つ武器抽象クラスと、それを使って実装された具体的な武器である剣クラスです。

Weapon.py
class Weapon(Item): # 武器を表す抽象クラス
    def __init__(self, name, power):
        self.name = name
        self.power = power

    def repair(self): # 武器が共通して持つ修繕するメソッド
        print(f"This {self.name} has been repaired.")
Sword.py
class Sword(Weapon): # 武器抽象クラスをより具体化
    def __init__(self): 
        super().__init__("Sword", 10)
Rifle.py
class Rifle(Weapon): # 武器抽象クラスをより具体化した銃クラス
    def __init__(self): 
        super().__init__("Rifle", 20)

このように武器やアイテムの種類に関わらず、共通の特性を一箇所に統一し扱えるようにし、それを使って具体的な実装をより簡単に行えるようにしています。
RPGを作った時は大量のアイテムやキャラクターを作成したため、この辺の方々には大変お世話になりました。

まとめ

以上、オブジェクト指向の基本的な原則とゲーム開発の特徴を合わせ、いかにオブジェクト指向が効果的かをここまで語りましたが、少しでも私の思いが伝わっていれば幸いです。
本当は今回の内容に合わせてドメイン駆動設計(DDD)とゲーム開発のお話もさせていただく予定だったのですが、これ以上書くと少々長くなってしまうのでまた次回のお楽しみとさせていただきたいと思います。

最後までお読みいただき、ありがとうございました。質問やフィードバックなどございましたら、ぜひコメント欄でお知らせください。

Discussion