天天看點

手把手教你開發互動遊戲,看 EVA 互動技術體系在金币小鎮的實踐

作者|馭劍

今年金币莊園迎來了一次改版,改版後的金币小鎮給使用者帶來了更豐富的視覺風格和全新的玩法。EVA體系是我們團隊在互動業務多年探索的基礎上産出的,它是一套上手快、開發效率高、能力完善的互動研發體系。EVA體系能大幅提升研發效率,小鎮快速改版上線就是一個很好的範例。本文将介紹EVA體系是如何在小鎮項目中實踐以及一些個人的總結。

如下圖就是金币小鎮的首頁界面,它包含了上方的遊戲區域和下方的商業化部分,今天主要介紹上方的遊戲區域。

手把手教你開發互動遊戲,看 EVA 互動技術體系在金币小鎮的實踐

遊戲方案

2.5D 簡介

小鎮的遊戲方案采用了

2D Isometric

(等軸測或者等距)的方案來實作,

2D Isometric

一般被簡單稱為2.5D。2.5D 是指一種在2D遊戲中制造出3D效果的顯示方法,這種方案更多的是對于視覺設計的規範,視覺按照一定的規範來設計素材,遊戲開發同學基于素材來做放置拼接素材來搭建場景。

手把手教你開發互動遊戲,看 EVA 互動技術體系在金币小鎮的實踐

2.5D遊戲素材【左側】,拼接後的效果【右側】(注:圖檔素材來源kenney網站)

細心看就會發現上方右側圖檔地面上有一個個平行四邊形的格子,素材就是基于這一個個格子作為基線來擺放拼接的,這就是我們經常說的

Tiled Map

(瓦片地圖)。

Tiled Map

是使用一些小單元(瓷磚)來拼成一副大地圖的遊戲做法,

Tiled Map

可以通過

Tiled Map Editor

這類工具來搭建。

小鎮用的就是用的上方的方案,下圖就是小鎮遊戲區域的

Tiled Map

視覺示意圖,其中數字代表的是每個建築的點位。

手把手教你開發互動遊戲,看 EVA 互動技術體系在金币小鎮的實踐

小鎮遊戲分層

小鎮項目整體涉及建築部分邏輯相對比較單一(渲染和更新替換資源),整體邏輯集中在領金币和多人的助力合力業務玩法部分。針對這種互動遊戲中碰到的普遍情況,既有Canvas又有DOM+CSS,我們一般使用混合開發的開發方式。

小鎮項目中會将遊戲區分為三層,具體分層如下:

手把手教你開發互動遊戲,看 EVA 互動技術體系在金币小鎮的實踐

Background層

負責遊戲場景的背景圖檔,

Scene層

(遊戲引擎開發)負責建築的排列渲染,

Hud層

負責業務邏輯的展示,利用傳統DOM和CSS的排版優勢,更能跟上業務的節奏。

開發鍊路

小鎮開發的基本工作鍊路如下:通過EVA Store的一站式上傳、預覽、代碼導出流程後就能傳遞遊戲引擎的資源了,然後将傳遞後資源使用EVA編輯器搭建場景後輸出場景資料,最終将場景資料交由EVAJS渲染遊戲場景,業務UI層使用DOM+CSS開發。

手把手教你開發互動遊戲,看 EVA 互動技術體系在金币小鎮的實踐

遊戲部分

基于上方的三層分層後,

Scene層

渲染使用到EVAJS遊戲引擎,EVAJS采用了ECS的設計模式作為底層架構,EVAJS ECS設計如下:

手把手教你開發互動遊戲,看 EVA 互動技術體系在金币小鎮的實踐

了解Unity的同學一定對這個不陌生,EVAJS提供了全套對于EVA Store豐富素材System、Component的支援。

我們舉例要渲染一個龍骨動畫,這時候我們需要建立一個實體,然後将龍骨動畫(

Dragonbones

)元件添加到實體上,最終龍骨動畫渲染系統來管理相關的龍骨動畫元件。僞代碼如下:

import { Game, GameObject, resource, RESOURCE_TYPE } from '@ali/eva.js';
import { RendererSystem } from '@ali/eva-plugin-renderer';
import { DragonBone, DragonBoneSystem } from '@ali/eva-plugin-renderer-dragonbone';

const game = new Game({
  systems: [
    new RendererSystem({
      canvas: document.getElementById('canvas'),
      width: 324,
      height: 240,
      transparent: true,
    }),
    // System: DragonBone系統
    new DragonBoneSystem()
  ],
});

// Entiy:遊戲對象
const dragonbone = new GameObject('dragonbone', {
  position: {
    x: 162,
    y: 240
  }
});

// Component: Dragonbone元件
const dragonboneCom = new DragonBone({
  resource: 'dragonbone',
  armatureName: 'B-1-9-3-2x2'
});

// 将Component添加到Entiy
const animation = dragonbone.addComponent(dragonboneCom);

animation.play('newAnimation');           

互動素材準備

我們了解了如何使用EVAJS渲染一個龍骨動畫到場景中,小鎮中每個建築對應的是一個龍骨動畫,小鎮中随着使用者等級提升龍骨動畫素材個數會達到120個以上,這時候這麼多的素材要如何管理呢?

互動中的素材管理面臨如下三個問題:

  1. 素材格式衆多:面對圖檔素材、模型素材、動效素材、音視訊這麼多個素材格式,每個素材格式又可能包含多個資源檔案,如果我們采用單獨上傳到CDN,對于引擎使用和後期的維護都是相當困難的。
  2. 如何最優化素材:不同格式的素材在上傳後如何才能最優的跑在引擎上,如何讓設計師同學即時預覽到素材效果?
  3. 多人協作:在協作流程中,如何讓不同角色的人協作起來

針對上面三個問題,EVA Store給出了很好的一站式解決方案:

1、EVA Store支援的如下衆多格式,并且這些互動素材的協定标準是由經濟體互動小組統一制定的,這就意味這些沉澱在平台上的素材資源可以放心的使用。

手把手教你開發互動遊戲,看 EVA 互動技術體系在金币小鎮的實踐

2、針對每種不同的素材格式,EVA Store都有相對應的算法進行優化,如幀動畫的圖檔合成壓縮、龍骨動畫(DragonBone)的頂點優化、雪碧圖最小記憶體占用壓縮等,這些操作可以保證素材達到最優化的效果,同時上傳後給提供了即時預覽友善設計同學檢視效果,如下

手把手教你開發互動遊戲,看 EVA 互動技術體系在金币小鎮的實踐

3、由于遊戲有好多章節,每個章節有對應各自建築的素材,借助EVA Store我們可以将每個章節設定一個項目并且設定權限友善不同角色之間的分工協作。

手把手教你開發互動遊戲,看 EVA 互動技術體系在金币小鎮的實踐

同時EVA Store的代碼預覽和線上實時編輯功能也能幫助前端定位資源問題:

手把手教你開發互動遊戲,看 EVA 互動技術體系在金币小鎮的實踐

場景搭建

素材管理的事情EVA Store很好的幫我解決了,接下來就是如何将這些素材按照視覺稿樣子放置龍骨動畫後生成場景資料,上面我們提到了tiled Map Editor工具能幫助我們搭建場景,但是現階段要和我們的EVA體系進行整合還是需要費點力氣:

  1. 小鎮中建築使用的龍骨動畫,這個很難在tiled Map Editor中實作所見即所得的效果,同時在面對後期更多的素材資源格式也不能很好适應。
  2. 前端要基于視覺稿中的點位來放置建築,類似于數格子後将建築放上去,這是一個比較枯燥費時的事。
  3. 需要産出基于我們EVA規範的資料地圖檔案,友善後期二次開發。

我們做了個簡單的編輯工具來解決上述問題(EVA Design已經在開發中):

1、我們提供了基于EVA Store素材格式導入(未來打通個人權限下的素材,選擇項目素材後導入資源面闆)

手把手教你開發互動遊戲,看 EVA 互動技術體系在金币小鎮的實踐

2、采用将視覺稿疊加在搭建場景下方層來協助我們将素材資源放置在對應的點位即可

手把手教你開發互動遊戲,看 EVA 互動技術體系在金币小鎮的實踐

3、支援導出json格式儲存到EVA Store,我們制定了基于這類2.5D遊戲場景格式,如下格式:

{
  "mapConfig": {
    "width": 30,
    "height": 30,
    "tileWidth": 108,
    "tileHeight": 54
  },
  "layers": [
    {
      "layerName": "buildings",
      "align": [-0.5, -1],
      "data": [
        [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
        // 1這個數字又對應的下方objects數組中index為1的元素( 即:building1 建築)
        [0,0,0,0,0,0,0,0,0,0,0,0,  1, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
      ],
      "layerOrder": 0,
      // 對應上方數組中的index
      "objects": [
        {}, 
        { "resourceKey": "building1" }
      ],
      // 對應上方的objects中的resourceKey, building1描述的是個龍骨動畫
      "resources": {
        "building1": {
          "name": "building1",
          "type": "DRAGONBONE",
          "src": {
            "image": {
              "type": "png",
              "url": "https://gw.alicdn.com/tfs/TB1A85jLEz1gK0jSZLeXXb9kVXa-256-256.png"
            },
            "tex": {
              "type": "json",
              "url": "https://pages.tmall.com/wow/eva/f9b692a8e8b90fb695caf9a5fedf12ee.json"
            },
            "ske": {
              "type": "json",
              "url": "https://pages.tmall.com/wow/eva/7ec54ea534ef1c121bdefb04636dee7e.json"
            }
          }
        }
      }
    }
  ]
}           

很多人可能會問,直接使用視覺稿中的定位來放置不是更簡單嗎? 其實搞這麼複雜的二維矩陣有它一定的優勢:

1、建築之間是深度的,存在互相遮擋關系,越是靠近螢幕頂部的物體應當越早地被畫出來,我們現在隻要按順序周遊二維數組就能做到這點,不需要開發過程中人為指定

2、導出地圖想當于将整個地圖劃分成一個個格子,我們可以通過移動格子來友善定位

3、友善實作移動對象的移動碰撞操作, 我們通過是否是道路瓦片來生成一份可行走的地圖,如下僞代碼:

// 0:可移動 1:障礙
const walkable = [
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0],
  [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0],
  [1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0],
  [1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1],
  [1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1],
  [1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1],
  [1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1],
  [0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0],
  [0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0],
  [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0],
  [1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0],
  [1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1],
  [1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1],
  [1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1],
  [1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1],
  [0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0],
  [0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
];           

可行走的地圖數組産出後,我們就可以使用尋路算法(如AStarFinder)來實作角色從一個點移動到另一個點的尋路效果了。

建築渲染

EVAJS支援完善的插件機制,我們可以很簡單的基于EVAJS的Plugin腳手架來開發插件,場景渲染中我們需要将上方産出的json格式渲染成建築群。我們需要将一個個的點位轉換成畫布上的x,y值,這些x,y值就是Component中的純資料表現,Componet僞代碼如下:

/**
 * 建立一個isometric的sprite精靈
 * isoSprite相對于傳統的sprite的Position是通過x和y根據tileWidth和tileHeight排布的
 * isoSprite還具有sortable的,是以需要設定zIndex
 *
 * @extends Eva.Component
 * @param {number} x - 在二維矩陣中第幾列
 * @param {number} y - 在二維矩陣中的第幾行
 * @param {number} tileWidth - 瓦片寬度,計算isoX,isoY使用
 * @param {number} tileHeight - 瓦片高度,計算isoX,isoY使用
 */
class IsoSprite extends Component {
  static componentName = 'IsoSprite';

  _depth: number = 0;
  isoX: number;
  isoY: number;

  init(params: IIsoSprite) {
    const { x, y, tileWidth, tileHeight } = params;
    this.isoX = (x - y) * tileWidth / 2;
    this.isoY = (x + y) * tileHeight / 2;

    // 盡量拉開每個面片的層級,為了友善後期插入元素時設定層級
    this._depth = 10 * (x + y);

    this.addComponents();
  }

  addComponents() {
    this.gameObject.addComponent(
      new Render({
        zIndex: this._depth,
      })
    );

    this.gameObject.transform.position = {
      x: this.isoX,
      y: this.isoY,
    };
  }

  setZorder(depth: number) {
    this._depth = depth;
  }
}           

System通過裝飾器監聽它所關心的Component,這裡面我們監聽的是上方的IsoSprite,當IsoSprite的_depth更變時就會觸發System相關操作

@decorators.componentObserver({
  IsoSprite: ["_depth"],
  Render: ["zIndex"],
})
class TileSystem extends System {
  static systemName: string = 'TileSystem';
 
  init(params: TileSystemParams) {
    this.placeTile();
  }   


  createComponentByType(resourceName: number, spriteName?: string) {
    // 通過不同的資源類型生成不同的資源執行個體
  }

  /**
   * 通過map放置點位
   * @param layer 地圖點位
   * @param mapConfig 地圖資訊
   */
  placeTile(layer, mapConfig) {
    const { tileWidth, tileHeight } = mapConfig;
    const { data, align, layerName, objects } = layer;
    for (let y = 0; y < data.length; y++) {
      for (let x = 0; x < data[y].length; x++) {
        // 基于不同的資源類型放置不同的資源
        this.createComponentByType()
      }
    }
  }

  update() {
    const changes = this.componentObserver.clear();
    for (const change of changes) {
      if (change.type === "ADD") {
        // do something tiles add
      }
      if (change.type === "CHANGE") {
        // do something tiles change
      }
      if(change.type === 'REMOVE') {
        // do something tiles remove
      }
    }
  }
}           

上方開發的Component元件中有一個_depth(深度)變量,這個深度就類似CSS的zIndex,如下圖的主建築①的zIndex會高于遮擋住建築②,由于建築①占用了很多位置,這時會導緻建築②的一大半部分被擋住導緻很難點中。

手把手教你開發互動遊戲,看 EVA 互動技術體系在金币小鎮的實踐

遊戲中我們會設定模型的hitArea來設定它的可點區域,我們使用右側小工具來生成hitArea資料,我們設定主建築的可點區域為如上形狀,這樣就能避免在點選到主建築空白區域也觸發事件,這樣就可以規避了遮擋問題。

遊戲和DOM互動

混合開發方式中遊戲和DOM層由于分層了後,兩個層之間的互動一般采用的消息事件來進行排程,消息事件機制是遊戲開發中比較常見的解耦工具,為了規範事件排程機制,友善多人協作中事件發送監聽的無縫銜接,我們采用EVA Base中的 MX(一套資料流轉和事件通訊的方案)作為事件和資料中樞。

手把手教你開發互動遊戲,看 EVA 互動技術體系在金币小鎮的實踐

如下代碼示範了如何通過消息事件機制來實作遊戲區和HUD層的互動:

/** Eva Game */
import {Event} from '@ali/eva-plugin-render';
const evt = dragonboneGameObject.addComponent(new Event())
evt.on('tap', (e) => {
  // 點選遊戲建築
  mx.event.emit('ClickOnGameBuilding', {
    ev: e
  });
});

/** HUD */
function HUD(props) {
  useEffect(() => {
    mx.event.on('ClickOnGameBuilding', e => {
      console.log('show game building tooltip')
    });
  }, []);
  return <div></div>
}           

HUD層

可通路性優化

值得慶幸的是,我們在金币小鎮上全鍊路是對Web可通路性做了優化。對于過渡依賴于讀螢幕軟體,比如iOS的“VoiceOver”, Android的“TalkBack”,可以順暢的在小鎮玩耍:

點選檢視視訊

金币小鎮中針對Web可通路性方面(也稱之為無障礙)的優化主要兩個部分:遊戲區域和非遊戲區域。在這裡我們主要和大家一起探讨遊戲區域的可通路性是如何進行優化的。就是上圖紅色框中的部分:

手把手教你開發互動遊戲,看 EVA 互動技術體系在金币小鎮的實踐

這個區域是金币小鎮的遊戲化區域,也是使用者進來互動的區域,比如點選建築物有相應的提示介紹,進點按鈕會進入到相應的二級頁面或者說拉引任務系統之類等等。

就從這個區域開始吧!如果你是Web開發者,通過浏覽器調試工具檢視這個區域,可以看到

<canvas>

和其他一些HTML标簽(比如

<div>

)組合在一起:

手把手教你開發互動遊戲,看 EVA 互動技術體系在金币小鎮的實踐

采用浏覽器調試工具的分層工具來檢視将會更清晰一些:

手把手教你開發互動遊戲,看 EVA 互動技術體系在金币小鎮的實踐

前面提到過,開發遊戲區域前端主要采用的是Rax EVA進行開發的,遊戲區域的可通路性優化都集中在 Hud 層:

手把手教你開發互動遊戲,看 EVA 互動技術體系在金币小鎮的實踐

在整個遊戲區域點選每個建築物都會有相應的提示資訊,或者有彈窗以及跳轉等互動:

對于Hud中的圖示按鈕,這個問題不大,他們就是純DOM:

手把手教你開發互動遊戲,看 EVA 互動技術體系在金币小鎮的實踐

比較麻煩的是 點選遊戲區域建築物的互動 。為了讓這個互動能和螢幕閱讀器這樣的輔助技術有一個較好的通訊,我們采用了 Canvas和DOM分離的操作。即:在Hud層内置了和遊戲區域相比對的點選錨點 , 比如下圖中小圓圈所示:

手把手教你開發互動遊戲,看 EVA 互動技術體系在金币小鎮的實踐

這些錨點并不會影響我們整個UI效果,我們使用CSS做了一些處理,正常情況下他們都是一個

2px x 2px

的透明矩形,但在開啟螢幕閱讀器下面,錨點得到焦點時會有相應的焦點樣式:

手把手教你開發互動遊戲,看 EVA 互動技術體系在金币小鎮的實踐

錨點的DOM結構大緻像下面這樣:

<!-- 提示框未顯式,使用者未點選錨點對應的建築物 -->
<div class="home__inresultant--force">
  <div class="tooltip__anchor" role="button" tabindex="-1" aria-expanded="false">
    <span class="sr-only">氧氧溫室</span>
  </div>
</div>

<!-- 提示框顯式,使用者點選錨點對應的建築物 -->
<div class="home__inresultant--force">
  <div class="tooltip__anchor" role="button" tabindex="-1" aria-expanded="true" aria-describedby="a11y__inresultant--force">
    <span class="sr-only">氧氧溫室</span>
  </div>
</div>           

比如上面示例的錨點,它有對應的提示框:

<div>
  <div class="tooltips tooltip__inresultant--force " role="alert" tabindex="-1" id="a11y__inresultant--force" >
    <svg focusable="false" aria-hidden="true" width="216" height="118.4375" >
      <!-- 提示框UI,使用SVG建構 -->
    </svg>
    <div class="tooltips__content">
      <div class="tooltip__inresultant--force--content">
        <!-- 提示框内容 --> 
      </div>
    </div>
  </div>
</div>           

不管是在錨點還是提示框的DOM元素上,我們都看到了ARIA相關的特性,比如 角色 、 屬性 和 狀态 :

  • 角色 :在錨點的

    div

    使用了

    role="button"

    告訴螢幕閱讀器,它是一個按鈕;在提示框的

    div

    role="alert"

    告訴螢幕閱讀器,它是一個警告框(或提示框)
  • 屬性 :在錨點的

    div

    aria-describedby

    屬性綁定提示框的

    id

    值,讓他們有一個綁定關系
  • 狀态 :在錨點的

    div

    aria-expanded

    來告訴螢幕閱讀器提示框的狀态,如果提示框未顯示,該屬性的值為

    false

    表示提示框是折疊狀态,反之為

    true

    ,表示提示框是展示狀态,螢幕閱讀器可以讀出提示框的相應資訊

有關于ARIA更多的介紹這裡就不展開了,如果你對這方面知識感興趣的話,可以閱讀下面這些資料:

我們從ARIA的世界中回來。

在錨點和提示框上除了使用ARIA之外,還使用了一些其他對螢幕閱讀器友好的特性,比如使用

tabindex

來給非聚焦元素設定焦點;比如在不需要被螢幕閱讀器識别(朗讀出來)的元素上顯式設定

aria-hidden="true"

; 比如使用CSS讓文本隻讓螢幕閱讀器可以識别:

.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0,0,0,0);
    clip-path: inset(100%);
    white-space: nowrap;
    border-width: 0;
}           

由于這樣的場景在整個遊戲區的使用頻率非常的高,加上UI個性化較強(Tooltips尖角各異),是以我們封閉了一個svg-tooltip的元件,在封裝這個元件的時候,我們把無障礙相關的特性内置進去。在使用的時候隻需要像下面這樣即可:

<SvgToolTip
  className="tooltip__growth-wrap"
  a11yId="a11y__lock"
  a11yRole="tooltip"
  visible={true}
  trigger="none"
  content={growthContent}
  closeOutSide={false}
  {...NormalToolTip}
  onVisibleChange={v => onToolTipVisibleChange(v, config.petName)}>
    <div
      className="tooltip-anchor"
      style={calPosStyle(toolTipsPos)}
      role="button"
      tabIndex={-1}>
      <span className="sr-only">{skin.name}</span>
    </div>
</SvgToolTip>           

在調用

SvgToolTip

時,需要給該元件透傳

a11yId

這個

props

,并且與觸發

SvgToolTip

元素的

aria-describedby

綁定在一起。即

aria-describedby

的值和

a11yId

值等同。

另外有一個細節需要注意的就是,Tooltips提示框有兩種不同的互動類型,一種是無需任何互動,一進入頁面提示框就展示;另外一種就是帶有互動,使用者點選建築物之後提示框展示,經過幾秒或使用者點選另外的地方,該提示框會隐藏。是以在設定

a11yRole

時要選擇不同的值:

  • 提示框不需要任何互動顯示的,給

    a11yRole

    傳一個

    tooltip

  • 提示框需要點選才顯示的,給

    a11yRole

    alert

這樣在編譯出來的代碼,就是像我們上面所說的一樣。

SVG 的使用

手把手教你開發互動遊戲,看 EVA 互動技術體系在金币小鎮的實踐

在項目中我們使用了很多不規則尖角的氣泡樣式,為了兼顧優雅和通用性,我們使用了SVG來實作toolTip的外框,并且開發相對應的工具來解決更複雜的氣泡樣式。具體文章可以移步《用SVG實作一個優雅的提示框》。

動态彈窗方案之 EVA Ware

受制于蘋果對于動态化H5遊戲稽核政策的限制,我們的項目需要跟随IOS手淘的節奏來內建代碼到包中,這樣一來大大降低了H5動态能力。面對業務中大量的玩法政策需要由彈窗來承接,我們接入了一套經曆過大促考驗的彈窗規模化解決方案,簡單來說就是拉取彈窗表現層的DSL,在用戶端來渲染并且基于彈窗管理器來管理各個彈窗的生命周期, 具體方案可以移步

《互動生産力進化之路 | 618 淘系前端技術分享》

其他

  • CSS不規則形狀的蒙層: 領淘金币按鈕上的掃光效果使用的css不規則蒙層
  • 适度使用APNG: APNG在手淘中表現已經非常不錯了,項目中部分動效我們使用了APNG,在配置和修改的便利度來說遠勝于一般動效。

最後

第十五屆 D2 前端技術論壇的 D2 SPACE 也是使用 EVA 來開發的,由淘系互動團隊傾情支援。歡迎大家使用 EVA 體系來開發互動項目,我們團隊的目标是【人人可開發,處處有互動】。如果你對 工程/搭建/低代碼研發方向 或者 WebGL/圖形渲染/特效方向 等感興趣,歡迎微信聯系/釘釘進群一起交流。

手把手教你開發互動遊戲,看 EVA 互動技術體系在金币小鎮的實踐
手把手教你開發互動遊戲,看 EVA 互動技術體系在金币小鎮的實踐

關注「Alibaba F2E」

把握阿裡巴巴前端新動向

繼續閱讀