1. 程式人生 > >unity幀同步遊戲極簡框架及例項(附客戶端伺服器原始碼)

unity幀同步遊戲極簡框架及例項(附客戶端伺服器原始碼)

閱前提示: 
此框架為有幀同步需求的遊戲做一個簡單的示例,實現了一個精簡的框架,本文著重講解幀同步遊戲開發過程中需要注意的各種要點,伴隨框架自帶了一個小的塔防sample作為演示.

目錄:

哪些遊戲需要使用幀同步

如果遊戲中有如下需求,那這個遊戲的開發框架應該使用幀同步: 

  • 多人實時對戰遊戲 
  • 遊戲中需要戰鬥回放功能 
  • 遊戲中需要加速功能 
  • 需要伺服器同步邏輯校驗防止作弊

LockStep框架就是為了上面幾種情況而設計的.

如何實現一個可行的幀同步框架

主要確保以下三點來保證幀同步的準確性: 

  • 可靠穩定的幀同步基礎演算法 
  • 消除浮點數帶來的精度誤差 
  • 控制好隨機數

幀同步原理

相同的輸入 + 相同的時機 = 相同的顯示

客戶端接受的輸入是相同的,執行的邏輯幀也是一樣的,那麼每次得到的結果肯定也是同步一致的。為了讓執行結果不與硬體執行速度快慢相關聯,則不能用現實歷經的時間(Time.deltaTime)作為差值閥值進行計算,而是使用固定的時間片段來作為閥值,這樣無論兩幀之間的真實時間間隔是多少,遊戲邏輯執行的次數是恆定的,舉例: 
我們預設每個邏輯幀的時間跨度是1秒鐘,那麼當物理時間經過10秒後,邏輯便會執行10次,經過100秒便會執行100次,無論在執行速度快的機器上還是慢的機器上均是如此,不會因為兩幀之間的跨度間隔而有所改變。 
而渲染幀(一般為30到60幀),則是根據邏輯幀(10到20幀)去插值,從而得到一個“平滑”的展示,渲染幀只是邏輯幀的無限逼近插值,不過人眼一般無法分辨這種滯後性,因此可以把這兩者理解為同步的.

如果硬體的執行速度趕不上邏輯幀的執行速度,則有可能出現邏輯執行多次後,渲染才執行一次的狀況,如果遇到這種情況畫面就會出現卡頓和丟幀的情況.

幀同步演算法

基礎核心演算法

下面這段程式碼為幀同步的核心邏輯片段:

<span style="color:#000000"><code>m_fAccumilatedTime = m_fAccumilatedTime + deltaTime;

<span style="color:#880000">//如果真實累計的時間超過遊戲幀邏輯原本應有的時間,則迴圈執行邏輯,確保整個邏輯的運算不會因為幀間隔時間的波動而計算出不同的結果</span>
<span style="color:#000088">while</span> (m_fAccumilatedTime > m_fNextGameTime) {

    <span style="color:#880000">//執行與遊戲相關的具體邏輯</span>
    m_callUnit.frameLockLogic();

    <span style="color:#880000">//計算下一個邏輯幀應有的時間</span>
    m_fNextGameTime += m_fFrameLen;

    <span style="color:#880000">//遊戲邏輯幀自增</span>
    GameData.g_uGameLogicFrame += <span style="color:#006666">1</span>;
}

<span style="color:#880000">//計算兩幀的時間差,用於執行補間動畫</span>
m_fInterpolation = (m_fAccumilatedTime + m_fFrameLen - m_fNextGameTime) / m_fFrameLen;

<span style="color:#880000">//更新渲染位置</span>
m_callUnit.updateRenderPosition(m_fInterpolation);</code></span>

渲染更新機制

由於幀同步以及邏輯與渲染分離的設定,我們不能再去直接操作transform的localPosition,而設立一個虛擬的邏輯值進行代替,我們在遊戲邏輯中,如果需要變更物件的位置,只需要更新這個虛擬的邏輯值,在一輪邏輯計算完畢後會根據這個值統一進行一輪渲染,這裡我們引入了邏輯位置m_fixv3LogicPosition這個變數.

<span style="color:#000000"><code><span style="color:#880000">// 設定位置</span>
<span style="color:#880000">// </span>
<span style="color:#880000">// @param position 要設定到的位置</span>
<span style="color:#880000">// @return none</span>
<span style="color:#000088">public</span> <span style="color:#000088">override</span> <span style="color:#000088">void</span> <span style="color:#009900">setPosition</span>(FixVector3 position)
{
    m_fixv3LogicPosition = position;
}</code></span>

渲染流程如下: 

只有需要移動的物體,我們才進行插值運算,不會移動的靜止物體直接設定其座標就可以了

<span style="color:#000000"><code>//只有會移動的物件才需要採用插值演算法補間動畫,不會移動的物件直接設定位置即可
if ((m_scType == <span style="color:#009900">"soldier"</span> || m_scType == <span style="color:#009900">"bullet"</span>) && interpolation != <span style="color:#006666">0</span>)
{
    m_gameObject<span style="color:#009900">.transform</span><span style="color:#009900">.localPosition</span> = Vector3<span style="color:#009900">.Lerp</span>(m_fixv3LastPosition<span style="color:#009900">.ToVector</span>3(), m_fixv3LogicPosition<span style="color:#009900">.ToVector</span>3(), interpolation)<span style="color:#880000">;</span>
}
else
{
    m_gameObject<span style="color:#009900">.transform</span><span style="color:#009900">.localPosition</span> = m_fixv3LogicPosition<span style="color:#009900">.ToVector</span>3()<span style="color:#880000">;</span>
}</code></span>

定點數

定點數和浮點數,是指在計算機中一個數的小數點的位置是固定的還是浮動的,如果一個數中小數點的位置是固定的,則為定點數;如果一個數中小數點的位置是浮動的,則為浮點數。定點數由於小數點的位置固定,因此其精度可控,相反浮點數的精度不可控.

對於幀同步框架來說,定點數是一個非常重要的特性,我們在在不同平臺,甚至不同手機上執行一段完全相同的程式碼時有可能出現截然不同的結果,那是因為不同平臺不同cpu對浮點數的處理結果有可能是不一致的,遊戲中僅僅0.000000001的精度差距,都可能在多次計算後帶來蝴蝶效應,導致完全不同的結果 
舉例:當一個士兵進入塔的攻擊範圍時,塔會發動攻擊,在手機A上的第100幀時,士兵進入了攻擊範圍,觸發了攻擊,而在手機B上因為一點點誤差,導致101幀時才觸發攻擊,雖然只差了一幀,但後續會因為這一幀的偏差帶來之後更多更大的偏差,從這一幀的不同開始,這已經是兩場截然不同的戰鬥了. 
因此我們必須使用定點數來消除精度誤差帶來的不可預知的結果,讓同樣的戰鬥邏輯在任何硬體,任何作業系統下執行都能得到同樣的結果.同時也再次印證文章最開始提到的幀同步核心原理: 
相同的輸入 + 相同的時機 = 相同的顯示 
框架自帶了一套完整的定點數庫Fix64.cs,其中對浮點數與定點數相互轉換,操作符過載都做好了封裝,我們可以像使用普通浮點數那樣來使用定點數

<span style="color:#000000"><code>Fix64 <span style="color:#006666">a</span> = (Fix64)<span style="color:#006666">1</span><span style="color:#880000">;</span>
Fix64 b = (Fix64)<span style="color:#006666">2</span><span style="color:#880000">;</span>
Fix64 c = <span style="color:#006666">a</span> + b<span style="color:#880000">;</span></code></span>

關於dotween的正確使用

提及定點數,我們不得不關注一下專案中常用的dotween這個外掛,這個外掛功能強大,使用非常方便,讓我們在做動畫時遊刃有餘,但是如果放到幀同步框架中就不能隨便使用了. 
上面提到的浮點數精度問題有可能帶來巨大的影響,而dotween的整個邏輯都是基於時間幀(Time.deltaTime)插值的,而不是基於幀定長插值,因此不能在涉及到邏輯相關的地方使用,只能用在動畫動作渲染相關的地方,比如下面程式碼就是不能使用的

<span style="color:#000000"><code>DoLocalMove() <span style="color:#000088">function</span><span style="color:#4f4f4f">()</span>
    <span style="color:#880000">//移動到某個位置後觸發會影響後續判斷的邏輯</span>
    m_fixMoveTime = Fix64.Zero;
<span style="color:#000088">end</span></code></span>

如果只是渲染表現,而與邏輯運算無關的地方,則可以繼續使用dotween. 
我們整個幀框架的邏輯運算中沒有物理時間的概念,一旦邏輯中涉及到真實物理時間,那肯定會對最終計算的結果造成不可預計的影響,因此類似dotween等動畫外掛在使用時需要我們多加註意,一個疏忽就會帶來整個邏輯運算結果的不一致.

隨機數

遊戲中幾乎很難避免使用隨機數,恰好隨機數也是幀同步框架中一個需要高度關注的注意點,如果每次戰鬥回放產生的隨機數是不一致的,那如何能保證戰鬥結果是一致的呢,因此我們需要對隨機數進行控制,由於不同平臺,不同作業系統對隨機數的處理方式不同,因此我們避免使用平臺自帶的隨機數介面,而是使用自定義的可控隨機數演算法SRandom.cs來替代,保證隨機數的產生在跨平臺方面不會出現問題.同時我們需要記錄下每場戰鬥的隨機數種子,只要確定了種子,那產生的隨機數序列就一定是一致的. 
部分程式碼片段:

<span style="color:#000000"><code><span style="color:#880000">// range:[min~(max-1)]</span>
<span style="color:#000088">public</span> <span style="color:#000088">uint</span> <span style="color:#009900">Range</span>(<span style="color:#000088">uint</span> min, <span style="color:#000088">uint</span> max)
{
    <span style="color:#000088">if</span> (min > max)
        <span style="color:#000088">throw</span> <span style="color:#000088">new</span> ArgumentOutOfRangeException(<span style="color:#009900">"minValue"</span>, <span style="color:#000088">string</span>.Format(<span style="color:#009900">"'{0}' cannot be greater than {1}."</span>, min, max));

    <span style="color:#000088">uint</span> num = max - min;
    <span style="color:#000088">return</span> Next(num) + min;
}

<span style="color:#000088">public</span> <span style="color:#000088">int</span> <span style="color:#009900">Next</span>(<span style="color:#000088">int</span> max)
{
    <span style="color:#000088">return</span> (<span style="color:#000088">int</span>)(Next() % max);
}</code></span>

伺服器同步校驗

伺服器校驗和同步運算在現在的遊戲中應用的越來越廣泛,既然要讓伺服器執行相關的核心程式碼,那麼這部分客戶端與伺服器共用的邏輯就有一些需要注意的地方. 

邏輯和渲染如何進行分離

伺服器是沒有渲染的,它只能執行純邏輯,因此我們的邏輯程式碼中如何做到邏輯和渲染完全分離就很重要

雖然我們在進行模式設計和程式碼架構的過程中會盡量做到讓邏輯和渲染解耦,獨立執行(具體實現請參見sample原始碼),但出於維護同一份邏輯程式碼的考量,我們並沒有辦法完全把部分邏輯程式碼進行隔離,因此怎麼識別當前執行環境是客戶端還是伺服器就很必要了

unity給我們提供了自定義巨集定義開關的方法,我們可以通過這個開關來判斷當前執行平臺是否為客戶端,同時關閉伺服器程式碼中不需要執行的渲染部分 
 
我們可以在unity中Build Settings–Player Settings–Other Settings中找到Scripting Define Symbols選項,在其中填入

<span style="color:#000000"><code>_CLIENTLOGIC_</code></span>

巨集定義開關,這樣在unity中我們便可以此作為是否為客戶端邏輯的判斷,在客戶端中開啟與渲染相關的程式碼,同時也讓伺服器邏輯不會受到與渲染相關邏輯的干擾,比如:

<span style="color:#000000"><code><span style="color:#009900">#if _CLIENTLOGIC_</span>
        m_gameObject<span style="color:#009900">.transform</span><span style="color:#009900">.localPosition</span> = position<span style="color:#009900">.ToVector</span>3()<span style="color:#880000">;</span>
<span style="color:#009900">#endif</span></code></span>

邏輯程式碼版本控制策略

  • 版本控制: 
    同步校驗的關鍵在於客戶端伺服器執行的是完全同一份邏輯原始碼,我們應該極力避免原始碼來回拷貝的情況出現,因此如何進行版本控制也是需要策略的,在我們公司專案中,需要伺服器和客戶端同時執行的程式碼是以git子模組的形式進行管理的,雙端各自有自己的業務邏輯,但子模組是相同的,這樣維護起來就很方便,推薦大家嘗試.
  • 不同伺服器架構如何適配: 
    客戶端是c#語言寫的,如果伺服器也是採用的c#語言,那正好可以無縫結合,共享邏輯,但目前採用c#作為遊戲伺服器主要語言的專案其實很少,大多是java,c++,golang等,比如我們公司用的是skynet,如果是這種不同語言架構的環境,那我們就需要單獨搭建一個c#伺服器了,目前我們的做法是在fedora下結合mono搭建的戰鬥校驗伺服器,閘道器收到戰鬥校驗請求後會轉發到校驗伺服器進行戰鬥校驗,把校驗結果返回給客戶端,具體的方式請參閱後文:戰鬥校驗伺服器簡單搭建指引

哪些unity資料型別不能直接使用

  • float 
  • Vector2 
  • Vector3 
    上面這三種類型由於都涉及到浮點數,會讓邏輯執行結果不可控,因此都不能在幀同步相關的邏輯程式碼中直接使用,用於替代的是在Fix64.cs中定義的定點數型別:
原始資料型別 替代資料型別
float Fix64
Vector2 FixVector2
Vector3 FixVector3

同時還有一種例外的情況,某些情況下我們會用Vector2來存放int型物件,在客戶端這是沒問題的,因為int物件不存在精度誤差問題,但是遺憾的是伺服器並無法識別Vector2這個unity中的內建資料型別,因此我們不能直接呼叫,而是需要自己構建一個類似的資料型別,讓構建後的資料型別能夠跨平臺. 
在Fix64.cs中新增了NormalVector2這個資料型別用於替代這些unity原生的資料型別,這樣就可以同時在客戶端和伺服器兩端運行同樣的邏輯程式碼了. 
那專案中是不是完全沒有float,沒有Vector3這些型別了呢,其實也不完全是,比如設定顏色等API呼叫還是需要使用float的:

<span style="color:#000000"><code><span style="color:#000088">public</span> <span style="color:#000088">void</span> <span style="color:#009900">setColor</span>(<span style="color:#000088">float</span> r, <span style="color:#000088">float</span> g, <span style="color:#000088">float</span> b)
{
<span style="color:#009900">#<span style="color:#009900">if</span> _CLIENTLOGIC_</span>
    m_gameObject.GetComponent<SpriteRenderer>().color = <span style="color:#000088">new</span> Color(r, g, b, <span style="color:#006666">1</span>);
<span style="color:#009900">#<span style="color:#009900">endif</span></span>
}</code></span>

鑑於專案中既存在浮點數資料型別也存在定點數資料型別,因此在框架中使用了匈牙利命名法進行區分,讓所有參與編碼的人員能一眼分辨出當前變數是浮點數還是定點數

<span style="color:#000000"><code>Fix64 m_fixElapseTime = Fix64.Zero;  <span style="color:#880000">//字首fix代表該變數為Fix64型別</span>
<span style="color:#000088">public</span> FixVector3 m_fixv3LogicPosition = <span style="color:#000088">new</span> FixVector3(Fix64.Zero, Fix64.Zero, Fix64.Zero); <span style="color:#880000">//字首fixv3代表該變數為FixVector3型別</span>
<span style="color:#000088">float</span> fTime = <span style="color:#006666">0</span>;  <span style="color:#880000">//字首f代表該變數為float型別</span></code></span>

哪些unity介面不能直接呼叫

unity中某些特有的介面不能直接呼叫,因為伺服器環境下並沒有這些介面,最常見介面有以下幾種: 

  • Debug.Log 
  • PlayerPrefs 
  • Time 
    不能直接呼叫不代表不能用,框架中對這些常用介面封裝到UnityTools.cs,並用上文提到的_CLIENTLOGIC_開關進行控制,
<span style="color:#000000"><code><span style="color:#000088">public</span> <span style="color:#000088">static</span> <span style="color:#000088">void</span> <span style="color:#009900">Log</span>(<span style="color:#000088">object</span> message)
{
<span style="color:#009900">#<span style="color:#009900">if</span> _CLIENTLOGIC_</span>
    UnityEngine.Debug.Log(message);
<span style="color:#009900">#<span style="color:#009900">else</span></span>
    System.Console.WriteLine (message);
<span style="color:#009900">#<span style="color:#009900">endif</span></span>
}

<span style="color:#000088">public</span> <span style="color:#000088">static</span> <span style="color:#000088">void</span> <span style="color:#009900">playerPrefsSetString</span>(<span style="color:#000088">string</span> key, <span style="color:#000088">string</span> <span style="color:#000088">value</span>)
{
<span style="color:#009900">#<span style="color:#009900">if</span> _CLIENTLOGIC_</span>
    PlayerPrefs.SetString(key, <span style="color:#000088">value</span>);
<span style="color:#009900">#<span style="color:#009900">endif</span></span>
}</code></span>

這樣在邏輯程式碼中呼叫UnityTools中的介面就可以實現跨平臺了

<span style="color:#000000"><code>UnityTools<span style="color:#009900">.Log</span>(<span style="color:#009900">"end logic frame: "</span> + GameData<span style="color:#009900">.g</span>_uGameLogicFrame)<span style="color:#880000">;</span></code></span>

加速功能

實現了基礎的幀同步核心功能後,加速功能就很容易實現了,我們只需要改變Time.timeScale這個系統閥值就可以實現.

<span style="color:#000000"><code>//調整戰鬥速度
btnAdjustSpeed<span style="color:#009900">.onClick</span><span style="color:#009900">.AddListener</span>(delegate ()
{
    if (Time<span style="color:#009900">.timeScale</span> == <span style="color:#006666">1</span>)
    {
        Time<span style="color:#009900">.timeScale</span> = <span style="color:#006666">2</span><span style="color:#880000">;</span>
        txtAdjustSpeed<span style="color:#009900">.text</span> = <span style="color:#009900">"2倍速"</span><span style="color:#880000">;</span>
    }
    else if (Time<span style="color:#009900">.timeScale</span> == <span style="color:#006666">2</span>)
    {
        Time<span style="color:#009900">.timeScale</span> = <span style="color:#006666">4</span><span style="color:#880000">;</span>
        txtAdjustSpeed<span style="color:#009900">.text</span> = <span style="color:#009900">"4倍速"</span><span style="color:#880000">;</span>
    }
    else if (Time<span style="color:#009900">.timeScale</span> == <span style="color:#006666">4</span>)
    {
        Time<span style="color:#009900">.timeScale</span> = <span style="color:#006666">1</span><span style="color:#880000">;</span>
        txtAdjustSpeed<span style="color:#009900">.text</span> = <span style="color:#009900">"1倍速"</span><span style="color:#880000">;</span>
    }
})<span style="color:#880000">;</span></code></span>

需要注意的是,由於幀同步的核心原理是在單元片段時間內執行完全相同次數的邏輯運算,從而保證相同輸入的結果一定一致,因此在加速後,物理時間內的計算量跟加速的倍數成正比,同樣的1秒物理時間片段,加速兩倍的計算量是不加速的兩倍,加速10倍的運算量是不加速的10倍,因此我們會發現一些效能比較差的裝置在加速後會出現明顯的卡頓和跳幀的狀況,這是CPU運算超負荷的表現,因此需要根據遊戲實際的運算量和表現來確定最大加速倍數,以免加速功能影響遊戲體驗

小談加速優化

實際專案中很容易存在加速後卡頓的問題,這是硬體機能決定的,因此如何在加速後進行優化就很重要,最常見的做法是優化美術效果,把一些不太重要的特效,比如打擊效果,buff效果等暫時關掉,加速後會導致各種特效的頻繁建立和銷燬,開銷極大,並且加速後很多細節本來就很難看清楚了,因此根據加速的等級選擇性的遮蔽掉一些不影響遊戲品質的特效是個不錯的思路.由此思路可以引申出一些類似的優化策略,比如停止部分音效的播放,遮蔽實時陰影等小技巧.

戰鬥回放功能

通過上面的基礎框架的搭建,我們確保了相同的輸入一定得到相同的結果,那麼戰鬥回放的問題也就變得相對簡單了,我們只需要記錄在某個關鍵遊戲幀觸發了什麼事件就可以了,比如在第100遊戲幀,150遊戲幀分別觸發了出兵事件,那我們在回放的時候進行判斷,當遊戲邏輯幀執行到這兩個關鍵幀時,即調用出兵的API,還原出兵操作,由於操作一致結果必定一致,因此我們就可以看到與原始戰鬥過程完全一致的戰鬥回放了.

記錄戰鬥關鍵事件

1.在戰鬥過程中實時記錄

<span style="color:#000000"><code>GameData<span style="color:#009900">.battleInfo</span> info = new GameData<span style="color:#009900">.battleInfo</span>()<span style="color:#880000">;</span>
info<span style="color:#009900">.uGameFrame</span> = GameData<span style="color:#009900">.g</span>_uGameLogicFrame<span style="color:#880000">;</span>
info<span style="color:#009900">.sckeyEvent</span> = <span style="color:#009900">"createSoldier"</span><span style="color:#880000">;</span>
GameData<span style="color:#009900">.g</span>_listUserControlEvent<span style="color:#009900">.Add</span>(info)<span style="color:#880000">;</span></code></span>

2.戰鬥結束後根據戰鬥過程中實時記錄的資訊進行統一儲存

<span style="color:#000000"><code><span style="color:#880000">//- 記錄戰鬥資訊(回放時使用)</span>
<span style="color:#880000">// </span>
<span style="color:#880000">// @return none</span>
<span style="color:#000088">void</span> recordBattleInfo() {
    <span style="color:#000088">if</span> (<span style="color:#000088">false</span> == GameData.g_bRplayMode) {
        <span style="color:#880000">//記錄戰鬥資料</span>
        <span style="color:#000088">string</span> content = <span style="color:#009900">""</span>;
        <span style="color:#000088">for</span> (<span style="color:#000088">int</span> i = <span style="color:#006666">0</span>; i < GameData.g_listUserControlEvent.Count; i++)
        {
            GameData.battleInfo v = GameData.g_listUserControlEvent[i];
            <span style="color:#880000">//出兵</span>
            <span style="color:#000088">if</span> (v.sckeyEvent == <span style="color:#009900">"createSoldier"</span>) {
                content += v.uGameFrame + <span style="color:#009900">","</span> + v.sckeyEvent + <span style="color:#009900">"$"</span>;
            }
        }

        UnityTools.playerPrefsSetString(<span style="color:#009900">"battleRecord"</span>, content);
        GameData.g_listUserControlEvent.Clear();
    }
}</code></span>

Sample為了精簡示例流程,戰鬥日誌採用字串進行儲存,用’$’等作為切割識別符號,實際專案中可根據實際的網路協議進行制定,比如protobuff,sproto等

復原戰鬥事件

1.把戰鬥過程中儲存的戰鬥事件進行解碼:

<span style="color:#000000"><code><span style="color:#880000">//- 讀取玩家的操作資訊</span>
<span style="color:#880000">// </span>
<span style="color:#880000">// @return none</span>
<span style="color:#000088">void</span> loadUserCtrlInfo()
{
    GameData.g_listPlaybackEvent.Clear();

    <span style="color:#000088">string</span> content = battleRecord;

    <span style="color:#000088">string</span>[] contents = content.Split(<span style="color:#009900">'$'</span>);

    <span style="color:#000088">for</span> (<span style="color:#000088">int</span> i = <span style="color:#006666">0</span>; i < contents.Length - <span style="color:#006666">1</span>; i++)
    {
        <span style="color:#000088">string</span>[] battleInfo = contents[i].Split(<span style="color:#009900">','</span>);

        GameData.battleInfo info = <span style="color:#000088">new</span> GameData.battleInfo();

        info.uGameFrame = <span style="color:#000088">int</span>.Parse(battleInfo[<span style="color:#006666">0</span>]);
        info.sckeyEvent = battleInfo[<span style="color:#006666">1</span>];

        GameData.g_listPlaybackEvent.Add(info);
    }
}</code></span>

2.根據解碼出來的事件進行邏輯復原:

<span style="color:#000000"><code><span style="color:#880000">//- 檢測回放事件</span>
<span style="color:#880000">// 如果有回放事件則進行回放</span>
<span style="color:#880000">// @param gameFrame 當前的遊戲幀</span>
<span style="color:#880000">// @return none</span>
<span style="color:#000088">void</span> checkPlayBackEvent(<span style="color:#000088">int</span> gameFrame)
{
    <span style="color:#000088">if</span> (GameData.g_listPlaybackEvent.Count > <span style="color:#006666">0</span>) {
        <span style="color:#000088">for</span> (<span style="color:#000088">int</span> i = <span style="color:#006666">0</span>; i < GameData.g_listPlaybackEvent.Count; i++)
        {
            GameData.battleInfo v = GameData.g_listPlaybackEvent[i];

            <span style="color:#000088">if</span> (gameFrame == v.uGameFrame) {
                <span style="color:#000088">if</span> (v.sckeyEvent == <span style="color:#009900">"createSoldier"</span>) {
                    createSoldier();
                }
            }
        }
    }
}</code></span>
  • 5 
    整個框架中最核心的程式碼為LockStepLogic.cs(幀同步邏輯),Fix64.cs(定點數)和SRandom.cs(隨機數) 
    其餘程式碼作為一個示例,如何把核心程式碼運用於實際專案中,並且展示了一個稍微複雜的邏輯如何在幀同步框架下良好執行. 
  • battle目錄下為幀同步邏輯以及戰鬥相關的核心程式碼 
  • battle/core為戰鬥核心程式碼,其中 
    -action為自己實現的移動,延遲等基礎事件 
    -base為基礎物件,所有戰場可見的物體都繼承自基礎物件 
    -soldier為士兵相關 
    -state為狀態機相關 
    -tower為塔相關 
  • ui為戰鬥UI 
  • view為檢視相關

自帶sample流程

流程:戰鬥—戰鬥結束提交操作步驟進行伺服器校驗—接收伺服器校驗結果—記錄戰鬥日誌—進行戰鬥回放 

  • 綠色部分為完全相同的戰鬥邏輯 
  • 藍色部分為完全相同的使用者輸入

示例sample中加入了一個非常簡單的socket通訊功能,用於將客戶端的操作傳送給伺服器,伺服器根據客戶端的操作進行瞬時回放運算,然後將運算結果發還給客戶端進行比對,這裡只做了一個最簡單的socket功能,力求讓整個sample最精簡化,實際專案中可根據原有的伺服器架構進行替換.

戰鬥校驗伺服器簡單搭建指引

  • 安裝mono環境 
  • 編譯可執行檔案 
  • 實現簡單socket通訊回傳

安裝mono環境

編譯可執行檔案

1.開啟剛才安裝好的monodeveloper 
2.點選file->new->solution 
3.在左側的選項卡中選擇Other->.NET 
4.在右側General下選擇Console Project 

在左側工程名上右鍵匯入子模組中battle資料夾下的所有原始碼 
 
點選build->Rebuild All,如果編譯通過這時會在工程目錄下的obj->x86->Debug資料夾下生成可執行檔案 
如果編譯出錯請回看上文提到的各種注意點,排查哪裡出了問題.

開發過程中發現工程目錄下如果存在git相關的檔案會導致monodeveloper報錯關閉,如果遇到這種情況需要將工程目錄下的.git資料夾和.gitmodules檔案進行刪除,然後即可正常編譯了.

執行可執行檔案

cmd開啟命令列視窗,切換到剛才編譯生成的Debug檔案目錄下,通過mono命令執行編譯出來的exe可執行檔案

<span style="color:#000000"><code>mono LockStepSimpleFramework<span style="color:#009900">.exe</span></code></span>

伺服器端戰鬥校驗邏輯

可執行檔案生成後並沒有什麼實際用處,因為還沒有跟我們的戰鬥邏輯發生聯絡,我們需要進行一些小小的修改讓驗證邏輯起作用. 
修改新建工程自動生成的Program.cs檔案,加入驗證程式碼

<span style="color:#000000"><code>BattleLogic battleLogic = new BattleLogic ()<span style="color:#880000">;</span>
battleLogic<span style="color:#009900">.init</span> ()<span style="color:#880000">;</span>
battleLogic<span style="color:#009900">.setBattleRecord</span> (battleRecord)<span style="color:#880000">;</span>
battleLogic<span style="color:#009900">.replayVideo</span>()<span style="color:#880000">;</span>

while (true) {
    battleLogic<span style="color:#009900">.updateLogic</span>()<span style="color:#880000">;</span>
    if (battleLogic<span style="color:#009900">.m</span>_bIsBattlePause) {
        <span style="color:#000088">break</span><span style="color:#880000">;</span>
    }
}
Console<span style="color:#009900">.WriteLine</span>(<span style="color:#009900">"m_uGameLogicFrame: "</span> + BattleLogic<span style="color:#009900">.s</span>_uGameLogicFrame)<span style="color:#880000">;</span></code></span>

通過上述程式碼我們可以看到,首先構建了一個BattleLogic物件,然後傳入客戶端傳過來的操作日誌(battleRecord),然後用一個while迴圈在極短的時間內把戰鬥邏輯運算了一次,當判斷到m_bIsBattlePause為true時證明戰鬥已結束. 
那麼我們最後以什麼作為戰鬥校驗是否通過的衡量指標呢?很簡單,通過遊戲邏輯幀s_uGameLogicFrame來進行判斷就很準確了,因為只要有一丁點不一致,都不可能跑出完全相同的邏輯幀數,如果想要更保險一點,還可以加入別的與遊戲業務邏輯具體相關的引數進行判斷,比如殺死的敵人個數,發射了多少顆子彈等等合併作為綜合判斷依據.

實現簡單socket通訊回傳

光有戰鬥邏輯校驗還不夠,我們需要加入伺服器監聽,接收客戶端傳送過來的戰鬥日誌,計算出結果後再回傳給客戶端,框架只實現了一段很簡單的socket監聽和回發訊息的功能(儘量將網路通訊流程簡化,因為大家肯定有自己的一套網路框架和協議),具體請參看Sample原始碼.

<span style="color:#000000"><code>Socket serverSocket <span style="color:#4f4f4f">=</span> <span style="color:#006666">new</span> Socket(SocketType<span style="color:#4f4f4f">.</span>Stream, ProtocolType<span style="color:#4f4f4f">.</span>Tcp);
IPAddress ip <span style="color:#4f4f4f">=</span> IPAddress<span style="color:#4f4f4f">.</span>Any;
IPEndPoint point <span style="color:#4f4f4f">=</span> <span style="color:#006666">new</span> IPEndPoint(ip, <span style="color:#006666">2333</span>);
<span style="color:#880000">//socket繫結監聽地址</span>
serverSocket<span style="color:#4f4f4f">.</span>Bind(point);
Console<span style="color:#4f4f4f">.</span>WriteLine(<span style="color:#009900">"Listen Success"</span>);
<span style="color:#880000">//設定同時連線個數</span>
serverSocket<span style="color:#4f4f4f">.</span>Listen(<span style="color:#006666">10</span>);

<span style="color:#880000">//利用執行緒後臺執行監聽,否則程式會假死</span>
<span style="color:#000088">Thread</span> <span style="color:#000088">thread</span> <span style="color:#4f4f4f">=</span> <span style="color:#006666">new</span> <span style="color:#000088">Thread</span>(Listen);
<span style="color:#000088">thread</span><span style="color:#4f4f4f">.</span>IsBackground <span style="color:#4f4f4f">=</span> <span style="color:#006666">true</span>;
<span style="color:#000088">thread</span><span style="color:#4f4f4f">.</span>Start(serverSocket);

Console<span style="color:#4f4f4f">.</span>Read();</code></span>

框架原始碼

客戶端

伺服器

客戶端伺服器共享邏輯

共享邏輯以子模組的形式分別加入到客戶端和伺服器中,如要執行原始碼請在clone完畢主倉庫後再更新一下子模組,否則沒有共享邏輯是無法通過編譯的

子模組更新命令:

<span style="color:#000000"><code><span style="color:#880000">git</span> <span style="color:#880000">submodule</span> <span style="color:#880000">update</span> <span style="color:#006666">-</span><span style="color:#006666">-</span><span style="color:#880000">init</span> <span style="color:#006666">-</span><span style="color:#006666">-</span><span style="color:#880000">recursive</span></code></span>
  • 環境: 
    客戶端:win10 + unity5.5.6f1 
    伺服器:fedora27 64-bit

相關推薦

unity同步遊戲框架例項(客戶伺服器原始碼)

閱前提示:  此框架為有幀同步需求的遊戲做一個簡單的示例,實現了一個精簡的框架,本文著重講解幀同步遊戲開發過程中需要注意的各種要點,伴隨框架自帶了一個小的塔防sample作為演示. 目錄:

同步遊戲開發框架(推倒重構版)

幀同步訊息、物件池、邏輯/檢視層物件、事件訊息! 1. 幀同步訊息 幀同步訊息由Msg目錄下的幾個檔案進行管理,所有Input進行的操作皆是SendMsg來開始。 2. 物件池 物件池管理比較複雜, 但是對於整個程式而言十分重要,且可以幫助理清程式的結構,一定程度上幫助

基於 swoole 的框架 One 1.3.1 釋出

   主要更新 新增Rpc服務支援內建了http,TCP協議支援 增加Tcp協程客戶端連線池 增加 globalData 自增方法 背景 在用過laravel框架,發現它的路由和資料庫ORM確實非常好用,但是整體確實有點慢,執行到控制器大於需要耗時60ms左右。於是打

基於 swoole 的框架 One 1.3.3 釋出

   主要更新 新增功能: 捕獲協成內丟擲的錯誤 新增環境配置檔案app.ini 新增佇列以及push,pop,length方法 優化/修復功能: 在多個數據庫情況下orm模型快取結構有衝突可能 findAll()->toArray() 沒有資料返回空陣列

基於Unity遊戲專案客戶伺服器尋路同步方案

    Unity中目前提供的基於Navmesh的網格尋路,如果僅僅是單機遊戲,其實功能還是能滿足的,當然,如果你做的是大規模兵海流的 rts遊戲,Unity的網格尋路還是會碰到多人尋路相互擠壓的問題。     由於我們目前的工作主要集中在手遊,而又以聯網RPG遊戲為主

使用cocos2dx製作 同步 遊戲

重構了之前的遊戲程式碼,尋思把遊戲做成可以網路聯機的遊戲,於是百度各種資源,想學習一下。 學習資料倒是很多,最終選擇使用跟 皇室戰爭 一樣的同步方式,進行幀同步。 具體資料自行百度。 主要記錄一下遇到的問題。 一、修改原始碼 由於使用的是cocos2dx引擎,看原始碼,在c

Unity進階之ET網路遊戲開發框架 02-ET的客戶啟動流程分析

版權申明: 本文原創首發於以下網站: 部落格園『優夢創客』的空間:https://www.cnblogs.com/raymondking123 優夢創客的官方部落格:https://91make.top 優夢創客的遊戲講堂:https://91make.ke.qq.com 『優夢創客』的微信公眾號:um

同步遊戲開發小結

*本文發表於程式設計師劉宇的個人部落格,轉載請註明來源,https://www.cnblogs.com/xiaohutu/p/12402399.html* 這幾年做了一些網路同步專案,總結一下幀同步的一些東西。 ## 1. 幀同步基本特點 1. 所有的邏輯行為運算都在客戶端進行,客戶端保證彼此之間執

JAVA+Maven+TestNG搭建介面測試框架例項

1、配置JDK 2、安裝Eclipse以及TestNG Eclipse下載地址:http://beust.com/eclipse TestNG安裝過程: 線上安裝 輸入網址:http://beust.com/eclipse 線上安裝會比較慢,有的人可能還會連結不上這個地址,所以下面介紹一個離線下載的方

+++++++icfg,ip,nmcli網絡屬性配置網絡客戶工具使用

linuxLinux主機聯網:ifcofnig,route,netstat,ip,ss別名、主機名、接口命名網絡客戶端工具命令: ftp,lftp,wget,lftpgetnmap,ncat,tcpdump工具nmcli,nmtui <沒有CentOS 7 不能使用。。以後在補>Linux主機聯網

yii 框架 自定義規則客戶驗證

esp 失去 屬性 sbo sid func 需要 當前 條件 前提:yii 自定義規則不能通過失去焦點驗證 view層中:設置form的3個屬性,validationUrl 可以不設置,默認為當前頁面,但是一般情況驗證不會跟提交數據在一個方法中處理 $form = zA

用最簡單的方式實現基於libevent框架的http client客戶

#include <event2/event_struct.h> #include <event2/event.h> #include <event2/bufferevent.h> #include <event2/http.h> #include

mysql 8.0的完美安裝連線Navicat客戶(全網獨此一篇!!!)---整合篇

    首先跟大家嘮一嘮家常,隨著MySQL迅速的更新,MySQL突飛猛進已經更新到了8.0版本,那麼它和我們之前用的5.X版本有什麼明顯的區別那? 首先給大家看下MySQL5.X自帶表的查詢速度 之後獻上MySQL8.0的自帶表的查詢速度 一

Netty 一個非阻塞的客戶/伺服器框架

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow 也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!        

WebApiThrottle限流框架——獲取API的客戶key

預設情況下,WebApiThrottle的ThrottlingHandler(限流處理器)會從客戶端請求head裡通過Authorization-Token key取值。如果你的API key儲存在不同的地方,你可以重寫ThrottlingHandler.SetIndenti

推薦一款好用的國外網盤 mega(客戶下載使用教程)

國內,免費好用的網盤基本絕跡了。比如百度網盤,免費各種限速,速度坑的一比,雖然有一些方法可以突破百度網盤限速,不過百度網盤演算法也一直在更新,可能之前有用的方法後面就失效了。而 115 網盤,也是需要付費才能體驗比較好的服務,而且費用還不便宜。 現在介紹國外的一款網盤,名為 mega 網

WebApiThrottle限流框架——基於IP和客戶key的端點限流

如果同一個ip的客戶端,在同一秒內,呼叫了2次api/values,其最後一次的呼叫將會被拒絕掉。 如果想介面通過唯一key去識別限制客戶端,忽略客戶端的ip地址限制,應該配置IpThrottling為false。 config.MessageHandlers.Add(n

Redis叢集監控Redis桌面客戶

之前在生產環境部署了Redis叢集,一直苦於沒有工具監控,最近找了下網上推薦redmon和Redislive的比較多,查看了兩個專案的github,都幾年沒有更新,這兩個專案應該沒有人在維護了,如果哪位有更好的替代方案麻煩告知! 僅將自己的部署方案貼出來,以供自己翻查! 具

所有的平臺瀏覽器獲取客戶IP(外網): 使用的搜狐介面

<script src="http://pv.sohu.com/cityjson?ie=utf-8"></script>   <script type="text/javascript"&

cxf釋出webservice簡介 wsdl2java生成客戶程式碼

下面我們簡要介紹如何通過cxf框架釋出webservice 首先新建一個Java project為cxfmodel_server 首先去官網下載cxf:http://cxf.apache.org/download.html 下面我們就開始使用 (1)釋出服務 第一步:新建