本文作者:QHC
前言:
長久以來,傳統前端的工作大多時候在與DOM打交道,近年來,瀏覽器廠商也在不斷努力提高DOM渲染性能,以提高用户體驗。但是更多複雜場景的出現,例如近幾年隨着在線直播、社交娛樂、各種小遊戲的火爆,前端性能的關注度持續提高。特別是遊戲場景,而我們團隊也面臨着一大波h5遊戲化場景,那麼這個系列文章,將帶讀者朋友們一起了解,雲音樂社交直播業務的遊戲化場景解決方案的整體思路與落地案例分享。希望能給大家在今後的開發中帶來一些啓發。
一、遊戲開發的技術選型
其實,在前期我們接到一些小遊戲的需求時,我經常在想一個問題,就是為什麼業界都主張使用Canvas來作為遊戲開發的主旋律?我們對於DOM的運用和理解,在某種程度上是比較自信的。運用自己更熟悉的手段去實現需求不就可以了麼?
這裏主要還是涉及到性能問題和一些少見但會有的場景實現問題。下面就簡單從性能和場景支持度兩個角度來跟大家聊一下Canvas作為遊戲開發主旋律的必要性。
1.1、性能比較
首先為什麼我們可以這麼肯定地説Canvas的渲染性能比DOM來的優秀。瀏覽器廠商明明在DOM渲染上已經做了足夠多的優化。比如渲染樹的處理方式、重排重繪的機制優化、Chrome瀏覽器通過預解析技術將DOM生成速度提高了40%等等看着都挺優秀的優化。它還是不及Canvas麼?答案是肯定的。因為雖然瀏覽器廠商在DOM渲染上做了很多優化,但是DOM元素是作為矢量圖進行渲染的,每個元素的邊距都需要單獨處理,瀏覽器需要將它們全部處理成像素才能輸出到屏幕上,計算量非常龐大。當頁面上存在大量DOM元素時,這些內容的渲染速度就會變慢。相比之下,Canvas本質上是一張位圖,瀏覽器在渲染Canvas時只需要在JavaScript引擎中執行繪製邏輯,在內存中構建畫布,然後遍歷整個畫布中的像素顏色,直接輸出到屏幕上即可。無論Canvas裏面的元素有多少個,瀏覽器在渲染階段都只需要處理一張畫布。
DOM:駐留模式\
駐留模式(Retained Mode)是DOM在瀏覽器中的渲染模式。粗略工作流程如下(圖片來源https://zhuanlan.zhihu.com/p/400391575
Canvas:快速模式\
Canvas採用了和DOM不同的快速模式(Immediate Mode),粗略工作流程如下:
兩者的區別在與駐留模式會生成一個(scene)和模型(model)存儲到內存中,然後再調用系統的繪製API(如Windows程序員熟悉的GDI/GDI+),把這些中間產物繪製到屏幕。也就意味着場景中每增加一點東西就需要額外消耗一些內存。而這在即時渲染模式下是不會發生的。Canvas繪製將這些場景和模型都交給開發者在開發階段自我實現了。
1.2、場景支持能力
除了上面説的性能優勢以外。在一些特定場景下,Canvas也許是唯一解。是DOM所無法替代的。比如經常出現在我們遊戲場景中的一些透明視頻素材的動效或者其他比如lottie等格式的動效資源播控。
1.3、已有方案的選擇
我們已經知道了整體的技術選型方向,接下來選擇一個合適的解決方案我們認為需要考慮到以下幾點要素:
**1、對需求的支持能力(簡而言之就是該技術棧是否能夠讓我們把需求完整的落地)\
2、頁面性能(即遊戲幀率、卡頓比、CPU或GPU佔用率等遊戲相關指標)\
3、開發效率和維護成本\
4、學習成本(這裏指的學習成本應該是對於整個團隊而言而非個人)\
5、技術支撐、技術生態**
為此,我們做了一些簡單的對比
| DOM + CSS | PixiJS | Eva.js | Cocos/Egret/Phaser*專業的H5遊戲引擎 | |
|---|---|---|---|---|
| 頁面性能 | 中 *涉及迴流重繪時 | 高 | 高 | 高 |
| 開發效率 | 高 | 中 | 中 | 中 |
| 學習成本 | 低 | 中 | 中 | 高 |
| 技術團隊支撐 | 社區 | 社區 | 社區 | 社區 |
| 功能支持 | 基礎能力支持其他需要第三方庫 | 缺少原生 Flex 佈局、透明視頻、Lottie 等動效格式支持 | 基本與PIXI一致 | 較為全面支持發佈微信小遊戲等平台 |
考慮到業務遊戲場景的複雜度並沒有非常高,輕量級的js庫可能更適合我們這種業務場景,而專業的遊戲引擎相對來説啓動成本就比較高了。所以我們在後續的開發中,PixiJS和Eva.js都有使用過。在這個過程中,積累了一些經驗的同時我們體驗到很多對於前端開發者來説非常不友好的體驗。
二、遊戲開發中的痛點
1.1、社交直播業務中的遊戲現狀
與傳統小遊戲不同的是,在社交直播商業化玩法體系中。遊戲往往是作為一個完整的需求的一部分。在一個web頁面中,不僅僅是隻有一整片的遊戲場景構建而成的,而是伴生着很多的傳統頁面元素的渲染和交互在裏面。第二個特點是我們的遊戲場景中即時狀態修改不會特別頻繁(類似高頻操作類),而基本都是線性的弱人機交互。以下幾張截圖是我們已經上線的一些小遊戲
<img src="https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/28021041026/e08c/a808/c191/8134306e5673913038b0102f530bc6cb.jpeg" alt="" width="40%">
<img src="https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/28021041023/0a76/7af7/edce/1f8f2a950131aba3b4eaa98b15595a93.png" alt="" width="40%">
<img src="https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/28021040537/b75c/820f/7230/067127918e2cfa083d156fbc589e83b2.jpeg" alt="" width="40%">
<img src="https://p5.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/28021041024/3734/95b3/4a94/0ba20c16276981bca8d17b0f6b4350aa.jpeg" alt="" width="40%">
同時不難看出,在遊戲界面中我們還有大量的榜單、任務、聊天室、UI彈窗等傳統元素的繪製與交互。這也意味着在遊戲開發過程中,如果我們完全使用三方遊戲引擎如Eva.js、PixiJS、cocos creator來繪製頁面的話,難免會損失一定的UI排版、UI細節處理上的效率。
1.2、 痛點分析
其實我們面臨的一個很大的問題是,PixiJS也好、Eva.js也好,它們無非是一套基於Canvas的渲染方案,而當前端開發者沉浸於DSL開發時(比如我們團隊就是以react為基礎技術棧),PixiJS、Eva.js並沒有提供一套與之對應的DSL開發模式。這就使得我們遭遇了幾個重點難題:
痛點1、無法高效的去繪製一些界面內容,各種元素的繪製都需要append節點來做,非常低效,而為了解決這個問題。我們嘗試將一個需求拆解為DOM層和遊戲層這種分層設計,這樣確實可以最大程度利用DOM的高效排版能力。可是這又帶來了另外的問題;
痛點2、當react和這些渲染引擎的代碼穿插出現在業務中的時候,往往帶來的代碼管理成本是非常高的。比如狀態管理就無法在遊戲側和UI側同時共享;
以一個卡牌類桌遊場景為例。於是就有了以下這種很棘手的開發流程
痛點3、除此之外,代碼裏也需要有大量的訂閲發佈、面向對象開發、甚至有時需要單獨維護一套狀態機。在使用Eva.js的過程中,我們還需要遵循ECS的架構思路來安排自己的代碼。這一切,都與DSL有所割裂。而完全在需求中摒棄DSL卻又會導致開發效率的直線下滑。
三、遊戲UI要是能用react antd該多好
我想這應該是前端開發者在遊戲開發過程中繞不開的一個想法。而其實PixiJS團隊有提供一套react-pixi這樣的庫。於是我們嘗試去使用了。但我們發現,它還是相對比較簡單。對於需求的實現我們需要額外做很多別的事情。比如資源管理、事件管理、各種css佈局能力、各種格式的動畫素材播放能力、高效的緩動體系等。它都是不具備的。故此我們自研Alice.js的想法萌芽了。這裏分享以下三點關於Alice.js的核心觀念
1、Alice.js的目標是什麼?
形成一套完整的 H5 小遊戲解決方案。在現有的 React 技術體系下,通過框架提供的遊戲研發能力,讓開發同學用熟悉的 JSX 和 Hooks 語法編寫動畫、遊戲場景的代碼。
- 貼合實際業務,與 React 生態緊密結合(數據管理和 UI 構建)
- 支持 JSX 寫法,學習成本低,會 React 就能快速上手
- 輕量級、高性能、可擴展
- 形成一套完整的 H5 小遊戲解決方案
2:Alice.js的使用場景是什麼?
因為在渲染層我們採用了PixiJs來作為渲染引擎,所以如果要指定一個試用範圍,我想應該是所有PixiJs可以cover的場景,都可以使用Alice.js進行開發。而對於無法單純使用PixiJs實現的場景,通過Alice的高擴展能力也能夠覆蓋。當然了,因為PixiJs本身是一個2d渲染引擎。所以當我們遇到3D場景時,目前是無法覆蓋的。
3:Alice.js的優勢在哪裏?
1、Alice.js將渲染引擎和傳統UI框架有效的進行了融合。這使得我們可以用JSX標聲明式開發遊戲UI內容。也就是説,我們提供一整套DSL遊戲開發模式。\
2、我們提供了一整套佈局方案,你可以輕鬆的以cssinjs的形式對遊戲元素進行排版和修飾\
3、優秀的可擴展性,支持了各種類型動效素材的播控,和各種遊戲常用組件的庫的提供\
4、提供了一整套遊戲開發必備的資源管理體系,這使得遊戲的資源管理變得非常高效\
5、因為底層是藉助 react-reconciler 編寫自定義 renderer,所以天然支持使用各種狀態管理庫,技術棧割裂的現象將不復存在
四、Alice.js的架構設計
Alice的整體架構如下圖:
篇幅原因,本文主要簡單介紹Alice.js的整體架構設計,在本系列後續文章中,將詳細為大家講解我們是如何將這一整套架構的實現。敬請讀者朋友們期待。
1、架構分層-橋接層
根據我們的整體目標做一下拆解。首先,我們希望實現一整套基於React框架的聲明式小遊戲DSL開發模式。這也就意味着,我們需要將傳統的eva.js也好還是PixiJs的語法轉為React框架下的JSX語法。例如,如下代碼我們實現一張canvas畫布上繪製有藍天白雲、草地上有男孩、女孩。
<Stage>
<Sky>
<Cloud /> // 雲彩是動態的
</Sky>
<Background>
<Boy /> // 人物可以做一些動作,這取決於動畫素材
<Girl />
</Background>
</Stage>
1.1 打通React和PixiJs的橋樑
為了實現這一點,我們利用了react-reconciler作為橋樑。react-reconciler 是一個抽象層,用於實現自定義的渲染器。它允許你在 React 的基礎上構建自己的渲染器,例如將 React 渲染到非 DOM 環境(如移動端原生組件、Canvas 等)。
於是我們擁有了一個自定義的renderer:
import Reconciler from 'react-reconciler';
const PixiFiber = Reconciler(hostConfig);
接下來需要實現Stage作為整個遊戲界面的載體,我們認為所有的遊戲元素都應該在Stage裏呈現,而Stage組件本身輸出的是一個Canvas元素而已。只不過我們在Stage組件加載的各個生命週期裏,需要調用我們自定義渲染器能力,以Stage所輸出的Canvas元素為畫布,將各種遊戲元素渲染到這張畫布上。
自定義渲染器關鍵的調用節點在<Stage />組件的幾個重要生命週期中:componentDidMount、componentDidUpdate、componentWillUnmount。
1、當<Stage />在componentDidMount階段,調用PixiFiber.createContainer(PIXI.Application.stage) 方法創建 React reconciler 根節點,將 PixiJS 的舞台作為根節點。這樣,PixiJS 的渲染結果就可以與 React Fiber 進行協調,實現將 PixiJS 和 React 結合起來的能力。並通過 PixiFiber.updateContainer 方法更新容器內容。值得一提的是Pixi 的 Scene Graph 本身就是樹結構,非常適合使用 JSX 語法構建。
2、在<Stage />的 componentDidUpdate 生命週期方法中,根據傳入的屬性通過 PixiFiber.updateContainer 方法更新容器內容。而在<Stage />內部的子元素狀態更新時,因為這些子元素已經處於 PixiFiber 創建的容器內了,<Stage />作為 React Fiber 的根節點,任何對該容器內子節點的更新都會觸發 React Fiber 的 diff 算法進行協調。也就天然支持子元素的自主更新了。換句話説,當舞台(stage)中的子節點發生變化時,PixiFiber 會使用 React Fiber 的協調機制來判斷哪些子節點需要更新、添加或刪除,並進行相應的操作。這包括比較虛擬 DOM(Virtual DOM)的變化、調度更新任務、執行生命週期方法等。
3、在 componentWillUnmount 生命週期方法中,通過 PixiFiber.updateContainer 方法清空掛載點裏的所有內容,並銷燬 PIXI 應用實例。
1.2 豐富的Pixi原子
我們已經知道,有一個<Stage />組件來作為主舞台對應PIXI.Application.stage,那麼如何把一個PIXI元素作以子組件的方式添加到<Stage />呢?比如一個基本的精靈圖PIXI.Sprite。
這裏主要需要了解的是react-reconciler的參數HostConfig 對象,這個對象定義了自定義渲染器的行為。
hostConfig 對象的方法包括:
createInstance(type, props, rootContainerInstance, hostContext, internalInstanceHandle): 創建新的節點實例。finalizeInitialChildren(parentInstance, type, props, rootContainerInstance, hostContext): 在創建初始子節點後,完成初始化操作。prepareUpdate(instance, type, oldProps, newProps, rootContainerInstance, hostContext): 在節點更新前,準備更新所需的信息。commitUpdate(instance, updatePayload, type, oldProps, newProps, internalInstanceHandle): 執行節點的更新操作。appendChild(parentInstance, child): void: 將子節點添加到父節點中。insertBefore(parentInstance, child, beforeChild): void: 在指定子節點之前插入一個新的子節點。removeChild(parentInstance, child): void: 從父節點中移除一個子節點。appendChildToContainer(container, child): void: 將子節點添加到容器中。removeChildFromContainer(container, child): void: 從容器中移除一個子節點。
這些方法的具體實現將取決於自定義的渲染器的需求和特性。createContainer 方法將使用你提供的 hostConfig 對象來執行相應的操作,並在容器中渲染和更新 React 元素。
再來看我們所拋出來的問題:如何把一個PIXI元素作以子組件的方式添加到<Stage />呢? 當我們調用PixiFiber.updateContainer時,就會對<Stage />裏所有的子元素進行更新,比如PIXI.Sprite就是其中一個子元素,它的JSX表現形式為
<Stage>
<Image />
</Stage>
當我們的自定義PixiFiber調度遍歷到<Image />時,會執行我們提前設計的HostConfig 對象中的createInstance,在這個方法裏,我可以做想做的任何事情,比如創建一個PIXI.Sprite實例並。同樣的道理對於樹節點的插入、移除、更新都可以利用對應的HostConfig 對象屬性來進行操作。以下是關鍵節點的偽實現代碼
// 創建一個react虛擬dom樹節點對應的pixi元素
createInstance(type, props, rootContainer) {
// 創建實例
const instance = new PIXI.Sprite();
// 設置實例在關鍵生命週期和屬性應用時所需要執行的方法
instance._customDidAttach = () => { ... }; // 在被掛載時執行
instance._customWillDetach = () => { ... }; // 在被卸載時執行
instance._customApplyProps = () => { ... }; // 將屬性設置到Pixi元素上
// 將props屬性添加給Sprite實例
instance._customApplyProps(props);
return instance
}
// 在一個父節點中插入子節點
appendChild(parent, child) {
parent.addChild(child);
// 執行子節點被掛載自帶方法
child._customDidAttach.call(child, child, parent);
}
// 在一個父節點中移除子節點
appendChild(parent, child) {
parent.removeChild(child);
// 在被卸載時執行
child._customWillDetach.call(child, child, parent);
// 銷燬子節點實例
child.destroy();
}
...
如此,我們就可以成功的將一個PIXI.Sprite渲染到Stage之中。而以上代碼只是簡單説明了如何將單個PIXI.Sprite渲染出來,在實際生產中,我們可能會用到非常多的Pixi元素。所以把這些可能會用到的pixi原生元素都統一封裝起來,形成一個elements集合
這樣便極大程度豐富了Alice所支持的Pixi原子,而這些原子未來在引擎層可以封裝成為更多具備定製能力的組件。
1.3 定製化元素的支持
我們已經知道,在橋接層Alice提供了很多Pixi原生的元素,作為渲染節點。但是在實際生產中,往往我們需要自定義的去擴展一些定製組件,比如需要實現一個可以播放lottie素材的動畫組件<LottieSprite />。那麼該組件底層渲染一定也是基於PIXI去實現的,於是我們就需去做以下幾個步驟來實現這樣一個自定義的組件。(其實這個自定義的組件可以理解為類似於DOM裏的<div />)
第一步:聲明一個類,該類繼承於橋接層所提供的原生組件<Image />,並且在初始化的時候具備解析lottie素材為紋理的能力。以及可以監聽參數變化時觸發渲染內容的更新。
第二步:通過工廠函數,輸出一個具備完整生命週期的組件,這裏就是在上文提到的節點的插入、移除、更新等方法。
第三步:createInstance的過程中能夠調用這些生命週期函數,從而達到渲染到畫布的效果
為了達到這一目的,我們在橋接層提供了PixiComponent這樣的一個註冊函數(工廠函數),以實現在上層(引擎層)創建自定義元素,可以很好的被底層Pixi所渲染。
1.4 橋接層的擴展性
在橋接層我們可以做很多事情,因為在本方案中,我們使用的React作為DSL技術方案,所以在橋接層我們用了react-reconciler作為渲染調度器。如果我們換成Vue的話呢?其實只需要將調度器換成vue-next基本就可以轉換技術棧。
除此之外,因為我們掌握了虛擬節點樹轉換為真實渲染節點的完整週期,所以我們可以在週期內賦予一些其他的遊戲元素能力,比如物理效果。在元素被掛載時,就可以實例一個Matter.Engine實例,並將一個對應的剛體加入Matter.Composite。同步剛體的座標與Pixi元素的座標,就可以實現一個簡單的物理世界效果。
如果有需求,我們甚至可以依然使用這套框架去實現一個基於3d渲染的能力擴展
2、架構分層-引擎層
根據架構圖,我們可以發現橋接層的上層是引擎層,這也就意味着引擎層是與應用層更加接近的一層。所以在引擎層,我們增加一些業務能力比如:基於橋接層封裝了更多遊戲業務常用的一些組件、依賴橋接層提供的自定義原生組件能力擴展了更多動畫播控組件、基於yoga提供了強大的flex佈局能力、基於tween.js提供了高效的緩動效果開發能力以及各種交互事件的轉發和元素之間碰撞檢測能力。這一塊內容我們會在本系列的後續文章中詳細介紹其實現方案和原理。敬請期待。
3、完備的資源管理體系
在架構圖中,資源管理確實是最頂端的一層。而這一小節的標題,沒有帶”架構分層“的字眼。
先説一下我們架構圖中的資源管理層。在最開始,我們的想法比較直接,在遊戲的開發過程中,一般需要使用到大量的圖片、動畫素材、音頻等資源來豐富整個遊戲內容,而大量的資源就會帶來管理上的困難。因此提供了一套資源管理器來幫助開發者管理其資源的使用。開發者編寫遊戲時,無需關心資源的預加載、解析、紋理轉換等工作。只需要在資源Map文件裏聲明需要預處理的資源即在項目中隨處可用。與此同時,將提供豐富的加載生命週期鈎子、資源插入和銷燬等API。以及進度條的UI組件。
那麼為什麼如今我們把它從整個Alice裏拿出去了呢?主要考慮到其實這套資源管理模式,不僅僅是在遊戲開發中紋理轉換場景可以使用。在我們常規的H5活動中,也可以把這套管理體系拿來使用以提高整個界面的流暢度和用户體驗的提升。
在本系列後續文章中,會有專門一篇來介紹資源管理的方案和實現。會詳細介紹它與目前我們常用的已有資源管理三方包的區別。敬請期待。
五、本篇小結
在本文中,我們探討了Alice遊戲引擎的架構設計,並介紹了其分層結構中的關鍵設計理念和功能。通過橋接層、引擎層和資源管理層的劃分,Alice遊戲引擎提供了靈活、可擴展的開發環境,滿足了遊戲開發和應用開發的不同需求。
在橋接層,我們實現了DSL與Pixi渲染能力的結合。這為聲明式的遊戲場景開發提供的可能性。
在引擎層,我們基於橋接層提供了豐富的業務能力和組件,包括常用遊戲組件的封裝、動畫播放控制、靈活的佈局能力以及高效的緩動效果開發能力,為開發者提供了便利和效率。
資源管理層則為開發者提供了一套完備的資源管理體系,簡化了資源的加載和管理流程,不僅適用於遊戲開發,還可以在其他H5活動中使用,提升界面的流暢度和用户體驗。
通過對Alice遊戲引擎的架構設計和功能介紹,我們可以看到它為開發者提供了比較高效的遊戲場景開發模式,幫助開發者更高效地創建出豐富、流暢的遊戲和應用。在後續的文章中,我們將深入探討每個層級的具體實現方案和原理,
在未來的發展中,我們會推進更多關於Alice遊戲引擎的技術探索和創新,以及它在實際項目中的應用案例。通過不斷完善和擴展,期望Alice能夠形成一整套H5遊戲場景解決方案。
參考資料
- Eva.js:https://eva-engine.gitee.io
- pixi-react:https://github.com/pixijs/pixi-react
- react-konva:https://github.com/konvajs/react-konva
- 詳解Canvas優越性能: https://zhuanlan.zhihu.com/p/400391575
本文發佈自網易雲音樂技術團隊,文章未經授權禁止任何形式的轉載。我們常年招收各類技術崗位,如果你準備換工作,又恰好喜歡雲音樂,那就加入我們 grp.music-fe(at)corp.netease.com!