/* * cocos2d for iPhone: http://www.cocos2d-iphone.org * * Copyright (c) 2008-2010 Ricardo Quesada * Copyright (c) 2011 Zynga Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ // cocos2d imports #import "CCScheduler.h" #import "ccMacros.h" #import "Support/uthash.h" #import "Support/utlist.h" #import "Support/ccCArray.h" // // Data structures // #pragma mark - #pragma mark Data Structures // A list double-linked list used for "updates with priority" typedef struct _listEntry { struct _listEntry *prev, *next; TICK_IMP impMethod; id target; // not retained (retained by hashUpdateEntry) NSInteger priority; BOOL paused; BOOL markedForDeletion; // selector will no longer be called and entry will be removed at end of the next tick } tListEntry; typedef struct _hashUpdateEntry { tListEntry **list; // Which list does it belong to ? tListEntry *entry; // entry in the list id target; // hash key (retained) UT_hash_handle hh; } tHashUpdateEntry; // Hash Element used for "selectors with interval" typedef struct _hashSelectorEntry { struct ccArray *timers; id target; // hash key (retained) unsigned int timerIndex; CCTimer *currentTimer; BOOL currentTimerSalvaged; BOOL paused; UT_hash_handle hh; } tHashSelectorEntry; // // CCTimer // #pragma mark - #pragma mark - CCTimer @implementation CCTimer @synthesize interval; -(id) init { NSAssert(NO, @"CCTimer: Init not supported."); [self release]; return nil; } +(id) timerWithTarget:(id)t selector:(SEL)s { return [[[self alloc] initWithTarget:t selector:s] autorelease]; } +(id) timerWithTarget:(id)t selector:(SEL)s interval:(ccTime) i { return [[[self alloc] initWithTarget:t selector:s interval:i] autorelease]; } -(id) initWithTarget:(id)t selector:(SEL)s { return [self initWithTarget:t selector:s interval:0]; } -(id) initWithTarget:(id)t selector:(SEL)s interval:(ccTime) seconds { if( (self=[super init]) ) { #if COCOS2D_DEBUG NSMethodSignature *sig = [t methodSignatureForSelector:s]; NSAssert(sig !=0 , @"Signature not found for selector - does it have the following form? -(void) name: (ccTime) dt"); #endif // target is not retained. It is retained in the hash structure target = t; selector = s; impMethod = (TICK_IMP) [t methodForSelector:s]; elapsed = -1; interval = seconds; } return self; } - (NSString*) description { return [NSString stringWithFormat:@"<%@ = %08X | target:%@ selector:(%@)>", [self class], self, [target class], NSStringFromSelector(selector)]; } -(void) dealloc { CCLOGINFO(@"cocos2d: deallocing %@", self); [super dealloc]; } -(void) update: (ccTime) dt { if( elapsed == - 1) elapsed = 0; else elapsed += dt; if( elapsed >= interval ) { impMethod(target, selector, elapsed); elapsed = 0; } } @end // // CCScheduler // #pragma mark - #pragma mark - CCScheduler @interface CCScheduler (Private) -(void) removeHashElement:(tHashSelectorEntry*)element; @end @implementation CCScheduler static CCScheduler *sharedScheduler; @synthesize timeScale = timeScale_; + (CCScheduler *)sharedScheduler { if (!sharedScheduler) sharedScheduler = [[CCScheduler alloc] init]; return sharedScheduler; } +(id)alloc { NSAssert(sharedScheduler == nil, @"Attempted to allocate a second instance of a singleton."); return [super alloc]; } +(void)purgeSharedScheduler { [sharedScheduler release]; sharedScheduler = nil; } - (id) init { if( (self=[super init]) ) { timeScale_ = 1.0f; // used to trigger CCTimer#update updateSelector = @selector(update:); impMethod = (TICK_IMP) [CCTimer instanceMethodForSelector:updateSelector]; // updates with priority updates0 = NULL; updatesNeg = NULL; updatesPos = NULL; hashForUpdates = NULL; // selectors with interval currentTarget = nil; currentTargetSalvaged = NO; hashForSelectors = nil; updateHashLocked = NO; } return self; } - (void) dealloc { CCLOG(@"cocos2d: deallocing %@", self); [self unscheduleAllSelectors]; sharedScheduler = nil; [super dealloc]; } #pragma mark CCScheduler - Custom Selectors -(void) removeHashElement:(tHashSelectorEntry*)element { ccArrayFree(element->timers); [element->target release]; HASH_DEL(hashForSelectors, element); free(element); } -(void) scheduleSelector:(SEL)selector forTarget:(id)target interval:(ccTime)interval paused:(BOOL)paused { NSAssert( selector != nil, @"Argument selector must be non-nil"); NSAssert( target != nil, @"Argument target must be non-nil"); tHashSelectorEntry *element = NULL; HASH_FIND_INT(hashForSelectors, &target, element); if( ! element ) { element = calloc( sizeof( *element ), 1 ); element->target = [target retain]; HASH_ADD_INT( hashForSelectors, target, element ); // Is this the 1st element ? Then set the pause level to all the selectors of this target element->paused = paused; } else NSAssert( element->paused == paused, @"CCScheduler. Trying to schedule a selector with a pause value different than the target"); if( element->timers == nil ) element->timers = ccArrayNew(10); else { for( unsigned int i=0; i< element->timers->num; i++ ) { CCTimer *timer = element->timers->arr[i]; if( selector == timer->selector ) { CCLOG(@"CCScheduler#scheduleSelector. Selector already scheduled. Updating interval from: %.2f to %.2f", timer->interval, interval); timer->interval = interval; return; } } ccArrayEnsureExtraCapacity(element->timers, 1); } CCTimer *timer = [[CCTimer alloc] initWithTarget:target selector:selector interval:interval]; ccArrayAppendObject(element->timers, timer); [timer release]; } -(void) unscheduleSelector:(SEL)selector forTarget:(id)target { // explicity handle nil arguments when removing an object if( target==nil && selector==NULL) return; NSAssert( target != nil, @"Target MUST not be nil"); NSAssert( selector != NULL, @"Selector MUST not be NULL"); tHashSelectorEntry *element = NULL; HASH_FIND_INT(hashForSelectors, &target, element); if( element ) { for( unsigned int i=0; i< element->timers->num; i++ ) { CCTimer *timer = element->timers->arr[i]; if( selector == timer->selector ) { if( timer == element->currentTimer && !element->currentTimerSalvaged ) { [element->currentTimer retain]; element->currentTimerSalvaged = YES; } ccArrayRemoveObjectAtIndex(element->timers, i ); // update timerIndex in case we are in tick:, looping over the actions if( element->timerIndex >= i ) element->timerIndex--; if( element->timers->num == 0 ) { if( currentTarget == element ) currentTargetSalvaged = YES; else [self removeHashElement: element]; } return; } } } // Not Found // NSLog(@"CCScheduler#unscheduleSelector:forTarget: selector not found: %@", selString); } #pragma mark CCScheduler - Update Specific -(void) priorityIn:(tListEntry**)list target:(id)target priority:(NSInteger)priority paused:(BOOL)paused { tListEntry *listElement = malloc( sizeof(*listElement) ); listElement->target = target; listElement->priority = priority; listElement->paused = paused; listElement->impMethod = (TICK_IMP) [target methodForSelector:updateSelector]; listElement->next = listElement->prev = NULL; listElement->markedForDeletion = NO; // empty list ? if( ! *list ) { DL_APPEND( *list, listElement ); } else { BOOL added = NO; for( tListEntry *elem = *list; elem ; elem = elem->next ) { if( priority < elem->priority ) { if( elem == *list ) DL_PREPEND(*list, listElement); else { listElement->next = elem; listElement->prev = elem->prev; elem->prev->next = listElement; elem->prev = listElement; } added = YES; break; } } // Not added? priority has the higher value. Append it. if( !added ) DL_APPEND(*list, listElement); } // update hash entry for quicker access tHashUpdateEntry *hashElement = calloc( sizeof(*hashElement), 1 ); hashElement->target = [target retain]; hashElement->list = list; hashElement->entry = listElement; HASH_ADD_INT(hashForUpdates, target, hashElement ); } -(void) appendIn:(tListEntry**)list target:(id)target paused:(BOOL)paused { tListEntry *listElement = malloc( sizeof( * listElement ) ); listElement->target = target; listElement->paused = paused; listElement->markedForDeletion = NO; listElement->impMethod = (TICK_IMP) [target methodForSelector:updateSelector]; DL_APPEND(*list, listElement); // update hash entry for quicker access tHashUpdateEntry *hashElement = calloc( sizeof(*hashElement), 1 ); hashElement->target = [target retain]; hashElement->list = list; hashElement->entry = listElement; HASH_ADD_INT(hashForUpdates, target, hashElement ); } -(void) scheduleUpdateForTarget:(id)target priority:(NSInteger)priority paused:(BOOL)paused { tHashUpdateEntry * hashElement = NULL; HASH_FIND_INT(hashForUpdates, &target, hashElement); if(hashElement) { #if COCOS2D_DEBUG >= 1 NSAssert( hashElement->entry->markedForDeletion, @"CCScheduler: You can't re-schedule an 'update' selector'. Unschedule it first"); #endif // TODO : check if priority has changed! hashElement->entry->markedForDeletion = NO; return; } // most of the updates are going to be 0, that's way there // is an special list for updates with priority 0 if( priority == 0 ) [self appendIn:&updates0 target:target paused:paused]; else if( priority < 0 ) [self priorityIn:&updatesNeg target:target priority:priority paused:paused]; else // priority > 0 [self priorityIn:&updatesPos target:target priority:priority paused:paused]; } - (void) removeUpdateFromHash:(tListEntry*)entry { tHashUpdateEntry * element = NULL; HASH_FIND_INT(hashForUpdates, &entry->target, element); if( element ) { // list entry DL_DELETE( *element->list, element->entry ); free( element->entry ); // hash entry [element->target release]; HASH_DEL( hashForUpdates, element); free(element); } } -(void) unscheduleUpdateForTarget:(id)target { if( target == nil ) return; tHashUpdateEntry * element = NULL; HASH_FIND_INT(hashForUpdates, &target, element); if( element ) { if(updateHashLocked) element->entry->markedForDeletion = YES; else [self removeUpdateFromHash:element->entry]; // // list entry // DL_DELETE( *element->list, element->entry ); // free( element->entry ); // // // hash entry // [element->target release]; // HASH_DEL( hashForUpdates, element); // free(element); } } #pragma mark CCScheduler - Common for Update selector & Custom Selectors -(void) unscheduleAllSelectors { // Custom Selectors for(tHashSelectorEntry *element=hashForSelectors; element != NULL; ) { id target = element->target; element=element->hh.next; [self unscheduleAllSelectorsForTarget:target]; } // Updates selectors tListEntry *entry, *tmp; DL_FOREACH_SAFE( updates0, entry, tmp ) { [self unscheduleUpdateForTarget:entry->target]; } DL_FOREACH_SAFE( updatesNeg, entry, tmp ) { [self unscheduleUpdateForTarget:entry->target]; } DL_FOREACH_SAFE( updatesPos, entry, tmp ) { [self unscheduleUpdateForTarget:entry->target]; } } -(void) unscheduleAllSelectorsForTarget:(id)target { // explicit nil handling if( target == nil ) return; // Custom Selectors tHashSelectorEntry *element = NULL; HASH_FIND_INT(hashForSelectors, &target, element); if( element ) { if( ccArrayContainsObject(element->timers, element->currentTimer) && !element->currentTimerSalvaged ) { [element->currentTimer retain]; element->currentTimerSalvaged = YES; } ccArrayRemoveAllObjects(element->timers); if( currentTarget == element ) currentTargetSalvaged = YES; else [self removeHashElement:element]; } // Update Selector [self unscheduleUpdateForTarget:target]; } -(void) resumeTarget:(id)target { NSAssert( target != nil, @"target must be non nil" ); // Custom Selectors tHashSelectorEntry *element = NULL; HASH_FIND_INT(hashForSelectors, &target, element); if( element ) element->paused = NO; // Update selector tHashUpdateEntry * elementUpdate = NULL; HASH_FIND_INT(hashForUpdates, &target, elementUpdate); if( elementUpdate ) { NSAssert( elementUpdate->entry != NULL, @"resumeTarget: unknown error"); elementUpdate->entry->paused = NO; } } -(void) pauseTarget:(id)target { NSAssert( target != nil, @"target must be non nil" ); // Custom selectors tHashSelectorEntry *element = NULL; HASH_FIND_INT(hashForSelectors, &target, element); if( element ) element->paused = YES; // Update selector tHashUpdateEntry * elementUpdate = NULL; HASH_FIND_INT(hashForUpdates, &target, elementUpdate); if( elementUpdate ) { NSAssert( elementUpdate->entry != NULL, @"pauseTarget: unknown error"); elementUpdate->entry->paused = YES; } } -(BOOL) isTargetPaused:(id)target { NSAssert( target != nil, @"target must be non nil" ); // Custom selectors tHashSelectorEntry *element = NULL; HASH_FIND_INT(hashForSelectors, &target, element); if( element ) { return element->paused; } return NO; // should never get here } #pragma mark CCScheduler - Main Loop -(void) tick: (ccTime) dt { updateHashLocked = YES; if( timeScale_ != 1.0f ) dt *= timeScale_; // Iterate all over the Updates selectors tListEntry *entry, *tmp; // updates with priority < 0 DL_FOREACH_SAFE( updatesNeg, entry, tmp ) { if( ! entry->paused && !entry->markedForDeletion ) entry->impMethod( entry->target, updateSelector, dt ); } // updates with priority == 0 DL_FOREACH_SAFE( updates0, entry, tmp ) { if( ! entry->paused && !entry->markedForDeletion ) { entry->impMethod( entry->target, updateSelector, dt ); } } // updates with priority > 0 DL_FOREACH_SAFE( updatesPos, entry, tmp ) { if( ! entry->paused && !entry->markedForDeletion ) entry->impMethod( entry->target, updateSelector, dt ); } // Iterate all over the custome selectors for(tHashSelectorEntry *elt=hashForSelectors; elt != NULL; ) { currentTarget = elt; currentTargetSalvaged = NO; if( ! currentTarget->paused ) { // The 'timers' ccArray may change while inside this loop. for( elt->timerIndex = 0; elt->timerIndex < elt->timers->num; elt->timerIndex++) { elt->currentTimer = elt->timers->arr[elt->timerIndex]; elt->currentTimerSalvaged = NO; impMethod( elt->currentTimer, updateSelector, dt); if( elt->currentTimerSalvaged ) { // The currentTimer told the remove itself. To prevent the timer from // accidentally deallocating itself before finishing its step, we retained // it. Now that step is done, it's safe to release it. [elt->currentTimer release]; } elt->currentTimer = nil; } } // elt, at this moment, is still valid // so it is safe to ask this here (issue #490) elt = elt->hh.next; // only delete currentTarget if no actions were scheduled during the cycle (issue #481) if( currentTargetSalvaged && currentTarget->timers->num == 0 ) [self removeHashElement:currentTarget]; } // delete all updates that are morked for deletion // updates with priority < 0 DL_FOREACH_SAFE( updatesNeg, entry, tmp ) { if(entry->markedForDeletion ) { [self removeUpdateFromHash:entry]; } } // updates with priority == 0 DL_FOREACH_SAFE( updates0, entry, tmp ) { if(entry->markedForDeletion ) { [self removeUpdateFromHash:entry]; } } // updates with priority > 0 DL_FOREACH_SAFE( updatesPos, entry, tmp ) { if(entry->markedForDeletion ) { [self removeUpdateFromHash:entry]; } } updateHashLocked = NO; currentTarget = nil; } @end