GameObject и длииииииииинные асинхронные операции
Хорошо, когда игровой объект писать легко и приятно – простой линейный код радует глаз скриптера. Всё легко, красиво, понятно.
[code lang="cpp"]
// если мы стоим на земле
if ( RayTrace ( Vector3(0,-1,0),2 ) )
PlaySound(
[/code]
Рис.1
Только очень и очень медленно. RayTrace – медленная функция. Хуже, система коллизий хорошо умеет трассировать десятка два лучей сразу. Ещё хуже – система коллизий умеет работать асинхронно (на другой нитке, на SPU, как-то ещё), и возвращать результат тогда, когда он готов.
Аналогично PlaySound тоже не играет звук мгновенно. Объект добавляется в спискок для проигрывания звуков, а потом кто-то где-то его играет.
Одно из типичных решений – сделать из игрового объекта стейт-машину; мелко нарезать, размешать, замучаться отлаживать.
[code lang="cpp"]
...
beginTrace:
result = RayTrace ( Vector3(0,-1,-),2 );
switchToState(waitForResult);
return;
...
waitForResult:
if ( result.IsReady() )
switchToState(continueWorking);
return;
...
[/code]
Если есть удобный и кошерный скрипт (наприме LUA), то можно пользоваться сопрограммами.
Если хочется писать на С++, то забивать гвозди можно fiber-ами. Завести fifo объектов, вызывать fiber-ы пока все не закончат. Тех, кто не закончил, класть в начало fifo. Тех кто закончил больше не обрабатывать. Кстати, примерно так-же GPU от NVIDIA обрабатывает пиксели.
После чего длинные функции переписываются в терминах yield-а, а код остаётся таким-же простым и линейным, как на Рис.1
[code lang="cpp"]
Vector3 RayTrace ( Vector3 dir, float length )
{
result = Physics::RayTrace(dir,length);
while ( !result.IsReady() )
Yield();
return result.value;
}
[/code]
Плохонький пример реализации ниже. Идею, впрочем, он вполне себе иллюстрирует.
[code lang="cpp"]
/// игровой объект
class CGameObject
{
friend class CGameObjectManager;
public:
CGameObject()
: m_fiber(NULL)
, m_isDone(false)
{};
protected:
/// вот эту функцию мы перекрываем
virtual void OnTick() = 0;
/// подождать результата
void Yield();
/// закончили с кадром
void Done() { m_isDone = true; }
private:
CGameObjectManager * m_pManager; ///< менеджер этого объекта
LPVOID m_fiber; ///< его файбер
bool m_isDone; ///< OnTick для данного кадра кончился?
};
/// менеджер игровых объектов
class CGameObjectManager
{
friend class CGameObject;
public:
CGameObjectManager();
~CGameObjectManager();
/// основной игровой цикл
void OnTick();
/// добавить объект в обработку в этом цикле
void PushBack ( CGameObject * pObj );
private:
/// запускаем объект в отдельном fiber-е
static void __stdcall TickGameObject ( LPVOID pObj );
private:
std::list
LPVOID m_mainFiber; ///< главный fiber
};
void CGameObject::Yield()
{
SwitchToFiber(m_pManager->m_mainFiber);
}
CGameObjectManager::CGameObjectManager()
{
ConvertThreadToFiber(NULL);
m_mainFiber = GetCurrentFiber();
}
CGameObjectManager::~CGameObjectManager()
{
ConvertFiberToThread();
m_mainFiber = NULL;
}
void __stdcall CGameObjectManager::TickGameObject ( LPVOID pObj )
{
CGameObject * pGameObj = (CGameObject *) pObj;
pGameObj->m_isDone = false;
SwitchToFiber(pGameObj->m_pManager->m_mainFiber);
// вот в этом цикле теперь и будет крутиться наш объект
for ( ;; )
{
pGameObj->OnTick();
pGameObj->Done();
pGameObj->Yield();
}
}
void CGameObjectManager::PushBack ( CGameObject * pObj )
{
if ( pObj->m_fiber==NULL )
{
// если объект ещё никто не обрабатывал
// то для него надо сделать fiber
pObj->m_pManager = this;
pObj->m_fiber = CreateFiber ( 65536, CGameObjectManager::TickGameObject, pObj );
SwitchToFiber ( pObj->m_fiber );
}
pObj->m_isDone = false;
m_mainFifo.push_back(pObj);
}
void CGameObjectManager::OnTick()
{
// пока fifo не пустая
while ( m_mainFifo.size() )
{
// вынимаем из fifo
CGameObject * pObj = *m_mainFifo.begin();
m_mainFifo.pop_front();
// обрабатываем
SwitchToFiber(pObj->m_fiber);
// если не закончили, засовываем опять в fifo
if ( !pObj->m_isDone )
m_mainFifo.push_back(pObj);
}
}
//////////////////////////////////////////////////////////////////////////
class CMyObj : public CGameObject
{
private:
int m_id;
public:
CMyObj ( int id ) : m_id(id) {};
virtual void OnTick ()
{
printf ( "%i: begin tick\n", m_id );
printf ( "%i: try 1\n", m_id );
int res = SomeLongRoutine();
if ( res>50 )
{
printf ( "%i: try 2\n", m_id );
res = SomeLongRoutine();
}
printf ( "%i: res=%i, done\n", m_id, res );
}
int SomeLongRoutine ( void )
{
// здесь мы эмулируем процедуру, которая долго ждёт результата
Yield();
static int value = 13;
value += 117;
int result = value;
Yield();
return result % 100;
}
};
//////////////////////////////////////////////////////////////////////////
int main()
{
std::vector
for ( int i=0; i<5; i++ )
allObjects[i] = new CMyObj(i);
CGameObjectManager manager;
for ( ;; )
{
printf ( "Game loop\n" );
for ( int i=0; i<5; i++ )
manager.PushBack(allObjects[i]);
manager.OnTick();
printf ( "\n" );
}
return 0;
}
[/code]
Замечу, что наличие стека означает, что большую часть состояния объекта можно (и нужно) хранить на нём. И вместо OnTick делать ещё более длинную функцию, на весь LifeTime объекта. Код высокого уровня (пойти из точки а в точку б, итп) – будет такой-же линейный и простой. Как если-бы объект существовал в единственном экземпляре.
Такой подход предполагает целый ряд (решаемых) сложностей во взаимодействии с другими объектами, но про это я писать не буду :) Выводов тоже не будет.
Ну и, наконец, внятные файберы вполне себе можно реализовать на setjmp\longjmp – если на платформе их почему-то нет. Вполне себе дешевые будут.
P.S. Стек конечно можно и нужно мельче. Пример-то плохонький :)