在cocos2d-x游戏的主循环中,只用了三行语句便完成了整个引擎的调度系统,十分优雅。
void CCDirector::drawScene(void)
{
...
//处理定时器的调用
if (! m_bPaused)
{
m_pScheduler->update(m_fDeltaTime);
}
...
}
在导演类中定义了一个全局的定时器,有导演单独控制,也正是这个定时器,维护着整个引擎的调度。
bool CCDirector::init(void)
{
...
// scheduler
m_pScheduler = new CCScheduler();
...
}
然后再通过主循环每帧的去调度它,从而完成调度。
在此之前得先知道外界是如何注册定时器供引擎调度的。在之前的CCNode文章中就已经提及到CCNode包括调用和停止定时器的功能,也就是说真正的入口还是在节点类,由节点去注册,然后由导演去完成每帧的调度。
其中,CCNode握有导演类的全局定时器的指针:
CCNode::CCNode(void)...
{
...
m_pScheduler = director->getScheduler();
m_pScheduler->retain();
...
}
然后再通过这个全局定时器完成相应的注册和停止等工作,也就是说,CCNode只是对真正的定时器类进行了包装,这样便能隐藏定时器的细节。
//设置一个新的定时器
void CCNode::setScheduler(CCScheduler* scheduler)
{
if( scheduler != m_pScheduler ) {//如果是新的定时器
this->unscheduleAllSelectors();//停止旧定时器的所有调度
CC_SAFE_RETAIN(scheduler);//新定时器的引用计数加1
CC_SAFE_RELEASE(m_pScheduler);//释放掉旧定时器的资源
m_pScheduler = scheduler;//设置新定时器
}
}
//获取定时器
CCScheduler* CCNode::getScheduler()
{
//放回当前的定时器
return m_pScheduler;
}
//注册update类型的定时器
void CCNode::scheduleUpdate()
{
scheduleUpdateWithPriority(0);//调用优先级为0的定时器
}
//注册带有优先级的update类型的定时器
void CCNode::scheduleUpdateWithPriority(int priority)
{
m_pScheduler->scheduleUpdateForTarget(this, priority, !m_bRunning);//调用指定优先级的定时器
}
//lua脚本相关,略过
void CCNode::scheduleUpdateWithPriorityLua(int nHandler, int priority)
{
unscheduleUpdate();
m_nUpdateScriptHandler = nHandler;
m_pScheduler->scheduleUpdateForTarget(this, priority, !m_bRunning);
}
//停止当前节点的update定时器调度
void CCNode::unscheduleUpdate()
{
m_pScheduler->unscheduleUpdateForTarget(this);//停止当前节点的update定时器
if (m_nUpdateScriptHandler)//脚本相关,略过
{
CCScriptEngineManager::sharedManager()->getScriptEngine()->removeScriptHandler(m_nUpdateScriptHandler);
m_nUpdateScriptHandler = 0;
}
}
//注册自定义定时器,每帧调用一次
void CCNode::schedule(SEL_SCHEDULE selector)
{
this->schedule(selector, 0.0f, kCCRepeatForever, 0.0f);//无限次调用指定定时器
}
//注册自定义定时器,每隔interval秒调用一次
void CCNode::schedule(SEL_SCHEDULE selector, float interval)
{
this->schedule(selector, interval, kCCRepeatForever, 0.0f);//无限次的每隔interval时间调用指定定时器
}
//注册自定义定时器,每隔interval秒调用一次,重复调度repeat次,并设置延迟时间delay
void CCNode::schedule(SEL_SCHEDULE selector, float interval, unsigned int repeat, float delay)
{
//检查参数合法化
CCAssert( selector, "Argument must be non-nil");
CCAssert( interval >=0, "Argument must be positive");
m_pScheduler->scheduleSelector(selector, this, interval , repeat, delay, !m_bRunning);//执行调度
}
//延迟delay调用一次
void CCNode::scheduleOnce(SEL_SCHEDULE selector, float delay)
{
this->schedule(selector, 0.0f, 0, delay);//delay时间后调用一次指定定时器
}
//根据选择器停止定时器
void CCNode::unschedule(SEL_SCHEDULE selector)
{
// explicit nil handling
if (selector == 0)//过滤空引用
return;
m_pScheduler->unscheduleSelector(selector, this);//停止指定的定时器
}
//停止该节点的所有定时器
void CCNode::unscheduleAllSelectors()
{
m_pScheduler->unscheduleAllForTarget(this);//停止该节点的所有定时器
}
//恢复该节点的所有定时器和动作
void CCNode::resumeSchedulerAndActions()
{
m_pScheduler->resumeTarget(this);//该节点的定时器重新工作
m_pActionManager->resumeTarget(this);//该节点的动作重新被执行
}
//暂停该节点的所有定时器和动作
void CCNode::pauseSchedulerAndActions()
{
m_pScheduler->pauseTarget(this);//暂停该节点的定时器
m_pActionManager->pauseTarget(this);//暂停该节点的动作
}
//更新函数,一般重写它
void CCNode::update(float fDelta)//一般重写它,在主循环中会自动去调用它
{
if (m_nUpdateScriptHandler)
{
CCScriptEngineManager::sharedManager()->getScriptEngine()->executeSchedule(m_nUpdateScriptHandler, fDelta, this);
}
if (m_pComponentContainer && !m_pComponentContainer->isEmpty())
{
m_pComponentContainer->visit(fDelta);
}
}
可见经过封装后的定时器在节点中使用十分简洁清晰明了。
接下分析真正的定时器类:CCSchedule。
在cocos2d-x中有两种定时器:
1)update定时器:每帧都会被触发,使用scheduleUpdate方法来启动。
2)schedule定时器:根据选择器来自定义的定时器,可以设置触发时间间隔,延迟,重复次数等,使用schedule的相关方法来启动。
其相关的属性和方法如下:
class CC_DLL CCScheduler : public CCObject
{
public:
//构造
CCScheduler();
/**
* @js NA
* @lua NA
*/
//析构
~CCScheduler(void);
//返回时间刻度
inline float getTimeScale(void) { return m_fTimeScale; }
/** Modifies the time of all scheduled callbacks.
You can use this property to create a 'slow motion' or 'fast forward' effect.
Default is 1.0. To create a 'slow motion' effect, use values below 1.0.
To create a 'fast forward' effect, use values higher than 1.0.
@since v0.8
@warning It will affect EVERY scheduled selector / action.
*/
//设置时间刻度,默认为1.0,设置它,可以改变定时器的调度速度
inline void setTimeScale(float fTimeScale) { m_fTimeScale = fTimeScale; }
/** 'update' the scheduler.
* You should NEVER call this method, unless you know what you are doing.
* @js NA
* @lua NA
*/
//update定时器的调度函数
void update(float dt);
/** The scheduled method will be called every 'interval' seconds.
If paused is YES, then it won't be called until it is resumed.
If 'interval' is 0, it will be called every frame, but if so, it's recommended to use 'scheduleUpdateForTarget:' instead.
If the selector is already scheduled, then only the interval parameter will be updated without re-scheduling it again.
repeat let the action be repeated repeat + 1 times, use kCCRepeatForever to let the action run continuously
delay is the amount of time the action will wait before it'll start
@since v0.99.3, repeat and delay added in v1.1
@js NA
@lua NA
*/
//自定义定时器的调度函数
void scheduleSelector(SEL_SCHEDULE pfnSelector, CCObject *pTarget, float fInterval, unsigned int repeat, float delay, bool bPaused);
/** calls scheduleSelector with kCCRepeatForever and a 0 delay
* @js NA
* @lua NA
*/
//自定义定时器的调度函数(repeat = kCCRepeatForever,delay = 0)
void scheduleSelector(SEL_SCHEDULE pfnSelector, CCObject *pTarget, float fInterval, bool bPaused);
/** Schedules the 'update' selector for a given target with a given priority.
The 'update' selector will be called every frame.
The lower the priority, the earlier it is called.
@since v0.99.3
@lua NA
*/
//带有优先级定时器的调度函数
void scheduleUpdateForTarget(CCObject *pTarget, int nPriority, bool bPaused);
/** Unschedule a selector for a given target.
If you want to unschedule the "update", use unscheudleUpdateForTarget.
@since v0.99.3
@lua NA
*/
//根据定时选择器移除定时器的回调
void unscheduleSelector(SEL_SCHEDULE pfnSelector, CCObject *pTarget);
/** Unschedules the update selector for a given target
@since v0.99.3
@lua NA
*/
//根据目标移除定时器的回调
void unscheduleUpdateForTarget(const CCObject *pTarget);
/** Unschedules all selectors for a given target.
This also includes the "update" selector.
@since v0.99.3
@js unscheduleCallbackForTarget
@lua NA
*/
//移除所有带有目标的定时器的回调
void unscheduleAllForTarget(CCObject *pTarget);
/** Unschedules all selectors from all targets.
You should NEVER call this method, unless you know what you are doing.
@since v0.99.3
@js unscheduleAllCallbacks
@lua NA
*/
//移除所有定时器的回调
void unscheduleAll(void);
/** Unschedules all selectors from all targets with a minimum priority.
You should only call this with kCCPriorityNonSystemMin or higher.
@since v2.0.0
@js unscheduleAllCallbacksWithMinPriority
@lua NA
*/
//移除所有小于指定优先级的定时器的回调
void unscheduleAllWithMinPriority(int nMinPriority);
/** The scheduled script callback will be called every 'interval' seconds.
If paused is YES, then it won't be called until it is resumed.
If 'interval' is 0, it will be called every frame.
return schedule script entry ID, used for unscheduleScriptFunc().
@js NA
*/
//脚本相关,略过
unsigned int scheduleScriptFunc(unsigned int nHandler, float fInterval, bool bPaused);
/** Unschedule a script entry.
* @js NA
*/
//脚本相关,略过
void unscheduleScriptEntry(unsigned int uScheduleScriptEntryID);
/** Pauses the target.
All scheduled selectors/update for a given target won't be 'ticked' until the target is resumed.
If the target is not present, nothing happens.
@since v0.99.3
@lua NA
*/
//暂停目标定时器
void pauseTarget(CCObject *pTarget);
/** Resumes the target.
The 'target' will be unpaused, so all schedule selectors/update will be 'ticked' again.
If the target is not present, nothing happens.
@since v0.99.3
@lua NA
*/
//恢复目标定时器
void resumeTarget(CCObject *pTarget);
/** Returns whether or not the target is paused
@since v1.0.0
@lua NA
*/
//目标定时器是否被暂停
bool isTargetPaused(CCObject *pTarget);
/** Pause all selectors from all targets.
You should NEVER call this method, unless you know what you are doing.
@since v2.0.0
@lua NA
*/
//暂停所有目标定时器
CCSet* pauseAllTargets();
/** Pause all selectors from all targets with a minimum priority.
You should only call this with kCCPriorityNonSystemMin or higher.
@since v2.0.0
@lua NA
*/
//暂停所有小于指定优先级的目标定时器
CCSet* pauseAllTargetsWithMinPriority(int nMinPriority);
/** Resume selectors on a set of targets.
This can be useful for undoing a call to pauseAllSelectors.
@since v2.0.0
@lua NA
*/
//恢复set集合中指定的目标定时器
void resumeTargets(CCSet* targetsToResume);
private:
void removeHashElement(struct _hashSelectorEntry *pElement);//从普通定时器散列表移除一个普通定时器
void removeUpdateFromHash(struct _listEntry *entry);//从update定时器散列表中移除一个update定时器
// update specific
void priorityIn(struct _listEntry **ppList, CCObject *pTarget, int nPriority, bool bPaused);//按指定的优先级往链表合适的位置增加一个定时器
void appendIn(struct _listEntry **ppList, CCObject *pTarget, bool bPaused);//往链表追加一个定时器
protected:
float m_fTimeScale;//时间刻度
//
// "updates with priority" stuff
//
struct _listEntry *m_pUpdatesNegList; //优先级小于0的调度器链表
struct _listEntry *m_pUpdates0List; //优先级等于0的调度器链表
struct _listEntry *m_pUpdatesPosList; //优先级大于0的调度器链表
struct _hashUpdateEntry *m_pHashForUpdates; //记录全部update定时器的散列表
// Used for "selectors with interval"
struct _hashSelectorEntry *m_pHashForTimers; //记录全部计时器的散列表
struct _hashSelectorEntry *m_pCurrentTarget; //当前的目标选择器散列表
bool m_bCurrentTargetSalvaged;//回收标记
bool m_bUpdateHashLocked;//是否锁定定时器。如果为true,则在调用unschedule时不会直接在散列表中删除该定时器,而是该定时器仅被标记为已删除在散列表中
CCArray* m_pScriptHandlerEntries;//脚本相关
};
一、update定时器:
update定时器在一个节点中只能注册一个,调用次序主要依据三个保存了update定时器的链表的优先级来执行。
struct _listEntry *m_pUpdatesNegList; //优先级小于0的调度器链表
struct _listEntry *m_pUpdates0List; //优先级等于0的调度器链表
struct _listEntry *m_pUpdatesPosList; //优先级大于0的调度器链表
typedef struct _listEntry//定时器链表结构
{
struct _listEntry *prev, *next; //链表前驱节点,链表后继节点
CCObject *target; //目标
int priority; //优先级
bool paused; //是否暂停
bool markedForDeletion; //是否被标记为删除
} tListEntry;
而为了方便检索这些节点的update定时器,又定义了一个hash散列表来保存这些定时器(本身并不执行调度任务)。
struct _hashUpdateEntry *m_pHashForUpdates; //记录全部update定时器的散列表
typedef struct _hashUpdateEntry//update定时器散列表结构
{
tListEntry **list; //链表容器
tListEntry *entry; //链表元素,即update定时器的链表结构(value)
CCObject *target;//目标(key)
UT_hash_handle hh;//散列表操作工具
} tHashUpdateEntry;
其中以节点类为key,定时器链表结构为value。
文章的开头提过,cocos2d-x游戏的定时器的调度是在主循环中用一个全局定时器来完成调度的,其执行的方法就是CCSchedule::update,它包括了update定时器和自定义定时器的调度:
//主循环调用
void CCScheduler::update(float dt)
{
m_bUpdateHashLocked = true;//锁定定时器,这样检索定时器时保证迭代不会被破坏
if (m_fTimeScale != 1.0f)//如果时间刻度不等于默认的1.0
{
dt *= m_fTimeScale;//调整定时器调度时间
}
// Iterate over all the Updates' selectors
tListEntry *pEntry, *pTmp;//用于迭代链表
// updates with priority < 0
//迭代优先级小于0的定时器
DL_FOREACH_SAFE(m_pUpdatesNegList, pEntry, pTmp)
{
if ((! pEntry->paused) && (! pEntry->markedForDeletion))//如果该定时器没有被暂停和标记已删除
{
pEntry->target->update(dt);//触发定时器调度
}
}
// updates with priority == 0
//迭代优先级等于0的定时器
DL_FOREACH_SAFE(m_pUpdates0List, pEntry, pTmp)
{
if ((! pEntry->paused) && (! pEntry->markedForDeletion))//如果该定时器没有被暂停和标记已删除
{
pEntry->target->update(dt);//触发定时器调度
}
}
// updates with priority > 0
//迭代优先级大于0的定时器
DL_FOREACH_SAFE(m_pUpdatesPosList, pEntry, pTmp)
{
if ((! pEntry->paused) && (! pEntry->markedForDeletion))//如果该定时器没有被暂停和标记已删除
{
pEntry->target->update(dt);//触发定时器调度
}
}
...
// delete all updates that are marked for deletion
// updates with priority < 0
//移除已被标记为删除且优先级小于0的定时器
DL_FOREACH_SAFE(m_pUpdatesNegList, pEntry, pTmp)
{
if (pEntry->markedForDeletion)
{
this->removeUpdateFromHash(pEntry);
}
}
// updates with priority == 0
//移除已被标记为删除且优先级等于0的定时器
DL_FOREACH_SAFE(m_pUpdates0List, pEntry, pTmp)
{
if (pEntry->markedForDeletion)
{
this->removeUpdateFromHash(pEntry);
}
}
// updates with priority > 0
//移除已被标记为删除且优先级大于0的定时器
DL_FOREACH_SAFE(m_pUpdatesPosList, pEntry, pTmp)
{
if (pEntry->markedForDeletion)
{
this->removeUpdateFromHash(pEntry);
}
}
m_bUpdateHashLocked = false;
m_pCurrentTarget = NULL;
}
这里省略了在中间的自定义定时器的执行。
可以看到真个update定时器的调用过程是(通过重写update,并以多态的形式调用,多态真是一个BT的东西):
1)调度优先级高的的定时器(优先级小于0,一般是动作类)
2)调度优先级中等的的定时器(优先级等于0,一般是默认注册的定时器)
3)调度优先级低的的定时器(优先级大于0,一般是自定义优先级的定时器)
4)在三个存储了update定时器的链表中分别检查那些已经被标记为已删除的定时器,并从散列表中删除它,这样这个定时器便被停止了
实现细节:
1)注册update定时器(CCNode在调用scheduleUpdate时最终会调用到这个方法)
void CCScheduler::scheduleUpdateForTarget(CCObject *pTarget, int nPriority, bool bPaused)
{
tHashUpdateEntry *pHashElement = NULL;
HASH_FIND_INT(m_pHashForUpdates, &pTarget, pHashElement);//通过pTarget索引查询哈希表中的相应元素,找到则将其赋给pHashElement
if (pHashElement)//找到元素
{
#if COCOS2D_DEBUG >= 1
CCAssert(pHashElement->entry->markedForDeletion,"");
#endif
// TODO: check if priority has changed!
pHashElement->entry->markedForDeletion = false;//将该元素的删除标记标记为false
return;
}
//没有找到,根据优先级往对应的链表插入一个新的元素
// most of the updates are going to be 0, that's way there
// is an special list for updates with priority 0
if (nPriority == 0)
{
appendIn(&m_pUpdates0List, pTarget, bPaused);
}
else if (nPriority < 0)
{
priorityIn(&m_pUpdatesNegList, pTarget, nPriority, bPaused);
}
else
{
// priority > 0
priorityIn(&m_pUpdatesPosList, pTarget, nPriority, bPaused);
}
}
void CCScheduler::appendIn(_listEntry **ppList, CCObject *pTarget, bool bPaused)
{
tListEntry *pListElement = (tListEntry *)malloc(sizeof(*pListElement));//申请一块内存空间
pListElement->target = pTarget;//设置目标
pListElement->paused = bPaused;//设置暂停
pListElement->markedForDeletion = false;//设置删除标记
DL_APPEND(*ppList, pListElement);//往链表追加一个元素
// update hash entry for quicker access
//向散列表中增加一个节点,用于快速访问
tHashUpdateEntry *pHashElement = (tHashUpdateEntry *)calloc(sizeof(*pHashElement), 1);
pHashElement->target = pTarget;
pTarget->retain();
pHashElement->list = ppList;
pHashElement->entry = pListElement;
HASH_ADD_INT(m_pHashForUpdates, target, pHashElement);
}
void CCScheduler::priorityIn(tListEntry **ppList, CCObject *pTarget, int nPriority, bool bPaused)
{
tListEntry *pListElement = (tListEntry *)malloc(sizeof(*pListElement));//申请一块内存空间
pListElement->target = pTarget;//设置目标
pListElement->priority = nPriority;//设置优先级
pListElement->paused = bPaused;//设置暂停
pListElement->next = pListElement->prev = NULL;//设置前驱和后继节点
pListElement->markedForDeletion = false;//设置删除标记
// empty list ?
if (! *ppList)//链表容器不为空
{
DL_APPEND(*ppList, pListElement);//往链表中增加一个节点
}
else
{
bool bAdded = false;//记录节点是否插在链表的中间或头部
for (tListEntry *pElement = *ppList; pElement; pElement = pElement->next)//遍历链表
{
if (nPriority < pElement->priority)//找到一个优先级比次节点小的节点
{
if (pElement == *ppList)//如果是第一个元素
{
DL_PREPEND(*ppList, pListElement);//将此节点放到链表的头部
}
else
{
pListElement->next = pElement;//将新增加的节点的后继节点指向当前被遍历的节点
pListElement->prev = pElement->prev;//将新增加的节点的后继节点指向当前被遍历的节点的前驱节点
pElement->prev->next = pListElement;//当前被遍历的节点的前驱节点的后继节点指向新增加的节点
pElement->prev = pListElement;//将当前被遍历的节点的前驱节点指向新增加的节点
}
bAdded = true;//该节点已在中间或头部插入
break;
}
}
// Not added? priority has the higher value. Append it.
if (! bAdded)//如果之前的操作中节点没有被增加,则将此节点追加到链表的尾部(即优先级最低)
{
DL_APPEND(*ppList, pListElement);
}
}
// update hash entry for quick access
//向散列表中增加一个节点,用于快速访问
tHashUpdateEntry *pHashElement = (tHashUpdateEntry *)calloc(sizeof(*pHashElement), 1);
pHashElement->target = pTarget;
pTarget->retain();
pHashElement->list = ppList;
pHashElement->entry = pListElement;
HASH_ADD_INT(m_pHashForUpdates, target, pHashElement);
}
2)停止当前节点的update定时器调度(CCNode在调用unscheduleUpdate时最终会调用到这个方法)
void CCScheduler::unscheduleUpdateForTarget(const CCObject *pTarget)
{
if (pTarget == NULL)//目标为空,返回
{
return;
}
tHashUpdateEntry *pElement = NULL;
HASH_FIND_INT(m_pHashForUpdates, &pTarget, pElement);//通过pTarget索引查询哈希表中的相应元素,找到则将其赋给pElement
if (pElement)//找到元素
{
if (m_bUpdateHashLocked)//是否锁定定时器
{
pElement->entry->markedForDeletion = true;//标记删除状态
}
else
{
this->removeUpdateFromHash(pElement->entry);//直接从散列表中移除
}
}
}
void CCScheduler::removeUpdateFromHash(struct _listEntry *entry)
{
tHashUpdateEntry *element = NULL;
HASH_FIND_INT(m_pHashForUpdates, &entry->target, element);//通过entry->target索引查询哈希表中的相应元素,找到则将其赋给element
if (element)//找到元素
{
// list entry
//从链表删除它的元素并释放资源
DL_DELETE(*element->list, element->entry);
free(element->entry);
// hash entry
//从散列表删除它并释放资源
CCObject* pTarget = element->target;
HASH_DEL(m_pHashForUpdates, element);
free(element);
// target#release should be the last one to prevent
// a possible double-free. eg: If the [target dealloc] might want to remove it itself from there
//目标的引用计数减1(在往散列表增加节点时retain了一次,这里被删除,要手动release一次)
pTarget->release();
}
}
3)删除某一个节点update定时器
void CCScheduler::unscheduleUpdateForTarget(const CCObject *pTarget)
{
if (pTarget == NULL)//目标为空,返回
{
return;
}
tHashUpdateEntry *pElement = NULL;
HASH_FIND_INT(m_pHashForUpdates, &pTarget, pElement);//通过pTarget索引查询哈希表中的相应元素,找到则将其赋给pElement
if (pElement)//找到元素
{
if (m_bUpdateHashLocked)//是否锁定定时器
{
pElement->entry->markedForDeletion = true;//标记删除状态
}
else
{
this->removeUpdateFromHash(pElement->entry);//直接从散列表中移除
}
}
}
二、自定义定时器:
这个定时器就相对复杂得多,因为他会涉及到计时器的管理。维护的它的数据结构是两个散列表和一个辅助属性:
struct _hashSelectorEntry *m_pHashForTimers; //记录全部计时器的散列表
struct _hashSelectorEntry *m_pCurrentTarget; //当前的目标选择器散列表
bool m_bCurrentTargetSalvaged;//回收标记
typedef struct _hashSelectorEntry//自定义定时器散列表结构
{
ccArray *timers;//计时器容器(value)
CCObject *target; //目标(key)
unsigned int timerIndex;//计数器索引
CCTimer *currentTimer;//当前的计时器
bool currentTimerSalvaged;//回收标记
bool paused;//是否暂停
UT_hash_handle hh;//散列表操作工具
} tHashTimerEntry;
这个散列表的key是依然是一个调度节点类,value是一个存储了多个计时器的数组。
在分析实现之前得懂得这个计时器的作用。
由于一个节点允许有多个自定义定时器,那么一个节点就要维护这个一对多的关系,所以在上面的上面的散列表用了一个数组才存储它,并用一个指针标明当前所引用的计时器是哪个(在数组中,以选择器来区分同一节点下的不同自定义定时器),而且自定义定时器还允许用户自定义什么时候去调用它(延迟调用)、调用的时间间隔、重复调用次数,所以这个计时器必要保存这些信息。
计时器的头文件:
class CC_DLL CCTimer : public CCObject
{
public:
/**
* @js ctor
* @lua NA
*/
//构造
CCTimer(void);
/** get interval in seconds */
//返回时间间隔(单位:秒)
float getInterval(void) const;
/** set interval in seconds */
//设置时间间隔(单位:秒)
void setInterval(float fInterval);
/**
* @lua NA
*/
//获取定时器
SEL_SCHEDULE getSelector() const;
/** Initializes a timer with a target and a selector.
* @lua NA
*/
//初始化目标定时器的计时器
bool initWithTarget(CCObject *pTarget, SEL_SCHEDULE pfnSelector);
/** Initializes a timer with a target, a selector and an interval in seconds, repeat in number of times to repeat, delay in seconds.
* @lua NA
*/
//初始化目标定时器的计时器
bool initWithTarget(CCObject *pTarget, SEL_SCHEDULE pfnSelector, float fSeconds, unsigned int nRepeat, float fDelay);
/** Initializes a timer with a script callback function and an interval in seconds. */
//脚本相关,略过
bool initWithScriptHandler(int nHandler, float fSeconds);
/** triggers the timer */
//触发计数器调度
void update(float dt);
public:
/** Allocates a timer with a target and a selector.
* @lua NA
*/
static CCTimer* timerWithTarget(CCObject *pTarget, SEL_SCHEDULE pfnSelector);
/** Allocates a timer with a target, a selector and an interval in seconds.
* @lua NA
*/
static CCTimer* timerWithTarget(CCObject *pTarget, SEL_SCHEDULE pfnSelector, float fSeconds);
/** Allocates a timer with a script callback function and an interval in seconds. */
static CCTimer* timerWithScriptHandler(int nHandler, float fSeconds);
/**
* @lua NA
*/
inline int getScriptHandler() { return m_nScriptHandler; };
protected:
CCObject *m_pTarget;//目标
float m_fElapsed;//记录过去的时间
bool m_bRunForever;//是否是永久的调用
bool m_bUseDelay;//是否是延迟调用
unsigned int m_uTimesExecuted;//调用次数
unsigned int m_uRepeat; //重复调用的次数,0为1次,1为2次,以此类推
float m_fDelay;//延迟时间
float m_fInterval;//时间间隔
SEL_SCHEDULE m_pfnSelector;//选择器(目标函数)
int m_nScriptHandler;//脚本相关,略过
};
计时器的实现:
CCTimer::CCTimer()
: m_pTarget(NULL)
, m_fElapsed(-1)
, m_bRunForever(false)
, m_bUseDelay(false)
, m_uTimesExecuted(0)
, m_uRepeat(0)
, m_fDelay(0.0f)
, m_fInterval(0.0f)
, m_pfnSelector(NULL)
, m_nScriptHandler(0)
{
}
CCTimer* CCTimer::timerWithTarget(CCObject *pTarget, SEL_SCHEDULE pfnSelector)
{
CCTimer *pTimer = new CCTimer();//创建一个新的计时器
pTimer->initWithTarget(pTarget, pfnSelector, 0.0f, kCCRepeatForever, 0.0f);//初始化计时器
pTimer->autorelease();//放入内存回收池
return pTimer;
}
CCTimer* CCTimer::timerWithTarget(CCObject *pTarget, SEL_SCHEDULE pfnSelector, float fSeconds)
{
CCTimer *pTimer = new CCTimer();//创建一个新的计时器
pTimer->initWithTarget(pTarget, pfnSelector, fSeconds, kCCRepeatForever, 0.0f);//初始化计时器
pTimer->autorelease();//放入内存回收池
return pTimer;
}
CCTimer* CCTimer::timerWithScriptHandler(int nHandler, float fSeconds)//脚本相关,略过
{
CCTimer *pTimer = new CCTimer();
pTimer->initWithScriptHandler(nHandler, fSeconds);
pTimer->autorelease();
return pTimer;
}
bool CCTimer::initWithScriptHandler(int nHandler, float fSeconds)//脚本相关,略过
{
m_nScriptHandler = nHandler;
m_fElapsed = -1;
m_fInterval = fSeconds;
return true;
}
bool CCTimer::initWithTarget(CCObject *pTarget, SEL_SCHEDULE pfnSelector)
{
return initWithTarget(pTarget, pfnSelector, 0, kCCRepeatForever, 0.0f);//初始化计时器
}
bool CCTimer::initWithTarget(CCObject *pTarget, SEL_SCHEDULE pfnSelector, float fSeconds, unsigned int nRepeat, float fDelay)
{
//初始化工作
m_pTarget = pTarget;//设置目标
m_pfnSelector = pfnSelector;//设置选择器(目标函数)
m_fElapsed = -1;//设置记录过去时间
m_fInterval = fSeconds;//设置时间间隔
m_fDelay = fDelay;//设置延迟时间
m_bUseDelay = (fDelay > 0.0f) ? true : false;//如果延迟时间大于0,则认为是延迟调用
m_uRepeat = nRepeat;//设置重复调用次数
m_bRunForever = (nRepeat == kCCRepeatForever) ? true : false;//设置是否永远重复执行
return true;
}
void CCTimer::update(float dt)
{
if (m_fElapsed == -1)//如果过去的时间被重置-1
{
m_fElapsed = 0;//重新记录过去的用时时间,所以将m_fElapsed置0,开始计时
m_uTimesExecuted = 0;//调用次数
}
else
{
if (m_bRunForever && !m_bUseDelay)//如果是永远的执行且非延迟
{//标准计时器的调用
m_fElapsed += dt;//过去的时间加1
if (m_fElapsed >= m_fInterval)//过去的时间大于设置时间间隔(如一个定时器每隔2秒调用一次,那么被记录的过去时间大于2的话,则认为可以触发该定时器的调用)
{
if (m_pTarget && m_pfnSelector)
{
(m_pTarget->*m_pfnSelector)(m_fElapsed);//触发定时器的调用(m_pfnSelector是个函数指针,m_pTarget是目标对象,m_fElapsed是时间参数)
}
if (m_nScriptHandler)//脚本相关,略过
{
CCScriptEngineManager::sharedManager()->getScriptEngine()->executeSchedule(m_nScriptHandler, m_fElapsed);
}
m_fElapsed = 0;//重新记录过去的用时时间,所以将m_fElapsed置0,重新开始计时
}
}
else
{//update和CCDelay计时器的调用
m_fElapsed += dt;//过去时间加上帧差
if (m_bUseDelay)//如果是延迟调用
{
if( m_fElapsed >= m_fDelay )//当已记录的过去的时间大于延迟时间
{
if (m_pTarget && m_pfnSelector)
{
(m_pTarget->*m_pfnSelector)(m_fElapsed);//触发定时器的调用(m_pfnSelector是个函数指针,m_pTarget是目标对象,m_fElapsed是时间参数)
}
if (m_nScriptHandler)//脚本相关,略过
{
CCScriptEngineManager::sharedManager()->getScriptEngine()->executeSchedule(m_nScriptHandler, m_fElapsed);
}
m_fElapsed = m_fElapsed - m_fDelay;//重置过去时间
m_uTimesExecuted += 1;//调用次数加1
m_bUseDelay = false;//标记延迟调用结束
}
}
else
{
if (m_fElapsed >= m_fInterval)//过去的时间大于设置时间间隔(如一个定时器每隔2秒调用一次,那么被记录的过去时间大于2的话,则认为可以触发该定时器的调用)
{
if (m_pTarget && m_pfnSelector)
{
(m_pTarget->*m_pfnSelector)(m_fElapsed);//触发定时器的调用(m_pfnSelector是个函数指针,m_pTarget是目标对象,m_fElapsed是时间参数)
}
if (m_nScriptHandler)//脚本相关,略过
{
CCScriptEngineManager::sharedManager()->getScriptEngine()->executeSchedule(m_nScriptHandler, m_fElapsed);
}
m_fElapsed = 0;//重新记录过去的用时时间,所以将m_fElapsed置0,重新开始计时
m_uTimesExecuted += 1;//调用次数加1
}
}
if (!m_bRunForever && m_uTimesExecuted > m_uRepeat)//如果是不是永久的重复调用,且调用次数大于重复调用次数的,则停止调用
{ //unschedule timer
CCDirector::sharedDirector()->getScheduler()->unscheduleSelector(m_pfnSelector, m_pTarget);
}
}
}
}
float CCTimer::getInterval() const
{
//返回时间间隔
return m_fInterval;
}
void CCTimer::setInterval(float fInterval)
{
//设置时间间隔
m_fInterval = fInterval;
}
SEL_SCHEDULE CCTimer::getSelector() const
{
//返回选择器指针
return m_pfnSelector;
}
接下来看主循环是如何处理自定义定时器:
//主循环调用
void CCScheduler::update(float dt)
{
m_bUpdateHashLocked = true;//锁定定时器,这样检索定时器时保证迭代不会被破坏
if (m_fTimeScale != 1.0f)//如果时间刻度不等于默认的1.0
{
dt *= m_fTimeScale;//调整定时器调度时间
}
...
// Iterate over all the custom selectors
//迭代自定义定时器
for (tHashTimerEntry *elt = m_pHashForTimers; elt != NULL; )
{
m_pCurrentTarget = elt;
m_bCurrentTargetSalvaged = false;
if (! m_pCurrentTarget->paused)
{
// The 'timers' array may change while inside this loop
for (elt->timerIndex = 0; elt->timerIndex < elt->timers->num; ++(elt->timerIndex))
{
elt->currentTimer = (CCTimer*)(elt->timers->arr[elt->timerIndex]);
elt->currentTimerSalvaged = false;
elt->currentTimer->update(dt);
//如果在update执行了unscheduleXXX时删除该计时器,也就是自己在调度时候通知自己删除自己(计时器数组会改变,尽量避免这样做)
//(注意:这时的计时器数组的大小会发生改变,所以要小心处理timerIndex,另外这里之所以要release是因为这种情况计时器数组在移除计时器时只是从数组中移除掉而已,计时器本身还是存在的,所以要release一下,
//也就是说它在unscheduleXXX时已经执行了retian,之所以要在unscheuleXXX中retian它是因为是在update中通知自己删除自己,为了保证在计时器在调度时不会出错,要将自己retain一下,然后标记
//为回收状态,执行完调度(update)后再回收它)
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 = NULL;
}
}
// elt, at this moment, is still valid
// so it is safe to ask this here (issue #490)
elt = (tHashTimerEntry *)elt->hh.next;//下一个计时器
// only delete currentTarget if no actions were scheduled during the cycle (issue #481)
if (m_bCurrentTargetSalvaged && m_pCurrentTarget->timers->num == 0)//如果该节点没有计时器且被标记为可回收时,从散列表中移除它
{
removeHashElement(m_pCurrentTarget);
}
}
...
m_bUpdateHashLocked = false;
m_pCurrentTarget = NULL;
}
实现细节:
1)注册自定义定时器(CCNode在调用schedule或scheduleOnce时最终会调用到这个方法)
void CCScheduler::scheduleSelector(SEL_SCHEDULE pfnSelector, CCObject *pTarget, float fInterval, unsigned int repeat, float delay, bool bPaused)
{
CCAssert(pfnSelector, "Argument selector must be non-NULL");
CCAssert(pTarget, "Argument target must be non-NULL");
tHashTimerEntry *pElement = NULL;
//通过pTarget索引查询哈希表中的相应元素,找到则将其赋给pElement
HASH_FIND_INT(m_pHashForTimers, &pTarget, pElement);
if (! pElement)
{
//散列表没找到,则申请一块推空间
pElement = (tHashTimerEntry *)calloc(sizeof(*pElement), 1);
pElement->target = pTarget;//设置目标
if (pTarget)
{
pTarget->retain();//将目标的引用计数加1,防止野指针或空悬指针的调用
}
HASH_ADD_INT(m_pHashForTimers, target, pElement);//将元素放入散列表
// Is this the 1st element ? Then set the pause level to all the selectors of this target
pElement->paused = bPaused;//设置暂停
}
else
{
//找到则断言是否被暂停
CCAssert(pElement->paused == bPaused, "");
}
if (pElement->timers == NULL)//如果计时器为空
{
pElement->timers = ccArrayNew(10);//创建默认大小为10的计时器容器
}
else
{//如果已有计时器容器
for (unsigned int i = 0; i < pElement->timers->num; ++i)//遍历计时器容器(确保一个节点的某个选择器只能有一个)
{
CCTimer *timer = (CCTimer*)pElement->timers->arr[i];
if (pfnSelector == timer->getSelector())//如果在目标节点中存在该选择器,则返回
{
CCLOG("CCScheduler#scheduleSelector. Selector already scheduled. Updating interval from: %.4f to %.4f", timer->getInterval(), fInterval);
timer->setInterval(fInterval);//设置计时器时间间隔
return;
}
}
ccArrayEnsureExtraCapacity(pElement->timers, 1);//确保定时器有足够的内存空间,不够则扩展两倍
}
CCTimer *pTimer = new CCTimer();//创建一个新的计时器
pTimer->initWithTarget(pTarget, pfnSelector, fInterval, repeat, delay);//初始化计时器
ccArrayAppendObject(pElement->timers, pTimer);//将新创建的计时器加入到计时器容器中
pTimer->release();//还原pTimer的引用计数为1
}
2)停止当前节点的自定义定时器调度(CCNode在调用unschedule时最终会调用到这个方法)
void CCScheduler::unscheduleSelector(SEL_SCHEDULE pfnSelector, CCObject *pTarget)
{
// explicity handle nil arguments when removing an object
if (pTarget == 0 || pfnSelector == 0)//参数检查
{
return;
}
//CCAssert(pTarget);
//CCAssert(pfnSelector);
tHashTimerEntry *pElement = NULL;
HASH_FIND_INT(m_pHashForTimers, &pTarget, pElement);//通过pTarget索引查询哈希表中的相应元素,找到则将其赋给pElement
if (pElement)//如果找到
{
for (unsigned int i = 0; i < pElement->timers->num; ++i)//遍历计时器容器
{
CCTimer *pTimer = (CCTimer*)(pElement->timers->arr[i]);
if (pfnSelector == pTimer->getSelector())//如果找到对应的要移除的选择器
{
if (pTimer == pElement->currentTimer && (! pElement->currentTimerSalvaged))//当前的调度计时器与即将要执行移除的计时器相等(在调度时通知自己删除自己)且回收标记为false时
{
pElement->currentTimer->retain();//在主循环中处理它(防止计时器在执行调度时调用了unscheduleXXX方法来删除自己而出现的野指针错误)
pElement->currentTimerSalvaged = true;//可回收
}
//注意:从容器中移除时,计时器本身有可能还是存在的,如果当前的调度计时器与即将要执行移除的计时器相等的话计时器是不会被删除掉的,仅仅只是从容器中移除掉而已,当执行完调度后主循环会自动删除它
ccArrayRemoveObjectAtIndex(pElement->timers, i, true);//从计时器容器中移除该计时器
// update timerIndex in case we are in tick:, looping over the actions
if (pElement->timerIndex >= i)//保证主循环遍历时不会出错
{
pElement->timerIndex--;
}
if (pElement->timers->num == 0)//如果该节点的定时器的计时器容器的数量等于0
{
if (m_pCurrentTarget == pElement)//如果正在执行的节点与次遍历的节点相等(也是调度时通知自己删除自己)
{
m_bCurrentTargetSalvaged = true;//标记为当前的目标节点为可回收(主循环会处理它)
}
else
{
removeHashElement(pElement);//从散列表中移除该定时器
}
}
return;
}
}
}
}
void CCScheduler::removeHashElement(_hashSelectorEntry *pElement)
{
cocos2d::CCObject *target = pElement->target;
ccArrayFree(pElement->timers);//释放计时器的资源
HASH_DEL(m_pHashForTimers, pElement);//从散列表中删除该元素
free(pElement);//释放该定时器元素的资源
// make sure the target is released after we have removed the hash element
// otherwise we access invalid memory when the release call deletes the target
// and the target calls removeAllSelectors() during its destructor
target->release();//目标的引用计数减1(在创建一个计时器时已经节点已经被retain了一下,所以在移除时这里也要手动的release一下)
}
1)暂停恢复相关:
void CCScheduler::resumeTarget(CCObject *pTarget)
{
CCAssert(pTarget != NULL, "");
//处理自定义选择器
tHashTimerEntry *pElement = NULL;
HASH_FIND_INT(m_pHashForTimers, &pTarget, pElement);//通过pTarget索引查询哈希表中的相应元素,找到则将其赋给pElement
if (pElement)//找到元素
{
pElement->paused = false;//将暂停标志置为false
}
//处理update选择器
tHashUpdateEntry *pElementUpdate = NULL;
HASH_FIND_INT(m_pHashForUpdates, &pTarget, pElementUpdate);//通过pTarget索引查询哈希表中的相应元素,找到则将其赋给pElementUpdate
if (pElementUpdate)//找到元素
{
CCAssert(pElementUpdate->entry != NULL, "");
pElementUpdate->entry->paused = false;//将暂停标志置为false
}
}
void CCScheduler::pauseTarget(CCObject *pTarget)
{
CCAssert(pTarget != NULL, "");
//处理自定义选择器
tHashTimerEntry *pElement = NULL;
HASH_FIND_INT(m_pHashForTimers, &pTarget, pElement);//通过pTarget索引查询哈希表中的相应元素,找到则将其赋给pElement
if (pElement)//找到元素
{
pElement->paused = true;//将暂停标志置为true
}
//处理update选择器
tHashUpdateEntry *pElementUpdate = NULL;
HASH_FIND_INT(m_pHashForUpdates, &pTarget, pElementUpdate);//通过pTarget索引查询哈希表中的相应元素,找到则将其赋给pElementUpdate
if (pElementUpdate)//找到元素
{
CCAssert(pElementUpdate->entry != NULL, "");
pElementUpdate->entry->paused = true;//将暂停标志置为true
}
}
bool CCScheduler::isTargetPaused(CCObject *pTarget)
{
CCAssert( pTarget != NULL, "target must be non nil" );
//处理自定义选择器
tHashTimerEntry *pElement = NULL;
HASH_FIND_INT(m_pHashForTimers, &pTarget, pElement);
if( pElement )//找到元素,返回暂停标志位
{
return pElement->paused;
}
// We should check update selectors if target does not have custom selectors
//处理update选择器
tHashUpdateEntry *elementUpdate = NULL;
HASH_FIND_INT(m_pHashForUpdates, &pTarget, elementUpdate);
if ( elementUpdate )//找到元素,返回暂停标志位
{
return elementUpdate->entry->paused;
}
//买有找到,返回false
return false; // should never get here
}
CCSet* CCScheduler::pauseAllTargets()
{
return pauseAllTargetsWithMinPriority(kCCPrioritySystem);
}
CCSet* CCScheduler::pauseAllTargetsWithMinPriority(int nMinPriority)
{
//定义一个用于收集小于指定优先级的定时器容器
CCSet* idsWithSelectors = new CCSet();// setWithCapacity:50];
idsWithSelectors->autorelease();
//处理自定义选择器
for(tHashTimerEntry *element = m_pHashForTimers; element != NULL;
element = (tHashTimerEntry*)element->hh.next)//自定义选择器没有优先级,遍历它
{
element->paused = true;//设置暂停标志位为true
idsWithSelectors->addObject(element->target);//加到容器中
}
//处理update选择器,根据指定优先级,分别在对应的链表做相应处理
tListEntry *entry, *tmp;
if(nMinPriority < 0)
{
DL_FOREACH_SAFE( m_pUpdatesNegList, entry, tmp )
{
if(entry->priority >= nMinPriority)
{
entry->paused = true;
idsWithSelectors->addObject(entry->target);
}
}
}
if(nMinPriority <= 0)
{
DL_FOREACH_SAFE( m_pUpdates0List, entry, tmp )
{
entry->paused = true;
idsWithSelectors->addObject(entry->target);
}
}
DL_FOREACH_SAFE( m_pUpdatesPosList, entry, tmp )
{
if(entry->priority >= nMinPriority)
{
entry->paused = true;
idsWithSelectors->addObject(entry->target);
}
}
//返回小于指定优先级的定时器容器
return idsWithSelectors;
}
void CCScheduler::resumeTargets(CCSet* pTargetsToResume)
{
CCSetIterator iter;
for (iter = pTargetsToResume->begin(); iter != pTargetsToResume->end(); ++iter)//恢复容器中指定的定时器
{
resumeTarget(*iter);
}
}
void CCScheduler::unscheduleAllForTarget(CCObject *pTarget)
{
// explicit NULL handling
if (pTarget == NULL)
{
return;
}
//处理自定义定时器
tHashTimerEntry *pElement = NULL;
HASH_FIND_INT(m_pHashForTimers, &pTarget, pElement);//通过pTarget索引查询哈希表中的相应元素,找到则将其赋给pElement
if (pElement)//找到元素
{
if (ccArrayContainsObject(pElement->timers, pElement->currentTimer)
&& (! pElement->currentTimerSalvaged))//调度时自己通知删除自己
{
pElement->currentTimer->retain();//主循环处理它
pElement->currentTimerSalvaged = true;//标记回收状态
}
ccArrayRemoveAllObjects(pElement->timers);//移除该节点的所有计时器
if (m_pCurrentTarget == pElement)//如果正在执行的节点与次遍历的节点相等
{
m_bCurrentTargetSalvaged = true;//标记为当前的目标节点为可回收(主循环会处理它)
}
else
{
removeHashElement(pElement);//从散列表在移除该元素
}
}
// update selector
//移除该节点的定时器调用
unscheduleUpdateForTarget(pTarget);
}
void CCScheduler::unscheduleAll(void)
{
unscheduleAllWithMinPriority(kCCPrioritySystem);
}
void CCScheduler::unscheduleAllWithMinPriority(int nMinPriority)
{
// Custom Selectors
tHashTimerEntry *pElement = NULL;
tHashTimerEntry *pNextElement = NULL;
//计时器停止调用(自定义定时器没有优先级,直接停止调度)
for (pElement = m_pHashForTimers; pElement != NULL;)
{
// pElement may be removed in unscheduleAllSelectorsForTarget
pNextElement = (tHashTimerEntry *)pElement->hh.next;
unscheduleAllForTarget(pElement->target);
pElement = pNextElement;
}
// Updates selectors
//根据优先级在相应的链表中移除定时器
tListEntry *pEntry, *pTmp;
if(nMinPriority < 0)
{
DL_FOREACH_SAFE(m_pUpdatesNegList, pEntry, pTmp)
{
if(pEntry->priority >= nMinPriority)
{
unscheduleUpdateForTarget(pEntry->target);
}
}
}
if(nMinPriority <= 0)
{
DL_FOREACH_SAFE(m_pUpdates0List, pEntry, pTmp)
{
unscheduleUpdateForTarget(pEntry->target);
}
}
DL_FOREACH_SAFE(m_pUpdatesPosList, pEntry, pTmp)
{
if(pEntry->priority >= nMinPriority)
{
unscheduleUpdateForTarget(pEntry->target);
}
}
if (m_pScriptHandlerEntries)
{
m_pScriptHandlerEntries->removeAllObjects();
}
}
以上便是cocos2d-x定时器实现所有细节,熟悉这些细节后便可以分析cocos2d-x的动作类了。