因为时间多哈,特意花时间看看游戏是怎么做的,毕竟也是个未涉足的领域,很多地方都要好好学习。那么现在手游肯定会用到游戏方向操作盘,这篇文章就单独讲讲如何设计好一个游戏转盘。这里我将用swift语言进行开发,毕竟只有学习才能抚慰我那颗脆弱的心灵呐
言归正传了:而且操作盘的代码将全部在这篇文章贴出来
先来几张截图
首先,我们来分析一下思路:
1、我们的操作盘用来做什么,最终目的只有一个:将当前方向值告诉外界,外界只要成为代理即可获取方向值。
2、不管是点按还是长按,都会触发,而且还得考虑到手势的位置,假设操作手柄圆圈移动的最大距离为100,那么移动的速度是否根距离有关呢,再者,当手势点在中心多少范围内算不算不移动呢,当移动点距离中心大于等于100的时候,速度达到最快。
3、方向值的确认:这个得好好温习下数学的三角函数的知识了,
//atan 和 atan2 都是求反正切函数,如:有两个点 point(x1,y1), 和 point(x2,y2);
//那么这两个点形成的斜率的角度计算方法分别是:
//float angle = atan( (y2-y1)/(x2-x1) );
//或
//float angle = atan2( y2-y1, x2-x1 );
let angle = atan2(position.y-midY, position.x-midX)
在拖动的时候和touchBegin的时候更新方向值
4、如果比较省事又方便的将方向值进行回传呢: 这里我一开始也走了挺多弯路,因为在拖动手势和touchBegin在统计长按上代码上比较多且又麻烦。最终使用CADisplayLink
定时器,在定时器方法中,只要触发了方向盘就一直回调即可,
操作盘的属性
///内容显示
var contentView:UIView?
///内部小圈圈
var circelView:MyTankMidCircleView?
///最外层的方向箭头
var directionImageView:UIImageView?
///是否开始触发
var isBeginMove:Bool = false
///是否在中间
var isStandInMiddle:Bool = false
///移动的比例,0-1,越高则移动的越快
var moveRatio:CGFloat = 0
///方向值
var direction:CGFloat = 0
///定时器,用来监听执行isBeginMove
var displayLinkTimer:CADisplayLink? = nil
///代理,将方向值回调出去
weak var delegate : MyTankControlHandlerDelegate?
contentView
是操作盘显示的外圆内容view,其实主要用意就是想让操作盘多一些元素,不至于空荡荡的,
circelView
:内部小圆圈,类似于王者荣耀中的小圆圈,其实功能上没有影响,就是多一点元素看起来效果美观一些
isStandInMiddle
:当前用户的手势是否在操作盘的中心点多少距离内,假设在中心的10个点范围内都算作不动的话,那么这个isStandInMiddle就为true
moveRatio
表示移动的快慢,当在中心一定范围内是为0,当手势拖动在原因最外围则为1,值在0~1范围内
displayLinkTimer
:用定时器来监听并回调用户手势移动的操作,个人感觉这样非常方便哈,只有一发生手势变化就会将值通过代理回传。
😄😄😂😂
代码部分 讲解:
中间的方向球:
1、首先是弄了4个箭头,主要美观,没其他卵用
2、做了一个破浪动画,用意在手势为中间的时候展示一下,一个效果而已哈
class MyTankMidCircleView: UIView {
var shapeLayer:CAShapeLayer!
///是否展示波纹效果
var showRipple:Bool = false{
didSet{
self.waveAniamation(complete: showRipple)
}
}
override init(frame: CGRect) {
super.init(frame: frame)
let shapeLayer = CAShapeLayer()
self.shapeLayer = shapeLayer
shapeLayer.frame = self.bounds
shapeLayer.lineWidth = frame.size.width*0.5
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor.red.cgColor
let path = UIBezierPath.init(arcCenter: self.center, radius: frame.size.width*0.5-frame.size.width*0.25, startAngle: CGFloat(-Double.pi/2), endAngle: CGFloat(-Double.pi/2)+CGFloat(Double.pi)*2, clockwise: true)
shapeLayer.path = path.cgPath
self.layer.addSublayer(shapeLayer)
let gradientLayer = CAGradientLayer()
gradientLayer.frame = self.bounds
self.layer.addSublayer(gradientLayer)
let color0 = UIColor.init(red: 40.0/255.0, green: 123.0/255.0, blue: 1.0, alpha: 1.0).cgColor
let color1 = UIColor.green.cgColor
let color2 = UIColor.init(red: 0, green: 1, blue: 0, alpha: 0.7).cgColor
gradientLayer.colors = [color0,color1,color0,color2]
gradientLayer.locations = [0,0.25,0.5,0.75]
gradientLayer.mask = shapeLayer
//添加四个小箭头
let arrowRight = UIImageView(image: UIImage(named: "mytan_handler_arrow"))
self.addSubview(arrowRight)
arrowRight.ym_autoPinEdgeToSuperviewEdge(edge: .ALEdgeRight, inset: 0)
arrowRight.ym_autoAlignAxisToSuperviewAxis(axis: .ALAxisHorizontal)
let arrowBottom = UIImageView(image: UIImage(named: "mytan_handler_arrow"))
self.addSubview(arrowBottom)
arrowBottom.transform = CGAffineTransform.init(rotationAngle: CGFloat(Double.pi/2))
arrowBottom.ym_autoAlignAxisToSuperviewAxis(axis: .ALAxisVertical)
arrowBottom.ym_autoPinEdgeToSuperviewEdge(edge: .ALEdgeBottom, inset: 0)
let arrowLeft = UIImageView(image: UIImage(named: "mytan_handler_arrow"))
self.addSubview(arrowLeft)
arrowLeft.transform = CGAffineTransform.init(rotationAngle: CGFloat(Double.pi))
arrowLeft.ym_autoAlignAxisToSuperviewAxis(axis: .ALAxisHorizontal)
arrowLeft.ym_autoPinEdgeToSuperviewEdge(edge: .ALEdgeLeft, inset: 0)
let arrowTop = UIImageView(image: UIImage(named: "mytan_handler_arrow"))
self.addSubview(arrowTop)
arrowTop.transform = CGAffineTransform.init(rotationAngle: -CGFloat(Double.pi/2))
arrowTop.ym_autoAlignAxisToSuperviewAxis(axis: .ALAxisVertical)
arrowTop.ym_autoPinEdgeToSuperviewEdge(edge: .ALEdgeTop, inset: 0)
}
///波纹动画制作
func waveAniamation(complete:Bool) {
if(complete == false){
self.layer.removeAllAnimations()
}else{
let animation = self.layer.animation(forKey: "wave")
if(animation != nil){//如果当前动画在执行,则return
return
}
let curve = CAMediaTimingFunction.init(name: CAMediaTimingFunctionName.default)
let duration : CFTimeInterval = 1.0
let group = CAAnimationGroup()
group.fillMode = .backwards
group.duration = duration
group.repeatCount = HUGE
group.timingFunction = curve
let scale = CABasicAnimation()
scale.keyPath = "transform.scale"
scale.fromValue = 1.0
scale.toValue = 1.5
let opacity = CAKeyframeAnimation()
opacity.keyPath = "opacity"
opacity.values = [1.0,0.8,0.4]
opacity.keyTimes = [0,0.5,1]
group.animations = [scale,opacity]
self.layer.add(group, forKey: "wave")
}
}
}
自定义的中间四个大箭头
本来想找图片的,没找到,就索性自己画了,你们不喜欢可以直接忽略哈
class MyTankHandlerArrow: UIView {
///方向 默认向左 1:向上 2:向右 3:向下
var direction:NSInteger = 0
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.clear
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
let context = UIGraphicsGetCurrentContext()
context?.setLineCap(CGLineCap.round)
context?.setLineJoin(CGLineJoin.round)
context?.setLineWidth(4)
context?.setStrokeColor(red: 49.0/255, green: 148.0/255.0, blue: 219.0/255.0, alpha: 0.6)
context?.beginPath()
if self.direction == 0 {
context?.move(to: CGPoint(x: rect.size.width*0.5, y: 0))
context?.addLine(to: CGPoint(x: 0, y: rect.size.height*0.5))
context?.addLine(to: CGPoint(x: rect.size.width*0.5, y: rect.size.height))
}else if self.direction == 1 {
context?.move(to: CGPoint(x: 0, y: rect.size.height*0.5))
context?.addLine(to: CGPoint(x: rect.size.width*0.5, y: 0))
context?.addLine(to: CGPoint(x: rect.size.width, y: rect.size.height*0.5))
}else if self.direction == 2 {
context?.move(to: CGPoint(x: rect.size.width*0.5, y: 0))
context?.addLine(to: CGPoint(x: rect.size.width, y: rect.size.height*0.5))
context?.addLine(to: CGPoint(x: rect.size.width*0.5, y: rect.size.height))
}else {
context?.move(to: CGPoint(x: 0, y: rect.size.height*0.5))
context?.addLine(to: CGPoint(x: rect.size.width*0.5, y: rect.size.height))
context?.addLine(to: CGPoint(x: rect.size.width, y: rect.size.height*0.5))
}
context?.strokePath()
}
}
操作盘内部代码:
1、初始化代码就直接贴了:
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.clear
let contentView = UIView.init(frame: CGRect(x: 0, y: 0, width: ContentWidth, height: ContentHeight))
self.contentView = contentView
contentView.layer.cornerRadius = ContentWidth*0.5
contentView.layer.masksToBounds = true
contentView.center = self.center
contentView.backgroundColor = UIColor.init(red: 49.0/255, green: 148.0/255.0, blue: 219.0/255.0, alpha: 0.1)
self.addSubview(contentView)
let circelView = MyTankMidCircleView.init(frame: CGRect(x: 0, y: 0, width: 60, height: 60))
circelView.center = CGPoint(x: ContentWidth*0.5, y: ContentHeight*0.5)
contentView.addSubview(circelView)
self.circelView = circelView
//添加最边上的四个方向箭头
let arrowLeft = MyTankHandlerArrow.init(frame: CGRect(x: 0, y: 0, width: 30, height: 30))
arrowLeft.center = CGPoint(x: 15+2, y: ContentHeight*0.5)
arrowLeft.direction = 0
contentView.addSubview(arrowLeft)
let arrowTop = MyTankHandlerArrow.init(frame: CGRect(x: 0, y: 0, width: 30, height: 30))
arrowTop.center = CGPoint(x: ContentWidth*0.5, y: 15+2)
arrowTop.direction = 1
contentView.addSubview(arrowTop)
let arrowRight = MyTankHandlerArrow.init(frame: CGRect(x: 0, y: 0, width: 30, height: 30))
arrowRight.center = CGPoint(x: ContentWidth-15-2, y: ContentHeight*0.5)
arrowRight.direction = 2
contentView.addSubview(arrowRight)
let arrowBottom = MyTankHandlerArrow.init(frame: CGRect(x: 0, y: 0, width: 30, height: 30))
arrowBottom.center = CGPoint(x: ContentWidth*0.5, y: ContentHeight-15-2)
arrowBottom.direction = 3
contentView.addSubview(arrowBottom)
let panGester = UIPanGestureRecognizer.init(target: self, action: #selector(panHandlerAction))
contentView.addGestureRecognizer(panGester)
//底部放一个箭头指明方向
//mytank_direction
let directionImageView = UIImageView.init(image: UIImage(named: "mytank_direction"))
self.directionImageView = directionImageView
directionImageView.layer.anchorPoint = CGPoint(x: 0, y: 0.5)
directionImageView.frame = CGRect(x: self.bounds.size.width*0.5, y: self.bounds.size.height*0.5 - 52*0.5, width: self.bounds.size.width*0.5, height: 52)
directionImageView.isHidden = true
self.insertSubview(directionImageView, belowSubview: contentView)
displayLinkTimer = CADisplayLink(target: self, selector: #selector(moveUpdateAction))
//displayLinkTimer?.preferredFramesPerSecond = 2 //每5帧处理一次 大概 一秒60/5次
displayLinkTimer?.add(to: RunLoop.current, forMode: .common)
}
定时器监听
可以设置监听频率,但是建议还是用默认的吧
@objc func moveUpdateAction () {
///触发了方向盘
if (self.isBeginMove == false){
return
}
///是否当前在中间范围内,则考虑用户当前是否不动
if self.isStandInMiddle == true {
return
}
if self.delegate != nil {
self.delegate?.controlHandlerMove(handler: self, directionValue: self.direction,moveRatio:self.moveRatio)
}
}
手势结束后
结束后做了一个Spring动画
func circleMoveToCenter () {
UIView.animate(withDuration: 0.2, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.2, options: UIView.AnimationOptions.curveEaseInOut, animations: {
self.circelView?.center = CGPoint(x: ContentWidth*0.5, y: ContentHeight*0.5)
self.directionImageView?.isHidden = true
self.circelView?.showRipple = false
}) { (finish:Bool) in
self.isUserInteractionEnabled = false
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()+0.2, execute: {
self.isUserInteractionEnabled = true
})
}
}
拖动手势的回调:
@objc func panHandlerAction (gester:UIPanGestureRecognizer) {
//当前拖动的位置
let position = gester.location(in: self.contentView)
let midX = ContentWidth*0.5
let midY = ContentHeight*0.5
//atan 和 atan2 都是求反正切函数,如:有两个点 point(x1,y1), 和 point(x2,y2);
//那么这两个点形成的斜率的角度计算方法分别是:
//float angle = atan( (y2-y1)/(x2-x1) );
//或
//float angle = atan2( y2-y1, x2-x1 );
let angle = atan2(position.y-midY, position.x-midX)
self.direction = angle
let resultPoint = self.isCircleIn(center: CGPoint(x: ContentWidth*0.5, y: ContentHeight*0.5), rect: (self.contentView?.bounds)!, point: position)
self.circelView?.center = resultPoint
//如果当前位置在中间的一定范围内,这就当作是在原地不动
//以原点为中心,半径为10则就算是不动
let isInMiddle = self.isStandInMidCircle(center: CGPoint(x: ContentWidth*0.5, y: ContentHeight*0.5), point: position, maxDistance: 10.0)
if(isInMiddle == true){
self.directionImageView?.isHidden = true
self.circelView?.showRipple = true
self.isStandInMiddle = true
}else{
self.circelView?.showRipple = false
self.directionImageView?.isHidden = false
self.directionImageView?.layer.transform = CATransform3DMakeRotation(angle, 0, 0, 1)
self.isStandInMiddle = false
}
let state = gester.state
if state == UIGestureRecognizer.State.ended {
if(self.isBeginMove == false){
return
}
self.isBeginMove = false
self.circleMoveToCenter()
}else if state == UIGestureRecognizer.State.began {
self.isBeginMove = true
}
}
注意,当用户出现点一下就按住不动的时候,就会执行touchesBegin方法,所以手势和touches都是实现
touchBegin和touchesEnded回调
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.isBeginMove = true
let sets = touches as NSSet
let touch = sets.anyObject() as! UITouch
let position = touch.location(in: self)
let midX = ContentWidth*0.5
let midY = ContentHeight*0.5
let angle = atan2(position.y-midY, position.x-midX)
self.direction = angle
let resultPosition = self.isCircleIn(center: CGPoint(x: ContentWidth*0.5, y: ContentHeight*0.5), rect: (self.contentView?.bounds)!, point: position)
let isInMiddle = self.isStandInMidCircle(center: CGPoint(x: ContentWidth*0.5, y: ContentHeight*0.5), point: position, maxDistance: 10.0)
if(isInMiddle == true){
self.directionImageView?.isHidden = true
self.circelView?.showRipple = true
self.isStandInMiddle = true
}else{
self.circelView?.showRipple = false
self.directionImageView?.isHidden = false
self.directionImageView?.layer.transform = CATransform3DMakeRotation(angle, 0, 0, 1)
self.isStandInMiddle = false
}
UIView.animate(withDuration: 0.15) {
self.circelView?.center = resultPosition
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if(self.isBeginMove == false){
return
}
self.isBeginMove = false
self.circleMoveToCenter()
}
判断是否在原点多少范围内
func isStandInMidCircle(center:CGPoint,point:CGPoint,maxDistance:CGFloat) ->Bool {
var distance:Float = 0;
if(point.x == center.x && point.y == center.y){
distance = 0
}else if(point.y == center.y){
distance = fabsf(Float(center.x - point.x))
}else if(point.x == center.x){
distance = fabsf(Float(center.y - point.y))
}else{
let xValue = fabsf(Float(center.x - point.x))
let yValue = fabsf(Float(center.y - point.y))
distance = hypotf(xValue, yValue)
}
return distance < Float(maxDistance)
}
判断当前的位置是否超出一定范围,并将最终可到的位置回传
其实这段代码主要是为了中间圆圈服务的,为了就是让圆圈在固定范围内平滑移动
func isCircleIn(center:CGPoint,rect:CGRect,point:CGPoint) ->CGPoint {
//就是要算出点到圆心的距离是否大于半径
var distance:Float = 0;
var resultPoint:CGPoint = point
var moveRatio :Float = 0
//大圆半径
let radius = Float(rect.size.width*0.5)
//小圆半径
let circleRadicu = Float((self.circelView?.bounds.size.width)!*0.5)
//半径减掉内圆的半径才是最长的移动距离
let calculateRadius = radius - circleRadicu
if(point.x == center.x && point.y == center.y){
distance = 0
}else if(point.y == center.y){
distance = fabsf(Float(center.x - point.x))
if(distance >= calculateRadius) {
if(center.x > point.x){
resultPoint = CGPoint(x: CGFloat(circleRadicu), y: resultPoint.y)
}else{
resultPoint = CGPoint(x: rect.size.width-CGFloat(circleRadicu), y: resultPoint.y)
}
moveRatio = 1
}else{
moveRatio = distance / calculateRadius
}
}else if(point.x == center.x){
distance = fabsf(Float(center.y - point.y))
if(distance >= calculateRadius){
if(center.y > point.y){
resultPoint = CGPoint(x: resultPoint.x, y: CGFloat(circleRadicu))
}else{
resultPoint = CGPoint(x: resultPoint.x, y: rect.size.height-CGFloat(circleRadicu))
}
moveRatio = 1
}else{
moveRatio = distance / calculateRadius
}
}else{
let xValue = fabsf(Float(center.x - point.x))
let yValue = fabsf(Float(center.y - point.y))
distance = hypotf(xValue, yValue)
if(distance >= calculateRadius){
moveRatio = 1
let angle = atan2(point.y-center.y, point.x-center.x)
let sinYValue = sin(angle)*CGFloat(calculateRadius)//正弦,就是y值方向
let cosXValue = cos(angle)*CGFloat(calculateRadius)//余弦,就是x值方式
//print("angle:\(angle) , 垂直:\(yValue),水平:\(xValue)")
//当在右上角方向时,xValue为正 yValue为负
if(cosXValue > 0 && sinYValue < 0){
let fabsY = CGFloat(fabsf(Float(sinYValue)))
resultPoint = CGPoint(x: cosXValue + CGFloat(radius), y: CGFloat(radius) - fabsY)
}else if(xValue > 0 && yValue > 0){//当在右下角方向时,xValue 和 yValue 都为正
resultPoint = CGPoint(x: cosXValue + CGFloat(radius), y: sinYValue + CGFloat(radius))
}else if(cosXValue < 0 && sinYValue > 0){//当在左下角方向时,xValue为负 yValue为正
let fabsX = CGFloat(fabsf(Float(cosXValue)))
resultPoint = CGPoint(x: CGFloat(radius) - fabsX, y: sinYValue + CGFloat(radius))
}else if(cosXValue < 0 && sinYValue < 0){//当在左上角方向时,xValue 和 yValue 都为负
let fabsY = CGFloat(fabsf(Float(sinYValue)))
let fabsX = CGFloat(fabsf(Float(cosXValue)))
resultPoint = CGPoint(x: CGFloat(radius) - fabsX, y: CGFloat(radius) - fabsY)
}
}else{
moveRatio = distance / calculateRadius
}
}
self.moveRatio = CGFloat(moveRatio)
return resultPoint
}