Gratt
Модостроитель
- Регистрация
- 14 Ноя 2014
- Сообщения
- 3.300
- Благодарности
- 4.633
- Баллы
- 625
- Первое сообщение
- #1
Общий смысл vtable
Смысл виртуальных таблиц заключается в том, что вызов функции происходит в два этапа - 1. смещение к виртуальной таблице класса, в конструкторе которого был определен указатель, 2. определение адреса реализации по смещению в таблице.
Рассмотрим чуть более подробно о генерации таблиц. В коде ниже представлены два класса, где базовый имеет 5 виртуальных функций. Таблица получит 5 адресов на реализации, смещение которых будет равным порядку объявления методов. То есть при создании экземпляра от ClassBase в переменную будет записан указатель к `vtable ClassBase`.
Теперь разберемся с производным ClassA. Казалось бы, по коду у него 2 функции. В действительности их столько же, сколько у базового. Если посмотрим в таблицу этого класса, то увидим, что необъявленные функции будут вызывать реализацию от ClassBase. А func3, func4 - собственную. Получается, что создавая экземпляр ClassA в переменную запишется смещение к `vtable ClassA`.
Постановка задачи
Так вот. Чтобы пример не был пресным, реализуем 2 нативных класса, которые будут работать в движке без единого хука.
В первую очередь напишем класс oCNpcEx, унаследованный от oCNpc, и внедрим в него ряд новых возможностей:
1. Если камера находится слишком близко к NPC, то сделаем его прозрачным.
2. При удерживании ПКМ научим персонажа ускорять темп бега.
3. Регенерация здоровья, маны, стамины.
4. Чтение/Запись изменяемых данных в сохранение.
5. Добавим блокировки регенерации в заданных событиях.
Во-вторых создадим производный класс oCObjectFactoryEx от oCObjectFactory, который используется движком для создания экземпляров тех или иных классов:
1. Добавим собственную реализацию виртуальной функции CreateNpc через которую будем создавать экземпляры oCNpcEx, другие наc интересовать не будут.
2. Запишем в игровой экземпляр zfactory новое значение oCObjectFactoryEx.
I. Болванка класса oCNpcEx и реализация oCObjectFactoryEx
Чтобы игра приняла класс как свой родной, опишем внутри него интерфейс zCLASS_UNION_DECLARATION. Он добавит стандартные жизненно важные свойства и функции.
А чтобы реализация вошла в силу, в качестве определения используется zCLASS_UNION_DEFINITION.
Теперь необходимо связать класс oCObjectFactoryEx с движком. Создадим функцию, которая будет присваивать указателю движка значение нового экземпляра. А вызовем мы его из Game_DefineExternals в Application.cpp.
Данный код уже можно скомпилировать. Во время старта новой игры будут создаваться экземпляры NPC от класса oCNpcEx через функцию oCObjectFactoryEx::CreateNpc.
Также после сохранения движок запомнит новый класс и будет подгружать его из сейва.
II. Функция отсечения NPC камерой
Поскольку в игре УЖЕ работают наши NPC, то и делать с ними можно все что захотим . Для начала реализуем алгоритм, при котором NPC, мешающийся в фокусе, будет отсекаться путем изменения прозрачности модели.
В класс oCNpcEx добавим виртуальный метод ProcessNpc. Он вызывается каждую отрисовку кадра для всех NPC.
Формула отсечения будет такой: Если камера входит в область равную 0.8 от радиуса bbox'а, то модель начинает выцветать с продолжительностью до 0.5 от точки начала выцветания.
Также исключим из алгоритма главного героя.
III. Спринт
Данная функция будет использоваться только в рамках главного героя. Создадим невиртуальную функцию ProcessSprint, которую будем вызывать из предыдущей ProcessNpc.
По условию нам необходимо зажать ПКМ для активации спритна. Сам спринт будет ускорять штатную анимацию бега в 1.5 раза.
IV. Регенерация атрибутов
И теперь к восстановлению показателей. Доопределим оставшиеся поля класса.
Введем 2 функции: ProcessRegen для процедуры регенерации и OnDamage, которая срабатывает в момент получения урона. В ней будем приостанавливать регенерацию здоровья на 5 секунд и маны на 2 секунды.
V. Сохранение и загрузка значений
Новые поля классов являются динамическими и могут меняться непрерывно в течение всего игрового процесса. Будет правильно, если мы будем записывать их состояния в файл сохранения.
Добавляем 2 виртуальный метода Archive и Unarchive.
Все. Компилируем и начинаем новую игру.
VI. Заключение
Следуя данному примеру можно реализовать собственные классы от любого другого. Будь то oCItem или zCRenderer. Да, фактически рендер тоже можно повесить на другие рельсы, будь время и желание. А пока такой вот простенький класс, с которым можно делать что угодно и как угодно.
Собственно главная мысль, которую я пытался передать, - демонстрация нативности инструментов. Без костылей, без патчинга памяти и даже без хуков. так что дерзайте. Исходники выложу ниже.
Смысл виртуальных таблиц заключается в том, что вызов функции происходит в два этапа - 1. смещение к виртуальной таблице класса, в конструкторе которого был определен указатель, 2. определение адреса реализации по смещению в таблице.
C++:
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(); // Программа обратится к виртуальной таблице класса B со смещением 0
// и возьмет реализацию с выводом на экран "is B".
}
Рассмотрим чуть более подробно о генерации таблиц. В коде ниже представлены два класса, где базовый имеет 5 виртуальных функций. Таблица получит 5 адресов на реализации, смещение которых будет равным порядку объявления методов. То есть при создании экземпляра от ClassBase в переменную будет записан указатель к `vtable ClassBase`.
Теперь разберемся с производным ClassA. Казалось бы, по коду у него 2 функции. В действительности их столько же, сколько у базового. Если посмотрим в таблицу этого класса, то увидим, что необъявленные функции будут вызывать реализацию от ClassBase. А func3, func4 - собственную. Получается, что создавая экземпляр ClassA в переменную запишется смещение к `vtable ClassA`.
C++:
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
Постановка задачи
Так вот. Чтобы пример не был пресным, реализуем 2 нативных класса, которые будут работать в движке без единого хука.
В первую очередь напишем класс oCNpcEx, унаследованный от oCNpc, и внедрим в него ряд новых возможностей:
1. Если камера находится слишком близко к NPC, то сделаем его прозрачным.
2. При удерживании ПКМ научим персонажа ускорять темп бега.
3. Регенерация здоровья, маны, стамины.
4. Чтение/Запись изменяемых данных в сохранение.
5. Добавим блокировки регенерации в заданных событиях.
Во-вторых создадим производный класс oCObjectFactoryEx от oCObjectFactory, который используется движком для создания экземпляров тех или иных классов:
1. Добавим собственную реализацию виртуальной функции CreateNpc через которую будем создавать экземпляры oCNpcEx, другие наc интересовать не будут.
2. Запишем в игровой экземпляр zfactory новое значение oCObjectFactoryEx.
I. Болванка класса oCNpcEx и реализация oCObjectFactoryEx
Чтобы игра приняла класс как свой родной, опишем внутри него интерфейс zCLASS_UNION_DECLARATION. Он добавит стандартные жизненно важные свойства и функции.
А чтобы реализация вошла в силу, в качестве определения используется zCLASS_UNION_DEFINITION.
C++:
// .h file
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 file
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;
}
}
Теперь необходимо связать класс oCObjectFactoryEx с движком. Создадим функцию, которая будет присваивать указателю движка значение нового экземпляра. А вызовем мы его из Game_DefineExternals в Application.cpp.
C++:
// .cpp file
namespace Gothic_II_Addon {
// [ . . . Предыдущая реализация]
void OnInitFactory() {
zfactory = new oCObjectFactoryEx();
}
}
// global namespace
void GameGlobal_OnInitFactory() {
Gothic_II_Addon::OnInitFactory();
}
// Application.cpp
extern void GameGlobal_OnInitFactory();
void Game_DefineExternals() {
GameGlobal_OnInitFactory();
}
Данный код уже можно скомпилировать. Во время старта новой игры будут создаваться экземпляры NPC от класса oCNpcEx через функцию oCObjectFactoryEx::CreateNpc.
Также после сохранения движок запомнит новый класс и будет подгружать его из сейва.
II. Функция отсечения NPC камерой
В класс oCNpcEx добавим виртуальный метод ProcessNpc. Он вызывается каждую отрисовку кадра для всех NPC.
C++:
// .h file
class oCNpcEx : public oCNpc {
public:
zCLASS_UNION_DECLARATION( oCNpcEx );
CTimer TimerAI;
virtual void ProcessNpc();
};
Формула отсечения будет такой: Если камера входит в область равную 0.8 от радиуса bbox'а, то модель начинает выцветать с продолжительностью до 0.5 от точки начала выцветания.
Также исключим из алгоритма главного героя.
C++:
void oCNpcEx::ProcessNpc() {
// Привязка таймера к циклу NPC
TimerAI.Attach();
if( this != player ) {
// Определяем bbox модели. Вычисляем его фактический центр
// суммированием локального центра с положенем NPC в мире.
zTBBox3D BBox3D = GetModel()->bbox3D;
zVEC3 VobCenter = GetPositionWorld() + BBox3D.GetCenter();
zVEC3 CameraPosition = ogame->GetCameraVob()->GetPositionWorld();
float DistanceToCamera = VobCenter.Distance( CameraPosition );
// Далее вычисляем общий радиус модели через длину разницы
// максимума и минимума координат. По условию нас интересует
// 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;
// Если камера находится внутри ближней к NPC границе отсечения,
// то прозрачность персонажа всегда 0
if( DistanceToCamera < FadeDistanceEnd )
visualAlpha = 0.0f;
// Иначе работаем по формулам отношений, где определяем
// количество прозрачности в заданном положении камеры.
else {
float FadeLengthMax = FadeDistanceBegin - FadeDistanceEnd;
float FadeLength = DistanceToCamera - FadeDistanceEnd;
visualAlpha = 1.0f / FadeLengthMax * FadeLength;
}
}
}
// После работы нашего алгоритма
// также можем выполнить и родной.
oCNpc::ProcessNpc();
}
III. Спринт
Данная функция будет использоваться только в рамках главного героя. Создадим невиртуальную функцию ProcessSprint, которую будем вызывать из предыдущей ProcessNpc.
По условию нам необходимо зажать ПКМ для активации спритна. Сам спринт будет ускорять штатную анимацию бега в 1.5 раза.
C++:
// .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) из списка анимаций anictrl, где s_runl - массив идентификаторов.
// Далее проверяем активна ли анимация текущего 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();
}
IV. Регенерация атрибутов
И теперь к восстановлению показателей. Доопределим оставшиеся поля класса.
Введем 2 функции: ProcessRegen для процедуры регенерации и OnDamage, которая срабатывает в момент получения урона. В ней будем приостанавливать регенерацию здоровья на 5 секунд и маны на 2 секунды.
C++:
// .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() {
// ID таймеров регенерации
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 );
// Для каждого блока проверяется, не стоит ли блокировка на
// регенерацию и меньше ли параметр чем достигаемое значение
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++;
}
// А этот таймер снимает блокировки с регенов.
// Каждую секунду он понижает блокировки на 1.
// И когда значение становится 0 - регенерация
// снова начинает работать.
if( TimerAI( UnlockID, 1000 ) ) {
if( LockRegenHpTime )
LockRegenHpTime--;
if( LockRegenManaTime )
LockRegenManaTime--;
if( LockRegenStaminaTime )
LockRegenStaminaTime--;
}
}
void oCNpcEx::OnDamage( oSDamageDescriptor& damage ) {
oCNpc::OnDamage( damage ); // Вызываем оригинальный обработчик урона в классе oCNpc
LockRegenHpTime = 5; // Приостановка регенерации здоровья на 5 секунд
LockRegenManaTime = 2; // Приостановка регенерации здоровья на 2 секунды
}
void oCNpcEx::ProcessNpc() {
// [. . . Предыдущий код]
ProcessRegen();
}
V. Сохранение и загрузка значений
Новые поля классов являются динамическими и могут меняться непрерывно в течение всего игрового процесса. Будет правильно, если мы будем записывать их состояния в файл сохранения.
Добавляем 2 виртуальный метода Archive и Unarchive.
C++:
// .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 ) {
// Сохраняем оригинальные данные NPC в сейв
oCNpc::Archive( ar );
// Сохраняем наши данные в сейв
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 ) {
// Читаем оригинальные данные NPC из сейва
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 );
}
VI. Заключение
Следуя данному примеру можно реализовать собственные классы от любого другого. Будь то oCItem или zCRenderer. Да, фактически рендер тоже можно повесить на другие рельсы, будь время и желание. А пока такой вот простенький класс, с которым можно делать что угодно и как угодно.
Собственно главная мысль, которую я пытался передать, - демонстрация нативности инструментов. Без костылей, без патчинга памяти и даже без хуков. так что дерзайте. Исходники выложу ниже.
Вложения
Последнее редактирование: