Подписывайтесь на наш Telegram и не пропускайте важные новости! Перейти

Гайд [Сурс] GTA V Animal Rider — Система ездовых животных (C++/ImGui)

Sloppy
Начинающий
Начинающий
Статус
Оффлайн
Регистрация
13 Фев 2026
Сообщения
622
Реакции
16
Наткнулся на интересный вариант реализации маунтов в GTA Online. Тема не новая, но многие новички до сих пор спотыкаются на том, что при спавне животины и аттаче игрока всё наглухо фризится, а стандартные задания ходьбы (AI tasks) просто не отрабатывают.

В этом сурсе проблема решена через прямой контроль вектора скорости (velocity), что гораздо стабильнее для мультиплеера. Автор использует ATTACH_ENTITY_TO_ENTITY для посадки персонажа и кастомный обсчет хединга для поворотов.

Что внутри:
  1. Готовый конфиг с хэшами (олень, корова, пума, даже дельфин и косатка);
  2. Система защиты педа (God Mode, блокировка событий);
  3. Логика посадки/высадки через FiberPool;
  4. Управление через перехват контролов (W/S/A/D);
  5. Интерфейс на ImGui для интеграции в любой современный чит-база.

Техническая база для изучения:
Код:
Expand Collapse Copy
// ============================================================
// Animal Rider - Mount System
// ============================================================
struct AnimalDef
{
const char* name;
uint32_t hash;
float mountZ;
};

static const std::vector<AnimalDef> s_Animals = {
{"Deer", 0xD86B5A95, 0.30f},
{"Boar", 0xCE5FF074, 0.30f},
{"Cow", 0xFCFA9E1E, 0.50f},
{"Mountain Lion", 0x1250D7BA, 0.25f},
{"Coyote", 0x644AC75E, 0.25f},
{"Panther", 0xE71D5E68, 0.25f},
{"Chop", 0x14EC17EA, 0.20f},
{"Dolphin", 0x8BBAB455, 0.30f},
{"Killer Whale", 0x8D8AC8B9, 0.55f},
{"Humpback", 0x471BE4B2, 0.65f},
};

static int s_ActiveAnimal = 0;
static bool s_Dismounting = false;
static bool s_AnimalGodMode = true;
static std::string s_StatusMsg = "Pick an animal to ride.";

static constexpr const char* MOUNT_ANIM_DICT = "missarmenian1driving_taunts@lamar_1";
static constexpr const char* MOUNT_ANIM_NAME = "cmonmy[removed]";

static bool IsControlPressedEither(int pad, int control)
{
return PAD::IS_CONTROL_PRESSED(pad, control) || PAD::IS_DISABLED_CONTROL_PRESSED(pad, control);
}

static bool IsControlJustPressedEither(int pad, int control)
{
return PAD::IS_CONTROL_JUST_PRESSED(pad, control) || PAD::IS_DISABLED_CONTROL_JUST_PRESSED(pad, control);
}

static void ProtectAnimal(int animal)
{
if (!ENTITY:OES_ENTITY_EXIST(animal))
return;
ENTITY::SET_ENTITY_INVINCIBLE(animal, TRUE, FALSE);
PED::SET_BLOCKING_OF_NON_TEMPORARY_EVENTS(animal, TRUE);
PED::SET_PED_CAN_RAGDOLL(animal, FALSE);
PED::SET_PED_CONFIG_FLAG(animal, 202, TRUE);
int maxHp = ENTITY::GET_ENTITY_MAX_HEALTH(animal);
ENTITY::SET_ENTITY_HEALTH(animal, maxHp, 0, 0);
}

static void Dismount()
{
if (s_Dismounting || s_ActiveAnimal == 0)
return;
s_Dismounting = true;

int animal = s_ActiveAnimal;
s_ActiveAnimal = 0;
s_StatusMsg = "Dismounted. Pick an animal to ride again.";

FiberPool::Push([animal] {
auto selfPed = Self::GetPed();
int pid = selfPed.GetHandle();
auto pos = selfPed.GetPosition();

ENTITY:ETACH_ENTITY(pid, TRUE, TRUE);
ScriptMgr::Yield(80ms);
ENTITY::SET_ENTITY_COORDS(pid, pos.x, pos.y, pos.z + 0.5f, FALSE, FALSE, FALSE, FALSE);
ScriptMgr::Yield(80ms);
TASK::CLEAR_PED_TASKS_IMMEDIATELY(pid);
ScriptMgr::Yield(80ms);

if (ENTITY:OES_ENTITY_EXIST(animal))
{
ENTITY::SET_ENTITY_AS_MISSION_ENTITY(animal, TRUE, TRUE);
ScriptMgr::Yield(50ms);
if (ENTITY:OES_ENTITY_EXIST(animal))
PED:ELETE_PED(&const_cast<int&>(animal));
}

s_Dismounting = false;
Notifications::Show("Animal Rider", "Dismounted.", NotificationType::Info, 2000);
});
}

static void SpawnAndMount(const AnimalDef& def)
{
if (s_Dismounting)
{
Notifications::Show("Animal Rider", "Still dismounting, wait...", NotificationType::Warning);
return;
}
if (s_ActiveAnimal != 0)
{
Notifications::Show("Animal Rider", "Already riding! Press F to dismount.", NotificationType::Warning);
return;
}

FiberPool::Push([def] {
auto selfPed = Self::GetPed();
int pid = selfPed.GetHandle();
auto pos = selfPed.GetPosition();
float heading = selfPed.GetHeading();
uint32_t hash = def.hash;

// Load model
STREAMING::REQUEST_MODEL(hash);
int t = 0;
while (!STREAMING::HAS_MODEL_LOADED(hash) && t < 200)
{
ScriptMgr::Yield();
t++;
}
if (!STREAMING::HAS_MODEL_LOADED(hash))
{
Notifications::Show("Animal Rider", std::string("Failed to load: ") + def.name, NotificationType::Error);
STREAMING::SET_MODEL_AS_NO_LONGER_NEEDED(hash);
return;
}

// Spawn in front of player
float rad = heading * 3.14159265f / 180.0f;
float spawnX = pos.x + std::sin(-rad) * 2.0f;
float spawnY = pos.y + std::cos(-rad) * 2.0f;

int animal = PED::CREATE_PED(28, hash, spawnX, spawnY, pos.z, heading, TRUE, FALSE);
ScriptMgr::Yield(50ms);

if (!ENTITY:OES_ENTITY_EXIST(animal))
{
Notifications::Show("Animal Rider", std::string("Failed to spawn: ") + def.name, NotificationType::Error);
STREAMING::SET_MODEL_AS_NO_LONGER_NEEDED(hash);
return;
}

// Freeze & protect before anything
ENTITY::FREEZE_ENTITY_POSITION(animal, TRUE);
ProtectAnimal(animal);
ENTITY::SET_ENTITY_AS_MISSION_ENTITY(animal, TRUE, TRUE);
ENTITY::SET_ENTITY_LOD_DIST(animal, 9999);
ENTITY::SET_ENTITY_VISIBLE(animal, TRUE, FALSE);

// Teleport player to spawn position
ENTITY::FREEZE_ENTITY_POSITION(pid, TRUE);
ENTITY::SET_ENTITY_COORDS(pid, spawnX, spawnY, pos.z, FALSE, FALSE, FALSE, FALSE);

s_ActiveAnimal = animal;

// Attach player to animal
ENTITY::ATTACH_ENTITY_TO_ENTITY(
pid, animal, -1,
0.0f, 0.1f, def.mountZ,
0.0f, 0.0f, 0.0f,
FALSE, FALSE, FALSE, TRUE, 2, TRUE, FALSE);

ScriptMgr::Yield(100ms);

// Play riding animation
STREAMING::REQUEST_ANIM_DICT(MOUNT_ANIM_DICT);
t = 0;
while (!STREAMING::HAS_ANIM_DICT_LOADED(MOUNT_ANIM_DICT) && t < 200)
{
ScriptMgr::Yield();
t++;
}
TASK::TASK_PLAY_ANIM(pid, MOUNT_ANIM_DICT, MOUNT_ANIM_NAME, 8.0f, 1.0f, -1, 2, 1.0f, FALSE, FALSE, FALSE);

// Unfreeze
ENTITY::FREEZE_ENTITY_POSITION(animal, FALSE);
ENTITY::FREEZE_ENTITY_POSITION(pid, FALSE);
STREAMING::SET_MODEL_AS_NO_LONGER_NEEDED(hash);

s_StatusMsg = std::string("Riding ") + def.name + " | W = Move F = Dismount";
Notifications::Show("Animal Rider", std::string("Riding ") + def.name + "! Press F to dismount.", NotificationType::Success);
});
}

// Called from looped command to handle controls
void AnimalRiderTick()
{
if (s_ActiveAnimal == 0 || s_Dismounting)
return;

int animal = s_ActiveAnimal;
int pid = Self::GetPed().GetHandle();

// Protect animal every frame
if (s_AnimalGodMode)
ProtectAnimal(animal);

// Keep visible
ENTITY::SET_ENTITY_VISIBLE(animal, TRUE, FALSE);
ENTITY::SET_ENTITY_LOD_DIST(animal, 9999);

// Check death
bool animalDead = !ENTITY:OES_ENTITY_EXIST(animal) || ENTITY::IS_ENTITY_DEAD(animal, FALSE);
bool pidDead = ENTITY::IS_ENTITY_DEAD(pid, FALSE);

if (animalDead || pidDead)
{
s_ActiveAnimal = 0;
s_StatusMsg = "Dismounted (death).";
ENTITY:ETACH_ENTITY(pid, TRUE, TRUE);
TASK::CLEAR_PED_TASKS_IMMEDIATELY(pid);
FiberPool::Push([animal] {
ScriptMgr::Yield(300ms);
if (ENTITY:OES_ENTITY_EXIST(animal))
{
ENTITY::SET_ENTITY_AS_MISSION_ENTITY(animal, TRUE, TRUE);
PED:ELETE_PED(&const_cast<int&>(animal));
}
});
return;
}

// F to dismount (control 23)
if (IsControlJustPressedEither(0, 23))
{
Dismount();
return;
}

if (ENTITY:OES_ENTITY_EXIST(animal))
{
const bool forwardPressed = IsControlPressedEither(0, 32);
const bool backwardPressed = IsControlPressedEither(0, 33);
const float side = PAD::GET_CONTROL_NORMAL(0, 30);

float heading = ENTITY::GET_ENTITY_HEADING(animal);
if (std::fabs(side) > 0.01f)
{
heading += side * 4.0f;
ENTITY::SET_ENTITY_HEADING(animal, heading);
}

Vector3 currentVelocity = ENTITY::GET_ENTITY_VELOCITY(animal);
float moveInput = 0.0f;
if (forwardPressed)
moveInput += 1.0f;
if (backwardPressed)
moveInput -= 0.65f;

if (std::fabs(moveInput) > 0.01f)
{
// Direct velocity works more reliably here than AI walk tasks while mounted.
TASK::CLEAR_PED_TASKS_IMMEDIATELY(animal);

float rad = heading * 3.14159265f / 180.0f;
const float forwardSpeed = 4.5f;
const float strafeInfluence = 1.25f;
float vx = (std::sin(-rad) * forwardSpeed * moveInput) + (std::cos(-rad) * side * strafeInfluence);
float vy = (std::cos(-rad) * forwardSpeed * moveInput) - (std::sin(-rad) * side * strafeInfluence);
ENTITY::SET_ENTITY_VELOCITY(animal, vx, vy, currentVelocity.z);
}
else
{
ENTITY::SET_ENTITY_VELOCITY(animal, currentVelocity.x * 0.25f, currentVelocity.y * 0.25f, currentVelocity.z);
}
}
}

// ============================================================
// UI Builder
// ============================================================
std::shared_ptr<Category> BuildFunOptionsMenu()
{
auto menu = std::make_shared<Category>("Fun Options");

// --- Existing Fun Commands ---
auto funGroup = std::make_shared<Group>("Fun Toggles");
funGroup->AddItem(std::make_shared<BoolCommandItem>("selfdrunkmode"_J));
funGroup->AddItem(std::make_shared<BoolCommandItem>("selfragdollmode"_J));
funGroup->AddItem(std::make_shared<BoolCommandItem>("selfspinningcar"_J));
funGroup->AddItem(std::make_shared<BoolCommandItem>("selfrainbowcar"_J));

// --- Animal Rider ---
auto riderGroup = std::make_shared<Group>("Animal Rider");
riderGroup->AddItem(std::make_shared<ImGuiItem>([] {
if (s_ActiveAnimal != 0)
ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, 1.0f), "%s", s_StatusMsg.c_str());
else if (s_Dismounting)
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.1f, 1.0f), "Dismounting, please wait...");
else
ImGui::TextColored(ImVec4(0.9f, 0.8f, 0.2f, 1.0f), "%s", s_StatusMsg.c_str());

ImGui::Separator();
ImGui::Spacing();

if (s_ActiveAnimal != 0)
{
// God mode toggle
if (s_AnimalGodMode)
ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.3f, 1.0f), "Mount God Mode: ON");
else
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "Mount God Mode: OFF");

if (ImGui::Button("Toggle God Mode", ImVec2(ImGui::GetContentRegionAvail().x, 0)))
{
s_AnimalGodMode = !s_AnimalGodMode;
if (ENTITY:OES_ENTITY_EXIST(s_ActiveAnimal))
{
ENTITY::SET_ENTITY_INVINCIBLE(s_ActiveAnimal, s_AnimalGodMode ? TRUE : FALSE, FALSE);
PED::SET_PED_CAN_RAGDOLL(s_ActiveAnimal, s_AnimalGodMode ? FALSE : TRUE);
}
}

ImGui::Spacing();
ImGui::Text("W / Left / Right - Move & steer");
ImGui::Text("F - Dismount");
ImGui::Spacing();

if (ImGui::Button("Dismount Now", ImVec2(ImGui::GetContentRegionAvail().x, 0)))
Dismount();
}
else if (!s_Dismounting)
{
// God mode toggle (pre-spawn)
if (s_AnimalGodMode)
ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.3f, 1.0f), "Mount God Mode: ON");
else
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "Mount God Mode: OFF");

if (ImGui::Button("Toggle God Mode##pre", ImVec2(ImGui::GetContentRegionAvail().x, 0)))
s_AnimalGodMode = !s_AnimalGodMode;

ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
ImGui::Text("Spawn an animal to ride:");
ImGui::Spacing();

for (size_t i = 0; i < s_Animals.size(); i++)
{
const auto& def = s_Animals[i];
std::string label = std::string(def.name) + "##a" + std::to_string(i);
if (ImGui::Button(label.c_str(), ImVec2(ImGui::GetContentRegionAvail().x, 0)))
{
SpawnAndMount(def);
}
}

ImGui::Spacing();
ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Spawns at your position and auto-mounts.");
}
}));

menu->AddItem(funGroup);
menu->AddItem(riderGroup);
return menu;
}

Основная фишка в AnimalRiderTick. Вместо того чтобы просить игру «иди туда», код сам считает vx и vy на основе текущего угла поворота животного. Это позволяет избежать тупняков AI, когда на существе висит другой энтити. Для анимации используется словарь missarmenian1driving_taunts@lamar_1 — выглядит топорно, но для теста сойдет.

Важные нюансы:
— В коде зашит God Mode для маунта, отключайте, если боитесь лишних репортов.
— Для работы в онлайне проверяйте статус сессии, в коде есть завязка на соло-сессии.
— Оффсеты по Z для каждого зверя подобраны вручную, чтобы персонаж не проваливался в текстуры.

Код легко адаптируется под любую пасту на базе BigBaseV2 или аналогичных фреймворках. Если будете расширять список животных, не забудьте про REQUEST_MODEL, иначе просто вылетите при спавне.

Интересно было бы глянуть, как это поведет себя в полноценном лобби под десинком.
 
Последнее редактирование модератором:
Назад
Сверху Снизу