基于Flutter Canvas的飞机大战(二)

回顾

昨天下午笔者已经完成了背景动画的循环播放. 晚上笔者就开发中发现的问题在stackoverflow上进行提问.
问题大概内容:

如何在Canvas中, 将一个较小的图片, 拉伸平铺 问题链接

这个问题, 收到了二个有效的回答

  • Canvas.drawImageRect()
  • paintImage()

进过笔者测试
<figure class="half">
<img src="https://user-gold-cdn.xitu.io/2019/1/25/168842541b92ee59?w=776&h=1538&f=png&s=27498" style="wdith: 200px">
<img src="https://user-gold-cdn.xitu.io/2019/1/25/168842e80e058bc9?w=776&h=1538&f=png&s=265695" style="wdith: 200px">
</figure>

二者视觉效果相似, 可是 paintImage 的性能问题, 严重消耗了GPU资源. 查看了paintImage的源码, 发现这个函数实现的方式也是调用了 drawImageRect, 这个问题.有兴趣的同学可以深入了解一下. 共同探讨一下, 也行对于Flutter性能优化有很大的帮助.

void paintImage(
  ...
  if (centerSlice == null) {
    for (Rect tileRect in _generateImageTileRects(rect, destinationRect, repeat))
      canvas.drawImageRect(image, sourceRect, tileRect, paint);
  } else {
    for (Rect tileRect in _generateImageTileRects(rect, destinationRect, repeat))
      canvas.drawImageNine(image, centerSlice, tileRect, paint);
  }
  if (needSave)
    canvas.restore();
}

开始

本篇我们的主要任务是, 在画板上增加我们控制的飞机, 可以操作飞机移动.

绘制飞机

考虑到我们未来要绘制玩家的战机. 还要绘制敌机. 我们先抽象出一个 Plan 的类, 方便以后我们的开发.我们在 src 下, 新建一个叫 plan.dart的文件. 定义他的方法.

abstract class Plan {
  void init() {}
  void moveTo(double x, double y) {}
  void destroy() {}
  void paint(Canvas canvas, Size size) async {}
}

接下来我们就可以定义的的 MainHero我们的主角了. 我们的src下新建一个 hero.dart, 引用并继承 Plan, 并实现在上边定义的方法. 关于基本方法与属性如下:


enum PlanStatus {stay, move, die}

class MainHero extends Plan {
  // 飞机的中心坐标x
  double x = 100.0;
  // 飞机的中心坐标y
  double y = 100.0;
  // 战机宽度
  double width = 132.0;
  // 战机高度
  double height = 160.0;

  ui.Image image;

  @override
  void init() async {
    // TODO: implement init
    image = await Utils.getImage('assets/images/hero.png');
  }
  @override
  void moveTo(double x, double y) {
    // TODO: implement moveTo
  }
  @override
  void destroy() {
    // TODO: implement destroy
    super.destroy();
  }
 
 
  @override
  void paint(Canvas canvas, Size size) {
    Rect paintArea = Offset(100, 100) & Size(width, height);
    Rect planArea = Offset(0, 0) & Size(image.width, image.height)
    canvas.save();
    // 将画布向左上方偏移, 把绘图点, 迁移到飞机正中心
    canvas.translate( -width / 2, -height / 2);
    canvas.drawImageRect(image, planArea, paintArea, new Paint());
    frameIndex++;
    canvas.restore();
  }
}

在本次我们的绘图接口用的是 drawImageRect, 使用方法参考文档, 我们在游戏的 Enter入口文件中, 新建一个主角的实例, 完成初始化, 与绘图的逻辑, 具体细节与背景图类似, 我们就不细说了.

废话不多说, 直接上效果图

<img src="https://user-gold-cdn.xitu.io/2019/1/26/1688948e368cd8dc?w=388&h=769&f=png&s=13316" style="width: 200px" />

飞机的动效

在我们玩过的飞机类游戏里边. 我们控制的飞机通常都会有一个动态效果, 这个动态的效果会增强玩家的视觉体验, 笔者从网上找到了一份游戏飞机的动效如下:

image

这个飞机动效是一个 gif 类型的文件循环播放, 给人以动态的感觉. 我查阅了 flutter 貌似没有直接绘制gif的接口. 所以我们只能用绘制静态图的方式去想办法让飞机动起来, 做过h5的同学可能比较了解, 在早期html界面中的动画是由多帧拼接成一个胶片, 循环播放, 造成一种视觉停留的动画效果. 这里我们依然采用这种方式去实现本次的动态效果. 我们通过ps, 把每一帧拼接做成一个有2帧的132*80长帧图;
<Center>
<img src="https://user-gold-cdn.xitu.io/2019/1/26/1688938978fcd3f6?w=712&h=456&f=png&s=55383" style="width: 200px">
</Center>

接下来, 我们就要盘这张图,对我们的 MainHero进行改造, 把他动态显示在我们的屏幕上. 我们给它增加二个属性和一个方法, 每一次屏幕刷新, 我们都把 frameIndex 进行加1的操作, 当达到最后一帧, 将 frameIndex重置为0, 这样我们的飞机就可以动起来了

// 总帧数
int frameNumber = 2;
// 当前帧数
int frameIndex = 0;

// 动态获取飞机的长帧图的绘制区域
Rect getPlanAreaSize(int _frameIndex) {

double perFrameWidth = image.width / frameNumber;
double offsetX = perFrameWidth * _frameIndex;
double offsetY = 0;
if (offsetX >= image.width) {
  frameIndex = 0;
  return this.getPlanAreaSize(0);
}
return Offset(offsetX, offsetY) & Size(66.0, 80.0);
}

效果图如下:


image

飞机的控制

关于控制飞机飞行的思路是, 我们通过监听屏幕, 手指的运动, 动态的更新飞机绘制 (x,y) 的坐标点, 从而达到我们想要的效果.

Flutter的文档中, 我们找到了 GestureDetector 接口, 在 Enter 入口中 我们用GestureDetector控件包围住我们的CustomPaint画板 控件。我们接下来的工作就是,使用 GestureDetector 控件来捕获用户的拖动事件。并更新我们 MainHero 的坐标点.

实现方式如下:

 Widget build(BuildContext context) build () {
    ...
    return GestureDetector(
      child: CustomPaint(
          painter: MainPainter(background: background, hero: hero)
      ),
      onPanStart: (DragDownDetails) {
        hero.moveTo(DragDownDetails.globalPosition.dx, DragDownDetails.globalPosition.dy);
      },
      onPanUpdate: (DragDownDetails) {
     
        hero.moveTo(DragDownDetails.globalPosition.dx, DragDownDetails.globalPosition.dy);
      }
    )
}

接下来我们来改造我们的 MainHero 类, 完善他的 moveTo 方法. 在游戏过程中, 我们手指拖动, 飞机不可能以闪现的方式进行闪动, 它需要一点点移动到我们的想要的位置. 我们在 MainHero中定义几个属性与方法

// 飞行目标点坐标
double _x;
double _y;
double speed = 20;
// 动态计算新的坐标点
void calculatePosition() {}

我们在这里用一张图, 去展示新旧坐标点之前的关系:

image

通过以上这张图, 我们要以明白在飞机在x与y轴上, 速度的矢量关系与运算方法, 我们完善我们的 calculatePosition

 void moveTo(double x, double y) {
    // TODO: implement moveTo
    this._x = x;
    this._y = y;
 }
void calculatePosition() {
    Point  p1 = Point(x, y);
    Point  p2 = Point(_x, _y);
    double distance = p1.distanceTo(p2);
    double flyRadian = acos(((y - _y) / distance).abs());
    // 判断位移方向
    if (_x < x) {
      x -= speed * sin(flyRadian);
    } else {
      x += speed * sin(flyRadian);
    }
    if (_y < y) {
      y -= speed * cos(flyRadian);
    } else {
      y += speed * cos(flyRadian);
    }
  }

通过以上改造, 我们进行测试发现, 在运动到终点时,飞机会在终点发生抖动, 排查问题发现, 是我们的calculatePosition方法, 在计算x值的时候, 会在最后一次计算中, 产生一个 |x - _x| > 0的结果, 所以飞机会在坐标点来回的跳动. 为了避免这种情况, 我们再次改造 calculatePosition 方法

image

我们为 MainHero 增加一个飞机的飞行状态, 当飞机与目标点及其接近时, 直接手动覆盖(x, y), 并将飞机的状态设为 stay.

// stay 无人控制, 自由飞行
// move 有人控制, 飞行运动状态
// die  死了
enum PlanStatus {stay, move, die}

void calculatePosition() {
    ...
    // 避免抖动, 做一个判断. 距离
    if (distance < 10) {
      x = _x;
      y = _y;
      status = PlanStatus.stay;
      return null;
    }
}
// 同时为了更好的优化我们的Pain方法函数, 我们为其增加一个逻辑的判断
void paint(Canvas canvas, Size size) {
    ...
    if (status == PlanStatus.move) {
      calculatePosition();
    }
}

通过以上改造, 我们看一下最终的效果.


image

总结

第二部份, 大工告成, 内容可能会有错别字, 请大家指出, 我将进行改正, 剩下的逻辑. 我会一点点补上, 如果觉得本篇内容对您有帮助, 期待您的赞~ git传送门

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,265评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,078评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,852评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,408评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,445评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,772评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,921评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,688评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,130评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,467评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,617评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,276评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,882评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,740评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,967评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,315评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,486评论 2 348

推荐阅读更多精彩内容

  • 说明 小编也是初学者,为了了解flutter动画的使用与效果, 决定亲自定手用flutte写一款小游戏出来. 并...
    ontow阅读 5,013评论 0 8
  • canvas元素的基础知识 在页面上放置一个canvas元素,就相当于在页面上放置了一块画布,可以在其中进行图形的...
    oWSQo阅读 10,274评论 0 19
  • 1 CALayer IOS SDK详解之CALayer(一) http://doc.okbase.net/Hell...
    Kevin_Junbaozi阅读 5,133评论 3 23
  • 前言 什么值得买 是一家网购推荐网站,主要推荐「高性价比」产品,范围包括了国内及国外的多个电商网站。 在网页版的基...
    Ziya阅读 2,429评论 2 15
  • 今天去了谷德佳乐家,发现了一些端倪 首先,超市入口几个大字非常明显,超市标语也非常明朗 其次,几乎所有的价格都在同...
    峰之与你若只如初见阅读 207评论 0 0