CONDITION系統分析


  condition是利用系統的心跳來解決關于在一個不算太長的時間里,定時觸發種現象的解決方法。在MUD中,為了解決定時觸發某種現象,一般有兩種方法,一種是通過call_out()延時呼叫,另一種就是通過心跳。在spock翻譯的LPC及教材中曾對這兩種方法進行了比較,好象是說:如果時間較長的話采用心跳比較好,時間短的話采用call_out()。緊接著又說了句,其個人看不出這兩者有什么必要的區別,反正稀里糊涂的。不過,在實際運用中,兩者都有不同的效果,大家看明白了本文之后,可以有自己的理解,并進行正確的選擇。
  call_out(other_fun,t)這個外部函數就是起一種延時呼叫的作用,后面必須要加第一個參數,指定延時呼叫的函數名,第二個參數表示延時几秒,再之后加上的參數可以作為呼叫的那個函數的參數。象這一個也就是設定在t秒鐘后,呼叫other_fun這個事先指定的函數,你可以在這個函數里設定好應該進行的事情。比如,你在一個玩家進入監獄后,要做牢五分鐘,如果一定要用call_out()進行的話,你可以設定成:
call_out("out_jianyu",300,ob);
  意思就是在300秒后呼叫out_jianyu()這個函數,ob作為這個玩家的變量傳遞過去,然后在這個函數里進行將玩家從牢中放出來,告訴他做牢結束等等處理。但是,有的巫師看到這里,就會問了,如果五分鐘沒到,這個玩家退出游戲了怎么辦?對,如果這個玩家退出游戲,這個call_out()一到時間就會找不到這個操作對象,如果程序寫得不好就容易出錯,即使是不會出錯,那個玩家再次進入游戲后也就無法繼續剛才的延時過程,也就不能夠出牢。于是,對于要跨起離線前后的象做牢這類的事,大多都是采用condition。
  condition的處理方法,就是在開始的時候在玩家身上設定一定的點數的記號,這些記號會通過save()保存進玩家的檔案中。然后通過系統的心跳,每一次心跳就執行一次這個固定的condition的函數,函數每被執行一次就減一點記號,直至這個記號為0后,就可以觸發某種效果。比如做牢,就在進牢時設定一定點數,每一次心跳減一點,減為0時,將玩家放出。由于這個記號在玩家身上,因此,在玩家離線時不會發生到有關函數對這個玩家的操作。
  在ES系列MUD中。一個完整的condition系統包括三個部分:
一、首先就是調用或者說是觸發它的程序,也就是/inherit/char/char.c這個
文件,這是所有玩家與NPC都共同繼承的文件,在里面的heart_beat()函數,就是每一次心跳時調用執行的函數。里面有這么几句:
  if( tick-- ) return;
  else tick = 5 + random(10);
  cnd_flag = update_condition();
解釋:tick是這個文件里的一個全局變量,假設它只要>1,那么tick--就不會等于0,那么tick值就會減1,并且立即return;中止這個函數向下執行。假想如此這樣經過几次減1之后,終有一次tick--就會為0,那么這時,程序就不會中止而是執行下一句的else。這時,tick被重新賦值為5+random(10)。并執行update_condition()函數,update_condition()是一個什么函數呢?在char.c里怎么也找不到。
  那么我們再回頭看看char.c這個文件的開頭,就會看到,這個文件已經繼承了/feature/condition.c文件,update_condition()這個函數正是在這個文件里面。所以下面我們就開看這個文件。
  附:由于大多數MUD里的心跳是每兩秒調一次,5+random(10)是5至14次,因此可以看出每一個condition被調用的時間是平均19秒。知道了這此,你才能更加有數地設定一些毒的發作時間,一些做牢的時間長短了。

二、下面就是主程序,feature目錄下的condition.c文件。
程序詳解:

#include "condition.h"//繼承一些宏定義
mapping conditions;//定義一個映射集

/*更新函數 update_condition()
  這個函數首先要檢查玩家身上的每一個condition是否有效,如果出現了無效的condition,它會記錄進/log/condition.err這個文件里,經常性地檢查一下這個文件,可以發現并排除相當多的錯誤,因為一旦有一個錯誤,會在每一次的心跳中經常性地發生。然后它會按照condition的名字,也就是str這個變量去/kungfu/condition(某些MUDLIB下的路徑是/daemon/condition)目錄下去 尋找同名的.c文件進行執行。*/
nomask int update_condition()
{
  mixed *cnd, err;
  int i, flag, update_flag;
  object cnd_d;
  if( !mapp(conditions) || !(i=sizeof(conditions)) ) return 0;
//判斷玩家有無condition的映射,沒有就中止,畢竟不會每人都會有
  cnd = keys(conditions);
//從這個映射中把關鍵詞取出組成一個數組,實際上就是不同condition的名字
  update_flag = 0;//初始化這個變量
  while(i--)
//如果有1個以上的condition,就會一個個地循環執行
  {
    cnd_d = find_object(CONDITION_D(cnd[i]));
//到放condition的目錄下尋找這個文件名,這個目錄路徑有宏定義的文件指定,一般在XKX風格里大多是kungfu/condition/下,xyj等放在/daemons/condition/下
    if( !cnd_d )//如果沒有的話,再嘗試
    {
      err = catch(call_other(CONDITION_D(cnd[i]), "???"));//強制檢驗
      cnd_d = find_object(CONDITION_D(cnd[i]));//再次尋找
      if( err || !cnd_d )//如果強制檢驗與再次尋找中仍找不到,表示不存在這個condition
      {
        log_file("condition.err",sprintf("Failed to load condition daemon %s, removed from %O\nError: %s\n",CONDITION_D(cnd[i]), this_object(), err) );
//記錄下來
        map_delete(conditions, cnd[i]);
//刪除玩家身上的這個condition
        continue;//繼續循環下一個
      }
    }
    flag = call_other(cnd_d, "update_condition", this_object(), conditions[cnd[i]]);
//這個就開始執行這個conditon的設定文件里的update_condition()函數了,flag就是返回值,這個要看下面的具體設定文件的詳解
    if( !( flag & CND_CONTINUE ) )
      map_delete(conditions, cnd[i]);
//返回值為0就表示這個conditon完畢了,就刪掉
    update_flag |= flag;
  }
  if( !sizeof(conditions) ) conditions = 0;
//檢查一個也沒有了,就清零
  return update_flag;
}

/*改變大小函數 apply_codition(cnd,info),通過這個函數將玩家身上名叫的cnd的condition的值設為info,info可以為0,所以可以得用這個函數對玩家身上的condition進行增減。 */
nomask void apply_condition(string cnd, mixed info)
{
  if( !mapp(conditions) )
    conditions = ([ cnd : info ]);
//如果沒有的話,就添加上,以cnd為鍵名,info為內容值
  else
    conditions[cnd] = info;
//有的話,直接改變內容值
}

/*取值函數 query_codition(cnd)通過這個函數,可以調出某個玩家身上名叫cnd的這種condition的值有多少。*/
nomask mixed query_condition(string cnd)
{
  if( !mapp(conditions)||undefinedp(conditions[cnd]) )
    return 0;
//如果沒有conditions或者沒有這個cnd的condition,就返回為0
  return conditions[cnd];//否則返回具體值
}

/*清除函數 clear_condition()這個主要是在/feature/damage.c里的die()函數里調用,意思是一旦死亡,死者就會被清除所有的comdition。*/
nomask void clear_condition()
{
  conditions = 0;
}
//END

三、下面就是上面說的與cnd同名.c文件--具體設定文件。一般每一種condition種類都要對應一個文件。里面只有一個update_condition()函數,通過/feature/condition.c這個程序來調用它的這個函數,可以添加一些效果或信息,比如,中毒的就會減精減氣。但最主要是就每調用一次,將鍵名為這個cnd的內容值減少1點或多點。然后到了設定的點數,一般是到了0之后,會出現一些特殊的現象。象做牢的到了0就會被放出來,等等。
  下面看一個例子:少林的做牢的condition的詳解:
//kungfu/condition/bonze_jial.c
#include <ansi.h>
#include <login.h>

/*參照前面調用這個函數的/feature/condition.c文件,就可以發現,調用它的時候會傳遞一個玩家與他的名為bonze_jia1的這個condition的值*/
int update_condition(object me, int duration)
{
  if (duration < 1)
//如果這個點數參數小于1,就表示做牢時間夠了
  {
    me->move("/d/shaolin/guangchang1");
//從牢房里直接移到少林大門口
    message("vision","只聽乒地一聲,你嚇了一跳,定睛一看,\n"
    "原來是一個昏昏沉沉的家伙從大門里被扔了出來!\n",environment(me), me);
    tell_object(me, HIY "只覺一陣騰云駕霧般,你昏昏沉沉地被扔出了少林寺!\n" NOR);
//出一些信息
    me->set("startroom", START_ROOM);
    return 0;
  }
//如果不小于1,則設定減去一點,返回1,下次還會再執行
  me->apply_condition("bonze_jail", duration - 1);
  return 1;
}


  到這里,condition的三大部分介紹完了,我們就以這少林寺的做牢為例,看一看condition執行調用的流程。
  程序大約是在松林里被僧兵抓住后,進了戒律院,這個房間文件會通過apply_condition()這個函數在玩家身上被加上了名為bonze_jail的condition。于是玩家身上會多了這樣的一個映射:
  condition([({"bonze_jial":35},.....)])
  那么通過前面的文件,就會看到,由于我們玩家是繼承了char.c文件,每一個心跳中,就會檢查tick,如果tick為1時,那么就會開始調用update_conditon()函數了。
  這個函數首先取出玩家身上conditions映射中的關鍵詞組成一個數組cnd:
  cnd = ({"bonze_jial",......});
  首先發現了bonze_jia1,于是開始到/kungfu/condition/目錄下尋找名為bonze_jial.c的文件,也就是上面帖出的第三部分的文件,如果沒有找到這個文件就會被記錄進/log/condition.err文件里,有的話,開始傳遞時這個玩家與這個conditon的值,開始執行bonze_jial.c里的update_condition()函數 。第一次執行時duration也就是35,只要它大于或等1,就會被減出一點,返回值是1。會在下次tick為1的心跳是再次呼叫。這樣經過若干次調用后,duration已經等于0了,那么也就是durtion < 1,于是me被move到少林的廣場,出現信息:只聽乒地一聲,你......me又被覆蓋了startroom然后返回值為0。玩家身上就會被刪除這個condition。由于返回是0,那么/feature/condition.c文件里的update_condition()函數就會刪除玩家身上的這個bonze_jia1的內容。


  除了做牢之外,通過這樣的隨機調用,可以實現象毒這樣不定時發作的特殊效果,每調用一次就讓玩家減一些精呀氣之類的,卻出現一些恐怖的中毒信息症狀之類的。其實,你還以設計一些特殊的毒,不但在發作的過程中傷害人體。而且要求必須在這段時間找到解毒方法或解藥,否則,一旦到了最后一兩點還沒有徹底解毒,這個人就OVER死翹翹。呵呵,看起來有點恐怖呀,實際上我覺得這樣才是最符合實際情況的呢!

  采用condition處理的好處就在于,這個屬性是加在玩家身上的,通過在游戲中的心跳進行調用。如果玩家離了游戲就不會調用得到。一旦玩家進入游戲中又會開始繼續執行。這比call_out()一旦主呼叫者或呼叫中的參數失也就無法繼續執行的缺點要靈活得多。此外,它由心跳調用,不管玩家在做什么事情,它都可以即時地主動反饋情況。

  而如果你無需即時反映變化,比如說去領工資,只需要規定玩家在一定時間的間隔之內不能領兩次工資的話。可以在第一次領的時候,在玩家身上記下領的時間,第二次再去領時,將當前時間與記錄時間進行上減,看間隔夠不夠就行。象這樣子就很簡單,也几乎沒有任何系統負責,比采用condition林節約得多。而如果你需要有時間一到就立即通知玩家去領下一次工資的話。那就必須要采用conditon。
  它的缺點也顯而易見,放在每一次的心跳里呼叫。如果一個MUD系統里的生物與玩家身上的condition過多過長的話,系統的負擔也是不小的。

  最后談一談有關的BUG。許多新巫師沒有正確或完全理解condition的用法,就開始大范圍地使用。在一些不應該用的地方也使用condition。舉例,在某一個可以使玩家得到金錢等東西地方,設定任何玩家都必須間隔在線一定的時間才能去拿一次。如果這個通過condition來實現。就有可能利用死亡后清除所有condition的特點,讓一個玩家反復去死亡再立即重新去拿錢拿東西。象這種情況,你或者要修改死亡后會清除所有condition,或者就不能采用它來做那些拿錢等東西的效果。
  其二,象上面所講的少林監獄的condition文件中,就隱藏著一個BUG。因為在XKX的文件里,少林的監獄并非是一定要等時間到了才會被放出來,可以通過賄賂獄卒,走五行洞先出來。而這樣的話,一旦這個conditon到了最后的時候,不論這個玩家在哪里,都會被一下子Move到少林廣場,然后出現你被扔出監獄等的字樣。仔細想一想:如果MUD有不讓你隨便帶東西或正常出來的地方,那么你就可以通過這個BUG跑到這種地方,把那些不能帶出的東西放在身上,專心等時間一到,就會象乘飛機一樣,一下子從那個地方飛到少林的廣場。(例如誰與爭鋒里演武場)
  解決方法很簡單,只要在if(duration < 1)下面再加一個條件:
  if(environment(me)->query("short")=="監獄")
  才會move玩家出現信息。否則直接reuturn 0;就解決了。

  最后,舉一個毒的例子進行詳解作為結束吧:
// /kungfu/condition/ice_poison.c

#include <ansi.h>
#include <condition.h>

inherit F_CLEAN_UP;

int update_condition(object me, int duration)
{
  if( duration < 1 ) return 0;
  if( !living(me) )
//如果對象不是清醒著,出的信息
  {
    message("vision", me->name() + "渾身顫抖,痛苦地哼了一聲。\n", environment(me), me);
  }
  else//否則就清醒著
  {
    tell_object(me, HIB "忽然一陣奇寒從丹田升起,沁入四肢百骸,你中的寒冰綿掌發作了!\n" NOR );
    message("vision", me->name() + "的身子突然晃了兩晃,牙關格格地響了起來。\n",environment(me), me);
  }
  me->receive_wound("qi",15 + random(10));
  me->receive_wound("jing", 10);
//傷一定的精與氣
  me->apply_condition("ice_poison", duration - 1);
//這個值減一點
  if ( (int)me->query_temp("powerup") )
  {
    me->add_temp("apply/attack", -(int)(me->query_skill("force")/3));
    me->add_temp("apply/dodge", -(int)(me->query_skill("force")/3));
//降低他的攻擊力與躲避力
    me->delete_temp("powerup");
//結束他的powerup
  }
  if( duration < 1 ) return 0;
//如果到了0,就返回,什么也不做,表示毒發結束,好了
  return CND_CONTINUE;
}

下篇專題 系統刷新與清除分析