今回はゲームコンテストで作ろうと思っていたオンラインゲームのショップ画面をプロトタイプで作成してみました (たぶんコンテストにはこの要素はいれないです)
Demo
ショップ画面でお買い物がやっとできた!!(以下のBaaSを使用)https://t.co/ezJ5aXwCl8 pic.twitter.com/VZRWWNEoEF
— ようさん (@ayousanz) 2021年7月7日
開発環境
- Unity 2019.4.28f1
- Backendless(BaaS)
使用したアセット
あまり関係ないですが,内部で以下も使用しています
backendlessとは?
「SaaS」(Software as a Service:「サース」または「サーズ」)とは、ソフトウェアを利用者(クライアント)側に導入するのではなく、提供者(サーバー)側で稼働しているソフトウェアを、インターネット等のネットワーク経由で、利用者がサービスとして利用する状況を指します。
引用元
SaaSとは | クラウド・データセンター用語集/IDCフロンティア
参考にした参考書
公式ドキュメント
実装
実装した機能としては以下です
- Moqデータとしてコード側で一定するのデータをDBに追加する関数
- 画面上からアイテムの購入(図1)
- 購入したアイテムの削除
各種非同期部分の書きかたと改善作成
非同期部分の書き方はtwitter で KOGAさんにアドバイスいただきました
詳細は以下のtweetのスレをご確認ください (今回はアドバイスをすべて反映できていません)
https://t.co/wvydmKx7ZR.Of(tableName).Findの処理完了を取得したいんだけど,どうやってUniTaskに変換したらいいんだ?
— ようさん (@ayousanz) 2021年7月5日
クラス図
各スクリプト
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; } } }