How to make my CMMotionManager available through a singleton? - ios

So I have been designing an iOS game (in Sprite Kit) for a while now and just recently added a CMMotionManager to my project so that my character would be controlled by the tilt of the screen. It took some fiddling but I got it to work, and here's how I've implemented it:
In my initWithSize method I have
self.motionManager = [[CMMotionManager alloc] init];
self.referenceAttitude = nil;
Then, I've written methods beginMotionSensing and switchToLiveSensing. The first is intended to get an idea of how the phone is being held and the second starts the game itself. Using SKActions, I call the first method, wait a second, and call the second. They look something like this:
-(void)beginMotionSensing{
CMDeviceMotion *deviceMotion = self.motionManager.deviceMotion;
CMAttitude *attitude = deviceMotion.attitude;
self.referenceAttitude = attitude;
[self.motionManager startDeviceMotionUpdatesToQueue:[NSOperationQueue currentQueue] withHandler:^(CMDeviceMotion *motion, NSError *error) {
[self setNull:motion];
}];
self.motionManager.deviceMotionUpdateInterval = .3;
}
and,
-(void)switchToLiveSensing{
gameIsLive = YES;
[self.motionManager stopDeviceMotionUpdates];
[self.motionManager startDeviceMotionUpdatesToQueue:[NSOperationQueue currentQueue] withHandler:^(CMDeviceMotion *motion, NSError *error) {
[self captureRoll:motion];
}];
self.motionManager.deviceMotionUpdateInterval = .02;
}
So I had all of this set up and it was working great, until I added the ability to play the game more than once. Before this, I would simply have to close out of the app and quit the game when I lost. Obviously this became annoying so I added a "game over" scene, with the ability to go back and play the game scene again. The game scene is presented again like this:
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
SKTransition *transition = [SKTransition fadeWithColor:[UIColor colorWithRed:147.0/255.0 green:213.0/255.0 blue:216.0/255.0 alpha:1.0] duration:2.0f];
MyScene *gameScene = [MyScene alloc];
gameScene = [gameScene initWithSize:self.frame.size passedInFuel:100];
[self.view presentScene:gameScene transition:transition];
}
I had remembered someone saying something in a previous post about a singleton - the comment was directed at someone implementing a CMMotionManager. They had said something along the lines of, "you should probably make it accessible through a singleton if you are going to initialize your class more than once." Well, sure enough, that's what I'm doing now. I had forgotten about this until I was playing the game and noticed that suddenly the phone seemed to ignore my tilt and went along with it's own business, killing my character in the process. It hasn't happened since, but is clearly something that needs to be fixed.
Could someone help me implement this so-called "singleton"?

I'm not convinced this is the problem you had, as the CMMotionManager object should be released anyway when your SKScene goes away. Can you duplicate the problem by starting the game again quickly after it ends?
You could probably do it by adding a class method in your main view/navigation controller (which ever one stays around longest) to create a singleton and then make it available to your SKScene object.
e.g.
+ (CMMotionManager *)sharedInstance
{
static CMMotionManager *sharedCMMM = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedCMMM = [[CMMotionManager alloc] init];
// Do any other initialisation stuff here
});
return sharedCMMM;
}

Related

GameCenter showsCompletionBanner keeps showing

In my game (I'm using SpriteKit, and therefore only support iOS 7), when a player reaches his first 10 points, he is awarded with an achievement. I've implemented the achievement method as follows:
-(void) First10Points
{
GKAchievement *achievement = [[GKAchievement alloc] initWithIdentifier: #"Achievement_First10Points"];
if (achievement)
{
achievement.showsCompletionBanner = YES;
achievement.percentComplete = 100.0;
NSArray *achievements = [NSArray arrayWithObjects:achievement, nil];
[GKAchievement reportAchievements:achievements withCompletionHandler:^(NSError *error) {
if (error != nil) {
NSLog(#"Error in reporting achievements: %#", error);
}
}];
}
}
This works fine and the achievement is indeed earned at 10 points, with the game center banner indicating this to the player during the game. However, when the banner disappears it reappears after a second or so and continues to do so until i terminate the game. The game can still be played while it does this loop thing. I can't seem to understand why it does this and I have not come across this problem while searching the web.
Anyone an idea? Or should I implement my achievements in another way?
One possibility is that you're calling the First10Points method multiple times. You should check if the player has already reached the 10 points achievement before presenting the achievement again. If they have indeed already reached it, then don't call the method.
Try adding a variable like BOOL first10 = NO; When you run your check (score == 10), set first10 = YES; Everytime before you call First10Points, ensure that (first10 == NO)

How to completely unload SpriteKit Scene

When I hit the retry button in my game, I want it to reload the MainScene. I am doing this with:
-(void)retry
{
SKTransition *transition = [SKTransition fadeWithDuration:.4];
MainScene *gameOver = [[MainScene alloc] initWithSize:self.size];
[gameOver didMoveToView:self.view];
[self.scene.view presentScene:gameOver transition:transition];
}
However, this is causing the memory/CPU usage to increase (by a lot) each time I hit retry. After about 10-20 retries, there is a noticeable lag.
I made all my SKEmitterNode and SKSpriteNode static and that fixed the memory problem, so I suspect that my sprites, emitters, etc are not being released from the memory and are being re-loaded every time I hit retry, doubling it.
I am loading the sprites/emitters like this:
#implementation MainScene {
SKEmitterNode *_bubbleEmitter;
SKSpriteNode *_sunglasses;
...
}
- (id)initWithSize:(CGSize)size
{
if (self = [super initWithSize:size])
{
_sunglasses = [SKSpriteNode spriteNodeWithImageNamed:#"sunglasses"];
[_sunglasses setPosition:CGPointMake(self.size.width/2, self.size.height + 10)];
[self addChild:_sunglasses];
...
}
return self;
}
Am I loading the sprites or the retry wrong?
This may or may not be the cause, but it's certainly wrong to call this method yourself:
[gameOver didMoveToView:self.view];
The didMoveToView: method is sent to the scene by the SKView when you present the scene. That means this method will actually run twice.
Also verify that your scenes are deallocating properly by implementing:
-(void) dealloc
{
NSLog(#"dealloc: %#", self);
}
Watch for the log or set a breakpoint to confirm the scene deallocates. If it isn't, check for memory leaks and retain cycles.

How to detect when the phone has been put down

I would like an action to take place when the phone is stationary for 2 seconds. I've searched for ages around google and stack overflow. I discovered that "Accelerometer DidAccelerate" has been depreciated and that CoreMotion is the replacement. Everything I have seen has been to do with the 'shaking' motion. I've tried reading through apple's documentation but It just confuses me!
Basically, I want the app to detect that the g-forces on the phone have remained within a small limit for a certain amount of time (suggesting that the phone has been laid down on a table or something) and for it to call and instance or make the app do something.
Any help would be greatly appreciated.
It's similar to the problem described in Simple iPhone motion detect. The basic setup for CMMotionManager is described in the Apple docs like Mike Pollard stated in his comment. I recommend especially the Handling Processed Device Motion Data section.
What you then need is CMDeviceMotion.userAcceleration which contains the pure acceleration without gravity.
CMMotionManager *motionManager = [[CMMotionManager alloc] init];
// UPDATE: set interval to 0.02 sec
motionManager.deviceMotionUpdateInterval = 1.0 / 50.0;
[motionManager startDeviceMotionUpdatesToQueue:[NSOperationQueue mainQueue]
withHandler:^(CMDeviceMotion *deviceMotion, NSError *error) {
CMAcceleration userAcceleration = deviceMotion.userAcceleration;
double totalAcceleration = sqrt(userAcceleration.x * userAcceleration.x +
userAcceleration.y * userAcceleration.y + userAcceleration.z * userAcceleration.z);
// UPDATE: print debug information
NSLog (#"total=%f x=%f y=%f z=%f", totalAcceleration, userAcceleration.x, userAcceleration.y, userAcceleration.z);
// if(totalAcceleration < SOME_LIMIT) ...
Then proceed like codeplasma has described in his answer above.
Also be aware that the solution might not be precise if used in the underground, bus, etc. because of external accelerations.
You can do something like this:
CMMotionManager *mManager = [[CMMotionManager alloc] init];
if ([mManager isAccelerometerAvailable] == YES) {
__block float lastActivityBefore = 0.0;
[mManager setAccelerometerUpdateInterval:0.1];
[mManager startAccelerometerUpdatesToQueue:[NSOperationQueue mainQueue] withHandler:^(CMAccelerometerData *accelerometerData, NSError *error) {
double totalAcceleration = sqrt(accelerometerData.acceleration.x * accelerometerData.acceleration.x + accelerometerData.acceleration.y * accelerometerData.acceleration.y + accelerometerData.acceleration.z * accelerometerData.acceleration.z);
if(totalAcceleration < SOME_LIMIT)
lastActivityBefore = lastActivityBefore + 0.1;
else
lastActivityBefore = 0.0;
if(lastActivityBefore >= 2.0)
{
//do something
}
}];
}
Accelerometer will show some minimal acceleration even if your device is steady, so you should make a testing in order to determine SOME_LIMIT value.
Also be advised that you should have only one instance CMMotionManager class in your app, so you're better to put it in your AppDelegate and initialize it only once.

View Controller not deallocated after adding GameKit

My view controller no longer gets deallocated after adding the following:
#property (strong, nonatomic) GKLocalPlayer *player;
(in viewDidLoad)
self.player = nil;
[self authenticatePlayer];
- (void)authenticatePlayer
{
GKLocalPlayer *localPlayer = [GKLocalPlayer localPlayer];
__unsafe_unretained typeof(*localPlayer) *blockLocalPlayer = localPlayer;
localPlayer.authenticateHandler =
^(UIViewController *authenticateViewController, NSError *error)
{
if (authenticateViewController != nil)
{
[self presentViewController:authenticateViewController animated:YES
completion:nil];
}
else if (blockLocalPlayer.isAuthenticated)
{
self.player = blockLocalPlayer;
[self openGame];
}
else
{
// Disable Game Center
self.player = nil;
[self openGame];
}
};
}
- (void)setPlayer:(GKLocalPlayer *)player
{
_player = player;
NSString *playerName;
if (_player)
{
playerName = _player.alias;
}
else
{
playerName = #"Anonymous Player";
}
NSLog(#"%#", [NSString stringWithFormat:#"Welcome %#", playerName]);
}
The problem occurs whether or not the user connects to game center. There must be something in the code that is causing the view controller to remain in memory after it gets dismissed. If I comment these lines out:
self.player = nil;
[self authenticatePlayer];
the view controller will properly get deallocated when dismissed.
EDIT:
My hunch was correct. From Apple docs:
Game Kit maintains a strong reference to your completion handler even
after successfully authenticating a local player. If your game moves
into the background, Game Kit automatically authenticates the player
again whenever your game moves back to the foreground. Game Kit calls
your same completion handler each time it authenticates the local
player. Be mindful that in block programming, any Objective-C object
referenced inside a block is also strongly referenced by the block
until the block is released. Because Game Kit maintains a strong
reference to your completion handler until your game terminates, any
objects referenced from within your authentication handler are also
held indefinitely.
This is a problem for me though. I'm using Cocos2d and it has problems resetting its view unless the view controller is completely deallocated and created fresh.
Is there any way to get Game Kit to let go of my view controller?
Got sick of dealing with the issue and moved the GameKit authentication code to a different view controller, where it is not vital that it be deallocated.

CMMotionManager with multitasking

I'm using CMMotionManager in my app so I can get the device motion info. I have these two methods:
- (void)startDeviceMotion {
motionManager = [[CMMotionManager alloc] init];
motionManager.showsDeviceMovementDisplay = YES;
motionManager.deviceMotionUpdateInterval = 1.0 / 120.0;
[motionManager startDeviceMotionUpdatesUsingReferenceFrame:CMAttitudeReferenceFrameXTrueNorthZVertical];
}
Second is:
- (void)stopDeviceMotion {
[motionManager stopDeviceMotionUpdates];
[motionManager release];
motionManager = nil;
}
They're launched when the app starts and when the app finishes respectively. My problem is now multitasking. If I get my problem into background and then I bring it to foreground again, I get a message (with NSZombie activated) telling me that a [CMMotionManager retain] message is being sent to a deallocated instance.
Where could my problem be?
Thanks!
Try using Jonathan's suggestion here. Basically, to make sure only one instance of your CMMotionManager is created, put your motionManager in AppDelegate and retrieve it by this method wherever you want to use your motionManager.
-(CMMotionManager *)motionManager
{
CMMotionManager *motionManager = nil;
id appDelegate = [UIApplication sharedApplication].delegate;
if ([appDelegate respondsToSelector:#selector(motionManager)]) {
motionManager = [appDelegate motionManager];
}
return motionManager;
}
Let me know if this works for you.

Resources