メニュー

Unityでノベルゲームを作ろう!(第6回:四則演算(構文解析)・条件分岐編)

  • URLをコピーしました!

お疲れ様です、poppyです。本日もこちらのブログを見に来てくださりありがとうございます。
今回もUnityでアセットを使わずにノベルゲームを作るところをお見せいたします。
第6回は「四則演算・条件分岐編」ということで、四則演算(+剰余)による数値計算と前回の論理式を活かして条件分岐を行います。今後やる予定の選択肢実装などに役立つと考えています。

制作環境

制作環境は第1~5回と同様です。また、変数についても第1~5回のものを使い回すのでご注意ください。

  • バージョン:2022.3.24f1 (日本語化パッチ適用済み)
  • テンプレート:Universal 2D
  • スクリプト:Visual Studio 2022 (Windows)

今回は「宴」などのアセットは用いませんのでご了承ください。(多分使ったほうが早くできるとは思うけど)

免責事項

当ブログの記載内容や情報の信頼性については可能な限り十分注意をしておりますが、その完全性、正確性、妥当性及び公正性について保証するものではありません。
情報の誤りや不適切な表現があった場合には予告なしに記事の編集・削除を行うこともございます。あくまでもご自身の判断にてご覧頂くようにお願い致します。
当ブログの記載内容によって被った損害・損失については一切の責任を負いかねます。ご了承ください。

お詫び

あくまでも実装当時のコードを掲載しております。現在はバグなどを潰すために修正を行っているため本記事とは異なるコードになっておりますが、
こちらに反映する予定はございません。大変申し訳ありません。あくまでも参考程度に見ていただければ幸いです。

今回の目標

第6回では以下のことができるようにします。
・数値計算(四則演算+剰余)を行い、その結果を変数にセットする
・条件分岐

数値計算を構文解析で実現

第5回の論理式を構文解析したのと同様に、四則演算についても同じ構文解析で実現します。参考にしたものがそもそも四則演算なので論理演算にアレンジする必要のあった前回と違って今回のほうが楽です。一応参考資料を乗せておきます。

あくまでもゲーム制作のための手段としての構文解析なので、詳しい内容については省略しますが、大体以下のような形で構文解析をしていると理解しています。
・式(Expression) → 項(Term) [ {+|-} 項(Term)]*
※[ ]*は中のものが0回以上繰り返されていることを表す。つまり式は項(Term)単独or複数の項同士を「+または-({+|-}」で結合したものということです。
・項(Term) → 因子(Factor) [ {*|/|% 因子(Factor) ]*
・因子(Factor) → -(因子(Factor)) または 数値(Number) または ‘(‘ 式(Expression) ‘)’
・数値(Number) → 0~9または小数点(.)

これらを再帰的に繰り返すことで計算結果が得られるようにできています。ただし整数値(int)の計算をしたいのか浮動小数点(float)の計算をしたいのかがあるので、それはintFlagで管理しておきます。実装したのが以下になります。

 /* 計算系 */
 // 算術式を評価するメソッド
 public object EvaluateArithmeticExpression(string s)
 {
   int indexOfExpression = 0;
   s = ReplaceVariablesInExpression(s).Replace(" ", "");
   return ParseExpression(s, ref indexOfExpression);
 }

 // 算術式を解析して値を評価するメソッド
 private object ParseExpression(string s, ref int indexOfExpression)
 {
   // 式を評価するために、加算と減算を解析
   object val = ParseAdditionSubtraction(s, ref indexOfExpression);
   // 評価した値を返す
   return val;
 }

 // 加算と減算を解析するメソッド
 private object ParseAdditionSubtraction(string s, ref int indexOfExpression)
 {
   // 乗算と除算を解析して値を評価
   object val = ParseMultiplicationDivisionModulo(s, ref indexOfExpression);
   // 残りの式に対して、加算と減算を適用
   while (indexOfExpression < s.Length && (s[indexOfExpression] == '+' || s[indexOfExpression] == '-'))
   {
     char op = s[indexOfExpression];
     indexOfExpression++;
     object val2 = ParseMultiplicationDivisionModulo(s, ref indexOfExpression);
     // 加算または減算を実行して、値を更新
     if (op == '+')
     {
       val = intFlag ? Convert.ToInt32(val) + Convert.ToInt32(val2) : Convert.ToDouble(val) + Convert.ToDouble(val2);
       
     }
     else
     {
       val = intFlag ? Convert.ToInt32(val) - Convert.ToInt32(val2) : Convert.ToDouble(val) - Convert.ToDouble(val2);
     }   
   }
   // 評価した値を返す
   return val;
 }

 // 乗算と除算を解析するメソッド
 private object ParseMultiplicationDivisionModulo(string s, ref int indexOfExpression)
 {
   // 次の項を解析して値を評価
   object val = ParseFactor(s, ref indexOfExpression);
   // 残りの式に対して、乗算、除算、および剰余を適用
   while (indexOfExpression < s.Length && (s[indexOfExpression] == '*' || s[indexOfExpression] == '/' || s[indexOfExpression] == '%'))
   {
     char op = s[indexOfExpression];
     indexOfExpression++;
     object val2 = ParseFactor(s, ref indexOfExpression);
     // 乗算、除算、および剰余を実行して、値を更新
     if (op == '*')
     {
       val = intFlag ? Convert.ToInt32(val) * Convert.ToInt32(val2) : Convert.ToDouble(val) * Convert.ToDouble(val2);
     }else if (op == '/')
     {
       val = intFlag ? val = Convert.ToInt32(val) / Convert.ToInt32(val2) : Convert.ToDouble(val) / Convert.ToDouble(val2);

     }else if (op == '%')
     {
       val = intFlag ? val = Convert.ToInt32(val) % Convert.ToInt32(val2) : val = Convert.ToDouble(val) % Convert.ToDouble(val2);

     } 
   }
   // 評価した値を返す
   return val;
 }

 private object ParseFactor(string s, ref int indexOfExpression)
 {
   // 数値を解析して評価
   if (Char.IsDigit(s[indexOfExpression]) || (s[indexOfExpression] == '-' && indexOfExpression + 1 < s.Length ) || (s[indexOfExpression] == '.'))
   {
     if (s[indexOfExpression] == '-')
     {
       indexOfExpression++; // '-' をスキップ
       return intFlag ? -Convert.ToInt32(ParseFactor(s, ref indexOfExpression)) : Convert.ToDouble(ParseFactor(s, ref indexOfExpression)); // 負の数値として解析
     }
     else
     {
       return ParseNumber(s, ref indexOfExpression);
     }
   }
   // 括弧内の式を解析して評価
   else if (s[indexOfExpression] == '(')
   {
     indexOfExpression++;
     object val = ParseExpression(s, ref indexOfExpression);
     indexOfExpression++; // 括弧をスキップ
     return val;
   }
   else
   {
     throw new ArgumentException("Unexpected character: " + s[indexOfExpression]);
   }
 }

 // 数値を解析して評価するメソッド
 private object ParseNumber(string s, ref int indexOfExpression)
 {

   string n = "";
   // 数値の文字列を取得
   while (indexOfExpression < s.Length && (Char.IsDigit(s[indexOfExpression]) || (s[indexOfExpression] == '.') ))
   {
     
     n += s[indexOfExpression];
     indexOfExpression++;
   }
   // 数値を評価して返す
   return intFlag ? Convert.ToInt32(n) : Convert.ToDouble(n);  
 }

SetVariableの更新

これで数値計算もできるようになったので、数値型はこれを使うようにSetVariableを修正します。ついでに複合代入演算子(+=, -=,*=,/=)も実装しておきます。

/*変数設定系 */
public void SetVariable(string varName, string operand, string expression, string typeName)
{
  if (typeName == "bool" || typeName == "boolean")
  {
    variables[varName] = EvaluateExpression(expression);

  }
  else if (typeName == "int" || typeName == "float" || typeName == "number")
  {
    if (typeName == "int")
    {
      intFlag = true;
      int culcuratedValue = Convert.ToInt32(EvaluateArithmeticExpression(expression));
      if (operand == "+=")
      {
        variables[varName] = Convert.ToInt32(GetVariable(varName)) + culcuratedValue;
      }
      else if (operand == "-=")
      {
        variables[varName] = Convert.ToInt32(GetVariable(varName)) - culcuratedValue;
      }
      else if (operand == "*=")
      {
        variables[varName] = Convert.ToInt32(GetVariable(varName)) * culcuratedValue;
      }
      else if (operand == "/=")
      {
        variables[varName] = Convert.ToInt32(GetVariable(varName)) / culcuratedValue;
      }
      else
      {
        variables[varName] = culcuratedValue;
      }

    }
    else
    {
      intFlag = false;
      float culcuratedValue = (float)Convert.ToDouble(EvaluateArithmeticExpression(expression));
      if (operand == "+=")
      {
        variables[varName] = (float)Convert.ToDouble(GetVariable(varName)) + culcuratedValue;
      }
      else if (operand == "-=")
      {
        variables[varName] = (float)Convert.ToDouble(GetVariable(varName)) - culcuratedValue;
      }
      else if (operand == "*=")
      {
        variables[varName] = (float)Convert.ToDouble(GetVariable(varName)) * culcuratedValue;
      }
      else if (operand == "/=")
      {
        float currentValue = (float)Convert.ToDouble(GetVariable(varName));
        variables[varName] = currentValue / culcuratedValue;
      }
      else
      {
        variables[varName] = culcuratedValue;
      }
    }
  }
  else
  {
    variables[varName] = expression;
  }

}

さて、実験してみましょう。

こうすると最初のシナリオ番号では1、更新後では6が表示されるはずです。

両方ともうまく出力できているようです。floatもやっておきましょう。それぞれ7.77と11.655が出ればよいです。

この通り望み通りの数値が得られています。

条件分岐

続いて条件分岐を行います。以下の画像のように条件分岐することを考えます。

ここで活躍するのはB列においておいたインデント番号であり、「ifの中のif」のようないわゆる「入れ子」を表現するためにインデント番号が入れ子の深さを定義するようにします。
上記の画像を見れば分かる通り、必要な命令は「if」「elseif」「else」「ifend」の4つであり、「if」と「elseif」は次の列に条件式を書くようにすれば良さそうです。
当然「if」の条件式が真の場合は「elseif」「else」のステートメントはスキップして同じインデントのifendまで進む必要があります(elseifの場合も同様でさらに下のelseifやelseはスキップ)。
それを踏まえて書いていきましょう。

ExecuteStatement(UserScriptManager.cs)

// ステートメントを実行するメソッド
public void ExecuteStatement(string sentence)
{
  string[] words = sentence.Split(','); // 文章を単語に分割する
  float _fadetime = 1f;

  // 単語によって処理を分岐する
  switch (words[2].ToLower())
  {
   /* 他の処理は省略、 */
   /* ここから追加 */
    case "if":
      indentNum++;
      if (GameManager.Instance.variablesManager.EvaluateExpression(words[3]))
      {
        conditionMet[indentNum - 1] = true;
        GoToTheNextLine();
      }
      else
      {
        SkipToNextConditionalBlock();
      }
      break;
    case "elseif":
      if (conditionMet[indentNum - 1] || !(GameManager.Instance.variablesManager.EvaluateExpression(words[3])))
      {
        SkipToNextConditionalBlock();
      }
      else
      {
        conditionMet[indentNum - 1] = true;
        GoToTheNextLine();
      }
      break;
    case "else":
      if (conditionMet[indentNum - 1])
      {
        SkipToNextConditionalBlock();
      }
      else
      {
        conditionMet[indentNum - 1] = true;
        GoToTheNextLine();
      }
      break;
    case "ifend":
      conditionMet[indentNum - 1] = false;
      indentNum--;
      GoToTheNextLine();
      break;
    /* ここまで */
    default:
      break;
  }
}

ここで使用したのがすでに条件を満たした式が存在するかを調べる変数conditionMetと次の条件ブロックまでスキップするメソッドSkipToNextConditionalBlock()です。以下で定義します。

ConditionMetの定義

// テキストファイルから文章を読み込み、ステートメントの実行を管理するクラス
public class UserScriptManager : MonoBehaviour
{
  [SerializeField] TextAsset _textFile; // テキストファイルを格納するための変数
 
  private int indentNum;
  private bool[] conditionMet;
  List<string> _sentences = new List<string>(); // 読み込んだ文章を格納するリスト

  // スクリプトの初期化時に実行されるメソッド
  void Awake()
  {

    indentNum = 0;
    int conditionMetLength = 10;
    //条件分岐をとりあえず10個定義(falseで初期化)
    conditionMet = new bool[conditionMetLength];
    for (int i = 0; i < conditionMetLength; i++)
    {
      conditionMet[i] = false;
    }
  }
    /* 以下略 */

SkipToNextConditionalBlock()

private void SkipToNextConditionalBlock()
{
  //GoToTheNextLine();
  int statementIndentNum = 0;
  string statementWord = "";
  string sentence = "";
  while (true)
  {
    GameManager.Instance.lineNumber++;
    sentence = GetCurrentSentence();
    string[] words = sentence.Split(','); // 文章を単語に分割する
    statementIndentNum = ConvertToInt(words[1]); //現在のインデント番号を取得
    statementWord = words[2].ToLower(); //現在の命令を取得
    //ループから抜けるのはインデント番号が同じかつifend または 条件未達かつインデント番号同じかつelseifまたはelseのとき
    if ((statementIndentNum == indentNum && statementWord == "ifend") || (!conditionMet[indentNum - 1] && statementIndentNum == indentNum && (statementWord == "elseif" || statementWord == "else")))
    {
      ExecuteStatement(sentence);
      break;
    }
  }
}

これでやってみましょう。まずは以下の画像。これだとif→ifの中のif→ifの中のifend→ifendという形で通るはずです。

この4枚が出力されたので目論見通りいっているようです。続いて以下の画像でも試してみましょう。

ちゃんとifの中のelseifを通っていることがわかります。最後にこの画像でも確かめましょう。

ちゃんとこの場合はelseまでスキップされていることがわかります。以上で条件分岐を終わりにします。

終わりに

ということで今回はノベルゲーム開発の第6回ということで四則演算と条件分岐がメインとなりました。次回は多分選択肢などを実装しようと考えています。
それではまた!

シェアよろしくお願いします!
  • URLをコピーしました!