Система Секретных Пакетов для Rage MP

Lev Angel

Developer
Команда форума
Скриптер
Сообщения
795
В этом уроке напишем с нуля систему секретных пакетов. Подобная система есть во всех играх серии GTA. Идея простая: по карте разбросаны пикапы и когда игрок подбирает их все, то получает какую-то награду.

Видео часть с подробным описанием:
Untitled design.png

Условия:
- Нужно запоминать какие пакеты уже нашел игрок и при перезаходе их больше уже не показывать ему
- Когда игрок подбирает пакет, нужно показывать ему уведомление в котором будет указано сколько пакетов он уже нашел и сколько их всего
- Когда игрок подбирает все пакеты - выдать награду и соответствующее уведомление

Для начала нам понадобится список координат для пикапов де будут размещаться секретные пакеты. Для урока я взял 4 точки, которые находятся недалеко друг от друга.

JavaScript:
const packages = [
    { x: -422.77093505859375, y: 1166.211669921875, z: 325.973876953125},
    { x: -404.05560302734375, y: 1161.9130859375, z: 325.98529052734375},
    { x: -410.40142822265625, y: 1134.956298828125, z: 325.973297119140},
    { x: -428.8808288574219, y: 1138.2325439453125, z: 325.9739685058594}
];


При входе игрока на сервер будем отправлять ему этот список

JavaScript:
mp.events.add("playerReady", player => {
    player.call('playerInitSecretPackages', [packages]);
});

На клиентской стороне будем создавать пикапы по этому списку координат. Кроме пикапа создаем еще и колшейп.

JavaScript:
const pickupHash = 3732468094;


mp.events.add('playerInitSecretPackages', (packages)=> {
    packages.forEach((package, id) => {
        let colshape = mp.colshapes.newSphere(package.x, package.y, package.z, 1);
        colshape.secretPackageId= id;
        colshape.secretPickup = mp.game.object.createPickupRotate(pickupHash, package.x, package.y, package.z, 0, 0, 0, 512, 0, 0, true, pickupHash);
    });
});

20210108124516_1.jpg

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

JavaScript:
mp.events.add("playerEnterColshape", (colshape) => {
    if(colshape.secretPickup){
        mp.events.callRemote('playerCollectedPackage', colshape.secretPackageId ); // отправляем id пакета на сервер
        mp.game.object.removePickup(colshape.secretPickup);
        colshape.destroy();
    }
});

Когда игрок подбираем пикап мы будем отправлять на сервер событие playerCollectedPackage в котором будем передавать id этого пакета.
На сервере мы будем запоминать какие пакеты подобрал игрок, сохранять их в файл. Когда игрок заходит на сервер мы будем загружать список с id найденный пикапов и передавать на клиент не весь список packages, а только те, которые еще не были найдены.
Для этого модифицируем на серверный код

JavaScript:
var fs = require('fs'); // подключим модуль для работы с файлами

В папке packages/sevret-packages создадим папку data. В ней мы будем сохранять в файлах список id найденных пакетов для каждого игрока в отдельном файле. Название файла должно быть уникально и как-то связанно с игроком, чтобы при повторном заходе мы могли его найти и правильно определить. В этом уроке для простоты мы будем использовать ник игрока, но в реальном проекте советую использовать что-то другое, например, серийный номер клиента.
В самом файле мы будем хранить массив с id в виде JSON строки.

JavaScript:
mp.events.add("playerReady", player => {


    fs.readFile( __dirname + `/data/${player.name}.txt`, function(err,data){ // загружаем список найденный пакетов для игрока
        let collectedPackages = new Array();


        if(!err){
            collectedPackages = JSON.parse(data);
        }
        // если файла нет или возникла другая ошибка то collectedPackages будет просто пустым массивом


        player.packagesCollected = collectedPackages;


        let playerPackages = new Array(); // в этот массив запишем все координаты которые игрок еще не нашел


        packages.forEach((package, packageId) => {
            if (player.packagesCollected.indexOf(packageId) == -1){
                package.id = packageId // обязательно добавляем к координатам порядковый id пакета в массиве packages, т. к. в playerPackages порядок координат будет другой
                playerPackages.push(package);
            }
        });


        player.call('playerInitSecretPackages', [playerPackages]);


    });
});

Когда формируем массив playerPackages, то в него не попадут уже найденные ранее игроком координаты. Соответственно в самом playerPackages нумерация координат будет отличаться от исходной в packages. Когда мы сохраняем список id найденных игроком, то они соответствуют порядковым номерам в массиве packages. Поэтому чтобы с клиента нам приходили верные id - мы добавляем к координатам еще и id.
package.id = packageId

Клиентский код тоже немного модифицируем, чтобы учитывать наличие id в самом объекте с координатами.
JavaScript:
mp.events.add('playerInitSecretPackages', (packages)=> {
    packages.forEach((package) => {
        let colshape = mp.colshapes.newSphere(package.x, package.y, package.z, 1);
        colshape.secretPackageId= package.id;
        colshape.secretPickup = mp.game.object.createPickupRotate(pickupHash, package.x, package.y, package.z, 0, 0, 0, 512, 0, 0, true, pickupHash);
    });
});

Когда игрок подбирает пикам мы будем добавлять его в список найденных и сохранять его сразу в файл на сервере.
JavaScript:
mp.events.add("playerCollectedPackage", (player, packageId) => {
    player.packagesCollected.push(packageId);
    playerStorePackagesData(player);
    console.log(`${player.name} collected secret package id: ${packageId}`);
});




function playerStorePackagesData(player){
    fs.writeFile(__dirname + `/data/${player.name}.txt`, JSON.stringify(player.packagesCollected), () => {});
}

На клиент обратно будем слать событие с подтверждением. В нем мы отправим число - сколько всего пакетов и сколько из них нашел игрок

JavaScript:
mp.events.add("playerCollectedPackage", (player, packageId) => {
    ...
    
    player.call('playerSecretPackageInfo', [packages.length, player.packagesCollected.length]);
});

Для того чтобы показывать красивые уведомления мы будем использовать библиотеку scaleform_messages

JavaScript:
mp.events.add('playerSecretPackageInfo', (totalPackages, collectedPackages)=> {
    let title = "Секретный пакет найден";
    if(collectedPackages == totalPackages){
        title = "Вы нашли все Секретные пакеты";
    }


    mp.events.call("ShowShardMessage", title, `Найдено пакетов: ${collectedPackages}/${totalPackages} `);
});

20210108124529_1.jpg

Также на серверной стороне мы будем выдавать какую-то награду когда игрок найдет все секретные пакеты. В нашем случае это будет оружиме миниган :)

JavaScript:
mp.events.add("playerCollectedPackage", (player, packageId) => {
    ...


    if( player.packagesCollected.length == packages.length){
        playerCollectAllPackages(player);
    }
});




function playerCollectAllPackages(player){
    console.log(`${player.name} collected all secret packages`);
    player.giveWeapon(mp.joaat('weapon_minigun'), 10000);
}

20210108124615_1.jpg

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

Вложения

  • secret-packages.zip
    5,3 КБ · Просмотры: 22

Revalto

Trainee
Сообщения
9
JavaScript:
mp.events.add("playerCollectedPackage", (player, packageId) => {
    ...


    if( player.packagesCollected.length == packages.length){
        playerCollectAllPackages(player);
    }
});




function playerCollectAllPackages(player){
    console.log(`${player.name} collected all secret packages`);
    player.giveWeapon(mp.joaat('weapon_minigun'), 10000);
}

Не совсем понятно, зачем выносить в отдельную функцию ;o
JavaScript:
mp.events.add("playerCollectedPackage", (player, packageId) => {
    ...

    if( player.packagesCollected.length == packages.length){
        console.log(`${player.name} collected all secret packages`);
        player.giveWeapon(mp.joaat('weapon_minigun'), 10000);
    }
});

JavaScript:
mp.events.add('playerSecretPackageInfo', (totalPackages, collectedPackages)=> {
    let title = "Секретный пакет найден";
    if(collectedPackages == totalPackages){
        title = "Вы нашли все Секретные пакеты";
    }


    mp.events.call("ShowShardMessage", title, `Найдено пакетов: ${collectedPackages}/${totalPackages} `);
});

Отвечал по поповоду этого на видео
JavaScript:
mp.events.add('playerSecretPackageInfo', (totalPackages, collectedPackages)=> {
    mp.events.call("ShowShardMessage", collectedPackages == totalPackages ? `Вы нашли все Секретные пакеты` : `Секретный пакет найден`, `Найдено пакетов: ${collectedPackages}/${totalPackages} `);
});

Ну и приятный на глаз:
JavaScript:
mp.events.add('playerSecretPackageInfo', (totalPackages, collectedPackages)=> {
    mp.events.call(
        "ShowShardMessage", 
        collectedPackages == totalPackages 
            ? `Вы нашли все Секретные пакеты` 
            : `Секретный пакет найден`, 
        `Найдено пакетов: ${collectedPackages}/${totalPackages} 
    `);
});

JavaScript:
mp.events.add("playerReady", player => {


    fs.readFile( __dirname + `/data/${player.name}.txt`, function(err,data){ // загружаем список найденный пакетов для игрока
        let collectedPackages = new Array();


        if(!err){
            collectedPackages = JSON.parse(data);
        }
        // если файла нет или возникла другая ошибка то collectedPackages будет просто пустым массивом


        player.packagesCollected = collectedPackages;


        let playerPackages = new Array(); // в этот массив запишем все координаты которые игрок еще не нашел


        packages.forEach((package, packageId) => {
            if (player.packagesCollected.indexOf(packageId) == -1){
                package.id = packageId // обязательно добавляем к координатам порядковый id пакета в массиве packages, т. к. в playerPackages порядок координат будет другой
                playerPackages.push(package);
            }
        });


        player.call('playerInitSecretPackages', [playerPackages]);


    });
});

Мелочь, а целая строчка
JavaScript:
packages.forEach((package, packageId) => {
    if (player.packagesCollected.indexOf(packageId) == -1) {
        playerPackages.push({ id: packageId, ...package });
    }
});

JavaScript:
var fs = require('fs'); // подключим модуль для работы с файлами

Будем просто модными и не будем выделяться
JavaScript:
const fs = require('fs'); // подключим модуль для работы с файлами
 

Lev Angel

Developer
Команда форума
Скриптер
Сообщения
795
Спасибо за code review :)

Не совсем понятно, зачем выносить в отдельную функцию
Здесь наверное не очень удачное название для этой функции выбрал. Нужно было что-то вроде rewardPlayer.
Длинные функции которые много чего делают - это плохо. Да и человеку который будет ковыряться в коде на мой взгляд так будет более понятно.
Иногда я даже простую проверку выношу в отдельную функцию. Название функции показывает что делает этот код или что мы проверяем. Это делает код более читаемым и выразительным.
 

Dihan48

Middle Developer
Скриптер
Сообщения
61
Не совсем понятно, зачем выносить в отдельную функцию ;o
По сути это аналогично комментарию
Название функции показывает что делает этот код или что мы проверяем. Это делает код более читаемым и выразительным.
Хотя 2 строчки в отдельную функцию, которая ни где больше не используется, действительно не стоит выносить
Ну и приятный на глаз:
JavaScript:
mp.events.add('playerSecretPackageInfo', (totalPackages, collectedPackages)=> {
mp.events.call(
"ShowShardMessage",
collectedPackages == totalPackages
? `Вы нашли все Секретные пакеты`
: `Секретный пакет найден`,
`Найдено пакетов: ${collectedPackages}/${totalPackages}
`);
});
Кому как, мне больше например такой вариант приятнее на глаз:
JavaScript:
mp.events.add('playerSecretPackageInfo', (totalPackages, collectedPackages) => {
    const title = collectedPackages == totalPackages ? `Вы нашли все Секретные пакеты` : `Секретный пакет найден`;
    const msg = `Найдено пакетов: ${collectedPackages}/${totalPackages} `;
    mp.events.call("ShowShardMessage", title, msg);
});
 

geneff

Middle Developer
Скриптер
Сообщения
58
Мелочь, а целая строчка
JavaScript:
packages.forEach((package, packageId) => {
    if (player.packagesCollected.indexOf(packageId) == -1) {
        playerPackages.push({ id: packageId, ...package });
    }
});
Обьясните, пожалуйста, для вы тут используете спреад для package!?
 

Dihan48

Middle Developer
Скриптер
Сообщения
61
Обьясните, пожалуйста, для вы тут используете спреад для package!?
чтобы каждый отправленный на клиент объект выглядел вот таким образом
JavaScript:
{ id: 0, x: -422.77093505859375, y: 1166.211669921875, z: 325.973876953125 }
из-за того что игрок мог собрать до этого какие-то пакеты то массив для инициализации может быть меньше и чтобы не сравнивать потом по координатам "собранные" пакеты проще указать id.
id будет соответствовать индексу в исходном массиве всех пакетов packages
в последствии клиент будет отправлять id "собранного" пакета на сервер и сервер сразу поймет какой именно пакет был собран из этого списка packages
 

Edwards

Junior Developer
Скриптер
Сообщения
35
Доброго времени, коллега)
Я думаю если сделать файл - допустим dataPackages.Json
JavaScript:
{
 
[
   {
      "name": "package1",
      "position": {
         "x": 00000,
         "y": 0000,
         "z": 000
      },
      "width": 10,
      "height": 10,
      "dimension": 0
   },
   {
      "name": "package2",
      "position": {
         "x": 00000,
         "y": 0000,
         "z": 000
      },
      "width": 10,
      "height": 10,
      "dimension": 0
   },
]
   ....
дальше на сервере подключить:
const dataPackages = require( '../dataPackages .json' );
сделать цикл перебора, что бы не плодить миллион строк кода и выполнить при подключении игрока
JavaScript:
dataPackages.forEach( element =>
        {
            createPackages( element );//вызываем функцию и передаем element перебора
        } )

ну и собственно сам код

JavaScript:
createPackages( element )
 {
let package;                          //вот тут он берет значения с .json файла
package = mp.colshapes.newRectangle( element.position, element.width, element.height, element.dimension );
package.playerEnter((player, package)=>
{
  console.log(`${player.name} entered the colshape`);
 mp.events.add("playerEnter-package", playerEnterColshapeHandler);
  });
}
ну а дальше само собой объявить евент на клиенте и тд...
работоспособность не проверял, писал прямо в теме) но +- должно
 

Lev Angel

Developer
Команда форума
Скриптер
Сообщения
795
Как-то мысль оборвалась))
Я предпочитаю то что не критично утаскивать на клиент, чтобы лишний раз не напрягать сервак.
В данном случае width, height и dimension не меняются, поэтому можно их и не хранить в файле.
 

Edwards

Junior Developer
Скриптер
Сообщения
35
Как-то мысль оборвалась))
Я предпочитаю то что не критично утаскивать на клиент, чтобы лишний раз не напрягать сервак.
В данном случае width, height и dimension не меняются, поэтому можно их и не хранить в файле.
Чем больше значений ты хранишь на клиенте, тем больше вероятность, что их поменяет игрок(читер, хацкер и тд). Если какие-либо вычислительные процессы ставить на клиент, то нужно будет все равно проверять их сервером, так что проще все закинуть на сервер и результат отправлять на клиент.
 

Lev Angel

Developer
Команда форума
Скриптер
Сообщения
795
Чем больше значений ты хранишь на клиенте, тем больше вероятность, что их поменяет игрок(читер, хацкер и тд). Если какие-либо вычислительные процессы ставить на клиент, то нужно будет все равно проверять их сервером, так что проще все закинуть на сервер и результат отправлять на клиент.
Хз, тут конечно спорный вопрос. В этом случае я решил, что это не супер критичные данные. Сервер все-таки не резиновый :)
 
Верх