My spriteKit game work fine at 60fps but when enabling iAd it drops to 15, sometimes 10 or even lower.. and it's a pain, when I die sometimes it takes a while to reset the scene.
I've ran the analyzer on x-code 6 and I get a lot of low importance warnings:
but also this message:
I've added iAd in the implementation of my viewController.m file like this:
#implementation XYZViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//AUDIO BACKground
NSError *error;
NSURL * backgroundMusicURL = [[NSBundle mainBundle] URLForResource:#"spaceprueba160kbps" withExtension:#"mp3"];
self.backgroundMusicPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:backgroundMusicURL error:&error];
self.backgroundMusicPlayer.numberOfLoops = -1;
[self.backgroundMusicPlayer prepareToPlay];
[self.backgroundMusicPlayer play];
// Configure the view.
SKView * skView = (SKView *)self.view;
skView.showsFPS = YES;
skView.showsNodeCount = YES;
//iAd
ADBannerView* adView = [[ADBannerView alloc] initWithFrame:CGRectZero];
// adView.delegate = self;
[adView setFrame:CGRectMake(0, 0, 1024, 768)]; // set to your screen dimensions
[adView setBackgroundColor:[UIColor clearColor]];
[self.view addSubview:adView];
// self.canDisplayBannerAds = YES;
// Create and configure the scene.
SKScene * scene = [XYZWorld1 sceneWithSize:skView.bounds.size];
scene.scaleMode = SKSceneScaleModeAspectFill;
// Present the scene.
[skView presentScene:scene];
}
I know I should un-comment that line -> // self.canDisplayBannerAds = YES;
but it work fine like this and if I un-comment this line of code, I get a screen without iAdd and with my main camera looking a bit upper than the main screen.
My game don't run in this file but in an instance of SKScene.
So my question is if anybody know how I can get back my 60 fps while keeping iAd on my screen.. ?
I have a very heavyweight scene that takes like 5 seconds to load.
I want some loading screen with a spinner to appear while it is waiting to load - what is elegant solution to that?
Should I make intermediate scene and show spinner on it while my scene loads?
Can I alloc and initWithSize scene in background thread?
Here is how I do it now:
MyScene *newScene = [[MyScene alloc] initWithSize:self.size];
SKTransition *transition = [SKTransition flipHorizontalWithDuration:1.0];
[self.view presentScene:newScene transition:transition];
But as soon as I press the button - game freezes for these 5 seconds and provides no feedback for player what is going on.
Tagging this as coco2d since it has very similar API and problems.
Don't do things like this on a background thread. What you can do is show your spinner first, then dispatch_async the actual transition:
// Write code for showing your spinner
dispatch_async(dispatch_get_main_queue(), ^{
MyScene *newScene = [[MyScene alloc] initWithSize:self.size];
SKTransition *transition = [SKTransition flipHorizontalWithDuration:1.0];
[self.view presentScene:newScene transition:transition];
});
This will show your spinner and on the next run loop execute the transition.
Here is my approach:
In Interface Builder add SKView, Loading image, Large White Activity Indicator to GameSceneViewController:
Add outlets for added controls to GameSceneViewController:
#interface GameSceneViewController : UIViewController
#property (nonatomic, weak) IBOutlet SKView *skView;
#property (weak, nonatomic) IBOutlet UIImageView *loadingScreen;
#property (weak, nonatomic) IBOutlet UIActivityIndicatorView *activityIndicator;
#end
In your SKScene add class method, which loads all assets for the scene:
+(void)loadSceneAssetsWithCompletionHandler:(AGAssetLoadCompletionHandler)handler {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
// Load the shared assets in the background.
[self loadSceneAssets];
if (!handler) {
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
// Call the completion handler back on the main queue.
handler();
});
});
}
where AGAssetLoadCompletionHandler is defined in GameScene.h as
/* Completion handler for callback after loading assets asynchronously. */
typedef void (^AGAssetLoadCompletionHandler)(void);
Same approach uses Apple in their Sprite Kit Adventure Game.
Finally, GameSceneViewController's viewWillAppear method:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self.activityIndicator startAnimating];
if (!self.skView.window) {
[self.view addSubview:self.skView];
}
[GameScene loadSceneAssetsWithCompletionHandler:^{
GameScene *scene = [[GameScene alloc] initWithSize:self.skView.bounds.size];
scene.scaleMode = SKSceneScaleModeAspectFit;
[self.skView presentScene:scene transition:[SKTransition crossFadeWithDuration:1]];
[self.activityIndicator stopAnimating];
[UIView animateWithDuration:1.0 delay:0.0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
self.loadingScreen.alpha = 0.0f;
} completion:NULL];
}];
}
I have added iAds to my Sprite Kit game with the following code:
In the viewController.h file
#property (strong, nonatomic) IBOutlet ADBannerView * adBannerView;
In the viewController.m file
- (void)viewWillLayoutSubviews
{
[super viewWillLayoutSubviews];
// Configure the view.
SKView * skView = (SKView *)self.view;
if (!skView.scene) {
// Create and configure the scene.
SKScene * scene = [MenuScene sceneWithSize:skView.bounds.size];
scene.scaleMode = SKSceneScaleModeAspectFill;
_adBannerView = [[ADBannerView alloc] initWithFrame:CGRectZero];
_adBannerView.delegate = self;
[_adBannerView setFrame:CGRectMake(0, 0, 460, 320)];
[self.view addSubview:_adBannerView];
// Present the scene.
[skView presentScene:scene];
}
}
This shows the iAd in every scene. Is there a way to hide the iAd in some of the scenes?
Apple's iAd Programming Guide says:
Only create a banner view when you intend to display it to the user. Otherwise, it may cycle through ads and deplete the list of available advertising for your application.
Is this at all possible with scenes?
yes there is a way to hide the iAd in some of the scenes.
- (void)viewDidLoad
{
[super viewDidLoad];
//Add view controller as observer
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(handleNotification:) name:#"hideAd" object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(handleNotification:) name:#"showAd" object:nil];
// Configure the view.
SKView * skView = (SKView *)self.view;
skView.showsFPS = NO;
skView.showsNodeCount = NO;
// Create and configure the scene.
SKScene * scene = [MyScene sceneWithSize:skView.bounds.size];
scene.scaleMode = SKSceneScaleModeAspectFill;
// Present the scene.
[skView presentScene:scene];
self.canDisplayBannerAds = YES;
adView = [[ADBannerView alloc] initWithFrame:CGRectZero];
adView.frame = CGRectOffset(adView.frame, 0, 0.0f);
adView.delegate=self;
[self.view addSubview:adView];
self.bannerIsVisible=NO;
}
//Handle Notification
- (void)handleNotification:(NSNotification *)notification
{
if ([notification.name isEqualToString:#"hideAd"]) {
[self hidesBanner];
} else if ([notification.name isEqualToString:#"showAd"]) {
[self showBanner];
}
}
And in your scene in which you want to hide banner...
[[NSNotificationCenter defaultCenter] postNotificationName:#"showAd" object:nil];
//Sends message to viewcontroller to show ad.
[[NSNotificationCenter defaultCenter] postNotificationName:#"hideAd" object:nil];
//Sends message to viewcontroller to hide ad.
Well, In your specific scene follow the Apple's guide(same place as your question) on this issue at the link below, look at the section that says "banner view best practices":
https://developer.apple.com/library/ios/documentation/userexperience/conceptual/iAd_Guide/WorkingwithBannerViews/WorkingwithBannerViews.html#//apple_ref/doc/uid/TP40009881-CH4-SW3
In Summary they say: "remove the banner view from the view hierarchy, set its delegate to nil"
The most clean solution is to declare and implement a protocol to let the UIViewController know from the scene that it should hide the ad.
#protocol MySceneDelegate <NSObject>
- (void)hideAd;
#end
#interface MyScene : SKScene
#property (weak) id <MySceneDelegate> delegate;
#end
View controller that shows the scene should implement a hideAd method and set itself as a delegate of the scene. Example:
- (void)viewDidLoad
{
[super viewDidLoad];
// Configure the view.
SKView * skView = (SKView *)self.view;
skView.showsFPS = YES;
skView.showsNodeCount = YES;
// Create and configure the scene.
MyScene * scene = [MyScene sceneWithSize:skView.bounds.size];
scene.scaleMode = SKSceneScaleModeAspectFill;
// Set the delegate
[scene setDelegate:self];
// Present the scene.
[skView presentScene:scene];
}
Then in the scene you can call the hideAd method of the view controller which was set as a delegate:
if ([_delegate respondsToSelector:#selector(closeScene)])
{
[_delegate performSelector:#selector(hideAd)];
}
And remove the banner in hideAd method.
To hide the banner view, you should:
Resize your banner view's frame to be offscreen Resize your content
view's frame to cover the space originally hosting the banner
Hope it helps.
Hello I'm trying to insert in a scene, a cocos 2d project, a movie in the background and a sprite object above it.
I managed to insert the movie and the sprite and interagisono but I can not move the video below spritre as I would like.
The code that I wrote in my scene is as follows:
+(CCScene *) scene
{
// 'scene' is an autorelease object.
CCScene *scene = [CCScene node];
// 'layer' is an autorelease object.
lLevel0 *layer = [lLevel0 node];
// add layer as a child to scene
[scene addChild: layer];
// return the scene
return scene;
}
// on "init" you need to initialize your instance
-(id) init
{
// always call "super" init
// Apple recommends to re-assign "self" with the "super's" return value
if( (self=[super init]) ) {
[self startMovie];
//Creo un oggetto sulla scena
sBall *ball = [sBall Ball];
CGSize size = [[CCDirector sharedDirector] winSize];
ball.position = ccp( ball.contentSize.width/2 , size.height/2 );//Carico la
posizione della palla sulla sinistra (start poosition)
[self addChild:ball z:999];
[ball moveForever];
[self beginDetectingTouch];
}
return self;
}
- (void) startMovie
{
NSString *path = [[NSBundle mainBundle]pathForResource:#"sampleVideo"
ofType:#"mp4"];
NSURL *movieURL = [NSURL fileURLWithPath:path];
MPMoviePlayerViewController *mp = [[MPMoviePlayerViewController alloc]
initWithContentURL:movieURL];
//[[mp moviePlayer] setMovieControlMode:MPMovieControlModeHidden];
// Register to receive a notification when the movie has finished playing.
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(moviePlayBackDidFinish:)
name:MPMoviePlayerPlaybackDidFinishNotification
object:mp];
if ([mp respondsToSelector:#selector(view)]) {
if([MPMoviePlayerControllerinstancesRespondToSelector:#selector(view)]){
// Use the new 3.2 style API
mp.moviePlayer.controlStyle = MPMovieControlStyleNone;
mp.moviePlayer.shouldAutoplay = FALSE;
// This does blows up in cocos2d, so we'll resize manually
// [moviePlayer setFullscreen:YES animated:YES];
[mp.moviePlayer.viewsetTransform:CGAffineTransformMakeRotation((float)M_PI_2)];
CGSize winSize = [[CCDirector sharedDirector]winSize];
mp.moviePlayer.view.frame = CGRectMake(0, 0, winSize.height,
winSize.width); // width and height are swapped after rotation
[[[CCDirector sharedDirector] openGLView]addSubview:mp.moviePlayer.view];
[[[CCDirector sharedDirector] openGLView]sendSubviewToBack:mp.moviePlayer.view];
[mp.moviePlayer play];
}
} else {
// Use the old 2.0 style API
mp.moviePlayer.movieControlMode =MPMovieControlModeHidden;
[mp.moviePlayer play];
}
}
- (void)moviePlayBackDidFinishs:(NSNotification*)notification {
MPMoviePlayerController *moviePlayer = [notification object];
[moviePlayer play];
}
I am using AVFoundation's AVPlayer to play 2 video clips made from 1 longer video (so the end of the first matches the beginning of the second)
When the first video ends and the user taps, I create a new AVPlayer and assign it to my PlayerView, and start playing the second clip.
This all works, however, there is a prominent screen "flicker".
My assumption is that this is caused by the player view removing the first clip and then showing the second clip.
What I need is for this flicker to no appear, so that going between the two clips is seamless.
Do anyone know if there is a way to stop this flickr, either via the AVPlayer* classes, or a way to "fake" it by doing something to make it so this isn't visible.
Thanks
Below is the code of my load and play method:
- (void)loadAssetFromFile
{
NSURL *fileURL = nil;
switch (playingClip)
{
case 1:
fileURL = [[NSBundle mainBundle] URLForResource:#"wh_3a" withExtension:#"mp4"];
break;
case 2:
fileURL = [[NSBundle mainBundle] URLForResource:#"wh_3b" withExtension:#"mp4"];
break;
case 3:
fileURL = [[NSBundle mainBundle] URLForResource:#"wh_3c" withExtension:#"mp4"];
break;
case 4:
fileURL = [[NSBundle mainBundle] URLForResource:#"wh_3d" withExtension:#"mp4"];
break;
default:
return;
break;
}
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:fileURL options:nil];
NSString *tracksKey = #"tracks";
[asset loadValuesAsynchronouslyForKeys:[NSArray arrayWithObject:tracksKey] completionHandler:
^{
// The completion block goes here.
NSError *error = nil;
AVKeyValueStatus status = [asset statusOfValueForKey:tracksKey error:&error];
if (status == AVKeyValueStatusLoaded)
{
self.playerItem = [AVPlayerItem playerItemWithAsset:asset];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(playerItemDidReachEnd:) name:AVPlayerItemDidPlayToEndTimeNotification object:playerItem];
self.player = [AVPlayer playerWithPlayerItem:playerItem];
[playerView setPlayer:player];
[self.player seekToTime:kCMTimeZero];
[self play];
}
else {
// Deal with the error appropriately.
NSLog(#"The asset's tracks were not loaded:\n%#", [error localizedDescription]);
}
}];
}
You do not need to re-create AVPlayer for this task. You can just have multiple AVPlayerItems and then switch which one is current via [AVPlayer replaceCurrentItemWithPlayerItem:item].
Also, you can observe for when current item has changed with the code below.
static void* CurrentItemObservationContext = &CurrentItemObservationContext;
...
After creating a player, register the observer:
[player1 addObserver:self
forKeyPath:kCurrentItemKey
options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
context:CurrentItemObservationContext];
...
- (void)observeValueForKeyPath:(NSString*) path
ofObject:(id)object
change:(NSDictionary*)change
context:(void*)context {
if (context == CurrentItemObservationContext) {
AVPlayerItem *item = [change objectForKey:NSKeyValueChangeNewKey];
if (item != (id)[NSNull null]) {
[player1 play];
}
}
}
There are two workaround that I found. To me both approaches worked and I prefer the second one.
First, as #Alex Kennberg mentioned, create two set of AVPlayerLayer and AVPlayer. and switching them when you switch between videos. Be sure to set background color to clear color.
Second, use UIImageView as the owner view of AVPlayerLayer. Create thumbnail image of a video and set it to the imageview before switching the video. Be sure to set the view mode correctly.
I ran into the same issue with the video "flashing" and solved it this way in Swift 5.
Set my player variable to this
var player = AVPlayer(playerItem: nil)
Then inside my playVideo function, I changed this
self.player.replaceCurrentItem(with: AVPlayerItem(url: fileURL))
to this
player = AVPlayer(url: fileURL)
"fileURL" is the path to video I want to play.
This removed the flash and played the next video seamlessly for me.
You can initialise the PlayerItem and seek to zero some time before you assign it to the player.
Then the flickering disappears
I tried this and it worked for me.
if (layerView1.playerLayer.superlayer) {
[layerView1.playerLayer removeFromSuperlayer];
}
But I am also allocating my own AVPlayerLayer instead of using IB to do it.
After too many tries without success, I finally found a solution, not the best one, but works
My entry code bellow, have a look at the loadVideo method
#import "ViewController.h"
#import <AVKit/AVKit.h>
#interface ViewController ()<UIGestureRecognizerDelegate>
#property (nonatomic, strong) NSArray *videos;
#property (nonatomic, assign) NSInteger videoIndex;
#property (nonatomic, strong) AVPlayer *player;
#property (nonatomic, strong) AVPlayerLayer *playerLayer;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor blackColor];
self.videos = #[#"1.mp4", #"2.mp4", #"3.mp4", #"4.mp4", #"5.mp4", #"6.mp4"];
self.videoIndex = 0;
[self loadVideo];
[self configureGestures:self.view]; //changes video on swipe
}
-(void)prevZoomPanel{
if(self.videoIndex <= 0){
NSLog(#"cant go prev");
return;
}
self.videoIndex -= 1;
[self loadVideo];
}
-(void)nextZoomPanel{
if(self.videoIndex >= self.videos.count - 1){
NSLog(#"cant go next");
return;
}
self.videoIndex += 1;
[self loadVideo];
}
#pragma mark - Load Video
-(void)loadVideo{
NSURL * bundle = [[NSBundle mainBundle] bundleURL];
NSURL * file = [NSURL URLWithString:self.videos[self.videoIndex] relativeToURL:bundle];
NSURL * absoluteFile = [file absoluteURL];
AVPlayerItem *item = [AVPlayerItem playerItemWithURL:absoluteFile];
//*************
//DO NOT USE '[self.player replaceCurrentItemWithPlayerItem:item]', it flashes, instead, initialize the instace again.
//Why is replaceCurrentItemWithPlayerItem flashing but playerWithPlayerItem is NOT?
// if you want to see the diferente, uncomment the code above
self.player = [AVPlayer playerWithPlayerItem:item];
// if (self.player == nil) {
// self.player = [AVPlayer playerWithPlayerItem:item];
// }else{
// [self.player replaceCurrentItemWithPlayerItem:item];
// }
//*************
//create an instance of AVPlayerLayer and add it on self.view
//afraid of this
AVPlayerLayer *playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
playerLayer.videoGravity = AVLayerVideoGravityResizeAspect;
playerLayer.frame = self.view.layer.bounds;
[self.view.layer addSublayer:playerLayer];
//*************
//play the video before remove the old AVPlayerLayer instance, at this time will have 2 sublayers
[self.player play];
NSLog(#"sublayers before: %zd", self.view.layer.sublayers.count);
//*************
//remove all sublayers after 0.09s, to avoid the flash, 0.08 still flashing.
//TODO: tested on iPhone X, need to test on slower iPhones to check if the time is enough.
//Why do I need to wait to remove? Is that safe? What if I swipe a lot too fast, faster than 0.09s ?
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.09 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSArray* sublayers = [NSArray arrayWithArray:self.view.layer.sublayers];
NSInteger idx = 0;
for (CALayer *layer in sublayers) {
if (idx < self.view.layer.sublayers.count && self.view.layer.sublayers.count > 1) {
//to avoid memory crash, need to remove all sublayer but keep the top one.
[layer removeFromSuperlayer];
}
idx += 1;
}
NSLog(#"sublayers after: %zd", self.view.layer.sublayers.count);
});
//*************
//the code bellow is the same of the above, but with no delay
//uncomment the code bellow AND comment the code above to test
// NSArray* sublayers = [NSArray arrayWithArray:self.view.layer.sublayers];
// NSInteger idx = 0;
//
// for (CALayer *layer in sublayers) {
//
// if (idx < self.view.layer.sublayers.count && self.view.layer.sublayers.count > 1) {
//
// //to avoid memory crash, need to remove all sublayer but keep the top one.
// [layer removeFromSuperlayer];
// }
// idx += 1;
// }
//*************
//App's memory usage is about 14MB constantly, didn't increase on videos change.
//TODO: need to test with more than 100 heavy videos.
}
-(void)configureGestures:(UIView *)view{
UISwipeGestureRecognizer *right = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:#selector(userDidSwipeScreen:)];
right.direction = UISwipeGestureRecognizerDirectionRight;
right.delegate = self;
[view addGestureRecognizer:right];
UISwipeGestureRecognizer *left = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:#selector(userDidSwipeScreen:)];
left.direction = UISwipeGestureRecognizerDirectionLeft;
left.delegate = self;
[view addGestureRecognizer:left];
}
- (void)userDidSwipeScreen:(UISwipeGestureRecognizer *)swipeGestureRecognizer{
switch (swipeGestureRecognizer.direction) {
case UISwipeGestureRecognizerDirectionLeft: [self nextZoomPanel];break;
case UISwipeGestureRecognizerDirectionRight:[self prevZoomPanel];break;
default: break;
}
}
#end
I found a very simple solution (maybe too simple for some people, but for me it worked):
In Interface Builder I set the background color of my view (which gets the video layer attached to) to black. So it's just 'flashing' black now...
As what #blancos says in this answer
Firstly, AVPlayer doesn't show any white screen, its your background
which is white
He's 100% correct because when I set my background to white, the flash was white. But when I set the background to green, the flash was green. So to fix it, I set the background to black
view.backgroundColor = .black
When switching videos, I used player.replaceCurrentItem(...):
playerItem: AVPlayerItem?
func switchVideos(url: URL) {
playerItem = AVPlayerItem(url: url)
player.replaceCurrentItem(with: playerItem!)
// if necessary use the KVO to know when the video is ready to play
}