Эксклюзив SAMP NPCs | Интерактивные NPC для SAMP сервера

SAMP NPCs​

На случай, если вы хотели когда-то создать умных, интерактивных NPC для вашего SA-MP сервера​

Этот проект предназначен для создания умных, интерактивных NPC для серверов SA-MP (San Andreas Multiplayer). Он предоставляет множество функций для описания поведения NPC, их взаимодействий и логики передвижения, позволяя NPC ощущаться отзывчивыми, а не статичными стандартными актёрами или встроенными в SA-MP игроками, имитирующими NPC. Цель проекта — предоставить разработчикам серверов инструментарий для создания более интересных квестов и прочего функционала, за счёт NPC, которые могут взаимодействовать с игроками, выполнять задачи, следовать по маршрутам и имитировать базовое принятие решений. Всё основано на встроенной логике педов игры.

Возможности​

  • Создание иммерсивных, интерактивных NPC вместо простых SAMP-овских актёров.
  • Множество задач для NPC: атака игроков, NPC, атака транспорта игроков, следование за игроками и т. д.
  • Поиск пути и контроль движения для реалистичной навигации.
  • Поддержка взаимодействия с игроками.
  • Взаимодействие и координация между NPC.
  • Динамическое переключение поведения в зависимости от контекста (бой, ожидание, преодоление препятствий, залаз на здания и подобное).
  • Удобный SDK для компонентов, написанных на базе open.mp SDK.

Автор: @#Northn
GitHub репозиторий: https://github.com/Northn/samp_npcs

Начало работы​

Требования​


Сборка​

Bash:
# Клонируем репозиторий
git clone https://github.com/Northn/samp_npcs --recursive

# Переходим в директорию проекта
cd samp_npcs

# Генерация проекта
cmake -B build -DBUILD_CLIENT=YES -A Win32

# Сборка
cmake --build build --config RelWithDebInfo --parallel

Использование​

Код:
static NPC:created_npcs[1024] = {INVALID_NPC, ...};
stock bool:IsValidNpcSlot(slotid) {
    return slotid >= 0 && slotid < sizeof(created_npcs);
}

stock bool:IsValidNpcInSlot(slotid) {
    return IsValidNpcSlot(slotid) && IsValidNpc(created_npcs[slotid]);
}

stock NPC:GetNpcInSlot(slotid) {
    return IsValidNpcInSlot(slotid) ? created_npcs[slotid] : INVALID_NPC;
}

stock FindAvailableNpcSlot() {
    for (new i = 0; i < sizeof(created_npcs); ++i) {
        if (!IsValidNpcInSlot(i)) {
            return i;
        }
    }
    return -1;
}

stock CreateNpcSlot(skinid, Float:x, Float:y, Float:z, world) {
    new available_slot = FindAvailableNpcSlot();
    if (available_slot == -1) return -1;
    created_npcs[available_slot] = CreateNpc(skinid, x, y, z);
    SetNpcVirtualWorld(created_npcs[available_slot], world);
    return available_slot;
}

stock bool:DestroyNpcSlot(slotid) {
    if (!IsValidNpcInSlot(slotid)) return false;
    DestroyNpc(created_npcs[slotid]);
    created_npcs[slotid] = INVALID_NPC;
    return true;
}

#define ASSERT_NPC_EXISTS(%0,%1)                                                            \
    new NPC:%0 = GetNpcInSlot(%1);                                                          \
    if (npc == INVALID_NPC) {                                                               \
        SendClientMessage(playerid, COLOR_GREY, "NPC не существует.");                      \
        return 1;                                                                           \
    }

CMD:create_npc(playerid, const params[]) {
    new skinid = 0;
    if (sscanf(params, "i", skinid)) {
        SendClientMessage(playerid, COLOR_GREY, "* Создать интерактивного NPC [ /create_npc Skin ]");
        return 1;
    }
    new Float:x, Float:y, Float:z;
    GetPlayerPos(playerid, x, y, z);
    x += 0.25;
    y += 0.25;
    z += 0.25;
 
    new npc_slot = CreateNpcSlot(skinid, x, y, z, GetPlayerVirtualWorld(playerid));
    if (!IsValidNpcInSlot(npc_slot)) {
        SendClientMessage(playerid, COLOR_GREY, "* Буфер NPC заполнен");
        SendClientMessage(playerid, COLOR_GREY, "* Очистите его командой: [ /clear_npcs ]");
        return 1;
    }
    SendClientMessage(playerid, COLOR_GREY, "* NPC создан! ID: %d", npc_slot);
    return 1;
}

CMD:npc_attack(playerid, const params[]) {
    new npcid, targetid, bool: aggressive;
    if (sscanf(params, "iiI(0)", npcid, targetid, aggressive)) {
        SendClientMessage(playerid, COLOR_GREY, "* Приказать NPC атаковать игрока [ /npc_attack NPC_ID Target_ID Aggression* ]");
        return 1;
    }
    if (!IsPlayerConnected(targetid) || IsPlayerNPC(targetid)) {
        SendClientMessage(playerid, COLOR_GREY, "* Цель не в сети...");
        return 1;
    }
 
    if (npcid != -1) {
        ASSERT_NPC_EXISTS(npc, npcid)
        TaskNpcAttackPlayer(npc, targetid, aggressive);
    } else {
        for (new i = 0; i < sizeof(created_npcs); ++i) {
            if (!IsValidNpcInSlot(i)) continue;
            new NPC:npc = GetNpcInSlot(i);
            new Float:npc_x, Float:npc_y, Float:npc_z;
            GetNpcPosition(npc, npc_x, npc_y, npc_z);
            if (GetPlayerDistanceFromPoint(playerid, npc_x, npc_y, npc_z) <= 30.0) {
                TaskNpcAttackPlayer(npc, targetid, aggressive);
            }
        }
    }
    SendClientMessage(playerid, COLOR_GREY, "* Готово!");
    return 1;
}

CMD:npc_stand(playerid, const params[]) {
    new npcid;
    if (sscanf(params, "i", npcid)) {
        SendClientMessage(playerid, COLOR_GREY, "* Приказать NPC стоять на месте [ /npc_stand NPC_ID ]");
        return 1;
    }
 
    if (npcid != -1) {
        ASSERT_NPC_EXISTS(npc, npcid)
        TaskNpcStandStill(npc);
    } else {
        for (new i = 0; i < sizeof(created_npcs); ++i) {
            if (!IsValidNpcInSlot(i)) continue;
            new NPC:npc = GetNpcInSlot(i);
            new Float:npc_x, Float:npc_y, Float:npc_z;
            GetNpcPosition(npc, npc_x, npc_y, npc_z);
            if (GetPlayerDistanceFromPoint(playerid, npc_x, npc_y, npc_z) <= 30.0) {
                TaskNpcStandStill(npc);
            }
        }
    }
 
    SendClientMessage(playerid, COLOR_GREY, "* Готово!");
    return 1;
}

CMD:npc_go_here(playerid, const params[]) {
    new npcid, mode;
    if (sscanf(params, "ii", npcid, mode) || !(mode >= 0 && mode <= 2)) {
        SendClientMessage(playerid, COLOR_GREY, "* Приказать NPC подойти к вам [ /npc_go_here NPC_ID Mode ]");
        SendClientMessage(playerid, COLOR_GREY, "* Режим: 0 — шаг, 1 — лёгкий бег, 2 — спринт");
        return 1;
    }
 
    new Float:x, Float:y, Float:z;
    GetPlayerPos(playerid, x, y, z);
    if (npcid != -1) {
        ASSERT_NPC_EXISTS(npc, npcid)
        TaskNpcGoToPoint(npc, x, y, z, NPC_MOVE_MODE:mode);
    } else {
        for (new i = 0; i < sizeof(created_npcs); ++i) {
            if (!IsValidNpcInSlot(i)) continue;
            new NPC:npc = GetNpcInSlot(i);
            new Float:npc_x, Float:npc_y, Float:npc_z;
            GetNpcPosition(npc, npc_x, npc_y, npc_z);
            if (GetPlayerDistanceFromPoint(playerid, npc_x, npc_y, npc_z) <= 30.0) {
                TaskNpcGoToPoint(npc, x, y, z, NPC_MOVE_MODE:mode);
            }
        }
    }
 
    SendClientMessage(playerid, COLOR_GREY, "* Готово!");
    return 1;
}

CMD:npc_go_random(playerid, const params[]) {
    new npcid, mode;
    if (sscanf(params, "ii", npcid, mode) || !(mode >= 0 && mode <= 2)) {
        SendClientMessage(playerid, COLOR_GREY, "* Приказать NPC случайно переместиться рядом с вами [ /npc_go_random NPC_ID Mode ]");
        SendClientMessage(playerid, COLOR_GREY, "* Режим: 0 — шаг, 1 — лёгкий бег, 2 — спринт");
        return 1;
    }
    if (npcid != -1) {
        new Float:x, Float:y, Float:z;
        GetPlayerPos(playerid, x, y, z);
        x += 10 + random(15);
        y += 10 + random(15);
        ASSERT_NPC_EXISTS(npc, npcid)
        TaskNpcGoToPoint(npc, x, y, z, NPC_MOVE_MODE:mode);
    } else {
        for (new i = 0; i < sizeof(created_npcs); ++i) {
            if (!IsValidNpcInSlot(i)) continue;
            new Float:x, Float:y, Float:z;
            GetPlayerPos(playerid, x, y, z);
            x += 10 + random(15);
            y += 10 + random(15);
            new NPC:npc = GetNpcInSlot(i);
            new Float:npc_x, Float:npc_y, Float:npc_z;
            GetNpcPosition(npc, npc_x, npc_y, npc_z);
            if (GetPlayerDistanceFromPoint(playerid, npc_x, npc_y, npc_z) <= 30.0) {
                TaskNpcGoToPoint(npc, x, y, z, NPC_MOVE_MODE:mode);
            }
        }
    }
 
    SendClientMessage(playerid, COLOR_GREY, "* Готово!");
    return 1;
}

CMD:npc_follow(playerid, const params[]) {
    new npcid, targetid;
    if (sscanf(params, "ii", npcid, targetid)) {
        SendClientMessage(playerid, COLOR_GREY, "* Приказать NPC следовать за игроком [ /npc_follow NPC_ID Target_ID ]");
        return 1;
    }
    if (!IsPlayerConnected(targetid) || IsPlayerNPC(targetid)) {
        SendClientMessage(playerid, COLOR_GREY, "* Цель не в сети...");
        return 1;
    }
 
    if (npcid != -1) {
        ASSERT_NPC_EXISTS(npc, npcid)
        TaskNpcFollowPlayer(npc, targetid);
    } else {
        for (new i = 0; i < sizeof(created_npcs); ++i) {
            if (!IsValidNpcInSlot(i)) continue;
            new NPC:npc = GetNpcInSlot(i);
            new Float:npc_x, Float:npc_y, Float:npc_z;
            GetNpcPosition(npc, npc_x, npc_y, npc_z);
            if (GetPlayerDistanceFromPoint(playerid, npc_x, npc_y, npc_z) <= 30.0) {
                TaskNpcFollowPlayer(npc, targetid);
            }
        }
    }
    SendClientMessage(playerid, COLOR_GREY, "* Готово!");
    return 1;
}

CMD:npc_sethp(playerid, const params[]) {
    new npcid, Float:amount;
    if (sscanf(params, "if", npcid, amount)) {
        SendClientMessage(playerid, COLOR_GREY, "* Установить здоровье NPC [ /npc_sethp NPC_ID Health ]");
        return 1;
    }
 
    if (npcid != -1) {
        ASSERT_NPC_EXISTS(npc, npcid)
        SetNpcHealth(npc, amount);
    } else {
        for (new i = 0; i < sizeof(created_npcs); ++i) {
            if (!IsValidNpcInSlot(i)) continue;
            new NPC:npc = GetNpcInSlot(i);
            new Float:npc_x, Float:npc_y, Float:npc_z;
            GetNpcPosition(npc, npc_x, npc_y, npc_z);
            if (GetPlayerDistanceFromPoint(playerid, npc_x, npc_y, npc_z) <= 30.0) {
                SetNpcHealth(npc, amount);
            }
        }
    }
 
    SendClientMessage(playerid, COLOR_GREY, "* Готово!");
    return 1;
}

CMD:npc_setweapon(playerid, const params[]) {
    new npcid, weaponid;
    if (sscanf(params, "ii", npcid, weaponid)) {
        SendClientMessage(playerid, COLOR_GREY, "* Выдать оружие NPC [ /npc_setweapon NPC_ID Weapon_ID ]");
        return 1;
    }
    if (npcid != -1) {
        ASSERT_NPC_EXISTS(npc, npcid)
        SetNpcWeapon(npc, WEAPON:weaponid);
    } else {
        for (new i = 0; i < sizeof(created_npcs); ++i) {
            if (!IsValidNpcInSlot(i)) continue;
            new NPC:npc = GetNpcInSlot(i);
            new Float:npc_x, Float:npc_y, Float:npc_z;
            GetNpcPosition(npc, npc_x, npc_y, npc_z);
            if (GetPlayerDistanceFromPoint(playerid, npc_x, npc_y, npc_z) <= 30.0) {
                SetNpcWeapon(npc, WEAPON:weaponid);
            }
        }
    }
 
    SendClientMessage(playerid, COLOR_GREY, "* Готово!");
    return 1;
}

alias:npc_delete("npc_remove", "npc_clear", "npc_destroy")
CMD:npc_delete(playerid, const params[]) {
    new npcid;
    if (sscanf(params, "i", npcid)) {
        SendClientMessage(playerid, COLOR_GREY, "* Удалить интерактивного NPC [ /npc_delete NPC_ID ]");
        return 1;
    }
 
    if (npcid != -1) {
        ASSERT_NPC_EXISTS(npc, npcid)
        DestroyNpcSlot(npcid);
    } else {
        for (new i = 0; i < sizeof(created_npcs); ++i) {
            if (!IsValidNpcInSlot(i)) continue;
            new NPC:npc = GetNpcInSlot(i);
            new Float:npc_x, Float:npc_y, Float:npc_z;
            GetNpcPosition(npc, npc_x, npc_y, npc_z);
            if (GetPlayerDistanceFromPoint(playerid, npc_x, npc_y, npc_z) <= 30.0) {
                DestroyNpcSlot(i);
            }
        }
    }
 
    SendClientMessage(playerid, COLOR_GREY, "* Готово!");
    return 1;
}

CMD:clear_npcs(playerid, const params[]) {
    new amount = 0;
    for (new i = 0; i < sizeof(created_npcs); ++i) {
        if (DestroyNpcSlot(i)) {
            ++amount;
        }
    }
 
    SendClientMessage(playerid, COLOR_GREY, "* Готово! Удалено: %d", amount);
    return 1;
}
#undef ASSERT_NPC_EXISTS

public bool:OnPlayerGiveDamageNpc(NPC:npc, damagerid, Float:amount, weaponid, bodypart)
{
    return true;
}

public bool:OnNpcGiveDamageNpc(NPC:npc, NPC:damager, Float:amount, weaponid, bodypart)
{
    return true;
}

public OnPlayerTakeDamageNpc(NPC:npc, issuerid, Float:amount, weaponid, bodypart)
{
    return 1;
}

public OnNpcDeath(NPC:npc, killerid, reason)
{
    return 1;
}
 

moreveal

Известный
976
705
Ну, он нужен будет только на одном экземпляре игры, запущенном вместе с сервером, а игрокам ничего устанавливать не надо
клиент-плагин помимо отправки синхры непосредственно занимается созданием и управлением педами, без него их даже не будет, это ж не самповские игроки
твоя модель разве что предотвратит некоторые баги синхры, которые вызваны постоянной сменой авторитета (ща им считается ближайший к боту игрок)
 

Rei

Известный
Друг
1,629
1,695
клиент-плагин помимо отправки синхры непосредственно занимается созданием и управлением педами, без него их даже не будет, это ж не самповские игроки
твоя модель разве что предотвратит некоторые баги синхры, которые вызваны постоянной сменой авторитета (ща им считается ближайший к боту игрок)
Верно, но моя задумка в том, чтоб созданием и управлением педами занимался только один клиент, запущенный на одной машине с самп сервером