回顾
昨天下午笔者已经完成了背景动画的循环播放. 晚上笔者就开发中发现的问题在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" />
飞机的动效
在我们玩过的飞机类游戏里边. 我们控制的飞机通常都会有一个动态效果, 这个动态的效果会增强玩家的视觉体验, 笔者从网上找到了一份游戏飞机的动效如下:
这个飞机动效是一个 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);
}
效果图如下:
飞机的控制
关于控制飞机飞行的思路是, 我们通过监听屏幕, 手指的运动, 动态的更新飞机绘制 (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() {}
我们在这里用一张图, 去展示新旧坐标点之前的关系:
通过以上这张图, 我们要以明白在飞机在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 方法
我们为 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();
}
}
通过以上改造, 我们看一下最终的效果.
总结
第二部份, 大工告成, 内容可能会有错别字, 请大家指出, 我将进行改正, 剩下的逻辑. 我会一点点补上, 如果觉得本篇内容对您有帮助, 期待您的赞~ git传送门