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

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


0 коммент.:

Отправить комментарий