Thursday 1 March 2012

Smoothing accelerometer input

My upcoming game, Balls Abound, makes heavy use of the accelerometer for user input. The accelerometer readings on iOS devices are quite sensitive, and left untreated produce very jittery game play (especially when you are using them to drive the position of your scenes camera).

To make the data usable in my game, I decided to apply a rolling average to act as a sort of low pass filter, and smooth out what was happening on screen. I picked a filter size of 8 samples, tried a couple of either sizes, but they became too "loose" or too "tight", 8 worked well. From here, I just take the average of the last 8 samples, and replace the raw accelerometer data with the averaged data. Here's the code:


    //NOTE: z is not being properly handled, as I don't need it
    CMAcceleration accel = accelerometerData.acceleration;
    float x = accel.x;
    float y = accel.y;
                
    // we have to average samples together for any filter size > 1
    if (_filterSize > 1) {
        // the index into our samples array (this just goes 0->n, 0->n,...)
        int index = _numSamples++ % _filterSize;
        // get a handle to the sample
        CGPoint *point = &_samples[index];
        // adjust our accumulator (it holds the sum of all of our samples)
        _accumulator.x -= (point->x - x);
        _accumulator.y -= (point->y - y);
        // replace the data in our sample array
        // with the current accelerometer reading
        point->x = x;
        point->y = y;
                    
        // calculate the averaged accelerometer reading,
        // we report this to our delegates
        x = _accumulator.x / MIN(_numSamples, _filterSize);
        y = _accumulator.y / MIN(_numSamples, _filterSize);
    }                

    // if the idle timer is NOT disabled, then we want to poke the bear
    // every 30s (60s is the minimum settable screen lock, half that seem to 
    // work) as long as there is accelerometer input, this way
    // the screen can go dim if the device is put down, but stays lit
    // if the device is being moved but not touched
    if (!_idleTimerDisabled && _count++ > 10 * 60 /* assume 60fps */) {
        // any change in x or y magnitude >= 1% 
        // (remember accel x and y are in the range -1 to 1)
        BOOL changed = fabsf(_lastAccel.x - x) >= 0.01;
        if (!changed) {
            changed = fabsf(_lastAccel.y - y) >= 0.01;
        }
                    
        if (changed) {
            UIApplication *app = [UIApplication sharedApplication];
            app.idleTimerDisabled = YES;
            app.idleTimerDisabled = NO;
            _count = 0;
        }
    }
                
    _lastAccel.x = x;
    _lastAccel.y = y;

On each call to drawScene, CCDirectorIOS tells the CCMotionDispatcher to send the _lastAccel value to it's delegates.  

You may have also noticed some code mucking about with the idle timer. Balls Abound is driven by the accelerometer, so the screen is not touched often. But I didn't want to just disable the idle timer, if a user just put their device down with Balls Abound open then it would kill their battery, as the device would never sleep. So I devised a method of poking the idle timer whenever the accelerometer was actively reading changes in data.  

No comments:

Post a Comment