换工作了,这下是纯C#开发了,偏单机游戏,所以又要研究一下C#的存档做法。经过一阵时间的解决各种问题,现在已经稳定,需要的老铁可以参考一下。
1.导入ProtoBuf
https://github.com/protocolbuffers/protobuf/releases/
下载需要的语言,解压后导入到自己的目录中。文章来源:https://www.toymoban.com/news/detail-447772.html
2.协议声明
[ProtoContract]
public class DataRoot
{
[ProtoMember(1)]
public int sA;
[ProtoMember(2)]
public string sB;
[ProtoMember(3)]
public float[] sC;
[ProtoMember(4)]
public DataA sD;
[ProtoMember(5)]
public List<DataA> sE;
}
[ProtoContract]
public class DataA
{
[ProtoMember(1)]
public int sA1;
[ProtoMember(2)]
public int sA2;
}
- ProtoContract 声明要序列化的类
- ProtoMember 声明要序列化的成员
3.存档
数据生成
DataRoot data = new DataRoot()
{
sA = 0,
sB = "AAA",
sC = new float[2] { 1, 2 },
sE = null,
};
data.sD = new DataA();
Debug.Log("保存存档完成");
封包与保存
//将内容进行封包
File.Delete(Application.persistentDataPath + "/Save.dat");
FileStream stream = File.OpenWrite(Application.persistentDataPath + "/Save.dat");
Serializer.Serialize(stream, data);
stream.Dispose();
- ProtoBuf.Serializer.Serialize protobuf自带的序列化方法 将数据转为byte[]
- protobuf的写入方式是将bytes.length覆盖写入到文件中,最后用ProtoReader.TO_EOF作为结束符添加到之后的一位。也就是说文件的bytes.length永远保持为最大的长度。在项目中,不知道为什么,存档的byte[]变短之后,这个结束符一直有问题,导致存档读取出错,所以我干脆在每次存档时将原存档文件删除。
4.读档
读取与解包
private void TryDeserialize(out DataRoot data)
{
if (File.Exists(Application.persistentDataPath + "/Save.dat"))
{
//↓↓↓这一部分比较特殊 根据需要判断是否要添加↓↓↓
Support.SetInstanceFunc((Type type) =>
{
if (Type.GetType(type.FullName) == null)
{
return null;
}
return Activator.CreateInstance(type
#if !(CF || SILVERLIGHT || WINRT || PORTABLE || NETSTANDARD1_3 || NETSTANDARD1_4)
, nonPublic: true
#endif
);
});
//↑↑↑这一部分比较特殊 根据需要判断是否要添加↑↑↑
//对内容进行解包
FileStream stream = File.OpenRead(Application.persistentDataPath + "/Save.dat");
data = Serializer.Deserialize<DataRoot>(stream);
stream.Dispose();
}
else
{
data = null;
}
//↓↓↓这一部分比较特殊 根据需要判断是否要添加↓↓↓
Support.SetInstanceFunc(null);
//↑↑↑这一部分比较特殊 根据需要判断是否要添加↑↑↑
}
- ProtoBuf.Serializer.Deserialize<T> protobuf自带的反序列化方法 将byte[]转为数据
private static Func<Type, object> instanceFunc;
public static void SetInstanceFunc(Func<Type, object> func)
{
instanceFunc = func;
}
public static object CreateInstance(Type type)
{
if (instanceFunc != null)
{
object obj = instanceFunc(type);
if (obj != null)
{
return obj;
}
}
// ReSharper disable once AssignNullToNotNullAttribute
if (Type.GetType(type.FullName) == null)
{
return _appDomain.Instantiate(type.FullName);
}
return Activator.CreateInstance(type
#if !(CF || SILVERLIGHT || WINRT || PORTABLE || NETSTANDARD1_3 || NETSTANDARD1_4)
, nonPublic: true
#endif
);
- 因为我们的项目涉及C#代码的热更新,所以存档管理器在热更新的程序集里,而protobuf因为其他内容的需要,是放在更深层的程序集里,而这个程序集是没有引用热更新的程序集的,所以在解包的时候,找不到热更新的程序集里的类。我只好在ProtoBuf.Support的类中添加了一个方法,在Support的CreateInstance方法中,把实例化的方法勾出来,解包的时候从热更新的程序集中实例化。
数据恢复
public void Load()
{
TryDeserialize(out DataRoot data);
if (data != null)
{
int _sA = data.sA;
string _sB = data.sB;
float[] _sC = data.sC;
DataA _sD = data.sD;
List<DataA> _sE = data.sE;
if (_sE != null)
{
//TODO
}
Debug.Log("加载存档完成");
}
else
{
//TODO 没有存档,可以执行如注册或新手引导之类的方法
}
}
- List和ArrayList,如果在存档时数据的长度为0,解包出来的数据是null,注意判断这个情况。
5.加密
AESCryptionUtility
这里提供一份使用AES加密解密的代码,支持string和byte[]的加密解密。加密解密时需要传入一个长度为32的字符串作为密钥。文章来源地址https://www.toymoban.com/news/detail-447772.html
/// <summary>
/// AES加解密字符串
/// </summary>
public static class AESCryptionUtility
{
/// <summary>
/// AES加密 String
/// </summary>
/// <param name="str">被加密的明文</param>
/// <param name="key">密钥</param>
/// <returns>密文</returns>
public static string Encrypt(string str, string key)
{
MemoryStream mStream = new MemoryStream();
RijndaelManaged aes = new RijndaelManaged();
byte[] plainbytes = Encoding.UTF8.GetBytes(str);
byte[] bKey = new byte[32];
Array.Copy(Encoding.UTF8.GetBytes(key.PadRight(bKey.Length)), bKey, bKey.Length);
aes.Mode = CipherMode.ECB;
aes.Padding = PaddingMode.PKCS7;
aes.KeySize = 128;
//aes.Key = _key;
aes.Key = bKey;
//aes.IV = _iV;
CryptoStream cryptoStream = new CryptoStream(mStream, aes.CreateEncryptor(), CryptoStreamMode.Write);
try
{
cryptoStream.Write(plainbytes, 0, plainbytes.Length);
cryptoStream.FlushFinalBlock();
return Convert.ToBase64String(mStream.ToArray());
}
finally
{
cryptoStream.Close();
mStream.Close();
aes.Clear();
}
}
/// <summary>
/// AES加密 Bytes
/// </summary>
/// <param name="bytes">被加密的明文bytes</param>
/// <param name="key">密钥</param>
/// <returns>密文</returns>
public static byte[] Encrypt(byte[] bytes, string key)
{
MemoryStream mStream = new MemoryStream();
RijndaelManaged aes = new RijndaelManaged();
byte[] bKey = new byte[32];
Array.Copy(Encoding.UTF8.GetBytes(key.PadRight(bKey.Length)), bKey, bKey.Length);
aes.Mode = CipherMode.ECB;
aes.Padding = PaddingMode.PKCS7;
aes.KeySize = 128;
//aes.Key = _key;
aes.Key = bKey;
//aes.IV = _iV;
CryptoStream cryptoStream = new CryptoStream(mStream, aes.CreateEncryptor(), CryptoStreamMode.Write);
try
{
cryptoStream.Write(bytes, 0, bytes.Length);
cryptoStream.FlushFinalBlock();
return mStream.ToArray();
}
finally
{
cryptoStream.Close();
mStream.Close();
aes.Clear();
}
}
/// <summary>
/// AES解密 String
/// </summary>
/// <param name="str">被加密的明文</param>
/// <param name="key">密钥</param>
/// <returns>明文</returns>
public static string Decrypt(string str, string key)
{
byte[] encryptedbytes = Convert.FromBase64String(str);
byte[] bKey = new byte[32];
Array.Copy(Encoding.UTF8.GetBytes(key.PadRight(bKey.Length)), bKey, bKey.Length);
MemoryStream mStream = new MemoryStream(encryptedbytes);
//mStream.Write( encryptedbytes, 0, encryptedbytes.Length );
//mStream.Seek( 0, SeekOrigin.Begin );
RijndaelManaged aes = new RijndaelManaged();
aes.Mode = CipherMode.ECB;
aes.Padding = PaddingMode.PKCS7;
aes.KeySize = 128;
aes.Key = bKey;
//aes.IV = _iV;
CryptoStream cryptoStream = new CryptoStream(mStream, aes.CreateDecryptor(), CryptoStreamMode.Read);
try
{
byte[] tmp = new byte[encryptedbytes.Length + 32];
int len = cryptoStream.Read(tmp, 0, encryptedbytes.Length + 32);
byte[] ret = new byte[len];
Array.Copy(tmp, 0, ret, 0, len);
return Encoding.UTF8.GetString(ret);
}
finally
{
cryptoStream.Close();
mStream.Close();
aes.Clear();
}
}
/// <summary>
/// AES解密 Bytes
/// </summary>
/// <param name="bytes">被加密的明文bytes</param>
/// <param name="key">密钥</param>
/// <returns>明文</returns>
public static byte[] Decrypt(byte[] bytes, string key)
{
byte[] bKey = new byte[32];
Array.Copy(Encoding.UTF8.GetBytes(key.PadRight(bKey.Length)), bKey, bKey.Length);
MemoryStream mStream = new MemoryStream(bytes);
//mStream.Write( encryptedbytes, 0, encryptedbytes.Length );
//mStream.Seek( 0, SeekOrigin.Begin );
RijndaelManaged aes = new RijndaelManaged();
aes.Mode = CipherMode.ECB;
aes.Padding = PaddingMode.PKCS7;
aes.KeySize = 128;
aes.Key = bKey;
//aes.IV = _iV;
CryptoStream cryptoStream = new CryptoStream(mStream, aes.CreateDecryptor(), CryptoStreamMode.Read);
try
{
byte[] tmp = new byte[bytes.Length + 32];
int len = cryptoStream.Read(tmp, 0, bytes.Length + 32);
byte[] ret = new byte[len];
Array.Copy(tmp, 0, ret, 0, len);
return ret;
}
finally
{
cryptoStream.Close();
mStream.Close();
aes.Clear();
}
}
}
相关参数
private readonly string cryptionKey = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
private bool isCrtption;
- cryptionKey AES密钥,长度为32位的随机字符
- isCrtption 是否加密,作为存档Debug使用
存档加密
//加密
if (isCrtption)
{
//将内容进行封包
FileStream stream = File.OpenWrite(Application.persistentDataPath + "/tempS");
Serializer.Serialize(stream, data);
stream.Dispose();
//加载文件Bytes
FileStream reader = new FileStream(Application.persistentDataPath + "/tempS", FileMode.Open, FileAccess.Read);
byte[] fileBytes = new byte[reader.Length];
reader.Read(fileBytes, 0, fileBytes.Length);
reader.Dispose();
//将内容加密
fileBytes = AESCryptionUtility.Encrypt(fileBytes, cryptionKey);
//保存存档
File.Delete(Application.persistentDataPath + "/Save.dat");
FileStream writer = new FileStream(Application.persistentDataPath + "/Save.dat", FileMode.OpenOrCreate, FileAccess.Write);
writer.Write(fileBytes, 0, fileBytes.Length);
writer.Dispose();
//清理
File.Delete(Application.persistentDataPath + "/tempS");
}
else
{
//将内容进行封包
File.Delete(Application.persistentDataPath + "/Save.dat");
FileStream stream = File.OpenWrite(Application.persistentDataPath + "/Save.dat");
Serializer.Serialize(stream, data);
stream.Dispose();
}
- 先将数据序列化并存到一个临时文件中,以防止对同一个文件进行操作时的问题。
- 读取临时文件中的byte[]
- 对内容进行加密
- 将加密之后的byte[]保存到真正的存档文件中
- 删除临时文件
存档解密
//解密
if (isCrtption)
{
//加载文件Bytes
FileStream reader = new FileStream(Application.persistentDataPath + "/Save.dat", FileMode.Open, FileAccess.Read);
byte[] fileBytes = new byte[reader.Length];
reader.Read(fileBytes, 0, fileBytes.Length);
reader.Dispose();
//将内容解密
fileBytes = AESCryptionUtility.Decrypt(fileBytes, cryptionKey);
//写道临时文件中
FileStream writer = new FileStream(Application.persistentDataPath + "/tempS", FileMode.OpenOrCreate, FileAccess.Write);
writer.Write(fileBytes, 0, fileBytes.Length);
writer.Dispose();
//对内容进行解包
FileStream stream = File.OpenRead(Application.persistentDataPath + "/tempS");
data = Serializer.Deserialize<DataRoot>(stream);
stream.Dispose();
//清理
File.Delete(Application.persistentDataPath + "/tempS");
}
else
{
//对内容进行解包
FileStream stream = File.OpenRead(Application.persistentDataPath + "/Save.dat");
data = Serializer.Deserialize<DataRoot>(stream);
stream.Dispose();
}
- 先读取已加密的byte[]
- 对内容进行解密
- 将解密之后的byte[]写入到临时文件中。因为如果没有再进行存档的话,要保证原存档的正确,所以不能把内容覆盖到原存档中。
- 对解密之后的byte[]进行反序列化为真正的数据
- 删除临时文件
到了这里,关于【Unity】C#存档与加密的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!