最近几天正在找工作,所以更新BLOG晚了一些。今天在老婆的催促下终于要更新了。
通过以上两篇关于对中心服务器和登录服务器设计的阐述,大家应该对设计它们有了一定的了解。但是中心服务器和登录服务器毕竟是游戏外围的部分,也就是说设计好了它们也还是无法实现编写一款游戏的目的啊。今天我们就来探讨一下如何设计游戏服务器。
 
通过对QQ游戏、远航、联众等游戏的分析。我们可以发现一些规律:
1:每个已经登录到大厅的玩家需要在树形列表中选择需要进入的游戏房间。
2:在游戏房间中我们可以进行坐下、更换座位、离开房间等操作。
3:几乎每一款游戏进入后,都需要点击“开始”或者“准备”按钮。点击后玩家在房间游戏桌上的状态变为一个举手的标志,这表明玩家已经准备好随时进行游戏了。
4:当一个桌子的所有玩家都满足“游戏开始”状态以后,在游戏房间中会显示此游戏桌为游戏状态。
使用Delphi编写×××类游戏 – 设计篇(3)_第1张图片
以上4点是几乎每一款游戏都具有的过程。通过分析我们可以发现如果以玩家角度来看,一个玩家大致具有一下这么6种状态:
1、  空闲:玩家已经进入房间,但是并没有做其它的操作。
2、  坐下:玩家点击了椅子,自己的头像已经在椅子上显示,并且游戏界面已经打开。
3、  举手:玩家已经点击了游戏界面上的开始(有的游戏叫举手)按钮。游戏界面上已经显示自己处于“准备”(QQ游戏显示准备)或者“等待开始”信息。
4、  游戏:玩家处在游戏过程之中。这种状态也包含类似于连连看、对对碰游戏中用于自己已经失败,但是还有其它玩家在游戏的情况。
5、  旁观:玩家点击一个已经开始的游戏桌中的一个玩家头像,可以看见此玩家正在游戏的即时信息。
6、  断线:玩家的客户端和服务端已经断开连接时的状态。
而对玩家这6种状态的维护是×××类游戏的一个很关键的部分。大家可以看到对于一个玩家来说,从进入一款游戏到退出游戏,他的状态就在这6种状态中来回变换。
下图为玩家状态转换图:
使用Delphi编写×××类游戏 – 设计篇(3)_第2张图片
 
通过上面的分析,我们在定义玩家信息结构的时候就比较方便了。以下是我定义的玩家信息结构。
  RUserSocket = record
    Socket:TSocket;            //套接字
    UserID:Pchar;              //玩家编号
    UserName:Pchar;           //玩家名称
    UserKey:Pchar;            //玩家解密和加密时使用的密钥
    Room:Integer;             //玩家所在的房间
    Dask:Integer;              //玩家所在的座位
    PawID: Integer;            //座位号
    Sex: Boolean;              //玩家性别
    Email:pchar;               //电子邮件
    GameID: pchar;            //游戏编号
    ByName:Pchar;            //玩家昵称
    CurrState: Integer;          //玩家状态
    Face: Integer;              //玩家头像
    Grade: Integer;             //游戏等级
    Score: Integer;             //积分
    TotalScore: Integer;         //总成绩
    winnum: Integer;           //赢盘数
    losenum: Integer;           //输盘数
LookOnList:TList;          //旁观玩家列表
LogonTimer:TDateTime;     //玩家登录时间
    end;
  PUserSocket = ^RUserSocket;
对于一个游戏服务器上的用户的管理,我们可以放在一个全局链表中,对这个链表的维护我们可以放在一个类中(例如叫:TUserControl)。
如果我们以桌子为对象来看,游戏桌的状态应该分为:
1、  空闲状态:桌子没有开始游戏时候的状态。
2、  游戏状态:桌子正在游戏的时候的状态。
这样我们就可以设计出桌子的结构信息:
RDeskStatus = record
    GameID:string[2];           //游戏编号
    RoomID:Integer;            //房间编号
    DeskID:Integer;             //桌子编号
    UserNums:Integer;           //在这个桌子上的玩家个数(不含旁观用户)
    Status:Integer;               //桌子状态  0:没有开始游戏 1:已经开始游戏
  end;
PDeskStatus = ^RDeskStatus;
对于桌子的信息我们也放在一个链表中,并使用一个类来进行管理。(例如叫:TDeskControl
接下来的问题就是,如何将玩家的信息和桌子的信息关联起来呢?
我们知道,一个玩家进入房间后,这个房间的其它玩家的坐下、举手、游戏开始等等的状态他都应该可以接收到。所以每一个房间的玩家信息都应该由一个链表来维护。同时这个链表应该还维护这个房间桌子的状态信息。
以下是我设计的房间信息的结构:
RUserRoom = record
    Room:Integer;                 //房间编号
    DeskStatusList:TList;           //本房间桌子状态信息链表  存放PdeskStatus指针。
    ListUser:TList;                //玩家列表                存放PuserSocket指针。
  end;
PUserRoom = ^ RUserRoom;
对于这个结构的维护我们也可以使用一个类来做(例如:TRoomControl)。
以上的3个类是游戏服务器主要编写的3个类。如何实现我们将在“实现篇”中来说明。
 
我们知道我们设计出来的游戏服务器应该具有良好的可扩展性,以便于我们以后添加一些未知的游戏和游戏类型。那如何做到游戏服务器的可扩展性呢?通过分析我们发现,每一套游戏差别主要在于游戏的本身。例如象棋游戏和挖坑游戏,它们的区别在于游戏的规则(一个是棋类游戏,一个是牌类游戏),而不在于玩家的状态(这两款游戏玩家都有坐下、举手、游戏等等功能)。所以我们要做到游戏服务器的可扩展性,应该将游戏的逻辑部分和玩家的状态区分开来。将玩家状态部分让游戏服务器来管理,将游戏逻辑部分使用脚本或者DLL的方式来动态加载。这样我们就可以实现游戏服务器的可扩展性。
 
下一篇我们就来探讨如何让游戏服务器动态的加载一个未知的游戏逻辑信息。