原文地址:blog.liujunliang.com.cn
上篇文章介绍的PhotonServer实现在MySQL登录验证请求
本文继续上篇文章开发,通过PhotonServer来实现一个MMO Demo游戏(持续发送数据包)
以下是我对网络游戏开发编程的总结:
一、将需要在各个客户端一起显示的数据都发送到服务器端上(主角的移动、动作、技能特效等)
二、在PhotonServer中,一个客户端对应一个ClientPeer对象(所以可以将一个主角的信息作为ClientPeer一个属性存储起来)
三、在服务器端将连接进来的客户端有一个容器保存起来,待方便之后的逻辑处理(网络游戏的主角之间的逻辑部分在服务器端开发)
接下来一步步介绍MMO Demo开发流程,通过这么一个小小的案列,相信以后对网络游戏开发有深刻体会(学习下本系列文章前面的内容)!
(一)、将主角的移动位置数据发送到服务器端
主角的操作
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayControl : MonoBehaviour
{
[SerializeField]
private GameObject mLocalPlayer;
//上传位置信息请求
private PositionRequest mUpdatePositionRequest;
//创建主角的请求
private AddPlayerRequest mAddPlayerRequest;
//存储上一帧主角位置
private Vector3 mLastPosition = default(Vector3);
//计时器
private float mTimer = 0;
private float mTime = 0.2f;
private void Start()
{
//将本地主角的进行表示(颜色设置为红色)
mLocalPlayer.GetComponent<Renderer>().material.color = Color.red;
mUpdatePositionRequest = GameObject.Find("GameRequest").GetComponent<PositionRequest>();
mAddPlayerRequest = GameObject.Find("GameRequest").GetComponent<AddPlayerRequest>();
mAddPlayerRequest.OnOperationRequest();
}
void Update ()
{
PosControl();
//将主角数据按一定时间上传到服务器
// InvokeRepeating("SendPosition", 5.0f, 2.0f);//不晓得为啥我的延时方法无效 导致大量的发包堵塞了缓存区
mTimer += Time.deltaTime;
if (mTimer >= mTime)
{
mTimer = 0;
SendPosition();
}
}
void PosControl()
{
float horizontal = Input.GetAxis("Horizontal");
float vertical = Input.GetAxis("Vertical");
mLocalPlayer.transform.Translate(new Vector3(vertical, 0, horizontal) * Time.deltaTime * 3.0f);
}
//将主角位置信息发送到服务器
void SendPosition()
{
if (mLastPosition == default(Vector3) || Vector3.Distance(mLocalPlayer.transform.position, mLastPosition) >= 0.1f)
{
mLastPosition = mLocalPlayer.transform.position ;
mUpdatePositionRequest.playerPosition = mLocalPlayer.transform.position;
mUpdatePositionRequest.OnOperationRequest();//向服务器发送数据包请求
}
}
}
在一定时间段内,持续向服务器发送数据包请求
using System.Collections;
using System.Collections.Generic;
using ExitGames.Client.Photon;
using UnityEngine;
using System.Xml.Serialization;
using System.IO;
public class PositionRequest : BaseRequest
{
[HideInInspector]
public Vector3 playerPosition { get; set; }
public override void OnOperationRequest()
{
Collective.Vector3Pos vector3Pos = new Collective.Vector3Pos() { posX = playerPosition.x, posY = playerPosition.y, posZ = playerPosition.z };
string vector3PosStr;
using (StringWriter sw = new StringWriter())
{
XmlSerializer xs = new XmlSerializer(typeof(Collective.Vector3Pos));
xs.Serialize(sw, vector3Pos);
vector3PosStr = sw.ToString();
}
GameContext.GetInstance.peer.OpCustom(
(byte)this.operationMode,
new Dictionary<byte, object> { { (byte)Collective.ParameterMode.VECTOR3POS, vector3PosStr }},
true
);
}
public override void OnOperationResponse(OperationResponse operationResponse)
{
//将位置信息上传请求 无需做出响应
}
public override void Awake()
{
base.Awake();
this.operationMode = Collective.OperationMode.UPDATEPOS;
GameContext.GetInstance.AddRequest(this);
}
}
在服务器端接收请求,并将把位置信息更新存储起来
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Photon.SocketServer;
using System.Xml.Serialization;
using System.IO;
namespace MyGameServer.Request
{
class PositionRequest : BaseRequest
{
public PositionRequest()
{
this.operationMode = Collective.OperationMode.UPDATEPOS;
}
public override void OnOperationRequest(OperationRequest operationRequest, SendParameters sendParameters, ClientPeer peer)
{
MyGameServer.LOG.Info("获得位置数据");
//获取数据
Dictionary<byte, object> parameters = operationRequest.Parameters;
string vector3PosStr = (string)parameters.FirstOrDefault(q => q.Key == (byte)Collective.ParameterMode.VECTOR3POS).Value;
Collective.Vector3Pos vector3Pos;
//反序列化
vector3Pos = Collective.XMLTool.XMLDeserialze<Collective.Vector3Pos>(vector3PosStr);
MyGameServer.LOG.Info("主角位置:" + vector3Pos.posX + " ," + vector3Pos.posY + " ," + vector3Pos.posZ);
//设置主角位置信息
peer.mVector3Pos = vector3Pos;
}
}
}
(二)、主角登录后(在服务器端使用容器将客户端保存),此时发送一个请求,检测除主角外其他游戏客户端
检测到在之前有其他游戏客户端连接进服务器
将回发给客户端创建响应:告诉客户端有之前有其他玩家登录到了游戏场景,需要在场景创建其他角色;(注:一个场景创建多个角色)
并且将之前的游戏客户端上发送事件:告诉其他客户端,有个游戏主角加入了,需要在场景中创建该角色;(注:多个场景创建一个角色)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Photon.SocketServer;
namespace MyGameServer.Request
{
public class AddPlayerRequest : BaseRequest
{
public AddPlayerRequest()
{
this.operationMode = Collective.OperationMode.ADDPLAYER;
}
public override void OnOperationRequest(OperationRequest operationRequest, SendParameters sendParameters, ClientPeer peer)
{
/*********************将之前已经登录了的玩家查询出来,传给客户端*************************/
List<string> nameList = new List<string>();
//查询全部在线玩家
foreach (var tempPeer in MyGameServer.peerList)
{
if (!string.IsNullOrEmpty(tempPeer.mName) && tempPeer != peer)
{
//添加其他已经登入的玩家名
nameList.Add(tempPeer.mName);
}
}
MyGameServer.LOG.Info("需要添加的玩家数:" + nameList.Count);
//将玩家名字列表序列化
string nameListStr = Collective.XMLTool.XMLSerialize<List<string>>(nameList);
Dictionary<byte, object> parameter = new Dictionary<byte, object> { { (byte)Collective.ParameterMode.NAMELIST, nameListStr } };
OperationResponse response = new OperationResponse((byte)this.operationMode);
response.Parameters = parameter;
peer.SendOperationResponse(response, sendParameters);
/*************************************************************************************/
/************************将之前登录的玩家客户端上添加本Peer***************************/
foreach (var tempPeer in MyGameServer.peerList)
{
if (!string.IsNullOrEmpty(tempPeer.mName) && tempPeer != peer)
{
//其他已经上线了的客户端
EventData ed = new EventData((byte)Collective.EventModel.ADDPLAYER);
ed.Parameters = new Dictionary<byte, object> { { (byte)Collective.ParameterMode.PLAYNAME, peer.mName } };
tempPeer.SendEvent(ed, new SendParameters());
}
}
/************************************************************************************/
}
}
}
在客户端中接收响应,将创建游戏对象
using System.Collections;
using System.Collections.Generic;
using ExitGames.Client.Photon;
using UnityEngine;
using System.Linq;
public class AddPlayerRequest : BaseRequest
{
public override void Awake()
{
base.Awake();
this.operationMode = Collective.OperationMode.ADDPLAYER;
GameContext.GetInstance.AddRequest(this);
}
public override void OnOperationRequest()
{
//告诉服务器开始创建有一个客户端进来的
//在真正的游戏开发当中,一般会传用户名到服务器中,在登录的时候已经存储下来了
GameContext.GetInstance.peer.OpCustom((byte)this.operationMode, null, true);
}
public override void OnOperationResponse(OperationResponse operationResponse)
{
string nameListStr = (string)operationResponse.Parameters.FirstOrDefault(q => q.Key == (byte)Collective.ParameterMode.NAMELIST).Value;
//if (!string.IsNullOrEmpty(nameListStr))
{
//反序列化
List<string> nameList = Collective.XMLTool.XMLDeserialze<List<string>>(nameListStr);
foreach (var name in nameList)
{
if (!string.IsNullOrEmpty(name) && !string.Equals(name, GameContext.GetInstance.mLocalPlayerName))
{
//其他已经上线的玩家
//此时需要在场景中创建玩家
GameContext.GetInstance.CreatePlayer(name);
}
}
}
}
}
在其他游戏客户端接收到了事件,创建游戏角色
using ExitGames.Client.Photon;
using System.Linq;
using UnityEngine;
public class AddPlayerEvent : BaseEvent
{
public override void Awake()
{
base.Awake();
this.eventModel = Collective.EventModel.ADDPLAYER;
GameContext.GetInstance.AddEvent(this);
}
public override void OnEvent(EventData eventData)
{
Debug.Log("添加事件响应");
string name = (string)eventData.Parameters.FirstOrDefault(q => q.Key == (byte)Collective.ParameterMode.PLAYNAME).Value;
GameContext.GetInstance.CreatePlayer(name);
}
}
(三)、创建线程,实时更新各个客户端各个主角的位置信息
在有了之前步骤二,能够在各个客户端创建已经登录加入场景的游戏角色后
接下来就是实时更新游戏对象移动位置信息
思路是在服务器端主类里创建开启一个线程,持续不断地往各个客户端发送全部角色的位置信息的事件
服务器线程的创建与事件的派发
using Photon.SocketServer;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace MyGameServer.Thread
{
public class UpdatePlayersPosThread : BaseThread
{
public override void Start()
{
base.Start();
}
protected override void Run()
{
while (true)
{
//没0.2秒刷新位置数据
System.Threading.Thread.Sleep(200);
UpdatePlayerPos();
}
}
public override void Stop()
{
//终止线程
this.mThread.Abort();
}
private void UpdatePlayerPos()
{
//由于字典本身无法序列化 所以用一个对象来存储名字和位置信息
List<Collective.PlayerVector3Pos> playerPosList = new List<Collective.PlayerVector3Pos>();
//将其他在线的主角位置信息打包序列化
foreach (var tempPeer in MyGameServer.peerList)
{
if (!string.IsNullOrEmpty(tempPeer.mName) && tempPeer.mVector3Pos != null)
{
playerPosList.Add(new Collective.PlayerVector3Pos { name = tempPeer.mName, pos = tempPeer.mVector3Pos });
}
}
//序列化
string playerPosListStr = Collective.XMLTool.XMLSerialize<List<Collective.PlayerVector3Pos>>(playerPosList);
Dictionary<byte, object> parameters = new Dictionary<byte, object> { { (byte)Collective.ParameterMode.VECTOR3POSLIST, playerPosListStr } };
//向各个客户端发送位置数据
foreach (var tempPeer in MyGameServer.peerList)
{
if (!string.IsNullOrEmpty(tempPeer.mName))
{
EventData ed = new EventData((byte)Collective.EventModel.UPDATEPOS);
ed.Parameters = parameters;
tempPeer.SendEvent(ed, new SendParameters());
}
}
}
}
}
客户端处理事件响应
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ExitGames.Client.Photon;
using UnityEngine;
class UpdatePosEvent : BaseEvent
{
public override void Awake()
{
base.Awake();
this.eventModel = Collective.EventModel.UPDATEPOS;
GameContext.GetInstance.AddEvent(this);
}
public override void OnEvent(EventData eventData)
{
//取得数据
string playerPosListStr = (string)eventData.Parameters.FirstOrDefault(q => q.Key == (byte)Collective.ParameterMode.VECTOR3POSLIST).Value;
if (!string.IsNullOrEmpty(playerPosListStr))
{
//反序列化
List<Collective.PlayerVector3Pos> playerPosList = Collective.XMLTool.XMLDeserialze<List<Collective.PlayerVector3Pos>>(playerPosListStr);
//根据名字找到匹配的游戏对象 ,将服务器传过来的最新位置赋值上去
foreach (var playerPos in playerPosList)
{
if (!string.IsNullOrEmpty(playerPos.name))
{
GameObject go = GameContext.GetInstance.playerDic.FirstOrDefault(q => q.Key == playerPos.name).Value;
if (go)
{
if (playerPos.pos == null)
{
Debug.LogError("位置信息为空");
}
go.transform.position = new UnityEngine.Vector3(playerPos.pos.posX, playerPos.pos.posY, playerPos.pos.posZ);
}
}
}
}
}
}
会发现在游戏各个客户端可以实时更新角色位置信息!!
这系列文章的源码博主以全部上传,大家可以下载下来学习!!
如果本系列文章对你有帮助!
点击以下链接关注博文,更多内容持续更新!