Важно Форуму RPGRUSSIA 15 лет!
  • 2.339
  • 19
Друзья, сегодня нашему форуму исполняется 15 лет! Кажется, только вчера мы открывали первые разделы, спорили о правилах и радовались каждому новому участнику. Но годы пролетели - а мы всё здесь, и...
Новости Path of Exile 2: Патч 0.2.0 «Dawn of the Hunt» - краткое описание
  • 1.337
  • 0
Вчера вечером, в 22.00 по МСК, в прямом эфире вышла презентация по будущему патчу 0.2.0. В целом, игроки ждали нового класса и ребаланса существующих умений, но то что выкатили GGG на публику...
Новости Gothic 1 Remake - Demo (Nyras Prologue)
  • 4.797
  • 2
Ну что, заключённые, готовы к встрече с колонией? Мир, где каждый встречный мечтает вас зарезать за кусок хлеба, а единственный закон - сила. Вас ждёт совершенно новый пролог к легендарной...
Новости Большое интервью с HotA Crew - часть 2
  • 3.066
  • 0
HotA Crew о Кронверке и будущих обновлениях (часть 2) Какие герои будут вести армии Кронверка? Герои-воины зовутся Вожди, маги — Старейшины. Их параметры и способности подчеркнут сильные стороны...
Использование виртуальных таблиц для расширения NPC: регенерация, стамина и сейв

Гайд Использование виртуальных таблиц для расширения NPC: регенерация, стамина и сейв v.1.0

Нет прав доступа на загрузку
Расширяем NPC через VTable
Платформа игры
Gothic ½
Автор(ы)
@Gratt
Общий смысл виртуальной таблицы (vtable)
Виртуальные таблицы (или vtable) нужны для того, чтобы при вызове виртуальной функции программа могла понять, какую именно реализацию этой функции нужно вызвать — от базового класса или от производного. Этот процесс происходит в два шага:
  1. Определяется смещение к таблице виртуальных функций (vtable), на которую указывает указатель, установленный в конструкторе.
  2. По нужному смещению в этой таблице находится адрес соответствующей реализации функции.
Код:
class A {
  virtual void print() { cmd << "is A" << endl; }
};

class B : public A {
  virtual void print() { cmd << "is B" << endl; }
};

void func() {
  A* a = new B(); // Указатель на vtable будет установлен в конструкторе B
  a->print();           // Вызовется print из B по смещению 0 в vtable B
}

Теперь разберёмся подробнее, как формируется таблица. В примере ниже — базовый класс с пятью виртуальными методами. Таблица будет содержать пять адресов функций, отсортированных по порядку объявления.

Когда создаётся объект ClassBase, в него помещается указатель на vtable ClassBase.

Теперь рассмотрим наследника ClassA. Хотя у него явно определены только два метода, его таблица всё равно содержит пять слотов. Неопределённые методы просто указывают на реализацию из базового класса. А для переопределённых методов — свои адреса.

Таким образом, создавая экземпляр ClassA, в нём будет ссылка на vtable ClassA.
Код:
class ClassBase {
  virtual void Func1();
  virtual void Func2();
  virtual void Func3();
  virtual void Func4();
  virtual void Func5();
};

class ClassA : public ClassBase {
  virtual void Func3();
  virtual void Func4();
};

// Виртуальные таблицы.
[vtable ClassBase]
call void ClassBase::Func1
call void ClassBase::Func2
call void ClassBase::Func3
call void ClassBase::Func4
call void ClassBase::Func5

[vtable ClassA]
call void ClassBase::Func1
call void ClassBase::Func2
call void ClassA::Func3
call void ClassA::Func4
call void ClassBase::Func5

Постановка задачи
Чтобы пример был не теоретическим, реализуем два полноценных нативных класса, которые будут работать в движке Gothic напрямую — без единого хука.

Первым идёт новый класс oCNpcEx, унаследованный от oCNpc, с дополнительными возможностями:
  1. Если камера подойдёт слишком близко к NPC — он станет полупрозрачным.
  2. При зажатой ПКМ персонаж начнёт бегать быстрее.
  3. Добавим регенерацию здоровья, маны и стамины.
  4. Сделаем возможность сохранять/загружать новые параметры.
  5. Добавим возможность временной блокировки регенерации по событию.

Вторым будет класс oCObjectFactoryEx, унаследованный от oCObjectFactory. Это ключевая фабрика, которая отвечает за создание игровых объектов.

Что в нём меняем:
  1. Переопределим виртуальный метод CreateNpc, чтобы он создавал наш oCNpcEx вместо стандартного NPC.
  2. Свяжем новый класс с движком, передав его в глобальный экземпляр zfactory.

Заготовка классов: oCNpcEx и oCObjectFactoryEx
Чтобы движок принял новые классы как "родные", нужно правильно описать интерфейс. Для этого используем zCLASS_UNION_DECLARATION — он задаёт базовую структуру, важную для сериализации и жизненного цикла объекта. А подключаем реализацию через zCLASS_UNION_DEFINITION.

Заготовка классов .h файл:
Код:
namespace Gothic_II_Addon {
  class oCNpcEx : public oCNpc {
  public:
    zCLASS_UNION_DECLARATION( oCNpcEx );
  };

  class oCObjectFactoryEx : public oCObjectFactory {
    zCLASS_UNION_DECLARATION( oCObjectFactoryEx )
  public:
    virtual oCNpc* CreateNpc( int index ); // Создание только наших NPC
  };
}
Реализация .cpp файл:
Код:
namespace Gothic_II_Addon {
  // В определении интерфейса нам важны первые два параметра. Целевой класс и его предок
  zCLASS_UNION_DEFINITION( oCNpcEx, oCNpc, 0, 0 );
  zCLASS_UNION_DEFINITION( oCObjectFactoryEx, oCObjectFactory, 0, 0 );

  // Реализация виртуального метода, создающего наших NPC
  oCNpc* oCObjectFactoryEx::CreateNpc( int index ) {
    oCNpc* npc = new oCNpcEx();

    if( index != zPAR_INDEX_UNDEF )
      npc->InitByScript( index, 0 );

    return npc;
  }
}

Теперь нужно связать наш класс-фабрику с движком. Для этого создаём функцию, которая подменит глобальный zfactory на нашу реализацию, и вызываем её из Game_DefineExternals.

Подключение в движке:
Код:
// Внутри namespace Gothic_II_Addon
void OnInitFactory() {
   zfactory = new oCObjectFactoryEx();
}

// В глобальном пространстве имён
void GameGlobal_OnInitFactory() {
   Gothic_II_Addon::OnInitFactory();
}

// В Application.cpp
extern void GameGlobal_OnInitFactory();
void Game_DefineExternals() {
   GameGlobal_OnInitFactory();
}

Что в итоге?
Код уже можно собирать. При старте новой игры движок будет использовать oCObjectFactoryEx и создавать NPC как экземпляры oCNpcEx.

При сохранении игры движок также корректно сохранит ссылку на наш класс — и при загрузке из сейва он будет работать без проблем.

Функция отсечения NPC камерой
6LVtXkDFAC.gif


В игре уже функционируют наши NPC, так что мы можем свободно взаимодействовать с ними. Для начала реализуем механизм, при котором мешающийся в фокусе NPC будет становиться прозрачным.
Для этого добавим в класс oCNpcEx виртуальный метод ProcessNpc, который будет вызываться каждый кадр для всех NPC.
Код:
// .h file
  class oCNpcEx : public oCNpc {
  public:
    zCLASS_UNION_DECLARATION( oCNpcEx );
    CTimer TimerAI;

    virtual void ProcessNpc();
  };

Формула отсечения следующая: если камера входит в область, равную 0.8 от радиуса bbox’а, модель начинает выцветать. Полная прозрачность наступает при 0.5 от этой дистанции. Главный герой не участвует в этом алгоритме.
Код:
void oCNpcEx::ProcessNpc() {
 // Привязка таймера к циклу NPC
 TimerAI.Attach();

 if( this != player ) {
  // Определяем bbox модели. Вычисляем центр,
  // прибавляя локальный центр к позиции в мире.
  zTBBox3D BBox3D = GetModel()->bbox3D;
  zVEC3 VobCenter = GetPositionWorld() + BBox3D.GetCenter();
  zVEC3 CameraPosition = ogame->GetCameraVob()->GetPositionWorld();
  float DistanceToCamera = VobCenter.Distance( CameraPosition );

  // Считаем радиус через длину вектора (maxs - mins).
  // Далее берём 0.8 от этой длины — начало исчезновения,
  // и 0.5 от неё — конец (полная прозрачность).
  float FadeDistanceBegin = ( BBox3D.maxs - BBox3D.mins ).Length() * 0.8f;
  float FadeDistanceEnd = FadeDistanceBegin * 0.5f;

  // Если персонаж вне зоны — полностью видимый.
  if( DistanceToCamera > FadeDistanceBegin ) {
   if( visualAlphaEnabled ) {
    visualAlpha = 1.0f;
    visualAlphaEnabled = False;
   }
  }
  else {
   if( !visualAlphaEnabled )
    visualAlphaEnabled = True;

   // Если ближе ближнего порога — полностью прозрачный.
   if( DistanceToCamera < FadeDistanceEnd )
    visualAlpha = 0.0f;
   else {
    // Прозрачность в пределах порога по формуле.
    float FadeLengthMax = FadeDistanceBegin - FadeDistanceEnd;
    float FadeLength = DistanceToCamera - FadeDistanceEnd;
    visualAlpha = 1.0f / FadeLengthMax * FadeLength;
   }
  }
 }

 // Вызов родной реализации после нашего алгоритма
 oCNpc::ProcessNpc();
}

Спринт
Эта функция предназначена для главного героя. Мы создадим невиртуальную функцию ProcessSprint, которая будет вызываться из ProcessNpc. По условию: при зажатой ПКМ запускается спринт, ускоряющий анимацию бега в 1.5 раза.
Код:
// .h file
class oCNpcEx : public oCNpc {
public:
 zCLASS_UNION_DECLARATION( oCNpcEx );

 CTimer TimerAI;

 int LockRegenStaminaTime; // Блокировка восстановления стамины (секунды)

 bool32 SprintEnabled;   // Активен ли спринт
 int StaminaMax;      // Максимум стамины
 int Stamina;        // Текущая стамина

 oCNpcEx();        // Конструктор по умолчанию
 void ProcessSprint();  // Обработка спринта
 virtual void ProcessNpc();
};

// .cpp file
oCNpcEx::oCNpcEx() : oCNpc() {
 LockRegenStaminaTime = 0;
 SprintEnabled = False;
 StaminaMax = Stamina = 40;
}

void oCNpcEx::ProcessSprint() {
 // ID таймера трат стамины
 static const uint SpendStaminaID = 0;
 if( this != player )
  return;

 // Получаем анимацию бега по fmode
 zCModelAni* RunAni = GetModel()->GetAniFromAniID( anictrl->s_runl[fmode] );
 bool32 AniIsActive = GetModel()->IsStateActive( RunAni );
 bool32 CanSprint = zinput->GetMouseButtonPressedRight() && Stamina && AniIsActive;

 // Активация
 if( CanSprint && !SprintEnabled ) {
  RunAni->fpsRate = RunAni->fpsRateSource * 1.5;
  SprintEnabled = True;
 }
 // Деактивация
 else if( !CanSprint && SprintEnabled ) {
  RunAni->fpsRate = RunAni->fpsRateSource;
  SprintEnabled = False;
 }

 // Привязка таймера к игровому времени
 TimerAI.Suspend( SpendStaminaID, ogame->singleStep );

 // Трата стамины каждые 100мс при активном спринте
 if( SprintEnabled ) {
  LockRegenStaminaTime = 2;
  if( Stamina && TimerAI( SpendStaminaID, 100, TM_PRIMARY ) )
   Stamina–;
 }

 // Используем бар задержки дыхания для отображения стамины
 if( Stamina != StaminaMax ) {
  screen->InsertItem( ogame->swimBar );

  ogame->swimBar->SetMaxRange( 0, StaminaMax );
  ogame->swimBar->SetRange( 0, StaminaMax );
  ogame->swimBar->SetPreview( Stamina );
  ogame->swimBar->SetValue( Stamina );

  // Рендер и удаление
  ogame->swimBar->Render();
  screen->RemoveItem( ogame->swimBar );
 }
}

void oCNpcEx::ProcessNpc() {
 // [… Предыдущий код …]
 ProcessSprint();
}

Регенерация атрибутов
Теперь займёмся восстановлением характеристик. Расширим класс, добавив необходимые поля и реализуем две функции: ProcessRegen — отвечает за регенерацию, и OnDamage — вызывается в момент получения урона, временно блокируя восстановление. Здоровье будет приостанавливаться на 5 секунд, а мана — на 2.
Код:
// .h file
class oCNpcEx : public oCNpc {
public:
  zCLASS_UNION_DECLARATION( oCNpcEx );

  CTimer TimerAI;

  // Эти параметры определяют, сколько единиц в секунду будет восстанавливаться
  float RegenHpIntensity;
  float RegenManaIntensity;
  float RegenStaminaIntensity;

  // Эти переменные указывают, сколько секунд ещё запрещена регенерация
  int LockRegenHpTime;
  int LockRegenManaTime;
  int LockRegenStaminaTime;

  bool32 SprintEnabled;
  int StaminaMax;
  int Stamina;

  oCNpcEx();
  void ProcessSprint();
  void ProcessRegen(); // Новая функция для регенерации (не виртуальная)
  virtual void ProcessNpc();
  virtual void OnDamage( oSDamageDescriptor& damage ); // Вызывается при получении урона
};
Код:
// .cpp file
oCNpcEx::oCNpcEx() : oCNpc() {
  RegenHpIntensity         = 1.0f;
  RegenManaIntensity       = 1.0f;
  RegenStaminaIntensity    = 1.0f;
  LockRegenHpTime          = 0;
  LockRegenManaTime        = 0;
  LockRegenStaminaTime     = 0;
  SprintEnabled            = False;
  StaminaMax = Stamina     = 40;
}

void oCNpcEx::ProcessRegen() {
  // Уникальные идентификаторы таймеров для каждой из характеристик
  static const uint RegenHpID         = 1;
  static const uint RegenManaID       = 2;
  static const uint RegenStaminaID    = 3;
  static const uint UnlockID          = 4;

  // При паузе в игре таймеры также приостанавливаются
  TimerAI.Suspend( RegenHpID,         ogame->singleStep );
  TimerAI.Suspend( RegenManaID,       ogame->singleStep );
  TimerAI.Suspend( RegenStaminaID,    ogame->singleStep );
  TimerAI.Suspend( UnlockID,          ogame->singleStep );

  // Если регенерация HP не заблокирована и текущее значение меньше максимума
  if( !LockRegenHpTime && attribute[NPC_ATR_HITPOINTS] < attribute[NPC_ATR_HITPOINTSMAX] ) {
    // Вычисляем интервал времени на основе интенсивности
    int HpIntensity = ( 1000.0f / RegenHpIntensity );
    if( TimerAI( RegenHpID, HpIntensity ) )
      attribute[NPC_ATR_HITPOINTS]++;
  }

  // Аналогичная логика для регенерации маны
  if( !LockRegenManaTime && attribute[NPC_ATR_MANA] < attribute[NPC_ATR_MANAMAX] ) {
    int ManaIntensity = ( 1000.0f / RegenManaIntensity );
    if( TimerAI( RegenManaID, ManaIntensity ) )
      attribute[NPC_ATR_MANA]++;
  }

  // И для выносливости
  if( !LockRegenStaminaTime && Stamina < StaminaMax ) {
    int StaminaIntensity = ( 1000.0f / RegenStaminaIntensity );
    if( TimerAI( RegenStaminaID, StaminaIntensity ) )
      Stamina++;
  }

  // Отдельный таймер, который каждую секунду уменьшает блокировки
  if( TimerAI( UnlockID, 1000 ) ) {
    if( LockRegenHpTime )
      LockRegenHpTime--;

    if( LockRegenManaTime )
      LockRegenManaTime--;

    if( LockRegenStaminaTime )
      LockRegenStaminaTime--;
  }
}

void oCNpcEx::OnDamage( oSDamageDescriptor& damage ) {
  oCNpc::OnDamage( damage ); // Вызываем стандартную обработку урона
  LockRegenHpTime    = 5;    // Запрещаем восстановление здоровья на 5 секунд
  LockRegenManaTime  = 2;    // И маны на 2 секунды
}

void oCNpcEx::ProcessNpc() {
  // [... код, не относящийся к теме, опущен]
  ProcessRegen(); // Добавляем обработку регенерации
}

Сохранение и загрузка значений
Поскольку значения новых полей постоянно меняются во время игры, их нужно сохранять в файл. Для этого определим виртуальные методы Archive и Unarchive.
Код:
// .h file
class oCNpcEx : public oCNpc {
public:
  zCLASS_UNION_DECLARATION( oCNpcEx );

  CTimer TimerAI;
  float RegenHpIntensity;
  float RegenManaIntensity;
  float RegenStaminaIntensity;

  int LockRegenHpTime;
  int LockRegenManaTime;
  int LockRegenStaminaTime;

  bool32 SprintEnabled;
  int StaminaMax;
  int Stamina;

  oCNpcEx();
  void ProcessSprint();
  void ProcessRegen();
  virtual void ProcessNpc();
  virtual void OnDamage( oSDamageDescriptor& damage );
  virtual void Archive( zCArchiver& ar );     // Вызывается при записи NPC в сохранение
  virtual void Unarchive( zCArchiver& ar );   // Вызывается при загрузке NPC из сохранения
};
Код:
// .cpp file
void oCNpcEx::Archive( zCArchiver& ar ) {
  oCNpc::Archive( ar ); // Сначала сохраняем базовые поля NPC

  // Сохраняем значения новых полей
  ar.WriteFloat( "REGENHPINTENSITY",        RegenHpIntensity );
  ar.WriteFloat( "REGENMANAINTENSITY",      RegenManaIntensity );
  ar.WriteFloat( "REGENSTAMINAINTENSITY",   RegenStaminaIntensity );
  ar.WriteInt   ( "LOCKREGENHPTIME",        LockRegenHpTime );
  ar.WriteInt   ( "LOCKREGENMANATIME",      LockRegenManaTime );
  ar.WriteInt   ( "LOCKREGENSTAMINATIME",   LockRegenStaminaTime );
  ar.WriteInt   ( "STAMINAMAX",             StaminaMax );
  ar.WriteInt   ( "STAMINA",                Stamina );
}

void oCNpcEx::Unarchive( zCArchiver& ar ) {
  oCNpc::Unarchive( ar ); // Загружаем базовые поля NPC

  // Загружаем значения новых переменных
  ar.ReadFloat( "REGENHPINTENSITY",        RegenHpIntensity );
  ar.ReadFloat( "REGENMANAINTENSITY",      RegenManaIntensity );
  ar.ReadFloat( "REGENSTAMINAINTENSITY",   RegenStaminaIntensity );
  ar.ReadInt   ( "LOCKREGENHPTIME",        LockRegenHpTime );
  ar.ReadInt   ( "LOCKREGENMANATIME",      LockRegenManaTime );
  ar.ReadInt   ( "LOCKREGENSTAMINATIME",   LockRegenStaminaTime );
  ar.ReadInt   ( "STAMINAMAX",             StaminaMax );
  ar.ReadInt   ( "STAMINA",                Stamina );
}

Заключение
Пример показал, как можно реализовать дополнительную функциональность, не затрагивая память напрямую и не используя хаки. Создание наследников возможно от любых классов — хоть от oCItem, хоть от zCRenderer. Это простой, но гибкий способ модификации игрового поведения средствами движка.
Симпатии: Toshbam
Автор
GeorG
Загрузок
0
Тип файла
7z
Размер файла
3,1 КБ
Первый релиз
Новые обновления
Оценка
0,00 звезда(ы) 0 оценки(ок)

Другие ресурсы от GeorG

Сверху Снизу