Q&A
テーマ、知りたいこと
こんにちは。
いわゆる、RPGジャンルのゲームをいずれ作りたいなと考えており
その中で職業(クラス、ロール、ジョブ)を実装する際の仕組みを考えていました。
(職業ごとにステータスに差異があり、使えるスキルや特性が異なるようなイメージ。例えばドラクエの勇者や魔法使い、戦士など・・・)
まず思い浮かんだのが抽象クラスを使って
基底クラスから各職業(戦士、魔法使い、弓使いなど)のそれぞれのクラスを実装していくというものでした。
※画像引用:https://xr-hub.com/archives/19842
ただ、私がまずこれを思いつくという点でこれが最上の案ではないように思え
もっと良い方法があるのではないかと考えるようになりました。
なんと呼ぶのかわからないのですが
Unityのコンポーネントのように、「魔法のメラが使えるコンポーネント」みたいなものを
その人ができることとしてを1つずつ組み立てていく? など
他にも、何かしら良いアプローチが取れるような気がしています。
考え方の参考になるパターンや、キーワード、実装例、こういう機能も使えるなどがあれば意見をいただきたいです
よろしくお願いいたします。
気になる質問をクリップする
クリップした質問は、後からいつでもマイページで確認できます。
またクリップした質問に回答があった際、通知やメールを受け取ることができます。
下記のような質問は推奨されていません。
- 質問になっていない投稿
- スパムや攻撃的な表現を用いた投稿
適切な質問に修正を依頼しましょう。
回答7件
#1
総合スコア10905
投稿2023/02/10 03:59
編集2023/02/10 04:02enum CLASS{ Brave, Mage, Monk, Warrior } class Character { CLASS m_Class; }
まず,コレだと 具体的に 何が/どのように 問題となるのか? というのを明確にされると良いのではないでしょうか.
(何が必要か? 何が適しているか? というような話は,実際のゲームのルールに大きく依存すると思うので.)
#2
総合スコア110914
投稿2023/02/10 04:04
まずは全ジョブのアビリティを設定して、ジョブごとにどのアビリティをどのような成長速度でどのくらいの上限まで向上できるか設定すればよいのでは?
下記のような回答は推奨されていません。
- 質問の回答になっていない投稿
- スパムや攻撃的な表現を用いた投稿
このような回答には修正を依頼しましょう。
#3
総合スコア28309
投稿2023/02/10 04:35
職業やモンスターをそれぞれのクラスで実装するのは大抵の場合良くありません。
たとえば、戦士と魔術師の両方の能力を持つ上位職の魔法戦士というものを作りたくなったとしましょう。
これは戦士から派生するのが良いでしょうか?
それとも魔術師から派生するのが良いでしょうか?
正解は、戦士のスキルと魔術師のスキルをそれぞれクラスとして実装し、それを戦士、魔術師、魔法戦士のスキルとしてそれぞれのインスタンスに持たせることです。
クラスではなくプロパティにすることで、設定ファイルから読み込むことができるようになり、同じスキルを複数の職業に持たせることができるようになり、戦闘中にいちいち「もし魔術師もしくは魔法戦士もしくは賢者なら〜」と条件分岐しなくても良くなります。
下記のような回答は推奨されていません。
- 質問の回答になっていない投稿
- スパムや攻撃的な表現を用いた投稿
このような回答には修正を依頼しましょう。
#4
総合スコア579
投稿2023/02/11 21:50
編集2023/02/11 22:14(職業ごとにステータスに差異があり、使えるスキルや特性が異なるようなイメージ。例えばドラクエの勇者や魔法使い、戦士など・・・)
Unityのコンポーネントのように、「魔法のメラが使えるコンポーネント」みたいなものを
その人ができることとしてを1つずつ組み立てていく? など
ここらへんの仕様でZuishinさんの「スキルをクラスとして実装する」というアイデアを使いドラクエワードでUnityの機能使って考えてみましょうか。
スキルについて
まずスキル(特技・呪文)をコンポーネントっぽく…というかインスペクターにドラッグ&ドロップで抜き差しできるように作ります。
具体的にはScriptable Objectを使います。
スキルのinterface、ISkillを考えます。
戦闘などでスキル使うときはこのinterfaceを使う感じです。
とりあえずめっちゃ簡略化してこんな感じで。
C#
1public interface ISkill 2{ 3 // 使用に必要なコスト 4 int SPCost { get; } 5 // スキルを使用したときの実際の処理を書く。ダメージとか回復とか。 6 void UseSkill(); 7 // スキルを使用可能かどうか 8 bool IsUsable(int currentSP); 9}
そしてこのISkillを継承したスキルの基底クラス、SkillBaseをScriptableObjectで書きます。
C#
1public abstract class SkillBase : ScriptableObject, ISkill 2{ 3 [SerializeField] protected int spCost = 0; 4 public int SPCost => spCost; 5 6 public abstract void UseSkill(); 7 8 public virtual bool IsUsable(int currentSP) 9 { 10 return currentSP >= SPCost; 11 } 12}
で、このSkillBaseを使って、具体的なスキルをそれぞれ別クラスとして書きます。
C#
1// 通常攻撃 2[CreateAssetMenu(fileName = "NormalAttack")] 3public class NormalAttack : SkillBase 4{ 5 public override void UseSkill() 6 { 7 Debug.Log("通常攻撃!"); 8 Debug.Log("50ダメージ!"); 9 } 10}
C#
1// ばくれつけん 2[CreateAssetMenu(fileName = "BakuretsuKen")] 3public class BakuretsuKen : SkillBase 4{ 5 // 攻撃回数 6 [SerializeField] private int attackCount = 4; 7 public override void UseSkill() 8 { 9 Debug.Log("ばくれつけんをはなった!"); 10 for (int i = 0; i < attackCount; i++) 11 { 12 int damage = Random.Range(10, 30); 13 Debug.Log(damage + "ダメージ!"); 14 } 15 } 16}
C#
1// まじんぎり 2[CreateAssetMenu(fileName = "MajinGiri")] 3public class MajinGiri : SkillBase 4{ 5 // 会心の一撃が出る確率 6 [SerializeField] private float criticalRate = 37.5f; 7 8 public override void UseSkill() 9 { 10 Debug.Log("まじんのごとくきりかかった!"); 11 // 会心チェック 12 if (Random.Range(0, 100) < criticalRate) 13 { 14 Debug.Log("かいしんのいちげき!"); 15 Debug.Log("100ダメージ!"); 16 } 17 else 18 { 19 Debug.Log("ミス!"); 20 } 21 } 22}
そうするとこいつらはScriptbaleObjectなので、右クリックで作成することでAssetとして扱うことができます。
バランス調整がコードを変えずともエディターだけで可能です。
キャラクターについて
次にキャラクターを考えます。ステータスはHP、SPのみにします
キャラクターは3つのクラスに分けて考えます。
1. Statsクラス
キャラ名、基礎HP、基礎SPを持っている。キャラクターの基礎ステータス設定クラス。
2. Jobクラス
ジョブのHP係数、SP係数、スキル(ScriptableObject[])を持っている。ジョブ設定クラス。
3. Characterクラス
実際のキャラクターのクラス。ゲーム内で戦闘を行う。
HP、SP、スキル(ISkill[])、Statsクラス、Jobクラスを持っている。スキルを使って攻撃を行う。
Characterの中でStats * Job をかけ合わせて、キャラ性能を表現する感じですね。
まずStatsです。
Characterに設定できるように、ScriptableObjectで書きます。
C#
1[CreateAssetMenu(fileName = "Stats")] 2public class Stats : ScriptableObject 3{ 4 [SerializeField] private string characterName; 5 [SerializeField] private int baseHP; 6 [SerializeField] private int baseSP; 7 8 public string CharacterName => characterName; 9 public int BaseHP => baseHP; 10 public int BaseSP => baseSP; 11}
次はJobです。
Unityではinterfaceをインスペクターに登録することができません(シリアライズできないので)。
しかしinterfaceを継承させたオブジェクトをinterfaceにキャストして取り出すことで、実質的にインスペクターで扱うことが出来ます。
この方法を使って、Jobにスキルを持たせます。
C#
1[CreateAssetMenu(menuName = "Job")] 2public class Job : ScriptableObject 3{ 4 // ジョブ名 5 [SerializeField] private string jobName = "Job"; 6 // HP係数 7 [SerializeField] private float hpCoefficient = 1f; 8 // SP係数 9 [SerializeField] private float spCoefficient = 1f; 10 // スキル 11 [SerializeField] private ScriptableObject[] skills; 12 13 public float HPCoefficient => hpCoefficient; 14 public float SPCoefficient => spCoefficient; 15 16 // ScriptableObjectをISkillにキャストする。 17 public ISkill[] Skills 18 { 19 get 20 { 21 ISkill[] skillInterfaces = new ISkill[skills.Length]; 22 for (int i = 0; i < skills.Length; i++) 23 { 24 skillInterfaces[i] = (ISkill)skills[i]; 25 } 26 return skillInterfaces; 27 } 28 } 29}
だいたいこんな感じのScriptableObjectになります。
で、最後にこれらを使うCharacterクラスです。
C#
1public class Character : MonoBehaviour 2{ 3 // ステータス 4 [SerializeField] private int hp; 5 [SerializeField] private int sp; 6 7 // キャラ設定 8 [SerializeField] private Stats stats; 9 10 // ジョブ 11 [SerializeField] private Job job; 12 13 // 習得スキル 14 private ISkill[] skills; 15 16 // 初期化 17 public void Initialize() 18 { 19 skills = job.Skills; 20 // statsとjobをかけ合わせてステータス決定 21 hp = (int)(stats.BaseHP * job.HPCoefficient); 22 sp = (int)(stats.BaseSP * job.SPCoefficient); 23 } 24 25 // ジョブを変更した時 26 public void SetJob(Job newJob) 27 { 28 job = newJob; 29 Initialize(); 30 } 31 32 public void UseSkill(int skillIndex) 33 { 34 // SPが足りていたら 35 if (skills[skillIndex].IsUsable(hp)) 36 { 37 // スキルを使う 38 skills[skillIndex].UseSkill(); 39 // SPを減らす 40 sp-= skills[skillIndex].SPCost; 41 } 42 } 43}
あとはこいつを使うだけですね。
とりあえずStartで回してみます。
C#
1 private void Start() 2 { 3 Initialize(); 4 for (int i = 0; i < skills.Length; i++) 5 { 6 Debug.Log("◆スキル" + i); 7 UseSkill(i); 8 } 9 }
キャラクターに対して、スキル、キャラ設定、ジョブがそれぞれ別のScriptable Objectになっているので、既存コードに手を入れずに簡単に新要素の追加を行うことができます。
自分の場合はひとまずこんな感じです。
下記のような回答は推奨されていません。
- 質問の回答になっていない投稿
- スパムや攻撃的な表現を用いた投稿
このような回答には修正を依頼しましょう。
#5
総合スコア1
投稿2023/02/14 07:54
What about setting the skills of the characters first? You must make sure that all characters in the game can support each other. I you want to know more, you should refer some survival games in Drift Hunters. They will assist you in making a so-called RPG genre game.
下記のような回答は推奨されていません。
- 質問の回答になっていない投稿
- スパムや攻撃的な表現を用いた投稿
このような回答には修正を依頼しましょう。
#6
個別回答やベストアンサー的な機能がない?ようでしたので、回答経由にて失礼いたします。
総合スコア10905
投稿2023/02/10 03:59
編集2023/02/10 04:02
enum CLASS{ Brave, Mage, Monk, Warrior } class Character { CLASS m_Class; }
まず,コレだと 具体的に 何が/どのように 問題となるのか? というのを明確にされると良いのではないでしょうか.
(何が必要か? 何が適しているか? というような話は,実際のゲームのルールに大きく依存すると思うので.)
ご意見ありがとうございます。
抽象クラス以外の良いアプローチがあれば例を知りたかったので投稿させていただいた次第でした。
投稿内容が分かりづらいものとなっていたようで、申し訳ありません。
総合スコア110914
投稿2023/02/10 04:04
まずは全ジョブのアビリティを設定して、ジョブごとにどのアビリティをどのような成長速度でどのくらいの上限まで向上できるか設定すればよいのでは?
ご意見ありがとうございます。
魔法が5種類あるとしたらAさんはどのレベルでどれを覚えるor覚えないかを個別に設定するようなイメージでしょうか。たしかにそれであればコントロールが効く部分もありそうに思います。
総合スコア28309
投稿2023/02/10 04:35
職業やモンスターをそれぞれのクラスで実装するのは大抵の場合良くありません。
たとえば、戦士と魔術師の両方の能力を持つ上位職の魔法戦士というものを作りたくなったとしましょう。
これは戦士から派生するのが良いでしょうか?
それとも魔術師から派生するのが良いでしょうか?
正解は、戦士のスキルと魔術師のスキルをそれぞれクラスとして実装し、それを戦士、魔術師、魔法戦士のスキルとしてそれぞれのインスタンスに持たせることです。
クラスではなくプロパティにすることで、設定ファイルから読み込むことができるようになり、同じスキルを複数の職業に持たせることができるようになり、戦闘中にいちいち「もし魔術師もしくは魔法戦士もしくは賢者なら〜」と条件分岐しなくても良くなります。
総合スコア579
投稿2023/02/11 21:50
編集2023/02/11 22:14
(職業ごとにステータスに差異があり、使えるスキルや特性が異なるようなイメージ。例えばドラクエの勇者や魔法使い、戦士など・・・)
Unityのコンポーネントのように、「魔法のメラが使えるコンポーネント」みたいなものを
その人ができることとしてを1つずつ組み立てていく? など
ここらへんの仕様でZuishinさんの「スキルをクラスとして実装する」というアイデアを使いドラクエワードでUnityの機能使って考えてみましょうか。
スキルについて
まずスキル(特技・呪文)をコンポーネントっぽく…というかインスペクターにドラッグ&ドロップで抜き差しできるように作ります。
具体的にはScriptable Objectを使います。
スキルのinterface、ISkillを考えます。
戦闘などでスキル使うときはこのinterfaceを使う感じです。
とりあえずめっちゃ簡略化してこんな感じで。
C#
1public interface ISkill 2{ 3 // 使用に必要なコスト 4 int SPCost { get; } 5 // スキルを使用したときの実際の処理を書く。ダメージとか回復とか。 6 void UseSkill(); 7 // スキルを使用可能かどうか 8 bool IsUsable(int currentSP); 9}
そしてこのISkillを継承したスキルの基底クラス、SkillBaseをScriptableObjectで書きます。
C#
1public abstract class SkillBase : ScriptableObject, ISkill 2{ 3 [SerializeField] protected int spCost = 0; 4 public int SPCost => spCost; 5 6 public abstract void UseSkill(); 7 8 public virtual bool IsUsable(int currentSP) 9 { 10 return currentSP >= SPCost; 11 } 12}
で、このSkillBaseを使って、具体的なスキルをそれぞれ別クラスとして書きます。
C#
1// 通常攻撃 2[CreateAssetMenu(fileName = "NormalAttack")] 3public class NormalAttack : SkillBase 4{ 5 public override void UseSkill() 6 { 7 Debug.Log("通常攻撃!"); 8 Debug.Log("50ダメージ!"); 9 } 10}
C#
1// ばくれつけん 2[CreateAssetMenu(fileName = "BakuretsuKen")] 3public class BakuretsuKen : SkillBase 4{ 5 // 攻撃回数 6 [SerializeField] private int attackCount = 4; 7 public override void UseSkill() 8 { 9 Debug.Log("ばくれつけんをはなった!"); 10 for (int i = 0; i < attackCount; i++) 11 { 12 int damage = Random.Range(10, 30); 13 Debug.Log(damage + "ダメージ!"); 14 } 15 } 16}
C#
1// まじんぎり 2[CreateAssetMenu(fileName = "MajinGiri")] 3public class MajinGiri : SkillBase 4{ 5 // 会心の一撃が出る確率 6 [SerializeField] private float criticalRate = 37.5f; 7 8 public override void UseSkill() 9 { 10 Debug.Log("まじんのごとくきりかかった!"); 11 // 会心チェック 12 if (Random.Range(0, 100) < criticalRate) 13 { 14 Debug.Log("かいしんのいちげき!"); 15 Debug.Log("100ダメージ!"); 16 } 17 else 18 { 19 Debug.Log("ミス!"); 20 } 21 } 22}
そうするとこいつらはScriptbaleObjectなので、右クリックで作成することでAssetとして扱うことができます。
バランス調整がコードを変えずともエディターだけで可能です。
キャラクターについて
次にキャラクターを考えます。ステータスはHP、SPのみにします
キャラクターは3つのクラスに分けて考えます。
1. Statsクラス
キャラ名、基礎HP、基礎SPを持っている。キャラクターの基礎ステータス設定クラス。
2. Jobクラス
ジョブのHP係数、SP係数、スキル(ScriptableObject[])を持っている。ジョブ設定クラス。
3. Characterクラス
実際のキャラクターのクラス。ゲーム内で戦闘を行う。
HP、SP、スキル(ISkill[])、Statsクラス、Jobクラスを持っている。スキルを使って攻撃を行う。
Characterの中でStats * Job をかけ合わせて、キャラ性能を表現する感じですね。
まずStatsです。
Characterに設定できるように、ScriptableObjectで書きます。
C#
1[CreateAssetMenu(fileName = "Stats")] 2public class Stats : ScriptableObject 3{ 4 [SerializeField] private string characterName; 5 [SerializeField] private int baseHP; 6 [SerializeField] private int baseSP; 7 8 public string CharacterName => characterName; 9 public int BaseHP => baseHP; 10 public int BaseSP => baseSP; 11}
次はJobです。
Unityではinterfaceをインスペクターに登録することができません(シリアライズできないので)。
しかしinterfaceを継承させたオブジェクトをinterfaceにキャストして取り出すことで、実質的にインスペクターで扱うことが出来ます。
この方法を使って、Jobにスキルを持たせます。
C#
1[CreateAssetMenu(menuName = "Job")] 2public class Job : ScriptableObject 3{ 4 // ジョブ名 5 [SerializeField] private string jobName = "Job"; 6 // HP係数 7 [SerializeField] private float hpCoefficient = 1f; 8 // SP係数 9 [SerializeField] private float spCoefficient = 1f; 10 // スキル 11 [SerializeField] private ScriptableObject[] skills; 12 13 public float HPCoefficient => hpCoefficient; 14 public float SPCoefficient => spCoefficient; 15 16 // ScriptableObjectをISkillにキャストする。 17 public ISkill[] Skills 18 { 19 get 20 { 21 ISkill[] skillInterfaces = new ISkill[skills.Length]; 22 for (int i = 0; i < skills.Length; i++) 23 { 24 skillInterfaces[i] = (ISkill)skills[i]; 25 } 26 return skillInterfaces; 27 } 28 } 29}
だいたいこんな感じのScriptableObjectになります。
で、最後にこれらを使うCharacterクラスです。
C#
1public class Character : MonoBehaviour 2{ 3 // ステータス 4 [SerializeField] private int hp; 5 [SerializeField] private int sp; 6 7 // キャラ設定 8 [SerializeField] private Stats stats; 9 10 // ジョブ 11 [SerializeField] private Job job; 12 13 // 習得スキル 14 private ISkill[] skills; 15 16 // 初期化 17 public void Initialize() 18 { 19 skills = job.Skills; 20 // statsとjobをかけ合わせてステータス決定 21 hp = (int)(stats.BaseHP * job.HPCoefficient); 22 sp = (int)(stats.BaseSP * job.SPCoefficient); 23 } 24 25 // ジョブを変更した時 26 public void SetJob(Job newJob) 27 { 28 job = newJob; 29 Initialize(); 30 } 31 32 public void UseSkill(int skillIndex) 33 { 34 // SPが足りていたら 35 if (skills[skillIndex].IsUsable(hp)) 36 { 37 // スキルを使う 38 skills[skillIndex].UseSkill(); 39 // SPを減らす 40 sp-= skills[skillIndex].SPCost; 41 } 42 } 43}
あとはこいつを使うだけですね。
とりあえずStartで回してみます。
C#
1 private void Start() 2 { 3 Initialize(); 4 for (int i = 0; i < skills.Length; i++) 5 { 6 Debug.Log("◆スキル" + i); 7 UseSkill(i); 8 } 9 }
キャラクターに対して、スキル、キャラ設定、ジョブがそれぞれ別のScriptable Objectになっているので、既存コードに手を入れずに簡単に新要素の追加を行うことができます。
自分の場合はひとまずこんな感じです。
まとめてのご返信となり恐れ入ります。
非常にわかりやすい考え方を含め、回答をありがとうございます。
たしかに抽象クラスで作っていくと「似ているけどちょっと違う」みたいなものを作るときに頭をひねることになりそうですね・・・。
掲載いただいたサンプルも非常にわかりやすく、手元で写経のように組んでみたところなんとなくですが全体像が掴めたような気がします。これを拡張していく方向で考えを膨らませていきたいと思います。
総合スコア1
投稿2023/02/14 07:54
What about setting the skills of the characters first? You must make sure that all characters in the game can support each other. I you want to know more, you should refer some survival games in Drift Hunters. They will assist you in making a so-called RPG genre game.
Thanks for replay.
I wasn't sure about the reference examples, but I'll play it.
下記のような回答は推奨されていません。
- 質問の回答になっていない投稿
- スパムや攻撃的な表現を用いた投稿
このような回答には修正を依頼しましょう。
#7
総合スコア10905
投稿2023/02/18 02:15
投稿内容が分かりづらいものとなっていたようで、申し訳ありません。
別にそういうことを指摘しているみたいなことではないです.
…を明確にされると良いのではないでしょうか.
という言葉通りに受け取ってもらえれば.
要は,最も単純な方法で特に何も問題が無いならばそれでいいわけで,逆に言えば「どうすればいいのか?」と考えている際には,単純な方法だと やれない/面倒になる/etc な事柄が存在しているハズ.
そういった点がぼんやりしているのであれば,まずそれを明らかにしないと,「抽象クラス」が良い設計なのかどうかを判断することはできないでしょう.
例えば,他の方が「魔法戦士」という例の話をされていますが,もしもあなたが作る物には「戦士と魔法使いしか出てこない(魔法戦士は作らない)」のであれば,「この方法だと魔法戦士を作ろうとしたら苦労するよね」というその設計の欠点は,今回は度外視しても良いのかもしれないわけで.
下記のような回答は推奨されていません。
- 質問の回答になっていない投稿
- スパムや攻撃的な表現を用いた投稿
このような回答には修正を依頼しましょう。
関連した質問
意見交換
受付中
Javaの抽象クラスとインタフェースの使い分け
回答3
クリップ1
更新
2023/06/01
Q&A
解決済
NotionAPIでページコンテンツを取得したい
回答1
クリップ1
更新
2023/05/31
意見交換
受付中
Python経験2年からゲームプログラマーになるためには
回答18
クリップ2
更新
2023/05/31
意見交換
受付中
URLの末尾スラッシュの有無について
回答4
クリップ1
更新
2023/05/31
Q&A
解決済
ハンバーガーメニューの挙動について(transitionが、もとに戻る時には適応されない)
回答2
クリップ0
更新
2023/06/02
Q&A
解決済
Javaの外部ライブラリが使えない
回答1
クリップ0
更新
2023/05/30
Q&A
解決済
Atcoder ABC301 D Bitmaskが分かりません
回答1
クリップ0
更新
2023/06/01
下記のような回答は推奨されていません。
このような回答には修正を依頼しましょう。