天天看點

從零開始開發3D遊戲,助力B類場景互動創新嘗試

作者 | 楺楺
從零開始開發3D遊戲,助力B類場景互動創新嘗試

近些年,越來越多的業務都引入了互動遊戲,相信大家都玩過雙十一蓋樓、養貓、螞蟻森林等小遊戲。那麼互動在B類場景中是否也能帶來價值,我們發起了第一次嘗試:一達通 0-1 破蛋結合互動遊戲做客戶激勵,拉動客戶從準入到上行,遊戲部分的效果如下:

點選檢視視訊

👇 微信或釘釘掃碼玩遊戲,挑戰最高分2500~

從零開始開發3D遊戲,助力B類場景互動創新嘗試

截止目前的資料,線上每人平均玩了10次,說明這種競技類小遊戲對提高使用者粘性和活躍度發揮了較大的作用。當然隻有挑戰性和趣味性是不夠的,再結合任務道具、組隊PK、自選賽道、分享/傳播等手段,将帶來更好的業務效果。

場景分析

整個遊戲界面分成了3層:

從零開始開發3D遊戲,助力B類場景互動創新嘗試
  • DOM UI層:這一層主要來放置2D相關的UI元素,比如分數面闆、遊戲說明面闆、遊戲結束面闆等,然後通過事件與3D場景進行通信。
  • 3D場景層:包含了阿牛、賽道、能量塊在内的整個3D場景,主要通過 Oasis Engine 和 Oasis 3D Editor 實作,最終表現為一個 canvas 元件,下文會重點介紹這一層的實作。
  • 背景層:即放置在最底層的藍天大海背景。

⚠️ 值得注意的是,不要直接 css 設定 canvas 的背景,這樣會導緻微信或 Safari 壓背景之後出現場景重疊的異常現象。推薦的做法是把 gl.clearColor 的背景色設定為透明,然後在 canvas 外部放置一個背景層,讓 canvas 專注于3D渲染。

實作 3D 場景

對比我們平常的業務開發,3D場景開發包含了以下幾個流程:

從零開始開發3D遊戲,助力B類場景互動創新嘗試

場景搭建

我們從最簡單的3D場景搭建開始。得益于 Oasis Editor 的功能,我們可以直接編輯3D場景,所見即所得。

首先我們要了解好 ECS(Entity-Component-System)架構,在 Oasis 3D 中,萬物皆元件,一個實體代表什麼取決于它身上挂載了什麼元件。比如我們建立了一個實體,上面挂載了相機元件,那麼它就是一個相機實體。如果再給它添加飛行元件,那麼它就是會飛的相機。

基于這樣的思想,我搭建出一棵完整的場景樹(左側紅色框内):

從零開始開發3D遊戲,助力B類場景互動創新嘗試

其中,角色實體上挂載了 GLTF 模型元件,最終渲染出阿牛的模型。一般設計師在3D軟體中導出 FBX,Oasis Editor 會将它轉換成适合 Web 環境的 GLTF,并解析出骨骼動畫、材質、紋理等資訊。另外角色實體上挂載了不少腳本元件,這些都是包含了遊戲邏輯的自定義元件。

選擇腳本開發方式

Oasis Editor 包含了雲端代碼編輯器,而且提供了事件測試面闆。3D場景中監聽的事件會出現在輸入事件清單中,我們可以配置事件參數進行觸發;3D場景中觸發的事件則出現在輸出事件面闆中。

從零開始開發3D遊戲,助力B類場景互動創新嘗試

針對本遊戲,我們希望能通過 git 管理項目,并且為了使接入方隻需關心3D通信部分,把強制橫屏、降級、賽道配置化等交給遊戲元件本身,而這些并不屬于 Oasis 3D 的腳本,是以選擇了本地開發的形式。

腳本建立及綁定是在 Oasis 場景編輯器裡面操作,然後将項目下載下傳到本地進行開發。之後我們會經常需要調整3D場景或者添加一些新的腳本,這時候就需要用 @alipay/oasis-cli 把3D場景拉取到本地。⚠️ 需要注意的是,

oasis pull -s

會覆寫掉本地的腳本,是以一般執行

oasis pull

,隻拉取 schema 配置。

邏輯開發

1 、控制角色運動軌迹

利用碰撞檢測來擷取3D空間中兩個物體之間的相交情況,需要碰撞體包圍盒和碰撞檢測器。為了實作不同碰撞帶來的不同動作,我把角色的碰撞體包圍盒設定得比實際大一點,是以角色和藍色方塊的碰撞體預設是相交的。如圖所示,深藍色立方體是角色的碰撞體包圍盒

從零開始開發3D遊戲,助力B類場景互動創新嘗試

使用者第一次點選的時候做 x 軸直線運動,第二次點選時增加 y 軸分量,做上抛運動。

從零開始開發3D遊戲,助力B類場景互動創新嘗試
從零開始開發3D遊戲,助力B類場景互動創新嘗試

檢測角色與方塊碰撞的 end_overlop 事件,如果碰撞體對象為藍色方塊,則直接做自由下落運動。

從零開始開發3D遊戲,助力B類場景互動創新嘗試

檢測角色與方塊碰撞 begin_overlop 事件,如果撞到藍色方塊,則計算方塊與角色的相對位置。

  • 如果角色在方塊上方,隻做x軸直線運動。
從零開始開發3D遊戲,助力B類場景互動創新嘗試
  • 如果角色在側方或下方,則做自由下落運動。
從零開始開發3D遊戲,助力B類場景互動創新嘗試
從零開始開發3D遊戲,助力B類場景互動創新嘗試
  • 檢測角色與終點站台的碰撞,如果角色懸空位于站台上方,則做自由落體運動,同時切換相機視角。起始狀态和終點狀态都是可以計算出來,是以這裡利用 tween 動畫實作平滑的自由落體運動。
從零開始開發3D遊戲,助力B類場景互動創新嘗試

2、 相機跟随

相機是3D場景中必不可少的元素,它相當于一個觀察3D世界的眼睛,隻有在相機錐形體内的區域才會被渲染到螢幕上。在角色奔跑的過程中,我們希望相機永遠跟随在角色的右後方,是以在每一幀都把相機的位置設定為角色的相對位置。

在 Oasis 3D 腳本的生命周期中,onUpdate 和 onLateUpdate 都是在每幀更新中執行,差別在于 onLateUpdate 是晚于所有 onUpdate 之後更新的。假設角色運動由兩個腳本的 onUpdate 同時控制,相機在 onUpdate 進行跟随,這時候就可能出現抖動的現象,是以相機跟随一般放在 onLateUpdate 之中。

3、 骨骼動畫

我們的角色有多種骨骼動畫,比如水準運動時的奔跑動畫、達到終點時的招手動畫等,這些動畫資訊都包含在 FBX 檔案中。設計師從 C4D 導出的檔案把所有骨骼動畫都包含在同一個卡通片段中,導緻我們無法分段播放。是以還需要借助 Blender(一款開源的跨平台全能三維動畫制作軟體),在 Blender 的動畫攝影表中有一個動作編輯器,在裡面複制一個時間軸,删去多餘的、隻保留需要的動作。依次分好後,導出 FBX。

我們通過卡通片段的名稱來播放(playAnimationClip),那這些名稱如何擷取呢?可以打開 GLTF 源檔案,找到 animation 屬性。samplers 描述動畫資料的來源;channels 建立輸入(即從采樣器計算的值)和輸出(即動畫節點屬性)之間的連接配接;而 name 就是卡通片段的名稱了。

從零開始開發3D遊戲,助力B類場景互動創新嘗試
從零開始開發3D遊戲,助力B類場景互動創新嘗試

4、 粒子動畫

從零開始開發3D遊戲,助力B類場景互動創新嘗試

粒子系統重點要了解好它的發射參數,引擎會根據配置參數,自動生成幾何體和材質。在 Oasis Editor 中,我們可以直接為實體綁定粒子系統元件,以能量塊的爆炸粒子為例:爆炸是從一個點往四周發射的效果,是以把所有粒子的初始速度都置零,x 軸和 y 軸加上速度随機因子,給 y 軸負方向加了微小的加速度模拟重力和空氣摩擦力。

從零開始開發3D遊戲,助力B類場景互動創新嘗試
從零開始開發3D遊戲,助力B類場景互動創新嘗試

所有粒子預設都不播放的,隻有在檢測到碰撞才會發射:

const cd = this.entity.addComponent(o3.CollisionDetection);
cd.on("begin_overlop", e => {
  const colliderNode = e.collider._entity;
  // 擷取碰撞實體的類型
  const entityType = this.getEntityType(colliderNode);

  // 撞到能量塊
  if (entityType === "reward") {
    // 擷取粒子元件
    const particleComponent = colliderNode.getComponent(o3.GPUParticleSystem);
    // 發射粒子
    particleComponent && particleComponent.start();
    this.engine.dispatch("gotReward");
  }
});           

5、 Shader 動畫

從零開始開發3D遊戲,助力B類場景互動創新嘗試

角色碰到紅色方塊之後的消失動畫采用 Shader 幀動畫,具體實作參考我的

另一篇文章

6、 賽道配置化

基于 3.1 搭建出來的場景樹,我們可以通過克隆、拖拽的形式配置我們的賽道。

從零開始開發3D遊戲,助力B類場景互動創新嘗試

為了能夠靈活調整遊戲難度,我們需要把賽道配置抽象出來。賽道主要由兩部分組成:方塊和能量塊。實體之間的差別主要是位置,針對方塊來說,有危險方塊和普通方塊,各自比例也不同,我們通過實體名字進行區分,比如 normalBox11 代表 11:1:1的普通方塊,dangerousBox2 代表 2:1:1 的危險方塊。危險方塊可能包含旋轉、位移等動畫,是以都做成單獨的腳本元件。最終抽象出資料結構:

interface IGameConfig {
  // 方塊配置
  boxsConfig: {
    name: string;
    position: {
      x: number;
      y: number;
      z: number;
    };
    action?: string[];
  }[];
  // 能量塊配置
  rewardsConfig: {
    name: string;
    position: {
      x: number;
      y: number;
      z: number;
    };
  }[];
}           

遊戲内部設定了多個難度等級的配置,接入方可以傳入對應的 level,也可以自定義賽道。

7、 遊戲換膚

我們在 Oasis Editor 中編輯完場景之後,可以導出項目,其中 schema.json 是關鍵産物,裡面包含了實體(nodes)、元件(abilities)、資源(assets)、動畫(animator)等資訊,它們之間的關系:

從零開始開發3D遊戲,助力B類場景互動創新嘗試

其中,asset 包含了GLTF、材質、紋理、腳本等多種類型的資源。runtime 會根據 schema 配置來建構實體樹,并加載所有資源,資源與元件進行關聯,再将元件挂載到對應的實體上。是以我們需要在場景初始化之前,把 schema 中的相關紋理替換掉,進而達到自定義換膚的功能。

8、 手機橫屏方案

遊戲是橫屏顯示的,使用者可能會開啟手機的自動旋轉設定,我們可以通過監聽螢幕旋轉事件(onorientationchange)來設定我們的遊戲。如果螢幕的寬度大于螢幕的高度,遊戲容器的寬高直接等于螢幕寬高。如果螢幕的寬度小于螢幕高度,遊戲容器的寬設定為螢幕的高,遊戲容器的高設定為螢幕的寬,然後将遊戲容器旋轉90度,由于是圍繞中心點進行旋轉,是以還需要将遊戲容器往左下角偏移,如下圖所示:

從零開始開發3D遊戲,助力B類場景互動創新嘗試
const clientWidth = document.documentElement.clientWidth;
const clientHeight = document.documentElement.clientHeight;
gameContainer.style.top = (clientHeight - clientWidth) / 2 + "px";
gameContainer.style.left = 0 - (clientHeight - clientWidth) / 2 + "px";           

onorientationchange 在低端機上會有相容性問題,可以借助 resize 來觸發。這時候發現轉屏之後無法馬上擷取螢幕寬高,是以還需要延遲監聽。

優化

1、 畫布節能模式

在 Retina 屏中,一個邏輯像素等于多個實體像素,取決于裝置像素比。如果不對畫布做處理,就會出現下圖這種模糊的效果。通常我們會選擇高清模式,即畫布像素 1:1 填充到螢幕實體像素。具體做法是将畫布的寬高按裝置像素比來放大:

webcanvas.width = canvas.clientWidth * window.devicePixelRatio

。設定之後發現幀率下降,手機容易發燙,因為渲染的壓力和螢幕實體像素高寬成正比,實體像素越大,渲染壓力越大。權衡性能和效果,我們選擇了節能模式,即在高清模式的基礎上對畫布按照某個比例進行縮放

webcanvas.width = canvas.clientWidth * window.devicePixelRatio / scale

。一般 scale 設定為 3/2 即可。

從零開始開發3D遊戲,助力B類場景互動創新嘗試

2、對象池

由 3.3.6 分析得到了賽道的兩個主要素:方塊和能量塊,而且他們在場景中存在大量類似的節點。為了減少主循環過程中建立對象帶來的開銷、避免因建立釋放等操作帶來的GC,我們要用對象池來進行優化。如下圖中的能量塊對象池,在大池子中有幾個小池子,友善集中管理。我們在大池子中保留了幾個原始實體,這些實體在 3.1 中被我們挂載到同一個父實體中。我們先在每個小池子中克隆三個實體,需要的時候往池子中取,如果池子已經被取空,則先克隆之後再取出。當不需要的時候歸還實體,并恢複實體的初始狀态。

從零開始開發3D遊戲,助力B類場景互動創新嘗試

這時候我們發現存在大量 Drawcall,因為場景中存在太多的實體。是以我們進一步優化,以角色的位置為中心設定一個可視區域,隻有在可視區範圍内才從對象池中擷取,可視區外則歸還實體。比如我把可視區的範圍縮小,可以更直覺的看出效果:

從零開始開發3D遊戲,助力B類場景互動創新嘗試

我們在每局遊戲開始的時候得到一個實體配置隊列,實體配置結構見 3.3.6,這些資料可以在 Oasis Editor 可視化編排之後通過腳本輸出。在角色奔跑過程中,我們從隊列中取出可視區的實體配置,然後從對象池中取出對應實體進行擺放,當超出可視區之外則歸還實體。

/**
 * 擷取可見範圍内的實體配置
 * @param positionX - 角色在x軸的位置
 */
getVisibleRewardConfig(positionX: number) {
  return this.rewardsConfig.filter(item => {
    const { position, entity } = item;
    // 已經不可見的實體隐藏掉
    if (position.x < positionX + SHOW_RANGE[0] && entity) {
      // 對象池歸還實體
      this.entityPool.putEntity(entity);
      item.entity = null;
    }
    return (
      position.x >= positionX + SHOW_RANGE[0] &&
      position.x <= positionX + SHOW_RANGE[1]
    );
  });
}           

3、 垂直同步

遊戲運作在移動端上會出現很明顯的抖動,嚴重影響使用者體驗。抖動有多種原因,先拿比較容易複現的場景來講,當能量塊和方塊離得近的時候會抖動(如下圖)。因為觸發了能量塊的碰撞結束事件,導緻角色做自由下落運動。但此時角色是在方塊上方,又觸發了水準奔跑。是以“水準跑-下落-水準跑-下落”,循環引起了抖動。

從零開始開發3D遊戲,助力B類場景互動創新嘗試

另一種是跳躍過程中存在的抖動現象,由于找不到複現規律,排查起來也比較費勁,一開始是往應用層的邏輯思考,後來發現當使用者不進行任何操作也會出現場景抖動,是以懷疑是螢幕重新整理率不同步導緻。由于我們一開始設定了目标幀率,是以主動關閉了垂直同步。垂直同步即場同步(Vertical synchronization),我們看到的遊戲動畫,都是經過使用者的互動、顯示卡的計算和渲染,再由螢幕重新整理,最終才呈現出來。我們所說的幀率是由螢幕的重新整理率決定的,而顯示卡的渲染速率取決于某一幀的複雜程度。如果顯示卡在 60/1 s 的時間裡渲染了兩張圖檔,但螢幕的重新整理率隻有 60Hz,這時候螢幕就隻能把兩幀圖檔拼接在一起,造成抖動或畫面撕裂。為了解決這個問題,我們可以開啟垂直同步,讓顯示卡每次渲染完成後,必須挂機休息,等待螢幕重新整理結束再渲染下一張圖檔。

4、 WebGL 版本切換

一開始遊戲運作在安卓版的釘釘上會出現閃退現象。經排查,釘釘使用的是舊版本的U4核心,該版本運作 WebGL2.0 會導緻閃退。在釘釘未更新最新版的U4核心的情況下,我們需要針對這部分使用者使用 WebGL1.0 處理。引擎的 WebGLMode 預設為 Auto,即裝置支援優先選擇 WebGL2.0,不支援 WebGL2.0 會復原至 WebGL1.0。當檢測到遊戲運作在安卓的釘釘上時,需要将 WebGLMode 切換到 WebGL1.0。

沉澱

目前遊戲已經抽成一個通用元件,支援多種業務場景接入,支援配置遊戲難度等級、換膚、自定義賽道。業務接入十分很簡單,隻需要準備一個 canvas,當使用者點選時觸發遊戲開始或重置,同時監聽得分、結束等事件。業務關聯關鍵代碼如下:

const gameContainer = canvas.parentElement;
// 遊戲初始化
const oasis = await boot({
  canvas,
  gameContainer,
  level: 2,
  debug: false,
  handleDowngrade: () => {
    setDowngrade(true);
  }
});
const { engine } = oasis;

// 開始遊戲
engine.dispatch("start");
// 再玩一次
engine.dispatch("reset");
// 獲得能量塊
engine.on("gotReward", () => {
  setScore(x => x + 1);
});
// 遊戲結束:死亡
engine.on("gameDie", () => {
  // do something
});
// 遊戲結束:到達終點
engine.on("gameOver", () => {
  // do something
});           

3D 研發過程的一些思考

本次項目時間十分緊張,開發和聯調隻用了一周半,加上ICBU跨供第一次嘗試 3D 互動遊戲,整個研發流程暴露了不少問題,這裡提下自己對研發流程的一些總結 。

設計階段

在拿到3D美術資源後,我們需要針對性的做一些優化。事實證明,3D資源交接這個流程耗費了大量時間,很多資源來回修改,是以我們應該在設計之前就跟美術同學約定好規範。這裡提一些資源優化的參考:

  • FBX 或 GLTF 不能包含相機,會導緻重複渲染;
  • 紋理不要綁在模型裡面,後期經常要替換紋理,避免重複加載了兩張;
  • 優先使用非透明材質,較少性能消耗;
  • 光照資訊已經合并在漫反射貼圖,使用 Unlit 材質替代 PBR 材質;
  • 較少三角形面數,把不在使用者視野内的面挖空,整體三角面控制在5w以内;
  • 盡量合并材質,減少渲染批次;
  • 删除模型檔案裡面包含的多餘節點,較少transform計算;
  • 紋理都用 2 的 n 次方,避免使用太大的紋理,盡量用 jpg 或 webp 格式,并進行壓縮。像能量塊這種小的模型一般 256*256 就夠了;
  • 粒子紋理用透明的,去掉四周多餘空間。多粒子的情況下使紋理合并到一張 sprite 圖。

開發階段

複雜的3D場景不建議直接用API來開發,Oasis Editor 很好的幫我們解決了開發效率低、效果還原差的兩大痛點。在開始開發之前,我們需要跟營銷前端同學劃分好場景,3D部分盡量不要包含業務邏輯,并約定好業務H5與3D場景之間的通信事件。3D開發過程中我們很難去定位問題,這裡推薦一個 Chrome 插件----SpectorJS,我們可以友善地檢視目前的渲染狀态、Drawcall 等資訊。

從零開始開發3D遊戲,助力B類場景互動創新嘗試

在做性能優化時,我們可以利用 Chrome 的 Performance 工具,把 CPU 設定成 6x slowdown 來模拟一些比較低端的機型,然後看每一幀的渲染時長,理想情況下每幀 16.6ms,我們可以展開看具體耗時是哪個接口然後對其優化。同時我們也可以設定 3G 網絡來模拟弱網情況。

從零開始開發3D遊戲,助力B類場景互動創新嘗試

測試階段

不同于前端頁面的測試,3D場景測試的重點是性能和相容性。正常來說,WebGL 支援率在99%以上,不支援 WebGL 我們也做了降級處理,是以更多的是關注上層應用的相容性。對于使用者一些可能的操作流,比如壓背景、省電模式、豎排方向鎖定狀态、切換APP等,測試過程中也需要覆寫到。

性能方面,重點關注白屏時間、Crash率、FPS、記憶體。可以引入 Oasis 3D 提供的性能檢視面闆(@oasis-engine/stats),FPS 在低端手機上需要穩定在30以上,Memory 在 60M 以下,DrawCall 在 50 以下,總的三角面在 5w 以内。還可以利用 MPerf 采集更具體的資料,複雜的場景需要出性能報告,對于超标的情況需要優化後才能上線。

從零開始開發3D遊戲,助力B類場景互動創新嘗試

釋出階段

線上要做好降級,本次遊戲針對安卓 6.0.0 以下、IOS 10 以下的裝置做了降級,不支援webGL、資源加載異常也做了降級,最終降級率在 3% 左右。同時要監控腳本異常、白屏及 crash。我們這次遊戲運作在微信、釘釘、阿裡賣家 APP 上,通過 EMAS 可以看到 APP 整體 crash,不過不能區分是不是h5引起的。另外,觀察線上的得分情況,可以動态調整我們的遊戲難度。

總結

互動遊戲在傳播、促活上面起到了明顯的效果,後續3D互動将作為使用者成長體系的一個能力,結合内容營銷、搭建能力、成長激勵,支撐營運進行内容營銷推廣和客戶成長體系建設。

⭐️ 最後預告一下,Oasis 3D 将在今年2月1日舉行開源釋出會,屆時将在B站進行全網直播,歡迎加入釘釘群 31360432 了解更多開源資訊~ 🥰

從零開始開發3D遊戲,助力B類場景互動創新嘗試

關注「Alibaba F2E」

把握阿裡巴巴前端新動态

繼續閱讀