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