ボールを衝突させてUIKitとSpriteKitのパフォーマンスを比較してみる

iOSでゲーム開発をするために、SpriteKitがあります。
でも、UIKit用いてゲーム開発ができないことはありません。

ただ、UIKitはゲーム専用ではないので、SpriteKitに対して処理速度が遅い、また物理演算を記述するのが大変というイメージがあります。
本記事では、SpriteKit、UIKitそれぞれを用いて同じ動作をするコードを記述し、両者を比較してみたいと思います。

今回は、画面中央から無数のボールを発生させ、お互いに、衝突させるコードを、それぞれSpriteKitとUIKitで記述してみたいと思います。

まず、SpriteKitの方から。

```objc:GameScene.m
#import "GameScene.h"

@interface GameScene () <SKPhysicsContactDelegate>
{
    int count;
}
@end

@implementation GameScene

-(void)didMoveToView:(SKView *)view {
    /* Setup your scene here */
    count = 0;
    self.physicsBody = [SKPhysicsBody bodyWithEdgeLoopFromRect:self.frame];
    self.physicsWorld.contactDelegate = self;
}

-(void)createBall{
    CGFloat radius = 5;
    float angle = (arc4random() % UINT32_MAX)*2*M_PI;
    float speed = 60.0;
    CGFloat velocityX = speed * cosf(angle);
    CGFloat velocityY = speed * sinf(angle);

    SKShapeNode *ball = [SKShapeNode node];
    ball.name = @"ball";
    ball.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame) );
    
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddArc(path, NULL, 0, 0, radius, 0, M_PI * 2, YES);
    ball.path = path;
    ball.fillColor = [SKColor orangeColor];
    ball.strokeColor = [SKColor clearColor];
    
    CGPathRelease(path);
    
    ball.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:radius];
    ball.physicsBody.affectedByGravity = NO;
    ball.physicsBody.velocity = CGVectorMake(velocityX, velocityY);
    ball.physicsBody.restitution = 1.0f;
    ball.physicsBody.linearDamping = 0;
    ball.physicsBody.friction = 0;
    ball.physicsBody.usesPreciseCollisionDetection = NO;
    ball.physicsBody.allowsRotation = NO;
    
    [self addChild:ball];
}

-(void)update:(CFTimeInterval)currentTime {
    //100回のボールが出現
    if (count < 100) {
        [self createBall];
        count++;
    }
}

@end
```
上記のシーンを実行すると、画面中央からボールが飛び出して、お互いに衝突を繰り返します。
![iOS Simulator Screen Shot 2015.01.06 21.00.43.png](https://qiita-image-store.s3.amazonaws.com/0/35297/53009c60-ed81-5c4e-10e7-c00433aebff1.png)
上記では8.2fpsとパフォーマンスが悪いように見えますが、これはシミュレータでスクリーンショットを撮影したためです。
OpenGL ESをシミュレータでエミュレートするとパフォーマスンが非常に悪くなるので、パフォーマンスの検証は必ず実機で行う必要があります。

次に、UIKitの方のコードです。
UIViewを継承した、Ballクラスを作ります。
衝突のロジックは、自分で記述しなければいけません。

```objc:Ball.h
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

@interface Ball : UIView

@property(nonatomic) CGVector speed;

@end
```

```objc:ViewController.m
#import "ViewController.h"
#import "Ball.h"

@interface ViewController ()
{
    int count;
    NSTimer *sTimer;
    NSMutableArray *ballArray;
    IBOutlet UILabel *countLabel;
}

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    count = 0;
    sTimer = [NSTimer scheduledTimerWithTimeInterval:1.0/60.0
                                              target:self
                                            selector:@selector(doAction)
                                            userInfo:nil
                                             repeats:YES];
    ballArray = [NSMutableArray new];
}

//タイマーから呼ばれるメソッド
-(void)doAction{
    
    //100個のボールが発生
    if (count < 100) {
        [self createBall];
        count++;
    }
    
    [self moveBall];
    [self collisionInArray:ballArray];
}

//ボールを生成
-(void)createBall{
    Ball *ball = [Ball new];
    ball.frame = CGRectMake(0, 0, 10, 10);
    ball.center = self.view.center;
    ball.backgroundColor = [UIColor orangeColor];
    ball.layer.cornerRadius = ball.frame.size.width/2.0;
    float speed = 1.0;
    float angle = (arc4random() % UINT32_MAX)*2*M_PI;
    float speedX = speed * cosf(angle);
    float speedY = speed * sinf(angle);
    ball.speed = CGVectorMake(speedX, speedY);
    [self.view addSubview:ball];
    [ballArray addObject:ball];
    
    //ボールをカウント
    countLabel.text = [NSString stringWithFormat:@"%d", [countLabel.text intValue]+1];
}

//ボールを動かす
-(void)moveBall{
    [ballArray enumerateObjectsUsingBlock:^(Ball *ball, NSUInteger idx, BOOL *stop) {
        
        //ボールの移動
        ball.center = CGPointMake(ball.center.x+ball.speed.dx,
                                  ball.center.y+ball.speed.dy);
        
        //端で反射
        if (ball.center.x<0) {
            ball.speed = CGVectorMake(fabsf(ball.speed.dx), ball.speed.dy);
        }
        if (ball.center.x>self.view.frame.size.width){
            ball.speed = CGVectorMake(-fabsf(ball.speed.dx), ball.speed.dy);
        }
        if (ball.center.y<0){
            ball.speed = CGVectorMake(ball.speed.dx, fabsf(ball.speed.dy));
        }
        if (ball.center.y>self.view.frame.size.height){
            ball.speed = CGVectorMake(ball.speed.dx, -fabsf(ball.speed.dy));
        }
    }];
}

//ボールの衝突
-(void)collisionInArray:(NSArray *)viewArray{
    [ballArray enumerateObjectsUsingBlock:^(Ball *view1, NSUInteger i, BOOL *stop1) {
        for (NSUInteger j=i+1; j<[ballArray count]; j++) {
            Ball *view2 = ballArray[j];
            
            //円形の判定は高コストなため、最初に矩形で判定
            float hitRadiusX = (view1.frame.size.width+view2.frame.size.width)/2.0;
            float hitRadiusY = (view1.frame.size.height+view2.frame.size.height)/2.0;
            float hitRadius = (hitRadiusX+hitRadiusY)/2.0;
            
            float distanceY =   view2.center.y - view1.center.y;
            if (fabsf(distanceY) > hitRadius) {
                continue;
            }
            
            float distanceX =  view2.center.x - view1.center.x;
            if (fabsf(distanceX) > hitRadius) {
                continue;
            }
            
            //円形で衝突判定
            float distance = sqrtf(distanceX*distanceX+ distanceY*distanceY);
            if (distance < hitRadius) {
                
                //衝突後のスピード計算
                CGVector unitVector;
                unitVector.dx = distanceX/distance;
                unitVector.dy = distanceY/distance;
                double e = 1.0;
                double speedFrom = view1.speed.dx*unitVector.dx+view1.speed.dy*unitVector.dy;
                double speedTo = view2.speed.dx*unitVector.dx+view2.speed.dy*unitVector.dy;
                double aN = 0.5*(1+e)*(speedTo-speedFrom);
                double dSx1 = aN*unitVector.dx;
                double dSy1 = aN*unitVector.dy;
                double dSx2 = -aN*unitVector.dx;
                double dSy2 = -aN*unitVector.dy;
                view1.speed = CGVectorMake(view1.speed.dx+dSx1, view1.speed.dy+dSy1);
                view2.speed = CGVectorMake(view2.speed.dx+dSx2, view2.speed.dy+dSy2);
                
                //2つのボールの重なりの解消
                float overlap = hitRadius-distance;
                float radius1 = (view1.frame.size.width+view1.frame.size.height)/2.0;
                float radius2 = (view2.frame.size.width+view2.frame.size.height)/2.0;
                float totalMass = radius1*radius1 + radius2*radius2;
                float move1 = overlap/totalMass*radius2*radius2;
                float move2 = overlap/totalMass*radius1*radius1;
                float cosign = distanceX/distance;
                float sign = distanceY/distance;
                float dx1 = -move1*cosign;
                float dy1 = -move1*sign;
                float dx2 = move2*cosign;
                float dy2 = move2*sign;
                view1.center = CGPointMake(view1.center.x+dx1, view1.center.y+dy1);
                view2.center = CGPointMake(view2.center.x+dx2, view2.center.y+dy2);
            }
        }
    }];
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end
```

以下はスクリーンショットです。SpriteKitの際と同様に、ボールが衝突を繰り返します。
![iOS Simulator Screen Shot 2015.01.06 21.19.18.png](https://qiita-image-store.s3.amazonaws.com/0/35297/10dd3eeb-976a-40bc-f3de-d189f469242a.png)
SpriteKitの際と逆に、シミュレータだとパフォーマンスが良くなりすぎるので、UIKitの場合も実機での検証が大事になります。

両者で、ボールの数を変えてパフォーマンスを比較した結果が以下です。
検証には手元のiPhone5を使いました。
![image](https://qiita-image-store.s3.amazonaws.com/0/35297/6bfdd2fb-acf0-32f9-b631-df15ba481b0f.png)

SpriteKitの方はわかりませんが、UIKitの方のロジックでは全てのボールの同士の組み合わせに対して衝突判定を行っているので、ボールの数が増えると計算量が膨大になります。
さすがにボールの数が多い場合の動作感覚はSpriteKitの方がスムーズですが、UIKitの方もCPUの使用率などでは意外に健闘していることが分かります。
また、SpriteKitの方ではボールのスピードが速くても動きが自然に見えましたが、UIKitの方ではボールが早いとカクカクしたり霞んで見える傾向がありました。

以上により、激しいアクションや複雑な物理計算が必要なゲームは本職のSpriteKit
を選択した方がいいと思いますが、それほど激しいアクションや複雑な物理計算が必要でないゲームであれば、UIKitでゲームを作ることを検討するのもアリかと思います。
UIKitでゲームを作れば、ツールのノウハウをそのまま使うこともできるというメリットもありますしね。
どうやら、妥当な結果に落ち着いたようです。

ちなみに、以前にUIKitを作って弾幕シューティングを作ってみたことがあります。
流石に弾数が増えると処理落ちしますが、結構遊べるものは作れるようです。
https://itunes.apple.com/jp/app/star-gradiator-free/id806155270

今回のソースは、以下にアップしおきますね。
https://github.com/yukinaga/CollisionPerformanceTestSpriteKit
https://github.com/yukinaga/CollisionPerformanceTestUIKit