Backendless(BaaS)とUnityを使ったオンラインショップの画面の作成【Unity,SaaS】

今回はゲームコンテストで作ろうと思っていたオンラインゲームのショップ画面をプロトタイプで作成してみました (たぶんコンテストにはこの要素はいれないです)

Demo

開発環境

  • Unity 2019.4.28f1
  • Backendless(BaaS)

使用したアセット

あまり関係ないですが,内部で以下も使用しています

backendlessとは?

(ほかのSaaSと同じなので,SaaSの説明を引用)

SaaS」(Software as a Service:「サース」または「サーズ」)とは、ソフトウェアを利用者(クライアント)側に導入するのではなく、提供者(サーバー)側で稼働しているソフトウェアを、インターネット等のネットワーク経由で、利用者がサービスとして利用する状況を指します。

引用元

SaaSとは | クラウド・データセンター用語集/IDCフロンティア

参考にした参考書

booth.pm

公式ドキュメント

backendless.com

実装

実装した機能としては以下です

  • Moqデータとしてコード側で一定するのデータをDBに追加する関数
  • 画面上からアイテムの購入(図1)
  • 購入したアイテムの削除

f:id:ayousanz:20210707221351p:plain
図1

各種非同期部分の書きかたと改善作成

非同期部分の書き方はtwitter で KOGAさんにアドバイスいただきました

詳細は以下のtweetのスレをご確認ください (今回はアドバイスをすべて反映できていません)

クラス図

f:id:ayousanz:20210707221430p:plain

スクリプト

DBHandler

using System;
using System.Collections.Generic;
using _OnlineShop.Scripts;
using UnityEngine;
using BackendlessAPI;
using BackendlessAPI.Persistence;
using Cysharp.Threading.Tasks;
using UniRx;
using UnityEditor;
using Random = UnityEngine.Random;

public class DBHandler : IDBHandler,IDisposable
{
    private readonly CompositeDisposable _compositeDisposable = new CompositeDisposable();

    public async void DeleteItemData(string tableName,Dictionary<string, object> itemInfo)
    {
        var savedContact = await Backendless.Persistence.Of(tableName).SaveAsync(itemInfo);
        await Backendless.Persistence.Of(tableName).RemoveAsync(savedContact);
    }

    //データの新規追加(同じ名前があっても新規に追加される)
    public async void AddItemData(string itemName,int itemCount,int itemPrice)
    {
        var dict = new Dictionary<string, object>
        {
            {"itemName",itemName},
            {"itemCount",itemCount},
            {"itemPrice",itemPrice}
        };
        var result = await Backendless.Data.Of("ShopList").SaveAsync(dict);
        Debug.Log($"result:{result}");
    }

    public async void UpdateItemData(string itemGroup,string itemName,int itemCount,int itemPrice)
    {
        var result = await Backendless.Data.Of(itemGroup).FindFirstAsync();
        result[itemName] = 30;
        await Backendless.Data.Of("Person").SaveAsync(result);
    }

    public async UniTask<IList<Dictionary<string, object>>> GetTableData(string tableName)
    {
        var query = DataQueryBuilder.Create();
        query.AddSortBy("created desc");
        query.SetRelationsDepth( 0 );
        return await Backendless.Data.Of(tableName).FindAsync(query).AsUniTask();
    }

    public void AddMoqData(string tableName)
    {
        for (var index = 1; index <= 6; index++)
        {
            AddItem(index);
        }
        async void AddItem(int itemNo)
        {
            var itemName = $"{tableName}" + itemNo;
            var dict = new Dictionary<string, object>
            {
                {"itemName",itemName},
                {"itemCount",Random.Range(0,30) * 5},
                {"itemPrice",Random.Range(2,100) * 10}
            };
            await Backendless.Data.Of(tableName).SaveAsync(dict);
        }
    }
    
    #if UNITY_EDITOR
    void HandleOnPlayModeChanged( PlayModeStateChange stateChange )
    {
        // This method is run whenever the playmode state is changed.
        if ( stateChange == PlayModeStateChange.ExitingEditMode || stateChange == PlayModeStateChange.ExitingPlayMode )
        {
            Backendless.RT.Disconnect();
        }
    }
    #endif
    public DBHandler()
    {
#if UNITY_EDITOR
        EditorApplication.playModeStateChanged += HandleOnPlayModeChanged;
#endif
    }

    public void Dispose()
    {
        _compositeDisposable?.Dispose();
    }
}

GameLifetimeScope

using _OnlineShop.Scripts;
using UnityEngine;
using VContainer;
using VContainer.Unity;

public class GameLifetimeScope : LifetimeScope
{
    [SerializeField] private ShopShopView shopShopView;

    protected override void Configure(IContainerBuilder builder)
    {
        builder.RegisterComponent(shopShopView).AsImplementedInterfaces();
        builder.Register<DBHandler>(Lifetime.Singleton).AsImplementedInterfaces();
        builder.RegisterEntryPoint<GameManager>();
    }
}

GameManager

using System;
using System.Linq;
using _OnlineShop.Scripts;
using UniRx;
using UnityEngine;
using VContainer.Unity;

public class GameManager : IStartable,IDisposable
{
    private readonly IDBHandler _dbHandler;
    private readonly IShopView _shopView;
    private readonly CompositeDisposable _compositeDisposable = new CompositeDisposable();
    private GameManager(IDBHandler dbHandler,IShopView shopView)
    {
        _dbHandler = dbHandler;
        _shopView = shopView;
    }

    public async void Start()
    {
        // _dbHandler.AddMoqData("Food");
        var d = await _dbHandler.GetTableData("Food");
        foreach (var (item,index) in d.Select((item,index) => (item,index)))
        {
            _shopView.SetText(item["itemName"].ToString(),item["itemCount"].ToString(),item["itemPrice"].ToString(),index);
        }

        _shopView.BuyItemObservable().Subscribe(value =>
        {
            _shopView.SetBuy(true,value);
            Debug.Log($"food:{value}を購入");
            _dbHandler.DeleteItemData("Food",d[value]);
        }).AddTo(_compositeDisposable);
    }

    public void Dispose()
    {
        _compositeDisposable?.Dispose();
    }
}

Item

using System;
using UniRx;
using UnityEngine;
using UnityEngine.UI;

namespace _OnlineShop.Scripts
{
    public class Item : MonoBehaviour
    {
        [SerializeField] private Text itemNameText;
        [SerializeField] private Text itemCountText;
        [SerializeField] private Text itemPriceText;
        [SerializeField] private Image itemIcon;
        [SerializeField] private Button buyButton;
        [SerializeField] private Image backgroundImage;

        public IObservable<Unit> OnClickBuyButton()
        {
            return buyButton.OnClickAsObservable();
        }

        public void SetText(string itemName, int itemCount, int itemPrice,int iconNo)
        {
            itemNameText.text = itemName;
            itemCountText.text = $"{itemCount}個";
            itemPriceText.text = $"${itemPrice}";
            itemIcon.sprite = GetIcon(iconNo);
        }

        private static Sprite GetIcon(int iconNo)
        {
            return Resources.Load<Sprite>($"Food/Icon{iconNo}");
        }

        public void SetBuy(bool isBuy)
        {
            backgroundImage.color = new Color(115, 115, 115);
            buyButton.interactable = false;
        }
    }
}

ShopShopView

using System;
using System.Collections.Generic;
using System.Linq;
using UniRx;
using UnityEngine;

namespace _OnlineShop.Scripts
{
    public class ShopShopView : MonoBehaviour,IShopView
    {
        [SerializeField] private List<Item> itemList = new List<Item>();

        private readonly Subject<int> _buySubject = new Subject<int>();
        
        private void Awake()
        {
            foreach (var (item,index) in itemList.Select((item,index) => (item,index)))
            {
                item.OnClickBuyButton().Subscribe(_ =>
                {
                    _buySubject.OnNext(index);
                }).AddTo(this);
            }
        }

        public void SetView(bool isView)
        {
            gameObject.SetActive(isView);
        }

        public void SetText(string itemName, string itemCount, string itemPrice,int itemNo)
        {
            if (itemNo < itemList.Count)
            {
                itemList[itemNo].SetText(itemName,int.Parse(itemCount), int.Parse(itemPrice),itemNo+1);
            }
        }

        public void SetBuy(bool isBuy,int itemNo)
        {
            itemList[itemNo].SetBuy(isBuy);
        }

        public IObservable<int> BuyItemObservable()
        {
            return _buySubject;
        }
    }
}