没想到吧!这个可可爱爱的游戏居然是用 echarts 实现的!-星辰平台

发表于 2022/05/11 10:23:57 2022/05/11
【摘要】 echarts是一个很强大的图表库,除了我们常见的图表功能,还可以自定义图形,这个功能让我们可以很简单地在画布上绘制一些非常规的图形,基于此,我们来玩一些花哨的:做一个flappy bird小游戏。

欢迎关注:,并加入到开源生态的建设中来!

echarts是一个很强大的图表库,除了我们常见的图表功能,echarts有一个自定义图形的功能,这个功能可以让我们很简单地在画布上绘制一些非常规的图形,基于此,我们来玩一些花哨的。

flappy bird小游戏体验地址(看看你能玩几分):

下面我们来一步步实现他。

首先实例化一个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);

效果如下

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类,可以多种图形组合成你需要的形状,。对于我们游戏中的障碍物只需要使用矩形即可绘制出来,我们使用到下面两个类。

  • 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
        ]
      )
    }
  }

再将背景用游戏图片填充,我们就将整个游戏场景,绘制完成:

由于飞行轨迹和障碍物数据都很简单,所以我们可以将碰撞逻辑简化为小鸟图片的正方形中,我们判断右上和右下角是否进入了自定义图形的范围内。

对于特定坐标下的碰撞范围,因为柱子固定每格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提供了强大的图形绘制自定义能力,要使用好这种能力,一定要理解好数据坐标点和像素坐标点之间的转换逻辑,这是将数据具象到画布上的重要一步。

运用好这个功能,再也不怕产品提出奇奇怪怪的图表需求。

源码地址:

目前和都在大量招募社区贡献者,欢迎加入星辰平台!

添加小助手微信:devui-official,拉你进devui技术交流群。

欢迎关注:,并加入到开源生态的建设中来!


华为伙伴暨开发者大会2022火热来袭,大会采用线上直播 线下80余个分会场联动的形式,聚焦伙伴和开发者最为关切的话题、释放更多潜力,携手伙伴共同成就。干货满满。

【精彩活动】

勇往直前·做全能开发者→12场技术直播前瞻,8大技术宝典高能输出,还有代码密室、知识竞赛等多轮神秘任务等你来挑战。即刻闯关,开启终极大奖!戳踏上全能开发者晋级之路吧!

【技术专题】

未来已来,2022技术探秘→聚焦华为各领域的前沿技术、重磅开源项目、创新应用实践。站在智能世界的入口,探索未来如何照进现实,干货满满!

【星辰平台的版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区),文章链接,文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件至:进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容。
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。