3

Recently when I was playing with Stuart Hall's tutorial for UIKit Dynamics (http://stuartkhall.com/posts/flipcase-bounce-in-uikit-dynamics), I found there was a performance issue.

After I added about 50 items (bouncing balls) to the animator, the app became very slow -- almost frozen. A profiling shows [UIDynamicAnimator _animatorStep] takes 96% of CPU.

Does anybody know how to improve performance for an UIKit Dynamics app with large amount of UIDynamicItems?

You can download my code and see the performance issue yourself:

https://www.dropbox.com/s/zy7ajj6molxm9up/Flipper-UIKit-Dynamics-Poor-Performance.zip

Following is the code where everything happens:

#import "ViewController.h"
#import <CoreMotion/CoreMotion.h>

@interface ViewController () {
    CMMotionManager *_motionManager;
    NSOperationQueue *_queue;

    int _count;
}

@property(nonatomic, strong) UIDynamicAnimator *animator;
@property(nonatomic, strong) UIGravityBehavior *gravityBehavior;
@property(nonatomic, strong) UICollisionBehavior *collisionBehavior;
@property(nonatomic, strong) UIDynamicItemBehavior *bounceBehaviour;

@end

@implementation ViewController

static NSInteger const kBallSize = 25;

- (void)startMotion {
    if (_motionManager == nil) {
        _queue = [[NSOperationQueue alloc] init];
        _motionManager = [[CMMotionManager alloc] init];
        _motionManager.deviceMotionUpdateInterval = 0.1;
    }
    [_motionManager startDeviceMotionUpdatesToQueue:_queue withHandler:^(CMDeviceMotion *motion, NSError *error) {
//        angleLabel.text = [NSString stringWithFormat:@"%g", motion.attitude.pitch];
        self.gravityBehavior.angle = atan2(motion.gravity.x + motion.userAcceleration.x, motion.gravity.y + motion.userAcceleration.y); // motion.attitude.pitch + M_PI / 2.0;
    }];
}

- (void)stopMotion {
    if (_motionManager) {
        [_motionManager stopDeviceMotionUpdates];
    }

    _motionManager = nil;
}

- (void)viewDidLoad {
    [super viewDidLoad];

    [self startMotion];

    // Simple tap gesture
    UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onTap:)];
    [self.view addGestureRecognizer:tapGesture];

    // Create our animator, we retain this ourselves
    self.animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];

    // Gravity
    self.gravityBehavior = [[UIGravityBehavior alloc] initWithItems:@[]];
    self.gravityBehavior.magnitude = 10;
    [self.animator addBehavior:self.gravityBehavior];

    // Collision - make a fake platform
    self.collisionBehavior = [[UICollisionBehavior alloc] initWithItems:@[]];

    UIBezierPath *aPath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(284, 160)
                                                         radius:160
                                                     startAngle:(CGFloat) (M_PI * 0.0)
                                                       endAngle:(CGFloat) (M_PI * 2)
                                                      clockwise:YES];

    [self.collisionBehavior addBoundaryWithIdentifier:@"bottom" forPath:aPath];
    self.collisionBehavior.translatesReferenceBoundsIntoBoundary = YES;
    [self.animator addBehavior:self.collisionBehavior];

    CAShapeLayer *layer = [CAShapeLayer layer];
    layer.path = aPath.CGPath;
    layer.fillColor = [UIColor lightGrayColor].CGColor;

    [self.view.layer addSublayer:layer];

    // Bounce!
    self.bounceBehaviour = [[UIDynamicItemBehavior alloc] initWithItems:@[]];
    self.bounceBehaviour.elasticity = 0.75;
    self.bounceBehaviour.resistance = 0.1;
    self.bounceBehaviour.friction = 0.01;
    [self.animator addBehavior:self.bounceBehaviour];
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
}

- (BOOL)prefersStatusBarHidden {
    return YES;
}

#pragma mark - Gesture Recognizer

- (void)onTap:(UITapGestureRecognizer *)gesture {
    if (gesture.state == UIGestureRecognizerStateEnded) {
        // Grab the x of the touch for the center of our ball
        // Ignore the y, we'll drop from the top
        CGPoint pt = [gesture locationInView:self.view];
        [self dropBallAtX:pt];
    }
}

#pragma mark - Helpers

- (void)dropBallAtX:(CGPoint)p {
    _count++;
    self.label.text = [NSString stringWithFormat:@"Balls: %d", _count];

    // Create a ball and add it to our view
    UIView *ball = [[UIView alloc] initWithFrame:CGRectMake(p.x - (kBallSize / 2), p.y - (kBallSize / 2), kBallSize, kBallSize)];
    CGFloat hue = (arc4random() % 256 / 256.0);               //  0.0 to 1.0
    CGFloat saturation = (arc4random() % 64 / 256.0) + 0.75;  //  0.5 to 1.0, away from white
    CGFloat brightness = (arc4random() % 64 / 256.0) + 0.75;
    ball.backgroundColor = [UIColor colorWithHue:hue saturation:saturation brightness:brightness alpha:1.0];
    ball.layer.cornerRadius = kBallSize / 2;
    ball.layer.masksToBounds = YES;
    [self.view addSubview:ball];

    // Add some gravity
    [self.gravityBehavior addItem:ball];

    // Add the collision
    [self.collisionBehavior addItem:ball];

    // Add the bounce
    [self.bounceBehaviour addItem:ball];
}

@end
Yuchen Wang
  • 1,682
  • 2
  • 14
  • 16

1 Answers1

3

This strikes me as a pretty hairy problem, where each ball will impact its neighbors, which will in turn impact their neighbors recursively, etc. I'm not surprised this non-linear-complexity problem suffers from pretty serious performance degradation as you add more balls. I found that fps started to fall observably as you approached 30-40 balls (on iPhone 5), and like you said, as you approached 50 balls, it quickly approaches 1-2 fps. If you let it to reach quiescence (which is unlikely if you keep CMMotionManager in the mix), after a few seconds fps was restored, but as soon as you dropped another ball or CMMotionManager changes the gravity again, the complexity of the problem quickly brought the fps to its knees again.

Simple fixes (like increasing friction, reducing bounce, introducing angular resistance, moving to rectangular shapes, fixing items in one spot once they stop moving by adding attachment behavior (which doesn't make sense if using CMMotionManager, anyway), etc.) seemed to have modest impact. It strikes me that the only way to address this would be to abandon UIKit Dynamics for some other approach that could make some simplifying assumptions on the basis of your specific problem (e.g. since you're dealing with circular objects that are angularly symmetric you could eliminate rotational complexity, you could employ far simpler collision logic, adjust some of the aforementioned variables in order to dampen the behavior more quickly; etc.). I'm not familiar with any frameworks that could do that for you, and thus this might entail a non-trivial amount of code. And even if you did that, you're still dealing with a problem with non-linear complexity and performance is likely to still degrade at some point.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Yeah, I think that's probably the limitation of the UIKit Dynamics. I am sure SpriteKit will handle this better. – Yuchen Wang Jun 27 '14 at 17:49
  • @YuchenWang Perhaps, but I suspect, in the end, you'll suffer from the same combinatorial explosion where each ball results in a cascade of changes to the other 50 balls (which in turn, each will impact the other balls). But, it might be worth trying SpriteKit. It strikes me that you need some way to constrain the problem (e.g. fixing a ball at a location after a while (or after you programmatically deduce that additional balls can't hit it anymore). It's a non-trivial problem. – Rob Jun 27 '14 at 17:59
  • 1
    actually, if you take a look at the animation done by Jawbone's app "Up Coffee", it's exactly what I did in the sample app but with much better animation. I checked their app and didn't find any 3rd party libs. So they must did it with UIKit Dynamics. I am very curious how they achieve that. – Yuchen Wang Jun 30 '14 at 00:30
  • here is the link to the app: https://itunes.apple.com/us/app/up-coffee/id828031130?mt=8 – Yuchen Wang Jun 30 '14 at 00:32
  • @YuchenWang, "So they must did it with UIKit Dynamics". `SpriteKit` isn't 3rd party, is it? – Iulian Onofrei Jun 03 '15 at 14:04