Unityで電卓作成し、他の方法と比較してみた

勉強のためにWindows Form Applicationで,以下のように電卓を作成したとき。

![calculatorWFA.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3676736/81695d32-ff5b-f83e-0cf1-f81a81b61917.jpeg)

このとき、その手順がそのままUnityに使えるのではないか？と思いたちUnityでも電卓を作成してみた。
　確かに、UnityもWindows Form Applicationも、画面のButtonやLabel(TextField)を部品のように作成し、さらにC#でそのButtonをクリックしたときの挙動をひもづける（計算などして、Labelに表示する）という点で似通っていた。
　ただ、いずれの二つも、以前Python(モジュールがtkinker、PysimpleGUI, pygameのいずれも)やJavascriptでつくったとき 
 https://qiita.com/tkoji3744/items/08b71054dfba4f93f2c4
にくらべて、スクリプトの行数がとても長かった。これは、C#には、pythonやJavaScriptで使った関数eval()が用意されていないということが原因と思われた。
　すなわちPython の eval() 関数を使用すると、C# のように switch 文を使わずに 直接数式を評価できる ため、コードの簡潔さに大きく影響する。
 
```py
 eval() の動作
print(eval("5 + 3"))   # 8
print(eval("10 * 2"))  # 20
print(eval("50 / 5"))  # 10.0
```

このように、eval() を使うと 文字列の計算式をそのまま評価して、結果を取得できる ため、C# のように switch 文や if を使わずに済む。

C#（Windows Forms）の計算処理
C# では eval() に相当する組み込み関数がなく、数式の処理を switch 文や if を使って実装する必要がある。
```C#
switch (operation)
{
    case "+":
        result = firstNumber + secondNumber;
        break;
    case "-":
        result = firstNumber - secondNumber;
        break;
    case "×":
        result = firstNumber * secondNumber;
        break;
    case "÷":
        if (secondNumber != 0)
            result = firstNumber / secondNumber;
        else
            lblResult.Text = "Error";
        return;
}
```
C# では 演算子ごとに処理を分岐 させなければならないため、どうしてもコードが長くなる。

(参考) Python（Tkinter）の実装例
Python (Tkinter) を使った場合、eval() を活用することで 極めて短いコードで電卓を作成 できる。
```py
import tkinter as tk

def on_click(event):
    text = event.widget.cget("text")
    if text == "=":
        try:
            result.set(eval(result.get()))
        except:
            result.set("Error")
    elif text == "C":
        result.set("")
    else:
        result.set(result.get() + text)

root = tk.Tk()
root.geometry("300x400")

result = tk.StringVar()
entry = tk.Entry(root, textvar=result, font="Arial 20")
entry.pack(fill="both")

buttons = [
    "7", "8", "9", "+",
    "4", "5", "6", "-",
    "1", "2", "3", "*",
    "C", "0", "=", "/"
]

for i in range(0, len(buttons), 4):
    frame = tk.Frame(root)
    frame.pack()
    for text in buttons[i:i+4]:
        btn = tk.Button(frame, text=text, font="Arial 20", height=2, width=5)
        btn.pack(side="left")
        btn.bind("<Button-1>", on_click)

root.mainloop()
```
![1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3676736/0caba02b-49fe-eb20-a1db-aa331a405bc3.jpeg)


eval() の強力さ
result.set(eval(result.get()))
この1行で 全ての計算を実行 できるため、C# の switch 文のような処理が不要になる。

「これだけ」のpythonに比べて、これから以下に書いていくUnityでの電卓つくりは、むなしくなるほど圧倒的に大変である、といえる。

さて、Unity作成した電卓は図のようである。
 ![calculatorUnity.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3676736/ea81fb87-e162-8d57-d68c-69fe06501336.jpeg)


その手順を要約する。

1. プロジェクトの準備
(1) Unityのインストール
Unity Hubを使用して最新のLTSバージョンのUnityをインストール。
(2) 新しいプロジェクトの作成
Unity Hubを開き、「新しいプロジェクト」。テンプレート: 2D を選択。
プロジェクト名: 「Calculator」。保存場所を選び、「作成」。

2. UIの作成
　各UIの役割
Button 19個：計算機のボタン（0〜9、演算子４つ：+-*/、クリア、イコール、正負逆転、％、小数点）
Input Field 1個目	ユーザーの入力を表示
Input Field 2個目	計算結果を表示

(1) Canvasの作成
Hierarchyウィンドウで Right Click → UI → Canvas を作成。
Canvasの Render Mode を Screen Space - Overlay に設定

(2) 入力表示用のテキストボックス
Input Field (TMP)（2個） を配置
Hierarchyで Right Click → UI → Panel
（その子として）そのPnelの上で、Right Click → UI → Input Field (TMP) を作成（TextMeshProのバージョン推奨）Rect Transform を調整して画面上部に配置。Text を 0 等にして、Font Size を適切に調整。
複製して、２つ作成。


(3) ボタンの作成

まず画面上に、Button（19個）を配置

HierarchyでRight Click → UI → Button を作成し、名前を Button_1 に変更。
Text コンポーネントを 1 等に変更。Rect Transform でサイズと位置を調整。
作成した Button を Ctrl+D（Duplicate）で複製し、19個用意
他の数字 (0~9), 記号 (+, -, *, /, =, C) も同様に作成していく。


3.スクリプトの作成

Assets フォルダ内で Right Click → Create → C# Script
スクリプト名を CalculatorScript に変更し、以下のコードを記述。

ボタンに共通のスクリプトを適用すると効率がよい。
共通スクリプト（Calculator.cs）を作成した後、
OnClick() のイベントに同じ OnButtonClick() メソッドを登録
それぞれのボタンで、引数（ボタンの値） を渡す、等、このあとでおこなっていく予定


（１）スクリプトを1つ作成
```c#
using UnityEngine;
using UnityEngine.UI;
using TMPro;

public class Calculator : MonoBehaviour
{
    public TMP_InputField inputField;
    public TMP_InputField resultField;

    private string currentInput = ""; // 数式を保持
    private bool isResultDisplayed = false;

    // ボタンを押したときの処理
    public void OnButtonClick(string value)
    {
        Debug.Log($"Button Pressed: {value}");

        // 直前に計算結果が表示されていた場合
        if (isResultDisplayed)
        {
            // 数字が押された場合は currentInput をリセット
            if (char.IsDigit(value[0]) || value == ".")
            {
                currentInput = value;
            }
            // 演算子が押された場合は、計算結果を継続
            else
            {
                currentInput += value;
            }
            isResultDisplayed = false;
        }
        else
        {
            currentInput += value;
        }

        inputField.text = currentInput;
        Debug.Log($"Updated Input: {currentInput}");
    }

    // クリアボタンの処理
    public void OnClearClick()
    {
        Debug.Log("Clear Pressed");
        currentInput = "";
        inputField.text = "";
        resultField.text = "";
        isResultDisplayed = false;
    }

    // 計算結果を表示
    public void OnEqualClick()
    {
        Debug.Log($"Evaluating: {currentInput}");

        try
        {
            float result = EvaluateExpression(currentInput);
            Debug.Log($"Final Result: {result}");

            // 計算結果を currentInput に保存（次の計算に使えるようにする）
            currentInput = result.ToString();
            inputField.text = currentInput; // 入力フィールドに結果を表示
            resultField.text = currentInput; // 結果フィールドにも表示

            isResultDisplayed = true; // 計算完了フラグをセット
        }
        catch
        {
            resultField.text = "Error";
            Debug.LogError("Calculation Error");
        }
    }

    // 数式の評価
    private float EvaluateExpression(string expression)
    {
        Debug.Log($"Computing: {expression}");

        try
        {
            expression = expression.Replace("\"", "");

            object result = new System.Data.DataTable().Compute(expression, "");
            Debug.Log($"Computed Result: {result}");
            return float.Parse(result.ToString());
        }
        catch (System.Exception ex)
        {
            Debug.LogError($"Error in computation: {ex.Message}");
            return 0;
        }
    }

    // +/- ボタンの処理
    private void ToggleSign()
    {
        if (!string.IsNullOrEmpty(currentInput))
        {
            if (currentInput[0] == '-')
                currentInput = currentInput.Substring(1);
            else
                currentInput = "-" + currentInput;

            inputField.text = currentInput;
        }
    }

    // % ボタンの処理
    private void ApplyPercentage()
    {
        if (!string.IsNullOrEmpty(currentInput))
        {
            try
            {
                float value = float.Parse(currentInput) / 100f;
                currentInput = value.ToString();
                inputField.text = currentInput;
            }
            catch
            {
                resultField.text = "Error";
            }
        }
    }
}
```

（２）ボタンにスクリプトを適用

スクリプトのアタッチ先について
スクリプト（Calculator.cs）は、Canvas や Button、Text にはアタッチせず。
代わりに、空の GameObject（例: CalculatorManager） を作成し、そこにアタッチする。

①	 空のオブジェクトを作成
Unityの Hierarchy で右クリック、あるいはGameObject → Create Empty を選択。名前を CalculatorManager に変更。

②	CalculatorManagerのInspector で
Add Component をクリック
Calculator スクリプトを追加。スクリプトに UI 要素を紐づける。

Calculator スクリプトの inputField に、Input Field (TMP) を設定
Calculator スクリプトの resultField に、結果表示用 Input Field (TMP) を設定

③各ボタンの OnClick() にスクリプトのメソッドを登録
次に、ボタンの OnClick() に Calculator スクリプトの OnButtonClick() を設定。

Hierarchy で Button 1 などを選択
Inspector の Button コンポーネントを確認
Inspector内に表示されているOnClick() のリストを開く
+ボタンを押して、新しいイベントを追加
イベントの対象を CalculatorManager に設定　（AssetsでなくSceneにある）

None (Object) の部分をクリック
CalculatorManager を選択
（メソッドを Calculator.OnButtonClick(string) に設定、すなわち）
No Function → Calculator → OnButtonClick(string)　と選んでいく

引数にボタンの値を設定（下記表）

各ボタンに設定する引数の対応表
|ボタン|設定するメソッド|OnClick() の引数（“”をつけること）|
|---|---|---|
|0	|OnButtonClick(string)	|"0"|
|1	|OnButtonClick(string)	|"1"|
|2	|OnButtonClick(string)	|"2"|
|3	|OnButtonClick(string)	|"3"|
|4	|OnButtonClick(string)	|"4"|
|5	|OnButtonClick(string)	|"5"|
|6	|OnButtonClick(string)	|"6"|
|7	|OnButtonClick(string)	|"7"|
|8	|OnButtonClick(string)	|"8"|
|9	|OnButtonClick(string)	|"9"|
|.	|OnButtonClick(string)	|"."|
|+  |OnButtonClick(string)	|"+"|
|-  |OnButtonClick(string)	|"-"|
|*  |OnButtonClick(string)	|"＊"|
|/	|OnButtonClick(string)	|"/"|
|%	|OnButtonClick(string)	|"%"|
|+/-	|OnButtonClick(string)	|"+/-"|


特別なボタンの設定
|ボタン|設定するメソッド|OnClick() の引数|
|---|---|---|
|=	|OnEqualClick()	|(不要)|
|C	|OnClearClick()	|(不要)|
4.動作確認