我正在使用 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
最佳答案
试试这个:
- 在屏幕上创建一个额外的 sprite 节点以整体显示所有静态球(如下所述)。
- 创建一个
CGPoint
数组来跟踪所有停止的球的位置。 - 定期检查所有事件的球 Sprite ,看看哪些已经停止。
- 对于每个停止的球,从场景中移除该 srpite,并将其位置 (
CGPoint
) 添加到 #2 中描述的数组中。 - 在数组中的每个位置渲染一个由一个球实例组成的图像,并将该图像(纹理)分配给#1 中描述的 Sprite 节点。
- 回到#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/