3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

サイバーエージェント24卒内定者Advent Calendar 2023

Day 4

ゲームの仕様をコードで表現するTips!!

Last updated at Posted at 2023-12-03

はじめに

 こんにちは!サイバーエージェント24卒内定者 Advent Calendarの4日目の記事を担当する原島由和と申します!
職種はクライアントエンジニアで、普段は専門学校にてゲーム開発をしております。

今回は設計パターンを通して "モデリング” に着目してみたいと思います。

コードで表現(モデリング)って何?

しばしばデザインパターンは開発シーンの ”テクニック” としてその手法にスポットライトが当てられますが、真の目的はプロダクトの要件に沿って仕様をコードやゲームエンジンに落とし込んでいくこと、すなわち ”モデリング” だと言えます。
(DDDにおけるドメインモデリングと似た思想ですね)

ゲームなどの複雑なソフトウェアの仕様や概念を正しく表現するというのはとても難しいです。
本来は目的の手段としてデザインパターンや設計思想はあるべきなのですが、手段を優先して無理やり仕様に当てはめようとした場合、いっけん小綺麗だけど使いにくいナニカが生まれてしまうことも…
(ドメインモデル貧血症)

設計やアーキテクチャを採用する際は仕様を理解してこのモデリングを正しく行えるかが鍵と言えます。

実装

今回は想定して仕様に沿うことを意識して、ゲームのオブジェクトを2つ表現してみました。
Unity,C#を想定しています。

値オブジェクトで通貨を表現する

まずは、ゲームでよく登場するゴールドの概念をコードで表現してみたいと思います!

何も考えずにやった場合

以下はRPGゲームのプレイヤークラスで、所持金を意味するint型の変数所持金の値を変更するメソッドを持っています。また、ゴールドは0以上の正の整数で表現されます。

一見なんの変哲もないコードですし、正しく動作します。

using System;
using UnityEngine;

public class Player : MonoBehaviour
{
    // 所持金
    int _gold = 0;

    // 所持金の値を変更する
    public void ChangeMoney(int gold)
    {
				// 0以下の場合は0として扱う
        _gold = _gold > 0 ? gold : 0 ; 
    }
}

では、ゴールドの最大数を10000に制限する仕様変更が起きた場合はどうなるでしょうか。

以下のようにゴールドの値を変更するメソッド内の条件を変更することで対応可能です。

// 所持金の値を変更する
    public void ChangeMoney(int gold)
    {
				// 0から10000で値を制限
        _gold = Mathf.Clamp(gold,0,10000);
    }
}

ゴールドを意味する変数を持つクラスがPlayerだけなら変更を容易いですが、これが商人、敵、宝箱・・・などと複数ある場合は大変です。

ゴールドの値を変更しているクラス、箇所を特定して全ての条件に該当のコードを追加していく地獄の作業が待っています。

値オブジェクトを活用する

ゴールドの概念を値オブジェクトというパターンを用いてモデリングするとこうなります。

金額の値となるint型のValueを持ち、初期化時にそれを決定します。Changeメソッドなども持たないためあとからValueの値を変更することはできません。

Add関数はValueの値を足して新たなGoldクラスのインスタンスを返す処理を行っています。

using System;

public class Gold : IEquatable<Gold>
{
		// ゴールドの額
    public readonly int Value;

    public Gold(int value)
    {
        // 値が0未満、10000以上の場合はエラーを出力する
        if (value < 0 || 10000 < value)
        {
            throw new ArgumentException("valueが不正な値です");
        }
        Value = value;
    }

		// ゴールドを足す関数
    public Gold Add(Gold arg)
    {
        if(arg == null)
        {
            throw new ArgumentException();
        }
        return new Gold(Value + arg.Value);
    }

    public bool Equals(Gold other)
    {
        if (ReferenceEquals(null, other))
        {
            return false;
        }
        else if (ReferenceEquals(this, other))
        {
            return true;
        }

        return this.Value == other.Value;
    }

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(null, obj) || obj.GetType() != this.GetType())
        {
            return false;
        }
        else if (ReferenceEquals(this, obj))
        {
            return true;
        }
        return Equals((Gold)obj);
    }

    public override int GetHashCode() { return Value.GetHashCode(); }
}

以下はGoldを変数に持つPlayerクラスです。

using UnityEngine;

public class Player : MonoBehaviour
{
    // 所持金
    Gold _gold = new Gold(0);

    // 所持金の値を変更する
    public void ChangeMoney(Gold gold)
    {
        Gold result = _gold.Add(gold);
        _gold = result;
    }
}

以下は値オブジェクトを使用するメリットです。

  • 不変である
  • 振る舞いを持つことが出来る
  • 不正な値を排除できる

ひとつずつ説明していきます。

不変である

値オブジェクトの特徴として、変化せずに代入によってのみ値が変化します。

所持金のGold変数のValueが自分の意図していないタイミングで変更されることはありません。

しかしながら、値を変更したい場合は毎度インスタンスを生成する必要があるためパフォーマンスの観点から見ると微妙というデメリットも同時に存在します。

振る舞いをもつことができる

値オブジェクトはただのデータコンテナではなくクラスなので振る舞いを定義することが可能です。

ドメインにできること逆にできないことを自由に表現することが可能です。これはプリミティブな型にはない強みといえます。

不正な値を排除できる

コンストラクタでValueに入る値を制限することで不正な値を検知し処理することが可能です。

冗長に不正な値を防ぐ条件分岐を書く必要はなく値オブジェクトのインスタンスを生成するだけでよいので非常に楽です。

Goldクラスを見るとAdd関数がありGold同士を足したインスタンスを返していますし、0以上10000以下で値が制限されていることが分かります。

これはプリミティブな型にコメントを書くよりも、コードを後から見た人や利用する人にゴールドの要件と役割が正しく伝わりやすくなると言えるでしょう。

エンティティとしてユーザーを表現する

プレイヤーは以下の要件を持ちます

  • 名前、IDなどのパラメータを持つ
  • インスタンスがユニーク
  • ライフサイクルを持つ(アカウントの発行、削除)

プレイヤーは先ほどの通貨と大きく仕様が異なります。従って実装も変更します。

using System;

public class User : IEquatable<User>
{
    public readonly Guid ID;

    public string Name { get; private set; }

    public User(string name)
    {
        ID = Guid.NewGuid();
        Name = name;
    }

    public void SetName(string name)
    {
        if (name == "不適切")
        {
            return;
        }

        Name = name;
    }

    public bool Equals(User other)
    {
        if (ReferenceEquals(null, other)) return false;
        if (ReferenceEquals(this, other)) return true;
        return ID == other.ID && Name == other.Name;
    }

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(null, obj)) return false;
        if (ReferenceEquals(this, obj)) return true;
        if (obj.GetType() != this.GetType()) return false;
        return Equals((User)obj);
    }

    public override int GetHashCode()
    {
        return HashCode.Combine(ID, Name);
    }
}

例え同姓同名でも違うユーザーとして扱っています。

また、ただの代入ではなく振る舞いとして名前を変更することで名前の変更を許しています。

先ほどのGoldとは違い、

  • 可変である
  • 値が同一でも違うオブジェクトである

という点を意識してモデリングをしました。

まとめ

いかがだったでしょうか。モデリングは言語やエンジンの性質を活かした記法やパターンを知ることと同じかそれ以上に大切なことだと思います。

実際のソフトウェアは今回の例より複雑ですが、仕様に沿った正しい表現を意識することで開発のしやすい自然な設計を目指す助けになれば幸いです!

参考

https://little-hand-s.notion.site/10-f79853f8e1e54f429e14ad9cecc87f35

成瀬允宣著 ドメイン駆動設計入門 ボトムアップでわかる! ドメイン駆動設計の基本(2020)

3
1
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
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?