• Уважаемые гости и новички, приветствуем Вас на нашем форуме
    Здесь вы можете найти ответы практически на все свои вопросы о серии игр «Готика» (в том числе различных модах на нее), «Ведьмак», «Ризен», «Древние свитки», «Эра дракона» и о многих других играх. Можете также узнать свежие новости о разработке новых проектов, восхититься творчеством наших форумчан, либо самим показать, что вы умеете. Ну и наконец, можете обсудить общие увлечения или просто весело пообщаться с посетителями «Таверны».

    Чтобы получить возможность писать на форуме, оставьте сообщение в этой теме.
    Удачи!

5. Виртуальная таблица. Пишем новый класс NPC.

Gratt


Модостроитель
Регистрация
14 Ноя 2014
Сообщения
3.301
Благодарности
4.636
Баллы
625
  • Первое сообщение
  • #1
Общий смысл vtable
Смысл виртуальных таблиц заключается в том, что вызов функции происходит в два этапа - 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 камерой
74733
Поскольку в игре УЖЕ работают наши NPC, то и делать с ними можно все что захотим :). Для начала реализуем алгоритм, при котором 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. Да, фактически рендер тоже можно повесить на другие рельсы, будь время и желание. А пока такой вот простенький класс, с которым можно делать что угодно и как угодно.
Собственно главная мысль, которую я пытался передать, - демонстрация нативности инструментов. Без костылей, без патчинга памяти и даже без хуков. так что дерзайте. Исходники выложу ниже.
 

Вложения

  • Sources.7z
    3,1 KB · Просмотры: 152
Последнее редактирование:

Caterpillar

Участник форума
Регистрация
17 Фев 2020
Сообщения
5
Благодарности
0
Баллы
60
Привет,
Код для определения oCObjectFactoryEx::CreateNpc(int) вызывает Access Violation в новой игре. Union 1.0g, Union SDK 1.0f.
Это минимальный код:
C++:
namespace Gothic_II_Addon {
    class oCNpcEx : public oCNpc {
    public:
        zCLASS_UNION_DECLARATION(oCNpcEx);
    };

    class oCObjectFactoryEx : public oCObjectFactory {
    public:
        zCLASS_UNION_DECLARATION(oCObjectFactoryEx);
        virtual oCNpc* CreateNpc(int);
    };
}
C++:
namespace Gothic_II_Addon {
   
    zCLASS_UNION_DEFINITION(oCNpcEx, oCNpc, 0, 0);
    zCLASS_UNION_DEFINITION(oCObjectFactoryEx, oCObjectFactory, 0, 0);


    oCNpc* oCObjectFactoryEx::CreateNpc(int index) {
        /* Does not work:
        oCNpc* npc = new oCNpcEx();

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

        return npc;
        */
        // Does work:
        return oCObjectFactory::CreateNpc(index);
    }


    void OnInitFactory() {
        zfactory = new oCObjectFactoryEx();
    }
}

void GameGlobal_OnInitFactory() {
    Gothic_II_Addon::OnInitFactory();
}
C++:
//...

extern void GameGlobal_OnInitFactory();
void Game_DefineExternals() {
    GameGlobal_OnInitFactory();
}

//...
CApplication* lpApplication = CApplication::CreateRefApplication (
  Enabled(False) Game_Entry,
  Enabled(False) Game_Init,
  Enabled(False) Game_Exit,
  Enabled(False) Game_Loop, 
  Enabled(False) Game_SaveBegin,
  Enabled(False) Game_SaveEnd,
  Enabled(False) Game_LoadBegin_NewGame,
  Enabled(False) Game_LoadEnd_NewGame,
  Enabled(False) Game_LoadBegin_SaveGame,
  Enabled(False) Game_LoadEnd_SaveGame,
  Enabled(False) Game_LoadBegin_ChangeLevel,
  Enabled(False) Game_LoadEnd_ChangeLevel,
  Enabled(False) Game_LoadBegin_Trigger,
  Enabled(False) Game_LoadEnd_Trigger,
  Enabled(False) Game_Pause,
  Enabled(False) Game_Unpause,
  Enabled( True ) Game_DefineExternals
  );
что с этим не так?
 

Gratt


Модостроитель
Регистрация
14 Ноя 2014
Сообщения
3.301
Благодарности
4.636
Баллы
625
Caterpillar, какая версия студии? Собираешь в релизе или дебаге?
 

Caterpillar

Участник форума
Регистрация
17 Фев 2020
Сообщения
5
Благодарности
0
Баллы
60
Caterpillar, какая версия студии? Собираешь в релизе или дебаге?
компилятор VS10, кодирование VS 2017.
В Visual Studio ошибок нет, предыдущий урок работает нормально.
Я пробовал релизе, получаю ошибку LINK1123 при дебаге.
 

Gratt


Модостроитель
Регистрация
14 Ноя 2014
Сообщения
3.301
Благодарности
4.636
Баллы
625
Caterpillar, не важно есть ошибки при компиляции или нет, это не показатель работоспособности.
Исправляй 1123 и компилируй в релизе. Выполни очистку решения если потребуется.
 

Caterpillar

Участник форума
Регистрация
17 Фев 2020
Сообщения
5
Благодарности
0
Баллы
60
Gratt, Я чувствую, что я сделал все, что мог с Visual Studio, но безрезультатно. Затем я попытался собрать с SDK 1.0c вместо SDK 1.0f, и он прекрасно работает в игре. Более сложный код тоже работает, пока проблем нет. Я, вероятно, буду придерживаться 1.0c сейчас. Я не знаю, почему новая версия не работает для меня, хотя.
 

alexeich2019

Участник форума
Регистрация
28 Июн 2019
Сообщения
191
Благодарности
73
Баллы
175
Gratt, у меня тоже самое на Union 1.0g и SDK 1.0f. Падает с oCObjectFactoryEx. Я уже писал об этом, вроде. Если ставлю Union 1.0f и старый патч - все прекрасно работает.
 

Caterpillar

Участник форума
Регистрация
17 Фев 2020
Сообщения
5
Благодарности
0
Баллы
60
Итак, мне удалось обойти предыдущие проблемы с помощью перехвата хуков.
C++:
    void oCNpcEx::InitExValues()
    {
        staminaRegenLockTime = 0;
        currentStamina = maxStamina = 40;
        staminaUsage = 1;
        sprintActive = False;
        // timerAi;  // this is declared in header file, is it not defined?
        oneStaminaRegenTime = 10; // in miliseconds
    }

// I also tried replacing the return type with oCNpcEx* in oCObjectFactory_CreateNpc
oCNpc* _fastcall oCObjectFactory_CreateNpc(oCObjectFactory* _this, void* vtable, int idx);
    CInvoke<oCNpc * (__thiscall*)(oCObjectFactory*, int)> Ivk_oCObjectFactory_CreateNpc(0x006C8560, &oCObjectFactory_CreateNpc);
    oCNpc* _fastcall oCObjectFactory_CreateNpc(oCObjectFactory* _this, void* vtable, int idx) {
        oCNpcEx* npc = static_cast<oCNpcEx*>( Ivk_oCObjectFactory_CreateNpc(_this, idx) );
        // here I need to init exclusive values of my npc class,
        // since they are never actually constructed, but rather converted.
        npc->InitExValues();
     
        // only casting seems to not crash, creating new and initbyscript doesn't work.

        return npc;
    }

Теперь, если я перезапишу oCNpc:ProcessNpc() так:

C++:
void oCNpcEx::ProcessNpc() {
       // this is actually never called, anything I put in here won't ever run.
}

Поэтому вместо этого я перехватываю oCNpc:ProcessNpc().
C++:
    void oCNpcEx::ProcessExNpc() {
        TimerAI.Attach();
        if (this != player)
            ProcessFade(); // works just fine

        if (this == player) {
            ProcessSprint(); // causes crashes - problem with timer
      }

    void _fastcall oCNpc_ProcessNpc(oCNpcEx* _this, void* vtable);
    CInvoke<void(__thiscall*)(oCNpcEx*)> Ivk_oCNpc_ProcessNpc(0x0069AE50, &oCNpc_ProcessNpc);
    void _fastcall oCNpc_ProcessNpc(oCNpcEx* _this, void* vtable) {
        Ivk_oCNpc_ProcessNpc(_this); // this makes completely no change, whether it's commented out or not
        _this->ProcessExNpc();
    }
Кажется, это работает просто отлично, позволяя исчезнуть npcs, но, похоже, не работает с CTimer. Я получаю сбой нарушения прав доступа при запуске спринта:
(This seems to work just fine, allowing to fade npcs, but it doesn't seem to work with CTimer. I get access violation crash when STARTING sprint:)
=============================================== CALLSTACK : ============================================================== 0023:0F87386E (0x1AD7C180 0x00615F5C 0x12AE9854 0x1A80F380) SHW32.DLL, Common::CTimer::Attach()+222 byte(s)
Причиной этого является чтение с адреса 00000000h.

Код предназначен для Gothic I Classic, но если вы замените значения адреса на любой другой движок, он будет вести себя так же. Версия SDK - 1.0f1. Компилятор в порядке.
Я думаю, что это может быть потому, что oCNpcEx никогда не создается (но он приводится), а объект CTimer не инициализируется? Как бы я его инициализировал, если в этом проблема?
(The code is for Gothic I Classic, but if you replace the address values with that of any other engine, it behaves the same way. SDK version is 1.0f1. Compiler is fine.
I think it may be because oCNpcEx is never created (but it's casted), and the CTimer object doesn't get initialized? How would I initialize it if that's the problem?)

Я прилагаю полный код.

Я использовал Google Translate, поэтому оставил исходный текст там, где чувствовал, что возможны ошибки перевода.
 

Вложения

  • code.zip
    2,7 KB · Просмотры: 44
Последнее редактирование:

Gratt


Модостроитель
Регистрация
14 Ноя 2014
Сообщения
3.301
Благодарности
4.636
Баллы
625
Caterpillar,
A main problem - operator new oCNpcEx is never called.
In the your source code you call the original function. And get the vanilla oCNpc object. Is NOT same oCNpcEx.
oCNpcEx* npc = static_cast<oCNpcEx*>( Ivk_oCObjectFactory_CreateNpc(_this, idx) );

For example:
Ivk_oCObjectFactory_CreateNpc will return oCNpc, suppose it is A class.
But TimerAI and other belong to your oCNpcEx class, suppose it is B class.
And this is what happens:
1582817141838.png


The target object A doesn't have fields from class B. When you use the TimerAI - you addressing to uninitialized memory, as a result the game to crash.
Problem solution:
Use operator new oCNpcEx in your code:
oCNpcEx* npc = new oCNpcEx();

ATTENTION!
Never use static_cast if you are not 100% sure that an object of one type is an object of another.
For casting one type to another can help CastTo<typeName>() (special dynamic_cast) method in Gothic API:
C++:
oCNpcEx* npcEx = npc->CastTo<oCNpcEx>();
if (npcEx != Null)
{
    Message::Box("Yes, it's oCNpcEx object :)");
}
else
{
    Message::Box("No, it's not oCNpcEx object :(");
}

Fix it and if new problems arise, ask me again. :)
 

zeratul47

Участник форума
Регистрация
10 Янв 2020
Сообщения
21
Благодарности
0
Баллы
60
Здравствуйте. Подскажите, пожалуйста, что мне нужно сделать, чтобы вывести на экран меню (или не меню), со списком управления?

То есть по нажатию зарезервированной клавиши, должна выводится менюшка (такая же как со статистикой персонажа, т.е. просто текст в рамочке), где будут отображены текущие настройки управления. Я сначала думал сделать через zCView, но тут не понятно как поставить игру на паузу и откуда взять настройки управления. Потом подумал написать свое меню, но теперь дошел до проблемы, что не знаю как инициализировать меню и вообще откуда взять данные об управлении.

Ещё появился вопрос о том, как получить доступ к глобальным ресурсам? Ну вот мы можем получить доступ к screen. А если нам нужен какой-нибудь game_manager? Как я понял глобально доступны их адреса в виде интов, но не очень понятно как получить сами ресурсы.
 

Gratt


Модостроитель
Регистрация
14 Ноя 2014
Сообщения
3.301
Благодарности
4.636
Баллы
625
как получить доступ к глобальным ресурсам?
Глобальные объекты - classDef_XX.cpp. Первые 60 строчек как в движке. ogame
А дальше разыменованные. Gothic::Game::Session

Нормальный вариант, если унаследуешь zCView и реализуешь в нем метод HandleEvent.
C++:
  static zSTRING GetKeyNameByID( unsigned short logicalKey ) {
    switch( logicalKey ) {
    case GAME_LEFT:           return "Налево";
    case GAME_RIGHT:          return "Направо";
    case GAME_UP:             return "Вперед";
    case GAME_DOWN:           return "Назад";
    case GAME_ACTION:         return "Действие";
    case GAME_SLOW:           return "Шаг";
    case GAME_ACTION2:        return "GAME_ACTION2";
    case GAME_WEAPON:         return "Достать оружие";
    case GAME_SMOVE:          return "Прыжок";
    case GAME_SMOVE2:         return "GAME_SMOVE2";
    case GAME_SHIFT:          return "GAME_SHIFT";
    case GAME_END:            return "Меню";
    case GAME_INVENTORY:      return "Инвентарь";
    case GAME_LOOK:           return "Осмотреться";
    case GAME_SNEAK:          return "Подкрадывание";
    case GAME_STRAFELEFT:     return "Стрейф влево";
    case GAME_STRAFERIGHT:    return "Стрейф вправо";
    case GAME_SCREEN_STATUS:  return "Статус";
    case GAME_SCREEN_LOG:     return "Дневник";
    case GAME_SCREEN_MAP:     return "Карта";
    case GAME_LOOK_FP:        return "От первого лица";
    case GAME_LOCK_TARGET:    return "Зафиксировать цель";
    case GAME_PARADE:         return "Парировать";
    case GAME_ACTIONLEFT:     return "GAME_ACTIONLEFT";
    case GAME_ACTIONRIGHT:    return "GAME_ACTIONRIGHT";
    case GAME_LAME_POTION:    return "GAME_LAME_POTION";
    case GAME_LAME_HEAL:      return "GAME_LAME_HEAL";
    }

    return "<UNKNOWN>";
  }

  class zCViewControls : public zCView {
  public:



    void Show() {
      // Вычисляем размер объекта (диапазон экрана 0-8192)
      // преобразуя пиксельный размер в виртуальный
      int sizeX = zPixelX( 600 );
      int sizeY = zPixelY( 800 );

      // Устанавливаем координаты для вывода объекта в центр
      int posX  = 4196 - sizeX / 2;
      int posY  = 4196 - sizeY / 2;

      // Применяем вычисления
      SetSize( sizeX, sizeY );
      SetPos( posX, posY );

      SetEnableHandleEvent( True );                 // Включаем обработчик нажатия клавиш для данного экземпляра
      SetHandleEventTop();                          // Ставим самый высокий приоритет этому обработчику
      Gothic::Game::Session->Pause( 1 );            // Ставим игру на паузу
      Gothic::Views::Screen->InsertItem( this );    // Добавляем объект на экран
      player->human_ai->PC_Turnings( False );       // Запрещаем игроку поворачиваться
      player->human_ai->PC_Strafe( False );         // Запрещаем игроку стрейфить
      SetFont( Gothic::Views::Screen->GetFont() );  // Устанавливаем шрифт объекта как у screen

      InsertBack( "black" ); // Указываем имя фоновой текстуры
      DrawControls();
    }



    void DrawControls() {
      int verticalOffset = FontY();   // Вычисляем высоту шрифта
      auto& keyMap = zinput->mapList; // Берем список биндов

      for( int i = 0; i < keyMap.GetNum(); i++ ) {

        // Слева выводим имя ключа
        auto& bindInfo = keyMap[i];
        Print( 50, 50 + verticalOffset * i, GetKeyNameByID( bindInfo->logicalID ) );


        // Определяем наличие связанных кнопок
        auto& values = bindInfo->controlValues;
        if( values.GetNum() == 0 ) {
          Print( 2000, 50 + verticalOffset * i, "<EMPTY>" ); // Выводим при отсутствии
          return;
        }

        // Создаем список клавиш
        string bindedString = zinput->GetNameByControlValue( values[0] );
        for( int j = 1; j < values.GetNum(); j++ )
          bindedString += ", " + A zinput->GetNameByControlValue( values[j] );
      
        // выводим справа от ключа
        Print( 3000, 50 + verticalOffset * i, bindedString );
      }
    }


    virtual int HandleEvent( int key ) {
      bool_t closeMenu = zinput->IsBinded( GAME_END, key ) && zinput->GetToggled( GAME_END ); // Проверяем является ли key кнопкой возврата и нажата ли она
      if( closeMenu ) {
        SetEnableHandleEvent( False );             // Отключаем обработчик нажатия клавиш
        Gothic::Game::Session->Unpause();          // Снимаем игру с паузы
        Gothic::Views::Screen->RemoveItem( this ); // Удаляем объект с экрана
        player->human_ai->PC_Turnings( False );    // Разрешаем игроку поворачиваться
        player->human_ai->PC_Strafe( False );      // Разрешаем игроку стрейфить
        delete this;                               // Удаляем объект из памяти
        return True;
      }

      return True; // True - не передавать управление хендлерам нижнего уровня
    }
  };

  // ...

  void Game_Loop() {
    if( !Gothic::Game::Session->IsOnPause() ) {
      if( zKeyToggled( KEY_NUMPAD8 ) ) {
        zCViewControls* menu = new zCViewControls();
        menu->Show();
      }
    }
  }

Либо смотри в сторону меню самой игры, но там надо писать соответствующий скрипт.
Хотя как вариант можно поиграть и с таким кодом:
C++:
    if( zKeyToggled( KEY_NUMPAD7 ) ) {
      Gothic::Game::Session->Pause( 1 );
      oCMenu_ChgKeys* menu_chgkeys = new oCMenu_ChgKeys( "MENU_OPT_CONTROLS" );
      menu_chgkeys->Run();
      delete menu_chgkeys;
    }
 

Last_Imba

Участник форума
Регистрация
27 Дек 2015
Сообщения
16
Благодарности
0
Баллы
150
Хм, пытаюсь реализовать пример, однако получаю странную ошибку.

function "Gothic_II_Addon::oCObjectFactoryEx::operator delete" (declared at line 6 of "D:\REPOS\GOTHIC\TESTPLUGIN\TESTPLUGIN\oCNpcEx.cpp") is inaccessible

Вот в этом месте. Причем если заменить класс на родителя, то все компилируется. Не пойму никак в чем дело. Я немного нуб в плюсах, возможно упустил какую-то очевидную вещь? Пробовал просто копировать код из архива приложенного, все равно есть эта ошибка.
1651671969361.png

Сам код Cpp пока выглядит вот так
C++:
namespace GOTHIC_ENGINE {
    zCLASS_UNION_DEFINITION(oCNpcEx, oCNpc, 0, 0);
    zCLASS_UNION_DEFINITION(oCObjectFactoryEx, oCObjectFactory, 0, 0);

    oCNpc* oCObjectFactoryEx::CreateNpc(int index) {
        oCNpc* npc = new oCNpcEx();
        if (index != zPAR_INDEX_UNDEF) {
            npc->InitByScript(index, 0);
        }

        return npc;
    }

    void InitFactory() {
        zfactory = new oCObjectFactoryEx();
    }
}

void GameGlobal_InitFactory() {
    Gothic_II_Addon::InitFactory();
}
 

Gratt


Модостроитель
Регистрация
14 Ноя 2014
Сообщения
3.301
Благодарности
4.636
Баллы
625
Вероятно zCLASS_UNION_DECLARATION не в паблике. В актуальных апи их надо выносить руками. Внутри макроса находятся те самые операторы.
 

Last_Imba

Участник форума
Регистрация
27 Дек 2015
Сообщения
16
Благодарности
0
Баллы
150
Gratt, Не мог бы ты подсказать, класс CTimer куда-то переехал? Я вижу что есть zCTimer, но не вижу там ни Attach() метода, ни Suspend().
 

Nuno

Участник форума
Регистрация
22 Авг 2022
Сообщения
3
Благодарности
0
Баллы
25
A very good textbook. However, I may be too dumb to fully understand this, but what is the point of VTable?
 

Jedi

Участник форума
Регистрация
11 Янв 2023
Сообщения
11
Благодарности
2
Баллы
15
@Cbrhex, А что ты еще хочешь увидеть? :) Зайди в CamInst.d, там все используемые игрой режимы...

А не подскажете, как можно менять эти режимы?) Пробую так:
Код:
zCAICamera* aicam = ogame->GetCameraAI();
zCArray<zCVob*> instNames;
instNames.Insert(player);
aicam->SetMode(Z "CAMMODDEATH", instNames);
или просто
aicam->curcammode = "CAMMODDEATH";

Режим вроде меняется судя по GetMode() или IsModeActive(Z "CAMMODDEATH"), но поведение воба камеры не меняется.

В daedalus скриптах не нашел где менять тоже.
 

Slavemaster


Модостроитель
Регистрация
10 Июн 2019
Сообщения
1.085
Благодарности
1.903
Баллы
320
Наверное, оно сразу меняется обратно здесь: oCAIHuman::ChangeCamModeBySituation
Мне подсказали, чтобы режим камеры не менялся, можно установить одну переменную zCAICamera::bCamChanges = 0;
 

Jedi

Участник форума
Регистрация
11 Янв 2023
Сообщения
11
Благодарности
2
Баллы
15
Мне подсказали, чтобы режим камеры не менялся, можно установить одну переменную zCAICamera::bCamChanges = 0;

Да, это заставило сменить мод, камера стала вести себя по-другому. Увы оказалось, что в моем случае это мне не помогло xD. Но все равно спасибо!

Контекст:
Я искал способ дать возможность крутить камеру во время анимаций, запущенных через player->GetEM(FALSE)->OnMessage() или player->GetModel()->StartAnimation().
В первом случае блокируется управление (это хорошо), но при этом камеру можно крутить только по оси Y.
Во втором камера крутиться как надо, но крутиться и сам персонаж по X) Можно так же отключить управление так: player->movlock = True , но эффект тот же, как в первом случае - камера крутится только по Y)

Эталоном для меня показалось состояние смерти героя - можно вертеть камеру по XYZ, при этом не можешь управлять героем. Думал мб дело в моде, но увы(
Я в принципе уже смирился, да и на форуме тут пишут, что с камерой гемор работать. Меня устроило сделать так, что бы камера крутилась вокруг ГГ без возможности ею управлять, пока идет анимация. Кому будет полезно, вот так:
Union:
// Активация
aicam = ogame->GetCameraAI();
lastPos = aicam->camVob->GetPositionWorld();
aicam->camVob->SetSleeping(TRUE);
aicam->camVob->SetPositionWorld(lastPos);
aicam->camVob->SetHeadingWorld(player->GetPositionWorld());
player->movlock = True;

/* Движения камеры по кругу внутри Game Loop */
step = 0.1f * ztimer->frameTimeFloat;
aicam->camVob->SetHeadingWorld(player);
aicam->camVob->MoveLocal(0.0f, step, 0.0f);

if (player->GetEM(FALSE)->IsEmpty(0)) {
    // последовательность анимаций
    player->GetEM(FALSE)->OnMessage(
        new oCMsgConversation(oCMsgConversation::EV_PLAYANI_NOOVERLAY, "S_CLAPHANDS"),
        player
    );
    // ...
}
/* Движения камеры по кругу внутри Game Loop END */


// Отключение
aicam->camVob->SetSleeping(FALSE);
player->movlock = False;
player->ClearEM();
 
Последнее редактирование модератором:
Сверху Снизу