Background info:
I'm creating a SpriteKit 2D platform-style game with multiple "floors". When the user enters a "portal" he is transported to another floor (either one up or one down). If the user dies at a different floor from where the most recent spawnpoint is, he is transported back to the floor with the spawnpoint.
Problem: After upgrading to iOS8, this scenario causes the game to crash with an EXC_BAD_ACCESS exception/ error. Even after going through multiple tutorials on how to debug such errors, I cannot seem to find the problem.
So I'd really appreciate if someone could take a look at my code and tell me if they see a better way of doing what I'm doing, preferably without causing the game to freeze.
Game layout:
I have a custom class TileMapLayer which is based on the same class from Ray Wenderlich's book iOS Games By Tutorials. It is basically a custom SKNode class containing multiple 32x32p tiles, combined creating the background for my game.
I load the appropriate floor when the game begins, and then call to load another floor when my user goes up/down. In this scenario, I cache the current floor first in a NSDictionary so I can access it later if the user returns to this floor.
// Cache the current floor
[_allFloors setObject:_bgLayer forKey:[NSString stringWithFormat:#"floor_%tu", (unsigned)_currentFloor]];
// Remove the current floor
[_bgLayer removeFromParent];
// Get the cached (next) floor if it exists, if not create it
TileMapLayer *cachedFloor = [_allFloors objectForKey:[NSString stringWithFormat:#"floor_%tu", (unsigned)(_currentFloor + 1)]];
if (!cachedFloor) {
_bgLayer = [self createScenery:[NSNumber numberWithInteger:(_currentFloor + 1)]];
NSLog(#"creating new floor");
} else {
_bgLayer = cachedFloor;
NSLog(#"getting cached floor");
}
// Display the new floor
[_worldNode addChild:_bgLayer];
// Increment the floor number
_currentFloor++;
(I have a similar method for going down a floor as well).
This works perfectly, both before and after upgrading to iOS8.
When the user dies, he is transported back to the last spawnpoint. If the last spawnpoint is on a different floor, the floor also changes appropriately.
I call this custom SKAction on the ball itself as a part of an animation:
SKAction *changeFloor = [SKAction runBlock:^{
if (self.spawnFloor != _currentFloor) {
[_bgLayer removeFromParent];
_bgLayer = [_allFloors objectForKey:[NSString stringWithFormat:#"floor_%tu", (unsigned)self.spawnFloor]];
[_worldNode addChild:_bgLayer];
_currentFloor = self.spawnFloor;
NSLog(#"Made it here");
}
}];
As you can see there isn't much difference. The Made it here gets logged to the console, but the game freezes immediately after. The next method I call (to move the ball to the correct location) is not executed at all.
Just for fun I tried caching the _bgLayer before removing it from its parent like so:
if (self.spawnFloor != _currentFloor) {
[_allFloors setObject:_bgLayer forKey:[NSString stringWithFormat:#"floor_%tu", (unsigned)_currentFloor]];
...
And for some reason the game does not freeze when I do this. However, the floors end up being mixed as if the _bgLayer never was removed from its parent (note: the "old" tiles does no longer react to any physics-simulations, they're just in the background doing nothing).
I also tried [_bgLayer removeAllChildren] (removing the individual tiles) right after removing from the parent:
[_bgLayer removeFromParent];
[_bgLayer removeAllChildren];
But this causes the _bgLayer to be empty when I return to this floor after respawning. As if I removed all the nodes before I stored it in the dictionary. My guess is that the dictionary only references the location of the _bgLayer, not the content itself. So when I remove all the children on screen, I effectively remove all of them in the cache as well.
Wrapup:
I know this is a long question, and I'd like to say thank you if you made it this far. If you have any questions, please don't hesitate to ask them in the comments below.
Ultimately, what I'd like to know is this: How can I resolve my problem so the game doesn't freeze? What's the best practice for caching the floors without causing memory problems? What's the best practice for removing and adding the nodes on screen, and always referring to the current floor (the one that's on screen) as _bgLayer?
Again, thank you so much!
iOS8 seems to behave different when removing nodes inside blocks, as if you tried to do it on a background thread so sometimes it causes strange crashes. It might be a bug but until then I'd suggest two things:
1.- Mark the node to be removed inside the block but do it on the update: loop. You will not notice any difference.
2.- Make sure the removeFromParent happens in the main thread. But not sure this will fix the problem.
__strong typeof(_bgLayer) strongLayer = _bgLayer;
dispatch_async(dispatch_get_main_queue(), ^{
[strongLayer removeFromParent];
});
Related
So basically I have two questions. I am also copying off this video from 2014: https://www.youtube.com/watch?v=w8JgpEjm8i8&t=2792s
Question 1
At the end of the video above the guys computer completely crashed and he wasnt able to show the end of how to setup a death animation for my player (link). So currently I have setup a movie clip of the death animation - just the playing spinning around - and put it in another movie clip called 'All Sprites' in which I have my other movie clips such as the different walking directions. So I have placed it in there, labelled it with "Link Death Frame". I then went back to the main script and added code so that once my player dies, the animation will play. Such as:
~
if(linkHealthBarMC.scaleX <= 0.01)
{
linkAlive = false;
}
trace(linkAlive);
if(linkAlive == false)
{
linkMC.gotoAndStop("Link Death Frame");
}
~
However, the issue comes in which the animation doesnt actually play, it just goes straight to the first frame and doesnt play. I have tested it and I know for sure that it gets stuck on the first frame and that something must be wrong with the animation as I tested it with another animation and it worked fine (once I met the requirements for the animation to play). So does anyone have any idea how I can fix this issue so that my character can play a death animation?
Question 2
How do I stop time? Like just completely freeze everything after my character is dead? Because at the moment I am just going to the first frame of the death animation and can still move and attack.
I'd assume this is on an EnterFrame loop. Then you would have two possible causes for this:
1.) You're loop is constantly checking if the 'linkAlive' if statement is false (which it is) and setting your Animation to the first frame. You can check this method by putting a Trace statement in your if statement and if the output window overflows, then that's your culprit.
2.) What you want is gotoAndPlay(-insert label here-)
Tho it is outside the scope of the question you have, I create a variable~Switch State machine to control states for me:
1.) Current State as a number (int, number, or uint)
2.) function with a switch statement (Switch is kind of a fancy if Statement)
3.) inside the cases are instructions
switch(current_state){
case 1:
linkMC.gotoAndPlay('death animation');
break;
}
if (current_state != 1){
-put movement code here-
}
This is close to what I use for my game. Just checking states or you can have a variable like the one above that explicitly checks for the death state and remove the ability to move. If you use a mouse (and I assume event listener) then you can remove the event listener for the mouse. If you use a solution like keyboard inputs then an if statement would be more what you are looking for.
My game was working fine in iOS7, since iOS 8, I am having contact occur with physics bodies that are not even close to each other. I verified this by enabling 'showphysics' - also the error always seems to be with a newly created object which is offscreen..
Has anyone seen this, and know any way of fixing this issue?
If you can see in this screenshot, there are birds and butterflies flying around.. a collision has just occured with the ball and an object which is offscreen!, and the NSLog is showing the two objects, and their X positions.. they aren't even close.. one clue, the second body is always newly created, and offscreen to the right ( > screen.x, but never < 0 ).
I've got an NSTextView subclass acting as its NSTextStorage delegate. I'm trying to do 2 things:
Highlight the text in some ways
Evaluate the text and then append the answer to the textview.
I'm doing this in two different methods, both invoked by the - (void)textStorageWillProcessEditing:(NSNotification *)notification delegate callback.
I can do the syntax highlighting just fine, but when it comes to appending my answer, the insertion point jumps to the end of the line and I don't really know why. My evaluate method looks like the following:
NSString *result = ..;
NSRange lineRange = [[textStorage string] lineRangeForRange:[self selectedRange]];
NSString *line = [[textStorage string] substringWithRange:lineRange];
line = [self appendResult:result toLine:line]; // appends the answer
[textStorage replaceCharactersInRange:lineRange withString:line];
Doing that will append my result just fine, but the problem is, as mentioned, the insertion point jumps to the end.
I've tried:
Wrapping those above calls up in [textStorage beginEditing] and -endEditing.
Saving the selection range (i.e., the insertion point) before changing the text storage so I can reset it afterwards, but no dice.
Am I doing this right? I'm trying to do this the least hackish way, and I'm also unsure if this is the ideal place to be doing my parsing/highlighting. The docs lead me to believe this, but maybe it's wrong.
Reason for the insertion point to move
Suprisingly, I never found an actual explanation to why these suggestion do (or do not) work.
Digging into it, the reason for the insertion point to move is: .editedCharacters (NSTextStorageEditedCharacters in ObjC)affects the position of the insertion point from NSLayoutManager.processEditing(from:editedMask:...).
If only .editedAttributes/NSTextStorageEditedAttributes is sent, the insertion point will not be touched. This is what you will want to achieve when you highlight: change attributes only.
Why highlighting affects the insertion point
The problem with highlighting here is that NSTextStorage collects all edited calls during a single processing run and combines the ranges, starting with the user-edited change (e.g. the insertion when typing), then forming a union of this and all ranges reported by addAttributes(_:range:). This results in one single NSLayoutManager.processEditing(from:editedMask:...) call -- with an editedMask of both [.editedCharacters, .editedAttributes].
So you want to send .editedAttributes for the highlighted ranges but end up forming a union with .editedCharacters instead. That union moves the insertion point waaaaaaaay beyond where it should go.
Changing the order in processEditing to call super first works because the layout manager will be notified of a finished edit. But this approach will still break for some edge cases, resulting in invalid layout or jiggling scroll views while you type in very large paragraphs.
This is true for hooking into NSTextStorageDelegate, too, by the way.
Hook into callbacks after layout has truly finished to trigger highlighting instead of processEditing
The only solution that will work robustly based on reasons inherent to the Cocoa framework is to perform highlighting from textDidChange(_:) exclusively, i.e. after the layout processing really has been finished. Subscribing to NSTextDidChangeNotification work just as well.
Downside: you have to trigger highlighting passes for programmatic changes to the underlying string as these will not invoke the textDidChange(_:) callback.
In case you want to know more about the source of the problem, I put more my research, different approaches, and details of the solution in a much longer blog post for reference. This post is still a self-contained solution in itself:
http://christiantietze.de/posts/2017/11/syntax-highlight-nstextstorage-insertion-point-change/
I know that this question has been long since answered, however I had exactly the same issue. In my NSTextStorage subclass I was doing the following:
- (void)processEditing {
//Process self.editedRange and apply styles first
[super processEditing];
}
However, the correct thing to do is this:
- (void)processEditing {
[super processEditing];
//Process self.editedRange and apply styles after calling superclass method
}
It's simple! I ended up breaking this problem into 2 parts. I still do my syntax highlighting as a result of the textStorage delegate callback, but now I do my evaluation and appending elsewhere.
I ended up overriding both -insertText: and -deleteBackwards: (I might also want to do the same for -deleteForwards:, too). Both overrides look like the following:
- (void)insertText:(id)insertString {
[super insertText:insertString];
NSRange selectedRange = [self selectedRange];
[self doEvaluationAndAppendResult];
[self setSelectedRange:selectedRange];
}
I ended up having to reset the insertion point manually here. I'd still love to figure out why that's necessary, but at least this feels like less of a hack.
- (UITableViewCell *)tableView:... cellForRowAtIndexPath:... {
// init and sanity check
if (indexPath.row == 0) {
// make cell look like a section header
// easier and less complex than using a custom header view
NSLog(#"header");
// should prove that only four cells get the bg image
// and yet, almost every time the table is reloaded
// one more cell unexpectedly gets the background
// without putting another line in the debugger
} else {
// normal cells after row 0
// BUG: some of these are getting the row 0 background
NSLog(#"row");
// firing exactly the right number of times --
// once for each row with an index greater than 0
// so why are some of these cells getting the header bg?
// when these cells get the headers' contents
// they behave exactly as one would expect
// except that they should be the normal rows
}
// misc stuff, not causing problems
return cell;
}
Short of forcing the user to completely relaunch the app just to have different data dumped into the table, I can't figure out how to fix the bug.
The problem is less severe if I collapse every section (that is, empty the expandedSections set and reload, leaving only the pseudo-headers visible), but it doesn't go away.
Edit:
Initial load: screenshot
After reloading: screenshot
Links instead of images because it's an iPad app.
Using some dummy content for testing.
Is this any help? I know there needs to be more code for serious help, but I don't know what else to add, short of a link to the code for the entire view.
Have you tried setting a different cell identifier for the first cell and another one for the rest of them?
The tableView reuses the same cell type when creating new cells so it might get them mixed up. By using different cell identifiers for each type, it will know exactly when to use each cell.
It's probably related to the cell cache...something about setting the image for row == 0 and not clearing it otherwise...but with the limited amount of code you're showing it's hard to be more specific.
So, to handle orientation changes (in a view, not a controller) we register with the UIDeviceOrientationDidChange notification. All good.
It gets called on app startup, reporting the correct dimensions but incorrectly saying something changed (due to having landscape views or other software-reasons this may be triggered).
After the few unnecessary messages, it will start sending legitimate messages. The device will report the correct "to" orientation, but it will still give the current frame (and bounds), which is invalid.
To scroll the page to the right, other SO questions lead me to remember to manually set the contentOffset, which has most of what I need. currentPage = current Y offset / width of scrollview. Basic math, cool.
That fixed most of my problems. For proper OO, I gave the scroll view a relayout function, which isn't quite the size of the iPad since it's a subview, and in it I do this:
float currentDeviceWidth = 768;
float currentHeight = 949; //logging the frame from portrait, landscape = 1024 w, 693 h
UIDeviceOrientation o = [[UIDevice currentDevice] orientation];
if (!UIDeviceOrientationIsPortrait(o)) {
//landscape
currentDeviceWidth = 1024;
currentHeight = 693;
}
else
NSLog(#"Moving to port, unless on startup, then its staying as");
Which handles the orientationDidChange: messages, but when the app starts, the scroll view is now smaller than it should be because it set its size as if the bounds it's getting are "about to change", when they're not.
Possible solutions:
a) [self performSelector:#selector(relayout) withObject:nil afterDelay:delayNum];
b) give a time delay before something like "BOOL dontIgnoreLayout" is set/unset
c) Find another way to test for orientation
d) manually go in and find out what's causing all the messages to be sent initially, remove all causes - this option is only possible if it's not created by the system on bootup. I can test this with a fresh project in a sec, though it's an enterprise app, may take some ripping if so.
Wondering if anyone solved this. If not, I have to put in "special case" or "time based" code, neither of which are OO style, at least not my preferred way (nor my coworkers).
If I missed something on SO, let me know, but as you can see, I have found a few answers thus far.
Thanks
I followed other people's examples by testing for Status Bar Orientation instead, it's seemingly always right, and shows conflicting messages with UIDevice currentOrientation... I know this has been found before, but the specific reason this fixed the problem is:
As per suggestions in other threads, I registered for notifications in ViewDidAppear and not viewDidLoad, this combination seems to be the win.