Cocos2d-x实时更新游戏对象

游戏是一个实时、动态的模拟系统,每一刻,游戏中各个对象的状态都可能发生改变。
因此,游戏系统使用一种游戏循环机制来更新游戏对象的状态。
游戏循环使用一定的时间频率来更新游戏对象的状态。以及各种游戏引擎子系统(物理、动画、渲染)。
游戏引擎的更新机制不仅要解决性能问题,还要保证各个子系统及逻辑更新的时序。

帧率

帧率是指程序中画面的绘制速度,通常称为每秒帧数(FPS),即每秒的周期数。Cocos2d-x中,默认以60fps进行绘制。

通过Director::setAnimationInterval()方法来设置帧率,修改的参数是帧率的倒数,即每两帧之间的间隔。
director->setAnimationInterval(1.0 / 60);
帧率决定了游戏循环以怎样的间隔进行更新。Application::run驱动着游戏循环。
Application的实现很简单: 在执行一次循环后,如果距离下一次更新还有空闲时间,则休眠,直至下一次循环更新的时间。

Scheduler

游戏引擎还提供了种更新机制用于更新自定义的各种游戏对象。
开发者通过向Scheduler注册一个回调函数来更新逻辑。
Scheduler提供了两种类型的回调更新。
1.类型与游戏循环的帧率保持一致,通过scheduleUpdate()方法注册。

1
2
3
4
5
6
7
8
9
10
11
12
class CC_DLL Scheduler : public Ref
{
public:
template <class T>

void scheduleUpdate(T *target, int priority, bool paused)
{
this->schedulePerFrame([target](float dt){
target->update(dt);
}, target, priority, paused);
}
};

使用这种方法可以指定一个更新的优先级,Scheduler会按照priority值从小到大的顺序执行更新回调。
2.通过schedule()方法注册自定义的更新回调。

1
2
3
4
5
6
class CC_DLL Scheduler : public Ref
{
public:
void schedule(const ccSchedulerFunc& callback, void *target, float interval, unsigned int repeat, float delay, bool paused, const std::string& key);
void schedule(const ccSchedulerFunc& callback, void *target, float interval, bool paused, const std::string& key);
};

其中,target指定需要更新的对象;callback指定回调方法;interval设置自定义更新间隔;repeat指定有限次数的更新,而不是永久更新;paused设置更新将被暂停,直至重新将其设置为truekey则可以为一个更新注册指定一个检索的标签。
在实现上,每个自定义的更新回调需要使用一个Timer类来计时,而这将花费更多的内存及计算时间,且不能指定更新的优先级。

时间线

通过Scheduler设置timeScale来设置其相对时间线。在默认情况下,timeScale的值为1.0,表示与游戏循环更新频率一致。通过修改timeScale,如果其值小于1.0,则将产生减慢的效果,如果其值大于1.0,则会产生加速的效果。在实现上,update()方法直接将间隔时间乘以timeScale的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
void Scheduler::update(float dt)
{
_updateHashLocked = true;

if (_timeScale != 1.0f)
{
dt *= _timeScale;
}

//
// Selector callbacks
//

// Iterate over all the Updates' selectors
tListEntry *entry, *tmp;

// updates with priority < 0
DL_FOREACH_SAFE(_updatesNegList, entry, tmp)
{
if ((! entry->paused) && (! entry->markedForDeletion))
{
entry->callback(dt);
}
}

// updates with priority == 0
DL_FOREACH_SAFE(_updates0List, entry, tmp)
{
if ((! entry->paused) && (! entry->markedForDeletion))
{
entry->callback(dt);
}
}

// updates with priority > 0
DL_FOREACH_SAFE(_updatesPosList, entry, tmp)
{
if ((! entry->paused) && (! entry->markedForDeletion))
{
entry->callback(dt);
}
}

// Iterate over all the custom selectors
for (tHashTimerEntry *elt = _hashForTimers; elt != nullptr; )
{
_currentTarget = elt;
_currentTargetSalvaged = false;

if (! _currentTarget->paused)
{
// The 'timers' array may change while inside this loop
for (elt->timerIndex = 0; elt->timerIndex < elt->timers->num; ++(elt->timerIndex))
{
elt->currentTimer = (Timer*)(elt->timers->arr[elt->timerIndex]);
elt->currentTimerSalvaged = false;

elt->currentTimer->update(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 = nullptr;
}
}

// 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 (_currentTargetSalvaged && _currentTarget->timers->num == 0)
{
removeHashElement(_currentTarget);
}
}

// delete all updates that are marked for deletion
// updates with priority < 0
DL_FOREACH_SAFE(_updatesNegList, entry, tmp)
{
if (entry->markedForDeletion)
{
this->removeUpdateFromHash(entry);
}
}

// updates with priority == 0
DL_FOREACH_SAFE(_updates0List, entry, tmp)
{
if (entry->markedForDeletion)
{
this->removeUpdateFromHash(entry);
}
}

// updates with priority > 0
DL_FOREACH_SAFE(_updatesPosList, entry, tmp)
{
if (entry->markedForDeletion)
{
this->removeUpdateFromHash(entry);
}
}

_updateHashLocked = false;
_currentTarget = nullptr;

#if CC_ENABLE_SCRIPT_BINDING
//
// Script callbacks
//

// Iterate over all the script callbacks
if (!_scriptHandlerEntries.empty())
{
for (auto i = _scriptHandlerEntries.size() - 1; i >= 0; i--)
{
SchedulerScriptHandlerEntry* eachEntry = _scriptHandlerEntries.at(i);
if (eachEntry->isMarkedForDeletion())
{
_scriptHandlerEntries.erase(i);
}
else if (!eachEntry->isPaused())
{
eachEntry->getTimer()->update(dt);
}
}
}
#endif
//
// Functions allocated from another thread
//

// Testing size is faster than locking / unlocking.
// And almost never there will be functions scheduled to be called.
if( !_functionsToPerform.empty() ) {
_performMutex.lock();
// fixed #4123: Save the callback functions, they must be invoked after '_performMutex.unlock()', otherwise if new functions are added in callback, it will cause thread deadlock.
auto temp = _functionsToPerform;
_functionsToPerform.clear();
_performMutex.unlock();
for( const auto &function : temp ) {
function();
}

}
}

逻辑更新优先级

Node基类提供了更方便的放来来注册更新回调。

1
2
3
4
5
6
void Node::scheduleUpdate(){
scheduleUpdateWithPriority(0);
}
void Node::scheduleUpdateWithPriority(int priority){
_scheduler->scheduleUpdate(this, priority, !_running);
}