7 июн. 2011 г.

Rule Based Systems


Rule Based Systems (cистема, основанная на правилах) - это система контроля не игровых персонажей (Non-Player Character — NPC), состоящая из ситуаций и действий (ЕСЛИ - ТО). RBS относится к рефлексивным детерминированным методам, а значит можно полностью предсказать реакцию поведения. Это, пожалуй, самый простой способ реализации ИИ в играх. Так как он прост, то и задача, с которой он должен справляться, тоже должна быть проста.

При реализации AI в этой статье (и в последующих тоже), я буду использовать скрипты на LUA. Очень часто приходится корректировать/менять/улучшать поведение NPC, поэтому использование скриптового языка LUA тут очень кстати.

Давайте сформулируем очень простую задачу:
  • Нашими NPC будут квадратики.
  • Врагом NPC будет курсор.
  • Как только враг будет в поле зрения, NPC будет убегать от него, пока не упрется в преграду.

Работу программы, которую мы создадим, можно посмотреть тут:


Перейдем к реализации. Так как мы будем использовать LUA для скриптинга, то давайте с ним ознакомимся. Его исходники можно скачать тут, так же нам потребуется LuaBind. Я не буду рассказывать, как их собирать и подключать, это можно найти на других сайтах, поэтому перейдем сразу к делу.

// создаем состояние lua
lua_State* pLua = lua_open();
// инициализация luabind
luabind::open(pLua);
// функция, которая открывает все стандартные библиотеки
luaL_openlibs(pLua);
// если ошибка при создании
if (NULL == pLua)
{
printf("Error Initializing lua\n");
return;
}
// загружает указанный файл
luaL_dofile(pLua, "ai.lua");
// эта функция расшаривает функции и классы С++,
// которые мы будем использовать в скрипте
LuaShareNPC(pLua);

Функцию "LuaShareNPC", которую мы использовали выше, мы рассмотрим позже. Так же не забудьте в конце программы, использовать функцию, которая закроет состояние lua:

lua_close(pLua);

Создадим новый класс "CNpc", который мы унаследуем от "CPathUser" (этот класс отвечает за перемещение, смотрите его реализацию в предыдущих статьях).

class CNpc: public CPathUser
{
private:
// необходимое время для паузы
int m_iPouseNeed;
// сколько времени уже прошло с последнего апдейта
int m_iPouseNow;
// имя скрипта
std::string m_sScriptName;
public:
CNpc(void);
~CNpc(void);
// установка имени скрипта
void SetScriptName(std::string sScriptName);
// установка времени для паузы между обновлением AI
void SetPauseTime(int iMilliSecond);
// видим ли врага
bool IsSeeEnemy(vector2f Target);
// рассчитываем путь, чтобы убежать от врага
void RunFromEnemy(vector2f EnemyPos);
// функция обновления
void Update(int iDeltaMilliSeconds);
};

Конструктор, деструктор, установка времени для паузы и имени скрипта.

CNpc::CNpc()
{
m_iPouseNow = 0;
m_iPouseNeed = 1000;
}

CNpc::~CNpc()
{
}

void CNpc::SetPauseTime(int iMilliSecond)
{
m_iPouseNeed = iMilliSecond;
}

void CNpc::SetScriptName(std::string sScriptName)
{
m_sScriptName = sScriptName;
}

Следующая функция возвращает true, если враг виден. VIEW_RADIUS - это радиус взора нашего NPC, устанавливаем значение этого параметра по своему желанию (у меня он равен 200).

bool CNpc::IsSeeEnemy(vector2f EnemyPos) 
{
vector2f Diff = GetPos() - EnemyPos;
float Len = length(Diff);
if (Len < VIEW_RADIUS)
return true;
return false;
}

Функция задания нового пути, следуя по которому, мы будем убегать от врага.

void CNpc::RunFromEnemy(vector2f EnemyPos) 
{
vector2f Diff = normalize(GetPos() - EnemyPos);
SetWay(Diff*CHUNK_SIZE*2 + GetPos());
}

Функция обновления нашего NPC. Обратите внимание, что скрипт срабатывает не каждый кадр, а в зависимости от параметра m_iPouseNeed, значение которому мы устанавливаем опять же по желанию (у меня это 300 миллисекунд). Таким способом мы уменьшаем нагрузку на наш компьютер и создаем иллюзию того, что NPC "думает" перед принятием решения. При такой реализации нам придется функцию передвижения использовать не в скрипте (иначе он бы у нас ходил раз в 300 миллисекунд).

void CNpc::Update(int iDeltaMilliSeconds)
{
m_iPouseNow += iDeltaMilliSeconds;
if (m_iPouseNow > m_iPouseNeed)
{
if (myLua)
// вызываем функцию из скрипта
luabind::call_function< void >(myLua, m_sScriptName.c_str(), this, CurPos);
m_iPouseNow = 0;
}
// передвигаемся
Move(iDeltaMilliSeconds);
}

Давайте рассмотрим функцию, которая расшаривает классы и функции для их использования в скриптах. Так как задача у нас простая, то нам потребуется всего две функции класса "CNpc", и сам класс "vector2f".


void LuaShareNPC(lua_State* pLua)
{
using namespace luabind;
module(pLua)
[
class_< CNpc >("Npc")
.def("RunFromEnemy", &CNpc::RunFromEnemy )
.def("IsSeeEnemy", &CNpc::IsSeeEnemy ),

class_< vector2f >("vec2")
];
}

И наконец, наш скрипт. Он очень прост: если мы видим врага (ситуация), находим новый путь (действие), чтобы от него удалиться, в противном случае ничего не делаем.


function Update(npc, enemy)
if npc:IsSeeEnemy(enemy) then
npc:RunFromEnemy(enemy)
end
end

Мы убедились, что это достаточно примитивный и простой способ для организации простого ИИ в играх. При большом объеме поставленных задач для ИИ, этот способ крайне неэффективен. В нашем примере всего одно условие и одно действие, как бы одна частичка (по поставленным изначально задачам, нам больше и не надо). При объединении этих частиц получается - автоматизированная система, основанная на правилах (проверках). Как только одно из условий этой цепи возвращает истину, выполняется его действие и выход из системы.


2 июн. 2011 г.

Очередь на поиск пути

Очередь на поиск путей позволит избежать случая, когда за один кадр ищется путь для большого количества объектов, что может вызвать лаг. Эта статья будет теоретической по двум причинам. Во-первых, реализация этого метода очень проста. А во-вторых, алгоритм нахождения пути, который мы разобрали, дает задержку в одну секунду при одновременном поиске 10000 путей за раз. Даже если у нас будет десять тысяч солдат в игре, то вряд ли мы ощутим такой лаг, так как они будут искать путь в разное время. Но все же давайте рассмотрим такой случай.

Чтобы избежать такой задержки, мы создадим очередь на поиск путей в виде массива. Каждый объект, которому нужно будет получить новый путь, будет записываться в эту очередь. Теперь нам надо каждый кадр брать часть запросов (или все запросы, если их мало) и находить для них путь. Но сколько запросов брать для обработки? Я бы не советовал указывать какое-то конкретное число и не стал бы распределять это количество в зависимости от FPS. «Тогда как?» - спросите вы (а если не спросите, то можете двигаться к следующей статье). А все просто, отведем, скажем, время в 100-200 миллисекунд на эту процедуру, после каждого найденного пути проверяем общее потраченное время за этот кадр, и если оно больше нашего заранее определенного, то продолжаем работу программы, а другие «счастливчики» будут ждать следующий кадр.

По своему опыту скажу честно, такого мне делать не приходилось, но если вам это все-таки понадобится, то не забудьте удалять из очереди запросы тех объектов, которые, скажем, погибли.