目录
前言
上一篇介绍了手势在画布上的应用,那么手势与绘制画布究竟能摩擦出怎样的火花呢,本篇文章将为你详解手游中操纵杆移动角色的的原理与实现过程。
基本思路
确定操纵杆区域,确定点击时手势响应区域,当手指滑动操纵杆时,计算出当前的手指位置与当前操纵杆圆心偏移弧度,从而确定当前角色的移动方向。接下来就一步一步实现吧。
绘制
绘制操纵杆的静态图形,玩过手游应该知道操纵杆基本构成由底部圆和手指移动圆球组成,手指移动的圆球围绕底圆进行360°旋转从而控制角色朝不同方向移动。
静态效果
操纵杆的核心是由两个圆形组成,代码也非常简单。
绘制代码:
// 底圆 canvas.drawCircle( Offset(0,0), bgR, _paint ..style = PaintingStyle.fill ..color = Colors.blue.withOpacity(0.2)); _paint.color = color; _paint.style = PaintingStyle.stroke; /// 手势小圆 canvas.drawCircle( Offset(0,0), bgR / 3, _paint ..style = PaintingStyle.fill ..color = Colors.blue.withOpacity(0.9));
添加手势交互 GestureDetector
大概思路:
当点击可触控区域,将操纵杆移动到当前手指按下的位置,移动手指,根据手指位置坐标和按下时圆心位置坐标计算偏移角度得出手指相对于底圆的坐标点,松开手指,操纵杆进行复位回到初始位置。
手势组件
return GestureDetector( child: CustomPaint( size: size, painter: JoyStickPainter( offset: _offset, offsetCenter: _offsetCenter, listenable: Listenable.merge([_offset, _offsetCenter])), ), // 按下 onPanDown: down, // 移动 onPanUpdate: update, // 抬起 onPanEnd: reset, );
备注:上篇文章介绍了,手指触控屏幕的坐标点永远都是以左上角为原点的,为了方便理解和计算,我们同样也需要将手指的坐标的原点进行偏移到画布中央和画布保持一致,所以这里我们通过手势获取的坐标点之后需要进行偏移。
不管手指点击、移动、还是抬起都要通知画布进行更新,这里使用ValueNotifier<Offset>
通知坐标点更新。
ValueNotifier<Offset> _offset = ValueNotifier(Offset.zero);
点击交互 down: 当用户点击可触控区域,将大圆和小圆移动至手指点击的位置。
因为底圆在点击之后抬起之前都是处于静止状态,当移动手指只有小圆移动,所以这里用两个坐标来保存底圆的圆心,和小圆的圆心,当点击时,底圆和小圆的中心是一致的,所以这里当点击时同时更新两个圆心位置。
down(DragDownDetails details) { Offset offset = details.localPosition; _offsetCenter.value = offset.translate(-size.width / 2, -size.height / 2); _offset.value = offset.translate(-size.width / 2, -size.height / 2); }
这里需要注意的是,当我们的手指点击在可触控区域边界距离小于底圆半径时,需要控制圆心位置的x轴和y轴距离可触控区域边界距离大于等于底圆半径。
如果不控制边界点击时,操纵杆会偏离出触控区域
所以这里最好在点击时可以加一个边界处理,上下左右加一个边界控制。
if (offset.dx > size.width - bgR) { offset = Offset(size.width - bgR, offset.dy); } if (offset.dx < bgR) { offset = Offset(bgR, offset.dy); } if (offset.dy > size.height - bgR) { offset = Offset(offset.dx, size.height - bgR); } if (offset.dy < bgR) { offset = Offset(offset.dx, bgR); }
之后再点击边界时就不会出界了。
移动交互 update: 当用户移动手指时,小圆根据手指在底圆内部进行移动。
手指移动是操纵杆的核心交互逻辑。
思路: 当手指点击之后移动离开圆心,计算当前坐标点以当前底圆圆心为原点的偏移弧度,通过反正切函数atan2(y,x)
可以得出当前坐标针对x轴向右为正,y轴向下为正的偏移弧度α
,默认范围 [-pi]-[pi]
, 为了方便理解计算,这里我们将得到的角度+pi
转换为 0-2pi,角度范围:0-360°
。见下图:
Offset
类里的direction(y,x)
方法就是通过atan2
方法计算当前坐标的偏移弧度。
/// The angle of this offset as radians clockwise from the positive x-axis, in /// the range -[pi] to [pi], assuming positive values of the x-axis go to the /// right and positive values of the y-axis go down. double get direction => math.atan2(dy, dx);
角色移动的关键就是通过得出的偏移弧度来进行不同方向的移动。
核心代码:
/// 手指移动坐标 var offsetTranslate = offset.value; /// 操纵杆圆心坐标 var offsetTranslateCenter = offsetCenter.value; /// 计算当前位置坐标点 左半区域 X为负数 double x = offsetTranslateCenter.dx - offsetTranslate.dx; /// y轴 下半区域 Y为负数 double y = offsetTranslateCenter.dy - offsetTranslate.dy; /// 反正切函数 通过此函数可以计算出此坐标旋转的弧度 为正 代表X轴逆时针旋转的角度 为负 顺时针旋转角度 /// 范围 [-pi] - [pi] double ata = atan2(y, x); /// 默认坐标系范围为-pi - pi 顺时针旋转坐标系180度 变为 0 - 2*pi; var thta = ata + pi; print("angle ${(180 / pi * thta).toInt()}");
这里手指移动分为2种情况,手指在底圆内部和手指在底圆外部。见下图:
当手指在底圆内部,我们可以直接使用当前手指传递的坐标计算。
当手指移动到底圆外部,我们需要控制小圆的圆形坐标不能跑到底圆的外部,控制小圆 不能超过底圆的的范围,所以,这里需要进行计算当前手指的坐标距离底圆圆心的距离有没有超过底圆半径,如果超出,需要计算小圆的临界坐标值。
有了偏移弧度α
,我们就可以通过三角函数计算出上面x1,y1的坐标点
,也就是当前手指控制小圆圆心的临界坐标。
核心代码:
/// 当前手指坐标距离底圆圆心长度 var r = sqrt(pow(x, 2) + pow(y, 2)); if (r > bgR) { var dx = bgR * cos(thta) + offsetTranslateCenter.dx; // x轴坐标点 var dy = bgR * sin(thta) + offsetTranslateCenter.dy; // y轴坐标点 offsetTranslate = Offset(dx, dy); }
松开交互 reset: 当用户点击可触控区域,将大圆和小圆移动至手指点击的位置。
将两个圆的圆心回归坐标系原点。
reset(DragEndDetails details) { _offset.value = Offset.zero; _offsetCenter.value = Offset.zero; }
注意的是,当点击和松开时,当前角色都是不动的,只有当移动时才传递角度值赋给角色进行移动,所以当这里需要判断当前手指触控点和底圆圆心是否重合,如果重合表示当前角色处于静止状态。因为默认不作处理,弧度获取的是pi
,所以这里需要特殊处理一下。 这里我们需要将获取的弧度值传递出去,如果当前处于静止状态,将弧度设为负数,因为我们的弧度范围是0-2pi
,移动状态中不可能为负。
if (x == 0 && y == 0) { onAngle?.call(-1); } else { onAngle?.call(thta); }
为了方便展示效果,我加了坐标轴辅助,这样看起来更直观一些。
最终效果:
通过当前获取的弧度值即可传递给角色进行移动。
总结
本篇文章主要介绍了操纵杆如何向角色传递有效信息从而控制角色移动,其实操纵杆的实现逻辑并不复杂,主要难点集中在手指移动计算偏移弧度哪里,还有就是小圆球的边界处理,掌握了这两点,也就掌握了核心逻辑。