I used profiler and detect that after I go back from view controller that contains game scene view (for example pop back to the main menu). The game scene is still in the memory.
I supposed it's connected to some SKAction I use. How track which object cause that issue.
I use some SKAction run block, repeat forever and etc, and I am sure that the something happen with it.
You cannot delete a SKScene. I would recommend creating a function for when your present the scene. Here is and example
(Swift)
func deleteView(deleteEveryThing:Bool) {
if deleteEveryThing {
self.removeAllActions()
self.removeAllChildren()
//Scene presentation code here
}
else {
self.removeAllChildren()
//Scene Presentation Code here
}
}
(Objective - C)
-(void)deletView:(BOOL)deleteEveryThing {
if (deleteEveryThing) {
[self removeAllNodes];
[self removeAllActions];
}
else {
[self removeAllNodes];
[self removeAllActions];
}
}
So what I did was create a function called deleteView and deleteView has a parameter that is a Boolean (True or false) and if it is trure then it will remove all actions : self.removeAllActions() or [self removeAllActions]; and all children: self.removeAllChildren() or [self removeAllChildren];in the SKScene. This can help in freeing up memory and once those two lines of code ran then you can handle the scene presentation code. There is also an else part which does the same thing but leaves the SKActions in the SKScene.
Related
In my ARKit app I am presenting a modal window. When I close the modal and go back to the ARSCNView then I find out that the session is paused due to this code:
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// Pause the view's session
sceneView.session.pause()
}
When I close the modal and go back to the ARKit camera view screen this code gets fired:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Create a session configuration
let configuration = ARWorldTrackingSessionConfiguration()
// Run the view's session
sceneView.session.run(configuration)
}
But this code never resumes the session. The screen is completely frozen on the last image it read. Any ideas?
I update the viewDidAppear code to be the following. It is still stuck on the camera screen with image frozen.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Create a session configuration
let configuration = ARWorldTrackingSessionConfiguration()
sceneView.session.delegate = self
if self.isPaused {
sceneView.session.run(sceneView.session.configuration!)
} else {
// Run the view's session
sceneView.session.run(configuration)
}
}
Not sure why your session isn't resuming, but... this generally isn't a situation you want to be in anyway.
Notice in the readme that ships with Apple's ARKit sample code (attached to the WWDC17 session on ARKit):
Avoid interrupting the AR experience. If the user transitions to another fullscreen UI in your app, the AR view might not be an expected state when coming back.
Use the popover presentation (even on iPhone) for auxiliary view controllers to keep the user in the AR experience while adjusting settings or making a modal selection. In this example, the SettingsViewController and VirtualObjectSelectionViewController classes use popover presentation.
To go into a bit more detail: if you pause the session, it won't be tracking the world while your user is away in a different fullscreen view controller. That means that when you resume, any virtual content placed in the scene won't be in the positions (relative to the camera) where you left it.
I don't know if the iOS 11 GM Seed or XCode 9 GM Seed versions fixed this today however I can successfully resume a paused ARSCNview with code as in the original question.
sceneView.session.run(sceneView.session.configuration!)
I get that you have chosen an answer, and that answer is what is recommended by apple, you can restart the AR Session. You can't unpause/resume the Session though, because the device stops it's tracking once you're out of your controller presenting the ARSceneView and will stop keeping track of the position of your device relative to the objects you've placed in the scene.
Anyway, I've managed to restart the session essentially by destroying all aspects of my session and rebuilding them them when my view reappears, or through a button press.
I'll post some sample code here. It's in Objective-C cause my project was written in that, but it might help future people with the same question.
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated]
[self setupScene];
[self setupSession];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[self destroySession];
[self destroyScene];
}
- (void)setupScene {
// Setup the ARSCNViewDelegate - this gives us callbacks to handle new
// geometry creation
self.sceneView.delegate = self;
// A dictionary of all the current planes being rendered in the scene
self.planes = [NSMutableDictionary new];
// Contains a list of all the boxes rendered in the scene
self.boxes = [NSMutableArray new];
// Show statistics such as fps and timing information
self.sceneView.showsStatistics = YES;
self.sceneView.autoenablesDefaultLighting = YES;
SCNScene *scene = [SCNScene new];
[self.sceneView setScene:scene];
self.sceneView.scene.physicsWorld.contactDelegate = self;
}
- (void)setupSession {
// Create a session configuration
ARWorldTrackingConfiguration *configuration = [ARWorldTrackingConfiguration new];
//ARWorldTrackingSessionConfiguration *configuration = [ARWorldTrackingSessionConfiguration new]; This has been deprecated in favor of the previous line in XCode 9 beta 5.
// Specify that we do want to track horizontal planes. Setting this will cause the ARSCNViewDelegate
// methods to be called when scenes are detected
//configuration.planeDetection = ARPlaneDetectionHorizontal;
// Run the view's session
[self.sceneView.session runWithConfiguration:configuration options:ARSessionRunOptionResetTracking];
}
-(void)destroyScene {
bottomPlane = nil;
[self.sceneView setScene:nil];
[self.sceneView setDebugOptions:nil];
self.boxes = nil;
self.planes = nil;
self.sceneView.delegate = nil;
}
-(void)destroySession {
[self.sceneView.session pause];
[self.sceneView setSession:nil];
}
These destroy methods are used when the view disappears. I am also restarting the AR Session on a button press, but it is not through these methods. It is as follows:
-(void)resetPressed{
NSLog(#"Reset Pressed");
[_sceneView.session pause];
SCNScene *scene = [[SCNScene alloc] init];
[_sceneView setScene:scene];
[_sceneView.scene.rootNode enumerateChildNodesUsingBlock:^(SCNNode * _Nonnull child, BOOL * _Nonnull stop) {
[child removeFromParentNode];
}];
ARWorldTrackingConfiguration *configuration = [[ARWorldTrackingSessionConfiguration ARWorldTrackingConfiguration] init];
[_sceneView.session runWithConfiguration:configuration options:ARSessionRunOptionResetTracking | ARSessionRunOptionRemoveExistingAnchors];
}
Hope it helps.
Here's an answer working with Swift 4.2 and iOS 12.
To present UI defined in another view controller over your AR scene, create your view controller instance and set it's modalPresentationStyle property to .overCurrentContext:
EXAMPLE:
func showMaterialPicker(completion: (Texture?) -> Void) {
// create an instance of your view controller, I have convenience functions
// setup to do this via an extension on UIViewController
guard let materialPicker = MaterialCategoriesViewController.instance(from: .product) else {
print("Unable to instantiate MaterialCategoriesViewController, bailing")
return
}
// set presentation style and transition style
materialPicker.modalPresentationStyle = .overCurrentContext
materialPicker.modalTransitionStyle = .crossDissolve
// present the controller
present(materialPicker, animated: true, completion: nil)
}
Bonus tip:
To make your overlay appear to slide up from the bottom like a drawer, set
materialPicker.modalTransitionStyle = .coverVertical
then constrain your views in your overlay view controller a comfortable height from the bottom and set the background color of the view controllers view to UIColor.clear.
If you want to darken the AR view while your overlay is displayed you can set the background color to a black color with an opacity/alpha value of approximately 0.75.
Something like this:
self.view.backgroundColor = UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.75)
or in the storyboard:
In the screenshot above I have a tableview pinned to the bottom and sides of the overlay view controllers view, and a height constraint of 300.
When done this way you can still see the AR view behind the overlay view controller and the scene continues to render.
Inside viewDidLoad: create a tap event.
arKitView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTap(recognizer:))))
Inside viewWillAppear:
arView.session.run(configuration)
And then define the tap event. In which first tap will pause the session and second tap will resume the session and so on.
I am making a game in which when 2 objects (object A and B) collide, the game gets over. Object A's position changes each time update is called. Object B's position is the same. The code is:
[self enumerateChildNodesWithName:#"objA" usingBlock:^(SKNode *node, BOOL *stop) {
if ([objB intersectsNode:node])
{
[node removeFromParent];
[self GameOver];
}
The problem is: I want objA NOT to disappear after the collision. So, for that I removed [node removeFromParent]; , but since update is called again and again. My number of nodes increases and the sound that i have added never seems to end. So, what i tried was adding:
[self performSelector:#selector(pauseGame) withObject:Nil afterDelay:0.1];
-(void) pauseGame
{
self.scene.view.paused = YES;
[self performSelector:#selector(gameOver) withObject:Nil afterDelay:0.1];
}
I had to use performSelector with delay, because putting self.scene.view.paused = YES; within the update wouldn't allow me to go to gameOver. However, I do not want any delays! is there a way to do this??
Thanks
Just use a state variable in your scene which will indicate the current state.
Use this state to run updates on your scene objects.
For example (pseudocode) :
update()
if (state == GAME_PLAY) {
// Update relevant game nodes
} else if (state == GAME_OVER) {
// Update only what needs to be updated when the game is over
}
This way you do not need to stop your entire scene and add only what is relevant to the current game state
I have a sprite-kit game where I need to be checking often to see if the player has lost
- (void)update:(NSTimeInterval)currentTime
{
for (SKSpriteNode *sprite in self.alienArray ) {
if (sprite.position.y < 10) {
LostScene *lostScene = [[LostScene alloc] initWithSize: CGSizeMake(CGRectGetWidth(self.view.bounds), CGRectGetHeight(self.view.bounds))];
NSLog(#"about to present");
[self.view presentScene:lostScene transition:[SKTransition fadeWithDuration:0.5]];
}
}
}
but when this method gets called (which I know is happening), no scene presents. What am I doing wrong? I believe it has something to do with the transition, because when I take it out, it works fine
You should add a property of the existing scene like: BOOL playerHasLost and edit your update method:
- (void)update:(NSTimeInterval)currentTime
{
if(!playerHasLost)
{
for (SKSpriteNode *sprite in self.alienArray )
{
if (sprite.position.y < 10)
{
playerHasLost = YES;
LostScene *lostScene = [[LostScene alloc] initWithSize: CGSizeMake(CGRectGetWidth(self.view.bounds), CGRectGetHeight(self.view.bounds))];
NSLog(#"about to present");
[self.view presentScene:lostScene transition:[SKTransition fadeWithDuration:0.5]];
break;//to get out of for loop
}
}
}
}
So this way as soon as the sprite get to position that you treat as lost position it will set the variable to YES, present the scene and it will not do it again.
The reason is simply that the update method gets called every frame, with a running transition the scene will be presented anew every frame and thus it appears as if nithing is happening. You should see the NSLog spamming the log console.
I am trying to get my head around the touch handling in Kobold2Ds's KKInput class. At the moment I just have some dummy methods that are checking for the three main phases of touch input, began, moving, and ended.
However the code for touches moving never seems to get called.
Here is my code. It is inside a CCNode subclass, that has a CCSprite that it is responsible for rendering and managing.
-(void) update:(ccTime)delta
{
// NSLog(#"Update");
KKInput* input = [KKInput sharedInput];
if ([input isAnyTouchOnNode:tileSprite touchPhase:KKTouchPhaseMoved])
{
NSLog(#"Tile touched moved");
}
if ([input isAnyTouchOnNode:tileSprite touchPhase:KKTouchPhaseBegan])
{
NSLog(#"Tile touched began");
self.frameNumber = #2;
}
if ([input isAnyTouchOnNode:tileSprite touchPhase:KKTouchPhaseEnded])
{
NSLog(#"Tile touched ended");
self.frameNumber = #1;
}
}
The code for KKTouchPhaseBegan and KKTouchPhaseEnded both get called, but the code for KKTouchPhaseMoved never does.
Can anyone clue me in to what i'm doing wrong?
Is it possible to detect which CCScene is currently showing on the scene? I have 2 CCScenes in my game and I want a certain action to occur if one is showing.
Also quick related question, if I wanted to check if a CCMenu is not showing currently would I do something like
if (!menu) {
//Menu is not showing currently
}
I am a bit of a noob when it comes to Cocos2D so please forgive me :)
Thanks!
You can use the CCDirector to tell which scene is running.
[[CCDirector sharedDirector] runningScene];
As for whether the menu is showing. You would have to check with the parent of the menu. If the parent where your CCLayer, then you could check by
// assume menu is set up to have tag kMenuTag
CCMenu * menu = [self getChildByTag:kMenuTag];
If the menu is child of some other node, you can get the parent through a similar method and get a reference to the menu.
If the menu == nil, it is not showing.
UPDATE
In cocos2d, you are discouraged from keeping references to all of your sprites, instead you should be giving each node a unique tag and use that to reference it. To achieve your first goal, you can give your scene a tag in your 2 respective CCLayer classes.
You can set up your unique tags in an enum in a file called Tags.h, then import that into any classes that need access to your tags
Example Tags.h
enum {
kScene1Tag = 0,
kScene2Tag = 1,
kMenuTag = 2};
Then in your layer class
+(id) scene
{
// 'scene' is an autorelease object.
CCScene *scene = [CCScene node];
scene.tag = kScene1Tag;
// 'layer' is an autorelease object.
HelloWorld *layer = [HelloWorld node];
// add layer as a child to scene
[scene addChild: layer];
// return the scene
return scene;
}
Now when you grab the current scene you can check against the tags
int currentSceneTag = [[CCDirector sharedDirector] runningScene].tag;
if (currentSceneTag == kScene1Tag) {
} else if (currentSceneTag == kScene2Tag) {
}
The tag property is from CCNode which is the base class of CCLayer, CCScene, CCSprite, CCMenu...
This how to find out which scene is running
if ([CCDirector sharedDirector].runningScene == yourScene1) {
// your scene 1 is showing
} else {
// your scene 2 is showing
}
and to find out if a node is child of the running scene
BOOL isShowing = NO;
CCNode *node = yourMenu;
while (node != nil) {
if (node == [CCDirector sharedDirector].runningScene) {
isShowing = YES;
break;
} else {
node = node.parent;
}
}
if (isShowing) {
// your menu is in the display hierarchy
}