在Unity中使用自定义Binary文件保存和载入游戏

Unity中保存和载入游戏

在Unity,保存游玩进度粗略有三种方式:

  • 使用Unity自带的PlayerPrefs
  • 使用自定义Class并转换成JSON或XML格式
  • 使用自定义Class并转换成Binary文件

其中PlayerPrefs适合记录少量的游戏记录,如玩家的High Scores,Player Name等。如果要保存更加复杂的游戏进度,则最好是自己设计一个SaveData类来保存所有需要保存的数据。而这个SaveData类需要被转换成游戏外的文件来保存,这时候就有两类文件格式可以选择。如果选择JSON或XML格式,优点是可读性强,但与此同时也会导致玩家可以非常轻易的修改游戏记录。为了防止这一点,需要将SaveData类转换成更加难以修改的文件格式,如Binary格式。

举例用的三个类

  • SaveData:想要存储的数据都放在这儿。
  • SaveSystem: 负责实际写入写出文件。
  • PlayerTest: 随便写的一个调用SaveSystem执行保存和载入操作的测试类。

设计自己的SaveData类

首先,需要设计一个SaveData类,里面的属性是所有你所需要保存的数据,例如:

[System.Serializable]
public class SaveRecord
{
    public int PlayerHealth;
    
    public float[] PlayerPosition;

    public float PlayerSpeed;
	
	# ...加入其他需要的属性    

    public SaveRecord(int playerHealth, float[] playerPosition, float playerSpeed)
    {
        PlayerHealth = playerHealth;
        PlayerSpeed = playerSpeed;
        PlayerPosition = playerPosition;
    }
}

注意

  1. 不要继承MonoBehavior,这个类只是单纯记录数据用的!
  2. 一定要加[System.Serializable]

转化不可序列化的数据

在这个例子中,PlayerPositionVector3数据类型的,而这个数据类型在Unity中不可以直接序列化。因此,我们需要设计某种方式来将其转化成可序列化的数据类型,如float[]。无论怎么做,转化这一步都是必要的,只是看你想要在哪一层去做。这里把他写成一个helper函数,把放在了PlayerTest类里面。

	public float[] SetPlayerPosition (Vector3 position)
    {
        float[] PlayerPosition = new float[3];
        PlayerPosition[0] = position.x;
        PlayerPosition[1] = position.y;
        PlayerPosition[2] = position.z;
        return PlayerPosition;
    }

    public Vector3 GetPlayerPosition (float[] PlayerPosition)
    {
        if (PlayerPosition == null)
        {
            Debug.LogError("Player Position is null");
            return Vector3.zero;
        }
        return new Vector3(PlayerPosition[0], PlayerPosition[1], PlayerPosition[2]);
    }

这里只是举例了Vector3的转化,还有很多数据类型都是不可序列化的,也都需要执行转化的步骤,可以自己去写高度解耦的多个helper函数来做他(现在写的项目比较简单所以我就不弄复杂了)。
或者可以去买UnityAssetStore里的Easy Save直接一买解千愁,如果项目复杂性高且有这方面需求建议直接买他。

SaveSystem - 设计管理保存与加载的类

SaveSystem是最关键的类,主要负责写入与读取存档文件,之前说的二进制转化也在这一层实现。这里的实现比较简单,没有考虑到多个存档的情况,路径也是唯一的,所以如果有需求肯定需要修改一下这个地方。

using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using UnityEngine;

public static class SaveSystem
{
    public static void SaveGame (int playerHealth, float[] playerPosition, float playerSpeed)
    {
        string path = Path.Combine(Application.persistentDataPath, "/mySaveFile.ngm");
        SaveRecord record = new SaveRecord(playerHealth, playerPosition , playerSpeed);
        using (FileStream stream = new FileStream(path, FileMode.Create))
        {
            BinaryFormatter formatter = new BinaryFormatter();
            formatter.Serialize(stream, record);
        }
    }

    public static SaveRecord LoadGame ()
    {
        string path = Path.Combine(Application.persistentDataPath, "/mySaveFile.ngm");
        if (File.Exists(path))
        {
            using (FileStream stream = new FileStream(path, FileMode.Open))
            {
                BinaryFormatter formatter = new BinaryFormatter();
                SaveRecord record = formatter.Deserialize(stream) as SaveRecord;
                return record;
            }
        }
        else
        {
            Debug.LogError("No Save File Found in path");
            return null;
        }
    }
}

注意

  1. 使用using语句来处理stream
  2. 使用Path.Combine来把路径合起来,而不要直接使用+

PlayerTest - 检测程序

其实到这里已经完成了一个基本的保存和载入的体系,设计这个PlayerTest类仅仅是为了测试这个体系的正确性。面对不同的需求会有各种不同的调用情况,这里展示的是非常基本的调用情况。

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

public class PlayerTest : MonoBehaviour
{
    public int playerHealth = 100;

    public float playerSpeed = 100f;

    public void SavePlayer ()
    {
        SaveSystem.SaveGame(playerHealth, SetPlayerPosition(transform.position), playerSpeed);
    }

    public void LoadPlayer ()
    {
        SaveRecord record = SaveSystem.LoadGame();
        playerHealth = record.PlayerHealth;
        playerSpeed = record.PlayerSpeed;
        gameObject.transform.position = GetPlayerPosition(record.PlayerPosition);
    }

    public float[] SetPlayerPosition (Vector3 position)
    {
        float[] PlayerPosition = new float[3];
        PlayerPosition[0] = position.x;
        PlayerPosition[1] = position.y;
        PlayerPosition[2] = position.z;
        return PlayerPosition;
    }

    public Vector3 GetPlayerPosition (float[] PlayerPosition)
    {
        if (PlayerPosition == null)
        {
            Debug.LogError("Player Position is null");
            return Vector3.zero;
        }
        return new Vector3(PlayerPosition[0], PlayerPosition[1], PlayerPosition[2]);
    }
}

这个类会调用SaveSystem执行保存和载入操作,现在只需要在游戏程序中调用LoadPlayerSavePlayer就好。我们让PlayerPosition与挂载了PlayerTest类的物体的position关联起来,这样在测试中就能直观的感受到有没有正确保存信息。

Unity场景

现在需要:

  • 一个挂载PlayerTest的任意gameObject(最好是能看出来在动的),可以用Cube物体。
  • 两个Button,分别负责调用这个PlayerTest物体的SaveGameLoadGame

完成的例子如下,这个是初始状态:

初始状态
在这里保存:

保存
随便移动一下:

随便移动了一下
再点击载入会回到保存好的位置:

在这里插入图片描述

其他

后续应该考虑一下怎么做多个存档的保存与载入。

为什么在C#中,使用using语句处理Stream类是一种更好的做法?

  • 简化资源管理:Stream类代表了一个字节流,可能会占用系统资源,如文件、网络连接等。在不再需要使用Stream实例时,必须确保它被正确释放和关闭,以避免资源泄漏。using语句能够在代码块结束时自动释放资源,无论是通过正常流程执行还是遇到异常情况,都能确保资源的正确释放。这样可以减少手动处理资源释放的代码量,提高代码的可读性和可维护性。

  • 实现Dispose模式:Stream类实现了IDisposable接口,该接口定义了Dispose方法,用于显式释放资源。通过使用using语句,可以自动调用Dispose方法,而不需要手动编写代码来调用该方法。这样可以避免因忘记或错误地调用Dispose方法而导致资源泄漏。

  • 嵌套使用:using语句支持嵌套使用,可以在一个using语句块内创建和处理多个Stream实例。在这种情况下,当最内层的using语句块结束时,会按照嵌套的顺序逐个释放和关闭Stream实例,确保资源的正确释放。

为什么在C#中,使用Path.Combine方法来合并而不使用+操作符连接路径字符串?

  • 跨平台兼容性:Path.Combine方法能够在不同操作系统上正确地合并路径,包括Windows、Linux和Mac等。不同操作系统使用不同的路径分隔符,例如Windows使用反斜杠\,而Linux和Mac使用正斜杠/。Path.Combine方法会根据当前操作系统自动选择正确的路径分隔符,确保生成的路径在不同平台上都能够正常工作。

  • 处理多个路径片段:Path.Combine方法可以接受多个路径片段作为参数,而不仅仅是两个路径。这样可以更方便地合并多个路径,并避免使用多个+操作符连接字符串时出现拼接错误或遗漏路径分隔符的问题。

  • 处理相对路径和绝对路径:Path.Combine方法能够正确处理相对路径和绝对路径的合并。它会根据传入的路径片段自动判断并生成正确的合并路径,无论是绝对路径还是相对路径。

  • 自动处理路径分隔符:Path.Combine方法会自动处理路径分隔符的问题,确保生成的路径中只有一个路径分隔符,避免出现多个路径分隔符连续出现的情况。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值