• Из-за обновления GTA 5 (был добавлен новый патч) может временно не работать вход в RAGE Multiplayer.

    ERROR: Your game version is not supported by RAGE Multiplayer.

    Данная ошибка говорит о том, что GTA V обновилась до новой версии (GTA Online тоже). Вам необходимо обновить саму игру в главном меню вашего приложения (Steam / Epic Games / Rockstar Games).
    Если после этого RAGE:MP все равно не работает - вам нужно дождаться выхода патча для самого мультиплеера (обычно это занимает от нескольких часов до нескольких дней).

    Новости и апдейты Rockstar Games - https://www.rockstargames.com/ru/newswire/
    Статус всех служб для Rockstar Games Launcher и поддерживаемых игр: https://support.rockstargames.com/ru/servicestatus


    Grand Theft Auto 5 (+ GTA Online) последний раз были обновлены:

Урок Гайд на проектирование

MoonFusion

Гуру
Автор темы
high coder
14 Июн 2021
308
249
123
Скажу сразу, я окончил суммарно 8 классов, не пошел в университет и прочёл всего 5 книг за всю свою жизнь, я не претендую на звание "топ архитектор" и не утверждаю что материалы описанные ниже являться стандартом и обязательны к реализации на любом проекте, это лишь мой опыт, который позволил мне решать более широкий спектр задач.

Данные руководство подразумевает что вы как минимум знакомы с основами программирования и изучали объектно ориентированный подход ( ООП )
В данном уроке используется сервис draw.io и базовые элементы UML.


Мотивацией написать это руководство служит безграничное количество оупен-сорс/слитых проектов с процедурным, нечитаемым, ужасным кодом, по этому и начнём мы с этого.

Ниже я привёл несколько популярных вопросов и заблуждений связанных с темой.

Зачем проектировать? Я разбиваю свой проект на модули и этого достаточно!
Я думаю примерно такие мысли посещают умы людей которые пишут ресурсы/проекты процедурно, используя "модульный подход".
К сожалению модульный подход не решает все проблемы, он лишь обеспечивает читабельность кода и структурирует файлы проекта.

В чем конкретно поможет мне проектирование?
В первую очередь проектирование поможет найти дыры в логике, потенциальные проблемы с реализацией ещё до того как вы откроете вашу любимую среду разработки.
Вы сможете до мелочей продумать определённую систему, разбить все на конкретные классы и исключить повторение кода, что уже добавить вас +100 к эффективности разработки.
Так же это даст вам полный контроль над системой, упростится её поддержка и расширение, при условии что вы все сделали правильно.

Проектирование занимает слишком много времени
Я слышал это утверждение сразу от нескольких моих знакомых, "Проектировать на моём пет проекте/маленьком проекте не имеет смысла, я потрачу время зря".
Забавно но все работает в точности да наоборот, как раз таки проектирование поможет сохранить ваше время в будущем, потратив час-второй на то что бы расписать классы и их взаимодействие на draw.io сэкономит вам уйму времени в будущем.


Теперь же, после того как мы выяснили зачем нам это нужно, мы можем приступить к более подробному алгоритму действий, а так-же к наглядным примерам.

Важная ремарка, что бы закрепить все дальнейшие действия на практике, рекомендую повторить это с любой уже написанной вами ранее системой.


Что бы понять как все это работает мы сделаем все как обычно, без проектирования, просто сядем и напишем какую-то процедурную систему, которая будет выполнять одну простенькую задачу, а затем сядем и спроектируем эту же задачу, после чего снова реализуем её в коде, писать мы будем систему админ-авто, её описание ( ТЗ ) будет ниже.

Важно отметить что данный код не будет тестироваться в игре, это ни к чему, моя задача показать обычный код, который написал бы работяга, а затем спроектировать эту же систему и реализовать её, дабы наглядно показать разницу в эффективности.


Техническое задание:

Реализовать систему "админ-транспорта", добавить команду которая будет спавнить авто для администратора с указанной моделью авто, при выходе администратора из авто, оно удаляется спустя N времени, реализовать возможность на ходу заменить транспорт на другой, предусмотреть возможность починки транспорта и смены его цвета. Так-же не забываем про ручное удаление авто.
Для начала сделаем базовую настройку проекта, я буду использовать NodeJS + TypeScript ( Server ) / JavaScript + TypeScript ( Client ) / Esbuild в качестве сборщика.
Разделим проект на Server/Client, сделаем npm i в каждой папочке, настроим Esbuild так, что бы он собирал все в один файлик в папку дист как для сервера так и для клиента.
Я не буду показывать как, все это вы сможете найти в github репозитории.

Далее я сделаю один костыль, я немного расширю базовый тип PlayerMp, добавив туда пару своих полей.
( не рекомендую это делать, рано или поздно вы запутаетесь в этом, но для примера - сойдёт. )

1686190871021.png


Посмотрим на структуру файлов:
1686207574771.png


Основной файл нашего модуля adminCar.ts, с него и начнём.

JavaScript:
// Время после выхода из авто, для его удаления
const TIMEOUT_TIME = 10000;

// Функция создающая админ-машину
export function createCar(player: PlayerMp, model: number): VehicleMp {
    const { x, y, z } = player.position;

    // Создаём машину рядом с игроком
    const vehicle = mp.vehicles.new(model, new mp.Vector3(x + 2, y, z), {
        dimension: player.dimension,
        heading: player.heading,
        numberPlate: 'JhonnyGay(ts)'
    });

    // Таймер - это костыль, что бы подождать пока машина появится
    setTimeout(() => {
        // Засовываем игрока на место водителя
        player.putIntoVehicle(vehicle, 0);
    }, 100);

    // Возвращаем авто
    return vehicle;
}

// Функция для уничтожения админ-машины
export function destroyCar(vehicle: VehicleMp, reason?: string) {
    // Проверяем есть ли такая машина на сервере
    if(!mp.vehicles.exists(vehicle)) {
        return
    }

    // Выкидываем всех игроков с машины
    removeOccupants(vehicle);

    // Удаляем Entity машины
    vehicle.destroy();

    // Дополнительно, выводим по какой причине была удалена машина
    if(reason) {
        console.log(`Машина удалена по причине: ${reason}`);
    }
}

// Функция для обработки выхода админа из машины
export function resetTimeout(player: PlayerMp, vehicle: VehicleMp) {
    // Проверяем что машина и игрок на сервере
    if(!mp.players.exists(player) || !mp.vehicles.exists(vehicle)) {
        return
    }

    // Запускаем таймер, и в хендлере удаляем авто
    const timeout = setTimeout(() => {
        destroyCar(vehicle, 'timeout');
        player.adminCar = null;
    }, TIMEOUT_TIME);

    // Возвращаем таймер
    return timeout;
}

// Функция для замены авто на другое
export function swapCar(player: PlayerMp, currentVehicle: VehicleMp, newVehicleModel: number): VehicleMp {
    if(mp.vehicles.exists(currentVehicle)) {
        removeOccupants(currentVehicle);
        destroyCar(currentVehicle, 'swapCar')
    }

    return createCar(player, newVehicleModel);
}

// Функция для починки авто
export function repairCar(vehicle: VehicleMp) {
    if(!mp.vehicles.exists(vehicle)) {
        return;
    }

    vehicle.repair();
}

// Функция для смены цвета авто
export function changeColor(vehicle: VehicleMp, color: RGB) {
    if(!mp.vehicles.exists(vehicle)) {
        return
    }

    vehicle.setColorRGB(color[0], color[1], color[2], color[0], color[1], color[2]);
}

// Утилити функция для высадки всех из машины
function removeOccupants(vehicle: VehicleMp) {
    if(vehicle.getOccupants().length > 0) {
        vehicle.getOccupants().forEach((player) => {
            if(mp.players.exists(player)) {
                player.removeFromVehicle();
            }
        });
    }
}

Далее нам нужно обрабатывать ивенты такие как игрок вышел с машину, что бы удалить машину, игрок вышел из игры, что бы опять же удалить машину, игрок зашел в игру, что бы подготовить его окружение к системе и игрок вошел обратно в транспорт, что бы сбросить таймер, все это мы обрабатываем в events.ts

JavaScript:
import { destroyCar, resetTimeout } from "./adminCar";

// Игрок зашел в игру
mp.events.add('playerJoin', (player: PlayerMp) => {
    // Подготовим игрока к системе
    player.isAdmin = true;
    player.adminCar = null;
    player.adminCarTimer = null;
});

// Игрок покинул транспорт
mp.events.add('playerExitVehicle', (player: PlayerMp, vehicle: VehicleMp) => {
    // Куча проверок, что бы не получить ошибки при исполнении
    if(!player.adminCar || player.adminCar.id !== vehicle.id) {
        return;
    }

    if(player.adminCarTimer !== null) {
        clearTimeout(player.adminCarTimer);
        player.adminCarTimer = null;
    }

    // Ресетим таймер модулем
    player.adminCarTimer = resetTimeout(player, vehicle);
});

// Игрок зашел в машину
mp.events.add('playerEnterVehicle', (player: PlayerMp, vehicle: VehicleMp) => {
    // Опять же, миллион проверок.
    if(!player.adminCar || player.adminCar.id !== vehicle.id) {
        return;
    }

    if(player.adminCarTimer === null) {
        return;
    }

    // Чистим таймер
    clearTimeout(player.adminCarTimer);
    player.adminCarTimer = null;
});

// Игрок вышел
mp.events.add('playerQuit', (player: PlayerMp) => {
    // Убираем машину
    if(player.adminCar !== null) {
        destroyCar(player.adminCar);
    }

    // Чистим таймер
    if(player.adminCarTimer !== null) {
        clearTimeout(player.adminCarTimer);
    }
});

Последний файл, commands.ts, задача файла принимать команды игрока, обрабатывать доступ и параметры, после чего делегировать логику на основной модуль ( adminCar.ts )

JavaScript:
import { changeColor, createCar, destroyCar, repairCar, swapCar } from "./adminCar";

mp.events.addCommand('admincar', (player: PlayerMp, fullText: string, model: string) => {
    // Куча проверок
    if(!player.isAdmin || player.adminCar !== null) {
        return;
    }

    // Получаем модель
    const modelHash = model ? mp.joaat(model) : null;

    if(modelHash === null) {
        return;
    }

    // Создаём машину с помощью основного модуля
    player.adminCar = createCar(player, modelHash);
});

// Удаляем машину, все аналогично
mp.events.addCommand('delcar', (player: PlayerMp, fullText: string) => {
    if(!player.isAdmin || player.adminCar === null) {
        return;
    }

    destroyCar(player.adminCar, 'delcar');

    if(player.adminCarTimer !== null) {
        clearTimeout(player.adminCarTimer);
        player.adminCarTimer = null;
    }

    player.adminCar = null;
});

// Меняем цвет машины
mp.events.addCommand('colorcar', (player: PlayerMp, fullText: string, red: string, green: string, blue: string) => {
    if(!player.isAdmin || player.adminCar === null) {
        return;
    }


    if(!red || !green || !blue) {
        return;
    }

    const rgb: RGB = [parseInt(red), parseInt(green), parseInt(blue)];
    changeColor(player.adminCar, rgb);
});

// Меняем машину
mp.events.addCommand('swapcar', (player: PlayerMp, fullText: string, newModel: string) => {
    if(!player.isAdmin || player.adminCar === null) {
        return;
    }

    const modelHash = newModel ? mp.joaat(newModel) : null;

    if(modelHash === null) {
        return;
    }

    player.adminCar = swapCar(player, player.adminCar, modelHash);
});

// Чиним машину
mp.events.addCommand('repair', (player: PlayerMp, fullText: string) => {
    if(!player.isAdmin || player.adminCar === null) {
        return;
    }

    repairCar(player.adminCar);
});

Подведём итоги нашей реализации.

Эта реализация не худшая из тех что я видел, но все же процедурный подход даёт о себе знать.
Ниже я постарался расписать плюсы и минусы такой реализации.

Минусы:

  • Низкая читабельность кода, причём чем больше сюда будет добавляться методов, тем менее читабельнее будет код.
  • Трудно расширять, что бы что то добавить нужно писать ещё одну функцию, причем чем больше мы добавляем, тем сложнее добавлять, а что бы к примеру добавить уровни к админке и и сделать все эти команды с определённого уровня нужно будет залезть в каждую команду и поменять условия доступа, а что если нужно будет ввести авторизацию для администратора? Теперь нам нужно опять идти по всем командам связанным с системой и добавлять ещё одну проверку.
  • Низкая эффективность, исходя из пунктов выше вытекает этот, что бы расширять систему, её нужно читать, а у нас проблемы и с тем и с другим.
  • Контроль над кодом, у нас есть плавающие значения по типу player.adminCar и player.adminCarTimer, которые нужно постоянно перепроверять.

Плюсы:

  • Модульная структура файлов
  • Быстрая реализация
  • TypeScript :roflanebalo:

А теперь попробуем спроектировать эту систему, используя объектно ориентированный подход

Без лишних слов перейдём к более серьёзному подходу к задаче и начнём мы на этот раз не с открытия Visual Studio Code, а с перехода на сайт draw.io.
Для более глубокого понимания происходящего мне не так давно посоветовали прочитать эту книгу, я её конечно не прочитал, просто пробежался по главам, но уверен, кому то информация отсюда будет восприниматься куда легче.
Для начала немного объясню что мы будем использовать внутри сервиса, для удобства я создаю белый прямоугольник на фоне и блокирую его с помощью контекстного меню, почему то меня раздражает базовая сетка. Далее обратим внимание на блок слева, там нам в основном понадобится раздел UML.

UML - это по сути язык разметки, которым мы будем описывать сущности в нашем случае системы, связи между этими сущностями и так далее


Начать стоит с разделение нашей системы на сущности в нашем примере будет логично создать три сущности: Admin, AdminHandler и AdminVehicle, причем в нашем варианте реализации мы будем использовать агрегацию.

Агрегация - это концепция которая говорит нам о том, что один объект содержит в себе в себе ссылку, или ссылки на другие объекты.
Создадим две сущности, в Admin добавим поля: id: number, player: PlayerMp, vehicle: AdminVehicle, как видим вот и агрегация, наш Admin в определённый момент времени будет содержать ссылку на объект AdminVehicle, но при это AdminVehicle остаётся отдельной сущностью, далее опишем некоторые методы, так-как наш класс Admin в данном случае выступает классом-ацессором, который будет выполнять всего две функции, проверять доступ к командам и предоставлять доступ к AdminVehicle, тут будет всего четыре метода:
JavaScript:
hasAccess(), createVehicle(mode: Hash), onPlayerLeave() и onVehicleDestroy()

В AdminVehicle добавим следующие поля:
JavaScript:
vehicleEntity: VehicleMp, destroyTimer: ReturnType<typeof setTimeout> model: Hash, color: RGB
Как видим наша сущность машины даже не знает о существовании игрока, собственно нам это и не нужно.

Так же добавим методы для обработки некоторых действий, а именно:
JavaScript:
init(player: PlayerMp), destroy(), changeColor(color: RGB), swapModel(newModel: Hash), onDriverExit(), onDriverEnter(), removeOccupants(), checkVehicle(), resetTimer(), repair()
Все эти методы нужны для обработки определённых событий, но каких событий?

Мы создадим два процедурных файл - controller.ts, который будет отвечать за обработку необходимых нам ивентов, таких как выход из авто, вход в авто, выход и вход игрока на сервер, так же если бы у нас было клиент-серверное взаимодействие, там были бы и другие ивенты которые мы бы вызывали с клиента и commands.ts, который будет обрабатывать команды игрока.


А теперь подумаем о том, как же нам получить доступ к классу Admin? Откуда мы будем у какого игрока есть сущность Admin, а у какого нет?

Тут на самом деле есть два варианта развития событий, первый как и в предыдущей реализации мы добавим в типизацию PlayerMp поле содержащее ссылку на объект Admin, и будем присваивать ссылку например при входе игрока на сервер, но у такого варианта есть несколько недостатков, самый главный из которых - сложность в читабельности. Мы никогда точно не знаем есть ли там объект, или нет, что является проблемой.

По этому мы пойдём по второму пути, мы создадим статический класс AdminHandler, который будет создавать, хранить, удалять и отдавать экземпляры.
Давайте посмотрим какие у него будут поля и методы, начнём с поля, тут есть одна очень распространённая ошибка делать storage статическим полем класса, таким образом мы сможем изменять этот сторедж с любого места программы, что нам не подходит, почему? Думаю объяснять не нужно. Тут есть много всяких вариантов и рекомендаций, но проще всего - создать переменную чуть выше класса, и не экспортировать её, что позволит нам иметь доступ к ней только внутри файла, чего нам будет достаточно.

JavaScript:
const storage = new Map<number, Admin>();
let idGenerator = 0;

Это - хранилище наших администраторов, сюда они будут попадать после создания их экземпляра и удаляться при удалении экземпляра.
Так же добавим utility переменную idGenerator, в коде будет ясно зачем она нужна.

Методы нашего статического класса:
JavaScript:
create(player: PlayerMp), getByPlayer(player: PlayerMp), remove(id: number).

В целом это все что нужно, давайте посмотрим на результат.

Получилась довольно простенькая диаграмма, три сущности, AdminHandler, Admin и AdminVehicle, AdminVehicle связан с Admin агрегацией, а AdminHandler создаёт экземпляры Admin, так же AdminHandler использует две внешние переменные, storage и idGenerator.
1686248743463.png


Выглядит неплохо, я потратил на это от силы минут 10, теперь же давайте напишем код


Реализация в отличии от предыдущей будет опираясь на парадигму ООП, с использованием принципов SOLID, DRY и так далее.

Важная ремарка, я сделаю один костыль, что бы не писать хуки, метод AdminVehicle destroy будет оповещать класс AdminHandler о том, что машина уничтожена, в свою очередь AdminHandler будет находить кому эта машина принадлежит и присваивать полю adminVehicle = null, так делать не нужно, обычно для таких целей использую хуки, либо ивенты.
Рассмотрим файлы, структура получилась следующая:
1686248904254.png


Admin.ts

JavaScript:
import { AdminVehicle } from "./AdminVehicle";

export class Admin {
    public readonly id: number;
    private readonly _player: PlayerMp;
    private _adminVehicle: AdminVehicle;


    public get player() : PlayerMp {
        return this._player;
    }

    public get adminVehicle() : AdminVehicle {
        return this._adminVehicle;
    }

    constructor(id: number, player: PlayerMp) {
        this.id = id;
        this._player = player;
        this._adminVehicle = null;
    }

    hasAccess(): boolean {
        if(this._adminVehicle === null) {
            return false;
        }

        // Сколько угодно различных проверок
        return true;
    }

    createVehicle(model: number) {
        if(this._adminVehicle !== null) {
            return;
        }

        this._adminVehicle = new AdminVehicle(model, [0, 0, 0]);
        this._adminVehicle.init(this.player);
    }

    onPlayerLeave() {
        this._adminVehicle.destroy();
    }

    onVehicleDestroy() {
        this._adminVehicle = null;
    }
}

AdminHandler.ts

JavaScript:
import { Admin } from "./Admin";
import { AdminVehicle } from "./AdminVehicle";

const storage = new Map<number, Admin>();
let idGenerator = 0;

export class AdminHandler {
    static create(player: PlayerMp) {
        if(this.getByPlayer(player)) {
            return;
        }

        const id = idGenerator++;
        const admin = new Admin(id, player);

        storage.set(id, admin);
    }

    static getByPlayer(player: PlayerMp) {
        return [...storage.values()].find((item) => item.player.id === player.id);
    }

    static getByAdminVehicle(adminVehicle: AdminVehicle) {
        return [...storage.values()].find((item) => item.adminVehicle.vehicleEntity.id === adminVehicle.vehicleEntity.id);
    }

    static remove(id: number) {
        if(storage.has(id)) {
            storage.delete(id);
        }
    }
}

AdminVehicle.ts
JavaScript:
import { AdminHandler } from "./AdminHandler";

const DESTROY_TIMER = 10000;

export class AdminVehicle {
    private _vehicleEntity: VehicleMp | null;
    private destroyTimer: ReturnType<typeof setTimeout> | null;
    private model: number;
    private color: RGB;

    public get vehicleEntity(): VehicleMp {
        return this._vehicleEntity;
    }

    constructor(model: number, color: RGB) {
        this.model = model;
        this.color = color;

        this.destroyTimer = null;
        this._vehicleEntity = null;
    }

    init(player: PlayerMp) {
        const { x, y, z } = player.position;

        this._vehicleEntity = mp.vehicles.new(this.model, new mp.Vector3(x + 2, y, z), {
            dimension: player.dimension,
            color: [this.color, this.color]
        });

        setTimeout(() => {
            player.putIntoVehicle(this._vehicleEntity, 0);
        }, 100);
    }

    changeColor(newColor: RGB) {
        if(!this.checkVehicle()) {
            return;
        }

        this.color = newColor;
        this._vehicleEntity.setColorRGB(this.color[0], this.color[1], this.color[2], this.color[0], this.color[1], this.color[2]);
    }

    swapModel(player: PlayerMp, newModel: number) {
        if(!this.checkVehicle()) {
            return
        }

        this.model = newModel;
        this.destroy();
        this.init(player);
    }

    onDriverExit() {
        if(!this.checkVehicle()) {
            return;
        }

        this.resetTimer();
        this.destroyTimer = setTimeout(() => {
            this.destroy();
        }, DESTROY_TIMER);
    }

    onDriverEnter() {
        if(!this.checkVehicle) {
            return;
        }

        this.resetTimer();
    }

    repair() {
        if(!this.checkVehicle()) {
            return;
        }

        this._vehicleEntity.repair();
    }

    private removeOccupants() {
        if(!this.checkVehicle()) {
            return;
        }

        const occupants = this._vehicleEntity.getOccupants();

        if(occupants.length > 0) {
            occupants.forEach((player) => {
                player.removeFromVehicle();
            });
        }
    }

    private checkVehicle() {
        if(this._vehicleEntity === null) {
            return false;
        }
      
        if(!mp.vehicles.exists(this._vehicleEntity)) {
            return false
        }
      
        return true;
    }

    private resetTimer() {
        if(this.destroyTimer !== null) {
            clearTimeout(this.destroyTimer);
            this.destroyTimer = null;
        }
    }

    destroy() {
        this.resetTimer();

        if(mp.vehicles.exists(this._vehicleEntity)) {
            this.removeOccupants();
            this._vehicleEntity.destroy();
        }

        const admin = AdminHandler.getByAdminVehicle(this);
        admin.onVehicleDestroy();
    }
}

commands.ts
JavaScript:
import { AdminHandler } from "./AdminHandler";

mp.events.addCommand('admincar', (player: PlayerMp, fullText: string, model: string) => {
    const admin = AdminHandler.getByPlayer(player);

    if(!admin || !admin.hasAccess()) {
        return;
    }

    const modelHash = model ? mp.joaat(model) : null;

    if(modelHash === null) {
        return;
    }

    admin.createVehicle(modelHash);
});

// Удаляем машину, все аналогично
mp.events.addCommand('delcar', (player: PlayerMp, fullText: string) => {
    const admin = AdminHandler.getByPlayer(player);

    if(!admin || !admin.hasAccess()) {
        return;
    }

    admin.adminVehicle.destroy();
});

// Меняем цвет машины
mp.events.addCommand('colorcar', (player: PlayerMp, fullText: string, red: string, green: string, blue: string) => {
    const admin = AdminHandler.getByPlayer(player);

    if(!admin || !admin.hasAccess()) {
        return;
    }

    const rgb: RGB = [parseInt(red), parseInt(green), parseInt(blue)];
    admin.adminVehicle.changeColor(rgb);
});

// Меняем машину
mp.events.addCommand('swapcar', (player: PlayerMp, fullText: string, newModel: string) => {
    const admin = AdminHandler.getByPlayer(player);

    if(!admin || !admin.hasAccess()) {
        return;
    }

    const modelHash = newModel ? mp.joaat(newModel) : null;

    if(modelHash === null) {
        return;
    }

    admin.adminVehicle.swapModel(player, modelHash);
});

// Чиним машину
mp.events.addCommand('repair', (player: PlayerMp, fullText: string) => {
    const admin = AdminHandler.getByPlayer(player);

    if(!admin || !admin.hasAccess()) {
        return;
    }

    admin.adminVehicle.repair();
});

controller.ts
JavaScript:
import { AdminHandler } from "./AdminHandler";

mp.events.add('playerJoin', (player: PlayerMp) => {
    AdminHandler.create(player);
});

mp.events.add('playerQuit', (player: PlayerMp) => {
    const admin = AdminHandler.getByPlayer(player);

    if(admin) {
        admin.onPlayerLeave();
        AdminHandler.remove(admin.id);
    }
});

mp.events.add('onPlayerExitVehicle', (player: PlayerMp, vehicle: VehicleMp) => {
    const admin = AdminHandler.getByPlayer(player);

    if(admin || admin.hasAccess()) {
        if(vehicle.id === admin.adminVehicle.vehicleEntity.id) {
            admin.adminVehicle.onDriverExit();
        }
    }
});

mp.events.add('onPlayerEnterVehicle', (player: PlayerMp, vehicle: VehicleMp) => {
    const admin = AdminHandler.getByPlayer(player);

    if(admin || admin.hasAccess()) {
        if(vehicle.id === admin.adminVehicle.vehicleEntity.id) {
            admin.adminVehicle.onDriverEnter();
        }
    }
});


Подведём итоги этой реализации.

Хоть кода стало и больше, но писать его по ощущениям было намного проще, это заняло у меня куда меньше времени чем реализация с процедурным подходом, код стал в разы читабельнее, а самое главное - код стал легко расширяемым, мы заменили все проверки на вызов одного метода, и теперь, что бы добавить какую-то проверку - мне достаточно просто добавить её в одном месте кода, что бы добавить какой то функционал мне больше не нужно трогать то что я уже написал, мне нужно будет просто добавить новый метод и обработать команду в контроллере.

Минусы:

  • Нужно потратить время на проектирование
  • Сложное взаимодействие классов в месте, где происходит удаление/создание авто

Плюсы:

  • Высокая читабельность кода, все разбито на сущности
  • Объектно ориентированный подход
  • Легко расширять систему
  • Относительно просто дебажить
Спасибо что дочитал до конца, напомню, прежде чем писать гневный ответ, перечитай самое начало темы.
Делюсь всеми ресурсами:

GitHub репозиторий: ( будет добавлен если попросите )
Draw.io диаграмма:
Тык
 
Последнее редактирование:

Harland David Sanders

Куратор портала
Команда форума
Куратор портала
VIP
high coder
media
10 Сен 2020
3,063
2,455
219

wholinc

Специалист
24 Янв 2023
775
454
95
23
+rep за работу
 

kirillzver

Гуру
2 Ноя 2020
156
117
104
Как для нынешнего комьюнити разработчиков на нашей любимой платформе, урок более чем исчерпывающий.

Немного бросается в глаза история с множеством mp.events.addCommand, их можно было впихнуть объектом в один вызов addCommand, плюс организовав функции в класс и/или добавив декоратор для того, чтобы абстрагироваться от одинаковой проверки на админа и его доступ.
Также, не стоит забывать про обработку ошибок с помощью try catch и что не менее важно — проверять наличие сущности (mp.pool.exists) перед первым взаимодействием с ней.
По большей части это касается игроков, так как рейдж любитель выбросить сегу. Хоть статья не об этом, но это достаточно важно.

В общем и целом, может хоть кто-то подчеркнёт для себя что-то новое и в сливах мы будет видеть приемлемый код, хотя бы на уровне слитого драйва/стрита.
 

MoonFusion

Гуру
Автор темы
high coder
14 Июн 2021
308
249
123
Как для нынешнего комьюнити разработчиков на нашей любимой платформе, урок более чем исчерпывающий.

Немного бросается в глаза история с множеством mp.events.addCommand, их можно было впихнуть объектом в один вызов addCommand, плюс организовав функции в класс и/или добавив декоратор для того, чтобы абстрагироваться от одинаковой проверки на админа и его доступ.
Также, не стоит забывать про обработку ошибок с помощью try catch и что не менее важно — проверять наличие сущности (mp.pool.exists) перед первым взаимодействием с ней.
По большей части это касается игроков, так как рейдж любитель выбросить сегу. Хоть статья не об этом, но это достаточно важно.

В общем и целом, может хоть кто-то подчеркнёт для себя что-то новое и в сливах мы будет видеть приемлемый код, хотя бы на уровне слитого драйва/стрита.
Отличное дополнение.
 
  • Like
Реакции: kirillzver

MiLT

Начинающий специалист
2 Янв 2023
59
45
47
а самое главное - код стал легко расширяемым, мы заменили все проверки на вызов одного метода, и теперь, что бы добавить какую-то проверку - мне достаточно просто добавить её в одном месте кода

А ООП то где? Ничто не мешало в первом примере так же вынести проверку в отдельную функцию и добавлять где душе угодно.
А самое главное, код стало не легко расширять, а легко модифицировать. Не стоит путать. Модификация - внесение изменение в уже существующий код. Расширение - изменение функциональности за счет написания нового кода.

Но в целом, гайд хороший и уже это большая часть здешнего комьюнити пропустит мимо ушей и продолжит дальше клепать что угодно куда угодно как угодно. На вторую часть можно пример и поудачнее выбрать, ибо это не то, что действительно стоит прям проектировать в полном смысле этого слова.
 
Последнее редактирование:
  • Like
Реакции: Inoi

MoonFusion

Гуру
Автор темы
high coder
14 Июн 2021
308
249
123
А ООП то где? Ничто не мешало в первом примере так же вынести проверку в отдельную функцию и добавлять где душе угодно.
А самое главное, код стало не легко расширять, а легко модифицировать. Не стоит путать. Модификация - внесение изменение в уже существующий код. Расширение - изменение функциональности за счет написания нового кода.

Но в целом, гайд хороший и уже это большая часть здешнего комьюнити пропустит мимо ушей и продолжит дальше клепать что угодно куда угодно как угодно. На вторую часть можно пример и поудачнее выбрать.
Второй части не будет, ты меня оскорбил..
 
  • Like
Реакции: MaxFanti

Amazingevich

Гуру
27 Апр 2021
666
487
124
Я не буду показывать как, все это вы сможете найти в github репозитории.
GitHub репозиторий: ( будет добавлен если попросите )
Ультрамегахарош :j3r:

В целом полезно и хорошо расписано, но применять в здравом смысле это мало кто будет (если конечно у разработчика проектирование не привито изначально). Почему? Потому что каждый дрочит как он хочет и при разработке хороших (RAGE) проектов на это смотрят в последний момент. Именно поэтому мы все удивляемся говнокоду, когда сливается очередной большой проект в публичный доступ (а это происходит, потому что задача №1 была не подготовить понятный для другого разработчика код, а побыстрее написать свою задумку и открыть проект).

Если смотреть за пределы рейджа - всё верно @MoonFusion красавчик
 

MoonFusion

Гуру
Автор темы
high coder
14 Июн 2021
308
249
123
Ультрамегахарош :j3r:

В целом полезно и хорошо расписано, но применять в здравом смысле это мало кто будет (если конечно у разработчика проектирование не привито изначально). Почему? Потому что каждый дрочит как он хочет и при разработке хороших (RAGE) проектов на это смотрят в последний момент. Именно поэтому мы все удивляемся говнокоду, когда сливается очередной большой проект в публичный доступ (а это происходит, потому что задача №1 была не подготовить понятный для другого разработчика код, а побыстрее написать свою задумку и открыть проект).

Если смотреть за пределы рейджа - всё верно @MoonFusion красавчик
На счёт гита, кринжово вышло, я на одном дыхании написал статью, а потом заливал это на гит, да так залил, что случайно шифт делитнул весь проект, но файлы описаны в теме, по этому собрать заново не проблема, вопрос лишь в том на сколько это востребовано
 
  • RoflanEbalo
Реакции: Amazingevich

JJIGolem

Гуру
high coder
19 Окт 2020
216
275
142
Как видим наша сущность машины даже не знает о существовании игрока, собственно нам это и не нужно.
Противоречие, так как в методы init и swapModel всё же передается игрок, хотя можно было бы обойтись и без этого. Плюс не совсем нравится то, что машина порождает себя и сажает игрока в себя же.

ошибка делать storage статическим полем класса

таким образом мы сможем изменять этот сторедж с любого места программы
Почему это ошибка? Разве нельзя сделать поле приватным, только для чтения и пользоваться им внутри самого класса? Если поле приватное, им никто другой, кроме твоего класса и не воспользуется.

AdminHandler, который будет создавать, хранить, удалять и отдавать экземпляры
Больше похоже на Repository, а не Handler, но это дело наименования.

опираясь на парадигму ООП, с использованием принципов SOLID, DRY
Не могли бы Вы, уточнить в каких местах мы использовали какой-то из принципов SOLID?

код стало не легко расширять, а легко модифицировать. Не стоит путать. Модификация - внесение изменение в уже существующий код. Расширение - изменение функциональности за счет написания нового кода.
Согласен с этим. Поэтому и стало интересно, где же это мы принципы SOLID использовали
 

MoonFusion

Гуру
Автор темы
high coder
14 Июн 2021
308
249
123
Противоречие, так как в методы init и swapModel всё же передается игрок, хотя можно было бы обойтись и без этого. Плюс не совсем нравится то, что машина порождает себя и сажает игрока в себя же.




Почему это ошибка? Разве нельзя сделать поле приватным, только для чтения и пользоваться им внутри самого класса? Если поле приватное, им никто другой, кроме твоего класса и не воспользуется.


Больше похоже на Repository, а не Handler, но это дело наименования.


Не могли бы Вы, уточнить в каких местах мы использовали какой-то из принципов SOLID?


Согласен с этим. Поэтому и стало интересно, где же это мы принципы SOLID использовали

Так, ну разберём тобою сказанное.

Противоречие, так как в методы init и swapModel всё же передается игрок, хотя можно было бы обойтись и без этого. Плюс не совсем нравится то, что машина порождает себя и сажает игрока в себя же.
Да я считаю что тут в общем и целом пример не удачный, не только моё мнение кстати. А на счёт противоречие, тут скорее имелось ввиду что сам класс не содержит поля игрока, но да, ты прав, можно было бы обойтись и без игрока в параметре.

Почему это ошибка? Разве нельзя сделать поле приватным, только для чтения и пользоваться им внутри самого класса? Если поле приватное, им никто другой, кроме твоего класса и не воспользуется.
Приватное поле в статическом классе? Он на то и статический, что бы не создавать его экземпляр.

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

А так, спасибо за внимание к теме и её изучение.
 

MoonFusion

Гуру
Автор темы
high coder
14 Июн 2021
308
249
123
Так, ну разберём тобою сказанное.


Да я считаю что тут в общем и целом пример не удачный, не только моё мнение кстати. А на счёт противоречие, тут скорее имелось ввиду что сам класс не содержит поля игрока, но да, ты прав, можно было бы обойтись и без игрока в параметре.


Приватное поле в статическом классе? Он на то и статический, что бы не создавать его экземпляр.


Я думаю было бы куда лучше и проще, если бы ты вместо того что бы задавать этот вопрос мне, написал где именно не соблюдается принцип SOLID, люди которые прочитают эту тему, думаю прочитают так-же и ответы, увидят твоё исправление и запомнят, в том числе возможно и я.

А так, спасибо за внимание к теме и её изучение.
Повнимательнее изучил вопрос с приватным статическим методом, оказывается TypeScript поддерживает такое, о чем я к сожалению не знал, да, в таком случае можно использовать статический приватный метод.
 

JJIGolem

Гуру
high coder
19 Окт 2020
216
275
142
Я думаю было бы куда лучше и проще, если бы ты вместо того что бы задавать этот вопрос мне
Имелось ввиду, раз для новичков готовится гайд, в котором мы упоминаем такое (SOLID), то лучше проводить эту параллель, как бы было не используя мы этот принцип, и как используя. Если мы это не подчеркиваем никак хотя бы, то лучше такое не упоминать.

S - удаление авто стучится в Handler, чтобы взять админа, к которому привязан, чтобы сообщить ему о том, что мы себя удалили. Все равно не избавились от проблемы, что используем админа и к нему привязаны. Также когда сажаем того же игрока при создании сущности авто в себя.
Не лучше ли, чтобы какой-то отдельный класс полностью взял на себя ответственность за удаление, создание авто и посадку в авто. То машина у нас стучится в админа, то админ в машину.

O - если мы захотим создать несколько тачек для админа, то натыкаемся на проблему, что придется переписать практически все 3 класса.
 
  • Like
Реакции: Inoi и MoonFusion

MaxFanti

Активный участник
5 Апр 2022
3
1
43
Автор 👍, ты молодец, что решил затронуть такую важную тему! 🌟 Естественно, небольшие косяки в коде имеются, о которых говорили выше, но он все равно данный код является прекрасным примером для наглядного изучения. 👌😊