在正式的商业游戏中,一个玩家的存档涉及很多个数据库,这里的存档使用单库,一般独立游戏使用。

存档实体类

在MySqlData文件夹内新建GlobalSave文件

1
2
3
4
5
6
7
8
9
10
11
12
13
using SqlSugar;

namespace MySql.MySQLData
{
[SugarTable("globalsave")]
public class GlobalSave
{
[SugarColumn(IsPrimaryKey = true,IsIdentity = true)]
public int Id { get; set; }
public int UserId { get;set; }
public string? Data { get; set; }
}
}

打开Navicat,添加globalsave表

列名 类型 长度 小数点 不是null 主键 其他
Id int 11 0 自动递增、无符号、无重复
UserId int 11 0 无重复
Data MediumText N/A N/A

服务器端存档读档逻辑

修改UserManager,在其中添加存档读档的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/// <summary>
/// 用户存档
/// </summary>
/// <param name="id">用户id</param>
/// <param name="data">存档内容</param>
/// <returns>是否成功</returns>
public BaseResult GlobalSave(int id, string data)
{
try
{
GlobalSave? global = RedisMgr.Instance.GetClass<GlobalSave>(id);
if (global != null)
{
global.Data = data;
RedisMgr.Instance.SetClass(id, global);
}
else//代表玩家第一次存档
{
global = new GlobalSave
{
UserId = id,
Data = data
};
RedisMgr.Instance.SetClass(id, global);
}
return BaseResult.Success;
}
catch (Exception ex)
{
Debug.LogError("用户存档失败:" + ex);
return BaseResult.Failure;
}
}
/// <summary>
/// 获取用户存档
/// </summary>
/// <param name="id">用户Id</param>
/// <param name="data">用户存档内容</param>
/// <returns>是否成功获取</returns>
public GetDataResult GetGlobalData(int id,out string data)
{
data = "";
try
{
GlobalSave? global = RedisMgr.Instance.GetClass<GlobalSave>(id);
if (global != null)
{
if (global.Data != null)
{
data = global.Data;
return GetDataResult.Success;
}
else
{
return GetDataResult.NoData;
}

}
else { return GetDataResult.NoData; }
}
catch (Exception ex)
{
Debug.LogError("获取用户存档失败:" + ex);
return GetDataResult.Failure;
}
}

修改ServerEnum,在其中添加获取数据的结果类型的枚举,在客户端中也要添加

1
2
3
4
5
6
public enum GetDataResult
{
Success,
Failure,
NoData
}

存档读档协议

这里我们准备两条协议,一条存档协议,一条读档协议

ProtocolEnum中添加协议,客户端也添加

1
2
3
4
5
6
7
8
9
public enum ProtocolEnum
{
//...

MsgGlobalSave = 400,//存档协议
MsgGetGlobal = 401,//读档协议
//...
}

UserMsg中实现协议,客户端也添加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
[ProtoContract()]
public class MsgGlobalSave : MsgBase
{
public MsgGlobalSave()
{
ProtoType = ProtocolEnum.MsgGlobalSave;
}
[ProtoMember(1)]
public override ProtocolEnum ProtoType { get; set; }
[ProtoMember(2)]
public string? Data { get; set; }
[ProtoMember(3)]
public BaseResult Result { get; set; }
}
[ProtoContract()]
public class MsgGetGlobal : MsgBase
{
public MsgGetGlobal()
{
ProtoType = ProtocolEnum.MsgGetGlobal;
}
[ProtoMember(1)]
public override ProtocolEnum ProtoType { get; set; }
[ProtoMember(2)]
public string? Data { get; set; }
[ProtoMember(3)]
public GetDataResult Result { get; set; }
}

这里的Data是string类型,可以根据需要传输byte类型。客户端将存档数据转换成byte数组,服务端将byte数组转换成字符串再存进数据库。

服务器分发存读档协议

修改MsgHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/// <summary>
/// 分发存档协议
/// </summary>
public static void MsgGlobalSave(ClientSocket client, MsgBase msg)
{
if (msg == null)
{
Debug.LogError(client.Socket?.RemoteEndPoint?.ToString() + "MsgGlobalSave协议接收错误!");
}
else
{
MsgGlobalSave globalSave = (MsgGlobalSave)msg;
if (globalSave.Data != null)
globalSave.Result = UserManager.Instance.GlobalSave(client.UserId, globalSave.Data);
else
globalSave.Result = BaseResult.Failure;
globalSave.Data = null;
ServerSocket.Send(client, globalSave);
}
}
/// <summary>
/// 分发读档协议
/// </summary>
public static void MsgGetGlobal(ClientSocket client, MsgBase msg)
{
if(msg == null)
{
Debug.LogError(client.Socket?.RemoteEndPoint?.ToString() + "MsgGetGlobal协议接收错误!");
}
else
{
MsgGetGlobal msgGetGlobal = (MsgGetGlobal)msg;
msgGetGlobal.Result = UserManager.Instance.GetGlobalData(client.UserId, out string data);
msgGetGlobal.Data = data;
ServerSocket.Send(client,msgGetGlobal);
}
}

客户端存档读档

修改客户端的ProtocolManager

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void SaveDataRequest(string data,System.Action<BaseResult> callback)
{
//应该有一个存档的类,包含了所有的存档数据,在这里进行计算,赋值对应数据,然后将数据转成json字符串,或者是二进制,然后再进行协议提交
MsgGlobalSave msg = new MsgGlobalSave();
NetManager.Instance.AddProtoListener(ProtocolEnum.MsgGlobalSave, (msg) =>
{
MsgGlobalSave msgGlobalSave = msg as MsgGlobalSave;
callback?.Invoke(msgGlobalSave.Result);
});
msg.Data = data;
NetManager.Instance.SendMessage(msg);
}
public static void GetDataRequest(System.Action<GetDataResult,string> callback)
{
MsgGetGlobal msg = new MsgGetGlobal();
NetManager.Instance.AddProtoListener(ProtocolEnum.MsgGetGlobal, (msg) =>
{
MsgGetGlobal msgGetGlobal = msg as MsgGetGlobal;
callback?.Invoke(msgGetGlobal.Result, msgGetGlobal.Data);
});
NetManager.Instance.SendMessage(msg);
}

修改NetTest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
void Update()
{
//...
if(Input.GetKeyDown(KeyCode.E))
{
ProtocolManager.SaveDataRequest("SaveTest", (res) =>
{
switch(res)
{
case BaseResult.Failure:
Debug.Log("Save Failed");
break;
case BaseResult.Success:
Debug.Log("Save Success");
break;
}
});
}
if(Input.GetKeyDown(KeyCode.F))
{
ProtocolManager.GetDataRequest((res, data) =>
{
switch (res)
{
case GetDataResult.Success:
Debug.Log("GetSave Success: " + data);
break;
case GetDataResult.Failure:
Debug.Log("GetSave Failure");
break;
case GetDataResult.NoData:
Debug.Log("GetSave NoData");
break;
}
});
}
}

测试时先登录,再按下相应的按键进行测试。

Redis Desktop Manager

Redis定时存储

现在Redis有了玩家存档数据,需要完善定时存储的逻辑了。

修改服务端UserManager

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class UserManager : SingletonPattern<UserManager>
{
public void Init()
{
RedisMgr.Instance.SaveRedisAction += SaveDataToMysql;
}
private void SaveDataToMysql()
{
try
{
//遍历数据库所有数据,在里面挑选Redis有的数据,然后更新MySql。当MySql数据特别多时,费时
List<User> allUser = MySqlMgr.Instance.SqlSugarDB!.Queryable<User>().ToList();
List<GlobalSave> updateList = new List<GlobalSave>();
foreach (User user in allUser)
{
GlobalSave? global = RedisMgr.Instance.GetClass<GlobalSave>(user.Id);
if(global != null)
{
updateList.Add(global);
}
}
//先缓存,然后一次性更新所有角色数据
MySqlMgr.Instance.SqlSugarDB.Updateable(updateList).ExecuteCommand();
}
catch (Exception ex)
{
Debug.LogError("Redis写入MySql出错:" + ex);
}

//第二种方式,遍历Redis,从中找到需要存到MySql的数据(这里是“GlobalSave” + 玩家Id)
//这需要我们在存储Redis时添加一些符号将类型和玩家Id分隔开,如:GlobalSave|5
//在遍历Redis时,由于Redis是分布式的,不支持遍历,需要使用 GetServer().Keys() ,具体可以见官方文档
//获取到玩家后,分割字符串,得到玩家Id再执行和上面相似的更新操作。
}
//...
}