ぱぴブログ

ぱぴブログ

プログラミング好きの大学生のひとりごと

UnityでRPGを作りたい話

はじめに

この記事は私の所属する大学のアドベントカレンダー12/23の記事です。

adventar.org

みなさんこんにちは。
中学からの友達とスマブラしながら話をするのが最高に楽しかったさんたろーです。

戦車ゲームが完成し、次のゲーム制作にとりかかっています。
戦闘システムは、よくあるコマンド選択型RPGにするつもりです(スマホでもプレイできるようにしたい)。
今はゲーム仕様がある程度固まり、基礎となるキャラクターのクラスを作成している段階です。

前回の戦車ゲームはろくに仕様定義や設計をせずに、行き当たりばったりの開発でした。
しかし今回はそうはいきません。
なんせ、扱うキャラクター数も、ひとつのキャラクターが持つパラメータ数もとても多いです。

というわけで、膨大な数のキャラクター・パラメータを扱うための試行錯誤が今回の内容です。

GitHubに開発中のコード、仕様などを書いたDocumentを載せています。

github.com

キャラクターの育成要素(仕様)

RPGで欠かせないのがキャラクターの育成要素だと思います。
育成要素の仕様によって、持つべきパラメータなども大きく変わってきますからね。

レベルアップ

キャラクターが一定の経験値を獲得したら、レベルが上がるというシステム。
基本的には最大ステータスの上昇やスキルの獲得が伴います。

しかしですね、仮に最大レベルを100として全キャラクターの各レベルのステータスを設定するとしたら...
とてつもない作業量です。
「1レベル上がるごとに一定量上がるようにすればいい」
というとみんな同じようなステータスになったり、バランス調整が大変です。

よって最大ステータスはレベルに関係なく固定とすることに。

代わりに、「最大HP+5%」などパッシブスキルを特定のレベルで獲得できるようにしました。
また、「ファイア」などの技スキルも同様に特定のレベルで獲得するように。

進化と限界突破

「進化」と「限界突破」も、RPGでよく見かけるシステムです。
しかし、この「進化」システムはやっかいです。

Aさんは頑張って手に入れたモンスターXをレアアイテムを使って限界突破数を最大にしました。
アップデートにより、モンスターXの進化が可能になりました。
しかし、モンスターXを進化すると、限界突破数が初期化されてしまう...

このように、進化システムには難しい部分があります。
仮に限界突破数を継続させるとしたら、進化後のモンスターの育成難度が低くなります(それもアリですが)

これの実質的な解決方法は、育成されたモンスターX0とは別にモンスターX1を手に入れ、モンスターX1を進化させることです。
つまり、進化前モンスターの入手難度を下げ、進化する難度を上げる(進化素材の入手難度など)。
こうすれば、進化の難度を下げないまま、不満も減らせるでしょう。

私も最初、この方法を用いて、進化システムを実装しようと考えていました。

しかしですね、進化システムは結構必要なパラメータが多いんですよね...
進化先の数、進化後のID、進化素材のID群、必要Gold等...

んーーー。進化無くてヨシッ!!
というわけで、心おきなく限界突破機能だけ付けることにしました。
ちなみに限界突破は同キャラ合成による最大Lvの上昇です。
スキル獲得型の仕様と上手くマッチしてます。

Unityで大量生産したいとき、継承が不便(実装)

いざキャラクターのクラスを作ったとき、大きく分けて、「自分が持つキャラクター」と「敵キャラクター」に分かれると考えました。
そうなったとき私はCharacterクラスを継承したPlayerCharacterクラスとEnemyCharacterクラスを用意。
つまり3クラス実装しようと考えました。...安直ですね。

C#のクラスとしては、これで問題なかったかもしれません。
しかし、実際にUnityでキャラクターを大量生産しようとすると問題が生じます。

f:id:papyrustaro:20191218171729p:plain
PlayerCharacter.csに継承元のパラメータも表示されている

f:id:papyrustaro:20191218171820p:plain
EnemyCharacter.csも同様

若干パラメータが違うのは許してください(過去のスクショを漁ってきました)
コンストラクタによる初期化なら問題ないかもしれませんが、Inspector上からの初期化だと上の画像の状態だと問題が生じます。

たとえば、スライムのIDを1としましょう。
基礎パラメータや属性などは敵も味方も変わりません。
しかし、上の画像の状態では、IDや基礎パラメータですら一意に定めることができません。

「えーっと、キングスライムのIDは...3だから...」
みたいな作業をインスタンスを作る度にしなければなりません。

これではだめです。私がやりたかったのは
「このダンジョンに出現するキングスライムのパラメータは2...、こっちは3...、プレイヤーが使用するキングスライムは5...」
といったことです。

結局どうしたか。
継承しません。
PlayerCharacter.csとEnemyCharacter.csの中から、そのインスタンスにあるCharacter.csにアクセスします。

こうすることで、Character.csのみをアタッチしたprefabから、EnemyCharacter.csまたはPlayerCharacter.csを追加でアタッチしたprefabを派生する作業ができます。

f:id:papyrustaro:20191218173744p:plain
基礎となるPrefab

f:id:papyrustaro:20191218173815p:plain
基礎から派生した敵スライム0

f:id:papyrustaro:20191218173837p:plain
基礎から派生したプレイヤーが所持するスライム

これならたくさんのキャラクターを生産できそうですね(それでも大変ですが泣)

列挙型(実装)

Unityにおいて列挙型はとても便利なものに感じます。
Inspector上から、一目瞭然だからです。
もはやマジックナンバーは全て列挙型でいいのでは?と思ってしまいます。

しかし、ひとつ問題を抱えています。
キャラクターのIDを列挙型で管理しようとすると、列挙型の要素が多すぎてInspector上から探しにくいのです。
エディタ編集などの記事も探しましたが、解決方法が見つかっていません(´;ω;`)

f:id:papyrustaro:20191218184301p:plain
列挙型の例

f:id:papyrustaro:20191218184327p:plain
Inspector上から見やすい

ラムダ式によるパラメータ取得(実装)

C#にはgetter,setterという便利なパラメータへのアクセス機能が備わっていますが、
ラムダ式を使うことでもっと便利にアクセスできるかも、というお話。

詳しくはこちらなど↓

www.atmarkit.co.jp

特に読み取り専用のパラメータは記述が簡単になります。

public class Sample{
  private int hp;
  
  public int Hp => hp;
}

public class Main{
  private Sample s;

  public void AddHp(int value){
    s.Hp += value;
  }
}

できるだけ自動化する

たくさんのキャラクターのパラメータを入力するので、入力するパラメータが少ない方がいいのは当然のことです。
たとえばAの値がわかればBの値も決まる、といったときBの値もプレイヤーに入力させるのは作業が増えるだけでなくミスの元。

私が作っているキャラクターのパラメータでも、現在のLvと最大Lvはありません。
特定のレベルに必要な経験値は全キャラ同じで、最大Lvが違うようにしています。
このことにより、現在所持している経験値から、現在のLvが求められます。
そして、キャラのレアリティと限界突破数から最大Lv(最大獲得経験値)を求めています。

ここで、先ほどのラムダ式が役に立ちます。
たとえば、最大Lvをフィールドとして用意せず、メソッドから毎回求めてみましょう。
※引数があるほうが違いがわかりやすいので、別のクラスメソッドを呼んでます

//レベルを扱うクラス
public class LV{
  private const int MIN_LV = 60; //全キャラ60レベルまではいく
  public static int GetLV(int rarity, int exceedLimitNum){
    return MIN_LV + rarity * 10 + exceedLimitNum * 10;
  }
}

//普通にメソッドを呼ぶと、長い&引数考えるのたいへん
public class Sample1{
  private int rarity; //レア度
  private int exceedLimitNum; //限界突破数

  public void Start(){ //インスタンス化されたときに呼ばれる
    Debug.Log("MaxLV: " + LV.GetLV(this.rarity, this.exceedLimitNum));
  }
}

//ラムダ式を使うと、パラメータのように扱える
public class Sample1{
  private int rarity;
  private int exceedLimitNum;

  public int LV => LV.GetLV(this.rarity, this.exceedLimitNum));

  public void Start(){
    Debug.Log("MaxLV: " + LV);
  }
}

読み取り専用パラメータをラムダ式で定義する
これは、求めるのが面倒だが自明なパラメータに強力だと感じました

最後に

新しい知見をたくさん得られてとても楽しいですが、
ゲーム自体の進捗はまだまだです。

RPGはとても実装する機能が多く大変ですが、何とかリリースまで持っていけたら、と思っています。

特に大事だと思った点などは、できるだけ記事に残していくつもりです。

papyrustaro.hatenablog.jp

余談

実装は順調でしたが、ここにきて戦闘システムの仕様でかなり悩まされています。
そもそも私、RPG制作について何も知らない......。
というわけで、今はRPGの戦闘システムのノウハウを学んでいます。

開始当時「このシステムいいじゃん!これだ!」って感じでいただけに、少しテンション下がってます。
とりあえずRPGの制作が大変だということが、わかってきました...。

あまりにダメそうなら、潔くUnrealEngineでのアクションゲームに集中しようと思います。

うぉおお、はやく仕様決めて実装してぇえええ(´;ω;`)