As it was mentioned, we need to catch an initial device position (accelerometer value) and use it as zero reference. We catch reference value once when game starts and subtract this value from every next accelerometer update.
static const double kSensivity = 1000;
@interface ViewController ()
{
CMMotionManager *_motionManager;
double _vx, _vy; // ball velocity
CMAcceleration _referenceAcc; // zero reference
NSTimeInterval _lastUpdateTimeInterval; // see update: method
}
Initially, ball is motionless (velocities = 0). Zero reference is invalid. I set significant value in CMAcceleration
to mark it as invalid:
_referenceAcc.x = DBL_MAX;
Accelerometer updates. As the app uses landscape right mode only we map y-acceleration to x-velocity, and x-acceleration to y-velocity. accelerometerUpdateInterval
factor is required to make velocity values independent of update rate. We use negative sensitivity value for x-acceleration, because direction of accelerometer X axis is opposite to landscape right orientation.
-(id)initWithSize:(CGSize)size {
if (self = [super initWithSize:size]) {
_vx = 0;
_vy = 0;
_referenceAcc.x = DBL_MAX;
_motionManager = [CMMotionManager new];
_motionManager.accelerometerUpdateInterval = 0.1;
[_motionManager
startAccelerometerUpdatesToQueue:[NSOperationQueue mainQueue]
withHandler:^(CMAccelerometerData *accelerometerData, NSError *error) {
CMAcceleration acc = accelerometerData.acceleration;
if (_referenceAcc.x == DBL_MAX) {
_referenceAcc = acc;
_referenceAcc.x *= -1;
_referenceAcc.y *= -1;
}
_vy += kSensivity * (acc.x+_referenceAcc.x) * _motionManager.accelerometerUpdateInterval;
_vx += -kSensivity * (acc.y+_referenceAcc.y) * _motionManager.accelerometerUpdateInterval;
}];
self.ball = [SKSpriteNode spriteNodeWithImageNamed:@"ball"];
self.ball.position = CGPointMake(self.size.width/2, self.size.height/2);
[self addChild:self.ball];
}
return self;
}
Your update:
method does not respect currentTime
value. Intervals between update calls can be different. It would be better to update distance according to time interval.
- (void)update:(NSTimeInterval)currentTime {
CFTimeInterval timeSinceLast = currentTime - _lastUpdateTimeInterval;
_lastUpdateTimeInterval = currentTime;
CGSize parentSize = self.size;
CGSize size = self.ball.frame.size;
CGPoint pos = self.ball.position;
pos.x += _vx * timeSinceLast;
pos.y += _vy * timeSinceLast;
// check bounds, reset velocity if collided
if (pos.x < size.width/2) {
pos.x = size.width/2;
_vx = 0;
}
else if (pos.x > parentSize.width-size.width/2) {
pos.x = parentSize.width-size.width/2;
_vx = 0;
}
if (pos.y < size.height/2) {
pos.y = size.height/2;
_vy = 0;
}
else if (pos.y > parentSize.height-size.height/2) {
pos.y = parentSize.height-size.height/2;
_vy = 0;
}
self.ball.position = pos;
}
EDIT: alternative way
By the way, I found an alternative way solve it. If you use SpriteKit
, it is possible to configure gravity of physics world in response to accelerometer changes. In that case there's no need to move a ball in update:
method.
We need to add physics body to a ball sprite and make it dynamic:
self.physicsWorld.gravity = CGVectorMake(0, 0); // initial gravity
self.ball.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:self.ball.size.width/2];
self.ball.physicsBody.dynamic = YES;
[self addChild:self.ball];
And set updated gravity in accelerometer handler:
// set zero reference acceleration
...
_vy = kSensivity * (acc.x+_referenceAcc.x) * _motionManager.accelerometerUpdateInterval;
_vx = -kSensivity * (acc.y+_referenceAcc.y) * _motionManager.accelerometerUpdateInterval;
self.physicsWorld.gravity = CGVectorMake(_vx, _vy);
Also we need to set physical bounds of the screen in order to limit ball movement.