我正在创建一个应用程序(除其他外)显示一个简化的罗盘图像,根据设备的旋转旋转。问题是这样做:
float heading = -1.0f * M_PI * trueHeading / 180.0f; //trueHeading is always between 0 and 359, never 360
self.compassNeedle.transform = CGAffineTransformMakeRotation(heading);
在CLLocationManager的didUpdateHeading方法中使动画变得丑陋和不连贯。 我已经使用了Instruments来查明它的应用程序是否只能以超过30-48 fps的速度渲染,但事实并非如此。
如何平滑图像视图的旋转,使其更像Apple自己的Compass应用程序?
答案 0 :(得分:3)
尝试使用真实航向的最后N个值的平均值,而不是使用当前的即时值。价值可能会在一瞬间大量跳跃,但可以“平均”稳定下来。
假设你有一个成员变量storedReadings,它是一个NSMutableArray:
-(void)addReading(float):newReading
{
[storedReadings addObject:[NSNumber numberWithFloat:newReading]];
while([storedReadings count] > MAX_READINGS)
{
[storedReadings removeObjectAtIndex:0];
}
}
然后当你需要平均值(定时器更新?)
-(float)calcReading
{
float result = 0.0f;
if([storedReadings count] > 0)
{
foreach(NSNumber* reading in storedReadings)
{
result += [reading floatValue];
}
result /= [storedReadings count];
}
return result;
}
您可以先验选择MAX_READINGS。
NEXT LEVEL(S)UP
如果读数没有跳得那么多但动画仍然不稳定,你可能需要做一些像“平滑”旋转的事情。在任何给定的时间,你有你正在显示的当前角度,theta(在你的班级存储它,从0开始)。你也有目标角度,称之为目标。这是从平滑的calcReading函数中获得的值。 错误 定义为两者之间的差异:
error = target-theta;
设置一个定时器回调,其周期为0.05秒(每秒20次)。您要做的是调整theta
,以便将error
推向0.您可以通过以下几种方式执行此操作:
thetaNext += kProp * (target - theta);
//这是比例反馈。thetaNext += kStep * sign(target-theta);
//这会在每次更新时将theta移动一个固定的数量。如果x> = 0,则sign(x)= +1,如果x <0,则为-1。 0 第一种解决方案将导致旋转距离目标越远,其变化越大。当它摆动超过“零”点时,它也可能会稍微摆动一下。 更大的kProp值会产生更快的响应,但也会产生更多的振荡。需要进行一些调整。
第二种解决方案将更容易控制......它只是每次“嘀嗒”罗盘针。你可以将kStep设置为1/4度,这使你的旋转“速度”约为(1/4度/更新)*(20更新/秒)=每秒5度。这有点慢,但您可以看到数学并更改kStep以满足您的需求。 注意您可以“绑定”“错误”值,以便在错误<1时不采取任何措施。 kStep(或类似的东西)。当角度非常接近目标时,这可以防止指南针移动。您可以在错误较小时更改kStep,以便它“滑动”到结束位置(即,当错误很小时,kStep会更小)。
为了处理角度问题(环绕),我将角度“标准化”,使其始终在-Pi / Pi内。我不保证这是完美的方式,但它似乎完成了工作:
// Takes an angle greater than +/- M_PI and converts it back
// to +/- M_PI. Useful in Box2D where angles continuously
// increase/decrease.
static inline float32 AdjustAngle(float32 angleRads)
{
if(angleRads > M_PI)
{
while(angleRads > M_PI)
{
angleRads -= 2*M_PI;
}
}
else if(angleRads < -M_PI)
{
while(angleRads < -M_PI)
{
angleRads += 2*M_PI;
}
}
return angleRads;
}
通过这种方式,-pi是您在向左/向右继续旋转时从任一方向前进的角度。也就是说,从0到359度的数字没有不连续性。
将这一切放在一起
static inline float Sign(float value)
{
if(value >= 0)
return 1.0f;
return -1.0f;
}
//#define ROTATION_OPTION_1
//#define ROTATION_OPTION_2
#define ROTATION_OPTION_3
-(void)updateArrow
{
// Calculate the angle to the player
CGPoint toPlayer = ccpSub(self.player.position,self.arrow.position);
// Calculate the angle of this...Note there are some inversions
// and the actual image is rotated 90 degrees so I had to offset it
// a bit.
float angleToPlayerRads = -atan2f(toPlayer.y, toPlayer.x);
angleToPlayerRads = AdjustAngle(angleToPlayerRads);
// This is the angle we "wish" the arrow would be pointing.
float targetAngle = CC_RADIANS_TO_DEGREES(angleToPlayerRads)+90;
float errorAngle = targetAngle-self.arrow.rotation;
CCLOG(@"Error Angle = %f",errorAngle);
#ifdef ROTATION_OPTION_1
// In this option, we just set the angle of the rotated sprite directly.
self.arrow.rotation = CC_RADIANS_TO_DEGREES(angleToPlayerRads)+90;
#endif
#ifdef ROTATION_OPTION_2
// In this option, we apply proportional feedback to the angle
// difference.
const float kProp = 0.05f;
self.arrow.rotation += kProp * (errorAngle);
#endif
#ifdef ROTATION_OPTION_3
// The step to take each update in degrees.
const float kStep = 4.0f;
// NOTE: Without the "if(fabs(...)) check, the angle
// can "dither" around the zero point when it is very close.
if(fabs(errorAngle) > kStep)
{
self.arrow.rotation += Sign(errorAngle)*kStep;
}
#endif
}
我将此代码放入我为Cocos2d编写的演示程序中。它显示了一些角色(大盒子)被一些怪物追逐(较小的盒子)并且中心的箭头总是指向角色。 updateArrow调用定期在计时器滴答(更新(dt)函数)上进行。玩家在屏幕上的位置由用户点击屏幕设置,角度基于从箭头到玩家的向量。在该功能中,我显示了设置箭头角度的所有三个选项:
选项1
根据玩家的位置设置(即设置)。
选项2
每次使用比例反馈调整箭头角度。
选项3
如果误差角度大于步长,则逐步调整箭头的角度。
这张照片大致显示了它的样子:
而且,all the code is available here on github。只需查看HelloWorldLayer.m文件即可。
这有用吗?