ios - 在一个场景中处理数千个 SKSpriteNode

标签 ios sprite-kit skspritenode

我正在使用 sprite kit 构建一个游戏,这是一个将球放入桶中并让桶长大的游戏。随着桶的增长,球(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

最佳答案

试试这个:

  1. 在屏幕上创建一个额外的 sprite 节点以整体显示所有静态球(如下所述)。
  2. 创建一个 CGPoint 数组来跟踪所有停止的球的位置。
  3. 定期检查所有事件的球 Sprite ,看看哪些已经停止。
  4. 对于每个停止的球,从场景中移除该 srpite,并将其位置 (CGPoint) 添加到 #2 中描述的数组中。
  5. 在数组中的每个位置渲染一个由一个球实例组成的图像,并将该图像(纹理)分配给#1 中描述的 Sprite 节点。
  6. 回到#3 并重复。

注意:我已经有一段时间没有使用 SpriteKit 了,我不确定如何实现第 5 点,但应该不会太难。 SKEffectNode 有一个选项 (shouldRasterize) 来缓存它的外观 - 即渲染一次并在所有后续帧上重复使用相同的图像。

关于第 3 步中描述的“定期间隔”,实际值(例如,每 10 帧)将取决于您测量的性能和实际游戏的动态;你需要自己找到它。如果过于频繁,反复渲染静态球纹理的开销将导致性能下降。相距太远,您将花费比必要更多的帧来渲染许多静止的、单独的 Sprite ,否则这些 Sprite 可能会被“分组”。


替代解决方案:

不是在每个球变为静止时从屏幕上移除 Sprite ,而是可以将它们移动到不同的容器节点(作为它的子节点),并让该节点光栅化而不是每帧重新渲染。

这将每个球保持为一个单独的 SKSpriteNode 实例(即使当那些球停止时)并允许 SpriteKit 物理体(但不确定具有不同 parent 的 Sprite 是否可以相互碰撞。从未使用过 SpriteKit 物理)。


在任何情况下,碰撞检测对性能的影响随着球的数量增加,与您是否每帧都绘制它们无关。 我不确切知道 SpriteKit 的物理优化做了什么(例如,修剪等),但是 n 个对象之间碰撞的天真方法是测试每个对象与其他每个对象的对比,所以最坏的情况是 O(n^2) .


最后的想法:

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

关于ios - 在一个场景中处理数千个 SKSpriteNode,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/34603910/

相关文章:

ios - 如何在SKShapeNode中添加灯光效果

ios - PhysicsBody 是零使用 SpriteKit 和 Swift

swift - SKSpriteNode 导致场景变灰

ios - 无法将 View 场景链接到创建的 CustomClass

objective-c - 保留 NSArray 中的元素

ios - 在 Sprite Kit 场景中隐藏 iAd

ios keyed archive Sprite Kit 解码错误 : SKTexture: Error loading image resource: "Missing Resource.png"

ios - SKScene 和内存问题

ios - webkit-背面可见性使Safari和Chrome(IOS)崩溃

ios - 如何在 iOS 中播放来自没有文件扩展名的 URL 的视频?