处理场景中的数千个SKSpriteNodes

时间:2016-01-05 03:56:56

标签: ios sprite-kit skspritenode

我正在使用精灵工具包构建一个游戏,它是一个游戏,你可以将球送入水桶并长出水桶。随着水桶的增长,球(SKSpriteNodes)留在现场。我试图了解如何在管理数千个节点的同时保持高性能。知道我怎么能这样做吗? 700左右后,模拟器中的FPS低于10 tps。

这是我场景中的代码。任何帮助表示赞赏。

//
//  GameScene.m
//

#import "GameScene.h"

@implementation GameScene
@synthesize _flowIsON;

NSString *const kFlowTypeRed = @"RED_FLOW_PARTICLE";
const float kRED_DELAY_BETWEEN_PARTICLE_DROP = 0.1; //delay for particle drop in seconds

static const uint32_t kRedParticleCategory         =  0x1 << 0;
static const uint32_t kInvisbleWallCategory        =  0x1 << 1;

NSString *const kStartBtn = @"START_BTN";
NSString *const kLever = @"Lever";

NSString *const START_BTN_TEXT = @"Start Game";

CFTimeInterval lastTime;


-(void)didMoveToView:(SKView *)view {

    [self initializeScene];

}

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {


    for (UITouch *touch in touches) {
        CGPoint location = [touch locationInNode: self];

        SKNode *node = [self nodeAtPoint:location];

        if ([node.name isEqualToString:kStartBtn]) {
            [node removeFromParent];

            //initalize to ON
            _flowIsON = YES;

            //[self initializeScene];
        } else if ([node.name isEqualToString:kLever]) {

            _leverNode = (SKSpriteNode *)node;

            [self selectNodeForTouch:location];

        }
    }
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch *touch = [touches anyObject];
    CGPoint positionInScene = [touch locationInNode:self];
    CGPoint previousPosition = [touch previousLocationInNode:self];

    CGPoint translation = CGPointMake(positionInScene.x - previousPosition.x, positionInScene.y - previousPosition.y);

    [self panForTranslation:translation];
}

-(void)update:(CFTimeInterval)currentTime {

    float deltaTimeInSeconds = currentTime - lastTime;

    //NSLog(@"Time is %f and flow is %d",deltaTimeInSeconds, _flowIsON);

    if ((deltaTimeInSeconds > kRED_DELAY_BETWEEN_PARTICLE_DROP) && _flowIsON) {

        [self startFlow:kFlowTypeRed];

        //only if its been past 1 second do we set the lasttime to the current time
        lastTime = currentTime;

    }



}


- (void) initializeScene {

    SKLabelNode *startBtn = [SKLabelNode labelNodeWithFontNamed:@"Chalkduster"];

    startBtn.text = START_BTN_TEXT;
    startBtn.name = kStartBtn;
    startBtn.fontSize = 45;
    startBtn.position = CGPointMake(CGRectGetMidX(self.frame),
                                    CGRectGetMidY(self.frame));

    [self addChild:startBtn];

    //init to flow off
    _flowIsON = NO;

    // Set physics body delegate
    self.physicsWorld.contactDelegate = self;
    self.shouldRasterize = YES;
    self.view.showsDrawCount = YES;
    self.view.showsQuadCount = YES;


    //Set collision mask for invisible wall
    _nonWallNode =  (SKSpriteNode *) [self.scene childNodeWithName:@"NonWall"];
    _nonWallNode.physicsBody.categoryBitMask = kInvisbleWallCategory;
    _nonWallNode.physicsBody.collisionBitMask = kRedParticleCategory;
    _nonWallNode.physicsBody.contactTestBitMask = kRedParticleCategory | kInvisbleWallCategory;

}


- (void) startFlow:(NSString *)flowKey  {

//    //SKSpriteNode *redParticleEmitter = [SKSpriteNode spriteNodeWithImageNamed:@"RedFlowParticles"];
//    
//    SKShapeNode *redParticleEmitter = [[SKShapeNode alloc] init];
//    
//    CGMutablePathRef myPath = CGPathCreateMutable();
//    CGPathAddArc(myPath, NULL, 0,0, 15, 0, M_PI*2, YES);
//    redParticleEmitter.path = myPath;
//    
//    redParticleEmitter.lineWidth = 1.0;
//    redParticleEmitter.fillColor = [SKColor blueColor];
//    redParticleEmitter.strokeColor = [SKColor whiteColor];
//    redParticleEmitter.glowWidth = 0.5;
//    
//    //set size to 20px x 20px
//    //redParticleEmitter.size = CGSizeMake(10, 10);


    SKSpriteNode *redParticleEmitter = [SKSpriteNode spriteNodeWithImageNamed:@"RedFlowParticles"];

    //set size to 20px x 20px
    redParticleEmitter.size = CGSizeMake(10, 10);

    SKPhysicsBody *redParticleEmitterPB = [SKPhysicsBody bodyWithCircleOfRadius:redParticleEmitter.frame.size.width/2];
    redParticleEmitterPB.categoryBitMask = kRedParticleCategory;
    redParticleEmitterPB.collisionBitMask = kRedParticleCategory;
    redParticleEmitterPB.contactTestBitMask = kRedParticleCategory | kInvisbleWallCategory;

    //set this to 5% of the width of the scene
    redParticleEmitter.position = CGPointMake(self.frame.size.width*0.05, self.frame.size.height);
    redParticleEmitter.physicsBody =redParticleEmitterPB;
    redParticleEmitter.name = @"RedParticle";

    [self addChild:redParticleEmitter];



}

- (void)selectNodeForTouch:(CGPoint)touchLocation {
    //1
    SKSpriteNode *touchedNode = (SKSpriteNode *)[self nodeAtPoint:touchLocation];

    //2
    if(![_leverNode isEqual:touchedNode]) {

        [_leverNode removeAllActions];
        [_leverNode runAction:[SKAction rotateToAngle:0.0f duration:0.1]];

        _leverNode = touchedNode;
        //3
        if([[touchedNode name] isEqualToString:kLever]) {
            SKAction *sequence = [SKAction sequence:@[[SKAction rotateByAngle:degToRad(-4.0f) duration:0.1],
                                                      [SKAction rotateByAngle:0.0 duration:0.1],
                                                      [SKAction rotateByAngle:degToRad(4.0f) duration:0.1]]];
            [_leverNode runAction:[SKAction repeatActionForever:sequence]];
        }
    }

}


float degToRad(float degree) {
    return degree / 180.0f * M_PI;
}

- (CGPoint)boundLayerPos:(CGPoint)newPos {
    CGSize winSize = self.size;
    CGPoint retval = newPos;
    retval.x = MIN(retval.x, 0);
    retval.x = MAX(retval.x, -[self size].width+ winSize.width);
    retval.y = [self position].y;
    return retval;
}

- (void)panForTranslation:(CGPoint)translation {
    CGPoint position = [_leverNode position];
    if([[_leverNode name] isEqualToString:kLever]) {
        [_leverNode setPosition:CGPointMake(position.x + translation.x, position.y + translation.y)];
    }
//    else {
//        CGPoint newPos = CGPointMake(position.x + translation.x, position.y + translation.y);
//        [_background setPosition:[self boundLayerPos:newPos]];
//    }
}


# pragma mark -- SKPhysicsContactDelegate Methods

- (void)didBeginContact:(SKPhysicsContact *) contact {

    if (([contact.bodyA.node.name isEqualToString:@"RedParticle"] && [contact.bodyB.node.name isEqualToString:@"NonWall"]) ||
        ([contact.bodyB.node.name isEqualToString:@"RedParticle"] && [contact.bodyA.node.name isEqualToString:@"NonWall"])) {

        //NSLog(@"Red particle Hit nonwall");

        //contact.bodyA.node.physicsBody.pinned = YES;
        //once red particle passes the invisible wall we need to stop it from going back through the wall


    }
}


- (void)didEndContact:(SKPhysicsContact *) contact {
    //NSLog(@"didEndContact called");

    if (([contact.bodyA.node.name isEqualToString:@"RedParticle"] && [contact.bodyB.node.name isEqualToString:@"NonWall"]) ||
        ([contact.bodyB.node.name isEqualToString:@"RedParticle"] && [contact.bodyA.node.name isEqualToString:@"NonWall"])) {
       //NSLog(@"Red particle left");

        contact.bodyB.collisionBitMask = kRedParticleCategory | kInvisbleWallCategory;
        //once red particle passes the invisible wall we need to stop it from going back through the wall


    }
}


@end

2 个答案:

答案 0 :(得分:1)

试试这个:

  1. 在屏幕上创建一个额外的精灵节点,以显示所有静态球的整体(如下所述)。
  2. 创建一个CGPoint数组,以跟踪停止的所有球的位置。
  3. 定期检查所有活动球精灵,看看哪些已经停止。
  4. 对于已经停止的每个球,从场景中移除该srpite,然后将其位置(CGPoint)添加到#2中描述的阵列。
  5. 在阵列中的每个位置渲染由一个球实例组成的图像,并将该图像(纹理)指定给#1中描述的精灵节点。
  6. 回到#3并重复。
  7. 注意:我暂时没有使用过SpriteKit而且我不确定如何实现第5点,但它不应该太难。 SKEffectNode有一个选项(shouldRasterize)来缓存它的外观-i.e.,渲染一次并在所有后续帧上重复使用相同的图像。

    关于步骤#3中描述的“常规间隔”,实际值(例如,每10帧)将取决于您的测量性能和实际游戏的动态;你需要自己找到它。如果它太频繁,反复渲染静态球纹理的开销将导致性能损失。太远了,你将花费更多的帧而不是必要的渲染许多静止的,单独的精灵本来可以“分组”。

    替代解决方案:

    当每个球变为静态时,您可以将它们移动到不同的容器节点(作为它的子节点)而不是从屏幕中移除精灵,而是将该节点光栅化而不是每帧重新渲染。

    这会将每个球保持为单独的SKSpriteNode实例(即使是停止的实例)并允许使用SpriteKit物理实体(不确定具有不同父项的精灵是否可以相互碰撞。从未使用过SpriteKit物理)。

    在任何情况下,由于碰撞检测导致的性能下降会随着球的数量而增加,而不管您是否每帧都画出它们。 我不确切知道SpriteKit的物理特性究竟做了什么(例如,prunning等),但n对象之间碰撞的天真方法是针对每个其他对象测试每个对象,所以最坏的情况是O(n ^ 2)

    最后的想法:

    因为你可以安全地假设静止球不再移动,所以静止球的“组”始终保持相同的形状(直到新球停止并添加,即)。 理想情况下,您可以计算“包络”(可能是非凸多边形,带有圆角)并对移动球进行碰撞测试。仍然不是一项微不足道的任务,但至少它可以帮助你跳过对组内部静态球的碰撞测试,它们永远不应该碰撞(它们被组中边界的球“屏蔽”)。

答案 1 :(得分:1)

这里你的问题是所有那些物理实体,你有1000个精灵检查1000个其他精灵,无论它们是否需要碰撞。一种可以让它更快一点的方法是将屏幕分成子集并让你的节点进行碰撞检测只检查周围的邻居象限和它自己的精灵。例如。将屏幕分为9个部分,左上部分有自己的位掩码,只能与左上角,中间顶部,中间中心和中心部分的精灵相撞。如果此精灵移动到中间顶部,其类别将变为中间顶部,并且仅检查左上角,中间顶部,右上角,左中心,中间中心和右中心的精灵。节点越少检查就越好。