2023-03-15に更新

Unity練習 セーブデータを作ろうぜ(^~^)?

読了目安:13分

セーブデータの作り方調べ

📖 Unityでセーブデータを作る方法まとめ

ramen-tabero-futsu2.png
「 👆 Unity でセーブデータを作る方法を調べようぜ?」

kifuwarabe-futsu.png
「 JSONファイルでいいんじゃないか?」

ohkina-hiyoko-futsu2.png
「 Unity にファイルチューザーが あんの?」

📖 JsonUtility
📖 EditorJsonUtility.ToJson

ramen-tabero-futsu2.png
「 👆 ユーティリティーが2種類あるらしい。使ってみようぜ?」

ramen-tabero-futsu2.png
「 サンプルが無くて 分からんな」

📖 【Unity】JsonUtilityを触ってみた

ramen-tabero-futsu2.png
「 👆 セーブファイルを 100個ぐらい分けたいとき どうするんだぜ?」

kifuwarabe-futsu.png
「 1つのJSONファイルに すべて詰め込むんじゃないか?」

ramen-tabero-futsu2.png
「 容量は 何バイトまで 保存できるんだぜ?」

ohkina-hiyoko-futsu2.png
「 移植先にもよるんじゃないの?」

ramen-tabero-futsu2.png
「 Webブラウザーの上で実行するアプリだったら、どこへ保存される?」

kifuwarabe-futsu.png
「 それも含めて調査しろだぜ」

📖 永続データ ― ゲームの状態と設定を保存する方法

ramen-tabero-futsu2.png
「 👆 保存先の設定をプログラムしないといけないらしいぜ」

📖 UnityTechnologies/UniteNow20-Persistent-Data

ramen-tabero-futsu2.png
「 👆 サンプル・プログラムは これらしいぜ」

ramen-tabero-futsu2.png
「 ソース読んだが 使い方 分からん。
どうやって クロス・プラットフォーム のセーブデータを作れる?
Windows を想定して ファイルパス書いていいのか?」

📖 PlayerPrefsと同様な使い勝手で独自クラスもセーブできる機能実装【Unity】【セーブ】【Json】

ramen-tabero-futsu2.png
「 👆 これを真似てみるかだぜ」

ramen-tabero-futsu2.png
「 なんだこのクラスは。真似たくない書き方だな」

セーブボタンを置こう

202303_unity_11-2200--saveDataManager-1.png

ramen-tabero-futsu2.png
「 👆 とりあえず マネージャー・クラスを作る 鉄板のやり方で サンプルを書き直そう」

202303_unity_11-2211--onSave-1.png

ramen-tabero-futsu2.png
「 👆 OnSave という Publicメソッドを作って」

202303_unity_11-2211--saveButton-1.png

ramen-tabero-futsu2.png
「 👆 Save ボタンをクリックしたら、 SaveDataManager クラスの OnSave メソッドを呼び出すところまで
作るのは 鉄板だぜ」

マウスで動かせるオブジェクトを置こう

📖 オブジェクトを掴んで動かす

ramen-tabero-futsu2.png
「 👆 上のリンク先の、他人の記事を読めだぜ。
マウスで動かせるオブジェクトを画面上に置こうぜ?」

📖 Unity マウスからRayを打つ ~可視化もするよ~

ramen-tabero-futsu2.png
「 👆 別の記事も読まないと コードがおかしいぜ」

202303_unity_11-2333--inputManager-1.png

202303_unity_11-2326--script.png

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class InputManager : MonoBehaviour
{
    /// <summary>
    /// レイを描く距離
    /// </summary>
    float DebugDrawRayDistance = 15.0f;

    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        // マウス右ボタン 押しっぱなし時
        if (Input.GetMouseButton(0))
        {
            // レイ(見えない光線のようなもの)を飛ばす
            var ray = Camera.main.ScreenPointToRay(Input.mousePosition);

            if (Physics.Raycast(ray, out var hit))
            {
                // レイが当たったオブジェクト
                string goName = hit.collider.gameObject.name;
                Debug.Log("右クリック:" + goName);

                // そのオブジェクトの中心位置を、クリックした場所へ移動(カクカクした動き)
                GameObject.Find(goName).transform.position = new Vector3(hit.point.x, hit.point.y, 0);
            }
            else
            {
                Debug.Log("右クリック 外れ");
            }

            // レイを描く
            Debug.DrawRay(ray.origin, ray.direction * DebugDrawRayDistance, Color.green, 5, false);
        }
    }
}

ramen-tabero-futsu2.png
「 👆 カクカクしてるが、とりあえず これで 箱は動くぜ」

kifuwarabe-futsu.png
「 Update() じゃなくて、 LateUpdate() にすりゃいいんじゃないかだぜ?」

ramen-tabero-futsu2.png
「 うまくやってくれだぜ」

まず、 PlayerPrefs を練習しようぜ?

202303_unity_12-0002--loadButton-1.png

ramen-tabero-futsu2.png
「 👆 セーブボタンだけあっても練習できない。ロードボタンも追加するぜ」

SaveDataManager.cs :

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SaveDataManager : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {

    }

    // - イベントハンドラ

    public void OnSave()
    {
        Debug.Log("セーブします");

        var goCube = GameObject.Find("Cube");
        PlayerPrefs.SetFloat("x", goCube.transform.position.x);
        PlayerPrefs.SetFloat("y", goCube.transform.position.y);
        PlayerPrefs.SetFloat("z", goCube.transform.position.z);
    }

    public void OnLoad()
    {
        Debug.Log("ロードします");

        var goCube = GameObject.Find("Cube");
        var x = PlayerPrefs.GetFloat("x");
        var y = PlayerPrefs.GetFloat("y");
        var z = PlayerPrefs.GetFloat("z");
        goCube.transform.position = new Vector3(x, y, z);
    }
}

📺 開発中画面

📅 2023-03-12 sun 00:29

ramen-tabero-futsu2.png
「 👆 これで 思ってるように セーブとロードは でけた」

ramen-tabero-futsu2.png
「 バカでかい容量のセーブをしたいとき こんなシンプルな構造で 対応しきれるのかだぜ?
クラスを シリアライズ/デシリアライズ して 投げ込めないの?」

kifuwarabe-futsu.png
「 String 型のセッターに JSON を投げ込めだぜ」

ramen-tabero-futsu2.png
「 そんなこと していいのかな……」

まず、セーブ・スロットを3つにしようぜ?

ramen-tabero-futsu2.png
「 セーブ・ファイルって 複数個 持ちたいよな」

ohkina-hiyoko-futsu2.png
「 自分用、弟用、クリアー・データ用よね」

📖 【Unity uGUI】ドロップダウン(Dropdown)を使用してオプションを選択する方法

ramen-tabero-futsu2.png
「 👆 とりあえず ドロップダウン の作り方を調べるか」

202303_unity_12-1235--dropdown-1.png

ramen-tabero-futsu2.png
「 👆 さて、何を どうすればいいのか?」

kifuwarabe-futsu.png
「 UI は、スクリプトから 操作したいよな」

📖 【Unity】ドロップダウンの表示テキストを変える

ramen-tabero-futsu2.png
「 👆 何かをやろうとしても、はて、と分からんことばっかりで つらい。調べる」

202303_unity_12-1513--saveSlot.png

📅 2023-03-12 sun 15:14

SaveDataManager.cs :

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;

public class SaveDataManager : MonoBehaviour
{
    TMP_Dropdown dropdown;

    // Start is called before the first frame update
    void Start()
    {
        dropdown = GameObject.Find("Save Slot Dropdown").GetComponent<TMP_Dropdown>();

        // ドロップダウンリストのラベル
        dropdown.options[0].text = "Save Data 1";
        dropdown.options[1].text = "Save Data 2";
        dropdown.options[2].text = "Save Data 3";

        // コンボボックスのラベル
        //
        // - 最初に選択されている項目に合わせないと挙動がおかしくなる
        dropdown.captionText.text = dropdown.options[0].text;
    }

    // Update is called once per frame
    void Update()
    {

    }

    // - フィールド

    string[] gameObjectNamesToSave = new string[]
    {
        "Red Cube",
        "Green Cube",
        "Blue Cube",
    };

    // - イベントハンドラ

    public void OnSave()
    {
        Debug.Log($"{(dropdown.value)}+1番へ、セーブしたい");

        foreach (var gameObjectName in gameObjectNamesToSave)
        {
            var gameObject = GameObject.Find(gameObjectName);
            // ゲームオブジェクトの名前には、いろんな文字が使えるので、区切りなるような文字がない。とりあえずドット区切りにする
            var nameSpace = $"Slot{dropdown.value}.{gameObjectName}";

            PlayerPrefs.SetFloat($"{nameSpace}.x", gameObject.transform.position.x);
            PlayerPrefs.SetFloat($"{nameSpace}.y", gameObject.transform.position.y);
            PlayerPrefs.SetFloat($"{nameSpace}.z", gameObject.transform.position.z);
        }
    }

    public void OnLoad()
    {
        Debug.Log($"{(dropdown.value)}+1番から、ロードしたい");

        foreach (var gameObjectName in gameObjectNamesToSave)
        {
            var gameObject = GameObject.Find(gameObjectName);
            // ゲームオブジェクトの名前には、いろんな文字が使えるので、区切りなるような文字がない。とりあえずドット区切りにする
            var nameSpace = $"Slot{dropdown.value}.{gameObjectName}";

            gameObject.transform.position = new Vector3(
                PlayerPrefs.GetFloat($"{nameSpace}.x"),
                PlayerPrefs.GetFloat($"{nameSpace}.y"),
                PlayerPrefs.GetFloat($"{nameSpace}.z"));
        }
    }
}

ramen-tabero-futsu2.png
「 👆 よし、ドロップ・ダウン・リストの使い方を覚えたぜ」

kifuwarabe-futsu.png
「 やったぜ!」

ohkina-hiyoko-futsu2.png
「 やったわね!」

📅 2023-03-15 wed 19:18

ramen-tabero-futsu2.png
「 次は、XML 形式で保存できるように改造しようぜ」

📖 【Unity】JsonUtilityを使ったJson化

ramen-tabero-futsu2.png
「 👆 分からんことがいっぱいなので調べるぜ」

ramen-tabero-futsu2.png
「 よし、簡単なとこだけ つかんだので そこだけやろ」

202303_unity_15-2009--tree.png

ramen-tabero-futsu2.png
「 👆 階層構造はこうなるぜ。
関係あるところを見ていこう」

202303_unity_15-2012--gameObject.png

Assets.Scripts.Models.SaveData.GameObject.cs:

namespace Assets.Scripts.Models.SaveData
{
    using UnityEngine;

    /// <summary>
    /// UnityのGameObjectをラッピング
    /// </summary>
    [System.Serializable]
    public class GameObject
    {
        // -^ その他

        /// <summary>
        /// 生成
        /// </summary>
        /// <param name="gameObject"></param>
        /// <returns></returns>
        internal static GameObject FromGameObject(UnityEngine.GameObject gameObject)
        {
            return new GameObject
            {
                name = gameObject.name,
                x = gameObject.transform.position.x,
                y = gameObject.transform.position.y,
                z = gameObject.transform.position.z
            };
        }

        // - フィールド

        public string name;
        public float x;
        public float y;
        public float z;

        // - メソッド

        internal Vector3 ToPosition()
        {
            return new Vector3(this.x, this.y, this.z);
        }
    }
}

ramen-tabero-futsu2.png
「 👆 GameObject と同名で、保存したいところだけ作ったクラスを用意するぜ。
クラス名の上の行に [System.Serializable] が付いているのを忘れるなだぜ」

202303_unity_15-2015--init.png

Assets.Scripts.Models.SaveData.Init.cs:

namespace Assets.Scripts.Models.SaveData
{
    using System.Collections.Generic;
    using ModelOfSaveData = Assets.Scripts.Models.SaveData;

    /// <summary>
    /// セーブ・データ
    /// </summary>
    public class Init
    {
        // - その他

        internal Init()
        {
            this.gameObjects = new List<ModelOfSaveData.GameObject>();
        }

        // - フィールド

        // JsonUtilクラスは、Dictionary型には対応していない
        public List<ModelOfSaveData.GameObject> gameObjects;

        // - メソッド

        /// <summary>
        /// ゲームオブジェクトの追加
        /// </summary>
        /// <param name="item"></param>
        public void AddGameObject(ModelOfSaveData.GameObject item)
        {
            this.gameObjects.Add(item);
        }
    }
}

ramen-tabero-futsu2.png
「 👆 SaveData.Init クラスが、セーブデータ・クラス階層構造のトップ・レベルのクラスだぜ。
セーブスロット1個分のデータが全部入ってると思えだぜ」

202303_unity_15-2032--saveDataManager.png

Assets.Scripts.SaveDataManager.cs:

using TMPro;
using UnityEngine;
using ModelOfSaveData = Assets.Scripts.Models.SaveData;

public class SaveDataManager : MonoBehaviour
{
    TMP_Dropdown dropdown;

    // Start is called before the first frame update
    void Start()
    {
        dropdown = GameObject.Find("Save Slot Dropdown").GetComponent<TMP_Dropdown>();

        // ドロップダウンリストのラベル
        dropdown.options[0].text = "Save Data 1";
        dropdown.options[1].text = "Save Data 2";
        dropdown.options[2].text = "Save Data 3";

        // コンボボックスのラベル
        //
        // - 最初に選択されている項目に合わせないと挙動がおかしくなる
        dropdown.captionText.text = dropdown.options[0].text;
    }

    // Update is called once per frame
    void Update()
    {

    }

    // - フィールド

    string[] gameObjectNamesToSave = new string[]
    {
        "Red Cube",
        "Green Cube",
        "Blue Cube",
    };

    // - イベントハンドラ

    public void OnSave()
    {
        // セーブデータ・モデルの作成
        var saveDataModel = new ModelOfSaveData.Init();

        // 記憶したいゲームオブジェクトを格納
        foreach (var gameObjectName in gameObjectNamesToSave)
        {
            var gameObject = GameObject.Find(gameObjectName);
            var gameObject2 = ModelOfSaveData.GameObject.FromGameObject(gameObject);
            saveDataModel.AddGameObject(gameObject2);
        }

        // JSON文字列化(シリアライズ)
        var jsonText = JsonUtility.ToJson(saveDataModel);

        // 保存
        Debug.Log($"{(dropdown.value)}+1番へ、セーブしたい。 Json:{jsonText}");
        PlayerPrefs.SetString($"Slot{dropdown.value}", jsonText);
    }

    public void OnLoad()
    {
        // 読取(JSONテキスト取得)
        var jsonText = PlayerPrefs.GetString($"Slot{dropdown.value}");
        Debug.Log($"{(dropdown.value)}+1番から、ロードしたい。 Json:{jsonText}");

        // セーブデータ・モデルへ復元
        var saveDataModel2 = JsonUtility.FromJson<ModelOfSaveData.Init>(jsonText);
        if (saveDataModel2==null)
        {
            // 復元できません
            Debug.Log($"{(dropdown.value)}+1番 ロードできませんでした。 Json:{jsonText}");
            return;
        }

        // 記憶されているゲームオブジェクトを取出し
        foreach (var gameObject2 in saveDataModel2.gameObjects)
        {
            var gameObject = GameObject.Find(gameObject2.name);
            gameObject.transform.position = gameObject2.ToPosition();
        }
    }
}

ramen-tabero-futsu2.png
「 👆 セーブデータを読み書きするクラスだぜ」

kifuwarabe-futsu.png
「 これで ゲームをセーブできるな」

WebGL

ohkina-hiyoko-futsu2.png
「 WebGL 形式で出力して、ブラウザで動かしても セーブ/ロード できんの?」

202303_unity_15-2158--webGL.png

ramen-tabero-futsu2.png
「 👆 よっしゃ、実験してみるかだぜ」

202303_unity_15-2201--saveLoad.png

ramen-tabero-futsu2.png
「 👆 できたり、できなかったりするな、ちょっと 調べるか」

ramen-tabero-futsu2.png
「 気になったのは、 デバッグ・ログ そのまんま出てくるのか 整えないと カッコ悪いな」

202303_unity_15-2235--test.png

📅 2023-03-15 wed 22:38

ramen-tabero-futsu2.png
「 👆 よし、動いていることもあるし 完成とするかだぜ」

kifuwarabe-futsu.png
「 わらう」

ohkina-hiyoko-futsu2.png
「 Unity のサイトにアップロードしましょう!」

Unity Play

📖 Unity Play

ramen-tabero-futsu2.png
「 👆 アップロード後に ゲーム画面上の×ボタンを押して ブラウザの戻るボタンを押したら
画面が真っ白になった!」

kifuwarabe-futsu.png
「 一度 間違った操作を行うと 続けて 間違った操作を行ってしまう。
わらう」

ramen-tabero-futsu2.png
「 何もできん」

ohkina-hiyoko-futsu2.png
「 今日は終わりかしらねえ」

📺 Save Data Practice

📅 2023-03-15 wed 23:41

ramen-tabero-futsu2.png
「 👆 1時間放置したら アクセスできるようになった。アップロードした。
もう終わり!」

<おわり>

ツイッターでシェア
みんなに共有、忘れないようにメモ

view_list [連載] Unity練習
第1回 Unity練習 セーブデータを作ろうぜ(^~^)?

むずでょ

光速のアカウント凍結されちゃったんで……。ゲームプログラムを独習中なんだぜ☆電王戦IIに出た棋士もコンピューターもみんな好きだぜ☆▲(パソコン将棋)WCSC29一次予選36位、SDT5予選42位▲(パソコン囲碁)AI竜星戦予選16位

Crieitは誰でも投稿できるサービスです。 是非記事の投稿をお願いします。どんな軽い内容でも投稿できます。

また、「こんな記事が読みたいけど見つからない!」という方は是非記事投稿リクエストボードへ!

有料記事を販売できるようになりました!

こじんまりと作業ログやメモ、進捗を書き残しておきたい方はボード機能をご利用ください。
ボードとは?

コメント