核心问题
如何实现所有客户端玩家信息同步(如位置信息)?
同步模式一般分两种:状态同步和帧同步。而本文主要针对MMO类游戏,所以建议用状态同步。
状态发生后,客户端上传操作到服务器,服务器收到后处理行为的结果,然后以广播的方式把状态发送给所有客户端,客户端收到状态后再根据状态显示内容。
MMORPG类游戏制作思路分享
一、配置一个简单的服务器
1、SDK及许可证下载地址
首先,登录Photon官网下载PhotonServer:下载地址
然后申请下载100人连接数许可证:下载地址
解压SDK后如下:
把下载的许可证放到此目录下:deploy\bin_Win64
然后我们启动PhotonControl.exe,这时候我们就可以看到最大连接数到了100人了
2、配置Server文件
用VS创建一个类库MyGameServer,在这个路径下再创建一个文件夹MyGameServer,然后再MyGameServer文件夹里面再创建一个文件夹命名为bin,创建好后我们在VS里面右击项目—>属性—>生成—>输出路径,把输出路径放在我们刚刚创建的MyGameServer\bin文件里面。
接着导入引用集,打开下图路径,将ExitGamesLibs.dll、Photon.SocketServer.dll、PhotonHostRuntimeInterfaces.dll、log4net.dll(用于日志输出,可以把一个文本写入到文本文档里面)四个程序集引用添加进来。
引入后将服务器端继承自ApplicationBase
using Photon.SocketServer;
namespace MyGameServer
{
//所有的Server端 主类都要继承自applicationbase
public class MyGameServer : ApplicationBase
{
//当一个客户端请求连接的时候,服务器端就会调用这个方法
//我们使用peerbase,表示和一个客户端的链接,然后photon就会把这些链接管理起来
protected override PeerBase CreatePeer(InitRequest initRequest)
{
return new ClientPeer(initRequest);
}
//初始化(当整个服务器启动起来的时候调用这个初始化)
protected override void Setup()
{
}
//server端关闭的时候
protected override void TearDown()
{
}
}
}
然后我们在添加一个类ClientPeer,这个类是管理与客户端的连接的。继承自Photon.SocketServer.ClientPeer
using Photon.SocketServer;
using PhotonHostRuntimeInterfaces;
namespace MyGameServer
{
//管理跟客户端的链接的
class ClientPeer : Photon.SocketServer.ClientPeer
{
public ClientPeer(InitRequest initRequest) : base(initRequest)
{
}
//处理客户端断开连接的后续工作
protected override void OnDisconnect(DisconnectReason reasonCode, string reasonDetail)
{
}
//处理客户端的请求
protected override void OnOperationRequest(OperationRequest operationRequest, SendParameters sendParameters)
{
}
}
}
配置好这些信息后,我们右击项目—>生成
进入deploy\MyGameServer\bin文件夹下看到我们项目生产的dll程序集了
3、添加Server程序,打印日志
接下来开始配置我们的TCP,UDP配置文件。
在deploy\bin_Win64文件下找到PhotonServer.config,这个就是我们需要的配置文件。
创建一个Log对象,然后初始化日志,在MyGameServer这个类里面定义一个Log对象
//定义一个Log对象
public static readonly ILogger log = LogManager.GetCurrentClassLogger();
然后在Setup里面初始化日志 ,在CreatePeer方法里处理客户端连接的事件
//当一个客户端请求连接的时候,服务器端就会调用这个方法
//我们使用peerbase,表示和一个客户端的链接,然后photon就会把这些链接管理起来
protected override PeerBase CreatePeer(InitRequest initRequest)
{
log.Info("一个客户端连接进来了!");
return new ClientPeer(initRequest);
}
//初始化(当整个服务器启动起来的时候调用这个初始化)
protected override void Setup()
{
//日志的初始化(定义配置文件log4net位置)
//Path.Combine 表示连接目录和文件名,可以屏蔽平台的差异
// Photon: ApplicationLogPath 就是配置文件里面路径定义的属性
//this.ApplicationPath 表示可以获取photon的根目录,就是Photon-OnPremise-Server-SDK_v4-0-29-11263\deploy这个目录
//这一步是设置日志输出的文档文件的位置,这里我们把文档放在Photon-OnPremise-Server-SDK_v4-0-29-11263\deploy\bin_Win64\log里面
log4net.GlobalContext.Properties["Photon:ApplicationLogPath"] = Path.Combine(Path.Combine(Path.Combine(this.ApplicationRootPath,"bin_win64")),"log");
//this.BinaryPath表示可以获取的部署目录就是目录Photon-OnPremise-Server-SDK_v4-0-29-11263\deploy\MyGameServer\bin
FileInfo configFileInfo = new FileInfo(Path.Combine(this.BinaryPath,"log4net.config"));//告诉log4net日志的配置文件的位置
//如果这个配置文件存在
if (configFileInfo.Exists)
{
LogManager.SetLoggerFactory(Log4NetLoggerFactory.Instance);//设置photon我们使用哪个日志插件
XmlConfigurator.ConfigureAndWatch(configFileInfo);//让log4net这个插件读取配置文件
}
log.Info("Setup Completed!");//最后利用log对象就可以输出了
}
//server端关闭的时候
protected override void TearDown()
{
}
重新在VS里面生成下然后关闭并重启Photon,打开日志,就可以看到我们自己的日志了
二、创建Unity客户端
在Unity3D里面导入photon unity客户端的dll,继承IPhotonPeerListener
1、在客户端发起与服务器的连接,服务器回应给客户端
客户端通过SendOperation()方法发起指定的请求,服务器通过OnOperationRequest()方法接收客户端请求,然后调用SendOperationResponse()方法给客户端一个回应,客户端通过OnOperationResponse()方法获取请求的回调信息。
2、从服务器给客户端直接发送事件SendEvent
在unity客户端里面的PhotonEngine类里面有一个方法OnEvent(),这个方法表示如果客户端没有发起请求,但是服务器端向客户端通知一些事情的时候就会通过OnEvent来进行响应
三、服务器连接数据库
用NHibernate数据库映射工具来连接数据库
参考此博客:http://www.u3d8.com/?p=1414
四、Unity连接服务器实现登陆、注册
客户端
一、首先是工具类Singleton封装的单例脚本
二、消息订阅分发类HandlerMediat,用于分发接收到的服务器消息。如果不太懂观察者模式使用的 可以参考:消息订阅分发机制的实际应用
三、HandlerMediat类对应的枚举OperationCode,该枚举值是与服务器消息类型保持一致。记录该消息是属于什么类型,比如用户登录消息、用户注册消息、获取背包消息等等
四、创建监听服务器消息抽象基类HandlerBase
五、创建监听服务器登录消息类LoginHandler继承自:HandlerBase,这里因为登录和注册消息比较简单,所以我统一放在了登录消息类里。
该类建完后,在登录场景中创建物体, 命名为“Handler”,并挂载LoginHandler脚本
六、消息发送类LoginRequest,同样这里将登录消息和注册消息都放在了该类里
七、UI脚本,我们创建LoginPanel和RegisterPanel分别管理登录界面和注册界面的UI。这里为了方便,所有UI都是外部挂载上去的。
八、修改第二节挂载的PhotonEngine脚本。添加一个ReturnCode枚举值,对应服务器消息成功失败。在OnOperationResponse方法里处理接收到的消息。
服务器
首先我们在服务器创建一个文件夹Handler,这个文件夹下的文件都用来接收处理客户端发来的消息。
在Handler文件夹下创建相应的脚本IHandlerBase、LoginHandler
创建Net文件夹,并将与网络相关的ClientPeer、MyGameServer脚本拖入进去
在Net文件夹创建脚本HandlerMediat、OperationCode
创建Tools文件夹,并将NhibernateHelper脚本拖入进去
在Tools文件夹创建脚本DictTool
一、接口IHandlerBase 作为接收客户端消息类的基类
二、LoginHandler类,该类用来接收客户端发送的登录、注册消息的处理
三、HandlerMediat类,该类与Unity客户端的类似,用于分发接收到客户端的消息
四、HandlerMediat类对应的枚举OperationCode,该枚举值是记录该消息是属于什么类型,比如用户登录消息、用户注册消息、获取背包消息等等
五、DictTool类,该类是做字典解析的封装
六、修改MyGameServer脚本,添加一个ReturnCode枚举值,对应消息成功失败。在Setup和TearDown方法里分别添加Handler和移除Handler
七、修改ClientPeer脚本,在OnOperationRequest方法里分发消息
五、实现客户端玩家的创建同步
在客户端与服务器端同步创建玩家这个步骤里
客户端需要做三件事情:
1.当前客户端创建角色,发送告知服务器端
2.当前客户端接收服务器发送的其它在线客户端的玩家角色信息
3.其它客户端接收服务器发送的当前客户端的角色信息
服务器端需要做两件事情:
1.告诉当前客户端 其它在线客户端的信息,让当前客户端创建其它客户端玩家角色
2.告诉其它客户端 有新的客户端加入,需要创建新客户端玩家角色
六、Unity客户端玩家的位移同步
位移同步的思路
一、客户端每隔指定时间向服务器端发送位置信息
二、服务器接收并记录该客户端的位置信息
三、服务器每隔指定时间广播给所有在线客户端发送所有客户端的位置信息
四、客户端接收服务器发送的所有客户端位置信息,并修改他们位置
客户端位移同步
一、客户端每隔指定时间给服务器发送位置信息,实现位置实时更新
二、在客户端监听服务器发送的所有客户端位置信息,在场景中同步其它玩家位置
服务器端位移同步
一、接收客户端发来的位置信息
二、记录每个客户端的用户名,坐标
三、开启线程,每隔指定时间向所有在线客户端发送所有客户端的位置信息
using MyGameServer.Common;
using Photon.SocketServer;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Serialization;
namespace MyGameServer.Threads
{
class SyncPositionThread
{
private Thread t;
//启动线程的方法
public void Run()
{
t = new Thread(UpdataPosition);//UpdataPosition表示线程要启动的方法
t.IsBackground = true;//后台运行
t.Start();//启动线程
}
private void UpdataPosition()
{
Thread.Sleep(5000);//开始的时候休息5秒开始同步
while (true)//死循环
{
Thread.Sleep(100);//没隔0.1秒同步一次位置信息
//进行同步
SendPosition();
}
}
//把所有客户端的位置信息发送到各个客户端
//封装位置信息,封装到字典里,然后利用Xml序列化去发送
private void SendPosition()
{
//装载PlayerData里面的信息
List<PlayerData> playerDatraList = new List<PlayerData>();
foreach (ClientPeer peer in MyGameServer.Instance.peerList)//遍历所有客户段
{
if (string.IsNullOrEmpty(peer.username) == false)//取得当前已经登陆的客户端
{
PlayerData playerdata = new PlayerData();
playerdata.Username = peer.username;//设置playerdata里面的username
playerdata.x = peer.x;//设置playerdata里面的Position
playerdata.y = peer.y;
playerdata.z = peer.z;
playerDatraList.Add(playerdata);//把playerdata放入集合
}
}
//进行Xml序列化成String
StringWriter sw = new StringWriter();
XmlSerializer serializer = new XmlSerializer(typeof(List<PlayerData>));
serializer.Serialize(sw, playerDatraList);
sw.Close();
string playerDataListString = sw.ToString();
Dictionary<byte, object> data = new Dictionary<byte, object>();
data.Add(1, playerDataListString);//把所有的playerDataListString装载进字典里面
//把Xml序列化的信息装在字典里发送给各个客户端
foreach (ClientPeer peer in MyGameServer.Instance.peerList)
{
if (string.IsNullOrEmpty(peer.username) == false)
{
EventData ed = new EventData((byte)EventCode.SyncPosition);
ed.Parameters = data;
peer.SendEvent(ed, new SendParameters());
}
}
}
//关闭线程
public void Stop()
{
t.Abort();//终止线程
}
}
}
private SyncPositionThread syncPositinThread = new SyncPositionThread();
protected override void Setup()
{
Instance = this;
syncPositinThread.Run();
// 日志的初始化(定义配置文件log4net位置)
// Path.Combine 表示连接目录和文件名,可以屏蔽平台的差异
// Photon: ApplicationLogPath 就是配置文件里面路径定义的属性
//this.ApplicationPath 表示可以获取photon的根目录,就是Photon-OnPremise-Server-SDK_v4-0-29-11263\deploy这个目录
// 这一步是设置日志输出的文档文件的位置,这里我们把文档放在Photon-OnPremise-Server-SDK_v4-0-29-11263\deploy\bin_Win64\log里面
log4net.GlobalContext.Properties["Photon:ApplicationLogPath"] = Path.Combine(Path.Combine(Path.Combine(this.ApplicationRootPath, "bin_win64")), "log");
//this.BinaryPath表示可以获取的部署目录就是目录Photon-OnPremise-Server-SDK_v4-0-29-11263\deploy\MyGameServer\bin
FileInfo configFileInfo = new FileInfo(Path.Combine(this.BinaryPath, "log4net.config"));// 告诉log4net日志的配置文件的位置
// 如果这个配置文件存在
if (configFileInfo.Exists)
{
LogManager.SetLoggerFactory(Log4NetLoggerFactory.Instance);// 设置photon我们使用哪个日志插件
XmlConfigurator.ConfigureAndWatch(configFileInfo);// 让log4net这个插件读取配置文件
}
log.Info("Setup Completed!");// 最后利用log对象就可以输出了
AddHandler();
}
// server端关闭的时候
protected override void TearDown()
{
syncPositinThread.Stop();
RemoveHandler();
log.Info("关闭了服务器");
}
相关博客参考:
利用Photon实现实时联网对战(一)https://www.pianshen.com/article/45091657315/
利用Photon实现实时联网对战(二)PUN SDK介绍 https://www.pianshen.com/article/24311640962/
photon sever实战 http://www.u3d8.com/?cat=324
使用photon sever 自建服务器https://www.pianshen.com/article/17951701363/