1. 程式人生 > 其它 >沒想到吧!這個可可愛愛的遊戲居然是用 ECharts 實現的!

沒想到吧!這個可可愛愛的遊戲居然是用 ECharts 實現的!

摘要:echarts 是一個很強大的圖表庫,除了我們常見的圖表功能,還可以自定義圖形,這個功能讓我們可以很簡單地在畫布上繪製一些非常規的圖形,基於此,我們來玩一些花哨的:做一個 Flappy Bird 小遊戲。

本文分享自華為雲社群《沒想到吧!這個可可愛愛的遊戲居然是用 ECharts 實現的!》,作者: DevUI 。

前言

echarts 是一個很強大的圖表庫,除了我們常見的圖表功能,echarts 有一個自定義圖形的功能,這個功能可以讓我們很簡單地在畫布上繪製一些非常規的圖形,基於此,我們來玩一些花哨的。

Flappy Bird 小遊戲體驗地址(看看你能玩幾分):https://foolmadao.github.io/echart-flappy-bird/echarts-bird.html

下面我們來一步步實現他。

1 在座標系中畫一隻會動的小鳥

首先例項化一個 echart 容器,再從網上找一個畫素小鳥的圖片,將散點圖的散點形狀,用自定義圖片的方式改為小鳥。

const myChart = echarts.init(document.getElementById('main'));
option = {
  series: [
    {
      name: 'bird',
      type: 'scatter',
      symbolSize: 50,
      symbol: 'image://bird.png',
      data: [
        [
50, 80] ], animation: false }, ] }; myChart.setOption(option);

要讓小鳥動起來,就需要給一個向右的速度和向下的加速度,並在每一幀的場景中重新整理小鳥的位置。而小鳥向上飛的動作,則可以靠角度的旋轉來實現,向上飛的觸發條件設定為空格事件。

option = {
  series: [
    {
      xAxis: {
        show: false,
        type: 'value',
        min: 0,
        max: 200,
      },
      yAxis: {
        show: 
false, min: 0, max: 100 }, name: 'bird', type: 'scatter', symbolSize: 50, symbol: 'image://bird.png', data: [ [50, 80] ], animation: false }, ] }; // 設定速度和加速度 let a = 0.05; let vh = 0; let vw = 0.5 timer = setInterval(() => { // 小鳥位置和仰角調整 vh = vh - a; option.series[0].data[0][1] += vh; option.series[0].data[0][0] += vw; option.series[0].symbolRotate = option.series[0].symbolRotate ? option.series[0].symbolRotate - 5 : 0; // 座標系範圍調整 option.xAxis.min += vw; option.xAxis.max += vw; myChart.setOption(option); }, 25);

效果如下

2 用自定義圖形繪製障礙物

echarts 自定義系列,渲染邏輯由開發者通過 renderItem 函式實現。該函式接收兩個引數 params 和 api,params 包含了當前資料資訊和座標系的資訊,api 是一些開發者可呼叫的方法集合,常用的方法有:

  • api.value (…),意思是取出 dataItem 中的數值。例如 api.value (0) 表示取出當前 dataItem 中第一個維度的數值。
  • api.coord (…),意思是進行座標轉換計算。例如 var point = api.coord ([api.value (0), api.value (1)]) 表示 dataItem 中的數值轉換成座標系上的點。
  • api.size (…), 可以得到座標系上一段數值範圍對應的長度。
  • api.style (…),可以獲取到 series.itemStyle 中定義的樣式資訊。

靈活使用上述 api,就可以將使用者傳入的 Data 資料轉換為自己想要的座標系上的畫素位置。

renderItem 函式返回一個 echarts 中的 graphic 類,可以多種圖形組合成你需要的形狀,graphic 型別。對於我們遊戲中的障礙物只需要使用矩形即可繪製出來,我們使用到下面兩個類。

  • type: group, 組合類,可以將多個圖形類組合成一個圖形,子類放在 children 中。
  • type: rect, 矩形類,通過定義矩形左上角座標點,和矩形寬高確定圖形。
// 資料項定義為[x座標,下方水管上側y座標, 上方水管下側y座標]
data: [
  [150, 50, 80],
  ...
]

renderItem: function (params, api) {
    // 獲取每個水管主體矩形的起始座標點
    let start1 = api.coord([api.value(0) - 10, api.value(1)]);
    let start2 = api.coord([api.value(0) - 10, 100]);
    // 獲取兩個水管頭矩形的起始座標點
    let startHead1 = api.coord([api.value(0) - 12, api.value(1)]);
    let startHead2 = api.coord([api.value(0) - 12, api.value(2) + 8])
    // 水管頭矩形的寬高
    let headSize = api.size([24, 8])
    // 水管頭矩形的寬高
    let rect = api.size([20, api.value(1)]);
    let rect2 = api.size([20, 100 - api.value(2)]);
    // 座標系配置
    const common = {
        x: params.coordSys.x,
        y: params.coordSys.y,
        width: params.coordSys.width,
        height: params.coordSys.height
    }
    // 水管形狀
    const rectShape = echarts.graphic.clipRectByRect(
      {
        x: start1[0],
        y: start1[1],
        width: rect[0],
        height: rect[1]
      },common
    );
    const rectShape2 = echarts.graphic.clipRectByRect(
      {
        x: start2[0],
        y: start2[1],
        width: rect2[0],
        height: rect2[1]
      },
      common
    )

    // 水管頭形狀
    const rectHeadShape = echarts.graphic.clipRectByRect(
      {
        x: startHead1[0],
        y: startHead1[1],
        width: headSize[0],
        height: headSize[1]
      },common
    );

    const rectHeadShape2 = echarts.graphic.clipRectByRect(
      {
        x: startHead2[0],
        y: startHead2[1],
        width: headSize[0],
        height: headSize[1]
      },common
    );

    // 返回一個group類,由四個矩形組成
    return {
        type: 'group',
        children: [{
            type: 'rect',
            shape: rectShape,
            style: {
              ...api.style(),
              lineWidth: 1,
              stroke: '#000'
            }
        }, {
            type: 'rect',
            shape: rectShape2,
            style: {
              ...api.style(),
              lineWidth: 1,
              stroke: '#000'
            }
        },
        {
            type: 'rect',
            shape: rectHeadShape,
            style: {
              ...api.style(),
              lineWidth: 1,
              stroke: '#000'
            }
        },
        {
            type: 'rect',
            shape: rectHeadShape2,
            style: {
              ...api.style(),
              lineWidth: 1,
              stroke: '#000'
            }
        }]
    };
  },

顏色定義,我們為了讓水管具有光澤使用了 echarts 的線性漸變色物件。

itemStyle: {
  // 漸變色物件
  color: {
    type: 'linear',
    x: 0,
    y: 0,
    x2: 1,
    y2: 0,
    colorStops: [{
        offset: 0, color: '#ddf38c' // 0% 處的顏色
    }, {
        offset: 1, color: '#587d2a' // 100% 處的顏色
    }],
    global: false // 預設為 false
  },
  borderWidth: 3
},

另外,用一個 for 迴圈一次性隨機出多個柱子的資料

function initObstacleData() {
    // 新增minHeight防止空隙太小
    let minHeight = 20;
    let start = 150;
    obstacleData = [];
    for (let index = 0; index < 50; index++) {
      const height = Math.random() * 30 + minHeight;
      const obstacleStart = Math.random() * (90 - minHeight);
      obstacleData.push(
        [
          start + 50 * index,
          obstacleStart,
          obstacleStart + height > 100 ? 100 : obstacleStart + height
        ]
      )
    }
  }

再將背景用遊戲圖片填充,我們就將整個遊戲場景,繪製完成:

3 進行碰撞檢測

由於飛行軌跡和障礙物資料都很簡單,所以我們可以將碰撞邏輯簡化為小鳥圖片的正方形中,我們判斷右上和右下角是否進入了自定義圖形的範圍內。

對於特定座標下的碰撞範圍,因為柱子固定每格 50 座標值一個,寬度也是固定的,所以,可碰撞的橫座標範圍就可以簡化為 (x / 50 % 1) < 0.6

在特定範圍內,依據 Math.floor (x / 50) 獲取到對應的資料,即可判斷出兩個邊角座標是否和柱子區域有重疊了。在動畫幀中判斷,如果重疊了,就停止動畫播放,遊戲結束。

// centerCoord為散點座標點
function judgeCollision(centerCoord) {
  if (centerCoord[1] < 0 || centerCoord[1] > 100) {
    return false;
  }
  let coordList = [
    [centerCoord[0] + 15, centerCoord[1] + 1],
    [centerCoord[0] + 15, centerCoord[1] - 1],
  ]

  for (let i = 0; i < 2; i++) {
    const coord = coordList[i];
    const index = coord[0] / 50;
    if (index % 1 < 0.6 && obstacleData[Math.floor(index) - 3]) {
      if (obstacleData[Math.floor(index) - 3][1] > coord[1] || obstacleData[Math.floor(index) - 3][2] < coord[1]) {
        return false;
      }
    }
  }
  return false
}

function initAnimation() {
  // 動畫設定
  timer = setInterval(() => {
    // 小鳥速度和仰角調整
    vh = vh - a;
    option.series[0].data[0][1] += vh;
    option.series[0].data[0][0] += vw;
    option.series[0].symbolRotate = option.series[0].symbolRotate ? option.series[0].symbolRotate - 5 : 0;

    // 座標系範圍調整
    option.xAxis.min += vw;
    option.xAxis.max += vw;

    // 碰撞判斷
    const result = judgeCollision(option.series[0].data[0])

    if(result) { // 產生碰撞後結束動畫
      endAnimation();
    }

    myChart.setOption(option);
  }, 25);
}

總結

echarts 提供了強大的圖形繪製自定義能力,要使用好這種能力,一定要理解好資料座標點和畫素座標點之間的轉換邏輯,這是將資料具象到畫布上的重要一步。

運用好這個功能,再也不怕產品提出奇奇怪怪的圖表需求。

原始碼地址:https://github.com/foolmadao/echart-flappy-bird

點選關注,第一時間瞭解華為雲新鮮技術~