教你用 ECharts 轻松做一个Flappy Bird小游戏

x33g5p2x  于2022-05-13 转载在 其他  
字(5.0k)|赞(0)|评价(0)|浏览(430)

本文分享自华为云社区《没想到吧!这个可可爱爱的游戏居然是用 ECharts 实现的!》,作者: DevUI 。

前言

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

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

https://foolmadao.github.io/echart-flappy-bird/echarts-bird.html

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

1 在坐标系中画一只会动的小鸟

首先实例化一个echart容器,再从网上找一个像素小鸟的图片,将散点图的散点形状,用自定义图片的方式改为小鸟。

  1. const myChart = echarts.init(document.getElementById('main'));
  2. option = {
  3. series: [
  4. {
  5. name: 'bird',
  6. type: 'scatter',
  7. symbolSize: 50,
  8. symbol: 'image://bird.png',
  9. data: [
  10. [50, 80]
  11. ],
  12. animation: false
  13. },
  14. ]
  15. };
  16. myChart.setOption(option);

要让小鸟动起来,就需要给一个向右的速度和向下的加速度,并在每一帧的场景中刷新小鸟的位置。而小鸟向上飞的动作,则可以靠角度的旋转来实现,向上飞的触发条件设置为空格事件。

  1. option = {
  2. series: [
  3. {
  4. xAxis: {
  5. show: false,
  6. type: 'value',
  7. min: 0,
  8. max: 200,
  9. },
  10. yAxis: {
  11. show: false,
  12. min: 0,
  13. max: 100
  14. },
  15. name: 'bird',
  16. type: 'scatter',
  17. symbolSize: 50,
  18. symbol: 'image://bird.png',
  19. data: [
  20. [50, 80]
  21. ],
  22. animation: false
  23. },
  24. ]
  25. };
  26. // 设置速度和加速度
  27. let a = 0.05;
  28. let vh = 0;
  29. let vw = 0.5
  30. timer = setInterval(() => {
  31. // 小鸟位置和仰角调整
  32. vh = vh - a;
  33. option.series[0].data[0][1] += vh;
  34. option.series[0].data[0][0] += vw;
  35. option.series[0].symbolRotate = option.series[0].symbolRotate ? option.series[0].symbolRotate - 5 : 0;
  36. // 坐标系范围调整
  37. option.xAxis.min += vw;
  38. option.xAxis.max += vw;
  39. myChart.setOption(option);
  40. }, 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, 矩形类,通过定义矩形左上角坐标点,和矩形宽高确定图形。
  1. // 数据项定义为[x坐标,下方水管上侧y坐标, 上方水管下侧y坐标]
  2. data: [
  3. [150, 50, 80],
  4. ...
  5. ]
  6. renderItem: function (params, api) {
  7. // 获取每个水管主体矩形的起始坐标点
  8. let start1 = api.coord([api.value(0) - 10, api.value(1)]);
  9. let start2 = api.coord([api.value(0) - 10, 100]);
  10. // 获取两个水管头矩形的起始坐标点
  11. let startHead1 = api.coord([api.value(0) - 12, api.value(1)]);
  12. let startHead2 = api.coord([api.value(0) - 12, api.value(2) + 8])
  13. // 水管头矩形的宽高
  14. let headSize = api.size([24, 8])
  15. // 水管头矩形的宽高
  16. let rect = api.size([20, api.value(1)]);
  17. let rect2 = api.size([20, 100 - api.value(2)]);
  18. // 坐标系配置
  19. const common = {
  20. x: params.coordSys.x,
  21. y: params.coordSys.y,
  22. width: params.coordSys.width,
  23. height: params.coordSys.height
  24. }
  25. // 水管形状
  26. const rectShape = echarts.graphic.clipRectByRect(
  27. {
  28. x: start1[0],
  29. y: start1[1],
  30. width: rect[0],
  31. height: rect[1]
  32. },common
  33. );
  34. const rectShape2 = echarts.graphic.clipRectByRect(
  35. {
  36. x: start2[0],
  37. y: start2[1],
  38. width: rect2[0],
  39. height: rect2[1]
  40. },
  41. common
  42. )
  43. // 水管头形状
  44. const rectHeadShape = echarts.graphic.clipRectByRect(
  45. {
  46. x: startHead1[0],
  47. y: startHead1[1],
  48. width: headSize[0],
  49. height: headSize[1]
  50. },common
  51. );
  52. const rectHeadShape2 = echarts.graphic.clipRectByRect(
  53. {
  54. x: startHead2[0],
  55. y: startHead2[1],
  56. width: headSize[0],
  57. height: headSize[1]
  58. },common
  59. );
  60. // 返回一个group类,由四个矩形组成
  61. return {
  62. type: 'group',
  63. children: [{
  64. type: 'rect',
  65. shape: rectShape,
  66. style: {
  67. ...api.style(),
  68. lineWidth: 1,
  69. stroke: '#000'
  70. }
  71. }, {
  72. type: 'rect',
  73. shape: rectShape2,
  74. style: {
  75. ...api.style(),
  76. lineWidth: 1,
  77. stroke: '#000'
  78. }
  79. },
  80. {
  81. type: 'rect',
  82. shape: rectHeadShape,
  83. style: {
  84. ...api.style(),
  85. lineWidth: 1,
  86. stroke: '#000'
  87. }
  88. },
  89. {
  90. type: 'rect',
  91. shape: rectHeadShape2,
  92. style: {
  93. ...api.style(),
  94. lineWidth: 1,
  95. stroke: '#000'
  96. }
  97. }]
  98. };
  99. },

颜色定义, 我们为了让水管具有光泽使用了echarts的线性渐变色对象。

  1. itemStyle: {
  2. // 渐变色对象
  3. color: {
  4. type: 'linear',
  5. x: 0,
  6. y: 0,
  7. x2: 1,
  8. y2: 0,
  9. colorStops: [{
  10. offset: 0, color: '#ddf38c' // 0% 处的颜色
  11. }, {
  12. offset: 1, color: '#587d2a' // 100% 处的颜色
  13. }],
  14. global: false // 缺省为 false
  15. },
  16. borderWidth: 3
  17. },

另外,用一个for循环一次性随机出多个柱子的数据

  1. function initObstacleData() {
  2. // 添加minHeight防止空隙太小
  3. let minHeight = 20;
  4. let start = 150;
  5. obstacleData = [];
  6. for (let index = 0; index < 50; index++) {
  7. const height = Math.random() * 30 + minHeight;
  8. const obstacleStart = Math.random() * (90 - minHeight);
  9. obstacleData.push(
  10. [
  11. start + 50 * index,
  12. obstacleStart,
  13. obstacleStart + height > 100 ? 100 : obstacleStart + height
  14. ]
  15. )
  16. }
  17. }

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

3 进行碰撞检测

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

对于特定坐标下的碰撞范围,因为柱子固定每格50坐标值一个,宽度也是固定的,所以,可碰撞的横坐标范围就可以简化为 (x / 50 % 1) < 0.6

在特定范围内,依据Math.floor(x / 50)获取到对应的数据,即可判断出两个边角坐标是否和柱子区域有重叠了。在动画帧中判断,如果重叠了,就停止动画播放,游戏结束。

  1. // centerCoord为散点坐标点
  2. function judgeCollision(centerCoord) {
  3. if (centerCoord[1] < 0 || centerCoord[1] > 100) {
  4. return false;
  5. }
  6. let coordList = [
  7. [centerCoord[0] + 15, centerCoord[1] + 1],
  8. [centerCoord[0] + 15, centerCoord[1] - 1],
  9. ]
  10. for (let i = 0; i < 2; i++) {
  11. const coord = coordList[i];
  12. const index = coord[0] / 50;
  13. if (index % 1 < 0.6 && obstacleData[Math.floor(index) - 3]) {
  14. if (obstacleData[Math.floor(index) - 3][1] > coord[1] || obstacleData[Math.floor(index) - 3][2] < coord[1]) {
  15. return false;
  16. }
  17. }
  18. }
  19. return false
  20. }
  21. function initAnimation() {
  22. // 动画设置
  23. timer = setInterval(() => {
  24. // 小鸟速度和仰角调整
  25. vh = vh - a;
  26. option.series[0].data[0][1] += vh;
  27. option.series[0].data[0][0] += vw;
  28. option.series[0].symbolRotate = option.series[0].symbolRotate ? option.series[0].symbolRotate - 5 : 0;
  29. // 坐标系范围调整
  30. option.xAxis.min += vw;
  31. option.xAxis.max += vw;
  32. // 碰撞判断
  33. const result = judgeCollision(option.series[0].data[0])
  34. if(result) { // 产生碰撞后结束动画
  35. endAnimation();
  36. }
  37. myChart.setOption(option);
  38. }, 25);
  39. }

总结

echarts提供了强大的图形绘制自定义能力,要使用好这种能力,一定要理解好数据坐标点和像素坐标点之间的转换逻辑,这是将数据具象到画布上的重要一步。

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

源码地址:GitHub - foolmadao/echart-flappy-bird

点击关注,第一时间了解华为云新鲜技术~​

相关文章