Skip to content

Latest commit

 

History

History
571 lines (287 loc) · 40.3 KB

File metadata and controls

571 lines (287 loc) · 40.3 KB

第一章 网络游戏的开端

  1. Socket

    网络上的两个程序通过一个双向的通信连接实现数据交换,这个 连接的一端称为一个Socket。一个Socket包含了进行网络通信必需的 五种信息:连接使用的协议、本地主机的IP地址、本地的协议端口、 远程主机的IP地址和远程协议端口(如图1-6所示)。如果把Socket理 解成一台手机,那么本地主机IP地址和端口相当于自己的手机号码, 远程主机IP地址和端口相当于对方的号码。至少需要两台手机才能打 电话,同样地,至少需要两个Socket才能进行网络通信。

    0
  2. TCP、UDP

    从概念上讲,TCP是一种面向连接的、可靠的、基于字节流的传输 层通信协议,与TCP相对应的UDP协议是无连接的、不可靠的、但传输 效率较高的协议。在本章的语义中,“Socket通信”特指使用TCP协议 的Socket通信。 也许能够以寄快递的例子解释不同协议的区别。有些快递公司收 费低,对快递员的要求也低,丢件的事情频频发生;有些公司收费 高,但要求快递员在每个节点都做检查和记录,丢件率很低。不同快 递公司有着不同的行为规则,有的奉行低价优先,有的奉行服务至 上。TCP、UDP协议对应不同快递公司的行为规则。它们的目的都是将 数据发送给接收方,但使用的策略不同:TCP注重传输的可靠性,确保 数据不会丢失,但速度慢;UDP注重传输速度,但不保证所有发送的数 据对方都能够收到。至于孰优孰劣,得看具体的应用场景。游戏开发 最常用的是TCP协议,所以本书也以TCP为主。

第二章 分身有术:异步和多路复用

  1. 异步

    在Start方法中创建一个定时器对象timer(定时器Timer类位于 System.Threading命名空间内)。Timer类的构造函数有4个参数:第 一个参数TimeOut代表回调函数,即打印“铃铃铃”的方法;第三个参 数5000代表5000毫秒,即5秒;另外两个参数暂不需要关心。整个程序 的功能就是开启定时器,5秒后回调TimeOut方法,打印“铃铃铃”。 这种方法称为异步,它指进程不需要一直等下去,而是继续往下 执行,直到满足条件时才调用回调函数,这样可以提高执行的效率。

    异步的实现依赖于多线程技术。在Unity中,执行 Start、Update方法的线程是主线程,定时器会把定时任务交给另外的 线程去执行,在等待5秒后,“另外的某条线程”调用回调函数。主线 程继续往下执行代码,不受影响。

    1
  2. 状态检测Poll

    Poll方法将会检查Socket的状态。如果指定mode参数为 SelectMode.SelectRead,则可确定Socket是否为可读;指定参数为 SelectMode.SelectWrite,可确定Socket是否为可写;指定参数为 SelectMode.SelectError,可以检测错误条件。Poll将在指定的时段 (以微秒为单位)内阻止执行,如果希望无限期地等待响应,可将 microSeconds设置为一个负整数;如果希望不阻塞,可将 microSeconds设置为0。

    *在阻塞方法前加上一层判断,有数据可读才调用Receive,有 数据可写才调用Send,那不就既能够实现功能,又不会卡住程序了 *

  3. 多路复用Select

    多路复用,就是同时处理多路信号,比如同时检测多个 Socket的状态。同时检测多个 Socket的状态。在设置要监听的Socket列表后,如果有一个(或多 个)Socket可读(或可写,或发生错误信息),那就返回这些可读的 Socket,如果没有可读的,那就阻塞。

    Select可以确定一个或多个Socket对象的状态,如图所示。 使用它时,须先将一个或多个套接字放入IList中。通过调用 Select(将IList作为checkRead参数),可检查Socket是否具有可读 性。若要检查套接字是否具有可写性,可使用checkWrite参数。若要 检测错误条件,可使用checkError。在调用Select之后,Select将修 改IList列表,仅保留那些满足条件的套接字。如图2-13所示,把包含 6个Socket的列表传给Select,Select方法将会阻塞,等到超时或某个 (或多个)Socket可读时返回,并且修改checkRead列表,仅保存可读 的socket A和socket C。当没有任何可读Socket时,程序将会阻塞, 不占用CPU资源。

    2

第三章 实践出真知:大乱斗游戏

  1. 角色控制

    可以设计图所示的类结构,其基类BaseHuman是基础的角色 类,它处理“操控角色”和“同步角色”的一些共有功能;CtrlHuman 类代表“操控角色”,它在BaseHuman类的基础上处理鼠标操控功能; SyncHuman类是“同步角色”类,它也继承自BaseHuman,并处理网络同步(如果有必要)。

    2
  2. 网络模块

    在实际的网络游戏开发中,网络模块往往是作为一个底 层模块用的,它应该和具体的游戏逻辑分开,而不应该把处理逻辑的 代码(例如之前给recvStr赋值)写到ReceiveCallback里面去,因为 ReceiveCallback应当只处理网络数据,不应该去处理游戏功能。一个 可行的做法是,给网络管理类添加回调方法,当收到某种消息时就自 动调用某个函数,这样便能够将游戏逻辑和底层模块分开。

    2
  3. 进入游戏:Enter协议

    当玩家打开游戏,客户端程序会生成一个操控角色 (CtrlHuman),并把它放到场景中的一个随机位置。然后发送一条 Enter协议给服务端,包含了对玩家的描述、位置等信息。服务端将 Enter协议广播出去,其他客户端收到Enter协议后,创建一个同步角 色(SyncHuman)

    2 2
  4. 玩家列表:List协议

    客户端发送和接收List协议的代码如下所示,它解析参数后,生 成一个同步角色。

    2
  5. 移动同步:Move协议

    当玩家用鼠标点击场景,角色移动时,客户端应把目的地位置发 送给服务端。服务端一方面记录位置信息,另一方面将目的地位置信 息广播给其他客户端。其他客户端收到协议后,解析目的地位置信 息,然后控制SyncHuman走到对应的位置去。

    2
  6. 玩家离开:Leave协议

    当某个客户端掉线,服务端会广播Leave协议,客户端收到后删除 对应的角色。

    2
  7. 攻击动作:Attack协议

    Attack协议设计如图所示。它带有两个参数,第一个参数为 角色描述,第二个参数为攻击的方向。在CtrlHuman发起攻击动作后, 将Attack协议发送给服务端

    2
  8. 攻击伤害:Hit协议

    当玩家发起进攻,且打击到敌人时,敌人会受到伤害。假设不会 有玩家作弊,服务端完全信任客户端,一种可能的实现方式是,当攻 击到敌人时,攻击方发送Hit协议,如图所示,协议中带有被攻击 者的信息。服务端收到协议后,扣除被攻击角色的血量。

    2
  9. 角色死亡:Die协议

    当角色死亡时,服务端会广播Die协议(图3-57),客户端收到协 议后删除该角色。

    2

第四章 正确收发数据流

  1. TCP数据流:粘包半包现象

    在聊天软件中,客户端依次 发送“Lpy”和“_is_handsome”,期望其他客户端也展示出“Lpy” 和“_is_handsome”两条信息,但由于Receive可能把两条信息当作一 条信息处理,有可能只展示“Lpy_is_handsome”一条信息(如图所示)。Receive方法返回多少个数据,取决于操作系统接收缓冲区中 存放的内容

    2

    发送端发送的数据还有可能被拆分,如发送“HelloWorld”(如图所示),但在接收端调用Receive时,操作系统只接收到了部分 数据,如“Hel”,在等待一小段时间后再次调用Receive才接收到另 一部分数据“loWorld”。

    2
  2. 解决粘包问题的方法:长度信息法、固定长度法、结束符号法

    • 长度信息法

      长度信息法是指在每个数据包前面加上长度信息。每次接收到数 据后,先读取表示长度的字节,如果缓冲区的数据长度大于要取的字 节数,则取出相应的字节,否则等待下一次数据接收。

    • 固定长度

      每次都以相同的长度发送数据,假设规定每条信息的长度都为10 个字符,那么发送“Hello”“Unity”两条信息可以发送成 “Hello...”“Unity...”,其中的“.”表示填充字符,是为凑数, 没有实际意义,只为了每次发送的数据都有固定长度。

    • 结束符号法

      规定一个结束符号,作为消息间的分隔符。假设规定结束符号为 “$”,那么发送“Hello”“Unity”两条信息可以发送成“Hello$” “Unity$”。接收方每次读取数据,直到“$”出现为止,并且使用 “$”去分割消息。

  3. 大端小端

    大端:高位存低字节;小端相反

  4. 完整发送数据

    Send方法会把要发送的数据存入操作系统的发 送缓冲区,然后返回成功写入的字节数。这句话的另一层含义是,对 于那些没有成功发送的数据,程序需要把它们保存起来,在适当的时 机再次发送。

第五章 深入了解TCP,解决暗藏问题

  1. 从TCP到铜线:

    • 应用层

      应用层功能是应用程序(游戏程序)提供的功能。在给客户端发 送“hello”的例子中,程序把“hello”转化成二进制流传递给传输 层(传送给send方法,如图5-1所示)。操作系统会对二进制数据做一 系列加工,使它适合于网络传输。

    2
    • 传输层

      收到二进制数据后,传输层协议会对它做一系列加工,并提供数 据流传送、可靠性校验、流量控制等功能。依然想象一下寄信,在寄 出一封信后,为了确保对方一定会收到信件,人们可以约定如下的规则。

      1)加个确认机制,收到信件的人必须写回信,告诉对方收到了信 件。寄件人在收到回信后,可以确认对方一定收到了信件。

      2)信件寄出后,寄信人会等待回信。如果过一个月时间都没能收 到回信,说明信件很有可能丢失了,寄件人会重新写一封一模一样的 信,再次寄出,等待回信。如果三次重寄都没有回音,只能放弃,当 作对方不可能收到信件。

    • 网络层

      邮政系统并不是直达系统,当寄件人想要把信件从广州天河区寄 到北京西城区的时候,信件会先从天河区邮局发送到广州市邮局,再 由广州市邮局发送到北京市邮局,北京市邮局再发送到西城区邮局, 最后再由邮递员投递到指定地址。网络通信同理,数据包会经过一层 层传送,最终到达目的地(5.3.3节会有进一步介绍),所以网络消息 必须附带“寄件人地址”“收件人地址”等数据,方便“各地邮局” 投递。IP协议会给TCP数据添加本地地址、目的地地址等信息(如图所示)。

      2
    • 网络接口

      在多层处理后,数据通过物理介质(如电缆、光纤)传输到接收 方,接收方再依照相反的过程解析,得到用户数据。实际上,IP协议 还会被封装成更为底层的链路层协议,以完成数据校验等一些功能。

  2. 数据传输流程:

    • TCP连接的建立:三次握手

      2
    • TCP的数据传输

      2
    • TCP连接的终止:四次挥手

      2
  3. 常用TCP参数

    • ReceiveBufferSize

      ReceiveBufferSize指定了操作系统读缓冲区的大小,默认值是 8192

    • SendBufferSize

      SendBufferSize指定了操作系统写缓冲区的大小,默认值也是 8192

    • NoDelay

      指定发送数据时是否使用Nagle算法,对于实时性要求高的游戏, 该值需要设置成true。Nagle是一种节省网络流量的机制,默认情况 下,TCP会使用Nagle算法去发送数据。

    • TTL

      TTL指发送的IP数据包的生存时间值(Time To Live,TTL)。TTL 是IP头部的一个值,该值表示一个IP数据报能够经过的最大的路由器 跳数。

    • ReuseAddress

      ReuseAddress即端口复用,让同一个端口可被多个socket使用。 一般情况下,一个端口只能由一个进程独占,假设服务端程序都绑定 了1234端口,若开启两个服务端程序,虽然,第一个开启的程序能够 成功绑定端口并监听,但第二个程序会提示“端口已经在使用中”, 无法绑定端口。在计算机中,退出程序与释放端口并不同步。

    • LingerState

      LingerState的功能是设置套接字保持连接的时间。

  4. Close的恰当时机

    LingerState选项可以让程序在关闭连接前发完系统缓冲区中的数据,然而,这并不代表能将所有数据发出去。写入队列writeQueue保存要发送的数据,再逐 一发送。

  5. 心跳机制

    断开连接时,主动方会给对端发送FIN信号,开启4次挥手流程。 但在某些情况下,比如拿着手机进入没有信号的山区,更极端的,比 如有人拿剪刀把网线剪断。虽然断开了连接,但主动方无法给对端发 送FIN信号(网线剪断了还能干什么?),对端会认为连接有效,一直 占用系统资源。

    TCP有一个连接检测机制,就是如果在指定的时间内没有数据传 送,会给对端发送一个信号(通过SetSocketOption的KeepAlive选项 开启)。对端如果收到这个信号,回送一个TCP的信号,确认已经收 到,这样就知道此连接通畅。如果一段时间没有收到对方的响应,会 进行重试,重试几次后,会认为网络不通,关闭socket。

    游戏开发中,TCP默认的KeepAlive机制很“鸡肋”,因为上述的 “一段时间”太长,默认为2小时。一般会自行实现心跳机制。心跳机 制是指客户端定时(比如每隔1分钟)向服务端发送PING消息,服务端 收到后回应PONG消息。服务端会记录客户端最后一次发送PING消息的 时间,如果很久没有收到(比如3分钟),就假定连接不通,服务端会 关闭连接,释放系统资源。

第六章 通用客户端网络模块

  1. 网络模块设计:对外接口、内部设计

  2. 网络事件:事件类型、监听列表、分发事件

  3. 连接服务端:Connect、ConnectCallback、测试程序

  4. 关闭连接:isClosing、Close、测试

  5. Json协议:为什么会有协议类、使用JsonUtility、协议格式、协议文件、协议体的编码解码、协议名的编码解码

  6. 发送数据:Send、SendCallback、测试

  7. 消息事件

  8. 接收数据:新的成员、ConnectCallback、ReceiveCallback、OnReceiveData、Update、测试

  9. 心跳机制:PING和PONG协议、成员变量、发送PING协议、监听PONG协议、测试

  10. Protobuf协议:什么是Protobuf、编写proto文件、生成协议类、导入protobuf-net.dll、编码解码

第七章 通用服务端框架

  1. 服务端架构:总体架构

    服务端程序的两大核心是处理客户端的消息和存储玩家数据。下图展示的是最基础的单进程服务端结构,客户端与服务端通过TCP连 接,使两者可以传递数据;服务端还连接着MySQL数据库,可将玩家数 据保存到数据库中。

    2
  2. Json编码解码:添加协议文件、引用System.web.Extensions、修改MsgBase类、测试

  3. 网络模块:整体结构

    本章的服务端程序与第3章的服务端程序在结构上基本相似,是在第3章程序的基础上,添加粘包半包处理、协议解析、数据库存储等功能。除了协议解析相关,网络模块还分为4个部分:一是处理select多路复用的网络管理器NetManager,它是服务端网络模块的核心部件; 二是定义客户端信息的ClientState类,第3章的ClientState类相对简单,本章会继续完善它;三是处理网络消息的MsgHandler类,第3章中所有的消息处理都写在同一个文件里,但对于大型游戏来说,一个几十万行的文件不太容易编辑,本章会根据消息的类型,将MsgHandler 分拆到多个文件中(如BattleMsgHanler.cs专门处理战斗相关的协议,SysMsgHandler.cs专门处理MsgPing、MsgPong等系统协议);四是事件处理类EventHandler。 下图展示了服务端网络模块的整体结构,与第3章不同的是,程序引入了玩家列表,玩家登录后clientState会与player对象关联。通过判断clientState是否持有player对象即可判断客户端是处于“连接 但未登录”状态,还是处于“登录成功”状态。

    2
  4. 心跳机制:lastPingTime、时间戳、回应MsgPing协议、超时处理、测试程序

  5. 玩家的数据结构:完整的ClientState

    2
  6. 配置MySQL数据库:安装并启动MySQL数据库、安装Navicat for MySQL、配置数据表、安装connector、MySQL基础知识

  7. 数据库模块:连接数据库、防止SQL注入、IsAccountExist、Register、CreatePlayer、CheckPassword、GetPlayerData、UpdatePlayerData

  8. 登录注册功能:注册登录协议、记事本协议、注册功能、登录功能、退出功能、获取文本功能、保存文本功能、客户端界面、客户端监听、客户端注册功能、客户端登录功能、客户端记事本功能、测试

第八章 完整大项目《坦克大战》

  1. 《坦克大战》游戏功能: 登录注册、房间系统、战斗系统

  2. 坦克模型:导入模型、模型结构

  3. 资源管理器:设计构想

    代码与资源分离是游戏程序设计的核心思想之一,被广大游戏公 司所采用。相比于乱成一团的编码方式,它有以下几点优势。

    1)游戏公司里,美术人员负责模型的设计和制作,程序人员负责 实现功能。代码分离有利于美术人员和程序人员的分工合作,两者既 相互配合,又互不干扰。

    2)有利于代码的重复使用。功能相同但外观不同实体(如坦克) 只需一套代码。

    3)为游戏的热更新提供可能性。若游戏需要更新模型,只需要下 载新的模型资源。 当游戏中需要使用坦克模型时,可以使用动态加载的方式,即通 过代码把坦克模型加载到场景中,而不是在编辑场景的时候直接把坦 克模型拉进去。 Unity工程的Assets/Resources目录是个特殊的目录,它和 Resources类相关联。当使用形如Resources. Load ("tankPrefab")的语句去加载资源时,Unity会把 Assets/Resources/tankPrefab.prefab加载到内存。

  4. 坦克类:设计构想

    与第3章的角色(Human)类相似,设计如图8-13所示的坦克类结 构。BaseTank是坦克基类,它包含坦克的一些通用功能,比如开炮、 皮肤设置等。CtrlTank为玩家控制的坦克,它会包含行走控制等功 能。SyncTank是同步坦克,它会根据网络数据移动坦克、控制开炮。

    2
  5. 行走控制:速度参数、移动控制、测试、走在地形上

  6. 坦克爬坡:Unity的物理系统、添加物理组件、测试

  7. 相机跟随:功能需求、数学原理、编写代码、测试

  8. 旋转炮塔:炮塔元素、旋转控制、测试

  9. 发射炮:制作炮弹预设、制作爆炸效果、炮弹组件、坦克开炮、测试

  10. 摧毁敌人:坦克的生命值、焚烧特效、坦克被击中处理、炮弹的攻击处理、测试

第九章 UI界面模块

  1. 界面模块的设计:通用界面模块

    开发商业游戏,需要处理好9.1.1节提到的两个问题。首先,每一 个面板对应一个类,在这个类里面编写面板的功能。再定义一个界面 管理器,通过它来控制界面的显示和关闭。界面管理器有两个基本方 法,分别是Open和Close。形如: PanelManager.Open(); PanelManager.Close(); 只要程序调用“PanelManager.Open();”,游戏就 会显示出登录面板(对应LoginPanel类);只要调用 “PanelManager.Open("作者很帅");”就会弹出显示“作者很 帅”的提示框。下图展示了界面管理器的基本结构,除了Open和 Close两个方法,它一般还包含一个列表(这里取名为panels),索引 着所有已经打开的界面,避免重复打开。

    2
  2. 场景结构

    在Unity的界面系统中,所有面板组件都应放置在画布下。考虑到 不同面板间会有层级关系,我们在Unity场景中添加名为Root的空物 体,Root将永久保留在场景上。如图所示,Root下面有一个名为 Canvas的子物体,是一个画布。再下面有名为Panel和Tip的空物体, 代表不同的层级。

    2
  3. 面板基类BasePanel:设计要点

    这套界面系统由面板基类(BasePanel)、界面管理器 (PanelManager)和多个具体的面板组件(如LoginPanel、 RegisterPanel)组成。所有面板都继承自BasePanel,而 PanelManager提供打开某个面板、关闭某个面板的方法。BasePanel是 面板基类,所有的面板类都要继承它。BasePanel的一些设计要点如 下。

    1)面板的资源称为皮肤(skin,为GameObject类型),皮肤的路 径称为skinPath。界面管理器将会根据skinPath去实例化skin。

    2)由于某些面板有层级关系,比如提示框总要覆盖普通面板。在 PanelManager中会定义名为Layer的枚举,指定面板的层级。

    3)某些面板需要通过参数来确定它的表现形式。比如提示框显示 的内容由调用它的语句指定。

    4)面板有着图9-6所示的生命周期。在打开面板后,管理器会调 用面板的OnInit方法,做些初始化工作。随后加载资源,将面板预设 添加到场景上。再调用面板的OnShow方法,做些和面板资源有关的初 始化工作。当关闭面板时,会调用面板的OnClose方法。

    2
  4. 界面管理器PanelManager:层级管理

    既是层级管理,就需要定义有哪些层级。以下代码展示了界面管 理器的整体结构,定义枚举类型Layer,分别有Layer.Panel和 Layer.Panel两项。layers、root和canvas三个成员分别指向场景中的 物体,Dictionary类型的layers让Layer枚举和场景物体相对应,后续 的代码会让layers[Layer.Panel]指向“Root/Canvas/Panel”,让 layers[Layer.Tip]指向“Root/Canvas/Tip”,如图所示。

    2
  5. 登录面板LoginPanel:导入资源、UI组件、制作面板预设、登录面板类、打开面板、引用UI组件、网络监听、登录和注册按钮、收到登录协议

  6. 注册面板RegisterPanel:制作面板预设、注册面板类、按钮事件、收到注册协议

  7. 提示面板TipPanel:制作面板预设、提示面板类、测试面板

  8. 游戏入口GameMain:设计要点、代码实现、缓存用户名

    游戏入口类GameMain是一个挂载在场景中的组件,它作为整个游 戏的驱动类,解决下面几个问题。

    1)当玩家打开游戏时,登录面板会作为第一个弹出的面板。那么 游戏项目中,应该在哪里调用弹出登录面板的PanelManager.Open呢? 我们会把游戏的初始功能放置到GameMain里。

    2)客户端需要保存一些玩家数据,这些数据在整个游戏中都会用 到。例如在游戏的很多地方都会显示玩家的id,如果客户端能够自己 记录,便不需要从服务端获取。

    3)网络框架NetManager需要外部调用它的Update方法来驱动, GameMain可完成这项功能。

    4)GameMain还会完成一些通用事件的处理,例如当网络断开时弹 出提示,当玩家被顶下线时也弹出提示。

    2
  9. 功能测试:登录、注册、下线

第十章 游戏大厅和房间

  1. 列表面板预设:整体结构、个人信息栏、操作栏、房间列表栏、Scroll View、列表项Room

  2. 房间面板预设:整体结构、列表栏、列表项Player、控制栏

  3. 协议设计

    • 查询战绩MsgGetAchieve协

      服务端收到MsgGetAchieve协议后,返回玩家的总胜利次数win和 总失败次数lost。

    • 查询房间列表MsgGetRoomList协议

      服务端收到MsgGetRoomList协议后,会将所有房间信息发送给客 户端。协议类包含RoomInfo类型的数组,而RoomInfo类包含了房间的 各种信息,包括序号(id)、人数(count)、状态(status)。 status为0代表“准备中”状态,status为1代表“开战中”状态。

    • 创建房间MsgCreateRoom协议

      服务端收到MsgCreateRoom协议后,会创建一个新的房间并把玩家 添加到新的房间里。返回值result代表执行结果,result为0代表创建 成功,其他数值代表创建失败。例如,如果玩家已经加入别的房间 中,便不能创建新房间。

    • 进入房间MsgEnterRoom协议

      玩家请求加入房间时将房间序号(id)发送给服务端,服务端把 玩家添加到房间中。服务端的返回值result代表执行结果,result为0 代表成功进入,其他数值代表进入失败。例如玩家已经在房间中,就 不能重复进入。

    • 查询房间信息MsgGetRoomInfo协议

      玩家进入房间后,可以通过MsgGetRoomInfo协议请求该房间的详 细信息。服务端返回PlayerInfo类型的数组players,告诉客户端房间 里所有玩家的信息,包括账号(id)、所在队伍(camp)、胜利总数 (win)、失败总数(lost)、是否是房主(isOwner)。camp的取值 为1或者2,代表在第一个阵营或者第二个阵营;如果isOwner为1,代 表玩家是房主,如果为0,代表是普通成员。如有玩家加入或离开房 间,服务端还会给房间里的所有玩家推送该协议,让客户端更新界 面。

    • 退出房间MsgLeaveRoom协议

      玩家退出房间时发送MsgLeaveRoom协议,服务端的返回值result 代表离开房间的结果,result为0代表离开成功,其他数值代表离开失 败(如玩家不在房间中却发送离开房间的协议)。

    • 开始战斗MsgStartBattle协议

      当房主点击开始战斗按钮时,客户端会发送MsgStartBattle协 议。服务端的返回值result代表开始战斗的结果,result为0代表成 功,其他数值代表失败。

  4. 列表面板逻辑:面板类、获取部件、网络监听、刷新战绩、刷新房间列表、加入房间、创建房间、刷新按钮

  5. 房间面板逻辑:面板类、获取部件、网络监听、刷新玩家列表、退出房间、开始战斗

  6. 打开列表面板

  7. 服务端玩家数据:存储数据、临时数据

  8. 服务端房间类:管理器和房间类的关系、房间类的设计要点、添加玩家、选择阵营、删除玩家、选择新房主、广播消息、生成房间信息

  9. 服务端房间管理器:数据结构、获取房间、添加房间、删除房间、生成列表信息

  10. 服务端消息处理

    • 查询战绩MsgGetAchieve

      服务端收到查询战绩的协议后,返回玩家的总胜利次数win和总失 败次数lost。

    • 查询房间列表MsgGetRoomList

      服务端收到获取房间列表协议后,将房间列表信息发送给客户 端。由于RoomManager.ToMsg会返回带有列表信息的MsgGetRoomList对 象,只要把它发送给客户端即可。

    • 创建房间MsgCreateRoom

      服务端收到创建房间协议后,先进行一些条件检测,如果玩家在 房间中或者在战斗中,返回1表示不能创建。通过条件检测后,调用 RoomManager的AddRoom方法创建房间,再通过room.AddPlayer把玩家 添加到房间里面。最后返回0(msg.result)表示创建成功。

    • 进入房间MsgEnterRoom

      服务端根据客户端发来的房间序号(msg.id)找到房间,然后通 过room.AddPlayer方法把玩家添加到房间中。在下面的代码中,程序 会先做一些条件判断,如果玩家已经在房间里,那他不能够再进入房 间;如果客户端发送的房间序号(msg.id)不正确,服务端并没有这 个房间,也会返回失败。接着程序通过room.AddPlayer把玩家加入到 房间里。room.AddPlayer也会做一系列判断,比如房间是否处于准备 状态,房间是否满员了,如果这些条件不满足,依然会返回失败。如 果满足了room.AddPlayer的条件,程序不仅把玩家添加到房间中,还 会把房间信息推送给房间内的玩家

    • 查询房间信息MsgGetRoomInfo

      进入房间后,客户端会发送MsgGetRoomInfo协议请求该房间的详 细信息。在下面的处理方法中,程序先判断玩家是否在房间里 (if(room==null))。如果玩家不在房间,会回应空消息 (msg.players没有值);如果玩家在某个房间里面,程序会调用 room.ToMsg获取该房间的信息,然后发送给客户 端。

    • 离开房间MsgLeaveRoo

      服务端收到离开房间的协议后,会调用room.RemovePlayer将玩家 移出房间。下面的代码中,程序先会判断玩家是否在某个房间里 (if(room==null)),如果不在,自然就不存在退出房间的操作,会 返回失败(msg.result=1)。如果玩家确实在某个房间里,直接调用 room.RemovePlayer将玩家移出房间,room.RemovePlayer还会给房间 里的其他玩家广播MsgGetRoomInfo,让客户端更新界面。

  11. 玩家事件处理

  12. 测试

第十一章 战斗和胜负判定

  1. 协议设计

    • 进入战斗MsgEnterBattle

      服务端收到离开房间的协议后,会调用room.RemovePlayer将玩家 移出房间。下面的代码中,程序先会判断玩家是否在某个房间里 (if(room==null)),如果不在,自然就不存在退出房间的操作,会 返回失败(msg.result=1)。如果玩家确实在某个房间里,直接调用 room.RemovePlayer将玩家移出房间,room.RemovePlayer还会给房间 里的其他玩家广播MsgGetRoomInfo,让客户端更新界面。

    • 战斗结果MsgBattleResult

      服务端收到离开房间的协议后,会调用room.RemovePlayer将玩家 移出房间。下面的代码中,程序先会判断玩家是否在某个房间里 (if(room==null)),如果不在,自然就不存在退出房间的操作,会 返回失败(msg.result=1)。如果玩家确实在某个房间里,直接调用 room.RemovePlayer将玩家移出房间,room.RemovePlayer还会给房间 里的其他玩家广播MsgGetRoomInfo,让客户端更新界面。

    • 退出战斗MsgLeaveBattle

      网络游戏不可避免会出现战斗中掉线的情况,当战场中有玩家掉 线,服务端会向房间内的其他玩家广播MsgLeaveBattle协议,通过参 数id告知谁退出了比赛。

  2. 坦克:不同阵营的坦克预设、战斗模块、同步坦克SyncTank、坦克的属性

  3. 战斗管理器:设计要点、管理器类、坦克管理、重置战场、开始战斗、产生坦克、战斗结束、玩家离开

  4. 战斗结果面板:面板预设、面板逻辑

  5. 服务端开启战斗:能否开始战斗、定义出生点、坦克信息、开启战斗、消息处理

  6. 服务端胜负判断:是否死亡、胜负决断函数、定时器、Room::Update

  7. 服务端断线处理

  8. 测试:进入战场、离开战场

第十二章 同步战斗信息

  1. 同步理论

    • 同步的过程

      在客户端—服务端架构中,无论是用什么样的同步方法,都始终 遵循着图所示的过程。客户端1向服务端发送一条消息,服务端收 到后稍作处理,把它广播给所需的客户端(客户端1、客户端2和客户端3)。所传递的消息可以是坦克的位置、旋转这样的状态值,也可以 是“向前走”这样的指令值。前者称之为状态同步,后者称之为指令同步。

      2
    • 同步的难题

      由于存在网络延迟和抖动,往往很难做到精确的同步。图12-3左 图展示的是理想的网络情况,服务端定时发送消息给客户端,客户端 立刻就能够收到。而实际的网络情况并非如此,更像图右图所展 示的,存在两个问题:其一,消息的传播需要时间,会有延迟;其 二,消息的到达时间并不稳定,有时候两条消息会相隔较长时间,有时候却相隔很短。

      2

      网络延迟问题基本无解,只能权衡。比如,尽量发送更少的数 据,数据越少,发生数据丢失并重传的概率就越小,平均速度越快。 又比如,在客户端上做些“障眼法”,让玩家感受不到延迟。

  2. 状态同步

    状态同步指的是同步状态信息。在坦克游戏中,客户端把坦克的 位置坐标、旋转发送给服务端,服务端再做广播。

    • 直接状态同步

      最直接的同步方案莫过于客户端定时向服务端报告位置,其他玩 家收到转发的消息后,直接将对方坦克移动到指定位置。

    • 跟随算法

      为了解决“直接状态同步”的瞬移问题,人们引入了一种障眼 法,称为“跟随算法”。在收到同步协议后,客户端不直接将坦克拉 到目的地,而是让坦克以一定的速度移动。

    • 预测算法

      跟随算法的一大缺陷就是误差会变得很大,那么还有没有办法可 以减少误差呢?在某些有规律可循的条件下,比如坦克匀速运动,或 者匀加速运动,我们能够预测坦克在接下来某个时间点的位置,让坦 克提前走到预测的位置上去。这就是预测算法。

  3. 帧同步

    帧同步是指令同步的一种,即同步操作信息。基本上所有指令同 步方法都结合了帧同步,两者可以视为一体。这里“帧”的概念与 Unity中“每一帧执行一次Update”“30FPS(每秒传输帧数,Frames Per Second)”里的“Unity帧”有所不同,我们会实现独立于 “Unity帧”的另外一种“同步帧”。

    • 指令同步

      状态同步所同步的是状态信息,如果要同步坦克的位置和旋转, 那就需要同步六个值(三个坐标值和三个旋转值)。缓解网络延迟的一个办法是减少传输的数据量,如果只传输玩家的操作指令,数据量就会减少很多。

      缺点:有些电脑速度快,有些电脑 速度慢,尽管玩家2收到了玩家1的指令,但只要两者的电脑运行速度 不同,可能有人看到坦克走了很远,有人看到的却只移动了一点点的 距离。为了解决这个问题,人们在操作同步的基础上,引入了“同步 帧”的概念。

    • 从Update说起

      如果有一种办法,让不同的电脑有一致的运行效果,便可以解决 指令同步中的误差累积问题。在第8章中,我们在Update中设置控制坦克的新位置,代码如下:

       public void MoveUpdate(){ //Update中调用 …… 
           float y = Input.GetAxis("Vertical"); //如果是SyncTank,改为由网 络传播的指令 
           Vector3 s = y*transform.forward * speed * Time.deltaTime; 
           transform.transform.position += s; } 

      由于采用了“速度*时间”的计算式,理论上说,无论电脑运行速 度快慢,坦克移动的路程都能够保持一致。因为当电脑很慢时, Update的执行次数会变少,但Time.deltaTime的值变大,反之示然, 但坦克移动的路程保持不变。

      尽管如此,我们还不能够保证经由网络同步的坦克能够有一致的 行为。因为网络延迟的存在,从发出“前进”到“停止”指令之间的时间可能不一致,坦克移动的路程也就不同。

      一种解决办法是,在发送命令的时候附带时间信息,客户端根据指令的时间信息去修正路程的计算方式,使所有客户端表现一致。人们定义了一种名为“帧”的 概念,来表示时间(为和Unity本身的帧区分,这里称为“同步帧”)。

    • 什么是同步帧

      假如我们自己实现一个类似Update的方法,称之为FrameUpdate, 程序会固定每隔0.1秒就调用它一次。每一次调用FrameUpdate称之为 一帧,第1次调用称为第1帧,第2次调用称为第2帧,以此类推。

      在图 12-11中,在第0.1秒的时候执行了第1帧,在第0.2秒的时候执行了第2 帧。 然而图12-11展示的是一种理想情况,现实往往很残酷。比如在执 行第2帧的时候,系统突然卡顿了一下,这一帧的执行时间变长了,超 过0.1秒(图12-12),这会导致第3帧无法按时执行。为了保证后面的 帧能够按时执行,程序需要做出调整,即减少第2帧和第3帧之间、第3 帧和第4帧之间的时间间隔,保证程序在第0.5秒时,执行到第5帧。

      2 2
    • 指令

      比起操作同步,在指令同步中,客户端向服务端发送的指令包含 了具体的指令和时间信息,即是在哪一帧(特指同步帧)做了哪些操 作。例如:在第10帧发出了“前进”指令(按下“上”键),在第20 帧发出了“后退”指令(按下“下”键)。

      指令同步的协议形式如下,cmd代表指令的内容,可能是前进、后 退、左转、右转、停止。frame代表该指令在第几帧发出。

    • 指令的执行

      为了保证所有客户端有一样的表现,往往要做一些妥协,有两种常见的妥协方案。

      1)有的客户端运行速度快,有的运行速度慢,如果要让它们表现一致,那只能让快的客户端去等待慢的客户端,所有客户端和最慢的 客户端保持一致,才有可能表现一致,毕竟,慢的客户端无论如何都 快不了。这种方案对速度快的客户端较为不利。达成此方案的一个方 法称为延迟执行,如果客户端1在第3帧发出向前的指令,由于网络延 迟,客户端2可能在第5帧才收到,所以客户端1的坦克也只能在第5帧 (或之后的某一帧)才开始前进。

      2)对于速度慢客户端所发送的,丢弃那些已经过时的指令,直到 它赶上来。此种方案也称之为乐观帧同步,对速度慢的玩家较为不 利,因为某些操作指令会被丢弃。比如发出“前进”指令,但该指令 被丢弃了,坦克不会移动。

      所以,帧同步是一种为了保证多个客户端表现一致,让某些客户 端做妥协的方案。而且如果启用了延迟执行,在玩家发出“前进”指 令之后,要隔一小段时间坦克才能移动,玩家会感受到延迟。但无论 如何,只要帧率(每秒执行多少帧)足够高,玩家就不会感觉到明显 的延迟。

  4. 协议设计:位置同步MsgSyncTank、开火MsgFire、击中MsgHit

  5. 发送同步信息:发送位置信息、发送开火信息、发送击中信息

  6. 处理同步信息:协议监听、OnMsgSyncTank、OnMsgFire、OnMsgHit

  7. 同步坦克SyncTank:预测算法的成员变量、移动到预测位置、初始化、更新预测位置、炮弹同步

  8. 服务端消息处理:位置同步MsgSyncTank、开火MsgFire、击中MsgHit、调试

  9. 完善细节

    • 滚动的轮子和履带
    • 灵活操作
    • 准心
    • 自动瞄准
    • 界面和场景优化
    • 战斗面板
    • 击杀提示