Catlike学习笔记(2.1)-持久化GameObject

终于放假回国了~然而热到爆炸几乎不能出门,充了B站大会员在家看了花了两天补完「博人传 火影忍者新时代」和「齐木楠雄的灾难」第二季,今天终于要开始努力学习了嗯。。。那么今天来到了「Catlike教程」的第二部分——对象管理,那么这是一系列与创建,追踪,保存和加载对象的教程。今天就不多说了快速进入正题~

PART 1 概述

既然做要保存和加载 GameObject 那么我们肯定要先做生成的功能,生成完以后保存下来,然后清除掉,再把之前保存的内容加载出来这样~所以我们的任务目标是以下这些:

  • 一键生成随机方块并一键清除
  • 保存 GameObject 的状态写入文件
  • 加载已经保存的数据重新生成 GameObject
  • 重构一下将保存与加载抽象成独立模块

PART 2 完成游戏逻辑

我们的需求非常简单,大概就是按下一个按键就生成一个小方块在随机位置旋转和缩放,然后按下某个按键就可以清空场景内所有方块。那么首先我们随便创建一个方块的 Prefab,然后创建脚本名为PersistentDemo然后添加如下代码。

public class PersistentDemo : MonoBehaviour
{
    public Transform Prefab;
    public KeyCode CreateKey = KeyCode.C;

    private List<Transform> _objectList;

    private void Awake()
    {
        _objectList = new List<Transform>();
    }

    private void Update()
    {
        if (Input.GetKeyDown(CreateKey))
        {
            CreateObject();
        }
    }

    private void CreateObject()
    {
        Transform t = Instantiate(Prefab);
        t.localPosition = Random.insideUnitSphere * 5f;
        t.localRotation = Random.rotation;
        t.localScale = Vector3.one * Random.Range(0.1f, 1f);
        _objectList.Add(t);
    }
}

代码特别简单大概就是在Update()中检测按键,然后在随机位置生成一个随机旋转和大小的方块,然后加到ObjectList中。写好以后我们在场景中创建一个空 GameObject 取名叫Game然后挂上该脚本,再把之前制作好的 Cube Prefab 拖到脚本中。运行一下看看~

picture

嗯效果不错~然后我们需要设置一个快捷键可以一键清除所有方块以便重新开始生成。那么继续添加如下代码。大概就是检测到玩家按下按键后就遍历ObjectList中的所有 GameObject 并 Destroy,最后清空ObjectList

using System.Collections.Generic;
using UnityEngine;

public class PersistentDemo : MonoBehaviour
{
    ...
    public KeyCode NewGameKey = KeyCode.N;
    ...
    
    private void Update()
    {
        ...
        else if (Input.GetKey(NewGameKey))
        {
            BeginNewGame();
        }
    }

    ...

    private void BeginNewGame()
    {
        for (int i = 0; i < ObjectList.Count; i++)
        {
            Destroy(ObjectList[i].gameObject);
        }

        ObjectList.Clear();
    }
}

运行效果就不截图了总之就是所有的小方块都消失了~那么我们现在就完成了第一步。

PART 3 保存和读取

保存和读取的思路非常简单,我们在这里就不用 PlayerPref 之类的东西,而是采取更简单易懂的直接在 PersistentData 目录中创建一个文件把 GameObject 的信息写在里面就好了。那么事实上我们需要保存的数据就只有方块的数量以及每个方块其各自的位置,旋转和大小。那么我们尝试在PersistentDemo中添加如下代码。

public class PresistentDemo : MonoBehaviour
{
    ...
    public KeyCode SaveKey = KeyCode.S;

    private List<Transform> _objectList;
    private string _savePath;

    private void Awake()
    {
        _objectList = new List<Transform>();
        _savePath = Path.Combine(Application.persistentDataPath, "saveFile");
    }

    private void Update()
    {
        ...
        else if (Input.GetKeyDown(SaveKey))
        {
            Save();
        }
    }

    ...

    private void Save()
    {
        using (var writer = new BinaryWriter(File.Open(_savePath, FileMode.Create)))
        {
            writer.Write(_objectList.Count);
            for (int i = 0; i < _objectList.Count; i++)
            {
                Transform t = _objectList[i];
                writer.Write(t.localPosition.x);
                writer.Write(t.localPosition.y);
                writer.Write(t.localPosition.z);
            }
        }
    }
}

代码内容也非常简单,大概就是检测按键后在预设好的路径中创建文件,然后写入当前创建的方块的数量,并依次写入每个方块的Position。这样一来读取的代码也呼之欲出了,大概就是从预设路径的文件中取出方块的数量最后按照相应的位置信息生成 GameObject

public class PresistentDemo : MonoBehaviour
{
    ...
    public KeyCode LoadKey = KeyCode.L;
    ...

    private void Update()
    {
        ...
        else if (Input.GetKeyDown(LoadKey)) {
            Load();
        }
    }

    ...
    private void Load()
    {
        BeginNewGame();
        using (var reader = new BinaryReader(File.Open(_savePath, FileMode.Open)))
        {
            int count = reader.ReadInt32();
            for (int i = 0; i < count; i++)
            {
                Vector3 p;
                p.x = reader.ReadSingle();
                p.y = reader.ReadSingle();
                p.z = reader.ReadSingle();
                Transform t = Instantiate(Prefab);
                t.localPosition = p;
                _objectList.Add(t);

            }
        }
    }
}

运行一下看看效果~在这里博主特意把按键指令也显示在屏幕左下角方便大家看清楚发生了什么~

picture

那么大家会注意到,最后按下l的时候,所有的小方块的旋转和缩放信息都不见了,只剩下位置还是正确的,那么讲道理我们可以继续修改代码把旋转和缩放也写到文件里面,不过这样代码会变得异常丑陋,我们稍微重构一下代码再把旋转和缩放补全吧。

PART 4 抽象与重构

那么该如何抽象呢,大概的思路就是先创建一个WriterReader可以让我们方便的从Binary中读出Vector3Quaternion,然后创建PersistableObject挂载在我们的 Prefab 上,可以使外部方便的调用Save()Load()接口就可以把 Prefab 中的所有重要信息,如 Position Rotation Scale 等保存或读取出来。最后我们从PresistentDemo把保存和读取相关的代码提取出来单独作为一个PersistentStorage类,由PresistentDemo调用。那么方案确定下来以后就开始实施~

Writer 和 Reader

Writer 和 Reader 的作用就是允许我们方便的调用一个接口就可以把相应比较复杂的数据结构写入到 Binary 中或从中读取,从而避免大量的重复的类似writer.Write(t.localPosition.x)这样的代码。这两部分代码非常相似而且很简单,就不多解释了随便贴一下。。。。

public class GameDataReader
{
    private BinaryReader _reader;

    public GameDataReader(BinaryReader reader)
    {
        _reader = reader;
    }

    public float ReadFloat()
    {
        return _reader.ReadSingle();
    }

    public int ReadInt()
    {
        return _reader.ReadInt32();
    }

    public Quaternion ReadQuaternion()
    {
        Quaternion value;
        value.x = _reader.ReadSingle();
        value.y = _reader.ReadSingle();
        value.z = _reader.ReadSingle();
        value.w = _reader.ReadSingle();
        return value;
    }

    public Vector3 ReadVector3()
    {
        Vector3 value;
        value.x = _reader.ReadSingle();
        value.y = _reader.ReadSingle();
        value.z = _reader.ReadSingle();
        return value;
    }
}
public class GameDataWriter
{
    private BinaryWriter _writer;

    public GameDataWriter(BinaryWriter writer)
    {
        _writer = writer;
    }

    public void Write(float value)
    {
        _writer.Write(value);
    }

    public void Write(int value)
    {
        _writer.Write(value);
    }

    public void Write(Quaternion value)
    {
        _writer.Write(value.x);
        _writer.Write(value.y);
        _writer.Write(value.z);
        _writer.Write(value.w);
    }

    public void Write(Vector3 value)
    {
        _writer.Write(value.x);
        _writer.Write(value.y);
        _writer.Write(value.z);
    }
}

Persistable Object

接下来PersistableObject的作用是挂载在 Perfab 上从而使得外部可以简单的通过Save()Load()接口来将一个对象的所有数据一次性的保存或读取出来。后续如果我们不同种类的游戏对象需要保存和读取的数据更复杂的话就可以继承这个类并重写相关接口来实现而无需改动外部调用代码,不过这都是后话了,目前我们需要保存的就是localPositionlocalRotationlocalScale这样。所以代码如下

[DisallowMultipleComponent]
public class PersistableObject : MonoBehaviour
{
    public virtual void Save(GameDataWriter writer)
    {
        writer.Write(transform.localPosition);
        writer.Write(transform.localRotation);
        writer.Write(transform.localScale);
    }

    public virtual void Load(GameDataReader reader)
    {
        transform.localPosition = reader.ReadVector3();
        transform.localRotation = reader.ReadQuaternion();
        transform.localScale = reader.ReadVector3();
    }
}

是不是这样组织代码比在 PresistentDemo中实现所有功能要清晰很多呢,而且很容易扩展和修改~最后不要忘记挂在我们的 Prefab 上面。

Persistent Storage

最后PersistentStorage存在的意义是将文件操作相关代码从主逻辑中剥离出来,并没有很多内容。。。

public class PersistentStorage : MonoBehaviour
{
    private string _savePath;

    void Awake()
    {
        _savePath = Path.Combine(Application.persistentDataPath, "saveFile");
    }

    public void Save(PersistableObject o)
    {
        using (var writer = new BinaryWriter(File.Open(_savePath, FileMode.Create)))
        {
            o.Save(new GameDataWriter(writer));
        }
    }

    public void Load(PersistableObject o)
    {
        using (var reader = new BinaryReader(File.Open(_savePath, FileMode.Open)))
        {
            o.Load(new GameDataReader(reader));
        }
    }
}

将其挂在 Game 上后修改PresistentDemo,完整代码如下~注意我们 Override 的Save()Load()函数部分。以及按下 SaveKey 和 LoadKey 后调用的Storage.Save(this)Storage.Load(this)

public class PresistentDemo : PersistableObject
{
    public PersistableObject Prefab;

    public KeyCode CreateKey = KeyCode.C;
    public KeyCode NewGameKey = KeyCode.N;
    public KeyCode SaveKey = KeyCode.S;
    public KeyCode LoadKey = KeyCode.L;

    private List<PersistableObject> _objectList;

    public PersistentStorage Storage;

    private void Awake()
    {
        _objectList = new List<PersistableObject>();
    }

    private void Update()
    {
        if (Input.GetKeyDown(CreateKey))
        {
            CreateObject();
        }
        else if (Input.GetKey(NewGameKey))
        {
            BeginNewGame();
        }
        else if (Input.GetKeyDown(SaveKey))
        {
            Storage.Save(this);
        }
        else if (Input.GetKeyDown(LoadKey))
        {
            BeginNewGame();
            Storage.Load(this);
        }
    }

    private void CreateObject()
    {
        PersistableObject o = Instantiate(Prefab);
        var t = o.transform;
        t.localPosition = Random.insideUnitSphere * 5f;
        t.localRotation = Random.rotation;
        t.localScale = Vector3.one * Random.Range(0.1f, 1f);
        _objectList.Add(o);
    }

    private void BeginNewGame()
    {
        for (int i = 0; i < _objectList.Count; i++)
        {
            Destroy(_objectList[i].gameObject);
        }
        _objectList.Clear();
    }

    public override void Save(GameDataWriter writer)
    {
        writer.Write(_objectList.Count);
        for (int i = 0; i < _objectList.Count; i++)
        {
            _objectList[i].Save(writer);
        }
    }

    public override void Load(GameDataReader reader)
    {
        int count = reader.ReadInt();
        for (int i = 0; i < count; i++)
        {
            PersistableObject o = Instantiate(Prefab);
            o.Load(reader);
            _objectList.Add(o);
        }
    }
}

最后把各种东西引用都拖好大概像这样~

picture

最后运行下看看~

[图片上传失败...(image-43fc33-1535930032120)]

PART 5 总结

持续一边划水玩手机一边吃零食一边写文章花了两天终于完成了~自己回顾下来感觉代码有点多讲的不够细,但是都是非常简单的代码呀相信各位同学可以轻轻松松搞定的~嗯明天就开始下一篇!哦对了差点忘记顺手贴上「Github项目地址」,懒得自己码代码的同学可以下载下来直接运行哦~


原文链接:https://snatix.com/2018/08/05/025-persisting-objects/

本文由 sNatic 发布于『大喵的新窝』 转载请保留本申明

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,744评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,505评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,105评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,242评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,269评论 6 389
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,215评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,096评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,939评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,354评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,573评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,745评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,448评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,048评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,683评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,838评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,776评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,652评论 2 354

推荐阅读更多精彩内容