オフラインゲームのデータをGitHubに半永久的に保存する

序論

ひと昔ではオフラインゲーム(フリーゲーム)が流行っていました.
いまではオンラインゲームが主流となりゲームデータはクラウド上に保存され,SNSがあればどこからでもどの端末からでもセーブした段階からはじめることができるようになりました(*1). しかし,オフラインゲームはオンラインに保存されないため数年後に同じゲームをやりたいとなると過去のセーブデータを探さなくていけません.数年前のデータは基本保存されていない・どこにあるかわからないことが普通です.

(*1) : ログインしていない期間が長いため運営側でのアカウント削除やサービス終了は起こりうる

目的

今回はデータがどこにいったのかわからない・そもそも数年前のデータなんてないという事態をさけるために, GitHub を使った半永久的なデータ保存方法をメモしておきます
(ゲーム以外にも卒論の資料作成や小説の文章管理などさまざまなところにも活用できるかと思います)

対象者

  • Git,GitHubを触ったことがある人
  • オフラインゲームをやっていてセーブデータがなくて同じゲームを何回もやり直している人
  • 以下の環境の方
    • Windows OS
    • Gitがインストール済み

GitHubへbatファイルで一定時間ごとに保存する

今回は GitHubに手作業で commit・pushすることすらめんどくさいので,batファイルを使用して自動化してしまいます

Batファイルの作成

Batファイルについては以下を参考にしてください

e-words.jp

  1. はじめに 自動的に実行するためにBatファイルを作成します

右クリックからTextを選んでTextファイルを作成します.

f:id:ayousanz:20211213202743p:plain

2 . Textファイルの中身を以下のコードをコピペします

:loop

git add *
git commit -m "save"
git push origin main

powershell sleep 300

goto :loop
  1. テキストを任意の名前にして,Batファイルとして保存する

ファイルの内容にコードを入れたら,「名前をつけて保存」からBatファイルとして保存します

ファイル名の一番後ろに .bat と入れます. またファイルの種類は「すべての種類」を選択します

f:id:ayousanz:20211213203323p:plain

Batファイルの動作確認

生成したファイルをダブルクリックして,実行すると以下のような画面が出てくれば成功です.

f:id:ayousanz:20211213203556p:plain

GAS経由でGitHub IssueをGoogle Sheetへ自動更新【GAS,GitHub,Google 】

プロジェクト管理をするときにいろいろ選択肢があるのかで,個人プロジェクトや同人チームだとGoogle スプレッドシートを使うことが多々あるかと思います. GitHubのissueと当時に使用するときに両方更新するのはめんどくさいので,issue側を使用してシートは確認用で自動的に更新されるようにしました (実際に所属しているチームで使用しています)

導入前のタスク管理方法と過去の状態

以下のチーム(二人や一人のところもあるのでチームとは呼べない部分もありますが,ここではチームをします)があります.

  • Unityでのゲーム実装
  • ゲームの企画と仕様
  • ゲームで使用するイラスト
  • 運営上のタスク

これらの全体のタスクを可視化して,進捗を確認するためにすべてをGoogle スプレッドシートで管理を行っていましたが,開発側はGitHubを使用しているためissueを使いタスクのcloseなどを自動化したいという要望がありました. そこでGitHubのissueをメインで使用して,スプレッドシートを確認用だけに変更する方針にしました.

Demo

実際のGooleスプレッドシート側の画像になります.ぼかしていますが,タスク名・タスク内容・担当・issueへのリンクを表示しています.

f:id:ayousanz:20211108012234p:plain

GitHubからissueの取得

GitHubのアクセストークンの取得方法

まずはGASからGitHubにアクセスしたいので,GitHubのアクセストークンを発行します.

  1. GitHubの右上のところからSettingsを開きます.

f:id:ayousanz:20211114185909p:plain

2 . サイドメニューの Deleloper settings を開きます.

f:id:ayousanz:20211114190034p:plain

3 . Personal sccess token から Generate new token で新規にトークンを作成します

f:id:ayousanz:20211114190143p:plain

生成時の設定は repo に入れてます. 名前は任意のものを入れて,日程は使用する期間を設定します.

実装内容

上記でアクセストークンを取得することができたため,以下のスクリプトで任意のRepositoryのissueを取得することができるようになります. issueのステータスは,以下の3つを指定できますので使用する状況に合わせて変更していきます

状態 パラメータ
全て取得 all
開いているもの open
閉じているもの closed

(以下のコードはopenで指定しています)

参考サイト(GitHubのissueのAPIドキュメント)

Issues - GitHub Docs

function GetOpenIssues() {
  var url = 'https://api.github.com/repos/GitHub Your Name/RepositoryName/issues?state=open&per_page=100';
  var accessToken = 'your accessToken';
  var headers = {
    'Authorization': 'token '+ accessToken
  };

  var options = {
    'method': 'GET',    // GETやPUTなどを指定します。Qiita APIによって異なります
    'headers': headers,  // 上で作成されたアクセストークンを含むヘッダ情報が入ります
  };
  var response = UrlFetchApp.fetch(url, options).getContentText();
  var data = JSON.parse(response);
  return data;
}

Googleスプレッドシートの更新

スプレッドシートのIDの取得

まず更新シートのIDを取得します.

実装内容

取得したスプレッドシートのIDからデータの更新を行っていきます. 先ほどGitHubのissueから一覧を取得したので,これらの一覧データを引数に入れてスプレッドシート内のデータを更新しています.

function UpdateGSheet(issueDataList) {
  var sheetId = 'your sheet id';
  var sheetName = 'クライアント';
  var ss = SpreadsheetApp.openById(sheetId).getSheetByName(sheetName);

  var issueObjectList = []
  var issueSize = Object.keys(issueDataList).length;
  console.log("issue size:" + issueSize);
  for(var index = 0;index < issueSize ;index++){
    var d = issueDataList[index];
    var issueObject = UpdateIssue(d);
    issueObjectList.push(issueObject);
  }
  var issueInfoSize = Object.keys(issueObjectList[0]).length;
  console.log(issueSize + "," + issueInfoSize)

  // シートをクリア
  ClearBeforeSheetInfo(ss);

  ss.getRange(2,1,issueSize,issueInfoSize).setValues(issueObjectList);
}

function ClearBeforeSheetInfo(sheet){
  var range = sheet.getRange("A2:X100");
  range.clear({contentsOnly: true});
}

function UpdateIssue(issueData){
  var assign = issueData['assignee']
  var labelName = issueData['labels']

  var issueList = []
  if(assign == null){
    issueList = [issueData['number'],issueData['title'],"修正中","指定なし",issueData['url']]
  }else{
    issueList = [issueData['number'],issueData['title'],"修正中",GetUserName(assign['login']),issueData['url']]
  }
  return issueList;
}

一時間ごとに自動更新を行う.

手動でissueの更新をスプレッドシートに更新していたのでは意味がないので,GASのトリガーで一定時間ごとにスクリプトを実行するように設定します.

トリガーの設定画面 f:id:ayousanz:20211114191714p:plain

unity1week「ちゅう」のゲームで実装したイベントアニメーション【unity1week,Unity】

今回プロなろというグループでチーム開発を行いました!

ジャンプキングに似たやりこみのあるゲームになっています

Repositoryは公開しているので,細かい実装はRepositoryを参照ください

github.com

(注) : 実装説明により一部ゲームのネタバレ等含みます

開発環境

  • Unity 2020.3.17f1
  • Github(主にCI/CD用)

使用アセット(関連するもののに記載)

UPM

Asset Store

イベントアニメーション

各画面のイベントアニメーション

各画面にて以下のようなゲームイベントが存在します

これらを実装するために,以下を採用しました

タイトルの背景アニメーション f:id:ayousanz:20210915212114g:plain

ゲーム開始時のアニメーション(ゲーム画面) f:id:ayousanz:20210915213622g:plain

ゲームクリア時のアニメーション(ゲーム画面) f:id:ayousanz:20210915214430g:plain

クリア画面の背景アニメーション f:id:ayousanz:20210915213107g:plain

ゲームオーバー画面のアニメーション f:id:ayousanz:20210915213319g:plain

実装方法

こちら基本的には DOTweenを 以下の方法で使用してイベントアニメーションを再生しています

  1. Sequenceを生成して,任意のところで再生
  2. trasform.DO~をUniTask型に変換して,awaitで前の処理が終わるまで待機

タイトルは Sequenceで実装しているので,以下のように実装しました

_sequence = DOTween.Sequence()
                .Append(transform.DOJump(new Vector3(3.65f, _firstYPosition, 0f), 0.3f, 10, 10f))
                .AppendCallback(() => characterDialogue.SetView(true).Forget())
                .AppendCallback(() => _rigidbody2D.bodyType = RigidbodyType2D.Dynamic)
                .Append(transform.DORotate(new Vector3(0f, 0f, 360f), 5f, RotateMode.FastBeyond360))
                .AppendCallback(() =>
                {
                    characterDialogue.SetView(false).Forget();
                    //ループ対応
                    Reset();
                });

            _sequence.Restart();

ゲームの開始時のアニメーションはDOTweenをawaitしています

        public async UniTask StartEvent()
        {
            await FallHole();
            await SetStartPosition();
            await Conversation();
        }
        
        private async UniTask FallHole()
        {
            player.DORotate(new Vector3(0f, 0f, 360f + 180f), 5f, RotateMode.FastBeyond360);
            await player.DOMoveY(-911f, 5f).ToAwaiter();
        }

        private async UniTask SetStartPosition()
        {
            player.DOJump(new Vector3(-45f, -900f, 0f), 200f, 1, 3f);
            await player.DORotate(new Vector3(0f, 0f, -180f), 3f).SetRelative(true).ToAwaiter();
        }

        private async UniTask Conversation()
        {
            await playerCharacterDialogue.SetView(true);
            playerCharacterDialogue.SetContent(firstConversationContent[0]);
            await UniTask.Delay(TimeSpan.FromSeconds(WaitBetweenConversationTime));
            playerCharacterDialogue.SetContent(firstConversationContent[1]);
            await UniTask.Delay(TimeSpan.FromSeconds(WaitBetweenConversationTime));
            await playerCharacterDialogue.SetView(false);
        }

ゲームのクリア情報のツイート機能

こちらの記事を参考に実装しました

画像のツイートは,できない時とできるときがあり調査時間がなかったため文字のみになりました

unity-senpai.hatenablog.com

クリア時間のランキング機能

unity roomのランキングはnaichiさんが公開されているものを使用させていただきました

時間は 〇分〇秒で表示したかったのでフォーマットを mm:ss に設定しています

blog.naichilab.com

開発運用(CI/CD)

今回の制作ではGithub Actionsを使って,テストとビルドを自動化しました

前に記事を書いたので,そちらを参考にしてみてください

ayousanz.hatenadiary.jp

設計

1週間の期間ですが,設計をある程度意識して実装をしています

UniRx,UniTaskを使ってMV(R)Pで実装をしています

各シーンのクラス図は以下のようになっています

タイトルシーン f:id:ayousanz:20210915220125p:plain

ゲームシーン f:id:ayousanz:20210915220137p:plain

ゲームクリアシーン f:id:ayousanz:20210915220144p:plain

ゲームオーバーシーン f:id:ayousanz:20210915220154p:plain

またフォルダは一応(めっちゃあいまいですが)ドメインごとにわけました f:id:ayousanz:20210915220213p:plain

コンピュータサイエンスやその他最近の技術を学べる/活用できるサイト【Unity,Blender,Python ...】

コンピュータサイエンス全般

コンピュータサイエンスについての無料公開をまとめたリポジトリ

github.com

ゲームによく使われるアルゴリズムがまとまっているサイト

www.redblobgames.com

gRPCについて

github.com

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;
        }
    }
}

MessagePipeを使って簡単なアクションゲームを作る【MessagePipe,Unity】

最近注目を集めている MessagePipeを使ってみようと思いいろいろ調べて,自分なりにアクションゲームを作ってみました! (ゲームといってもMessagePipeを使うことが目的なので,ゲームにすらなっていないです)

成果物

Demo

キーボードのAを押すことで,左側のUnity chanに攻撃することができます
また,左のUnity chanからは定期的に(5秒後ごと)攻撃を受けます どちらかのHPが0になったら勝敗モーションに移行後,ゲーム終了します

リポジトリ

リポジトリはpublicにしていますので,詳細はリポジトリをご確認ください

github.com

WebGL

ayutaz.github.io

MessagePipeとは?

公式のGithubページより

MessagePipe is a high-performance in-memory/distributed messaging pipeline for .NET and Unity. It supports all cases of Pub/Sub usage, mediator pattern for CQRS, EventAggregator of Prism(V-VM decoupling), etc.

  • Dependency-injection first
  • Filter pipeline
  • better event
  • sync/async
  • keyed/keyless
  • buffered/bufferless
  • broadcast/response(+many)
  • in-memory/distributed

MessagePipe is faster than standard C# event and 78 times faster than Prism's EventAggregator.

簡単に言うと,型でイベントの発行等を管理するライブラリみたいです(理解が怪しい)

環境

© Unity Technologies Japan/UCL

(注) VContainerとMessagePipeを使うときは,こちらのMessagePipe.VContainerを入れないと動かない?気がします

f:id:ayousanz:20210601161227p:plain

MessagePipeでの値の受け渡し方

関係する部分のみ記載しています.実際に使用方法は, #MessagePipe部分のコード もしくはリポジトリをご確認ください

イベントPublish側

//使用するPublisherとDisposeを定義
[Inject] private IPublisher<PlayerData> PlayerData { get; set; }
private IDisposable _disposable;

//イベントの発行

PlayerData.Publish(new PlayerData(){AttackValue = value,IsLose = isLose});

イベントSubscribe側

[Inject] private ISubscriber<EnemyData> EnemyData { get; set; }
var d = DisposableBag.CreateBuilder();
            EnemyData.Subscribe(e =>
            {
                playerHp -= e.AttackValue;
                isPlay = e.IsLose;
                _animator.SetTrigger("IsDamage");
                playerHpText.text = $"enemy hp:{playerHp}";

                if (playerHp <= 0)
                {
                    _animator.SetBool("IsLose",true);
                    PublishData(0,true);
                }
            }).AddTo(d);

            _disposable = d.Build();

MessagePipe部分のコード

Player.cs

using System;
using MessagePipe;
using UnityEngine;
using UnityEngine.UI;
using VContainer;

namespace Model
{
    public class Player : MonoBehaviour
    {
        [SerializeField] private int playerHp;
        [SerializeField] private int attackValue;
        [SerializeField] private Text playerHpText;
        [Inject] private ISubscriber<EnemyData> EnemyData { get; set; }
        [Inject] private IPublisher<PlayerData> PlayerData { get; set; }
        private IDisposable _disposable;

        private Animator _animator;
        private bool isPlay;

        private void Awake()
        {
            playerHpText.text = $"player hp:{playerHp}";
            _animator = GetComponent<Animator>();
        }

        private void Start()
        {
            var d = DisposableBag.CreateBuilder();
            EnemyData.Subscribe(e =>
            {
                playerHp -= e.AttackValue;
                isPlay = e.IsLose;
                _animator.SetTrigger("IsDamage");
                playerHpText.text = $"enemy hp:{playerHp}";

                if (playerHp <= 0)
                {
                    _animator.SetBool("IsLose",true);
                    PublishData(0,true);
                }
            }).AddTo(d);

            _disposable = d.Build();
        }


        private void Update()
        {
            if (Input.GetKeyDown(KeyCode.A) && !isPlay)
            {
                Debug.Log("プレイヤーが攻撃");
                _animator.SetTrigger("IsAttack");
                PublishData(3,false);
            }
        }

        private void PublishData(int value, bool isLose)
        {
            PlayerData.Publish(new PlayerData(){AttackValue = value,IsLose = isLose});
        }

        private void OnDestroy()
        {
            _disposable?.Dispose();
        }
    }
    
}

Enemy.cs

using System;
using MessagePipe;
using UniRx;
using UnityEngine;
using UnityEngine.UI;
using VContainer;
using VContainer.Unity;

namespace Model
{
    public class Enemy : MonoBehaviour,IStartable
    {
        [SerializeField] private int enemyHp;
        [SerializeField] private Text enemyHpText;
        private bool _isPlay;

        [Inject] private ISubscriber<PlayerData> PlayerData { get; set; }
        [Inject] private IPublisher<EnemyData> EnemyData { get; set; }

        private Animator _animator;
        private IDisposable _disposable;

        private void Awake()
        {
            enemyHpText.text = $"enemy hp:{enemyHp}";
            _animator = GetComponent<Animator>();
        }

        public void Start()
        {
            var d = DisposableBag.CreateBuilder();
            PlayerData.Subscribe(e =>
            {
                enemyHp -= e.AttackValue;
                _isPlay = e.IsLose;
                _animator.SetTrigger("IsDamage");
                enemyHpText.text = $"enemy hp:{enemyHp}";

                if (enemyHp <= 0)
                {
                    PublishData(0,true);
                    _animator.SetBool("IsLose",true);
                }
            }).AddTo(d);

            _disposable = d.Build();

            Observable.Interval(TimeSpan.FromSeconds(5f))
                .Where(_=>!_isPlay)
                .Subscribe(_ =>
                {
                    Debug.Log("敵が攻撃");
                    _animator.SetTrigger("IsAttack");
                    PublishData(3,false);
                }).AddTo(this);
        }
        
        private void PublishData(int value, bool isLose)
        {
            EnemyData.Publish(new EnemyData(){AttackValue = value,IsLose = isLose});
        }

        private void OnDestroy()
        {
            _disposable?.Dispose();
        }
    }
}

DI(VContainer)

using MessagePipe;
using Model;
using VContainer;
using VContainer.Unity;


public class GameLifetimeScope : LifetimeScope
{
    protected override void Configure(IContainerBuilder builder)
    {
        var options = builder.RegisterMessagePipe();
        builder.RegisterMessageBroker<PlayerData>(options);
        builder.RegisterMessageBroker<EnemyData>(options);

    }
}

受け渡しているデータクラス

namespace Model
{
    public class Data
    {
        public int AttackValue;
        public bool IsLose;
    }

    public class PlayerData : Data
    {
        
    }

    public class EnemyData : Data
    {
        
    }
}

参考にしたサイト

piffett.hateblo.jp

qiita.com

OSS(SaveGameFree)を使ってUnityでセーブ・ロード機能を実装する【Unity,SaveGameFree】

今回は,ゲーム制作で無料ものでセーブ・ロード機能を実装してほしいと要望があったので実装してみました

使用したライブラリ

github.com

有料版もありましたので,気になる方は確認してみてください

assetstore.unity.com

ライブラリの機能(GithubPageより)

The below features made Save Game Free excellent:

セーブ・ロード機能の実装

以下のスクリプトでは,以下の機能を実装しています

  • カスタムクラスのデータをデーブ
  • パスワード付きデータで保存
  • デーブデータの暗号化
using _Project.Scripts.Interface;
using BayatGames.SaveGameFree;

namespace _Project.Scripts.Model
{
    public class SaveHandler : ISaveData
    {
        public void Save(SaveDataClass saveDataClass)
        {
            SaveGame.Encode = true;
            SaveGame.Save("SaveData",saveDataClass,"pass");
        }

        public SaveDataClass Load()
        {
            return SaveGame.Load<SaveDataClass>("SaveData",true,"pass");
        }
    }
}

備考

セーブデータの保存場所

デフォルトでは,Application.persistentDataPath に保存されるみたいです

各種プラットフォームのパスの詳細

qiita.com

データの暗号化

データの暗号化

暗号化は SaveGame.Encode = true; で有無を変更できるみたいです

暗号化されているのかの確認

まず,データが保存されていくところまで行きます f:id:ayousanz:20210531152440p:plain

メモ帳で開いてみます f:id:ayousanz:20210531152524p:plain

とりあえずソフトとかを使わない限りは見えてないと思います

パスワード付きデータ

セーブ時

パスワードは,SaveGame.Save("SaveData",saveDataClass,"pass"); という感じで引数にいれれば指定できます

ロード時

セーブデータをロードするときは,SaveGame.Load<SaveDataClass>("SaveData",true,"pass"); という感じで自分で設定したパスワードを入れます

パスワードが間違っている場合は以下のようにロードできないみたいです.

パスワードが違うときに表示されるエラー文

CryptographicException: Padding is invalid and cannot be removed.

f:id:ayousanz:20210531153005p:plain