繼之前用游戲引擎(青瓷引擎)做了斗地主單機版游戲之后,這里分享下使用socket.io來實現網絡對戰,代碼可已放到github上,在此談談自己整個的開發思路吧。
(點擊圖片進入游戲體驗)
前文鏈接:
javascript開發HTML5游戲--斗地主(單機模式part1)
javascript開發HTML5游戲--斗地主(單機模式part2)
javascirpt開發HTML5游戲--斗地主(單機模式part3)
本文章為網絡對戰第一部分內容。主要內容如下:
- 簡介
- 服務端項目搭建
- 客戶端准備
- 桌位管理
- 游戲流程
一、簡介
多人對戰游戲,我用Nodejs做開發服務端,使用socket.io做通訊。整個斗地主游戲流程是一樣的,只是單機版啥都要自己做,網絡版更多交給服務器,自己主要就是游戲界面展示了,先看看整個服務器的大致結構:

服務器中用Nodejs的mysql模塊做mysql數據庫存儲,這里並不是非常必要的,只是我嘗試使用熟悉這個東西,這里我只是存了個uid在客戶端瀏覽器緩存中,所以也不能真正達到持久化作用。如果要做持久化應該是要做個用戶登陸了,這里引擎給我們提供了微信支持,后面優化完善可以考慮加入微信登陸來保存用戶數據。
二、服務端項目搭建
對於Nodejs項目的搭建建,還是比較簡單的,簡單說明下項目搭建,在工作區下創建個目錄landlordServer,作為項目目錄。然后用命令行進入到該目錄下,加入Nodejs的socket.io和mysql模塊,命令如下:
npm install socket.io npm install mysql
出於之前編寫java服務端的習慣,我在項目目錄下創建了src目錄,用於放置代碼。在src下創建server.js作為入口腳本,加入socket.io的端口監聽,然后就可以用Nodejs來啟動了。這樣子我們的服務端也算是搭建完成了,相對java還真是簡易,接下來就可以寫邏輯代碼了。由於用了Nodejs,小弟對這個也不算非常熟悉,不過它的模塊化用起來感覺很像java類,一個文件一個類這樣的形式,這個類需要什么就import進來,Nodejs是require,這樣的話就不用去考慮腳本先后的問題。
三、客戶端項目准備
客戶端需要有socket.io.js的客戶端文件,這個文件在服務端項目中 node_modules/socket.io-client /路徑下,找到它並把它丟到客戶端Scripts下就可以了,這里我是放到了Scripts/operation目錄下,因為青瓷引擎也是基於Nodejs開發的,所以這里只要在Scripts也不需要聲明其他什么引用即可使用。
四、桌位管理
桌位
在網絡對戰斗地主中,都是三個玩家一桌,這樣的形式,有一些斗地主游戲還會把桌位合成再分為房間,再到大廳。我這里的實現只有桌位,一開始服務器啟動是沒有任何桌位的,有玩家進入創建桌位,后面的玩家進來就是進未滿員的桌位,找不到再創建新桌位,如果有一個桌位玩家全部離開了,就把該桌位刪除。這些事情都交給桌位管理器DeskMgr.js。我用的是一個desks對象來緩存座位信息,利用了js中對象可以是當成一個Map使用。socket.io還提供了一個分組功能,差不多是一個房間的概念,我把每一桌的玩家都加入到同一個分組中,這個分組的標識也就是這個桌位的桌位號,當然玩家退出的時候也要退出該分組。每個桌位有三個階段:
- 等待階段:這個階段中等待玩家加入並全部准備
- 搶地主階段:就搶地主
- 玩牌階段:
座位
每個桌位上都需要有3個座位,這里不僅是方便統計,也是為了方便確認先后順序。同時是用一個對象當做Map使用來存放三個座位,分別名為p1、p2、p3,這樣順序也就很容易確認,比如p1出了牌,輪到下家時,可以寫個獲取下家座位號的方法,然后得到p2,去通知p2出牌。
玩家
相比單機下的玩家信息,服務器上的玩家需要有其他屬性:
- 桌位號:用於確定玩家在哪一桌;
- 座位號:用於確定玩家在哪一個座位;
- 是否准備:玩家是否處於准備狀態;
- socketId:玩家連接的socket的id,在玩家初次加入到游戲的時候記錄下來,方便需要單獨給該玩家廣播消息的時候用到;
- 狀態:用於標記玩家是否離線
五、游戲流程
整個游戲流程跟單機模式是差不多的,只是網絡對戰是由服務器來做發牌、輪換等操作,相當三個人在斗地主,有一個助手替他們發牌,告訴他們輪到誰搶地主,輪到誰出牌,誰贏了,這樣,細分下服務器做的操作如下:
准備階段
玩家加入與退出:當有玩家加入的時候,服務器會返回給玩家的客戶端他所在桌位的信息,還有該玩家得到的桌位號。這樣在客戶端就可以顯示出整個桌位的信息了,其實就是顯示左右邊玩家的名字。除此之外,如果該桌位還有其他玩家,還需要給其他玩家廣播有新玩家加入的消息。退出(點擊退出、刷新、關閉頁面)時如果該桌位還有其他玩家也需要進行廣播通知。
玩家准備:每次玩家切換准備狀態時判斷玩家所在桌位是否有3個玩家准備了,符合條件就可以開始發牌了,這里都准備后就已經進入了搶地主階段。發牌由服務器來進行,所以原有的發牌代碼還需要搬一份到服務器上去,發完牌后分別通知給每一位玩家,給玩家對應的手牌信息,還要隨機生成哪一位座位號玩家先開始搶地主,這樣客戶端收到開始游戲信息后就播放發牌動畫,播放完畢后如果是自己先開始搶地主就顯示搶地主操作的按鈕。
搶地主階段
搶地主:玩家叫分后將數據(主要是玩家叫的分數和玩家信息)發給服務器,服務器處理后,將當前叫分交給下一家,廣播給當前桌位所有玩家,通知他們上一家叫分多少,還有現在輪到誰叫分搶地主了。這樣的客戶端就可以顯示相應信息,每個玩家根據自己的座位號顯示上家叫分和當前叫分,是自己就顯示叫分的操作按鈕。確定地主后再發生底牌信息給每個玩家,客戶端接收到消息后將底牌顯示出來。
退出/斷線:在這個階段退出的話我做的處理是直接結束游戲回到准備階段。因為地主還沒有確定,也無法進行出牌,直接結束是比較好的做法,再讓AI幫離線玩家叫分也是可以的,這就看開發者想怎么做了,對於離開的玩家也可以進行減分處罰。
出牌階段
玩家出牌:出牌階段的輪換跟邏輯搶地主差不多,主要是在客戶端渲染會有所差異。玩家將出的牌發給服務器,服務器判斷玩家出牌后是否還有手牌,有就繼續下家出牌,沒有就判定該玩家勝利。這跟單機版邏輯都是一樣的,只是這里服務器來做控制了。
退出/斷線:在出牌階段退出后,只有該桌位還有在線玩家,游戲就不會結束,離開的玩家會有AI代為出牌。為了讓斷線玩家會有回來的時間,AI第一次出牌會延遲出牌,這個使用定時器很容易實現。等玩家重連之后如果這個計時器還沒執行再去把這個計時器取消了。
斷線重連:這個問題一開始我比較沒思路,自己想了個方法實現,不知道算不算好的。實現思路:在出牌階段,有玩家離開了,就將這個玩家加入到一個離線列表中。當有玩家加入游戲后,不再是直接分配桌位,而是先去查離線列表,如果找到了,就進到原來的桌位,沒有找到再去匹配桌位。這樣找到了桌位信息,就可以在客戶端顯示當前游戲的狀況。玩家要是直到游戲結束都沒有重連,游戲結束時將本桌離線的玩家從離線列表刪除。或者一桌玩家都離線了,那就全部都從離線列表刪除。
游戲結束
有玩家完牌后,服務器給所有玩家廣播有人游戲結束,發送計算完后的玩家分數、沒出完玩家的手牌信息以及勝利玩家最后出的一手牌,該桌位回到准備階段。客戶端我的處理是先顯示剩下手牌,最后一手牌等信息,添加個定時器,3秒后游戲界面才渲染成准備階段狀態。
遇到的問題
我在開發過程中,遇到過個問題,控制台一直提示死循環,在報錯的地方查了好久,也沒發現異樣。后面跟進socket.io源碼才發現,是當我要發送給客戶端的數據中,有一個循環引用的問題,其實在我里面有個對象存着計時器,這樣將這個數據對象轉換成json時,轉換代碼就會陷入死循環。這個問題雖然不大,我也知道這種情況會引起轉換異常,但是不容易發現,一開始沒有引起我的注意,在此記錄下。網絡對戰的斗地主就介紹到這里,有需要改進望提出,互相學習。

