FiveM: система недвижимости и аренды жилья
Недвижимость — это то, ради чего половина игроков вообще заходит на RP-сервер: свой угол, гараж под тачки, место для сходок фракции. Но как только вы садитесь настраивать систему домов, вылезает три отдельные задачи — привязать дом к конкретному MLO-интерьеру, не дать одному игроку скупить полкарты, и организовать аренду так, чтобы неплательщиков выселяло автоматически, а не вручную через тикет в Discord. Разберём все три по порядку, с конфигами и SQL-таблицами.
Содержание
Как устроена система недвижимости изнутри
Любой скрипт недвижимости на FiveM — будь то qb-houses, esx_property или самописный — держится на трёх сущностях: точка на карте (где стоит дверь и появляется маркер), запись в базе (кому принадлежит, сколько стоит, есть ли ключи у друзей) и координаты интерьера, куда телепортируется игрок. Важно понимать: MLO — это не отдельная "ячейка" карты вроде IPL-интерьера из ванильной GTA, а обычная модель (ymap + ytyp), вставленная где-то на общей карте, часто далеко от основного города или под землёй. Скрипт просто телепортирует игрока из точки A (дверь снаружи) в точку B (координаты внутри MLO) и обратно.
Из этого следует важное следствие для планирования сервера: количество "домов" не ограничено количеством MLO-моделей физически — вы можете повесить 50 разных адресов на 5 уникальных интерьеров, если это устраивает игроков (просто у них будет одинаковая планировка). Экономичный вариант для старта — взять 8-10 бесплатных MLO с разной площадью (студия, дом, особняк) и размножить точки входа по районам карты.
Базовая структура таблицы в MySQL, вокруг которой строится почти любой скрипт:
CREATE TABLE `player_houses` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`house_id` VARCHAR(50) NOT NULL,
`owner` VARCHAR(64) DEFAULT NULL, -- citizenid игрока
`label` VARCHAR(100) NOT NULL,
`price` INT NOT NULL,
`coords` LONGTEXT NOT NULL, -- координаты двери снаружи
`interior_coords` LONGTEXT NOT NULL, -- координаты входа в MLO
`tier` INT DEFAULT 1,
`is_rented` TINYINT(1) DEFAULT 0,
`rent_price` INT DEFAULT 0,
`rent_due` DATETIME DEFAULT NULL,
`keyholders` LONGTEXT DEFAULT NULL -- JSON-массив citizenid с доступом
);
Привязка дома к MLO-интерьеру
Технически привязка — это конфиг-объект на каждый дом с двумя наборами координат и опциональным маркером. Пример на qb-target (актуален и для ox_target, синтаксис почти идентичен):
QBCore.Functions.CreateCallback('housing:server:GetHouseData', function(source, cb, houseId)
local house = MySQL.single.await('SELECT * FROM player_houses WHERE house_id = ?', {houseId})
cb(house)
end)
-- клиентская часть: точка входа
local function CreateDoorZone(house)
exports['qb-target']:AddBoxZone(house.house_id .. '_door', house.coords, 1.5, 1.5, {
name = house.house_id .. '_door',
heading = house.coords.w,
debugPoly = false,
minZ = house.coords.z - 1,
maxZ = house.coords.z + 1,
}, {
options = {
{
type = 'client',
event = 'housing:client:EnterHouse',
icon = 'fas fa-door-open',
label = 'Войти в дом',
houseId = house.house_id,
},
},
distance = 2.0,
})
end
При входе клиент запрашивает interior_coords и телепортирует игрока внутрь, а сервер параллельно ставит фриз на старую позицию (или использует SetEntityCoords с флагом warp = true, чтобы избежать провала под текстуры на слабом интернете). Ключевая грабля здесь — забыть выставить RequestCollisionAtCoord перед телепортом: без прогрузки коллизий игрок иногда падает сквозь пол MLO, особенно если интерьер стоит на большом удалении от игровой зоны и стример (ox_lib/interact-sound не в счёт, важен именно streaming range самой ymap-модели).
Для выхода делают вторую зону уже внутри интерьера (обычно у координат двери MLO), с тем же паттерном "телепорт + фриз на кадр". Если у дома несколько дверей/выходов (гараж, чёрный ход), под каждую заводится отдельная пара координат в том же JSON-поле coords.
Список готовых бесплатных и платных MLO, а также порядок их установки на сервер — отдельная тема, которую мы подробно разбирали в статье про установку кастомных MLO-карт.
Поднять сервер FiveM (GTA V) за пару минут
Готовый образ MAATRIX GAMES: NVMe, AMD EPYC, DDoS-защита, панель управления. Локации UK, US, RU. Оплата картой РФ, СБП или криптой.
Создать серверГотовые скрипты vs самопис
Не обязательно писать систему с нуля — на рынке FiveM есть устоявшиеся решения под оба популярных фреймворка. Разница принципиальная, поэтому сравните до того, как выберете фреймворк для сервера (если ещё не определились — см. статьи про базовую настройку ESX и базовую настройку QBCore).
| Скрипт | Фреймворк | Аренда "из коробки" | Лимит домов на игрока | Особенность |
|---|---|---|---|---|
qb-houses | QBCore | Нет, только покупка | Настраивается в config.lua | Простой, легко кастомизировать |
esx_property | ESX | Нет | Через SQL-триггер | Старый, но стабильный |
qs-housing | ESX/QBCore | Да, есть таймер аренды | Есть в конфиге | Гаражи + мебель, тяжелее по ресурсам |
renewed-housing | QBCore | Да, гибкая система тарифов | Есть, по tier | Активно поддерживается, магазин мебели |
Если аренда для вас критична с первого дня — не тратьте время на допиливание qb-houses под аренду, возьмите renewed-housing или qs-housing, где биллинг уже есть, и донастройте суммы/периодичность под экономику вашего сервера. Экономику RP-сервера в целом (курс валюты, зарплаты, налоги) мы разбирали в статье про базовую настройку экономики RP-сервера — суммы аренды стоит привязывать именно к этой шкале, а не брать "на глазок".
Настройка покупки недвижимости
Для примера — упрощённый серверный обработчик покупки на QBCore, который проверяет баланс и текущий лимит игрока (детали лимита — в следующем разделе):
RegisterNetEvent('housing:server:BuyHouse', function(houseId)
local src = source
local Player = QBCore.Functions.GetPlayer(src)
local house = MySQL.single.await('SELECT * FROM player_houses WHERE house_id = ?', {houseId})
if not house or house.owner ~= nil then
TriggerClientEvent('QBCore:Notify', src, 'Дом уже продан', 'error')
return
end
if Player.PlayerData.money.bank < house.price then
TriggerClientEvent('QBCore:Notify', src, 'Недостаточно денег на карте', 'error')
return
end
Player.Functions.RemoveMoney('bank', house.price, 'house-purchase')
MySQL.update('UPDATE player_houses SET owner = ? WHERE house_id = ?', {
Player.PlayerData.citizenid, houseId
})
TriggerClientEvent('QBCore:Notify', src, 'Поздравляем, дом ваш!', 'success')
end)
Ключ от дома стоит выдавать как предмет инвентаря (house_key_<id>), а не хранить доступ только в базе — так игроки могут передавать ключи друзьям физически, что укладывается в RP-логику лучше, чем команда /addkeyholder.
Аренда с регулярной оплатой
Аренда отличается от покупки тем, что требует фонового процесса, который проверяет просрочку и списывает деньги. Не делайте это через os.time() в реальном времени привязанным к серверному аптайму — используйте дату из базы (rent_due), чтобы рестарт сервера не сбрасывал таймеры:
-- серверный тред, проверка раз в 5 минут
CreateThread(function()
while true do
Wait(5 * 60 * 1000)
local overdue = MySQL.query.await(
'SELECT * FROM player_houses WHERE is_rented = 1 AND rent_due <= NOW()'
)
for _, house in pairs(overdue) do
local Player = QBCore.Functions.GetPlayerByCitizenId(house.owner)
local paid = false
if Player and Player.PlayerData.money.bank >= house.rent_price then
Player.Functions.RemoveMoney('bank', house.rent_price, 'rent-payment')
paid = true
end
if paid then
MySQL.update([[
UPDATE player_houses SET rent_due = DATE_ADD(NOW(), INTERVAL 7 DAY)
WHERE house_id = ?
]], {house.house_id})
else
-- игрок офлайн или без денег — либо предупреждение, либо выселение
MySQL.update('UPDATE player_houses SET rent_due = DATE_ADD(rent_due, INTERVAL 1 DAY) WHERE house_id = ?', {house.house_id})
-- через N дней просрочки — выселение
if house.rent_overdue_days and house.rent_overdue_days >= 3 then
MySQL.update('UPDATE player_houses SET owner = NULL, is_rented = 0 WHERE house_id = ?', {house.house_id})
end
end
end
end
end)
Реальная логика будет чуть сложнее (нужен счётчик просрочек, уведомление в Discord через вебхук за день до выселения, продление аренды досрочно), но принцип такой: не полагаться на онлайн-таймеры, держать всё в датах базы, обрабатывать список пачкой раз в несколько минут.
Отдельно продумайте, что происходит с вещами в доме при выселении — сброс инвентаря стеша сразу же вызывает жалобы игроков. Практика, которая снимает большинство конфликтов: при просрочке дом просто блокируется (владелец не может войти, аренда приостановлена), а полный сброс с очисткой стеша — только через отдельную команду администратора после разбора ситуации.
Ограничение числа объектов на игрока
Без лимита один задонативший игрок скупает половину улицы в первый день wipe'а, а остальным нечего покупать. Лимит проверяется на сервере при каждой попытке покупки/аренды — клиентскую проверку дублировать не нужно, она легко обходится:
QBCore.Functions.CreateCallback('housing:server:CanBuyHouse', function(source, cb)
local Player = QBCore.Functions.GetPlayer(source)
local count = MySQL.scalar.await(
'SELECT COUNT(*) FROM player_houses WHERE owner = ?',
{Player.PlayerData.citizenid}
)
local maxHouses = Config.MaxHousesPerPlayer or 2
cb(count < maxHouses)
end)
Разумные значения для старта: 1-2 дома в собственности плюс отдельный (не пересекающийся) лимит на арендованное жильё, например ещё 1. Держите лимиты покупки и аренды в разных счётчиках — иначе игроки, которые хотят просто снимать студию, конкурируют за тот же слот с теми, кто копит на особняк, и это не всегда справедливо по ощущениям сообщества. Если сервер про фракции — заведите отдельный тип объекта "штаб фракции" вне общего лимита физлиц, привязанный к ID группы, а не к citizenid.
Ещё один рабочий вариант ограничения — не жёсткий лимит по количеству, а прогрессивный налог: второй дом стоит дороже базовой цены на фиксированный процент, третий ещё дороже. Это мягче для RP (не запрещает, но делает невыгодным скупку) и проще продать игрокам как "рыночную логику", а не административное ограничение.
Поднять сервер FiveM (GTA V) за пару минут
Готовый образ MAATRIX GAMES: NVMe, AMD EPYC, DDoS-защита, панель управления. Локации UK, US, RU. Оплата картой РФ, СБП или криптой.
Создать серверЧастые вопросы
Можно ли сделать несколько выходов из одного дома (например, гараж отдельно от парадной двери)?
Да, для этого заводится не пара координат, а массив пар — каждая точка входа/выхода получает свой BoxZone с общим house_id, но разным индексом двери в таблице.
Что будет с игроком, если сервер перезагрузится, пока он внутри MLO-интерьера?
Ничего критичного — интерьер это просто координаты, при реконнекте игрок заспавнится либо на последней позиции (если она валидна), либо на дефолтном спавне, если стример ещё не прогрузил модель. Стоит добавить проверку на клиенте: если координаты входа "внутри" MLO, а модель ещё не загрузилась, держать игрока в fade-in до момента, пока HasCollisionLoadedAroundEntity не вернёт true.
Сколько стоят готовые платные MLO под жильё?
Разброс большой — от бесплатных ресурсов на Tebex/GitHub до платных паков за несколько десятков долларов за интерьер. Точные цены и версии моделей меняются слишком часто, чтобы называть конкретные числа — ориентируйтесь на актуальные витрины авторов на момент покупки.
Аренда обязательно должна списываться с банковского счёта, а не с наличных?
Технически можно списывать откуда угодно, но банк удобнее — списание работает даже если игрок офлайн (у наличных такого "виртуального кошелька" может не быть в вашем инвентарном скрипте).
Нужно ли отдельно проверять лимит домов при передаче дома другому игроку (например, через RP-сделку)?
Обязательно — если у вас есть команда /transferhouse или подобная, она должна дёргать тот же callback проверки лимита, что и покупка, иначе через передачу лимит легко обходится.