格鬥遊戲的招式指令實際代碼是如何判斷的?

以KOF紅丸為例:
雷韌拳:↓↘→+A或C
真空片手駒:↓↙←+A或C
那麼在鍵盤指令輸入到遊戲內,
是如何去進行指令匹配的 需不需要考慮輸入時間進行清空輸入緩衝區等等。
我自己的想法是。以一個巨大的輸入緩衝區。如果碰到 發射指令(非方向指令),則進入緩衝區進行招式的遍歷匹配 只要命中一條 則清空輸入緩衝區,不知這樣是否妥當!


謝邀。

問題描述中紅丸的出招指令是錯的這個事我就不細說了,反正也不是重點。

先談談你的設計思路。

能不能行?能行。
有沒有缺點?有。

第一,沒有人規定出招必須要使用「方向鍵以外的按鈕」觸發。拿KOF舉個例子,→·→是跑動,↓·↗是大跳;MD幽游白書中甚至有純方向指令的必殺技,如飛影的殘像、陣的空中浮游。這些指令用你的系統是判斷不出來的。
第二,你用一個所謂緩衝區去存所有的指令,觸發某個技能的時候清空對吧?那我問你,Super Cancel這種功能你如何實現?比如說波動拳(取消)真空波動拳,實際指令簡化是↓↘→P↓↘→P,但是按你的設計,波動拳觸發的時候就把前面的↓↘→給清掉了,然後自然就放不出真空。

我說說真正是怎麼實現的。


你可以用狀態機的概念來理解,但是注意:出招檢測主要使用的並不是角色的狀態,而是每個技能有一套自己的狀態,或者說指令階段

拿波動拳來說,指令是↓,↘,→,P。
一開始是等待輸入↓。當系統檢測到↓被輸入的時候,就進入等待↘?的狀態,檢測到↘?時則繼續進入下一個狀態,等待輸入→。
關於輸入時間限制,通常有這麼幾種方案:

  • 一段式:接收到第一個指令輸入後開始計時,所有的搖桿、按鍵序列必須在X幀內完成。
  • 兩段式:接收到第一個指令輸入後開始計時,所有的搖桿序列輸入必須在X幀內完成,然後重置計時器進入按鍵猶豫期,按鍵必須在Y幀內完成。
  • 多段式:接收到第一個指令輸入後開始計時,下一個指令必須在X1幀內完成,然後重置計時器,再下一個指令必須在X2幀內完成……

例如HyperSFII採用的是多段式,也就是↓(猶豫期X1)↘(猶豫期X2)→(猶豫期Y)P。同一個必殺技中方向指令之間的猶豫期是一樣長的,最後的按鍵分輕中重有差異,你可以看做是不同的技能。像PPP同按這種必殺技必須同時輸入。
(日本玩家實測數據:http://www8.plala.or.jp/ichirou/hypersf2/input.htm)

系統按幀掃描搖桿/按鍵輸入,並且遍歷出招表。每一個符合條件的技能都會被更新狀態。

你可以簡單想像成類似這樣的結構:

角色
體力值
能量值
氣絕值
防禦耐久值
可行動/浮空/倒地……(這裡可以用一個屬性標記,也可以用多個屬性)
(其它屬性略)

出招狀態表(集合&<技能狀態&>)
技能
技能指令(集合&<指令&>)
按鍵
猶豫期
(動畫,判定等屬性略)
當前指令階段
當前計時器

確定角色、對局初始化的時候,先利用角色固有的出招表創建出招狀態表。

到實際遊戲中,每幀再像下面這樣處理。我這裡為了方便你理解,使用的是類似OO的偽代碼。實際老一點的街機遊戲都是在演算法中直接寫死內存地址和偏移量的。

foreach (var 技能狀態 in 角色.出招狀態表)
{
if (技能狀態.當前計時器 &>= 0)
{
var 技能 = 技能狀態.技能;
var 所需指令 = 技能.技能指令[技能狀態.當前指令階段];
if (當前按鍵輸入.Matches(所需指令.按鍵)) //實現略
{
if (所需指令.IsLast) //可以用指令數量判斷
{
if (CanCast(角色, 技能)) //實現略
{
角色.Cast(技能); //出招
技能狀態.當前指令階段 = 0;
技能狀態.當前計時器 = 0;
}
}
else
{
技能狀態.當前指令階段++;
技能狀態.當前計時器 = 所需指令.猶豫期;
}
}
else
{
技能狀態.當前計時器--;
}
}
else
{
技能狀態.當前指令階段 = 0;
技能狀態.當前計時器 = 0;
}
}

注意,我再強調一遍,每一個技能都會獨立參與計算。簡單示意如下:

(技能/狀態/計時器)
波動拳:0,0
升龍拳:0,0
旋風腳:0,0
(假設這三個技能的搖桿猶豫期都是固定6幀、按鍵猶豫期10幀)

第1幀輸入↓,
升龍拳:0,0
波動拳:1,6(激活)
旋風腳:1,6(激活)

第2幀不變,
升龍拳:0,0
波動拳:1,5(激活)
旋風腳:1,5(倒計時)

第4幀輸入↘,
升龍拳:0,0
波動拳:2,6(進入下一狀態)
旋風腳:1,3(繼續倒計時)

第7幀輸入→,
升龍拳:1,6(激活)
波動拳:3,10(進入下一狀態)
旋風腳:1,0(時間到,下一幀複位)

第11幀輸入P,
升龍拳:1,2(倒計時)
波動拳:4,0(觸發)
旋風腳:0,0

大概就是這樣的感覺。我上面示意的演算法還模擬了http://games.t-akiba.net/sf2/theo_sys.html中提到的「入力再開1フレーム」現象,例如搖桿拉住→(經過X幀後)↓↘+P,如果輸入↓的時候正好是升龍計時歸零的那一幀,那麼之前輸入的→並不被承認,於是也就發不出升龍拳。再晚一幀的話,由於歸零後重新接受了→,指令就是有效的了。
拿上面的例子說,如果你沒有輸入↘,而是在第8幀(複位)時才輸入,那麼之前的↓相當於被抹掉了,最後是發不出波動的。


蓄力系指令有單獨的蓄力計時器,這裡不贅述,作為程序員你應該有能力類推出來怎麼實現。
Match按鍵輸入和指令的具體實現我也沒有提,這對於遊戲手感其實是有很大影響的——能接受什麼樣的簡化指令?如果最後同時滿足了多個技能的指令序列,具體執行哪一個?不同作品的處理方式不同,造成的效果也不同。

舉個例子,街霸中按鍵和抬鍵都算作輸入。
像前面說的波動取消真空波動,↓↘→P(按住)↓↘→P(抬起)也能承認。要實現這一點,在處理按鍵的時候就不能只拿按鍵當前狀態,而是要考慮把「按鍵狀態變化」當成事件。設置一個極小的緩衝區就可以解決。

再例如KOF中的←↙↓↘→P這一招,
在KOF96中是必須每個方向都搖到位才可以。
KOF97就不同了:實際指令變成了←↓→P,而↙在KOF97中視為同時輸入了←和↓,因此只輸入↙(兩幀以上)→P就可以搖出來。同理,←↘→P也是可以的。
但是←↘P就不行,因為它最後一個方向要求嚴格匹配,必須要單獨輸入→才可以。輸入→之後可以隨便輸入其它方向,例如←↓→↘P也可以。像跳躍中↓↘→K這種必殺技,可以在地面上直接輸入↓↘→↗K(也就是所謂低空鳳凰腳)。KOF97中,大部分必殺技只要是猶豫期內完成輸入,最後按鍵的時候搖桿在哪個方向都無所謂,換句話說↓↘→↖K可以後跳低空鳳凰腳。KOF99就不行,↓↘→↗的技術還存在,但是↓↘→↖是不接受的,含有←要素的方向會強製取消掉↓↘→系的指令。

更多系統實測可以參見http://freett.com/motdouga/kof-top.html。


看到這你應該差不多基本明白指令掃描是怎麼回事了。順帶著應該也能明白模擬器上的簡化出招作弊的幾種原理(其實都是改內存):

  1. 延長技能設定的指令窗口(猶豫期),本來是6幀,延長到10幀,相當於速度慢點系統也接受,就會更容易搖出技能。
  2. 縮短蓄力技的蓄力時間設定,本來要蓄70幀,改成1幀,不用蓄也能阿里斯古了。
  3. 改變技能的狀態,把當前指令階段鎖定到最後一段,系統認為你前面的已經搖過了,像↓↘→+P只輸入最後的→+P就可以了。有點一鍵出招的感覺。

以上,禁轉。


又是這種問題,我還是不請自來了,不知道這樣的問題為什麼沒人邀請我,邀請我的都是些很奇怪的問題(笑)。我就簡單說說2002年那會接到的格鬥遊戲外包(侍魂某作的移植工作,現在的孩子可能不太知道侍魂是什麼遊戲,你可以百度一下,當年也是世界級大作)是怎麼做吧。
1,有效輸入與無效輸入
首先,你必須明白遊戲中有有效輸入和無效輸入2個概念,大多時候輸入是有效的,但是當你的角色處於一些動作幀的時候,幀的屬性(按現在的理解就是「填表內容」)會告訴程序2個信息:一是這幀時用戶輸入是否有效,二是這幀時用戶輸入緩存(按照現在的概念來看,應該叫做
「輸入緩存」)是否清空。這裡有一個關鍵在於:在邏輯的世界中(並非你肉眼看到的,首先你要能做動作遊戲,你的資質必須支持你看的見共存的兩個世界——現實世界和邏輯世界,他們是耦合的,但並不是完全互相映射的)無效輸入和清理緩存的幀,當你看到圖形的時候未必能夠直接聯想的到關係,這需要很有經驗的策劃和美術的配合(這是現代分工法,當年做動作遊戲的這一批人,要麼就是美術型策劃,要麼都是能寫邏輯代碼的)才能製作好。
2,「輸入緩存」的產生和遍歷
在且僅在用戶輸入有效的時候,我們會向緩存中寫入我們的操作記錄,這個結構是非常簡單的,如果用偽代碼寫出來,他就是:
KeyRecord = {"key":按鈕,"time":按下時間}
其中按鈕是這條記錄代表他按了什麼按鈕,對於這個是有一個特殊處理的:
1)方向按鈕,抬起的時候產生KeyRecord,記錄時間卻是按下時間,再按住方向按鈕沒有抬起的時候,也會周期性的增加KeyRecord,time都是根據周期開始時候獲得的。
2)動作按鈕(對應遊戲機上ABCXYZ等),抬起的時候產生KeyRecord,記錄時間也是抬起時間。
最後我們會在內存中有一串玩家輸入的2維數組(為什麼是二維?因為遊戲機有2-4個手柄,我們需要第一維記錄是哪個手柄發起的),去記錄這個KeyRecord。當然沒一個角色的每一個招式的出招表,也是用這個結構的一個數組array of KeyRecord。
在每次記錄的時候,我們還需要做兩件事情:
1,)根據遊戲設定的「操作指令輸入時間」,通常是100幀(如果一個人用「秒」來說這類遊戲的單位,就證明了他壓根沒有開發過這類遊戲,都是在想當然的,因為在那個環境里,你能得到的時間只有幀,所以你會發現現在一些模擬環境下,你啟用模擬器加速,你的操作可能會要求更高才能發出招),我們要吧數組中「超時」的KeyRecord全部刪除,通常這裡很容易優化,就是循環到第一個不需要刪除的就好了(這是小聰明,現在我仍然覺得有點危險)。
2)遍歷角色每一個招式,首先判斷最後一個按鍵是否符合現在創建的這個KeyRecord(也就是最新輸入,這也是小聰明優化,但是依然說不出有什麼不好的),首先如果還有1條以上符合這個條件,那麼在針對這些符合條件的去查詢他們的出招過程是否符合了出招。
3,當符合了出招表的輸入產生了,我們要幹什麼?
只是符合了出招條件,並不等於我們必須放出招式,這裡是一個很重要的Key,我們還有很多邏輯判斷要做:
1)怎樣找出我最想放的是哪招?
當你在遍歷「輸入緩存」的時候,你很有可能找到兩條甚至更多條都符合你想出招的操作,這是很常見的,比如(我這裡就不用KeyRecord了,因為寫時間,地球人是寫不出來的,我就寫一下這個「緩存」裡面有過哪些按鈕):↓↘→↙↓→↘→此時我按下的是A,這裡還必須先讓你知道一件事情,就是這裡的方向,是面對你角色Flip過得,比如你的角色面向左邊的時候按左,記錄在緩存裡面的是按右。你會發現這樣的記錄並不奇怪,如果按照出招表(玩家通常都是在遊戲攻略得知的出招表),他可能什麼都發不出來,而是砍了輕刀(在那一作侍魂A是輕刀B是中刀C是重刀D是踢腿Select是挑釁),但事實上用戶這時候的意思很可能有3個(如果用戶操作的是霸王丸):
可能性1:他就是想來個輕刀。
可能性2:旋風斬:我自己給的稱呼,反正就是飛一個龍捲風出去,「遊戲配置表」中是↓→A,遊戲攻略中給出的↓↘→A or B or C,因為寫攻略的人並不知道這其實有3個技能。
可能性3:升龍斬:也是我自己起的名,就是刀轉兩下再來個遨遊跟的那個,→↓↘A,當然槽點還是跟旋風斬一樣。
然而,得出這些以後,我們還會經過幾個步驟,來得出最後我最想放的是哪招,為什麼要有這樣一個過程?因為玩家可能覺得自己的操作是非常符合出招表的,但是出招表給出的並不是官方的數據配置表,而且配合輸入設備的一些限制,總是會有一些「噪音」被加入到你的操作過程中,如果我們嚴格的按照沒有「噪音」的來做,你會發現一個遊戲根本搓不出招,手感極爛(這個你可以在真說侍魂武士道列傳,這個侍魂的RPG版本中體驗到有多讓你抓狂):
Step1:目前是否接受按鍵,不接受啥都不做(接收條件上面說了)。
Step2:列出一個列表,把這些招式放入表裡,並且定個「親密度」,並且根據親密度把表從上到下排列。親密度是根據最接近尾部的那次操作來算的,我們還是看範例中的「緩存」,然後編個號:1↓,2↘,3→,4↙,5↓,6→,7↘,8→,9A(這是最新加入的,也是本次按鍵)。那麼旋風斬「最近一次」符合輸入是569,升龍斬最近一次符合是3579,取(緩存長度-最大2個按鈕次數之差/需要按鍵次數-abs(最後2次按鍵次數差-平均按鍵次數差))得到親密度(很可能小於0,不過這只是比大小的,所以並沒什麼問題),則可以看出:
旋風斬:需要按鍵次數=3,最大2個按鈕次數之差(這個名詞真不好定,都很拗口)=3(來自9-6)-1(來自6-5)=2,最後2次按鍵次數差=9-6=3,平均按鍵次數差=(3+1)/2=2,結果就是9-1-abs(2-3)=7(長度是9)
升龍斬:需要按鍵次數=4,最大2個按鈕次數之差=0(都是2),最後2次按鍵次數差=2,平均按鍵次數差=2,那麼結果就是9-0-0=9。
因此我們這時候得到了親密度列表順序是:升龍斬→旋風斬。然後我們再把單個按鍵企圖做的動作(當前按鍵是A,所以是輕刀)push到這個列表的最後。
2)得出了這個列表幹什麼呢?
並不是得出列表我立即就放了第一個動作了,那是不可能的事情。我們按照列表的順序遍歷,判斷當前的情況是否可以轉入這個動作,如果可以,那麼就進入這個動作並且return,否則就繼續遍歷下一個,直到列表沒貨。判斷這個動作是否有效的依據是:
a)當前是否有武器,這是侍魂的一個特殊處理點,不適用於所有遊戲,但這也是一層優化,其實原本完全可以根據當前角色的動作來確定是否能夠Cancel的,但是這樣就算設計師配置表都會很複雜,所以乾脆增加了「持有武器」這個boolean,要知道在侍魂裡面,旋風斬和升龍斬都是必須有武器才能放的,所以當你「持有武器」==false的時候,就可以直接下一個loop了。
b)當前的動作是否可以被現在判斷的這個動作Cancel,之前有一篇我也發在GameRes的帖子[經驗心得]動作遊戲中連招的實現說到了關於Cancel的概念。如果不能Cancel,那麼當前判斷的這個動作不能釋放。
3)找到或者找不到合適的動作,我都要幹什麼?
通過上面一步,我們也許能找到一個合適的動作,作為現在要做的動作,那麼就讓角色立即做那個動作,並且清空這個「緩存」。如果沒有找到,則不執行任何改變。

以上這些是我的一些項目的實戰經驗,希望對大家有所幫助。


Buff只保存當前最近的4個字元,用數組保存大招指令集,然後判斷buff中的4個字元是否在數組中,這樣夠簡單了吧,效率最快吧


在技能的按鍵個數都是相同的情況下,可以用一個二維數組作為技能倉庫,然後用list來儲存玩家的按鍵,當按鍵數量達到技能的按鍵數量時開始對技能倉庫進行遍歷。晚些時候貼代碼。


樹結構吧,用技能的按鍵組合初始化,根結點處記錄時間,葉子結點觸發技能時間,沒有子結點時返回根節點。


這個我在自己的小遊戲中也有考慮過,我自己弄了一種簡單的組成方式,效率不是很高但是可擴展性還好。用的是Unity5.6實現。

首先我將連招信息做成預設物如圖:

其中的AttcckLinkString就是連招需要輸入的字元串,例如SDSDJ就是一個連招的話,這裡的AttcckLinkString就是SDSDJ。

然後將這些AttackLink統一用一個控制單元控制並搜索,如圖:

這樣輸入的時候就可以做檢測,例如輸入了SD:

可以看到MayUsing裡面只有SD開頭的連招字元串了,其實就是一個搜索的過程。

這個方法的特徵在於用的是字元串,所以如果想要加入新的招式動作,拿到一個預設物做字元串的賦值就可以了。

附上簡單的代碼如下:

//檢測連招的方法,每一次按鍵都要求檢測

//這個是根據玩家的輸入進行控制的方法,在製作玩家AI時需要繼承並重寫方法

// useInterpret標記著是否使用對應的翻譯機制

//對於人的輸入必須要用翻譯,對於AI則不需要

public virtual void makeAttack(string keyString,bool useInterpret = true)

{

if (!canControll)

return;//如果當前是不可操控的,那麼直接返回

attackBeDelete.Clear ();//每一次檢測都會刷新這個集合

char keyChar=" ";

if (string .IsNullOrEmpty( keyString) ==false)

{ //此處有待商榷,用這種方式只能支持A-Z的按鍵輸入(需要加入轉換的方法)

//keyChar = keyString.ToCharArray () [0];//取出這個字元

if (useInterpret)//人輸入也就是默認狀態下是需要「翻譯」的而AI輸入或者用於展示的輸入則不需要這一步

{

keyChar = systemValues.getFromA (thePlayer.playerIndex, keyString);

keyChar = directForKey (keyChar);//按鍵的方向調整

}

else

keyChar = keyString.ToCharArray() [0];

//print("--keyChar--"+keyChar);

foreach (attackLink AL in attackLinkMayUsing) //輸入符合要求,可以進行下一步的檢測了

{

char getChar = AL.getCharWithIndex (index); //獲取char

//正是因為獲取的是char,這個方法有很大的限制

if ( getChar != keyChar )

{

attackBeDelete .Add (AL);//將不符合規範的輸入去除掉

}

}

foreach (attackLink AL in attackBeDelete)

{

//因為List數據結構正在被使用(foreach),因此需要將刪除與遍歷分開進行

attackLinkMayUsing.Remove (AL);//記錄當前需要刪除的技能

}

if (attackLinkMayUsing.Count == 0)

{//如果當前已經沒有用於檢測的串,說明輸入不對,重頭開始

flashLink ();//更新列表

reMake();//完全重頭開始

}

else

{

startTimer = true;//檢測到了符合的公式,則開始計時

timerForLinkAtack += (timerAddForCheck* 1/timerDifficulty);

//有一點加成等待下一個鍵位的輸入(手殘黨的福利)

check ();//進行檢查

}

}

else

{

flashLink ();//更新列表

reMake();//完全重頭開始

}

}

void check()//真正進行檢測的方法

{

bool isOver = false;//標記量

theAttackLinkNow = null;//每次檢查之前都需要清空引用

foreach(attackLink AL in attackLinkMayUsing)

{

if (AL.attackLinkString.Length == index + 1 )//因為長度和index的關係,就比本的要求就是輸入正確(當然還有SP等等的需求)

{

//這個判斷非常的重要,如果取消,任何攻擊動作都有可能中間取消,這當然不符合我們的需求

if(canChangeToNextAttack())

{

thePlayer.onPlayattackActions ();

//如果遊戲人物是AI才會做下面的操作,玩家來玩沒必要使用

if(thePlayer .GetComponent&())

{

theAttackLinkNow = AL;//記錄當前選中的AL

Invoke("attackLinkNowFlash",1f);

}

AL.attackLinkEffect ();//發生效果

flashLink ();//更新列表

reMake();//完全重頭開始

isOver = true;//標記量,是否已經使用了一個技能

break;//每一次生效的連招只能夠有一個

}

}

}

if(!isOver )

index ++;//向下一個目錄前進

}


為什麼沒有人提到責任鏈或者響應鏈模式,現在手機上的手勢識別就是這麼做的?


這是一個狀態機,並不需要進行指令緩存
最簡單的:
先設定一個計時器,比如2秒,時間到的時候強制轉換狀態為初始狀態。
初始狀態就不要解釋了吧。
接收按鍵指令的時候,切換到當前狀態對於接受按鍵所對應的狀態,同時激活計時器。
當狀態走到了要出招的狀態是,直接出招並切回初始狀態。

舉個例子:
按鍵有1、2、3、4、A,一共5個
初始狀態為0
比如現在只有一招,操作是12A
狀態切換大概是這樣的:
0:
接1-&>1;接2、3、4、A-&>0.
1:
接2-&>12;接1、3、4、A-&>0.
12:
接A-&>12A(出招),接1、2、3、4-&>0.
12A:
直接切為0.(也可以沒有這個狀態,在12狀態的時候直接出招並切為0).

這種就類似於網上的那種測試題,選A轉5題,選B轉6題,最終會轉到結果ABCD。

使用這種邏輯就可以完全避免緩存按鍵記錄和掃描出招表了,只需要對這個狀態機進行維護
這個狀態機里,出去返回0狀態的路徑意外,其他結構都是一個單向的樹結構,外加一個計時器中斷,很簡單。

如果你學過編碼的話就更簡單了,每一個招式其實都是一個編碼,我們的狀態機就是解碼器,碰到了認識的編碼就把它輸出出來(出招),不認識的就當它不存在(歸0)


需要時間,需要清楚緩存。
沒做過,不過要是我做的話,就用散列表,每次輸入之後離開當前輸入的死區沒有離開連招判定時間的時間內獲取輸入,離開連招判定就到表裡面查。
這個更應該和動畫相關,動畫的哪一幀開始可以被打斷,哪一幀是結束點。決定了可以輸入和可以連擊的時間,然後這個還可以通過buff機制來做吧,比如你輸入一個鍵就加了一個可以連擊的 buff,buff就可以定製了,除了是否可以連擊還有是否霸體,是否可以受身之類的。
匹配是逃不掉的,怎麼存儲技能表和對應狀態等信息的結構,結構清晰方便策劃配置,便於理解方便debug,查詢速度快,這些比較有研究價值


你這麼做是可以的,而且也不需要很大的buffer,做一個長度等於最長連招的循環buffer即可。但是需要注意處理超時的情況。另外,如果這裡的字元串匹配演算法做的不好,是相當費時的。

我覺得比較好的做法是用狀態機來做,速度非常快,但要提前構造狀態機。這裡不好畫圖,我也沒法細說了。狀態機是需要根據每個人物的連招來進行構造的。比如一個人有n個連招,那麼,這個人有一個初始狀態,許多中繼狀態,和n個匹配狀態,每一次按鍵輸入都有一次狀態轉移,轉到某個連招對應的匹配狀態就發出連招即可,發完就可以回到初始狀態。

對於超時的處理也可以一併進行,只要每進入一個狀態重置計時器即可,計時器到點就發出一個重置事件(返回初始狀態)。另外,為了防止連招的連續發出,可以在加入一些cooldown狀態,具體狀態機怎麼構造就看你的需求了。


推薦閱讀:

街頭霸王需要6個鍵,這種系統是好還是不好?
格鬥遊戲中使用快捷鍵出招或者宏有沒有優勢?

TAG:遊戲設計 | 遊戲開發 | 代碼 | 程序 | 格鬥遊戲FTG |