游戏的背景:
前言:这个游戏是2015年出来的,刚开始当时aa(见缝插针)游戏很火,我也是玩了好大半天,深深地求虐感屡试不爽。那个时候我还是一个iOS小兵一枚,当时就感觉这游戏如此简单到炸裂,于是就想着尝试实现它,但是...(此处省略100字)。
时间过得真快,过去了两年,偶然翻看博客,发现有人也有相似的实现和想法,并且提供了很好的思路。于是乎,我又下定决心研究一翻,这才有了今天的实现。
游戏的逻辑:
每一关你都有不同数量的“针”,中心的圆盘上初始状态可能就会有“针”已经插入就绪,不过这是为增加游戏难度而设计的。你的任务就是将你所在关卡中所持有的所有“针”插入到中心转动的圆盘上去(插入点是相对固定的),前提是不能与其他“针”相互接触,否则游戏宣告失败。为了不让你很快通关,后面的关卡理所当然难度会越来越大,比如圆盘转速增加、初始就绪“针”数和持有针数增加、顺逆时针变速转动等。
实现思路:
中心的圆盘是一个CAShapeLayer,点击屏幕就去扩展CAShapeLayer的path,达到针插在圆盘上的效果。对于“针”的动态移动过程, 就可以创建一个过渡的“针”视图来等效代替。那么核心的问题就来了:我在扩展图层的路径时,怎么确定绘制的起始点和终点呢?因为在中心圆盘的旋转过程中,其上的个点也会随之一起做仿射变换。如果你在图层旋转时还是以圆盘静止时硬编码取到的起点和终点来确认绘制“针”的话,那么新绘制的“针”就会和之前“插上”的“针”重合在一起,导致的结果就是:无论你怎么“插针”,圆盘上看到的就只有一根“针”,虽然实际上它是很多“针”重叠在一起而表现出来的结果。
CAShapeLayer属于QuartzCore框架,继承自CALayer。CAShapeLayer是在坐标系内绘制贝塞尔曲线的,通过绘制贝塞尔曲线,设置shape(形状)的path(路径),从而绘制各种各样的图形以及不规则图形。因此,使用CAShapeLayer需要与UIBezierPath一起使用。
UIBezierPath类允许你在自定义的 View 中绘制和渲染由直线和曲线组成的路径。你可以在初始化的时候直接为你的UIBezierPath指定一个几何图形。
通俗点就是UIBezierPath用来指定绘制图形路径,而CAShapeLayer就是根据路径来绘图的。
仿射:无论变换矩阵用什么值,图层中平行的两条线在变换之后仍然保持平行。(平移,缩放,旋转等)
1、那么首要问题就是如何得到当前“针”的绘制起点和终点?
值得庆幸的是中心转轴是圆形的(故称之为圆盘),而且也只能是在圆形的情况下才能设计出aa这样的游戏效果。因为插针的起点和终点的计算依赖于圆形的中心到边界各点的距离相等这一性质,具体可以用如下两张图来表示坐标点的换算过程:
其中的基准点为在静止时“针”的插入点(对应于绘制“针”的起点),而终点可以通过相同的方法来实现换算,只不过半径r的值需要做出相应的调整。
基准点(x',y'),实际为了计算方便我们取(r,2r)
顺时针旋转诱导公式:
x = x' + r * sinθ
y = y' - r + r * cosθ
逆时针旋转诱导公式:
x = x' - r * sinθ
y = y' - r + r * cosθ
终点计算,只要把对应的r,换成实际的r即可。
根据公式,我们看出,要想求出针的起点和终点,我们必须知道图层旋转的角度。于是,第二个问题就出来了:
2、如何取得在旋转过程中图层的旋转角度?
我们都知道的是要是涉及到动画的话,图层会将动画的过程交给presentationLayer(表现层)来完成。至于动画的初值和终值之间的中间值则由系统不断计算,并通过表现层来展示。
既然我们选用的是图层,我们就需要知道它的transform属性是一个名为CATransform3D的结构体变量,而XXView的transform属性是一个名为CGAffineTransform的结构体变量。前者就自然没有a、b这样的成员变量,而是这样的一个矩阵结构:
struct CATransform3D
{
CGFloat m11, m12, m13, m14;
CGFloat m21, m22, m23, m24;
CGFloat m31, m32, m33, m34;
CGFloat m41, m42, m43, m44;
};
通过矩阵控制坐标变换
struct CATransform3D
{
CGFloat m11(x缩放), m12(y切变), m13(旋转), m14();
CGFloat m21(x切变), m22(y缩放), m23(和m32一起决定x轴的旋转), m24();
CGFloat m31(和m13一起决定y轴的旋转),m32(和m23一起决定x轴的旋转), m33(z缩放), m34(透视效果,要操作的这个对象要有旋转的角度,否则没有效果。正直/负值都有意义);
CGFloat m41(x平移), m42(y平移), m43(z平移), m44(初始为1);
};
其中的m14、m24、m34、m44只是作为矩阵的占位符,通常会将m14、m24、m34设置为0,m44设为1。
常见的变化矩阵:
举例:图层绕Z轴顺时针旋转推导过程
矩阵变换:
x' = m11 * x + m21 * y + m31 * z + m41
y' = m12 * x + m22 * y + m32 * z + m42
z' = m13 * x + m23 * y + m33 * z + m43
x1 = cosB * x0 - sinB * y0
y1 = sinB * x0 + cosB * y0
cosB sinB 0 0
-sinB cosB 0 0
0 0 1 0
0 0 0 1
aa中我们是围绕z轴旋转的,我们就可以使用反正切变换函数参入实际参数m21、m22的值就可以实时地知道图层的当前旋转角度了:
三角函数:
tanθ = {sinθ\over cosθ}
θ = atan({sinθ\over cosθ})
θ = atan2({sinθ,cosθ})
func transformRotationAngle() -> CGFloat {
var degreeAngle:CGFloat = -CGFloat(atan2f(Float(self.presentation()!.transform.m21), Float(self.presentation()!.transform.m22)))
if (degreeAngle < 0.0) {
degreeAngle = degreeAngle + (CGFloat)(2.0 * Double.pi)
}
return degreeAngle;
}
借助于数学诱导公式,坐标的变换计算公式可以统一为如下形式:
func convertPointWhenRotatingWithBenchmarkPoint(point:CGPoint,radius:CGFloat) -> CGPoint {
let rotationAngle:CGFloat = (self.presentation()?.transformRotationAngle())!
return CGPoint.init(x: point.x + CGFloat(sinf(Float(rotationAngle))) * radius, y: point.y - radius + CGFloat(cosf(Float(rotationAngle))) * radius)
}
在图层的旋转过程中,针的起点和终点都通过以上计算出来了,接着,生成一个贝塞尔曲线,然后追加到整体的贝塞尔曲线中,再赋值给CAShapeLayer(圆盘)的path,这样就达到了针的生成效果。
let strokeBezier = UIBezierPath()
strokeBezier.move(to: strokeStartPoint)
strokeBezier.addLine(to: strokeEndPoint)
strokeBezier.move(to: strokeEndPoint)
strokeBezier.addArc(withCenter: strokeEndPoint, radius: kTopRoundRadius, startAngle: 0, endAngle: CGFloat(Double.pi * 2.0), clockwise: true)
self.arrowStrokePath.append(strokeBezier)
self.centralAxisLayer.path = self.arrowStrokePath.cgPath;
游戏效果:
Github:
https://github.com/SpringAndSummer/Game
参考资料:
https://blog.csdn.net/u013282174/article/details/50388546
https://blog.csdn.net/zmmzxxx/article/details/74276077