How to Know when a Sprite Effect is Done - objective-c

I am running a sprite effect in an SKScene at certain points. When the effect is not running I want to pause the scene as otherwise it takes up additional CPU even with no effect running. I was not able to find a delegate that tells me when the effect stops rendering so that I can pause the scene again. I could brute force it by having it wait a safe time, but is there a more elegant way to know when the effect is finished?
Setup in viewWillAppear:
self.skScene = [EffectsScene sceneWithSize:self.view.frame.size];
[self.skScene setBackgroundColor:[UIColor whiteColor]];
[self.skView presentScene:self.skScene];
[self.skView setPaused:YES];
Method that gets called to run the effect at a specific time:
-(void)deleteEffect
{
if (shouldRunDeleteEffect)
{
[self.skScene smokeEffectAtX:self.view.frame.size.width/2 andY:0];
[self.skView setPaused:NO];
shouldRunDeleteEffect = NO;
}
}
The actual scene details:
-(void)smokeEffectAtX:(float)x andY:(float)y
{
SKEmitterNode *emitter = [NSKeyedUnarchiver unarchiveObjectWithFile:[[NSBundle mainBundle] pathForResource:#"SmokeEffect" ofType:#"sks"]];
emitter.position = CGPointMake(x,y);
emitter.numParticlesToEmit = 1000;
[self addChild:emitter];
}

This method creates an SKEmitterNode, calculates how long the emitter will take to run based on its properties and the duration parameter, and adds an SKAction to the emitter to remove it from the scene after it is done running.
- (void) newSmokeNodeAtPosition:(CGPoint)position withDuration:(NSTimeInterval)duration
{
SKEmitterNode *emitter = [NSKeyedUnarchiver unarchiveObjectWithFile:[[NSBundle mainBundle] pathForResource:#"SmokeEffect" ofType:#"sks"]];
emitter.position = position;
// Calculate the number of particles we need to generate based on the duration
emitter.numParticlesToEmit = duration * emitter.particleBirthRate;
// Determine the total time needed to run this effect.
NSTimeInterval totalTime = duration + emitter.particleLifetime + emitter.particleLifetimeRange/2;
// Run action to remove the emitter from the scene
[emitter runAction:[SKAction sequence:#[[SKAction waitForDuration:totalTime],
[SKAction removeFromParent]]]
completion:^{
// Add code here to run when emitter is done
self.scene.paused = YES;
}];
[self addChild:emitter];
}

Related

Completion Blocks Objective-C

I'm trying to write a completion handler in IOS with a block and am not sure exactly why it's not working.
This is what I have so far
typedef void(^myCompletion)(BOOL);
-(void) showAnswer {
[self showAnswer:^(BOOL finished) {
if (finished) {
LnbScene *scene = (LnbScene *)gameScene.lnbScene;
//once the tiles position have been updated then move to the next riddle
[scene nextRiddle];
}
}];
}
// This method should update the position of tiles on the scene
-(void) showAnswer:(myCompletion) compblock{
LnbScene *scene = (LnbScene *)gameScene.lnbScene;
NSArray *tilesArray = [scene.tilesBoundary children];
for (Tile *tile in tilesArray) {
if (tile.positionInAnswer != 17) {
[tile removeFromParent];
[scene.spacesBoundary addChild:tile];
CGPoint targetPoint = CGPointMake(tile.targetPoint.x, tile.targetPoint.y + 6);
tile.position = targetPoint;
}
}
compblock(YES);
}
And I am calling the showAnswer method from a Scene as follows:
GameViewController *gVC = (GameViewController *)self.window.rootViewController;
[gVC showAnswer];
As I step through the code I don't encounter any errors and the sequence proceeds as expected ie. The tile positions are changed and then the completion handler triggers to the nextRiddle method. Only problem is that none of the interface is updated. Is there something I am missing?
I've tested that the tile repositioned code works by taking out the completion block and I view it in the interface and get the desired results. So I'm pretty sure the problem lies in how I'm implementing the blocks. I've never written a block before so I'm really in the dark.
-(void) showAnswer {
LnbScene *scene = (LnbScene *)gameScene.lnbScene;
NSArray *tilesArray = [scene.tilesBoundary children];
for (Tile *tile in tilesArray) {
if (tile.positionInAnswer != 17) {
[tile removeFromParent];
[scene.spacesBoundary addChild:tile];
CGPoint targetPoint = CGPointMake(tile.targetPoint.x, tile.targetPoint.y + 6);
tile.position = targetPoint;
}
}
}
I think the problem seems to be a conceptual mistake - that the "flow" of code
in the -(void) showAnswer:(myCompletion) comp block method truthfully represents the flow of UI elements in time in the presentation layer.
However in practice it is not that straightforward. The UI layer is rendered with 60 Hz framerate. That means you see 60 (frames) screens per second and contents of each of those screens needs to be calculated in advance, processed, flattened, rendered.
That means 1 frame (screen) cycle/pass lasts approx. 16 milliseconds. If I remember correctly you as a programmer have 11 milliseconds to provide all the relevant code so that it can be processed into visual info. This chunk of code is processed all at once which is the answer to our problem.
Imagine if you had a method that would say
{
//some other UI code...
self.view.backgroundColor = [UIColor greenColor];
self.view.backgroundColor = [UIColor yellowColor];
//some other UI code...
}
Now imagine we are inside 1 such 16 millisecond pass.
This piece of code would be processed in advance before any rendering happens and the resultative background colour will be yellow. Why? Because in the cycle preparation phase this code is processed and the framework interprets the green colour as pointless because the value is immediately overwritten by a yellow one.
Thus instructions for the renderer will contain only the yellow background information.
Now back to your actual code. I think all your code is processed fast enough to fit into that 11 milisec window, and the problem is that you do not really wait for the swapping of tiles because there is that block with YES parameter as part of the method and that one already slides to whatever is next.
So the rendered does get instructions to simply move on the the next thing. And it does not animate.
Try putting this "tile swapping" into the "animation" block
[UIview animateWithDuration:....completion] method..and put your block into the completion block. Yes you will have a block in a block but thats fine.
[UIView animateWithDuration:0.4f
animations:^{
//tile swapping code
}
completion:^(BOOL finished)
{
complBlock(YES);
}];
I am not completely sure if it will work but let's try.
The UI relate method should call on main thread. U can try this:
[self showAnswer:^(BOOL finished) {
if (finished) {
LnbScene *scene = (LnbScene *)gameScene.lnbScene;
dispatch_async(dispatch_get_main_queue(), ^{
[scene nextRiddle];
});
}
}];

Label not changing countdown timer SpriteKit Objective C?

Edit no.2 , Ok, I think I have boiled this right down to the point now. I have used all your advice, and tested with breakpoints, so thank you.
The last bit I need to do, is run this wait action.
if (timerStarted == YES) {
[countDown runAction:[SKAction waitForDuration:1]];
if (countDownInt > 0) {
countDown.text = [NSString stringWithFormat:#"%i", countDownInt];
countDownInt = countDownInt - 1.0;
[self Timer];
}else{
countDown.text = [NSString stringWithFormat:#"Time Up!"];
}
The runAction: section doesn't seem to work. I am guessing this is because I have selected the wrong node to put in the place of the (SKLabelNode "countDown"). Which node could I use to run this code?
Thank you everyone who has helped so far
Here's an example of how to implement a countdown timer in SpriteKit.
First, declare a method that creates 1) a label node to display the time left and 2) the appropriate actions to update the label, wait for one second, and rinse/lather/repeat
- (void) createTimerWithDuration:(NSInteger)seconds position:(CGPoint)position andSize:(CGFloat)size {
// Allocate/initialize the label node
countDown = [SKLabelNode labelNodeWithFontNamed:#"Chalkduster"];
countDown.position = position;
countDown.horizontalAlignmentMode = SKLabelHorizontalAlignmentModeLeft;
countDown.fontSize = size;
[self addChild: countDown];
// Initialize the countdown variable
countDownInt = seconds;
// Define the actions
SKAction *updateLabel = [SKAction runBlock:^{
countDown.text = [NSString stringWithFormat:#"Time Left: %ld", countDownInt];
--countDownInt;
}];
SKAction *wait = [SKAction waitForDuration:1.0];
// Create a combined action
SKAction *updateLabelAndWait = [SKAction sequence:#[updateLabel, wait]];
// Run action "seconds" number of times and then set the label to indicate
// the countdown has ended
[self runAction:[SKAction repeatAction:updateLabelAndWait count:seconds] completion:^{
countDown.text = #"Time's Up";
}];
}
and then call the method with the duration (in seconds) and the label's position/size.
CGPoint location = CGPointMake (CGRectGetMidX(self.view.frame),CGRectGetMidY(self.view.frame));
[self createTimerWithDuration:20 position:location andSize:24.0];
I would not use the update method. Use SKActions to make a timer. For an example
id wait = [SKAction waitForDuration:1];
id run = [SKAction runBlock:^{
// After a second this is called
}];
[node runAction:[SKAction sequence:#[wait, run]]];
Even though this will only run once, you can always embed this in an SKActionRepeatForever if you want to be called every second or whatever time interval.
Source:
SpriteKit - Creating a timer

Objective-C:How to pause during a loop so that the sequence can finish before moving onto the next iteration?

I am making a simon game in objective-c using sprite kit in Xcode. What I am trying to accomplish is to pick random number, add it to the sequence, fadeout, pick next random number...
what happens when I run this code is that all the tiles in the sequence fadeout then back in at exactly the same time.
Here is my code:
-(void) sequence
{
NSMutableArray *sequenceArray = [[NSMutableArray alloc] initWithCapacity:50];
int randomNumber;
SKAction *fadeOut = [SKAction fadeOutWithDuration: 1]; //color of tile fades out
SKAction *fadeIn = [SKAction fadeInWithDuration: 1]; //color of tile fades in
SKAction *pulse = [SKAction sequence:#[fadeOut,fadeIn]]; //holds a sequence so tile fades out then fades in
CFTimeInterval startTime;
float elapsedTime;
for(int i = 0; i<level+1; i++)
{
startTime = CFAbsoluteTimeGetCurrent();
randomNumber=[self getRandomNumber]; //random number is picked
if(randomNumber==0)
{
sequenceArray[i]=blueBox;
}
else if(randomNumber==1)
{
sequenceArray[i]=redBox;
}
else if(randomNumber==2)
{
sequenceArray[i]=yellowBox;
}
else if(randomNumber==3)
{
sequenceArray[i]=greenBox;
}
[sequenceArray[i] runAction:[SKAction repeatAction:pulse count:1]]; //for each tile in the sequence the color fades
//out then back in
elapsedTime = CFAbsoluteTimeGetCurrent()-startTime; //trying to pause the loop so the sequence can finish before the
//next random number is picked
while(elapsedTime<.5)
{
elapsedTime = CFAbsoluteTimeGetCurrent()-startTime;
NSLog(#"elapsed time %f", elapsedTime);
}
}
}
Ok I have tried to implement some of the suggestions below. I still am running into errors, for example now the sequenceArray[index] at index=0 is out of bounds.
Here is my new code:
-(void)doLoop:(int)index
limit:(int)limit
sprite:(SKSpriteNode*) sprite
body:(void(^)(int))body
{
body(index);
index++;
if(index<level+1)
{
SKAction *loopAction;
[self runAction:loopAction completion:^{
[self doLoop:index limit:limit sprite:sprite body:body];
}];
}
}
-(void) sequence
{
NSMutableArray *sequenceArray = [[NSMutableArray alloc] initWithCapacity:50];
int index = 0;
SKAction *fadeOut = [SKAction fadeOutWithDuration: 1]; //color of tile fades out
SKAction *fadeIn = [SKAction fadeInWithDuration: 1]; //color of tile fades in
SKAction *pulse = [SKAction sequence:#[fadeOut,fadeIn]]; //holds a sequence so tile fades out then fades in
[self doLoop:0 limit:level+1 sprite:sequenceArray[index] body:^(int index){
int randomNumber=[self getRandomNumber]; //random number is picked
if(randomNumber==0)
{
sequenceArray[index]=blueBox;
}
else if(randomNumber==1)
{
sequenceArray[index]=redBox;
}
else if(randomNumber==2)
{
sequenceArray[index]=yellowBox;
}
else if(randomNumber==3)
{
sequenceArray[index]=greenBox;
}
[sequenceArray[index] runAction:pulse]; //for each tile in the sequence the color fades
//out then back in
NSLog(#"fadeout");
}];
}
Your attempt to delay your loop with a while statement is busy waiting and if this code is executing on your main thread provides no opportunity for your application to process events during the wait. This is the root cause of your problem.
What you need to do is replace your loop with a sequence of actions, each of which performs the work of one iteration, followed by completion blocks, where each completion block performs the "rest" of the loop. One way to do this is shown in this answer - in that case the need was for a simple delay and so the continuation of the "loop" is scheduled using dispatch_after().
In you case you can replace your calls to runAction: with calls to runAction:completion: to schedule the continuation of your "loop".
HTH
Addendum
Your attempt is a bit mixed up. Here is some untested code type directly into the answer - expect errors!
- (void) doLoop:(int)index
limit:(int)limit
sequenceArray:(NSMutableArray *)sequenceArray
action:(SKAction *)pulse
{
switch ([self getRandomNumber])
{
case 0:
sequenceArray[index] = blueBox; break;
case 1:
sequenceArray[index] = redBox; break;
case 2:
sequenceArray[index] = yellowBox; break;
case 3:
sequenceArray[index] = greenBox; break;
}
[sequenceArray[index] runAction:[SKAction repeatAction:pulse count:1]
completion:^{
// do next "iteration" if there is one
int next = index + 1;
if (next < limit)
[self doLoop:next
limit:limit
sequenceArray:sequenceArray
action:pulse];
}];
}
- (void) chainedSequence
{
NSMutableArray *sequenceArray = [[NSMutableArray alloc] initWithCapacity:50];
SKAction *fadeOut = [SKAction fadeOutWithDuration: 1]; //color of tile fades out
SKAction *fadeIn = [SKAction fadeInWithDuration: 1]; //color of tile fades in
SKAction *pulse = [SKAction sequence:#[fadeOut,fadeIn]]; //holds a sequence so tile fades out then fades in
[self doLoop:0 limit:(level+1) sequenceArray:sequenceArray action:pulse];
}
Make sure you understand what it is doing before using it! The previously referenced answer provides more detail.
The above code passes around and fills up your sequenceArray, however you never use this in the code you provide other than for temporary storage within the method. Do you really need this array? If so remember that it contents will not be completely valid until the after the last "iteration" even though the call to doLoop: in chainedSequence will return before even the first action is complete.
It looks like you're calling this method on the main thread (the same thread which manages all of your application's view drawing). That means that you are queuing up all of your actions and then running them all at one. Your while delay slows down the entire loop but never yields control for these animations to run. None of your view changes will appear until your sequence method returns and the current event loop is able to finish.
Instead of waiting for animations to finish that while is delaying them from starting.
Look at creating an SKAction sequence which exists for exactly this sort of scenario; when you want a number of SKActions to fire sequentially. By adding all of your actions to a sequence and then starting the sequence you save yourself from needing to either run a bunch of callback blocks on the main thread or synchronize some background thread selecting blocks with the main thread animating those selections.

Objective c - adding threading to UIView animation method results in only one animation running

I've written a method, sortAndSlideHandCards(), that moves 6 UIButtons. Each UIButton is moved to the same position. This is done via a for-each loop and the animateWithDuration method being called on each UIButton.
This method is called for a number of players at the same time. Currently the behaviour results in UIButtons from each player moving but only one at a time. No more than one UIButton can move at any time, as if each animation is waiting for whatever animation that is currently running to stop before attempting it's own animation, essentially the code is executed sequentially for each player/UIButton. I hoped threading would help me fix this.
when I added the threading code:
backgroundQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
sortAndSlideHandCardsGroup = dispatch_group_create();
for(Player* player in _playersArray) {
dispatch_group_async(sortAndSlideHandCardsGroup, backgroundQueue, ^(void) {
[player sortAndSlideHandCards];
});
dispatch_group_wait(sortAndSlideHandCardsGroup,DISPATCH_TIME_FOREVER);
I found that only the first UIButton animation is triggered for each player and that the code gets held up in the runloop "while" because "_animationEnd" never gets set as it would appear the second animation never gets going.
I can see the method launching in its own thread
- (void) sortAndSlideHandCards {
NSLog(#"PLAYER:sortAndSlideHandCards");
CGPoint newCenter;
Card* tempCard = nil;
int count = 1;
float duration = 0.2 / _speedMultiplyer;
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
for(Card *card in _handCards) { //move cards in hand to one postion in hand
if(count == 1) {
tempCard = [[Card alloc] init:_screenWidth:_screenHeight :[card getNumber] :[card getCardWeight] :[card getSuit] :[card getIsSpecial]];
[tempCard setImageSrc: _playerNumber :!_isPlayerOnPhone :count : true :_view: _isAI: [_handCards count]];
newCenter = [tempCard getButton].center;
}
_animationStillRunning = true;
if(![[DealViewController getCardsInPlayArray] containsObject:card] ) {
[UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionLayoutSubviews animations:^{[card getButton].center = newCenter;} completion:^(BOOL finished){[self animationEnd];}];
while (_animationStillRunning){ //endAnimation will set _animationStillRunning to false when called
//stuck in here after first UIButton when threading code is in play
[runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
} //endAnimation will set _animationStillRunning to false when called
}
count++;
}
}
When i comment out the threading code each UIButton (Card) will animate one after another.
With the threading code is in play the first UIButton will animate but during the second run through the for-loop, the code will be stuck in the while-loop, waiting for the animation to end. I'm guessing the second animation doesn't even start.
I also tried this for the threading code:
[player performSelectorInBackground:#selector(sortAndSlideHandCards) withObject:nil];
Same outcome
Anyone have any ideas why animateWithDuration doesn't like getting called in a loop when in a thread other than the main one?
You should just be able to kick off the animations you want from whatever UI action is performed. The UIView animateWith... methods return immediately, so you don't need to worry about waiting for them to complete.
If you have an unknown number of animations to kick off sequentially, use the delay parameter. Pseudocode:
NSTimeInterval delay = 0;
NSTimeInterval duration = 0.25;
for (UIView *view in viewsToAnimate)
{
[UIView animateWithDuration:duration delay:delay animations:^{ ... animations ...}];
delay += duration;
}
This will increase the delay for each successive animation, so it starts at the end of the previous one.

How to use UIActivityIndicatorView on custom UIButton?

I want to use UIActivityIndicatorView on my custom UIButton.
Here is my code:
if (sender.tag == 1)
{
// Start animating
activityIndicator.hidden = NO;
[activityIndicator startAnimating];
// Check if the network is available
if ([self reachable]) {
// Stop animating
activityIndicator.hidden = YES;
[activityIndicator stopAnimating];
}
}
What I want to do here is:
Once the user touch the button, I want to start the ActivityIndicatior while Reachable checking the network availability. Once it done pass it to the next view.
Update
UIActivityIndicator is on top of my custom UIButton. It build successfully, but ActivityIndicator is not showing when I touch the button.
I'm sure you had your answer since then.
After 4 years, language has changed to Swift, but I had the same problem : UIActivityIndicatorView is not showing when I add it through :
self.myButton.addSubview(self.myIndicatorView)
I had to climb a level up :
self.myButton.superview!.addSubview(self.myIndicatorView)
Assuming everything else is correct in your project, the problem here is that you're showing and hiding an activity indicator in the same event loop, without giving a pause to draw it. Let me explain:
If you have the code:
UIView* view = someView;
view.backgroundColor = [UIColor redColor];
// various synchronous operations
view.backgroundColor = [UIColor yellowColor];
The view will only ever have a background color of yellow.
To answer your question, you probably want to animate the spinner, do some asynchronous operation, then stop the animation. The key being to not stall on the main event loop while your asynchronous task is running. If your comfortable with blocks it would look something like this:
if (sender.tag == 1) {
// Start animating
activityIndicator.hidden = NO;
[activityIndicator startAnimating];
// Check if the network is available
[self checkReachableWithCallback:^{
// Stop animating
activityIndicator.hidden = YES;
[activityIndicator stopAnimating];
}];
}