ゲーム化!tomo_manaのブログ

ゲーム化!tomo-manaのブログ

Unityでゲームを作る方法について紹介しています

Unity×会計#12 取扱商品の複数化(Unity2019.4.4f1)

f:id:tomo_mana:20211220011635p:plain

第9回で追加したドロップダウンから商品を選択できるようにして、商品を選択できるようにします。

修正方針

個々の商品の仕入量・売上量を管理するため、仕入帳・売上帳を商品点数分用意します。

f:id:tomo_mana:20211127092355p:plain
データ構造(商品複数化の影響範囲)

ログにも各商品の在庫を表示するようにします。

f:id:tomo_mana:20211127094359p:plain
在庫表示(複数化)

仕入、売上の欄も商品5個の総額になるので、在庫を表示する時に合わせて計算します。

影響範囲

仕入、売上を計上する時に、これまでは商品が1種類しかなかったため、数量と取引の総額だけを意識すれば良かったですが、今回は商品が複数になるので、仕入・売上を計上する時に、どの商品の取引かを伝える必要があります。そのため、仕入・売上用のインターフェースを拡張します。

f:id:tomo_mana:20211127095403p:plain
データへのアクセス関数

仕入・売上用のインターフェース
ExecutePurchase()、ExecuteSales()に、商品ID を追加します。

// 購入個数、総額
//void ExecutePurchase(int amount, int price);
// 商品ID、購入個数、総額
void ExecutePurchase(int id, int amount, int price);

試算用の領域
仕入高・売上高の他、在庫高も商品数分の領域が必要ですので、試算用の領域が配列になります。個々の仕入高・売上高を合計した総仕入高・総売上高も追加します。

public class TrialBalance {
    public int cash;        // 現金
    public int[] purchase = new int[5];    // 仕入
    public int[] sales = new int[5];       // 売上
    public int[] q_remain = new int[5];    // 在庫残
    public int[] q_total = new int[5];  // 在庫総購入量
    public int[] unit = new int[5];         // 仕入単価

    public int purchase_all;   // 総仕入高
    public int sales_all;    // 総売上高
}


ここからは、ドロップダウンリストの使い方を確認していきます。

ドロップダウンリスト

ドロップダウンへの要素の追加

今回は、ドロップダウンへの要素の追加は、Inspectorウィンドウ から行います。今回はTextMeshPro版を使用していますが、基本的なインターフェースはText版Dropdownと同じです。TextMeshPro版のドロップダウンはTMProパッケージ内にある TMP_Dropdown クラスです。


オプションの設定
(1) Inspectorウィンドウ → Dropdown (TextMeshPro) コンポーネント → Options
(2) その他のメニューと同じように、+ボタンで項目追加 → 名前を追加

f:id:tomo_mana:20211127102108p:plain
Dropdown - TextmeshPro - Options


なお、名前の下にSprite設定欄があるのですが、ここにアイコンを入れても何も表示されません。この sprite設定 に関する記事もほとんど見つからない状態でした(残念)。

こういうのを期待したんですが…そうはならないようです。
f:id:tomo_mana:20211127183535p:plain

(イラストは七三ゆきさんのフリー素材を使わせていただいています。ありがとうございます。)


このSprite領域は、どちらかというと項目への画像の参照として使うためにあるのかもしれません(ある項目が選ばれた時に、その項目に該当するイメージ画像への参照を取得する)

要素の選択、確認

ドロップダウンの各要素は、一番上を0番としたリスト番号が振られていて、現在選択されている要素の番号はDropdownコンポーネントの「Value」で取得できます。

(リスト、選択状態、値の図)

f:id:tomo_mana:20211127173558p:plain
ドロップダウン表示
using TMPro;

// TextMeshPro版のドロップダウンはTMP_Dropdownクラス
TMP_Dropdown dropdown;
// 通常のドロップダウンと同じく value で選択番号 (先頭は0から開始) を取得できる
id = dropdown.value;

選択された要素情報の取得

選択された要素名は captionText に保存されています。

string text = dropdown.captionText.text;


選択された要素名は、同じく Dropdown下にある Label.text にも格納されています(表示用に使われます)

f:id:tomo_mana:20211127185526p:plain
Dropdown → Label.text


ドロップダウンの全要素は TMP_Dropdown.OptionDataクラスの options に保存されています。

foreach ( TMP_Dropdown.OptionData option in dropdown.options ){
    string text = option.text;
}

Dropdown.options
UI.Dropdown-options - Unity スクリプトリファレンス

Dropdown.optionData
UI.OptionData - Unity スクリプトリファレンス
optionDatastring textSprite image で構成されています。

格納された Image の方は Sprite クラスとしてアクセスできます。
UnityEngine.Sprite - Unity スクリプトリファレンス


尚、ドロップダウンの選択番号が変更された時に、コールバックを設定できます(onValueChanged)。

Dropdown.onValueChanged
UI.Dropdown-onValueChanged - Unity スクリプトリファレンス

今回は、ボタンが押された時にドロップダウンの選択番号を参照する方法で実装するので、コールバックは使用しませんが、

修正範囲

ここからは、商品の複数化(配列化)による修正範囲を見ていきます。

f:id:tomo_mana:20211127092355p:plain
修正範囲

個数の変更
仕入の例を示します。仕入の場合は、仕入帳purchaseを配列としてアクセスして、商品番号(itemid)指定で登録するように変更します。

public int ExecutePurchase(int itemId, int amount, int price)
{
    int e = 1;
    
    if( itemId < ITEM_ID_MAX ){
        if( cash.Get() >= price ){
            cash.Add( -price );
            purchase[itemId].Add( amount, price );
            e = 0;
        }
    }
    return e;
}

各個数から総高を計算
こちらも、各領域を配列指定に拡張して、総仕入高・総売上高計算を追加します。

public TrialBalance GetTrialBalance()
{
    tb.cash = cash.Get();
    tb.purchase_all = 0;
    tb.sales_all = 0;
    
    for( int i = 0; i < ITEM_ID_MAX; i++ ){
        // 仕入
        tb.purchase[i] = purchase[i].Get(1);
        tb.purchase_all += tb.purchase[i];    // 追加
        // 売上
        tb.sales[i] = sales[i].Get(1);
        tb.sales_all += tb.sales[i];    // 追加
        // 在庫
        tb.q_remain[i] = purchase[i].Get(0) - sales[i].Get(0);
        tb.q_total[i] = purchase[i].Get(0);
        // 単価(移動平均法)
        if( purchase[i].Get(0) != 0 ){
            tb.unit[i] = purchase[i].Get(1) / purchase[i].Get(0);
        } else {
            tb.unit[i] = 0;
        }
    }
    return tb;
}

コード

入力部(PurchaseInput.cs)

public class PurchaseInput : MonoBehaviour, ILangSwitchHandler
{
    void ExecutePurchase(int itemId, int amount, int price)
    {
        int e = 0;

        if( !ReferenceEquals(ts, null) ){
            e = ts.ExecutePurchase( itemId, amount, price );
        }
        if( e != 0 ){
            e = 1;
        }
        UpdateLog( e );
    }

    public struct InputLine {
        public TMP_Dropdown inputItemId;            // 商品名(ID)
        public TMP_InputField inputAmount;      // 個数
        public TMP_InputField inputNetPrice;   // 単価
    }
    InputLine[] inputLine = new InputLine[1];
    
    // Start is called before the first frame update
    void Start()
    {
        Button button;
        foreach( Transform child in gameObject.transform ){
            Transform gc = child;
            // InputLine に属するオブジェクトの参照を保持
            if( gc.GetComponent<TMP_InputField>() != null ){
                if( gc.gameObject.name == "InputAmount" ){
                    // デフォルトの仕入・売上数量
                    inputLine[0].inputAmount = gc.GetComponent<TMP_InputField>();
                    inputLine[0].inputAmount.text = "10";
                } else
                if( gc.gameObject.name == "InputNetPrice" ){
                    // デフォルトの仕入・売上価格
                    inputLine[0].inputNetPrice = gc.GetComponent<TMP_InputField>();
                    inputLine[0].inputNetPrice.text = "1000";
                }
            } else
            if( gc.GetComponent<Button>() ){
                button = gc.GetComponent<Button>();
                int ii = (int)buttonName;
                button.onClick.AddListener(() => ButtonClicked(ii));
            } else
            if( gc.GetComponent<TMP_Dropdown>() ){
                inputLine[0].inputItemId = gc.GetComponent<TMP_Dropdown>();
            }
        }
    }
}

データベース(SalesTransaction2.cs)

public class SalesTransaction2 : MonoBehaviour
{
    // 試算表
    TrialBalance tb = new TrialBalance();
    // 試算表(本来の意味で)
    TrialBalanceDB tbs = new TrialBalanceDB();
    
    // 最大商品種類
    int ITEM_ID_MAX = 5;
    // 販売物の仕入単価(現在は固定)
    int[] unitPrice = new int[5];
    // 仕入帳
    DoublePoint[] purchase = new DoublePoint[5];
    // 売上帳
    DoublePoint[] sales = new DoublePoint[5];
    // 棚卸
    InventoryDB[] iv = new InventoryDB[5];
    
    /* その他の定義 略 */
    
    // 配列化された商品の初期化
    void InstantiateClassArray()
    {
        for( int i = 0; i < ITEM_ID_MAX; i++ ){
            purchase[i] = new DoublePoint();
            sales[i] = new DoublePoint();
            iv[i] = new InventoryDB();
        }
    }
    
    // 途中経過を確認
    public TrialBalance GetTrialBalance()
    {
        tb.cash = cash.Get();
        tb.purchase_all = 0;
        tb.sales_all = 0;
        
        for( int i = 0; i < ITEM_ID_MAX; i++ ){
            tb.purchase[i] = purchase[i].Get(1);
            tb.purchase_all += tb.purchase[i];
            tb.sales[i] = sales[i].Get(1);
            tb.sales_all += tb.sales[i];
            tb.q_remain[i] = purchase[i].Get(0) - sales[i].Get(0);
            tb.q_total[i] = purchase[i].Get(0);
            if( purchase[i].Get(0) != 0 ){
                tb.unit[i] = purchase[i].Get(1) / purchase[i].Get(0);
            } else {
                tb.unit[i] = 0;
            }
        }
        return tb;
    }
    
    public void InitializeJournal()
    {
        // 現金出納帳
        cash.Clear();
        cash.Add(firstCash);
        
        for( int i = 0; i < ITEM_ID_MAX; i++ ){
            // 仕入帳
            purchase[i].Clear();
            // 売上帳
            sales[i].Clear();
        }
        
        bs.Set( cash.Get(), 0, 0, 0, 0 );
        pl.Set( 0, 0 ); 
    }
    
    public int ExecutePurchase(int itemId, int amount, int price)
    {
        int e = 1;
        
        if( itemId < ITEM_ID_MAX ){
            if( cash.Get() >= price ){
                cash.Add( -price );
                purchase[itemId].Add( amount, price );
                e = 0;
            }
        }
        return e;
    }
    
    public int ExecuteSale(int itemId, int amount, int price)
    {
        int e = 1;
        
        if( itemId < ITEM_ID_MAX ){
            if( (purchase[itemId].Get(0) - sales[itemId].Get(0)) >= amount ){
                cash.Add( price );
                sales[itemId].Add( amount, price );
                e = 0;
            }
        }
        return e;
    }
    
    void CarryOver()
    {
        int[] tmp = new int[2];
        
        // 現金の繰り越し
        tmp[0] = cash.Get();
        cash.Clear();
        cash.Add(tmp[0]);
        
        for( int i = 0; i < ITEM_ID_MAX; i++ ){
            // 仕入の繰り越し
            tmp[0] = iv[i].q_carryover;
            tmp[1] = tmp[0] * unitPrice[i];
            purchase[i].Clear();
            if( tmp[0] != 0 ){
                purchase[i].Add(tmp[0], tmp[1]);
            }
            
            // 売上の繰り越し
            sales[i].Clear();
        }
    }
    
    // 決算処理
    public int ExecuteSettle()
    {
        int i;
        
        // 棚卸と原価計算
        for(i = 0; i < ITEM_ID_MAX; i++){
            // 棚卸表の作成(今のところ仕入れてないものを売るは想定しない)
            iv[i].Set( purchase[i].Get(0), sales[i].Get(0) );
            
            // 原価計算(端数は当期中に処理すること)
            if( purchase[i].Get(0) != 0 ){
                unitPrice[i] = purchase[i].Get(1) / purchase[i].Get(0);
            } else {
                unitPrice[i] = 0;
            }
        }
        
        tbs.Clear();
        for(i = 0; i < ITEM_ID_MAX; i++){
            tbs.expenses += purchase[i].Get(1) - iv[i].q_carryover * unitPrice[i];
            tbs.revenue += sales[i].Get(1);
            tbs.flexAssets += iv[i].q_carryover * unitPrice[i];
        }
        
        // 損益計算
//      pl.Set( purchase.Get(1) - iv.q_carryover * unitPrice, sales.Get(1) );   
        pl.Set( tbs.expenses, tbs.revenue );    
        
        // 貸借対照表
//      bs.Set( cash.Get(), iv.q_carryover * unitPrice, 0, 0, 0 );
        bs.Set( cash.Get(), tbs.flexAssets, 0, 0, 0 );
        
        // 繰り越し
        CarryOver();

        return 0;
    }
    
    // Start is called before the first frame update
    void Start()
    {
        // クラス配列の初期化
        InstantiateClassArray();
        
        // 仕訳の初期化
        InitializeJournal();
    }
    
    /* 略 */
}

データ構造(Transaction.cs)

public class TrialBalance {
    public int cash;        // 現金
    public int[] purchase = new int[5];    // 仕入
    public int[] sales = new int[5];       // 売上
    public int[] q_remain = new int[5];    // 在庫残
    public int[] q_total = new int[5];  // 在庫総購入量
    public int[] unit = new int[5];         // 仕入単価
    
    public int purchase_all;
    public int sales_all;
}

// 試算表(より本来に近い)
public class TrialBalanceDB {
    public int expenses;    // 支出
    public int revenue;     // 収益
    public int flexAssets;  // 流動資産(箱型バランスシートのため現金除く)
    
    public void Clear()
    {
        expenses = 0;
        revenue = 0;
        flexAssets = 0;
    }
}

ログ(JournalLog.cs)

public class JournalLog : MonoBehaviour, ILangSwitchHandler
{
    public void UpdateLog(int error)
    {
        TrialBalance tb = default;
        
        if( !ReferenceEquals(ts, null) ){
            tb = ts.GetTrialBalance();
        }
        if( !ReferenceEquals(tb, null) ){
            if( error == -1 ){
                error = error_hold;
            } else {
                error_hold = error;
            }
            if( !ReferenceEquals(log, null) ){
                //現金:xxxx/仕入:xxxx/売上:xxxx/在庫:xx/xx(@100)
                if( selected == LANG.LANG_ENG ){
                    log.text = msg_e[error] + "\nCash:" + tb.cash + "/Purchases:" + tb.purchase_all + "/Sales:" + tb.sales_all;
                    log.text = log.text + "\nRemain:";
                } else {
                    log.text = msg_j[error] + "\n現金:" + tb.cash + "/仕入:" + tb.purchase_all + "/売上:" + tb.sales_all;
                    log.text = log.text + "\n在庫:";
                }
                for(int i = 0; i < 5; i++){
                    log.text = log.text + i + "]" + tb.q_remain[i] + "/" + tb.q_total[i] + "(@" + tb.unit[i] + ")";
                }
            }
        }
    }
}

プレイ画面

ゲームのURL(前回までと同じURLにしました)
tomo-mana.hatenablog.com


これで、財務諸表を扱うゲームの下地が完成しました。次回からは、ゲームの内容について案内していきます。

財務諸表への直接入力モードを、そのうちに復活させます。また、余裕が出てきたら、今度は工業簿記について触れていきたいと思います。


(参考)
ドロップダウンの使い方は、以下が分かりやすかったです。
xr-hub.com