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
Начало работы
Требования
- plugin-sdk
- SA-MP 0.3.7-R3
Сборка
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;
}