multithreading 多线程
non-blocking I/O 非阻塞
将套接字设置为非阻塞模式,游戏可以检查每个帧以查看是否存在
数据已准备好接收。 如果有数据,则游戏处理第一个待处理数据报。
如果没有,游戏立即移动到框架的其余部分而不等待。 如果你
想要处理的不仅仅是第一个数据报,你可以添加一个读取挂起的循环
数据报,直到它已读取最大数字,或者没有更多数据。 这很重要
限制每帧读取的数据报数量。 如果不这样做,恶意客户端可以发送
大量的单字节数据报比服务器可以处理它们更快,有效地停止了
服务器模拟游戏。
void DoGameLoop()
{
UDPSocketPtr mySock = SocketUtil::CreateUDPSocket(INET);
mySock->SetNonBlockingMode(true);
while(gIsGameRunning)
{
char data[1500];
SocketAddress socketAddress;
int bytesReceived = mySock->ReceiveFrom(data, sizeof(data),
socketAddress);
if(bytesReceived> 0)
{
ProcessReceivedData(data, bytesReceived, socketAddress);
}
DoGameFrame();
}
}
the select function.
套接字库提供了一种检查许多方法
一次插座,并在其中任何一个准备好后立即采取行动。为此,请使用
选择功能:
int select(int nfds,fd_set * readfds,fd_set * writefds,fd_set * exceptfds,
const timeval * timeout);
游戏必须格式化对象的数据,以便它可以由a发送传输层协议。缓冲区
LoginPanelController 脚本 (改为绑定到LoginPanel?)
Start函数:使用 PlayerPrefs 类 在本地保存玩家的昵称;SetLoginPanelActive函数:roomPanel!=null?
Update 函数:不断更新 Unity 客户端 与 Photon 服务器的连接状态。 我们可以使用条件编译指令 让 Update 函数只在 Unity 编辑器中编译运行
ClickLoginButton 函数:启用游戏大厅面板。使用 PhotonNetwork .ConnectUsingSettings 函数连接 Photon 服务器。 函数的参数 1.0 表示游戏版本标识符, 不同游戏版本标识符表示不同的游戏。()
LobbyPanelController 脚本:(继承自PunBehaviour)
OnEnable 函数: 初始化房间页数信息,启用游戏大厅加载的提示信息,禁用游戏房间加载的提示信息,获取所有房间信息的条目。(去掉roomMessagePanel,在 ShowRoomMessage函数 直接使用roomMessagePanel控件 )并且禁用这些房间信息条目。roomInfo用于和PUN沟通
最后移除返回按钮绑定的所有事件处理函数。(在本例中没有) 我们给它绑定新的事件处理函数(即按下该按钮后有哪些函数被调用)
覆写 OnReceivedListUpdate 回调函数:当游戏大厅的房间信息发生变化时该函数会被调用, 更新游戏大厅中房间列表的显示。 首先获取游戏大厅中的房间列表计算房间的页数,更新房间页数信息的显示 。调用 ButtonControl 函数 控制上一页和下一页翻页按钮的显示 接着调用 ShowRoomMessage 在房间信息面板中,依次显示游戏大厅的房间信息 最后,根据当前房间的个数 判断是否启用随机进入房间按钮的交互功能
CreateRoomController 脚本
RoomPanel 游戏房间面板显示房间内玩家的信息 游戏房间内有且仅有一名房主,也就是 PUN 中的 MasterClient 。它通常是创建游戏房间的玩家。 当创建游戏房间的玩家退出房间时Photon 服务器将指定房间的另一名玩家作为新的 MasterClient 。
在游戏房间面板中,玩家点击切换队伍按钮,选择自己所在的队伍 非 MasterClient 玩家点击准备按钮,切换自己的准备状态 所有玩家进入准备状态后,MasterClient 点击开始游戏按钮,所有玩家进入游戏战斗场景。
新建1个Photon命名空间的Hashtable类的对象并设置需要修改的状态, 使用 PhotonNetwork.Player.SetCustomProperties函数设置本客户端玩家的自定义属性
OnEnable 回调函数:
更新返回按钮的功能
使用 PhotonNetwork.room.name 获取房间的名称,
清空玩家操作未成功的提示信息 promptMessage。 DisableTeamPanel 函数 禁用队伍面板中所有的玩家信息控件 再调用 UpdateTeamPanel 函数,在队伍面板中更新(远程)玩家信息。本地玩家查找队伍面板中空缺的位置,把自己的昵称和状态填入该位置 接下来设置玩家的自定义属性。
调用 ReadyButtonControl 函数 设置本地客户端的 ReadyButton 按钮的功能。
ClickStartGameButton函数: 使用 RPC 方法 让房间内所有玩家加载游戏战斗场景 GameScene
ClickReadyButton 函数:对玩家的自定义属性 isReady 进行取反操作 切换玩家的准备状态。
ClickSwitchButton 函数:
首先检查玩家的自定义属性 isReady 。如果玩家 isReady 属性为 true,在 PromptMessage 提示信息框中显示 准备状态下不能切换队伍,使用 return 退出函数 。
如果玩家 isReady 属性为 false,遍历另一支队伍的 队伍信息面板,判断另一支队伍是否满员 如果另一支队伍未满员,就更改玩家的队伍属性 把 isSwitch 变量设置为 true,表示已切换队伍,如果另一支队伍已满员 则 isSwitch 变量仍为 false 在
最后读取 isSwitch 的变量的值,判断玩家是否成功切换队伍 如果切换失败,PromptMessage 显示另一支队伍已满,无法切换 如果切换成功,清空 PromptMessage 中的提示信息
这里如果你无法连接到 Photon 服务器 可能的原因是你的路由器为你的电脑分配了 新的 IP 地址。 你可以按照我们之前讲过的方法 在 PhotonControl 和 PhotonServerSettings中重新设置正确的 IP 地址
关于 PUN 的更多功能 感兴趣的同学可以打开 PUN 资源包中的 PhotonNetwork.Documentation.CHM 文件 阅读官方的说明文档,
当客户端加载战斗场景 GameScene 完毕后 游戏状态为 PreStart
使用 RPC 方法 调用所用客户端的 ConfirmLoad 方法 通知所有客户端自己已经完成场景的加载,
客户端初始化竞技队伍与玩家的得分信息,调用 UpdateScores 函数 更新玩家得分榜
UpdateScores 函数
首先禁用所有显示玩家得分榜的条目。
其次获取每位玩家的当前得分 Score ,保存在对应的 team 中 进行排序
调用 UpdateRealTimeScorePanel 函数,更新实时分数面板
让 MasterClient 负责游戏开始倒计时 MasterClient调用所有客户端RPC 函数 SetTime
传入两个参数,第一个是 Photon 服务器时间,PhotonNetwork.time 由 Photon 服务器决定 对于所有客户端是统一的,若使用 MasterClient 本地时间会造成客户端时间不统一
第二个是游戏开始倒计时时间,checkplayerTime
Update 函数
使用倒计时结束时间 EndTimer 减去服务器时间 PhotonNetwork.time 得到倒计时剩余时间 countDown
由于设置的游戏时长可能大于服务器的时间上限photonCircleTime,
通过循环确保倒计时剩余时间 countDown 的正确性。
调用 UpdateTimeLabel 函数使用 TimeLabelText 控件显示 countDown 的值
根据当前游戏状态做出不同的动作
如果当前游戏状态为 PreStart,Master Client 调用函数 CheckPlayerConnected 检查游戏开始条件是否已经满足。第一游戏开始倒计时 已经结束。
使所有客户端都调用 GameManager 的 RPC 函数 StartGame 执行游戏开始之前的准备工作
StartGame 函数: 设置游戏开始与结束时间。 其次 将游戏状态切换至 Playing 游戏进行状态。 接着,客户端调用 InstantiatePlayer 函数生成玩家对象 最后播放游戏开始音效,表示游戏开始。
InstantiatePlayer 函数:生成玩家对象
根据该属性实例化玩家预制件 EthanPlayer 或者RobotPlayer 根据玩家所在的队伍以及他在队伍中的编号,设置玩家对象的生成位置
InstantiatePlayer 函数 :
客户端生成玩家对象后,首先启用该玩家对象的 PlayerMove 脚本 表示客户端可以控制该玩家对象的移动,
其次获取该玩家对象的 PlayerHealth 脚本初始化显示玩家生命值血条的 Slider 控件
将场景中的摄像机设为玩家对象的子对象 让摄像机跟随玩家一起移动、 旋转,最后将玩家手中的枪械设为摄像机的子对象 使枪械始终显示在玩家视野范围内
- 在 project 视图中创建 resources 文件夹
- 将 prefabs 文件夹中的 RobotPlayer 和 EthanPlayer 拖放置 resources 文件夹中
- 把 RobotPlayer 和 EthanPlayer 拖入场景 ,把二者的标签 tag,设置为 Player
- 给 RobotPlayer 和 EthanPlayer 对象添加 Photon View 组件 , PlayerMove 脚本,PlayerHealth 脚本和 PlayerShoot 脚本,先禁用 PlayerMove 脚本和 PlayerShoot 脚本
- 添加 IK Controller 组件 并且设置参数
- 点击 apply 将修改应用到预置键
- 删除场景中的 RobotPlayer 和 EthanPlayer ,
- 把 Prefabs 文件夹中的 SpawnPositions 预置键拖入场景
Disabled 表示不使用插值算法 直接使用接收到的位置数据替换对象当前的位置数据
Fixed Speed,表示以固定的速度实现插值的计算。
Estimated Speed 根据最近几次接收到的位置信息,估算对象的移动速度 根据估算得到的对象移动速度,实现插值计算。 Synchronize Values 发送对象的实际速度与对象的实际速度实现插值计算 这种方法的网路开销比较大。
Lerp 表示使用先快后慢的速度完成插值计算 对象当前位置与目标位置较远时,使用较快的速度靠近目标位置 对象当前位置与目标位置较近时,使用较慢的速度靠近目标位置。
Rotation 与 Scale 的同步 只包含内插选项
不勾选 Synchronize Scale 表示不同步玩家对象的 Scale 属性。
把 RobotPlayer 和 EthanPlayer 预制件的 Transform 组件 拖入各自的 Photon View 的 Observed Component 属性中
动画和枪械的同步
游戏过程中 通过更改玩家对象的动画控制器参数,实现动画状态转移 完成玩家动画的切换。
创建玩家对象的客户端需要将 玩家动画参数以一定频率发送给其他客户端,其他客户端接收到动画参数后 在各自的场景中更新该玩家对象的动画参数
玩家动画状态的同步可以使用 OnPhotonSerializeView 函数来实现 该函数通过网络以一定频率发送游戏对象的动画参数 其他客户端接收到数据后,更改相应游戏对象的动画参数
Pun 包含了一个 PhotonAnimatorView 组件 它可以帮助开发者同步决策对象的动画状态 自动访问游戏对象的 Animator 组件,方便开发者设置动画参数的传递方式
它包含动画层权重的同步和动画参数的同步 它提供了三种同步方式,分别为 Disabled、 Discrete 和 Continuous 其中,Disabled 表示不同步数据 Discrete 表示离散,它采用直接替换数据的方式完成同步 适用于 bool 和 Trigger 类型动画参数的同步。 Continuous 表示连续 是用差值的方式,平滑改变参数,适用于动画层权重、 int 和 float 类型动画参数的同步
在《慕课英雄 2》的项目中,我们不需要同步动画层权重 因此我们将 Layer 0 的同步方式设置为 Disabled,玩家的动画参数均为 bool 类型 我们将动画参数的传递方式设置为 Discrete 最后我们把 Photon Animator View 组件拖到 Photon View 组件的 Observed Components 的属性中 完成玩家动画状态的同步 本页 PPT 给出 当本地客户端的玩家对象将枪口朝向天空时 其他客户端如果无法同步玩家对象的枪械参数 玩家持枪姿势存在很大差异。 下面我们讲解如何实现玩家手中枪械位置与朝向的同步 首先,我们对比本地客户端 的玩家对象与其他客户端的玩家对象在结构方面的差异 本地客户端的 Gun 对象位于 Main Camera 下 Gun 对象跟随它的父对象 Main Camera 进行移动和旋转,因此 Gun 的局部坐标保持不变 Photon Transform View 同步的是游戏对象的局部坐标 枪械的位置与朝向采用游戏场景的全局坐标来描述 因此不能使用 Photon Transform View 组件对它们进行同步 在《慕课英雄 2》项目中,我们使用 OnSerializeView 函数 自己编写差值算法,实现枪械位置与朝向的同步 我们在 GunController 脚本中实现枪械位置与朝向的同步 首先定义私有变量 m_position 和 m_rotation 勇于接受枪械的位置和朝向数据。
定义私有变量 lerpSpeed 表现插值速度 [空白_录音] 在 OnPhotonSerializeView 函数中 我们实现枪械位置与朝向数据的发送和接收。 玩家对象的 owner 通过网络发送枪械的位置与朝向数据 其他客户端接收该枪械的位置与朝向数据 保存在私有变量 m_position 和 m_rotation 变量中 位置与朝向数据均采用全局坐标来描述 我们在 Update 函数中实现枪械位置与朝向的平滑同步 使用 PhotonView.isMine 判断是否为本地玩家 如果是本地玩家,无需同步枪械的位置与朝向 如果是其他客户端,使用 Lerp 函数 实现枪械从当前状态向目标状态的平滑过渡 最后,我们给 EthanPlayer 与 RobotPlayer 预制件的 Gun 子对象添加 Gun Controller 脚本与 Photon View 组件 将 Gun Controller 脚本添加到 Photon View 组件的 Observed Component 属性中 完成枪械位置与朝向的同步 下一节我们讲解玩家的射击逻辑 最后进入演示环节 演示环节的内容包括给玩家对象添加 PhotonAnimatorView 组件 实现玩家动画状态的同步,给玩家枪械绑定 GunController 脚本 实现枪械位置和朝向的同步