摘要:原文從零到一,擼一個在線斗地主上篇作者背景朋友來深圳玩,若說到在深圳有什么好玩的,那當然是宅在家里斗地主了可是天算不如人算,撲克牌丟了幾張不全大熱天的,誰愿意出去買牌啊。
原文:從零到一,擼一個在線斗地主(上篇) | AlloyTeam
作者:TAT.vorshen
背景:朋友來深圳玩,若說到在深圳有什么好玩的,那當然是宅在家里斗地主了!可是天算不如人算,撲克牌丟了幾張不全……大熱天的,誰愿意出去買牌啊。不過問題不大,作為移動互聯(lián)網(wǎng)時代的程序猿,當然是擼一個手機在線斗地主來代替實體牌了。
github地址:https://github.com/vorshen/landlord
閱讀前注意:
本文分為上下兩篇,本篇講準備工作以及前端一些布局相關的知識;下一篇講webassembly實現(xiàn)核心邏輯和server端相關。
由于源碼在github上全部都有,所以文章更偏向于思路的講解。
業(yè)余時間有限,游戲樣式丑= =,有些細節(jié)也沒打磨,敬請諒解。不過還是達到了閉環(huán),線下開黑娛樂應該沒有問題。
游戲大概樣式
typescript + canvas + webassembly + c++(server)
首先肯定是Web的,人齊有個局域網(wǎng)server端啟動,然后QQ、微信、瀏覽器訪問,直接就開干了啊。既然是Web的,那必須是typescript啊,我覺得寫過ts的,這輩子應該不會再想寫js了吧……
斗地主作為一個元素不多、沒炫酷場景的游戲,其實dom完全可以吃得住。但是做個Web游戲,不用個canvas作為舞臺,總感覺哪里不對勁。所以最終我們還是用canvas來渲染。這里我們就沒有用成熟的渲染引擎了,鍛煉鍛煉自己。
既然作為練手作品,總要折騰點,webassembly作為目前很火的技術,我們當然要嘗試一下啦,所以游戲的一些核心邏輯采用了webassembly實現(xiàn),這里會在下一篇詳細講解。
編碼前既然是自己從零到一,產品設計開發(fā)都得是自己,我們先簡單梳理一下游戲的流程。我們這個斗地主不同于QQ斗地主,QQ斗地主是隨機進入房間,無法開黑。而我們追求的是一起玩,所以游戲房間的概念是一大不同。
簡單列了一下我們游戲的流程:
快速進入,即開即玩,無需注冊
創(chuàng)建房間或搜索加入房間
進入房間之后,傳統(tǒng)的斗地主邏輯
傳統(tǒng)的斗地主邏輯如下:
雖然這里貼出來了,但自己真正開始寫的時候,壓根沒梳理,就是一把梭,上來就擼碼。結果發(fā)現(xiàn)了不少邏輯上的沖突點和細節(jié)點,斗地主看起來是一個小游戲,不過邏輯還蠻復雜的,再加上在線非單機,完全低估了游戲的復雜度,一把辛酸淚……
設計沒啥好說的,從網(wǎng)上找了幾個圖就當作基本的元素了(難看就難看了……沒辦法)
下面就正式開始了
布局 橫屏首先斗地主這個游戲是橫屏的,這個蛋疼了,因為web對橫屏的控制太弱了一點。我們無法強制橫版,全部依賴系統(tǒng)的行為。
既然橫屏限制多不好用,那么我們能不能直接用豎屏來模擬橫屏呢?也就是手機保持豎屏狀態(tài),然后我們整個頁面旋轉一下,就模擬了豎屏了,寫樣式布局啥的,完全可以按照橫屏的來寫,還是挺方便的。
原理如下:
大概代碼
// 獲取旋轉元素父元素的寬高 let width = this._app.root.offsetWidth; let height = this._app.root.offsetHeight; this._box = document.createElement("div"); this._box.className = "room-box"; // 寬高反轉 this._box.style.width = `${height}px`; this._box.style.height = `${width}px`; this._box.style.transform = `translateX(${width}px) rotate(90deg)`;
注意!這樣的橫屏,會導致無法直接使用點擊事件的clientX/Y,這里也需要進行一下轉換,具體代碼在Stage.ts中,這里不再展開。
不過這種方案在模擬器上看起來沒啥問題,真機上還是有缺陷的,就是標題欄的問題,如圖
不過我覺得這個還行,無傷大雅,所以就采取了這種方式
適配游戲分為三個場景頁面:首頁,大廳頁,房間頁。其中首頁和大廳頁其實也就是走個流程,我們很隨意,房間頁就是對戰(zhàn)相關,最為復雜,這里就以房間頁來說。下面是經(jīng)典的QQ斗地主的房間頁:
我們大致劃分一下模塊,如圖所示:
不考慮細節(jié)的情況下還是比較簡單的,可以看出,主要就是六大區(qū)域:
頂部信息展示區(qū)
底部信息展示區(qū)
左側玩家區(qū)域
右側玩家區(qū)域
主視角玩家區(qū)域
特效區(qū)域
我們這就不考慮出牌特效啥的了(找?guī)讉€基礎的素材就要了我命了),如果用dom實現(xiàn),那直接flex就安排的明明白白,如下(只是舉例子,沒有用前面橫屏的方式)
上面是flex的實現(xiàn),很輕松,但是,我們使用canvas渲染,該如何針對不同屏幕尺寸進行適配呢?
這里有兩種大的考慮方向:
canvas模擬彈性布局
縮放解決
canvas模擬彈性布局眾所周知我們用原生canvas接口,繪制元素,都是用絕對定位的形式,不支持flex??戳讼聵I(yè)界一些游戲渲染引擎,alloyrender、erget、easelJS也都是用x,y坐標控制顯示對象的位置。
我的理解是既然你采用canvas了,自然是會出現(xiàn)頻繁重繪,彈性布局更偏向于靜止的頁面場景,對于游戲上需求不大,沒必要花大功夫吃力不討好。不過我們這個斗地主是一個偏頁面靜止的游戲,感興趣的同學可以嘗試嘗試,針對上面那五個模塊用固定大小+百分比的方式來實現(xiàn)一下彈性布局。由于時間和篇幅關系,這里就不貼效果圖和代碼了。
這種方式的優(yōu)勢是可以把屏幕使用率拉滿,也不會有變形;
劣勢就是太麻煩了,光是這五個區(qū)域的布局還好,但是還涉及到區(qū)域里面細節(jié)的時候,實在是hold不住了,所以我最終也沒有采用這種方式。如果有那些簡單的布局場景,還是可以試試。
縮放解決看名字就知道是采用「縮放」來抹平不同屏幕尺寸的差異了。怎么縮放,也是有很多種方案,我羅列兩個我覺得比較好的,應該也是用的比較多的
全部展示+黑邊
核心展示+無黑邊
兩者的原理如下所示:
二者的針對的場景也不太相同
「全部展示+黑邊」:所有內容都必須展示出來,黑邊可以用大背景掩蓋住
「核心展示+無黑邊」:整個舞臺可以很大,用戶只需要聚焦核心區(qū)域
綜上所述,我們肯定要采用的是第一種方式了
渲染整個頁面不是很復雜,為了練手,我們也沒有用業(yè)界成熟的渲染引擎。但是總不能用canvas原生的寫法,所以首先我們封裝了幾個基礎的組件
DisplayObject 顯示對象基類,只要對象要顯示,一定要繼承該類
Container 容器類
Bitmap 位圖類
Text 文本類
以上是這次游戲中需要用到的渲染相關的基類,我們具體的展示對象(撲克牌),或者容器(手牌)都是繼承它們,再進行一些擴充。具體的代碼github上都能看到。
下面用張圖表示一下整個項目中組件情況
這里假設我們要正式開發(fā)一個游戲,借助渲染引擎,意味著不需要考慮base部分了。那么大概流程是如下的。
我們要先規(guī)劃出場景,確定有幾個場景。
針對1中的場景,確定每個場景有哪些基于base的上層組件
組件抽象復用性判斷(不同場景類似的組件,是不是可以抽象成一個)
工具庫、第三方庫確定
流程基本上就是如此。
這里我們用頁面上最重要的一個組件為例,講一下
BasePukesContainer是非常重要的一個組件,如其名,它是負責撲克牌展示的。玩家的手牌(HandPukes)、玩家出的牌(DesktopPukes)都是繼承于它,所以BasePukesContainer抽象就很重要了
首先,我們確定下BasePukesContainer作為一個撲克牌展示承載容器,需要哪些方法
能帶著撲克牌(子元素)展示
能批量的增刪撲克牌
撲克牌的支持多種對齊方式、多行展示等
列個圖,看了BasePukesContainer已有的,和需要補充的
紅色部分是目前繼承base下來缺失的,那么我們就要擴充
最終代碼如此(完整源碼看github)
class BasePukesContainer extends Container { // 撲克牌寬度 protected _pukeWidth: number; // 撲克牌高度 protected _pukeHeight: number; // 撲克牌水平對齊方式 protected _horizontalAlign: PUKE_HORIZONTAL_ALIGN; // 撲克牌垂直對齊方式 protected _verticalAlign: PUKE_VERTICAL_ALIGN; // 撲克牌之間兩兩的覆蓋大小 private _interval: number; /** * 移除某張撲克牌 * @param {*} object */ protected _deletePuke(object: BasePuke) {} /** * 加入單張撲克牌 * @param {*} puke */ protected _postPuke(puke: BasePuke, zIndex?: number) {} /** * 觸發(fā)更新維護的撲克牌的位置 */ protected _updatePukes() {} constructor(options: i_BasePukesContainerOptions) {} /** * 移除部分撲克牌 * @param {string[]} pukes */ deletePukes(pukes: string[]) {} /** * 添加部分撲克牌 * @param {string[]} pukes */ postPukes(pukes: string[]) {} /** * 刪除所有牌 */ deleteAll() {} }
渲染引擎的組件和使用思想都講完了,當然細節(jié)和基礎組件肯定遠遠不止這些,比如動畫、粒子等等,感興趣的可以看下業(yè)界渲染引擎的源碼,帶著理解去讀,應該還是挺易懂的。
交互靜態(tài)渲染相關的都講完了,下面我們說說游戲開發(fā)中的交互
問題撲克牌排列渲染好了,玩家得出牌啊,touchstart和touchmove都應該觸發(fā)選牌。問題是canvas不是dom,不管展示啥,理論上要不是fill出來的,要不然是stroke出來的,都沒法綁定交互事件啊。
其實這個問題也不算是問題了,基本上大家應該都知道解決方案。
雖然fill出來的東西我們無法綁定事件,但是,我們可以給canvas標簽綁上事件啊。然后根據(jù)event的clientX/Y相對于canvas的位置,找到對應渲染的元素啊。
具體原理如下
(x3, y3)就是clientX/Y
它是全局坐標,我們先減去(x1, y1),得到相對于canvas舞臺的坐標(x", y")
此時一切都是相對于canvas舞臺的坐標系了,我們用(x", y")去和[x2, y2, w, h]這個矩形對比,判斷點在不在矩形中,如果在,就意味著點擊到了元素
如果頁面比較簡單,確實解決了。然后有些事情并非那么簡單……
元素重疊
有兩個元素(撲克)存在重疊,玩家點擊在了重疊的區(qū)域,該如何響應?
剛剛只有兩個坐標系,屏幕坐標系和canvas坐標系,如果再引入一個container呢,是不是又多了一個相對坐標?茫茫無盡的嵌套,該怎么辦呢?
一個點是否在矩形中,很好判斷;是否在圓中,也好判斷,但如果是不規(guī)則圖形呢?
針對元素重疊,首先我們肯定是不能觸發(fā)層級低元素的點擊事件的,那么就是我們判斷點是否在矩形中的時候,一定要按順序來。正好Container也保證了這個順序,代碼類似如下。
/** * touchstart,touchmove的時候觸發(fā) */ private _touch = (data: { x: number, y: number }) => { let { x, y } = data; let len = this._children.length; let i; let temp: BasePuke; let puke: BasePuke | undefined; for (i = len - 1; i >= 0; i--) { temp =this._children[i]; if (temp.contain(x, y)) { puke = temp; break; } } if (puke) { this._choosePuke(puke); } }
組件嵌套就稍微麻煩了些,這里的核心沖突是鼠標點擊的位置是絕對坐標,而canvas舞臺里面的元素,都是相對坐標。要對比的話,要么將絕對坐標轉為相對的,要么把相對的轉成絕對坐標。
這里我們采用的是將絕對坐標轉為相對的,比如當點擊坐標為(x1, y1)時,需要判斷是否點擊中了[x2, y2, w, h]這個矩形(注意:這個x2, y2是經(jīng)過層層嵌套的)
我們就需要求出(x1, y2)這個全局坐標,轉換到(x2, y2)坐標系的矩陣,然后變化一下即可
代碼如下:
// DisplayObject.ts /** * 判斷是否在AABB中 * 注意,這里x,y是global的坐標,沒有經(jīng)過transform * 所以要進行逆矩陣計算 * @param {*} x * @param {*} y */ contain(x: number, y: number) { let point = new Point(x, y); let matrix: Matrix2D; // 先求出完整的矩陣 if (this._parent) { matrix = this._parent._getGlobalMatrix(); } else { matrix = new Matrix2D(); } // 再求逆矩陣 matrix.invert(); // 點進行矩陣變換 point.transformWithMatrix(matrix); let rect = this._getAABB(); return rect.contains(point); }
變化矩陣就是根據(jù)需要判斷的元素,先獲取其全局的變換矩陣,然后求逆矩陣即可。如果了解矩陣的同學,應該很好理解,不了解的同學,可以查閱一下相關資料,這里篇幅原因,就不詳細說明了。
絕對轉相對是如此的,相對轉絕對也是類似的做法。
最后一個就是不規(guī)則圖形,規(guī)則圖形我們都可以用幾何法甚至代數(shù)法判斷其是否在元素內部,其實判斷的核心在于「邊」。但是不規(guī)則圖形,單純的想用「邊」的方式來判斷,太難了,所以就有了像素級別的判斷法:反畫家算法。還是篇幅問題,這里不進行展開,感興趣的同學自行查閱(我們這個斗地主游戲也沒有使用)。
總結到這里,上文就要結束了。我們從需求開始分析,將游戲中展示相關的工作都準備完畢,解決了橫屏問題,自己封裝了個簡易的渲染引擎,確定好了上層組件,也準備好了交互手勢,可以說非邏輯部分都已經(jīng)搞定了,已經(jīng)可以單機展示出來了。
那么該如何接收他人消息?游戲的同步是什么樣的?用戶進出房間有什么注意事項?出牌核心邏輯部分該如何編寫?Webassembly用在了哪里,如何使用?
敬請期待下篇。
AlloyTeam 歡迎優(yōu)秀的小伙伴加入。
簡歷投遞: [email protected]
詳情可點擊 騰訊AlloyTeam招募Web前端工程師(社招)
文章版權歸作者所有,未經(jīng)允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉載請注明本文地址:http://systransis.cn/yun/106196.html
摘要:原文從零到一,擼一個在線斗地主下篇作者上篇回顧我們說了斗地主游戲的渲染展示部分,最后也講了下中交互的情況,下篇的重點就是游戲邏輯。 原文:從零到一,擼一個在線斗地主(下篇) | AlloyTeam作者:TAT.vorshen 上篇回顧:我們說了斗地主游戲的渲染展示部分,最后也講了下canvas中交互的情況,下篇的重點就是游戲邏輯。 邏輯主要分成兩塊:流程邏輯和撲克牌對比邏輯。 gith...
摘要:由于公司項目轉型,需要創(chuàng)造一個小游戲平臺,需要使用一個比較成熟的前端游戲框架來快速開發(fā)小游戲。僅支持開發(fā)游戲,因為專注,所以高效。早在年的光棍節(jié)前一天晚上,這個游戲就誕生了。原型是一個之前很火的非常魔性的小游戲,叫尋找程序員。 showImg(https://segmentfault.com/img/bVMGY5?w=900&h=500); 寫在前面 實際上我從未想過我會接觸到H5小游...
摘要:小結使用深度優(yōu)先算法,我們能夠檢測性格測試游戲的邏輯正確性,相比以往課堂上的理論,在這里算是一個具體的應用場景吧。其實深度優(yōu)先算法的應用面也很廣,遲早還會再碰面的。 showImg(https://segmentfault.com/img/bVStEU?w=900&h=500); 寫在前面 在開始前想先說一下關于這個課題的感想——能學以致用是一件很快樂的事情。 深度優(yōu)先算法(簡稱DFS...
閱讀 2102·2023-04-25 22:58
閱讀 1452·2021-09-22 15:20
閱讀 2725·2019-08-30 15:56
閱讀 2035·2019-08-30 15:54
閱讀 2160·2019-08-29 12:31
閱讀 2782·2019-08-26 13:37
閱讀 627·2019-08-26 13:25
閱讀 2144·2019-08-26 11:58