unity基于网络从登录到背包与商店系统的简单实例
环境准备
客户端:unity2021
服务端:vs2019
工具:protobuf、cJson、libuv
流程图如下

角色登录与创建
协议
查看代码
客户端
通过UI点击事件作为驱动逻辑发送网络请求,接受请求的时候也作出相应回调
private void OnLoginClick()
{
//预先判断账号密码是否为空
if (string.IsNullOrEmpty(playerId.text))
{
Debug.Log("用户名为空");
return;
}
if (string.IsNullOrEmpty(password.text))
{
Debug.Log("密码为空");
return;
}
UserService.Instance().SendPlayerLogin(this.playerId.text, this.password.text);
}
private void OnLogin(int result,string reason)
{
//能够成功登录
if (result == 0)
{
SceneManager.LoadScene(1);
UserService.Instance().SendPlayerInfoReq(Player.Instance().PlayerID);
}
//登录失败
else
{
Debug.LogFormat("Login Fail [Result:{0} Reason:{1}]", result, reason);
}
}
UserService.cs
当客户端接收到Response,Service接收到该事件,并向UI层和其他数据管理模块进行回调
和NetWork的事件注册逻辑
public UserService()
{
EventModule.Instance().AddNetEvent((int)SERVER_CMD.ServerLoginRsp, OnPlayerLogin);
EventModule.Instance().AddNetEvent((int)SERVER_CMD.ServerCreateRsp, OnPlayerCreate);
EventModule.Instance().AddNetEvent((int)SERVER_CMD.ServerShopbuyRsp, OnShopBuy);
EventModule.Instance().AddNetEvent((int)SERVER_CMD.ServerPlayerinfoRsp, OnPlayerInfo);
}
public void OnDispose()
{
EventModule.Instance().RemoveNetEvent((int)SERVER_CMD.ServerLoginRsp, OnPlayerLogin);
EventModule.Instance().RemoveNetEvent((int)SERVER_CMD.ServerCreateRsp, OnPlayerCreate);
EventModule.Instance().RemoveNetEvent((int)SERVER_CMD.ServerShopbuyRsp, OnShopBuy);
EventModule.Instance().RemoveNetEvent((int)SERVER_CMD.ServerPlayerinfoRsp, OnPlayerInfo);
}
让其他模块通过这里进行注册回调
public UnityAction<int, string> OnLogin; //角色进入对UI回调
public UnityAction<int, string> OnCreate; //角色创建对UI回调
public UnityAction OnBuySuccess; //购买物品时回调
public UnityAction<int,string> OnBuyFail; //购买失败的回调
public UnityAction OnPlayerInfoLoad; //角色信息加载完成的回调
以角色登录举例 一个发送一个接受逻辑。角色登录成功会再向服务器发送一个角色信息的请求
//发送登录请求
public void SendPlayerLogin(string playerId, string password)
{
Debug.LogFormat("Send Player Login Request:[id:{0} password:{1}]", playerId, password);
PlayerLoginReq req = new PlayerLoginReq();
req.PlayerID = playerId;
req.Password = password;
Player.Instance().PlayerID = playerId;
Network.Instance().SendMsg((int)CLIENT_CMD.ClientLoginReq, req);
}
//接受登录响应
public void OnPlayerLogin(int cmd, IMessage msg)
{
PlayerLoginRsp rsp = msg as PlayerLoginRsp;
Debug.LogFormat("OnPlayerLogin:[Result:{0} Reason:{1}]", rsp.Result, rsp.Reason);
if (this.OnLogin != null)
{
OnLogin.Invoke(rsp.Result, rsp.Reason);
}
}
角色信息请求和响应
查看代码
玩家实体唯一,用单例的方式表示
查看代码
服务端
服务端的网络层客户端发来的网络包后通过解析Proto协议,根据Proto头进行不同处理
查看代码
角色信息请求的处理逻辑
//客户端拉取角色信息
bool PlayerMgr::playerInfo_request(uv_tcp_t* client,const PlayerInfoReq* req)
{
bool result = false;
Player* player = nullptr;
PlayerSaveData* playerData = new PlayerSaveData();
PlayerBagData* playerBag = new PlayerBagData();
PlayerInfoRsp rsp;
fprintf(stdout, "playerInfo request\n");
// 1. 查看玩家是否已经在游戏中,如果不在游戏中则返回失败
player = find_player(req->playerid());
if (player == nullptr) {
rsp.set_result(-1);
rsp.set_reason("player has not login");
player = nullptr;
goto Exit1;
}
// 2. 尝试从文件中加载玩家数据,如果未加载成功则返回失败
if (!_load_player(req->playerid(), playerData)) {
rsp.set_result(-2);
rsp.set_reason("player id exist");
goto Exit1;
}
// 3、尝试从文件中加载背包数据
if (g_itemMgr._load_player_Items(req->playerid(), playerBag))
{
rsp.set_allocated_playerbagdata(playerBag);
}
// 4. 应答客户端登录结果
rsp.set_allocated_playerdata(playerData);
rsp.set_result(0);
Exit1:
{
string r = rsp.SerializeAsString();
int len = encode(s_send_buff, SERVER_PLAYERINFO_RSP, r.c_str(), (int)r.length());
sendData((uv_stream_t*)client, s_send_buff, len);
}
result = true;
Exit0:
return result;
此外创建的时候要将角色存储下来
登录的时候要将角色读取出来存到角色容器
查看代码
为了让数据持久化,用文件方式存取数据
// 把玩家数据保存到文件中,成功返回true,失败返回false
bool PlayerMgr::_save_player(const Player* player) {
// todo 把玩家数据序列化后存盘,参考save函数
int retCode = 0;
PlayerSaveData playerData;
playerData.set_password(player->Password);
playerData.set_name(player->Name);
playerData.set_playerid(player->PlayerID);
playerData.set_money(player->Money);
retCode = save(player->PlayerID.c_str(), playerData.SerializeAsString().c_str(), playerData.ByteSize());
if (retCode != 0)
return false;
else
return true;
}// 从文件中加载玩家数据,用PlayerSaveData的方式读出 成功返回true,失败返回false
bool PlayerMgr::_load_player(string playerID, PlayerSaveData* playerData) {
// todo 从文件中读取数据,并反序列化到playerData中,参考load函数
int len = load(playerID.c_str(), s_send_buff, sizeof(s_send_buff));
if (len >= 0) {
playerData->ParseFromArray(s_send_buff, len);
return true;
}
return false;
}
Save文件
查看代码
Load文件
查看代码
Player实体数据
struct Player {
string PlayerID;
string Password;
string Name;
int Money;
};
背包与商城
配置表
客户端和服务端都持有一个配置表数据,记录关于存放在商城和背包的道具的静态信息。双方只需要传递道具的id就可以在各自的配置表中找出道具的具体信息
json配置表

客户端
客户端背包商城逻辑

客户端和服务端执行相应的读取操作,存储到相应的实体中去
客户端利用字典的结构进行存储
查看代码
发送购买请求和接收购买请求
//发送角色购买请求
public void SendShopBuy(string playerId,int itemId,int count)
{
Debug.LogFormat("Send Shop Buy Request:[id:{0} itemId:{1} count:{2}]", playerId, itemId, count);
ShopBuyReq req = new ShopBuyReq();
req.PlayerID = playerId;
req.ItemId = itemId;
req.Count = count;
Network.Instance().SendMsg((int)CLIENT_CMD.ClientShopbuyReq, req);
}
//接受购买请求响应
public void OnShopBuy(int cmd, IMessage msg)
{
ShopBuyRsp rsp = msg as ShopBuyRsp;
Debug.LogFormat("ShopBuyRsp:[Result:{0} Reason:{1}]", rsp.Result, rsp.Reason);
if (rsp.Result == 0)
{
OnBuySuccess.Invoke();
}
else
{
OnBuyFail.Invoke(rsp.Result, rsp.Reason);
Debug.LogFormat("Buy Fail [Result:{0} Reason:{1}]", rsp.Result, rsp.Reason);
}
}
协议结构如下:
查看代码
购买成功后一个是会修改角色的金钱数据和ui上的金币数量,一个是修改背包的道具信息
ShopUI记录了购买时点击的商品id和数量,当购买成功后修改角色背包信息,同时对UIBag的信息进行了更新
添加Player中的道具
public void AddBagItem(int itemId,int count)
{
if (!bag.ContainsKey(itemId))
{
bag.Add(itemId, count);
}
else
{
bag[itemId] += count;
}
if (OnBagChange != null)
{
OnBagChange.Invoke();
}
}
修改背包UI 重新刷新一遍
void UpdateItem()
{
ClearItems();
this.money.text = Player.Instance().Money.ToString(); //修改金币数量
Dictionary<int, int> bag = Player.Instance().bag;
foreach (int id in bag.Keys)
{
Item item = ItemManager.Instance().GetItem(id);
GameObject go = Instantiate<GameObject>(ItemPrefab, itemGrid.transform);
Slot ui;
if (go.TryGetComponent<Slot>(out ui))
{
if (ui is UIBagItem)
{
UIBagItem bagItem = ui as UIBagItem;
Sprite sprite;
ItemManager.Instance().TryGetSprite(item.iconName, out sprite);
//将item信息赋值给ui
bagItem.SetInfo(item, sprite);
bagItem.count.text = string.Format("*{0}", bag[id]);
bagItem.onClick.AddListener(OnSlotClick);
}
}
}
}
服务端
服务端也需要从配置表读取道具信息
查看代码
本地的实体结构如下
struct PlayerItem
{
string PlayerID;
map<int, int> Items;
};
struct Item
{
int id;
string name;
string introduce;
int price;
string iconName;
};
服务端维护了一个玩家背包的存储数据
相应的存取操作
查看代码
协议结构如下
message BagItem{
int32 ItemId=1;
int32 Count=2;
}
message PlayerBagData{
repeated BagItem BagItem=2;
}
接收到购买请求进行处理
bool ItemMgr::item_buy(uv_tcp_t* client, const ShopBuyReq* req) {
bool result = false;
Player* player = nullptr;
PlayerSaveData playerData;
ShopBuyRsp rsp;
PlayerBagData playerBagData;
PlayerItem* playerItem = new PlayerItem();
fprintf(stdout, "item buy request\n");
// 1. 查看玩家是否已经在游戏中,如果在游戏中登录失败
player = g_playerMgr.find_player(req->playerid());
if (player == nullptr) {
rsp.set_result(-1);
rsp.set_reason("player has not login");
goto Exit1;
}
//2、判断商店是否有该商品
if (m_storeMap.count(req->itemid()) == 0)
{
rsp.set_result(-2);
rsp.set_reason("store has not this item");
goto Exit1;
}
//3、读取当前玩家金钱数据 读取商店物品价格数据 判断是否足够购买
if (player->Money < m_storeMap[req->itemid()]->price * req->count())
{
rsp.set_result(-4);
rsp.set_reason("has not enough money");
goto Exit1;
}
player->Money -= m_storeMap[req->itemid()]->price * req->count();
// 4. 从文件中读取玩家背包数据并展开
playerItem->PlayerID = req->playerid();
if (_load_player_Items(playerItem->PlayerID, &playerBagData)) {
//如果存在玩家背包 用playerItem暂存
if (playerBagData.bagitem_size() > 0)
{
for (int i = 0; i < playerBagData.bagitem_size(); i++)
{
playerItem->Items.insert(make_pair(playerBagData.bagitem()[i].itemid(), playerBagData.bagitem()[i].count()));
}
}
}
//找到该道具 直接添加数量
if (playerItem->Items.count(req->itemid()) == 1)
{
playerItem->Items[req->itemid()] += req->count();
}
//没有找到 添加该道具
else
{
playerItem->Items.insert(make_pair(req->itemid(), req->count()));
}
//5、 保存背包信息
if (!_save_player_Items(playerItem))
{
fprintf(stdout, "save playerItems error\n");
goto Exit0;
}
//6、保存玩家信息
if (!g_playerMgr._save_player(player)) {
fprintf(stdout, "save playerInfo error\n");
goto Exit0;
}
// 7. 应答客户端登录结果
rsp.set_result(0);
Exit1:
{
fprintf(stdout, "buy success [item:%d count:%d moneyLeft:%d]\n", req->itemid(), req->count(), player->Money);
string r = rsp.SerializeAsString();
int len = encode(s_send_buff, SERVER_SHOPBUY_RSP, r.c_str(), (int)r.length());
sendData((uv_stream_t*)client, s_send_buff, len);
}
result = true;
Exit0:
return result;
}
来源链接:https://www.dianjilingqu.com/460927.html

