游 开
2009-08-09 13:10 宝宝合凤凰 阅读(1206) 评论(0) 收藏 举报作者:Yahle
曾用网名:Dogvane
原载:http://www.cnblogs.com/yahle
版权所有。转载时必须以链接形式注明作者和原始出处。
1 体系结构
1.1 传统的网站的架构
传统的网站一般都是以N层结构一般N为3,就是我们常说的三层架构。
3层架构分为数据层、业务逻辑层、页面显示层。
1.2 WebGame的架构
WebGame可以看作是网站和游戏的结合体,因此它具备了这两类系统的特性。我们不但可以把WebGame看作是一个网站,也可以把它看作是一个网络游戏。
的网站是B/S结构,网络游戏则是C/S结构,WebGame则是这两者的结合我们暂且称之为B/C/S结构。既在用户眼里,它是一个通过浏览器范围的网站。在服务器系统里,它又是一个传统的C/S结构的网络游戏。
 
从上图分析,用户通过浏览器访问服务器的时候,首先是访问网页服务器,如windows平台下的IIS,linux下的Apache。在通过网页服务器,以某种特殊的方式(分布式访问,如.net下的remoting)去访问游戏服务器,通知游戏逻辑服务器执行玩家操作,并从游戏逻辑服务器里获得游戏相关的信息,或者直接通过访问数据库而获得游戏数据。
1.2.1 为什么要将服务器分为游戏服务器和网页服务器
网页服务器的特点是触发执行,及当有用户访问网页的时候,才会执行该网页的程序代码。而我们常见的WebGame(Ogame,Travian)这些游戏实际上是需要24小时不间断执行的,因此网页服务器的执行方式并不适合与游戏。因此我们另外需要一个应用程序来执行这些24小时不间断要做的事情。这也就是我们需要增加一个游戏服务器的原因。
1.3 Web三国的架构
因为目前Web三国是非商业开发,因此如果照搬上面的WebGame架构来设计,会导致开发周期过长,开发效率低下的问题。实际上在开发初期,Web三国是按照上面的架构去开发,碰上了上述问题,才换了另外一种结构。
实际性现在Web三国的结构和WebGame架构差不多,只不过将游戏服务器集成到网页服务器里,项目里按照传统的网站架构,将游戏分为:数据层、游戏逻辑层、页面层。
至于如何实现24小时不间断处理,者是通过在游戏启动时,创建一个线程去处理。这个是asp.net里的一个功能,我不清楚php里是否也有这样的功能。
作者:Yahle
曾用网名:Dogvane
原载:http://www.cnblogs.com/yahle
版权所有。转载时必须以链接形式注明作者和原始出处。 
1 事件系统
事件系统是整个WebGame系统里一个核心的组成部分,我们用它来控制的进程,让游戏世界里能够24小时运转。
1.1 事件的概念
事件是指游戏里玩家的某个(系列)活动,它可以分为瞬时活动和非瞬时活动。
瞬时活动顾名思义就是在玩家发出指令的瞬间就能完成的活动。像RPG游戏里,玩家从NPC里购买一瓶药水,在玩家发出这个指令后,玩家的金钱减少,并获得药水,这一切都在玩家发出指令后瞬间完成(当然实际逻辑上处理还需要几个ms处理时间)。
而非瞬时活动则是在玩家发出某个指令后一段事件才会被执行。例如RPG里玩家鼠标点击地图上某个地方,游戏角色则会自动行走到刚才点击处。这个移动过程就是一个非瞬时过程,它有了一个移动的过程,这个过程需要消耗一定的时间(玩家能感知的事件)
非瞬时系列活动是指一位或者多位玩家通过一系列的瞬时\非瞬时活动完成一个动作(功能)。例如wow里面的拍卖场,有1位玩家提供道具,同服务器里的其他玩家对该道具进行竞拍。
在WebGame里,玩家的很多操作其实是非瞬时部分事件是村庄资源减少(前提投资),非瞬时事件是建筑物建设,这个动作(同能)的结果是建筑物等级上升。
又比如《Travian》里的攻击,瞬时事件是当前村庄的士兵减少(派出部队),非瞬时事件是减少的士兵移动到需要攻击的村庄(行军过程),动作结束是,两个村庄的部队开打了(战斗)。
1.2 触发器(事件队列)
前面说了瞬时事件和非瞬时事件的概念,当WebGame在24小时运转的时候,系统会产生大量的非瞬时事件,这些非瞬时事件不会在玩家点击页面时执行,而是需要等一段时间后才会执行,因此在游戏里把这些非瞬时事件拿出来,按事件的执行时间进行排序,组成一个事件队列。再通过一个触发器,在事件设定的执行时间到达的那时执行相对应的事件。
这里面就涉及到两个内容:
• 非瞬时的事件队列
• 事件触发器
1.2.1 事件队列
数据库除了用于存储外,其查询功能也非常强大,直接拿来做事件队列很合适。事件队列里通常保存事件涉及的对象(村庄),结束时间、事件类型以及事件相关参数等。
下表为我们系统里使用的事件表:
  
| ID | int | 
| VillageCode | int | 
| TargetVillageCode | int | 
| Type | int | 
| EndTime | DateTime | 
| EventObject | ntext | 
保存时将这些对象做xml序列化保存到EventObject字段里。当然如果为了效率还可以存储二进制序列化后的对象,这样在序列化以及反序列化时能节省大部分时间。
1.2.2 触发器
Asp.net
Asp.net的处理在触发器上的处理就比较简单了。在服务器程序启动的时候,就执行一个线程,定时(1s)从数据库里取结束时间<当前事件的事件进行处理。
 /// <summary>
        /// <summary>2
 /// 事件处理线程
        /// 事件处理线程3
 /// 每间隔1s处理一次到时间了的事件
        /// 每间隔1s处理一次到时间了的事件4
 /// </summary>
        /// </summary>5
 static void EventThread()
        static void EventThread()6
 {
        {7
 DateTime lastTime = DateTime.Now;
            DateTime lastTime = DateTime.Now;8
 while (Run)
            while (Run)9
 {
            {10
 try
                try11
 {
                {12
 Event.SystemEvent.DoEvent(lastTime);
                    Event.SystemEvent.DoEvent(lastTime);13
 }
                }14
 catch (Exception ex)
                catch (Exception ex)15
 {
                {16
 Common.Logging.Error(ex.ToString());
                    Common.Logging.Error(ex.ToString());17
 }
                }18

19
 long def = DateTime.Now.Ticks - lastTime.Ticks;
                long def = DateTime.Now.Ticks - lastTime.Ticks;20
 lastTime = DateTime.Now;
                lastTime = DateTime.Now;21

22
 if (def < 10000000)
                if (def < 10000000)23
 {
                {24
 //  线程休息,并等待下一次时间间隔
                    //  线程休息,并等待下一次时间间隔25
 int ms = (int)(10000000 - def) / 10000;
                    int ms = (int)(10000000 - def) / 10000;26
 if (ms > 0)
                    if (ms > 0)27
 System.Threading.Thread.Sleep(ms);
                        System.Threading.Thread.Sleep(ms);28
 }
                }29
 }
            }30
 }
        }31

 /// <summary>
  /// <summary>2
 /// 系统事件
    /// 系统事件3
 /// 主要作用是执行处理事件的过程
    /// 主要作用是执行处理事件的过程4
 /// </summary>
    /// </summary>5
 public class SystemEvent
    public class SystemEvent6
 {
    {7
 public static void DoEvent(DateTime time)
        public static void DoEvent(DateTime time)8
 {
        {9
 List<Event> evs = Event.GetEvents(time);
            List<Event> evs = Event.GetEvents(time);10
 foreach (Event e in evs)
            foreach (Event e in evs)11
 {
            {12
 try
                try13
 {
                {14
 IEvent ie = e.Object;
                    IEvent ie = e.Object;15
 if (ie != null)
                    if (ie != null)16
 {
                    {17
 ie.Parent = e;
                        ie.Parent = e;18
 ie.DoEvent();
                        ie.DoEvent();19
 }
                    }20
 }
                }21
 catch (Exception ex)
                catch (Exception ex)22
 {
                {23
 Common.Logging.Error(ex.ToString(), e.EventObject);
                    Common.Logging.Error(ex.ToString(), e.EventObject);24
 }
                }25
 try
                try26
 {
                {27
 e.Delete();    // 从数据库里删除信息
                    e.Delete();    // 从数据库里删除信息28
 }
                }29
 catch (Exception ex)
                catch (Exception ex)30
 {
                {31
 Common.Logging.Error(ex.ToString());
                    Common.Logging.Error(ex.ToString());32
 }
                }33
 }
            }34
 }
        }35
 }
    }36

php
PHP没用过,不过好像不能在PHP页面里无法创建线程,用纯PHP服务端来实现触发器估计有点难度。但是可以做成一个PHP的应用程序,由应用程序来实现触发器。
1.3 游戏资源的24小时自动增长
游戏资源的24小时自动增长,这是一个有趣的话题,很多刚开始设计WebGame的朋友都会在这个问题上卡一下。每个人对于如何实现这个功能都有自己的独到见解,我在这里就不能给出一个唯一的答案,这里给的解决方案只是自己正在做的WebGame用到的方案。
首先,我们否定了每个时间间隔(10分钟)就执行更新村庄资源的设计,这即不准确,同时也很消耗服务器资源。所以,我们的系统就只有在用户执行事件(瞬时的和非瞬时)的时候才将新的资源信息写入数据库。平时显示资源的时候用 
(当前时间 - 上一次更新事件)*资源每小时产量+上一次更新产量
公式计算出当前资源并显示在页面上。也就是说,每次页面更新时重新计算资源,但只只要用户没有做任何修改资源的动作(事件),就不会把重新计算后的资源写回数据库。
《Travian》在前台界面上看到资源不断的上涨其实是利用JavaScript实现的小效果。
作者:Yahle
曾用网名:Dogvane
原载:http://www.cnblogs.com/yahle
版权所有。转载时必须以链接形式注明作者和原始出处。 
1 多线程下数据库并发更新的处理
1.1 背景
不知道大家在玩《Travian》时有没有做过这样的事情:
同时打开多个集结点,并设定好要出发的士兵及数量,在快到压秒的时候,快速切换页面,不断的点确定,以确保游戏不会通讯问题导致压秒失败。
再看一个教科书里经常提到的数据库脏数据的案例:
A操作从表里获得数据D=10,在计算的时候,线程刚好进行切换,切换到B,B也需要操作D,并从数据库里取道值为10,在进行简单操作(D=D- 2)后将D=8的值写回数据库。B操作处理结束后,线程再切换回A操作,这时A在做自己的操作时,仍然采用先前取到D(10)的值,在进行一个简单操作(D=D-1)后,仍然写回数据库。这时,数据库里的值变为9,而实际上D的值应该是7(D=10-2-1).。造成这个问题主要是因为CPU在执行A、B操作时没有按照顺序来执行,而是让B抢先在A执行完之前执行,导致它们在计算D的时候,因为数据没有同步而发生写入脏数(A的数据覆盖了B的数据)据的问题。
A操作伪代码:
{
 D=GetDB()
 D=D-1
 SetDB(D)
}
B操作伪代码:
{
 D=GetDB()
 D=D-2
 SetDB(D)
}
一个简单的图表表示它们的操作:
  
| CPU | A操作 | B操作 | 数据库当前值 | 
| D=GetDB() | 10 | ||
| 线程切换 | D=GetDB() | 10 | |
| D=D-2 | 10 | ||
| SetDB(D) | 8 | ||
| 线程切换 | D=D-1 | 8 | |
| SetDB(D) | 9 | 
仿照第一个例子,当一位玩家同时打开多个页面点出兵进攻后,这些这些请求会同时到达服务器,服务器会根据这些Http请求创建相应的线程来处理进攻动作:
进攻的伪代码:
{
 村庄士兵数量=GetDB()
 if (村庄士兵数量 > 这次进攻士兵数量)
 {
  村庄士兵数量=村庄士兵数量-这次进攻士兵数量
  SetDB(村庄士兵数量)
 }
}
按照第二个脏数据的例子,应该很容易想到,我们在这次进攻的时候,很有可能派出了2只部队,但是只减少了1只部队的士兵。
多线程未同步是造成游戏bug的原因之一。
1.2 多线程并发与互斥
关于什么是多线程,以及多线程下面的同步及互斥的方法我这里就不过多的介绍了,相关内容可以到网上搜索,本文主要是讨论同步的时机以及避免死锁
cnblogs.com相关主题
1.3 WebGame里多线程数据同步的方法
1.3.1 在asp.net下用lock进行加锁操作
在我们的WebGame里,采用asp.net的lock方法。具体就是在方法体里,用lock里锁住一个对象,使其它方法在访问这个对象的时候被阻塞。当第一个访问对象的线程退出并释放锁以后,其它的线程才能取消阻塞状态继续操作该对象。
我们通过修改上面进攻的伪代码,增加lock操作:
{
 lock(锁定的对象)
 {
  村庄士兵数量=GetDB()
  if (村庄士兵数量 > 这次进攻士兵数量)
  {
   村庄士兵数量=村庄士兵数量-这次进攻士兵数量
   SetDB(村庄士兵数量)
  }
 }//unlock(锁对象)
}
CPU执行流程表:
  
| CPU | 线程A | 线程B | |
| lock(锁对象) | |||
| 村庄士兵数量=GetDB() | |||
| if (村庄士兵数量 > 这次进攻士兵数量) | |||
| 线程切换 | lock(锁对象) | 对象被锁,线程进入阻塞状态 | |
| 线程切换 | 村庄士兵数量=村庄士兵数量-这次进攻士兵数量 | ||
| SetDB(村庄士兵数量) | |||
| //unlock(锁对象) | |||
| 线程切换 | 村庄士兵数量=GetDB() | A线程释放锁,B线程被唤醒 | |
| if (村庄士兵数量 > 这次进攻士兵数量) | |||
| 村庄士兵数量=村庄士兵数量-这次进攻士兵数量 | |||
| SetDB(村庄士兵数量) | 
1.3.2 锁的粒度
上面的伪代码是对游戏里的逻辑代码进行了加锁处理,但只是简单的描述该方法体里需要进行加锁以及加锁的范围。但在实际的代码里,我们需要明确lock里锁定的对象。如果这个对象选择不合适,很有可能会造成性能损失或者死锁。
1.3.2.1 数据库锁
数据库锁是最一种简单的方法就,凡是在存在发生数据库写操作的代码里,都需要进行加锁处理,并同意用一个数据库锁对象。
这种方法使用简单,不容易造成死锁。当存在的问题也是明显,就是在发生写数据库操作时不能并发操作,这点特别是当用户访问量增大可能会造成一定的性能瓶颈。
1.3.2.2 村庄锁
为了提升写数据库的效率,我们必须解决锁粒度过大的问题,因此在我们的游戏系统里,对锁的粒度进行的细化,细化到村庄级别的对象。
在游戏一张村庄表对应是整个游戏里所有的村庄对象,而一个村庄对象在村庄表里只是一条记录。在使用数据库锁时,其实是告诉其它方法,现在我要写数据库,大家都等一下,等我写好后再写。当我们将锁的对象细化到村庄(一条数据库表记录)的时候,实际是告诉数据库,我现在要修改XXX村庄,大家都别动它,但你要修改YYY村庄我不管。
死锁
在前面虽然降低了锁的粒度,提高了数据库并发性能,但随之而来就很容易发生一个问题--死锁。
例如当两个村庄需要发生交易,这时我们需要同时修改A、B两个村庄对象,需要对其进行顺序锁定(先锁定A,再锁定B),这时候,又发生另外一个操作,也需要同时对A、B两个村庄进行锁定,恰巧这个锁定的顺序是B、A。这样就造成两个现在互相等待形成死锁。
中间变量解决死锁
对于死锁,在关于多线程介绍方面有很多解决方案,这里就不过多阐述。在WebGame里预防死锁,可以采用结合游戏的操作流程,对游戏处理流程及数据进行拆分,来预防死锁问题。简单的来说,就是将涉及2个村庄修改的流程拆分为2个流程,并用中间变量予以表示,两个处理流程涉及变化的值都在中间变量里予以保存。
以前面提到的交易为例,在游戏设计里,两个村庄在交易的时候,并不是瞬时交易,而是通过商人进行运输并交易。这样我们就以商人作为中间变量。
A<-->C<-->B
在上图里,交易开始,是由玩家触发交易事件,这时以村庄A为锁对象,进行锁定。C作为要交易资源从村庄A里被扣除。并将A修改后的数据回写到数据库。等过了一段事件后,商人C到达了目的地,这时由系统触发后续的交易事件,这时以村庄B作为锁对象进行锁定。村庄B在获得资源后写回数据库,整个交易事件就算完成了。
当然实际游戏的交易比上面的例子稍微复杂一点点,因为交易双方都有资源的减少与获得。完整的流程应该是需要锁定4次,并产生2个中间变量。
村庄A-->商人1-->村庄B
村庄A<--商人2<--村庄B
游戏里其它地方的锁定
基本上,凡是某个事件涉及到两个村庄修改的地方,都可以用上面的锁定的方法对处理流程进行修改。例如村庄A攻打村庄A。当然其它事件在数据修改方面只涉及1个村庄,那么就不需要怎么麻烦,直接对村庄加锁锁定即可。
好在在策略类的WebGame里涉及两个村庄的情况不多,涉及到的基本可以用中间变量对操作流程进行拆分,因此这种锁定方式在策略类WebGame还是比较合适的。当然实在不行也只能按照以往的预防死锁的方法进行处理
1.4 非Asp.net里同步的方案
1.4.1 Java
java在线程同步机制上与asp.net基本一致,因此上面所述的asp.net的方法也适合与java
1.4.2 php
了解不多,不过好像php没有线程锁与同步这个概念,如果直接通过语言环境进行同步可能比较困难。
不过在MySQL里,存在一个表锁定的方法,可以通过lock table的方法锁定表,不允许其它MySQL用户去进行操作。基本上和前面提到的数据库锁类一样,只不多执行方法的时候是在MySQL端执行。
web策略类游戏开发(四)一个可以承载万人在线的架构
 Webgame现在已经开始需要进入大统一服务器时代,每个游戏区域容纳的玩家数量将从现在的几万人发展到几十万人,因此在新的背景下,webgame如何处理大量用户的请求将成为问题。目前一台asp.net做的weggame服务器每秒能处理500~1000个页面请求,按照每个玩家每隔3~5秒做一次页面操作(页面请求),一台服务器能承受2k~4k的玩家在线,对于一个只有几万人的策略游戏来说,已经是足够了。但对于一个未来将承载几十万人的游戏来说远远不够。
 通过分析,玩家在游戏过程中,有80%以上的访问仅仅只是查看玩家在游戏里的状态,实际上真正会对游戏运行状态及数据修改的的页面请求不足20%。因此,我们可以将呈现页面和处理游戏逻辑的功能拆分为2组服务器:页面服务器和逻辑服务器。两者之间可以通过remoting的方式进行数据通讯。将服务器分离后,随着页面服务器的增加,页面访问能力能应该能提升4~6倍。在往上逻辑服务器就会出现访问瓶颈。解决方法可以让页面服务器在读取玩家数据时直接访问数据库或者增加一个对象缓存服务器。页面服务器只有在必要的时候(需要进行逻辑运算时)才访问逻辑服务器,而逻辑服务器在玩家数据发生改变后更新对象缓存服务器和数据库。这样就可以大大降低逻辑服务器的访问次数,使页面访问能力进一步提升,轻松突破万人在线。如果访问量还需要继续扩大,可以用httpd做前台负责相应图片以及css等静态文件。
 
有人希望看数据库表,在这里发一下表设计,基本上没有什么特别的地方需要解释的,数据库的字段名都写得很清楚了。当然,目前的字段只是游戏的基本字段,如果游戏功能多起来后,表设计会比现在复杂。
  
表名:Village 
| 序号 | 列名 | 数据类型 | 长度 | 小数位 | 标识 | 主键 | 允许空 | 默认值 | 说明 | 
| 1 | ID | int | √ | ||||||
| 2 | Name | varchar | 50 | ||||||
| 3 | code | int | 0 | ||||||
| 4 | PlayerID | int | 0 | ||||||
| 5 | villageType | int | 1 | ||||||
| 6 | CityCode | int | 0 | ||||||
| 7 | Building | varchar | 255 | ||||||
| 8 | BuildingLevel | varchar | 255 | ||||||
| 9 | X | int | 0 | ||||||
| 10 | Y | int | 0 | ||||||
| 11 | Population | int | 0 | ||||||
| 12 | Resource | varchar | 50 | ||||||
| 13 | MaxResource | varchar | 50 | ||||||
| 14 | OutPut | varchar | 50 | ||||||
| 15 | LastUpdateResource | datetime | Now() | 
表名:Troops
| 序号 | 列名 | 数据类型 | 长度 | 小数位 | 标识 | 主键 | 允许空 | 默认值 | 说明 | 
| 1 | ID | int | √ | ||||||
| 2 | PlayerID | int | 0 | ||||||
| 3 | VillageCode | int | 0 | ||||||
| 4 | Num | varchar | 50 | ||||||
| 5 | TroopType | varchar | 50 | ||||||
| 6 | AttackVillageCode | int | 0 | ||||||
| 7 | EndTime | datetime | |||||||
| 8 | State | int | 0 | ||||||
| 9 | AttackBuildingID | int | 0 | ||||||
| 10 | Code | varchar | 50 | ||||||
| 11 | HeroID | int | 0 | 
表名:Trade
| 序号 | 列名 | 数据类型 | 长度 | 小数位 | 标识 | 主键 | 允许空 | 默认值 | 说明 | 
| 1 | ID | int | √ | ||||||
| 2 | PlayerID | int | 0 | ||||||
| 3 | VillageCode | int | 0 | ||||||
| 4 | Type1 | int | 0 | ||||||
| 5 | Num1 | int | 0 | ||||||
| 6 | Type2 | int | 0 | ||||||
| 7 | Num2 | int | 0 | ||||||
| 8 | X | int | 0 | ||||||
| 9 | Y | int | 0 | 
表名:SystemMessage
| 序号 | 列名 | 数据类型 | 长度 | 小数位 | 标识 | 主键 | 允许空 | 默认值 | 说明 | 
| 1 | ID | int | √ | ||||||
| 2 | PlayerID | int | 0 | ||||||
| 3 | Type | int | 0 | ||||||
| 4 | Title | varchar | 50 | ||||||
| 5 | Object | varchar | 0 | ||||||
| 6 | IsRead | int | 0 | ||||||
| 7 | CreateTime | datetime | Now() | 
表名:PlayerMessage
| 序号 | 列名 | 数据类型 | 长度 | 小数位 | 标识 | 主键 | 允许空 | 默认值 | 说明 | 
| 1 | ID | int | √ | ||||||
| 2 | PlayerID | int | 0 | ||||||
| 3 | SendPlayerID | int | 0 | ||||||
| 4 | SendPlayerName | varchar | 50 | ||||||
| 5 | Title | varchar | 50 | ||||||
| 6 | Message | varchar | 0 | ||||||
| 7 | IsRead | int | 0 | ||||||
| 8 | CreateTime | datetime | Now() | ||||||
| 9 | IsDelete | int | 0 | 
表名:Player
| 序号 | 列名 | 数据类型 | 长度 | 小数位 | 标识 | 主键 | 允许空 | 默认值 | 说明 | 
| 1 | ID | int | √ | ||||||
| 2 | Name | varchar | 50 | ||||||
| 3 | Password | varchar | 50 | ||||||
| 4 | Alliance | int | 0 | ||||||
| 5 | Nationality | int | 0 | ||||||
| 6 | Gender | varchar | 50 | ||||||
| 7 | Location | varchar | 50 | ||||||
| 8 | Info | varchar | 0 | ||||||
| 9 | Info2 | varchar | 0 | ||||||
| 10 | Population | int | 0 | 
表名:Log
| 序号 | 列名 | 数据类型 | 长度 | 小数位 | 标识 | 主键 | 允许空 | 默认值 | 说明 | 
| 1 | ID | int | √ | ||||||
| 2 | PlayerID | int | 0 | ||||||
| 3 | TypeID | int | 0 | ||||||
| 4 | Memo | varchar | 0 | ||||||
| 5 | LogTime | datetime | Now() | 
表名:Hero
| 序号 | 列名 | 数据类型 | 长度 | 小数位 | 标识 | 主键 | 允许空 | 默认值 | 说明 | 
| 1 | ID | int | √ | ||||||
| 2 | Name | varchar | 50 | ||||||
| 3 | PlayerID | int | 0 | ||||||
| 4 | VillageCode | int | 0 | ||||||
| 5 | Level | int | 0 | ||||||
| 6 | exp | int | 0 | ||||||
| 7 | BaseProperty | varchar | 50 | 
表名:GoodFriend
| 序号 | 列名 | 数据类型 | 长度 | 小数位 | 标识 | 主键 | 允许空 | 默认值 | 说明 | 
| 1 | ID | int | √ | ||||||
| 2 | PlayerID | int | 0 | ||||||
| 3 | GoodFriendID | int | 0 | ||||||
| 4 | Type | int | 0 | 
表名:Event
| 序号 | 列名 | 数据类型 | 长度 | 小数位 | 标识 | 主键 | 允许空 | 默认值 | 说明 | 
| 1 | ID | int | √ | ||||||
| 2 | VillageCode | int | 0 | ||||||
| 3 | TargetVillageCode | int | 0 | ||||||
| 4 | Type | int | 0 | ||||||
| 5 | BeginTime | datetime | |||||||
| 6 | EndTime | datetime | |||||||
| 7 | ShowText | varchar | 50 | ||||||
| 8 | EventObject | varchar | 0 | 
既然是概述,就没有太多详细的东西,本文主要针对asp.net开发环境。
webgame需要缓存的内容包括
1.游戏的配置信息
2.玩家的信息
对于游戏配置信息,通常是指游戏里一些固定不变的信息,例如建筑物每次升级时需要多少资源,需要多少时间等数据,这些数据当然可以写死在代码里,但通常这些数据应该放在代码外,要么是以文件形式存放(xml或者txt),要么就是放在数据库里。这部分数据的缓存很好做,只需要在应用开始时,统一做一次加载就可以了。一般来说,做过1年开发的同学都知道这种数据应该用单例模式来加载和使用,这点对java同样适用。当然做成静态属性也是可以的,只要把握好加载的时机就可以了。这里还顺便说一下,如果游戏的配置信息存在交叉访问(索引),则要注意两者之间的加载顺序。或者对交叉访问的部分不做索引,每次都动态的访问(查询)。
对于玩家的信息,则有一些说头了。基本上,现在的.net项目都做成3成结构+ORM访问。缓存的对象应该是游戏的实体对象。同上实体对象和数据库表之间都是一一对应的,这点没什么好多说的。以玩家或者村庄对象为了,它们的索引通常是ID,只需要创建对应的Dictionary<int, Player> 字典对象用来存取数据就可以了。
public Player Getplayer(int ID)
{
Player ret;
if (playerMap.TryGetValue(ID, out ret))
return ret;
ret = 从数据库里获取玩家对象(ID);
playerMap.Add(ID, ret);
return ret;
}
上面是一个基本的用于从缓存里访问玩家对象的方法。这点对大家来说,都不算很难,有点经验的同学都能写得出来。
下一步如何更新这个缓存就是这个缓存系统才是webgame的麻烦地方。
我们缓存的对象和web应用的对象不一样,它存在着随时变化的可能,并且当他发生变化时,需要能及时反馈给用户。
web应用我们以blog为例,当某位用户添加了新文章到cnblogs的首页,可能不会立即被其他用户看到,因为cnblogs首页的缓存信息还没有被修改。通常根据需要这些缓存信息可能会是1分钟,也有可能是10分钟,只有当缓存过期了以后,系统才会生成新的首页内容。其目的是减少首页的数据库查询访问量。
webgame游戏则不太一样,我花资源升级,就希望在页面上能立即看到变化,因此,当我们完成某项业务逻辑操作后,需要人工的更新资源对象的缓存。
{
// 游戏逻辑处理
// 数据库数据提交
缓存更新();
}
catch (Exception ex)
{
// 日志处理
}
按照通常的做法,每次逻辑操作都包含在一个事务里面,如果逻辑操作失败时,则可以对事务做撤销处理,尽量避免数据异常。
当然,这个也不是绝对的,前两天在QQ上谈论到缓存更新的问题时,某位同学帖出了他的代码,代码里,缓存的更是是在数据库事务提交之前。如果提交发生失败,则整个游戏系统已缓存的数据为主。这个问题咋一看来,和我们的思路不一样,我们就认为这样做有问题,后来回家的路上仔细的想了想,其实这样做也不无道理,因为它是在数据库提交之前更新缓存,也就是说,如果发生错误,唯一可能错误的地方就是写数据库时写失败了。但如果整个游戏系统是以缓存数据为准,只要游戏逻辑在计算时没发生错误,将错误的数据写入缓存,那么就算当前的数据修改提交到数据库失败了,数据还有可能在下一次修改时,提交一份正确的数据到数据库。整个系统不会因为数据库瘫痪了而无法运行。这点感觉和网游的服务器设计思路近似,毕竟对于网游来说,不可能每次玩家的操作都将数据写回数据库,玩家的数据都以在服务器内存里的数据为准,以定时的方式将内存数据会写到数据库。
其实这两种设计思路的差别就在于,数据是以数据库为中心还是以内存数据为中心。对与web系统来说,自然是以数据库为中心。从网游的角度来说,自然以内存数据为中心。而webgame是这两大系统的结合,其数据访问思路自然综合了这两种观点,具体到某个游戏,则需要根据游戏的需要而加以取舍了。
除了以字典为主的缓存设计外,还有一个重要的缓存对象的设计需要说一下,那就是地图。目前常见的Webgame(Travian,武林三国)都是以一张400*400的世界地图为玩家的交战地图。通常是一次性全部加载到内存里。存放的格式,自然是以x,y轴坐标为依据的二维表里。虽然首次加载是数据会比较慢一些,内存占用的空间会多一些,但当玩家查看地图页时,你会发现页面生成的数据比从数据库里获取相应数据要快很多。再加上现在服务器内存动则4G,8G的。则几十兆的地图数据还是毛毛雨了。
 
                     
                    
                 
                    
                

 
             
                
            
         浙公网安备 33010602011771号
浙公网安备 33010602011771号