目录
一、准备工作
1.1 前言
1.2 完善聊天服务器
1.3 自定义聊天服消息
二、玩家登录聊天服
2.1 Gate网关与聊天服通讯
2.2 保存玩家实体映射实例ID
三、处理聊天逻辑
3.1 发送聊天消息的定义
3.2 定义聊天记录组件
3.3 编写发送聊天消息处理类
ET是一个游戏框架,用的编程语言是C#,游戏引擎是Unity,框架作者:熊猫 ET社区
聊天系统在各种网络游戏中也是必备的功能,我们可以通过系统进行世界聊天、私聊、发红包、组队等功能,实现游戏的社交属性。
(网上找的聊天框图片)
下面我将以我的开发经验记录用ET框架实现聊天系统的过程,有不足之处欢迎大家评论区讨论。
一、准备工作
1.1 前言
前几篇文章里实现的功能(商城、邮件)都是在Map服务器中实现的,我们实现聊天功能要单独起一个聊天服务器ChatInfo,来建立玩家实体映射来实现聊天消息的通讯。
下面这篇文章介绍的是ET框架单独起服务器的流程:
ET框架新起一个服务及实现服务之间的消息通讯_et startsceneconfig-CSDN博客https://blog.csdn.net/qq_48512649/article/details/136732262
按照流程新起一个聊天服务器ChatInfo
1.2 完善聊天服务器
创建组件ChatInfoUnitsComponent用来管理所有玩家映射实体,并把此组件挂载到聊天服中
namespace ET.Server
{
[ComponentOf(typeof(Scene))]
public class ChatInfoUnitsComponent : Entity,IAwake,IDestroy
{
public Dictionary<long, ChatInfoUnit> ChatInfoUnitsDict = new Dictionary<long, ChatInfoUnit>();
}
}
ChatInfoUnit用来保存玩家在聊天服中的基本信息,(每个玩家对应一个ChatInfoUnit)
namespace ET.Server
{
[ChildOf(typeof(ChatInfoUnitsComponent))]
public class ChatInfoUnit : Entity,IAwake,IDestroy
{
public long GateSessionActorId;
public string Name; //姓名
public long UnitId;
public int Gender; //性别
public int Level; //等级
public int HeadImg; //头像
//私聊信息
public Dictionary<long, List<PrivateTalk>> PrivateTalkInfo = new Dictionary<long, List<PrivateTalk>>();
public int AreaId = 1; //地区
public int ServerId = 2; //服务器ID
public int Online = 0; //是否在线
}
}
1.3 自定义聊天服消息
ET框架网络通讯消息_et mongohelper.deserialize-CSDN博客https://blog.csdn.net/qq_48512649/article/details/134028530
聊天服中发送和接收消息我们进行自定义,用来针对玩家级别的消息转发
namespace ET
{
// 不需要返回消息
public interface IActorChatInfoMessage: IActorMessage
{
}
public interface IActorChatInfoRequest: IActorRequest
{
}
public interface IActorChatInfoResponse: IActorResponse
{
}
}
在NetServerComponentOnReadEvent中判断,
如果消息类型是IActorChatInfoRequest或IActorChatInfoMessage则进行处理
其中player.ChatInfoInstanceId会在2.2中讲解是如何保存的
case IActorChatInfoRequest actorChatInfoRequest:
{
Player player = session.GetComponent<SessionPlayerComponent>().Player;
if (player == null || player.IsDisposed || player.ChatInfoInstanceId == 0)
{
break;
}
int rpcId = actorChatInfoRequest.RpcId; // 这里要保存客户端的rpcId
long instanceId = session.InstanceId;
IResponse iresponse = await ActorMessageSenderComponent.Instance.Call(player.ChatInfoInstanceId, actorChatInfoRequest);
iresponse.RpcId = rpcId;
// session可能已经断开了,所以这里需要判断
if (session.InstanceId == instanceId)
{
session.Send(iresponse);
}
break;
}
case IActorChatInfoMessage actorChatInfoMessage:
{
Player player = session.GetComponent<SessionPlayerComponent>().Player;
if (player == null || player.IsDisposed || player.ChatInfoInstanceId == 0)
{
break;
}
ActorMessageSenderComponent.Instance.Send(player.ChatInfoInstanceId, actorChatInfoMessage);
break;
}
二、玩家登录聊天服
在玩家登录游戏的流程中会发送这样一条消息
G2C_EnterGame g2CEnterGame = (G2C_EnterGame)await gateSession.Call(new C2G_EnterGame() { });
在这个消息的处理类中要做的事:
- 从数据库或缓存中加载出玩家Unit实体及其相关组件
- 玩家Unit上线后的初始化操作
- 玩家登录聊天服
我们重点看C2G_EnterGameHandler中玩家登录聊天服的实现
玩家登录聊天服的过程其实就是在聊天服生成玩家实体映射信息、获取并保存玩家实体映射信息的实例Id,有了实例Id后就可以与聊天服进行玩家级别的消息通讯了
2.1 Gate网关与聊天服通讯
private async ETTask<long> EnterWorldChatServer(Unit unit,int guildId)
{
UserCacheProto1 ucp = UnitHelper.GetUserCacheForChat(unit,guildId);
//读取配置表获取聊天服配置信息
StartSceneConfig startSceneConfig = StartSceneConfigCategory.Instance.GetBySceneName(unit.DomainZone(), "ChatInfo");
//网关服发送消息登录聊天服
Chat2G_EnterChat chat2GEnterChat = (Chat2G_EnterChat)await ActorMessageSenderComponent.Instance.Call(startSceneConfig.InstanceId, new G2Chat_EnterChat()
{
UserInfo = ucp,
GateSessionActorId = unit.GetComponent<UnitGateComponent>().GateSessionActorId
});
return chat2GEnterChat.ChatInfoUnitInstanceId;
}
网关服和聊天服通讯属于内部消息,所以Chat2G_EnterChat消息定义在InnerMessage.proto中
//ResponseType Chat2G_EnterChat
message G2Chat_EnterChat // IActorRequest
{
int32 RpcId = 90;
int64 GateSessionActorId = 1;
UserCacheProto1 UserInfo = 1;
}
message Chat2G_EnterChat // IActorResponse
{
int32 RpcId = 90;
int32 Error = 91;
string Message = 92;
int64 ChatInfoUnitInstanceId = 1;
}
//UserCacheProto1 用于玩家信息聊天传输的载体
//UserCacheProto1 定义在OuterMessage.proto中
message UserCacheProto1
{
string Name = 1;
int32 Gender = 2;
int32 Level = 3;
int32 HeadImg = 4;
int64 UnitId = 5;
}
消息处理类
namespace ET.Server
{
[ActorMessageHandler(SceneType.ChatInfo)]
[FriendOf(typeof(ChatInfoUnit))]
public class G2Chat_EnterChatHandler : AMActorRpcHandler<Scene, G2Chat_EnterChat, Chat2G_EnterChat>
{
protected override async ETTask Run(Scene scene, G2Chat_EnterChat request, Chat2G_EnterChat response)
{
//获取管理所有玩家映射实体的组件
ChatInfoUnitsComponent chatInfoUnitsComponent = scene.GetComponent<ChatInfoUnitsComponent>();
//获得具体的玩家映射信息
ChatInfoUnit chatInfoUnit = chatInfoUnitsComponent.Get(request.UserInfo.UnitId);
int flag = 0;
if ( chatInfoUnit == null )
{
chatInfoUnit = chatInfoUnitsComponent.AddChildWithId<ChatInfoUnit>(request.UserInfo.UnitId);
//挂载邮箱组件才会有消息处理能力(玩家级别的消息处理)
chatInfoUnit.AddComponent<MailBoxComponent>();
flag = 1;
}
chatInfoUnit.Logon(request.UserInfo, request.GateSessionActorId);
if (flag == 1)
{
chatInfoUnitsComponent.Add(chatInfoUnit);
}
response.ChatInfoUnitInstanceId = chatInfoUnit.InstanceId;
await ETTask.CompletedTask;
}
}
}
管理所有玩家映射实体ChatInfoUnitsComponent组件的具体行为:ChatInfoUnitsComponentSystem
using System.Collections.Generic;
namespace ET.Server
{
public class ChatInfoUnitsComponentDestroy : DestroySystem<ChatInfoUnitsComponent>
{
protected override void Destroy(ChatInfoUnitsComponent self)
{
foreach (var chatInfoUnit in self.ChatInfoUnitsDict.Values)
{
chatInfoUnit?.Dispose();
}
}
}
[FriendOf(typeof(ChatInfoUnit))]
[FriendOf(typeof(ChatInfoUnitsComponent))]
[FriendOf(typeof(UserCacheInfo))]
public static class ChatInfoUnitsComponentSystem
{
//从数据库中加载玩家的相关聊天信息
public static async ETTask LoadInfo(this ChatInfoUnitsComponent self)
{
var userCacheList = await DBManagerComponent.Instance.GetZoneDB(self.DomainZone()).Query<UserCacheInfo>(d => true,collection:"UserCachesComponent");
foreach ( UserCacheInfo userCache in userCacheList )
{
ChatInfoUnit chatInfoUnit = self.AddChildWithId<ChatInfoUnit>(userCache.UnitId);
chatInfoUnit.AddComponent<MailBoxComponent>();
chatInfoUnit.Name = userCache.Name;
chatInfoUnit.GateSessionActorId = 0;
chatInfoUnit.UnitId = userCache.UnitId;
chatInfoUnit.Gender = userCache.Gender;
chatInfoUnit.Level = userCache.Level;
chatInfoUnit.HeadImg = userCache.HeadImg;
chatInfoUnit.TitleImg = userCache.TitleImg;
chatInfoUnit.Online = 0;
self.ChatInfoUnitsDict.Add(userCache.UnitId,chatInfoUnit);
}
}
//将玩家实体映射信息添加到组件中
public static void Add(this ChatInfoUnitsComponent self, ChatInfoUnit chatInfoUnit)
{
if (self.ChatInfoUnitsDict.ContainsKey(chatInfoUnit.Id))
{
return;
}
self.ChatInfoUnitsDict.Add(chatInfoUnit.Id, chatInfoUnit);
}
//尝试从组件中获取玩家实体映射信息
public static ChatInfoUnit Get(this ChatInfoUnitsComponent self, long id)
{
self.ChatInfoUnitsDict.TryGetValue(id, out ChatInfoUnit chatInfoUnit);
return chatInfoUnit;
}
//移除玩家实体映射信息
public static void Remove(this ChatInfoUnitsComponent self, long id)
{
if (self.ChatInfoUnitsDict.TryGetValue(id, out ChatInfoUnit chatInfoUnit))
{
self.ChatInfoUnitsDict.Remove(id);
chatInfoUnit?.Dispose();
}
}
public static void Leave(this ChatInfoUnitsComponent self, long id)
{
if (self.ChatInfoUnitsDict.TryGetValue(id, out ChatInfoUnit chatInfoUnit))
{
self.Get(id).Leave();
//chatInfoUnit?.Dispose();
}
}
public static List<ChatInfoForList> GetChatInfo(this ChatInfoUnitsComponent self,int type, int index)
{
List<ChatInfoForList> cil = new List<ChatInfoForList>();
return cil;
}
}
}
2.2 保存玩家实体映射实例ID
经过Gate网关发送了G2Chat_EnterChat消息,我们可以看到把聊天服中玩家实体映射实例ID返回回来了。
玩家新增字段ChatInfoInstanceId用于保存聊天服玩家实体映射实例ID信息
在C2G_EnterGameHandler这一消息处理类中进行保存:
player.ChatInfoInstanceId = await this.EnterWorldChatServer(unit,gi.GuildId); //登录聊天服
保存好ChatInfoInstanceId后我们就可以使用自定义聊天服消息进行通讯了。
三、处理聊天逻辑
前边工作都做好后我们开始实现聊天相关的逻辑。
3.1 发送聊天消息的定义
我们都知道,当我们聊天的时候在输入框输入内容,点击发送,我们输入的内容就会出现在聊天框中,当别人发送消息时我们也会看到聊天框新弹出消息内容。
聊天过程:客户端 ——》聊天服,外部消息所以定义到OutMessage.proto中
//ResponseType Chat2C_SendChatInfo
message C2Chat_SendChatInfo // IActorChatInfoRequest
{
int32 RpcId = 1;
int32 ChatType = 2; //聊天类型
string Text = 3; //聊天内容
int64 UnitId = 4; //玩家ID
}
message Chat2C_SendChatInfo // IActorChatInfoResponse
{
int32 RpcId = 90;
int32 Error = 91;
string Message = 92;
}
message ChatInfoForList
{
UserCacheProto1 UserInfo = 1; //玩家实体映射信息
ChatMessage2 ChatMessage = 2; //封装的聊天信息
}
message ChatMessage2
{
int32 ChatType = 1; //聊天类型
string Text = 2; //聊天内容
int64 Timestamp = 3; //发送时间
}
3.2 定义聊天记录组件
namespace ET.Server
{
[ComponentOf(typeof(Scene))]
public class ChatInfoListsComponent : Entity,IAwake,IDestroy,IDeserialize,ITransfer,IUnitCache
{
[BsonIgnore]
public Dictionary<long, ChatInfoList> ChatInfoListsDict = new Dictionary<long, ChatInfoList>();
}
}
namespace ET.Server
{
[ChildOf(typeof(ChatInfoListsComponent))]
public class ChatInfoList: Entity,IAwake,IDestroy
{
public int Type = 0;
[BsonDictionaryOptions(DictionaryRepresentation.ArrayOfArrays)]
public Dictionary<long, ChatInfoForList> ChatInfos= new Dictionary<long, ChatInfoForList>();
}
}
3.3 编写发送聊天消息处理类
namespace ET.Server
{
[FriendOf(typeof(ChatInfoUnitsComponent))]
[FriendOf(typeof(ChatInfoListsComponent))]
[FriendOf(typeof(ChatInfoUnit))]
[FriendOf(typeof(ChatInfoList))]
[ActorMessageHandler(SceneType.ChatInfo)]
public class C2Chat_SendChatInfoHandler : AMActorRpcHandler<ChatInfoUnit, C2Chat_SendChatInfo, Chat2C_SendChatInfo>
{
protected override async ETTask Run(ChatInfoUnit chatInfoUnit, C2Chat_SendChatInfo request, Chat2C_SendChatInfo response)
{
//如果聊天内容为空直接return
if (string.IsNullOrEmpty(request.Text))
{
response.Error = ErrorCode.ERR_ChatMessageEmpty;
return;
}
if (request.ChatType == 4)
{
response.Error = 99999999;
return;
}
//私聊
ChatInfoUnitsComponent chatInfoUnitsComponent = chatInfoUnit.DomainScene().GetComponent<ChatInfoUnitsComponent>();
if (request.ChatType == 5)
{
PrivateTalk pt = new PrivateTalk();
pt.UnitId = chatInfoUnit.UnitId;
pt.Text = request.Text;
pt.TimeStamp = TimeHelper.ClientNow();
chatInfoUnit.PrivateTalks(pt,request.UnitId);
otherChatInfoUnit.PrivateTalks(pt,chatInfoUnit.UnitId);
}
//世界聊天
else
{
ChatInfoListsComponent chatInfoListsComponent = chatInfoUnit.DomainScene().GetComponent<ChatInfoListsComponent>();
UserCacheProto1 ucp = chatInfoUnit.ToMessage();
ChatMessage2 cm = new ChatMessage2()
{
ChatType = request.ChatType,
ChatDesType = request.ChatDesType,
Text = request.Text,
Timestamp = TimeHelper.ClientNow()
};
chatInfoListsComponent.Send(request.ChatType,ucp, cm,chatInfoUnitsComponent.ChatInfoUnitsDict,chatInfoUnit);
}
await ETTask.CompletedTask;
}
}
}
ChatInfoUnitSystem处理私聊信息
public static void PrivateTalks(this ChatInfoUnit self, PrivateTalk pt, long UnitId)
{
self.PrivateTalkInfo[UnitId].Add(pt);
if (self.GateSessionActorId > 0 && self.Online == 1)
{
ActorMessageSenderComponent.Instance.Send(self.GateSessionActorId, new Chat2C_NoticeChatInfo()
{
UserInfo = self.GetParent<ChatInfoUnitsComponent>().ChatInfoUnitsDict[pt.UnitId].ToMessage(),
ChatMessage = new ChatMessage2(){ChatType= 5,ChatDesType=pt.ChatDesType,Text = pt.Text,Timestamp = pt.TimeStamp},
});
}
}
ChatInfoListsComponentSystem处理世界聊天信息
public static void Send(this ChatInfoListsComponent self, int type , UserCacheProto1 ucp,ChatMessage2 cm , Dictionary<long, ChatInfoUnit> ChatInfoUnitsDict, ChatInfoUnit chatInfoUnit)
{
ChatInfoList cil = self.ChatInfoListsDict[id];
foreach (var otherUnit in ChatInfoUnitsDict)
{
ActorMessageSenderComponent.Instance.Send(otherUnit.Value.GateSessionActorId, new Chat2C_NoticeChatInfo()
{
UserInfo =ucp,
ChatMessage =cm,
});
}
}
Chat2C_NoticeChatInfo是服务器发送给客户端的一次性消息,由客户端来处理并展示聊天信息。