質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

Q&A

解決済

5回答

9904閲覧

Switch文でメソッドが長くなりすぎる

kyoro_san

総合スコア11

C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

0グッド

1クリップ

投稿2017/01/25 10:52

###疑問に思うこと
エクセルでモンスターの行動パターンを作ろうと思っています。
行動条件のIdをエクセル内で決めて、プログラム内でそのIdで行動するのかしないのかを判定するメソッドを書いているのですが、Idが多いためSwitch文が長くなってしまいます。

メソッドの行数は長くても30行程度が好ましいと聞いたことがあるのですが、やはり何か工夫をして短くするべきなのでしょうか?
ご指摘お願いします。

public class EN_AI { public string Id { get; private set; } //AIの名前. public List<EN_AI_Method> AI_Method { get; private set; } //AIの条件パターンのList. public EN_AI(string _id, List<EN_AI_Method> _ai_Method) { Id = _id; AI_Method = _ai_Method; } private bool isUsedOnlyOnce = false; private int conditionsValue = 0; public ICharaStatus_FVRPG MyStatus { private get; set; } public List<ICharaStatus_FVRPG> PlStatusList { private get; set; } public List<ICharaStatus_FVRPG> EnStatusList { private get; set; } private bool Check_Conditions(string conditionsId) { bool result = false; switch (conditionsId) { case "": result = true; break; //指定なしならtrue. case "UNC": result = true; break; //UNC==無条件でtrue. case "OnlyOnce": if (isUsedOnlyOnce == false) { isUsedOnlyOnce = true; result = true; } break; //一度きりの行動. case "Val_0": if (conditionsValue == 0) { result = true; } break; case "Val_1": if (conditionsValue == 1) { result = true; } break; case "Val_2": if (conditionsValue == 2) { result = true; } break; case "Val_3": if (conditionsValue == 3) { result = true; } break; case "Val_4": if (conditionsValue == 4) { result = true; } break; case "Val_5": if (conditionsValue == 5) { result = true; } break; case "Val_6": if (conditionsValue == 6) { result = true; } break; case "Val_7": if (conditionsValue == 7) { result = true; } break; case "Val_8": if (conditionsValue == 8) { result = true; } break; case "Rnd_1": if (MyRandom.Judgment_SettingParcent(1.0f)) { result = true; } break; //1%の確立でtrue. case "Rnd_3": if (MyRandom.Judgment_SettingParcent(3.0f)) { result = true; } break; case "Rnd_5": if (MyRandom.Judgment_SettingParcent(5.0f)) { result = true; } break; case "Rnd_10": if (MyRandom.Judgment_SettingParcent(10.0f)) { result = true; } break; case "Rnd_20": if (MyRandom.Judgment_SettingParcent(20.0f)) { result = true; } break; case "Rnd_30": if (MyRandom.Judgment_SettingParcent(30.0f)) { result = true; } break; case "Rnd_50": if (MyRandom.Judgment_SettingParcent(50.0f)) { result = true; } break; case "Rnd_80": if (MyRandom.Judgment_SettingParcent(80.0f)) { result = true; } break; case "Rnd_99": if (MyRandom.Judgment_SettingParcent(99.0f)) { result = true; } break; case "HP_Max": if (MyStatus.Hp_Now == MyStatus.Hp) { result = true; } break; case "HP99_&L": if(Calc_HpParcent(MyStatus) <= 0.99f) { result = true; } break; //HP99%以下ならtrue. case "HP95_O": if (Calc_HpParcent(MyStatus) > 0.95f) { result = true; } break; //HP95%より上ならtrue. case "HP95_&L": if (Calc_HpParcent(MyStatus) <= 0.95f) { result = true; } break; case "HP90_O": if (Calc_HpParcent(MyStatus) > 0.9f) { result = true; } break; case "HP90_&L": if (Calc_HpParcent(MyStatus) <= 0.90f) { result = true; } break; case "HP80_O": if (Calc_HpParcent(MyStatus) > 0.8f) { result = true; } break; case "HP80_&L": if (Calc_HpParcent(MyStatus) <= 0.8f) { result = true; } break; case "HP70_O": if (Calc_HpParcent(MyStatus) > 0.7f) { result = true; } break; case "HP70_&L": if (Calc_HpParcent(MyStatus) <= 0.7f) { result = true; } break; case "HP60_O": if (Calc_HpParcent(MyStatus) > 0.6f) { result = true; } break; case "HP60_&L": if (Calc_HpParcent(MyStatus) <= 0.6f) { result = true; } break; case "HP50_O": if (Calc_HpParcent(MyStatus) > 0.5f) { result = true; } break; case "HP50_&L": if (Calc_HpParcent(MyStatus) <= 0.5f) { result = true; } break; case "HP40_O": if (Calc_HpParcent(MyStatus) > 0.4f) { result = true; } break; case "HP40_&L": if (Calc_HpParcent(MyStatus) <= 0.4f) { result = true; } break; case "HP30_O": if (Calc_HpParcent(MyStatus) > 0.3f) { result = true; } break; case "HP30_&L": if (Calc_HpParcent(MyStatus) <= 0.3f) { result = true; } break; case "HP20_O": if(Calc_HpParcent(MyStatus) > 0.2f) { result = true; } break; //以下略。味方の属性が○○か、敵の属性が○○か、○○という名のキャラはいるのかなど、まだまた書かないといけない. } return result; } private float Calc_HpParcent(ICharaStatus_FVRPG checkStatus) { return checkStatus.Hp_Now / (float)checkStatus.Hp; } }

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答5

0

いいハマり方をしていますね。

「デザインパターン」で検索してみてください。
デザインパターンとは、過去の偉人たちが編み出した、「こういうときはこうするといいよ」というバターン集です。
様々なパターンがあり、そのどれもが「なるほど、よく考えたな」と思えるものばかりです。
オブジェクト指向以上に「そんなやり方があったのか!」と思わせてくれると思います。

そして今回の例では、「Stateパターン」などが比較的親和性が高いと思います。

簡単にどんなパターンか説明すると。
それぞれif文で分岐している部分を「状態(State)」と呼びます。
そしてこの「状態」を簡単に、かつメンテナンス性よく解決してくれるのが「Stateパターン」です。

ここからは個人的なイメージになりますが、各Stateは、例えばNintendo 3DSのカートリッジに近いイメージです。
というとなんのこっちゃかもしれませんが、つまり。

Aという状態にいるときはAというカートリッジを挿す。
すると、その状態ではAに定義された処理しかされなくなる、という具合です。
もしBという状態に変化しそうになったらBというカートリッジに差し替える、という具合に状態を変更していきます。
これの何がいいかというと、質問で書かれている状態処理を以下のようにとても簡潔に書くことができるようになります。

anyState.doAnything();

です。
そしてもし、状態が変わる場合は

anyState = new OtherState();

というように、anyState自体を「差し替えて」しまうのです。(これがカートリッジ、と例えた所以です)

こうした状態に応じた処理というのは概ね、「その状態の中ならこうする」と状態ごとに処理をまとめられることがほとんどです。
つまり、上記の例で言えばAnyStateクラスやOtherStateクラスのdoAnythingメソッドに、状態ごとの処理を書いておけば、あとはこの状態を差し替えるだけで完了します。
もし状態を増やしたくなったら、その分だけ新しい状態クラスを追加して差し替えるだけでよくなります。

個人的に、デザインパターンの中で一番好きなパターンがこのStateパターンです。
ぜひ色々「デザインパターン」で調べてみてください。

投稿2017/01/25 12:14

edo_m18

総合スコア2283

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

ozwk

2017/01/25 12:30

横からなんですが、excelに定義してあるということなので、 stateを生成するときに工夫しないと結局switch文が長くなりそうなんですが。
edo_m18

2017/01/25 12:34

そうですね。ステートの生成はまた別のロジックが必要だと思います。
ozwk

2017/01/25 12:46

その別のロジックってどうすればいいですかね?
edo_m18

2017/01/25 13:00

うーん、やりたいことによるので、質問の内容からだとこれ、というのはむずかしいですね。
guest

0

こういうの作ってみてはどうかな?

C#

1public class EN_AI 2{ 3 private static IDictionary<string, Func<EN_AI, bool>> predicates; 4 public static Register(string name, Func<EN_AI, bool> predicate) 5 { 6 predicates[name] = predicate; 7 } 8 9 private bool Check_Conditions(string conditionsId) 10 { 11 Func<EN_AI, bool> predicate; 12 if (!predicates.TryGetValue(conditionId, out predicate)) 13 { 14 throw new Exception("判定条件が登録されていません。"); 15 } 16 17 return predicate(this); 18 } 19}

これは貰ったサンプルを基にしてるけど、応用で分割したりグルーピングしたりファイルから読み込めるようにしたり…って具合です。ロジック自体はシンプルになると思うけどどうでしょうか。

こんな感じで他から追加できるので、Registerの方法を工夫するようにします。

C#

1foreach (var i in Enumerable.Range(0, 8)) 2{ 3 int num = i; 4 EN_AI.Register($"Val_{num}", ai => ai.ConditionsValue == num); 5}

これで条件文と判定の切り分けができます。

投稿2017/01/25 11:39

編集2017/01/26 01:16
haru666

総合スコア1591

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

0

ベストアンサー

デザインパターン云々…でいうと…
自分に思いつくのは今回の要件ではStrategyパターンぐらいなので、応用例を書いておきます。

※私はゲームプログラミングしたことがないので命名はかなり適当です。参考にしないでね

C#

1 // 条件を判定する要素の規定インターフェース 2 public interface IConditionPredicate 3 { 4 string ConditionId { get; } 5 6 // paramの型をどうするかは一考の余地がありますが 7 bool Check(EN_AI enemy, string[] parameters); 8 } 9 10 // 判定条件一つ一つをクラス化して… 11 public class ValueConditionPredicate : IConditionPredicate 12 { 13 public string ConditionId { get; } = "Val"; 14 15 public bool Check(EN_AI enemy, string[] parameters) 16 { 17 int value; 18 if (parameters.Length != 1 || !int.TryParse(parameters[0], out value)) return false; 19 return enemy.ConditionValue == value; 20 } 21 } 22 23 public class RandomConditionPredicate : IConditionPredicate 24 { 25 private readonly Random randomGenerator = new Random(Guid.NewGuid().GetHashCode()); 26 27 public string ConditionId { get; } = "Rnd"; 28 29 public bool Check(EN_AI enemy, string[] parameters) 30 { 31 int value; 32 if (parameters.Length != 1 || !int.TryParse(parameters[0], out value)) return false; 33 return randomGenerator.Next() % 100 < value; 34 } 35 } 36 37 public class HpConditionPredicate : IConditionPredicate 38 { 39 public string ConditionId { get; } = "HP"; 40 41 public bool Check(EN_AI enemy, string[] parameters) 42 { 43 if (parameters.Length != 2) return false; 44 45 string function = parameters[0]; 46 int value; 47 if (!int.TryParse(parameters[1], out value)) return false; 48 49 int percent = enemy.Hp.Percent; 50 switch (function) 51 { 52 case "G": 53 return value >= percent; 54 case "L": 55 return value <= percent; 56 case "E": 57 return value == percent; 58 } 59 return false; 60 } 61 } 62 63 // それを元に判定するクラスを作ります。 64 public class ConditionChecker 65 { 66 private IDictionary<string, KeyValuePair<string, string[]>> Replacements; 67 private IDictionary<string, IConditionPredicate> predicates; 68 69 // 条件を追加 70 public void AddConditionPredicate(IConditionPredicate predicate) 71 { 72 predicates.Add(predicate.ConditionId, predicate); 73 } 74 75 // とあるIDを別のIDに変換するためのマッピングを登録 76 public void AddReplacement(string name, string conditionId, string[] parameters) 77 { 78 Replacements.Add(name, new KeyValuePair<string, string[]>(conditionId, parameters)); 79 } 80 81 // マッピングされた置換を適用する関数 82 private void Adjust(ref string name, ref string[] parameters) 83 { 84 KeyValuePair<string, string[]> replace; 85 if (!Replacements.TryGetValue(name, out replace)) return; 86 87 name = replace.Key; 88 parameters = replace.Value; 89 } 90 91 // 初期化サンプル 92 // こういうのをある程度自動化する方法もあります。 93 // 例:IConditionPredicateを実装したクラスをリフレクションでインスタンス化します 94 public void InitializingSample() 95 { 96 AddConditionPredicate(new ValueConditionPredicate()); 97 AddConditionPredicate(new RandomConditionPredicate()); 98 AddConditionPredicate(new HpConditionPredicate()); 99 100 // ConditionIDがHP_MaxだったらHP100%と一致、という風な書き換え条件を登録 101 AddReplacement("HP_Max", "HP", new string[] { "E", "100" }); 102 } 103 104 // 本題の判定関数 105 public bool Check(EN_AI enemy, string conditionId, string[] parameters) 106 { 107 Adjust(ref conditionId, ref parameters); 108 109 IConditionPredicate predicate; 110 if (!predicates.TryGetValue(conditionId, out predicate)) 111 { 112 throw new Exception(); 113 } 114 115 return predicate.Check(enemy, parameters); 116 } 117 }

こんな感じでちょっとずつばらしていくと、判定毎に複雑なロジックを実装できます。

上記サンプルはExcelに複数行で条件を登録することを想定しています。
例えばHP80%以上の場合、HP | G | 80 とかそんな風に登録します。
複雑な条件を別の書き方ができるように、HP_Max ってIDをHP | E | 100 に変換したりもしています。

初期化部分を工夫していけば、今後の条件追加やロジック追加が簡単になります。

また、IConditionPredicateそのものを拡張してEN_AIっていうモンスター単体のオブジェクトから、BattleContextのような戦闘全体を把握できるステートも渡せるように改良すれば、例えば「3ターン目に実行する」とか、「味方が死んでいたら」とかそんな条件文も作ることができるようになります。

何にせよ、完璧なカスタマイズ性よりは、まず動くものを作るのが一番大事ですから、こういうのに挑戦するのはまだ先で良いと思います。

※オマケの部分

C#

1 public interface ICharaStatus_FVRPG 2 { 3 int MaxHp { get; set; } 4 int Hp { get; set; } 5 } 6 7 public class EN_AI 8 { 9 public int ConditionValue { get; set; } 10 public ICharaStatus_FVRPG MyStatus { private get; set; } 11 public HpAccessor Hp { get { return new HpAccessor(MyStatus); } } 12 } 13 14 public struct HpAccessor 15 { 16 private readonly ICharaStatus_FVRPG status; 17 public HpAccessor(ICharaStatus_FVRPG status) 18 { 19 this.status = status; 20 } 21 22 public int Percent 23 { 24 get 25 { 26 return Calc_HpPercent(status); 27 } 28 } 29 30 private static int Calc_HpPercent(ICharaStatus_FVRPG status) 31 { 32 return status.Hp * 100 / status.MaxHp; 33 } 34 }

投稿2017/01/26 03:01

編集2017/01/26 04:03
haru666

総合スコア1591

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

kyoro_san

2017/01/26 09:41

丁寧なサンプルコードありがとうございます。 ただ、今の自分のレベルでは難しいので、最後におっしゃっているようにとりあえずちゃんと動くようになってから、他の回答者さんが勧めてくれているデザインパターンや正規表現、上記のサンプルコードを調べながら自分のコードの改善点を探していこうと思います。 ありがとうございました。
guest

0

はいはい、こんばんは、なんとなく回答してみます。
最適なのはやっぱりStateパターンを使うのが一番かなとは思いますが、問いにあります
「Switch文でメソッドが長くなりすぎる」の回答案を書かせていただきます。
正直switchは使いたくない!と言うのが正直なところなのでif分に出してみました。

それと、処理をコンディションごとにメソッドに吐きだしています。

c#

1 private bool Check_Conditions(string conditionsId) { 2 bool result = false; 3 if (conditionsId == "") result = true; 4 if (conditionsId == "UNC") result = true; 5 if (conditionsId == "OnlyOnce") if (isUsedOnlyOnce == false) { isUsedOnlyOnce = true; result = true; }; 6 7 if(checkVal(conditionsId,conditionsValue))result = true; 8 if(checkRnd(conditionsId))result = true; 9 10 if(checkHP(conditionsId))result = true; 11 switch (conditionsId) { 12 13 case "HP_Max": if (MyStatus.Hp_Now == MyStatus.Hp) { result = true; } break; 14 case "HP99_&L": if(Calc_HpParcent(MyStatus) <= 0.99f) { result = true; } break; //HP99%以下ならtrue. 15 case "HP95_O": if (Calc_HpParcent(MyStatus) > 0.95f) { result = true; } break; //HP95%より上ならtrue. 16 case "HP95_&L": if (Calc_HpParcent(MyStatus) <= 0.95f) { result = true; } break; 17 case "HP90_O": if (Calc_HpParcent(MyStatus) > 0.9f) { result = true; } break; 18 case "HP90_&L": if (Calc_HpParcent(MyStatus) <= 0.90f) { result = true; } break; 19 case "HP80_O": if (Calc_HpParcent(MyStatus) > 0.8f) { result = true; } break; 20 case "HP80_&L": if (Calc_HpParcent(MyStatus) <= 0.8f) { result = true; } break; 21 case "HP70_O": if (Calc_HpParcent(MyStatus) > 0.7f) { result = true; } break; 22 case "HP70_&L": if (Calc_HpParcent(MyStatus) <= 0.7f) { result = true; } break; 23 case "HP60_O": if (Calc_HpParcent(MyStatus) > 0.6f) { result = true; } break; 24 case "HP60_&L": if (Calc_HpParcent(MyStatus) <= 0.6f) { result = true; } break; 25 case "HP50_O": if (Calc_HpParcent(MyStatus) > 0.5f) { result = true; } break; 26 case "HP50_&L": if (Calc_HpParcent(MyStatus) <= 0.5f) { result = true; } break; 27 case "HP40_O": if (Calc_HpParcent(MyStatus) > 0.4f) { result = true; } break; 28 case "HP40_&L": if (Calc_HpParcent(MyStatus) <= 0.4f) { result = true; } break; 29 case "HP30_O": if (Calc_HpParcent(MyStatus) > 0.3f) { result = true; } break; 30 case "HP30_&L": if (Calc_HpParcent(MyStatus) <= 0.3f) { result = true; } break; 31 case "HP20_O": if(Calc_HpParcent(MyStatus) > 0.2f) { result = true; } break; 32 //以下略。味方の属性が○○か、敵の属性が○○か、○○という名のキャラはいるのかなど、まだまた書かないといけない. 33 } 34 return result; 35 } 36 37 private boolen checkVal(string id, int value){ 38 if (!id.startsWith("Val_")) return false; 39 int cValue = id.Substring(4); 40 return (cValue == value); 41 } 42 private boolen checkRnd(string id){ 43 if (!id.startsWith("Rnd_")) return false; 44 float fValue = float.Parse(id.Substring(4)); 45 return MyRandom.Judgment_SettingParcent(fValue); 46 }

例のソースに
result = trueとありましたので、毎度 resultにフラグを立てていますが、もし下で返すだけを考えていらっしゃる場合でしたら

if (conditionsId == "") return true;

と書いた方がスマートですよ。

HP部分は値の抽出など正規表現を使うと同じように出来ますが、無理せずswitchか、swichを書いたメソッドにはき出すのが一番だと思いますよ。
正規表現 : Regex.Replace メソッド (System.Text.RegularExpressions)

case "HP80_&L": if (Calc_HpParcent(MyStatus) <= 0.8f) { result = true; } break;

今後を考えると、Stateパターンなど組み込むことなど考えるのも一興かもしれませんね。

19.State パターン | TECHSCORE(テックスコア)

投稿2017/01/25 15:57

退会済みユーザー

退会済みユーザー

総合スコア0

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

0

こういうときこそ、オブジェクト指向のポルモアフィズムを使います。
利用言語が書いてないので雰囲気だけですが、

public class Val extends EN_AI { int myCondition; Val(int c) { myCondition = c; } public bool Check_Conditions() { return conditionsValue == myCondition; } } public class Rnd extends EN_AI { ... } public class HP extends EN_AI { ... }

みたいにサブクラス毎に振る舞いを分けて、初期化時にサブクラスを指定して new し、数値パラメータもコンストラクタに与えてはどうでしょうか? あ、 Check_Condition は抽象メソッドにする必要があります。

投稿2017/01/25 11:08

mit0223

総合スコア3401

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問