I'm developing a game in iOS Spritekit using Objective C. I created a menu view in viewController and from that I'm calling a gameScene. In gameScene I uses five different nodes which has to released one by one randomly from the top of the screen. Each of those five nodes have 4 different images so it is stored in atlas. If the node moves to the bottom of the screen I change the image in the node and release from the top of the screen.
I'm releasing the node using the following code
self.wait = [SKAction waitForDuration: 1.0];
self.run = [SKAction runBlock:^{
if ([nodesArray count] > 0) {
//select the node to release randomly and remove it from array
}
}];
self.seq = [SKAction sequence: #[self.wait, self.run]];
Those five nodes are stored in an array called nodesArray.
Code for selecting the random node :
int i = arc4random() %[nodesArray count];
SKSpriteNode *mynode = nodesArray[i];
For removing the node in array :
[nodesArray removeObjectAtIndex:i];
After for setting the position :
mynode.position = CGPointMake(x, y)
and for velocity :
mynode.physicsBody.velocity = CGVectorMake(dx,dy)
And finally add the node to scene :
[self addChild: mynode];
Repeat these steps until the array count is 0.
Now my problem is that I noticed a stutter when the nodes are released from the array. I don't know why these stutter are occurs as it's random, these predominantly occurs when the scene is restarted or If I come from another scene to game scene. When I come directly to GameScene without any intermediate scene with just a Launchscreen.xib , there is no stutter.
For restarting the scene I call the scene by the following code
GameScene *firstScene = [GameScene sceneWithSize:self.size];
[self.view presentScene:firstScene transition:[SKTransition fadeWithDuration:1.0]]
PS. I also tried implementing Menu as a scene and returning to game scene, nevertheless it too causes random stutter in the game scene irrespective of whether I remove the Node's & Actions before I leave the Game Scene.
Already added a log statement to know how many nodes are available in array.
NSLog(#"Nodes Count is %lu and Random Number is %lu",(unsigned long)[nodesArray count],(unsigned long)i);
After removing the node, the array count will be one less than normal count.
NSLog(#"Nodes Count is %lu",(unsigned long)[nodesArray count]);
After removing all nodes, the method won't call so it wouldn't print any log inside this method.
Log Output :
2014-12-04 11:17:31.171 newGame[633:21010] Nodes Count is 5 and Random Number is 2
Now the second node in the array will be called. After removing the second node in array the array count will be shown in the next log.
2014-12-04 11:17:31.171 newGame[633:21010] Nodes Count after removal of node : 4
This is the bug which is stopping me from releasing the game, so any help will be appreciated.
Related
i have this method that makes the Lines
lineaGuida_Img = [SKTexture textureWithImageNamed:#"LineaGuida copia.jpeg"];
_Linea = [SKNode node];
_Linea.position = CGPointMake((self.frame.size.width / 7 ) * 2, self.frame.size.height / 2 );
_Linea.zPosition = -10;
SKSpriteNode * linea = [SKSpriteNode spriteNodeWithTexture:lineaGuida_Img];
linea.position = CGPointMake(0,0);
linea.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:linea.size];
linea.physicsBody.dynamic = FALSE;
linea.physicsBody.collisionBitMask = 0;
linea.physicsBody.categoryBitMask = CollisionEnemy;
[_Linea addChild:linea];
[self addChild:_Linea];
In the touchesBegan method when i touch the screen, the player is always on the center of screen, and those lines behind him have to move by -x.
[_Linea runAction:[SKAction moveByX: -(self.size.width/8) y:0 duration:0.1]];
[self spawn_Lines];
But this action is only executed by the last SKNode. I need to apply this to all Lines simultaneously. After that, when the line's position is less then the player's position, the line must be deleted.
SpriteKit uses a tree based model, so whatever the parent node does, the children fall suit. Create an SKNode to house all of your lines, add all of your lines to this SKNode, then apply the actions to the SKNode, not the lines.
Add them to NSMutableArray to keep the reference and use for each loop to make each one run the action. I would recommend you to use NSTimer to provide [SKAction moveByX: -1 y:0 duration:0]] with constant speed which will result in the same motion as you already used. Each time this NSTimer execute you will check all positions of object from your NSMutableArray and than delete it if it fits your conditions. Be careful when you want to lose the reference completely use [object removeFromParent]; and remove it also from your NSMutableArray to avoid lose of performance later on. For this I use rather forLoop with continue method
Someone please push me in the right direction.
Here's the thing: what I'm trying to do is to limit the amount of projectiles (shot by player) that can be on screen. That limit is recorded in a constant. So while the number of projectile nodes on the screen is equal to that constant, the player will not be able to shoot any more.
Once a projectile goes off screen, that projectile is getting removed, decreasing the amount of on-screen nodes, and the player should be able to shoot again, of course without exceeding the limit. (The shots are being done using a push of a button, not tapping the screen in the direction you want to shoot, if that is important. You can only shoot one at a time.)
What kind of algorithm can I use to solve this? How to keep up the number of projectiles currently on-screen? How to let the system know WHEN to decrease that amount (I've been using collision detection, having a "screen bounds node") and WHAT node to remove (I've been recording node ID in the name, but looks like doing that the wrong way)? Please suggest a solid solution to this. Hope what I have requested is possible.
You have made two claims here:
A projectile is created when shot by a player.
A projectile is deleted when it goes off screen.
If you already have code removing the projectile when it goes off screen, you could just leverage a simple static count:
#implementation Projectile
static NSUInteger projectileCount = 0;
+ (instancetype)fireProjectile {
if (projectileCount < MAX_PROJECTILES) {
projectileCount++;
return [[Projectile alloc] init];
}
return nil;
}
- (void)dealloc {
projectileCount--;
}
#end
Not sure if I understand exactly what you want, but based on what I have read you could have an array of projectiles that contains the amount of projectiles you want to allow.
// create an array
NSMutableArray *projectiles = [NSMutableArray array];
// populate the array with your projectiles
for (int index = 0;index < kMaxProjectiles;index++)
{
Projectile *projectile = [[Projectile alloc]init];
[projectiles addObject:projectile]
}
Now whenever you want to shoot a projectile, remove it from that array and shoot it.
Projectile *projectile = [projectiles objectAtIndex:0];
[projectiles removeObjectAtIndex:0];
When it goes off the screen, remove it from the parent and push it right back into the array.
[projectile removeFromParent];
[projectiles addObject:projectile];
It's basically a pooling system where you would create your projectiles once and recycle them. When choosing to shoot, you can just check the count of the array to determine if you have a projectile available :
if (projectiles.count > 0)
{
// yep, there are projectiles in the pool, fire away
}
Added advantage of pooling is that you are not constantly creating and killing your projectiles, which will result in pauses when Garbage Collection occurs or if you ever tried to create alot of projectiles at once.
i am copying a sprite from one NSMutableArray to another, but when i delete the CCSprite from the first NSMutableArray then it is also deleted in the second Array.
How can i prevent this?
In the init method the Arrays are initialized like below.
spriteTempArray = [[NSMutableArray alloc] init] ;
myPowerUpArray = [[NSMutableArray alloc] init] ;
This is the first method were the sprite is placed on screen and animated somewhere on the screen
CCSprite *powerUpSprite = [[CCSprite alloc] initWithFile:SpriteFileName ] ;
powerUpSprite.position = ccp(xcenter,ycenter);
powerUpSprite.scale = 0;
[self addChild:powerUpSprite z:20 tag:puTag];
[spriteTempArray addObject:powerUpSprite];
id zoomIn = [CCScaleTo actionWithDuration:0.2 scale:1] ;
id moveTo = [CCMoveTo actionWithDuration:0.2 position:ccp(winSize.width/2,120)];
[powerUpSprite runAction:zoomIn];
[powerUpSprite runAction:moveTo];
Then when de Sprite is touched, it's moved to the corner of the screen and also stored in another NSMutableArray (myPowerUpArray). But the delete action erases the sprite in both arrays.
CCSprite *powerUpSprite = [spriteTempArray objectAtIndex:0];
id zoomOut = [CCScaleTo actionWithDuration:0.2 scale:0.35] ;
id moveTo = [CCMoveTo actionWithDuration:0.2 position:ccp(winSize.width - 24,winSize.height -31 * myPowerUps -80)];
[powerUpSprite runAction:zoomOut];
[powerUpSprite runAction:moveTo]
[myPowerUpArray addObject:powerUpSprite];
[self deleteSpriteTempArray];
Below the sprite delete method.
-(void)deleteSpriteTempArray{
NSMutableArray *filesToRemove = [[NSMutableArray alloc] init];
for ( id obj in spriteTempArray) {
[filesToRemove addObject:obj];
[self removeChild:obj cleanup:YES];
}
[spriteTempArray removeObjectsInArray:filesToRemove];
}
I haven't observed the Array, other than de sprite is disappearing from the screen.
Your delete array method is odd. To begin why are you calling:
[self removeChild:obj cleanup:YES];
If your intent is not to remove the sprite from the scene? You are telling your code here to remove the sprite from the scene and to clean it up, but based on your post I don't get the impression you actually want that to happen at that moment. If it is your intent that it gets removed after it moves to the corner of your screen, then you should do the move and scale in a CCSpawn (let's simply call it moveScale) and in a CCSequence you should be adding that moveScale action followed by a block action that calls a method on the sprite to remove itself from its parent with cleanup:
CGSize winSize = [[CCDirector sharedDirector] winSize];
CCSprite* powerUp = ...;
float duration = 0.2f;
float desiredScale = 1.0f;
CGPoint desiredPosition = ccp(winSize.width / 2,120);
id zoomIn = [CCScaleTo actionWithDuration:duration scale:desiredScale] ;
id moveTo = [CCMoveTo actionWithDuration:duration position:desiredPosition];
id remove = [CCCallBlock actionWithBlock:^
{
[powerUp removeFromParentAndCleanup:YES];
}];
id moveScale = [CCSpawn actions:zoomIn, moveTo, nil];
id moveScaleRemove = [CCSequence actions:moveScale, remove, nil];
[powerUp runAction:moveScaleRemove];
Secondly why do you have a "files to remove" array in your deletion method that is built with objects to remove? That "files to remove" array is adding all objects from the sprite temp array to it, so it seems a little pointless. Just remove all objects from your sprite temp array. No need to build another temp list with everything to remove, just to essentially be saying remove everything from my original temp array list. That would only be useful if you were removing only SOME of the objects in the temp array. Since you are removing all, the "files to remove" array doesn't serve a purpose. Or at least that is what your code is showing that you are doing. Whether you intend on that behavior is another question.
Also if you have two arrays and an object in both, removing it from one array wouldn't have an impact on the second.
Another issue with your code is in the first code block. If you intend on running a move and a scale action together, they need to be executed via a CCSpawn. Instead you are telling it to scale, then immediately telling it to move which therefore it will be moving but not scaling as it moves. In other words both are not occurring at the same time. On the other hand if you intend for them to be done in sequence, you should be executing them via a CCSequence.
Another thing I noticed about your code is why are you deleting the entire sprite temp array when only one sprite was touched? Did you intend to only remove that one sprite that was touched? Why not just remove that one object, unless I am missing something?
[spriteTempArray removeObject:thisPowerUpIJustTouched];
A final issue I see is with your naming. Your code example indicates that the order of each example code block is the order in which events are occurring right? So why is the initial array you add powerups to called spriteTempArray while the array you add objects to after it is touched is called powerUpArray? Based on the code you've provided and the order they are shown, there is no point to the spriteTempArray. And even if there was, the naming looks backwards. Unless of course I'm missing something. It looks like your intention is to have a power ups array and when a power up is touched it is added to another array to mark that it is to be deleted. But as I've shown earlier in this post, it doesn't look like that second array has a true purpose since your desired behavior is A) not being done and B) would be done using the CCSpawn/CCSequence combo I showed earlier.
Hope this helped. I just woke up so hopefully I addressed your issue properly.
In an attempt to further familiarize myself with SpriteKit, I made a simple little app that would spawn a random (3 choices) ball at whatever location you click or touch (which automatically despawn after 7 seconds), then apply physics and gravity, and finish with manipulating gravity at set intervals. It was simple enough and worked quite well. However, I decided to add sound to it so the balls would make noise when hitting both the walls or each other.
While it worked, it brought up an issue I couldn't figure out on my own: the soundless version would lag if you spawned so many balls it couldn't handle it, but the sound version continued to lag thereafter. I checked out Xcode's trusty Debug Navigator and found that the memory of the app continued to expand with every ball added to the scene. The soundless version didn't expand on the memory NEARLY as much and additionally reclaimed some bits after the balls had been removed from their parents.
I can't help but think there was something wrong in my implementation that's not only keeping the sound files around in memory after they've been used, but keeping multiples of each file.
I'll happily share my project with anyone who requests, but this is the approximation of the methods that happen:
initialization, sound actions are created and stored in the scene class so they can be accessed later:
#interface REP_Balls () {
SKAction* ballSound01;
}
#end
#implementation REP_Balls {
...
...
...
-(void) setUpSounds {
NSArray* array = #[ #"ball_hit_01.wav",]; //I have more in the array, I'm just simplifying code for stackoverflow
ballSound01 = [SKAction playSoundFileNamed:[array objectAtIndex:0] waitForCompletion:NO];
}
I then have a contact listener activate a method that randomizes from the available sound actions and returns one of them:
-(void) didBeginContact:(SKPhysicsContact *)contact {
SKPhysicsBody *firstBody, *secondBody;
firstBody = contact.bodyA;
secondBody = contact.bodyB;
SKAction* randomSound;
//blah blah blah if categories match and whatnot, do this:
REP_BallSpawn* ball = (REP_BallSpawn*) firstBody.node;
randomSound = [self soundBankRandomizer:YES];
[ball runAction:randomSound];
And the method that randomizes:
-(SKAction*) soundBankRandomizer:(BOOL)isWallHit {
NSArray* array;
switch (isWallHit) {
case YES:
array = #[ //according sound actions
];
break;
case NO:
array = #[
ballSound01, //and other sound actions
];
default:
array = #[
//if neither case is true somehow, just choose from a bank of all sound actions
];
break;
}
int randomChoice = arc4random() % [array count];
SKAction* sound = (SKAction*)[array objectAtIndex:randomChoice];
return sound;
}
Beyond that, after 7 seconds the balls automatically despawn themselves, and I would assume any children with them (which I also assume should include actions, such as sound actions).
I even made a method to confirm no nodes are sticking around after they despawn:
[self enumerateChildNodesWithName:#"//*" usingBlock:^(SKNode *node, BOOL *stop) {
string = [NSString stringWithFormat:#"%# (())(()) %#", string, node];
}];
The result is then copied to the clipboard which results in a list of all nodes and children in the scene at the moment. (the " (())(()) " is in there for a unique pattern to search for in find/replace to make new lines)
Edit: I also noticed that CPU usage remains high after everything has despawned as well.
Please tell me I did something wrong!
===================================================
So I made some changes to my code. As mentioned below, I got it to work right, but upon some further testing, it appears to be finicky as to how exactly it gets implemented. Maybe someone else will see why, but it makes no sense to me.
METHOD THAT WORKS
Same as above with these changes:
-(void) setUpSounds {
NSArray* array = #[
#"ball_hit_01.m4a",
#"ball_hit_02.m4a",
#"ball_hit_03.m4a",
#"ball_hit_04.m4a",
#"wall_hit_01.m4a",
#"wall_hit_02.m4a",
#"floor_hit_01.m4a",
#"floor_hit_02.m4a",
];
SKAction* ballSound01 = [SKAction playSoundFileNamed:[array objectAtIndex:0] waitForCompletion:NO];
// etc
sounds = [NSArray arrayWithObjects:ballSound01, ballSound02, ballSound03, ballSound04, wallSound01, wallSound02, floorSound01, floorSound02, nil]; //is declared class wide
}
-(SKAction*) soundBankRandomizer:(BOOL)isWallHit {
int randomChoice = arc4random() % ([sounds count] / 2);
if (isWallHit) {
randomChoice += ([sounds count] / 2);
}
return [sounds objectAtIndex:randomChoice];
}
The contact listener is nearly identical (I removed the casting for the ball and applied the action to the world node instead). I've also tried it using a dictionary instead of an array and it works fine.
METHOD THAT DOESN'T WORK FOR SOME REASON
I would remove the soundBankRandomizer method, as well as remove any action calls from the contact listener. Instead, I added this method:
-(void)playSound:(BOOL)isWallHit {
int randomChoice = arc4random() % ([sounds count] / 2);
if (isWallHit) {
randomChoice += ([sounds count] / 2);
}
SKAction* randomSound = [sounds objectAtIndex:randomChoice];
[bgNull runAction:randomSound];
}
and then called it from the contact listener via:
[self playSound:YES]; //for wall hit
[self playSound:NO]; //for ball on ball action
For some reason, this resulted in the exact same performance issues I had initially. Does it make sense to anyone?!
I see a couple issues....By declaring the SKAction ballSound01 it'll stick around to be used again. Also I follow your thinking that by removing every node, it would remove every action, but again I think by declaring it, that won't happen. Also SKActions could be applied to multiple nodes, so I don't think you can really consider them children of the object they run on.
I think you are better off declaring an array which holds onto those SKActions, instead of creating it each time in the soundBankRandomizer.
Also I've never seen a switch statement with a bool variable like that. Just doing this...
if (isWallHit == YES) {
} else{
}
Is more to the point because the default block is never going to run anyway. Since isWallHit is either YES or NO, those two conditions are it, no need for the default.
You might want to consider making a Singleton class as a sound manager and keeping it totally separate from the nodes. So instead of calling...
randomSound = [self soundBankRandomizer:YES];
[ball runAction:randomSound];
You could call something like...
[[SoundManager sharedSounds] playRandomWallSound];
No need to make a specific node part of playing the sound.
I've been developing a game in Cocos2D for about 3 years which utilizes a transparent background to show a UIView. The reason for this is to have the parallax background still run as Cocos2D does scene transitions.
I'm having a new issue that started when I updated to iOS 7. Slow down occurs in combination of these circumstances:
-ONLY if the parallax background's frame position has changed.
-If I destroy an enemy which emits small sprites and a particle effect.
So it's the combination of those two things and it only happens sometimes. The debug value of the frame rate does not dip when the slow down happens. If I load a new scene it goes back to normal. Sometimes when I destroy another enemy the slow down disappears as well.
I have code in my parallax UIView that runs just about every frame of in-gameplay. I summed down the issue to one line:
-(void)updateImagePosWithPos:(CGPoint)pos{ // in game
// create vel based on last currentPos minus new pos
CGPoint vel = CGPointMake(currentPos.x-pos.x, currentPos.y-pos.y);
// init variables tmpVel and tempTotalImages
CGPoint tmpVel = CGPointZero;
int tmpTotalImages = 0;
// create indexLayerArr
NSMutableArray *indexLayerArr = [NSMutableArray array];
// for every parallax layer, add the number of images horizontally minus 1 to indexLayerArr
for (int j=0; j<totalLayers; ++j){
[indexLayerArr addObject:[NSNumber numberWithInt:[[totalImagesArr objectAtIndex:j] intValue]-1]];
}
int i = 0;
for (UIImageView *imageView in self.subviews) {
CGRect tmpRect = CGRectZero;
NSMutableArray *tmpRectArr = [rectContainer objectAtIndex:imageView.tag];
float speed = 0.00;
tmpTotalImages = [[totalImagesArr objectAtIndex:imageView.tag] intValue];
speed = [[speedArr objectAtIndex:imageView.tag] floatValue];
tmpVel = CGPointMake(vel.x*speed, vel.y*speed);
i = [[indexLayerArr objectAtIndex:imageView.tag] intValue];
tmpRect = [[tmpRectArr objectAtIndex:i] CGRectValue];
if(tmpRect.origin.x - tmpVel.x > wins.width){
tmpRect.origin.x -= (tmpTotalImages)*tmpRect.size.width;
}
else if(tmpRect.origin.x - tmpVel.x < -tmpRect.size.width){
tmpRect.origin.x += (tmpTotalImages)*tmpRect.size.width;
}
tmpRect.origin.x -= tmpVel.x;
tmpRect.origin.y += tmpVel.y;
[tmpRectArr replaceObjectAtIndex:i withObject:[NSValue valueWithCGRect:tmpRect]];
imageView.frame = [[tmpRectArr objectAtIndex:i] CGRectValue]; // <-- slow down cause
i--;
[indexLayerArr replaceObjectAtIndex:imageView.tag withObject:[NSNumber numberWithInt:i]];
}
currentPos = CGPointMake(pos.x, pos.y);
}
See commented line imageView.frame = [[tmpRectArr objectAtIndex:i] CGRectValue];
So if I comment that line out, the problem will never happen. If I keep the line and as well as don't change the values of tempRect, the problem also won't happen.
It looks like there's an issue in iOS 7 in changing the UIImageView's frame position, but only sometimes. Just wondering what other alternatives could I use? Or am I doing something definitely wrong in iOS 7?
Not a solution to your problem but workarounds. I'll start with the one that's probably requires the most code changes.
You don't actually have to have a UIView in order to keep background with transitions. Instead if you implement the background entirely in cocos2d (as part of the scene), you can achieve the same effect if instead of replacing scenes you transition layers in and out. Scene transitions for the most part use the same actions that also work on nodes.
Implement the background using cocos2d nodes, and have one parent node acting as the container (ie "layer") of the background nodes. You can do one of two things with that node:
a. Edit CCDirectorIOS's code and add a reference to your background node. Update the node before all other nodes in the drawScene method, by calling visit on the background node just before [_runningScene visit].
b. When transitioning to a new scene, either remove the background node from the current scene and add it to the new scene, or create a copy of the background with all the same settings and add it to the new scene. Ensure the copy starts with the exact same state as the original. Though this won't work with most transitions due to the nature of their animation (ie move/flip/zoom).
If you need the background to animate while a transition is running, there's a simple trick. Schedule update on a non-CCNode object that has global lifetime (ie AppDelegate). Then manually send the update to all nodes that should continue to update their state during a transition, and only during a transition.
You can register updates on non-node objects like this:
[_director.scheduler scheduleUpdateForTarget:self priority:0 paused:NO];
This update method will be called even during scene transitions. Alternatively it should also be possible to continue updating nodes by changing their paused state and thus resuming scheduler and actions, either by overriding the paused property or by explicitly unpausing specific nodes when a transition occurs.