Recognize current device position as flat
Asked Answered
G

3

11

So I have this app Im working on where you can roll the ball around the screen by tilting the device around(accelerometer). How can I alter the code below so that I don't have to hold the phone flat and have that as my neutral balance point. What I want is that whatever tilt you have with the device at the moment when the app loads, that will be the neural balance point. From that current angle your holding the device that is the neutral point. Neutral balance point meaning the point where the ball is pretty much still. Hope thats clear as to what I would like. Also the app is landscapeRight only.

note The code below works 100 percent well just like it need it to work for my app.Just I need to hold the phone flat to roll the ball around...

CGRect screenRect;
CGFloat screenHeight;
CGFloat screenWidth;
double currentMaxAccelX;
double currentMaxAccelY;
@property (strong, nonatomic) CMMotionManager *motionManager;

-(id)initWithSize:(CGSize)size {

     //init several sizes used in all scene
        screenRect = [[UIScreen mainScreen] bounds];
        screenHeight = screenRect.size.height;
        screenWidth = screenRect.size.width;

    if (self = [super initWithSize:size]) {

        self.motionManager = [[CMMotionManager alloc] init];
        self.motionManager.accelerometerUpdateInterval = .2;

       [self.motionManager startAccelerometerUpdatesToQueue:[NSOperationQueue currentQueue]
                                                 withHandler:^(CMAccelerometerData  *accelerometerData, NSError *error) {
                                                    [self outputAccelertionData:accelerometerData.acceleration];
                                                     if(error)
                                                     {
                                                         NSLog(@"%@", error);
                                                     }
                                                 }];
    }

    return self;

}

-(void)outputAccelertionData:(CMAcceleration)acceleration{

    currentMaxAccelX = 0;
    currentMaxAccelY = 0;

    if(fabs(acceleration.x) > fabs(currentMaxAccelX))
    {
        currentMaxAccelY = acceleration.x;
    }
    if(fabs(acceleration.y) > fabs(currentMaxAccelY))
    {
        currentMaxAccelX = acceleration.y;
    }
}

-(void)update:(CFTimeInterval)currentTime {

    /* Called before each frame is rendered */

    //set min and max bounderies
    float maxY = screenHeight - (self.ball.size.width/2);
    float minY = 0 + (self.ball.size.width/2);

    float maxX = screenWidth - (self.ball.size.height/2);
    float minX = 0 + (self.ball.size.height/2);

    float newY = 0;
    float newX = 0;
    //left and right tilt
    if(currentMaxAccelX > 0.05){
        newX = currentMaxAccelX * -10;
    }
    else if(currentMaxAccelX < -0.05){
        newX = currentMaxAccelX*-10;
    }
    else{
        newX = currentMaxAccelX*-10;
    }
    //up and down tilt
    newY = currentMaxAccelY *10;

    newX = MIN(MAX(newX+self.ball.position.x,minY),maxY);
    newY = MIN(MAX(newY+self.ball.position.y,minX),maxX);

    self.ball.position = CGPointMake(newX, newY);

}
Galvez answered 24/2, 2014 at 5:24 Comment(3)
If I understood correctly: At applaunch, register the x/y value, that's your new "zero". Then use this new zero as an offset to do what you want to do.Raber
@Raber This should be an answer, as it is the correct way to do itGumption
Yes but Id like the answer in code related to how I have things done in my code. Its hard to find any code examples of how to use an offset in my searches.Galvez
E
4

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.

Encarnacion answered 27/2, 2014 at 10:24 Comment(6)
Doesn't work do you know that my ball object is a SKSpriteNode? Property centre and superview are not recognized by that objectGalvez
Also don't see how you initialize CADDisplayLink?Galvez
@Rookie yes, I missed that this is a SpriteKit framework. Answer updated.Encarnacion
This works wonderfully. Thank you one last question I know I can make it less sensitive by lowering kSensitivity but how do I make it more responsive as in if the ball is rolling in one direction(for ex. left) and I tilt right really fast, Id like the ball to change directions faster.Galvez
So basically a faster response to direction change*Galvez
@Rookie try to introduce velocity factor: _vy = _vy*0.8 + kSensivity * <rest of formula>. += was changed to =. Same for _vx.Encarnacion
G
6

First, Larme's comment gives the correct answer for determining the starting point.

However, if you are trying to determine device tilt (attitude), you want to use the gyroscope, not the accelerometer. The accelerometer tells how fast the device is moving in each direction. That's useful for determining if the user is quickly moving or shaking the device but doesn't help you at all determine whether the device is being tilted. The gyroscope provides the device's current attitude and the rate of rotation.

Since it sounds like you are trying to implement a ball that will "roll" around a table as the user tilts the device, you probably want to get the attitude. To get the attitude, use startDeviceMotionUpdatesToQueue:withHandler:. Then you can use the attitude property of the CMDeviceMotion object to find out how the device is oriented on each axis.

Gambrinus answered 26/2, 2014 at 21:14 Comment(1)
The above code works 100 Percent for me in "rolling" the ball around the screen in the direction the phone is tilted. I have asked for an answer as to how to offset using the code already provided as its what I already have working for my app. Thanks for your advice thoughGalvez
E
4

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.

Encarnacion answered 27/2, 2014 at 10:24 Comment(6)
Doesn't work do you know that my ball object is a SKSpriteNode? Property centre and superview are not recognized by that objectGalvez
Also don't see how you initialize CADDisplayLink?Galvez
@Rookie yes, I missed that this is a SpriteKit framework. Answer updated.Encarnacion
This works wonderfully. Thank you one last question I know I can make it less sensitive by lowering kSensitivity but how do I make it more responsive as in if the ball is rolling in one direction(for ex. left) and I tilt right really fast, Id like the ball to change directions faster.Galvez
So basically a faster response to direction change*Galvez
@Rookie try to introduce velocity factor: _vy = _vy*0.8 + kSensivity * <rest of formula>. += was changed to =. Same for _vx.Encarnacion
R
0

Why don't you just take the numbers, at the point of start up, as a baseline and save them as a class property. Any further readings you have you can simply add/subtract the current numbers with your baseline. Unless I am wrong, that should give you the desired results.

Ruffian answered 27/2, 2014 at 2:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.