回顾
昨天下午笔者已经完成了背景动画的循环播放. 晚上笔者就开发中发现的问题在stackoverflow上进行提问. 问题大概内容:
如何在Canvas中, 将一个较小的图片, 拉伸平铺
这个问题, 收到了二个有效的回答
- Canvas.drawImageRect()
- paintImage()
进过笔者测试
二者视觉效果相似, 可是 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入口文件中, 新建一个主角的实例, 完成初始化, 与绘图的逻辑, 具体细节与背景图类似, 我们就不细说了.
废话不多说, 直接上效果图
飞机的动效
在我们玩过的飞机类游戏里边. 我们控制的飞机通常都会有一个动态效果, 这个动态的效果会增强玩家的视觉体验, 笔者从网上找到了一份游戏飞机的动效如下:
接下来, 我们就要盘这张图,对我们的 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);}复制代码
效果图如下:
飞机的控制
关于控制飞机飞行的思路是, 我们通过监听屏幕, 手指的运动, 动态的更新飞机绘制 (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() {}复制代码
我们在这里用一张图, 去展示新旧坐标点之前的关系:
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 方法
// 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(); }}复制代码
通过以上改造, 我们看一下最终的效果.
总结
第二部份, 大工告成, 内容可能会有错别字, 请大家指出, 我将进行改正, 剩下的逻辑. 我会一点点补上, 如果觉得本篇内容对您有帮助, 期待您的赞~