Введение в скриптинг GUI: Difference between revisions

From Multi Theft Auto: Wiki
Jump to navigation Jump to search
mNo edit summary
 
(9 intermediate revisions by 5 users not shown)
Line 1: Line 1:
{{wrongtitle | Введение в скриптинг GUI}}
<!-- place holder -->
<!-- place holder -->
Одна из важных особенностей MTA:DM - возможность создания собственного GUI (графический пользовательский интерфейс). Графический интерфейс состоит из окон, кнопок, текстовых полей... Проще говоря, из всех стандартных компонент графических интерфейсов. Они могут использоваться пока пользователь в игре, и использоваться для ввода/вывода вместо команд.  
Одной из важных особенностей MTA:SA является возможность программирования настраиваемого GUI (Graphic User Interface, графического интерфейса пользователя). GUI состоит из окон, кнопок, редактируемых полей, флажков... Практически всех стандартных компонентов для заполнения форм в графических средах. Они могут отображаться пока пользователь в игре и используются для ввода и вывода вместо привычных команд чата.  


[[Image:AdminGUI.png|thumb|Admin Console GUI]]
[[Image:AdminGUI.png|thumb|GUI Admin Console]]


==Руководство по созданию интерфейса авторизации==
==Руководство по созданию окна авторизации==
В этом руководстве мы сделаем простое окно авторизаци с двумя полями ввода и кнопкой. Окно появляется, когда игрок подключается к игре, и после того, как нажата кнопка, игрок респаунится. Это руководство - продолжение предыдущего ([[RU/Scripting Introduction|Введение в скриптинг]]). Теперь мы познакомимся с написанием клиентских скриптов.  
В этом руководстве мы сделаем простое окно запроса логина с двумя полями для ввода и одной кнопкой. Окно будет появляться при подключении игрока к серверу, а по нажатии на кнопку он заспавнится. Руководство продолжит мод, созданный нами во [[Введение в скриптинг|введении в скриптинг]] ''(Если вы пользовались [[Введение в скриптинг|введением в скриптинг]], вам понадобится убрать или закомментировать в коде строчку со [[spawnPlayer]] в функции "joinHandler", так как в данном руководстве мы заменим ее альтернативой с gui)''. Мы также ознакомимся со скриптингом на клиентской стороне.  


===Отрисовка окна===
===Отрисовка окна===
GUI работает на стороне клиента. Хорошим решением будет поместить все клиентские скрипты в отдельный каталог. Перейдите в каталог /Ваш сервер MTA/mods/deathmatch/resources/myserver/ и создайте подкаталог "client". В нём создайте текстовый файл и назовите его "gui.lua". В этом файле мы напишем функцию, отображающую окно:
Все GUI обязательно делаются на клиентской стороне. Это также может являться хорошей практикой хранения всех клиентских скриптов в отдельной папке.  
 
Переместитесь в директорию /ваш MTA Server/mods/deathmatch/resources/myserver/ и создайте папку с названием "client". В директории /client/ создайте текстовый файл и назовите его "gui.lua".  
 
В этом файле мы напишем функцию, которая будет отрисовывать окно. Для создания окна мы воспользуемся [[guiCreateWindow]]:
<syntaxhighlight lang="lua">
<syntaxhighlight lang="lua">
function CreateLoginWindow()
function createLoginWindow()
-- определяем позиции окна по осям X и Y
local X = 0.375
local X = 0.375
local Y = 0.375
local Y = 0.375
-- объявляем ширину и высоту окна
local Width = 0.25
local Width = 0.25
local Height = 0.25
local Height = 0.25
wdwLogin = guiCreateWindow(X, Y, Width, Height, "Please Log In", true)
-- создаем окно и сохраняем значение его элемента в переменной 'wdwLogin'
-- кликаем по имени функции, чтобы прочитать ее документацию
wdwLogin = guiCreateWindow(X, Y, Width, Height, "Пожалуйста, залогиньтесь", true)
end
end
</syntaxhighlight>
</syntaxhighlight>
Вы можете кликнуть по имени функции, чтобы прочитать её описание. Заметьте, что координаты окна задаются в ''процентах'' от размеров экрана. Это значит, что левая граница экрана по ширине принимается за 0, а правая за 1, соответственно, "X" равное 0.5 обозначает середину экрана. Аналогично и для позиции по высоте, ширины и высоты окна (если "width" равно 0.5, окно будет в половину ширины экрана). Теперь мы добавим текстовые меткм (с надписями "username:" и "password:"), поля ввода и кнопку. Замените функцию её полной версией:
 
===Относительные и абсолютные===
Заметьте, что последний аргумент, передаваемый guiCreateWindow в образце выше, ''true''. Это говорит о том, что координаты и габариты окна - '''относительны''', значит, измеряются в ''процентах'' от всего размера экрана. То есть если верхний левый угол экрана - это 0, а верхний правый - это 1, то позиция 0.5 по оси X является центром экрана по горизонтали. Аналогично этому, если вершина экрана - это 0, а низ - это 1, то позиция 0.2 по оси Y будет отступом на 20% всей высоты от верхней границы экрана. Те же принципы применимы к ширине и высоте (при Width, равной 0.5, окно будет шириной с пол-экрана).
 
Альтернативно использованию относительных значений, можно использовать '''абсолютные''' (указав ''false'' вместо true в guiCreateWindow). Абсолютные значения вычисляются как конкретное количество пикселей от верхнего левого угла родительского элемента (если родительским не указан никакой gui-элемент, то родителем является экран сам по себе). Если разрешением экрана является, допустим, 1920x1200, то верхняя левая сторона экрана будет 0 пикселями, а верхняя правая - 1920 пикселями, позиция 960 по оси X же будет являться центром экрана по горизонтали. Аналогично этому, если вершина экрана - это 0 пикселей, а низ - 1200, то позиция 20 по оси Y будет отступом на 20 пикселей от верхней границы экрана. Те же принципы применимы к ширине и высоте (при Width, равной 50, окно будет 50 пикселей в ширину). ''Вы можете пользоваться [[guiGetScreenSize]] и путем нехитрых вычислений получать нужные вам абсолютные значения.''
 
Разница между использованием относительных и абсолютных значений очевидна: gui, созданное с использованием абсолютных значений, всегда будет появляться с точь-в-точь одинаковыми габаритами и позицией, в то время как gui, созданное с использованием относительных значений, всегда будет связано процентным соотношением с размерами своего родителя.
 
Абсолютные значения в целом легче поддерживать при ручной правке кода, но выбор типа используемых значений варьируется от ситуации к ситуации.
 
Для разрешения целей из данного руководства мы будет пользоваться относительными значениями.
 
===Добавление компонентов===
Теперь мы добавим текстовые метки (называющиеся "логин:" и "пароль:"), редактируемые поля (для ввода информации) и кнопку для непосредственного залогинивания.
 
Для создания кнопок воспользуемся [[guiCreateButton]], а для создания редактируемых полей - [[guiCreateEdit]]:
 
'''Примите во внимание, что сейчас мы просто дописываем уже существующую функцию 'createLoginWindow'. Функция ниже не является новой, а предназначена заменить то, что мы уже имеем.'''
<syntaxhighlight lang="lua">
<syntaxhighlight lang="lua">
function CreateLoginWindow()
function createLoginWindow()
local X = 0.375
local X = 0.375
local Y = 0.375
local Y = 0.375
local Width = 0.25
local Width = 0.25
local Height = 0.25
local Height = 0.25
wdwLogin = guiCreateWindow(X, Y, Width, Height, "Please Log In", true)
wdwLogin = guiCreateWindow(X, Y, Width, Height, "Пожалуйста, залогиньтесь", true)
-- объявляем новые позиции для первой метки по осям X и Y
X = 0.0825
X = 0.0825
Y = 0.2
Y = 0.2
-- объявляем новые значения ширины и высоты для первой метки
Width = 0.25
Width = 0.25
Height = 0.25
Height = 0.25
guiCreateLabel(X, Y, Width, Height, "Username", true, wdwLogin)
-- создаем первую метку, заметьте, что последний указанный аргумент - 'wdwLogin', значит окно,
-- созданное выше, является родителем этой метки (так что значения ее позиции и габариты теперь связаны с позицией того окна)
guiCreateLabel(X, Y, Width, Height, "Имя", true, wdwLogin)
-- изменяем позицию по оси Y, чтобы вторая метка была чуть ниже первой
Y = 0.5
Y = 0.5
guiCreateLabel(X, Y, Width, Height, "Password", true, wdwLogin)
guiCreateLabel(X, Y, Width, Height, "Пароль", true, wdwLogin)
X = 0.415
X = 0.415
Y = 0.2
Y = 0.2
Line 43: Line 73:
Y = 0.5
Y = 0.5
edtPass = guiCreateEdit(X, Y, Width, Height, "", true, wdwLogin)
edtPass = guiCreateEdit(X, Y, Width, Height, "", true, wdwLogin)
-- для логина и пароля устанавливаем ограничение на максимальное количество введенных символов 50
guiEditSetMaxLength(edtUser, 50)
guiEditSetMaxLength(edtUser, 50)
guiEditSetMaxLength(edtPass, 50)
guiEditSetMaxLength(edtPass, 50)
Line 50: Line 81:
Width = 0.25
Width = 0.25
Height = 0.2
Height = 0.2
btnLogin = guiCreateButton(X, Y, Width, Height, "Log In", true, wdwLogin)
btnLogin = guiCreateButton(X, Y, Width, Height, "Логин", true, wdwLogin)
-- делаем окно невидимым
guiSetVisible(wdwLogin, false)
guiSetVisible(wdwLogin, false)
end
end
</syntaxhighlight>
</syntaxhighlight>
Обратите внимание на то, что каждый компонент интерфейса является дочерним по отношению к окну, это достигается указанием родительского элемента (wdwLogin, в данном случае) при создании элемента:
Заметьте, что каждый созданный компонент GUI является дочерним по отношению к окну, это сделано благодаря указанию родительского элемента (в данном случае, wdwLogin) при создании компонента.
 
Это очень полезно не только потому что все компоненты прикреплены к окну и будут двигаться вместе с ним, но и так как любые изменения с "родительским" окном будут применены дальше вниз по дереву к этим дочерним компонентам. Например, мы теперь можем спрятать весь только что созданный GUI просто спрятав окно:
<syntaxhighlight lang="lua">
<syntaxhighlight lang="lua">
guiCreateLabel(X, Y, Width, Height, "Password", true, wdwLogin)
guiSetVisible(wdwLogin, false) --прячет весь сделанный нами GUI, чтобы мы могли показать его игроку в соответствующий момент.  
</syntaxhighlight>
Это очень удобно, т.к. в дальнейшем, при отображении окна, можно обращаться только к родительскому элементу. К примеру:
<syntaxhighlight lang="lua">
guiSetVisible(wdwLogin, false) --прячет всё окно целиком так, что мы можем показать его игроку в любой момент.  
</syntaxhighlight>
</syntaxhighlight>
Для редактироования GUI вы также можете воспользоваться [[RU/Resource:GUI_Editor|GUI редактором]].


===Использование написаной нами функции===
===Использование написанной функции===
Функция CreateLoginWindow написана, но она не будет работать, пока мы её не вызовем. Рекомендуется создавать все окна при запуске ресурса на клиенте, прятать их, и показывать игроку позднее, когда они понадобятся. Для этого напишем обработчик события "[[onClientResourceStart]]", в котором будем создавать окно:
Теперь функция createLoginWindow доделана, но пока мы ее не вызовем, она сама по себе ничего не будет делать. Рекомендуется создавать весь GUI при старте клиентского ресурса, прятать его, а затем показывать игроку, когда понадобится. Следовательно, для создания окна мы напишем обработчик события "[[onClientResourceStart]]":
<syntaxhighlight lang="lua">
<syntaxhighlight lang="lua">
-- прикрепляем обработчик события в корневому (root) элементу ресурса
-- это значит, что он сработает только при старте этого ресурса
addEventHandler("onClientResourceStart", getResourceRootElement(getThisResource()),  
addEventHandler("onClientResourceStart", getResourceRootElement(getThisResource()),  
function ()
function ()
CreateLoginWindow()
createLoginWindow()
end
end
)
)
</syntaxhighlight>
</syntaxhighlight>
Мы хотим показывать окно когда клиент подключается к игре, используя то же событие "[[onClientResourceStart]]". Теперь обработчик выглядит так:
 
Так как это - окно входа пользователя, теперь нам надо показывать его при подключении игроков к серверу.
Это можно сделать, используя все то же событие, "[[onClientResourceStart]]", мы можем изменить код выше, включив показ окна:
 
'''Заметьте, что сейчас мы допишем код для уже существующего обработчика 'onClientResourceStart'. Это не новый обработчик события, код предназначен заменить то, что мы уже имеем.'''
<syntaxhighlight lang="lua">
<syntaxhighlight lang="lua">
addEventHandler("onClientResourceStart", getResourceRootElement(getThisResource()),  
addEventHandler("onClientResourceStart", getResourceRootElement(getThisResource()),  
function ()
function ()
CreateLoginWindow()
-- создаем окно входа и его компоненты
createLoginWindow()


                 outputChatBox("Welcome to My MTA DM Server, please log in. ")
-- выводим игроку краткое приветствие
                 outputChatBox("Добро пожаловать на мой MTA:SA сервер, пожалуйста, залогиньтесь.")


-- если GUI был создан успешно, показать его игроку
        if (wdwLogin ~= nil) then
        if (wdwLogin ~= nil) then
        guiSetVisible(wdwLogin, true)
guiSetVisible(wdwLogin, true)
else
-- если GUI не был создан успешно, сообщаем игроку об этом
outputChatBox("Возникла непредвиденная ошибка и GUI входа не был создан.")
        end  
        end  


-- активируем курсор игрока (чтобы он мог выбирать компонентф и кликать по ним)
        showCursor(true)
        showCursor(true)
-- отдаем контроль над клавиатурой GUI, позволяя игрокам (например) нажимать 'T', не открывая при этом одновременно чат
        guiSetInputEnabled(true)
        guiSetInputEnabled(true)
end
end
)
)
</syntaxhighlight>
</syntaxhighlight>
Заметьте, что мы выполняем проверку перед показом окна, так, в случае, если окно не создано, не возникнет ошибки. Функция [[showCursor]] включает управление мышью, а [[guiSetInputEnabled]] позволяет быть уверенным, что использование при вводе некоторых клавиш, таких как "A", "S", "D", "W", "T", не приведёт к движению персонажа, или вводу текста в чате. На следующем шаге мы заставим кнопку работать так, как задумывалось.


===Scripting the button===
Заметьте, что перед тем, как сделать окно видимым, имеет место простая проверка, так что даже при маловероятном случае, когда окно не будет создано в силу того, что wdwLogin - недействительный элемент, мы не получим ошибку и проинформируем игрока о произошедшем.
When the player clicks on any part of the GUI, the event "[[onClientGUIClick]]" will be triggered for the GUI component you clicked on. For instance, if you click on the button, you can add the event handler attached to the button:
Следующий шаг -  создание кнопочного функционала для кнопки логина.
 
==Программирование кнопки==
Теперь мы создали GUI и явили его игроку, сейчас же надо сделать его рабочим.
 
===Обнаружение клика===
Когда игрок кликает по какому-либо компоненту GUI, по отношении к этому компоненту срабатывает событие "[[onClientGUIClick]]". Это позволяет нам с легкостью отслеживать различные клики по тем элементам GUI, которые мы желаем использовать.
Например, мы можем прикрепить событие ко кнопке btnLogin для отслеживания кликов по ней:
<syntaxhighlight lang="lua">
<syntaxhighlight lang="lua">
addEventHandler("onClientGUIClick", theButtonElement, theHandlerFunction, false)
-- прикрепляем событие onClientGUIClick к btnLogin и с него переключение на функцию 'clientSubmitLogin'
addEventHandler("onClientGUIClick", btnLogin, clientSubmitLogin, false)
</syntaxhighlight>
</syntaxhighlight>
In our script, we only need the event handler attached to the button. When it's clicked, the client will tell the server to spawn the player. Find the event handler code for "onClientResourceStart" from the previous section, and add the following line right AFTER CreateLoginWindow() is called:
'''Заметьте, что последний аргумент - "false". Это говорит о том, что срабатывание события произойдет только именно от btnLogin, но не в результате его распространения вверх или вниз по дереву. Использование "true" при прикреплении к элементам gui повлечет за собой срабатывание события при клике по любому из элементов одной ветви.'''
 
Теперь эта строка кода может быть добавлена внутрь функции createLoginWindow. Распространенной ошибкой является пробовать и прикреплять события к несуществующим элементам GUI, так что всегда проверяйте, прикрепили ли вы свои события уже '''после''' создания элемента gui (в данном случае, кнопки):
 
'''Обратите внимание, что сейчас мы допишем код для уже существующей функции 'createLoginWindow'.'''
<syntaxhighlight lang="lua">
<syntaxhighlight lang="lua">
addEventHandler("onClientGUIClick", btnLogin, clientSubmitLogin, false)
function createLoginWindow()
-- создаем все наши элементы GUI
...
 
-- теперь добавим событие onClientGUIClick к свежесозданной кнопке
addEventHandler("onClientGUIClick", btnLogin, clientSubmitLogin, false)
</syntaxhighlight>
</syntaxhighlight>
The event handler must be added here because it makes sure variable btnLogin contains the actual button. You cannot attach an event to an non-existant element. You should have noticed that we need the "clientSubmitLogin" function, as stated in the line above.
 
===Управление кликом===
Теперь мы можем обнаружить клик игрока по кнопке, и нам нужно написать код, отвечающий за происходящее после него.
В нашем обработчике события [[onClientGUIClick]] мы распорядились переключаться на функцию clientSubmitLogin при клике по btnLogin.
Следовательно, теперь мы можем воспользоваться функцией clientSubmitLogin для контроля над происходящим после клика по кнопке:
<syntaxhighlight lang="lua">
<syntaxhighlight lang="lua">
function clientSubmitLogin(button)
-- создаем функцию и определяем параметры 'button' и 'state'
if button == "left" then
-- (которые автоматически передаются событием onClientGUIClick)
function clientSubmitLogin(button,state)
triggerServerEvent("SubmitLogin", getRootElement(), guiGetText(edtUser), guiGetText(edtPass))
-- если клик по кнопке был произведен левой кнопкой мыши, и положение кнопки мыши - "вверху" (отпущена)
if button == "left" and state == "up" then
-- отдаем контроль над клавиатурой обратно игре (позволяя игрокам передвигаться, писать в чат и т.д.)
guiSetInputEnabled(false)
guiSetInputEnabled(false)
-- прячем окно и все его компоненты
guiSetVisible(wdwLogin, false)
guiSetVisible(wdwLogin, false)
-- прячем курсор мыши
showCursor(false)
showCursor(false)
end
end
end
end
</syntaxhighlight>
</syntaxhighlight>
The variable "button" is passed by the event handler, and it's a string with the name of the button (either "left" or "right"). Here we also hit a new concept, which is custom events. Custom events can be triggered by script from the same side (server to server, or client to client), or from the other side (server to client, or vice versa). Here we use the [[triggerServerEvent]] function to trigger a custom event on the server, "SubmitLogin".
Теперь, когда по кнопке кликнули, окно будет спрятано и все управление будет передано обратно в руки игрока. Затем, мы скажем серверу, что игрокам можно спавниться.
 
===Вызов сервера===
Вызов сервера производится с помощью [[triggerServerEvent]]. Это позволяет вам вызывать указанное серверное событие из клиента. С помощью [[triggerClientEvent]] можно сделать и наоборот.
В данном случае мы будем использовать функцию [[triggerServerEvent]] для вызова созданного нами сами серверного события, названного "submitLogin", которое затем проконтроллирует респавн игрока на серверной стороне.
 
'''Примите во внимание, что сейчас мы просто дописываем код для уже существующей функции 'clientSubmitLogin'. Функция ниже не является новой, а предназначена заменить то, что мы уже имеем.'''  
<syntaxhighlight lang="lua">
function clientSubmitLogin(button,state)
if button == "left" and state == "up" then
-- получаем текст, введенный в поле 'username'
local username = guiGetText(edtUser)
-- получаем текст, введенный в поле 'password'
local password = guiGetText(edtPass)
 
-- если и username, и password существуют
if username ~= "" and password ~= "" then
-- вызвать серверное событие 'submitLogin', передав ему username и password
triggerServerEvent("submitLogin", getRootElement(), username, password)
 
-- прячем gui, курсор и возвращаем управление игроку
guiSetInputEnabled(false)
guiSetVisible(wdwLogin, false)
showCursor(false)
else
-- иначе выводим игроку сообщение, но не отсылаем информацию на сервер
-- и не прячем gui
outputChatBox("Пожалуйста, введите логин и пароль.")
end
end
end
</syntaxhighlight>


At this point we have all the code needed on the client side. On the server side, recall that we are spawning the player as soon as they join, as shown below:
===Создание серверного события===
На данный момент мы имеем весь нужный код для клиентской стороны, так что откройте ваш серверный файл 'script.lua' (из [[Введение в скриптинг|введения в скриптинг]]), либо любой другой подходящий для работы серверный файл.
 
На стороне сервера надо сделать так, чтобы игрок спавнился, как только залогинится.
Итак, для начала нам понадобится объявить свое событие, которым мы ранее пользовались на клиенте. Это можно сделать с помощью связки [[addEvent]] и [[addEventHandler]].
<syntaxhighlight lang="lua">
<syntaxhighlight lang="lua">
function joinHandler()
-- создаем функцию loginHandler с параметрами username и password (переданные от gui на клиенте)
local x,y,z
function loginHandler(username,password)
x = 1959.55
 
y = -1714.46
z = 10
spawnPlayer(source, x, y, z)
fadeCamera(source, true)
outputChatBox("Welcome to My Server", source)
end
end
addEventHandler("onPlayerJoin", getRootElement(), joinHandler)
 
-- объявляем свое событие и позволяем ему быть вызванным из клиента ('true')
addEvent("submitLogin",true)
-- добавляем обработчик события, чтобы при вызове submitLogin происходило переключение на функцию loginHandler
addEventHandler("submitLogin",root,loginHandler)
</syntaxhighlight>
</syntaxhighlight>
However, since we are going to spawn the player when the button is clicked, we must replace the "onPlayerJoin" event with the custom event triggered by the client script. Replace the above code with the following:
 
===Залогинивание===
Теперь у нас есть функция, которая вызывается через наше собственное событие 'submitLogin', теперь можно начать работать над процессом залогинивания и респавна игрока, задействовав функцию 'loginHandler':
<syntaxhighlight lang="lua">
<syntaxhighlight lang="lua">
function joinHandler(username, password)
function loginHandler(username,password)
local x,y,z
-- проверяем username и password на правильность
x = 1959.55
if username == "user" and password == "apple" then
y = -1714.46
-- игрок успешно залогинился, так что спавним его
z = 10
if (client) then
        if (client) then
spawnPlayer(client, 1959.55, -1714.46, 10)
      spawnPlayer(client, x, y, z)
fadeCamera(client, true)
      fadeCamera(client, true)
                        setCameraTarget(client, client)
      outputChatBox("Welcome to My Server", client)
outputChatBox("Добро пожаловать на мой сервер.", client)
         end
end
else
-- если username или password неправильны, выводим игроку соответствующее сообщение
outputChatBox("Неправильные логин и пароль. Пожалуйста, переподсоединитесь и попробуйте еще раз.",client)
         end
end
end


addEvent("SubmitLogin", true)
addEvent("submitLogin",true)
addEventHandler("SubmitLogin", getRootElement(), joinHandler)
addEventHandler("submitLogin",root,loginHandler)
</syntaxhighlight>
</syntaxhighlight>
Note that the second arguement of [[addEvent]] function (the one with value "true") specifies whether the event can be triggered by the other side, therefore we need to turn it on since the client is triggering it. Also note the variable "client", it's an internal variable used by MTA to identify the player who triggered the event.
'''Для целей данного руководства была использована очень простая система хранения логинов/паролей. Более вменяемыми альтернативами являются встроенная система аккаунтов и база данных MySQL.'''


Finally, do not forget to include the new gui.lua file in the meta.xml of the main resource, and label it as a client script:
Также заметьте, что была задействована переменная "client", это - внутренняя перменная, используемая MTA для идентификации игрока, вызвавшего срабатывание события.
 
 
Наконец, не забудьте включить новый файл gui.lua в meta.xml главного ресурса и отметить его как клиентский скрипт:
<syntaxhighlight lang="xml">
<syntaxhighlight lang="xml">
<script src="client/gui.lua" type="client" />
<script src="client/gui.lua" type="client" />
</syntaxhighlight>
</syntaxhighlight>


As to this point, we have a basic login window that spawns the player when the "login" button is clicked. You can also use the username and password submitted by the [[triggerServerEvent]] function to verify the player's identify before spawning him.


Теперь у нас есть базовое окно авторизации, проверяющее логин и пароль игрока по нажатии на кнопку залогинивания. Если они верны, игрок автоматически заспавнится.
Для дальнейшей помощи по GUI, посетите [[:Category:GUI_Tutorials|страницу с руководствами по GUI]].
[[Category:GUI_Tutorials]]
[[en:Introduction to Scripting the GUI]]
[[es:Introducción a la Programación de GUI]]
[[fr:Introduction GUI]]
[[it:Introduzione_allo_scripting_della_GUI]]
[[it:Introduzione_allo_scripting_della_GUI]]
[[pt-br:Introducao ao GUI scripting]]

Latest revision as of 09:38, 26 April 2020

Одной из важных особенностей MTA:SA является возможность программирования настраиваемого GUI (Graphic User Interface, графического интерфейса пользователя). GUI состоит из окон, кнопок, редактируемых полей, флажков... Практически всех стандартных компонентов для заполнения форм в графических средах. Они могут отображаться пока пользователь в игре и используются для ввода и вывода вместо привычных команд чата.

GUI Admin Console

Руководство по созданию окна авторизации

В этом руководстве мы сделаем простое окно запроса логина с двумя полями для ввода и одной кнопкой. Окно будет появляться при подключении игрока к серверу, а по нажатии на кнопку он заспавнится. Руководство продолжит мод, созданный нами во введении в скриптинг (Если вы пользовались введением в скриптинг, вам понадобится убрать или закомментировать в коде строчку со spawnPlayer в функции "joinHandler", так как в данном руководстве мы заменим ее альтернативой с gui). Мы также ознакомимся со скриптингом на клиентской стороне.

Отрисовка окна

Все GUI обязательно делаются на клиентской стороне. Это также может являться хорошей практикой хранения всех клиентских скриптов в отдельной папке.

Переместитесь в директорию /ваш MTA Server/mods/deathmatch/resources/myserver/ и создайте папку с названием "client". В директории /client/ создайте текстовый файл и назовите его "gui.lua".

В этом файле мы напишем функцию, которая будет отрисовывать окно. Для создания окна мы воспользуемся guiCreateWindow:

function createLoginWindow()
	-- определяем позиции окна по осям X и Y
	local X = 0.375
	local Y = 0.375
	-- объявляем ширину и высоту окна
	local Width = 0.25
	local Height = 0.25
	-- создаем окно и сохраняем значение его элемента в переменной 'wdwLogin'
	-- кликаем по имени функции, чтобы прочитать ее документацию
	wdwLogin = guiCreateWindow(X, Y, Width, Height, "Пожалуйста, залогиньтесь", true)
end

Относительные и абсолютные

Заметьте, что последний аргумент, передаваемый guiCreateWindow в образце выше, true. Это говорит о том, что координаты и габариты окна - относительны, значит, измеряются в процентах от всего размера экрана. То есть если верхний левый угол экрана - это 0, а верхний правый - это 1, то позиция 0.5 по оси X является центром экрана по горизонтали. Аналогично этому, если вершина экрана - это 0, а низ - это 1, то позиция 0.2 по оси Y будет отступом на 20% всей высоты от верхней границы экрана. Те же принципы применимы к ширине и высоте (при Width, равной 0.5, окно будет шириной с пол-экрана).

Альтернативно использованию относительных значений, можно использовать абсолютные (указав false вместо true в guiCreateWindow). Абсолютные значения вычисляются как конкретное количество пикселей от верхнего левого угла родительского элемента (если родительским не указан никакой gui-элемент, то родителем является экран сам по себе). Если разрешением экрана является, допустим, 1920x1200, то верхняя левая сторона экрана будет 0 пикселями, а верхняя правая - 1920 пикселями, позиция 960 по оси X же будет являться центром экрана по горизонтали. Аналогично этому, если вершина экрана - это 0 пикселей, а низ - 1200, то позиция 20 по оси Y будет отступом на 20 пикселей от верхней границы экрана. Те же принципы применимы к ширине и высоте (при Width, равной 50, окно будет 50 пикселей в ширину). Вы можете пользоваться guiGetScreenSize и путем нехитрых вычислений получать нужные вам абсолютные значения.

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

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

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

Добавление компонентов

Теперь мы добавим текстовые метки (называющиеся "логин:" и "пароль:"), редактируемые поля (для ввода информации) и кнопку для непосредственного залогинивания.

Для создания кнопок воспользуемся guiCreateButton, а для создания редактируемых полей - guiCreateEdit:

Примите во внимание, что сейчас мы просто дописываем уже существующую функцию 'createLoginWindow'. Функция ниже не является новой, а предназначена заменить то, что мы уже имеем.

function createLoginWindow()
	local X = 0.375
	local Y = 0.375
	local Width = 0.25
	local Height = 0.25
	wdwLogin = guiCreateWindow(X, Y, Width, Height, "Пожалуйста, залогиньтесь", true)
	
	-- объявляем новые позиции для первой метки по осям X и Y
	X = 0.0825
	Y = 0.2
	-- объявляем новые значения ширины и высоты для первой метки
	Width = 0.25
	Height = 0.25
	-- создаем первую метку, заметьте, что последний указанный аргумент - 'wdwLogin', значит окно,
	-- созданное выше, является родителем этой метки (так что значения ее позиции и габариты теперь связаны с позицией того окна)
	guiCreateLabel(X, Y, Width, Height, "Имя", true, wdwLogin)
	-- изменяем позицию по оси Y, чтобы вторая метка была чуть ниже первой
	Y = 0.5
	guiCreateLabel(X, Y, Width, Height, "Пароль", true, wdwLogin)
	

	X = 0.415
	Y = 0.2
	Width = 0.5
	Height = 0.15
	edtUser = guiCreateEdit(X, Y, Width, Height, "", true, wdwLogin)
	Y = 0.5
	edtPass = guiCreateEdit(X, Y, Width, Height, "", true, wdwLogin)
	-- для логина и пароля устанавливаем ограничение на максимальное количество введенных символов 50
	guiEditSetMaxLength(edtUser, 50)
	guiEditSetMaxLength(edtPass, 50)
	
	X = 0.415
	Y = 0.7
	Width = 0.25
	Height = 0.2
	btnLogin = guiCreateButton(X, Y, Width, Height, "Логин", true, wdwLogin)
	
	-- делаем окно невидимым
	guiSetVisible(wdwLogin, false)
end

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

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

guiSetVisible(wdwLogin, false) --прячет весь сделанный нами GUI, чтобы мы могли показать его игроку в соответствующий момент. 

Для редактироования GUI вы также можете воспользоваться GUI редактором.

Использование написанной функции

Теперь функция createLoginWindow доделана, но пока мы ее не вызовем, она сама по себе ничего не будет делать. Рекомендуется создавать весь GUI при старте клиентского ресурса, прятать его, а затем показывать игроку, когда понадобится. Следовательно, для создания окна мы напишем обработчик события "onClientResourceStart":

-- прикрепляем обработчик события в корневому (root) элементу ресурса
-- это значит, что он сработает только при старте этого ресурса
addEventHandler("onClientResourceStart", getResourceRootElement(getThisResource()), 
	function ()
		createLoginWindow()
	end
)	

Так как это - окно входа пользователя, теперь нам надо показывать его при подключении игроков к серверу. Это можно сделать, используя все то же событие, "onClientResourceStart", мы можем изменить код выше, включив показ окна:

Заметьте, что сейчас мы допишем код для уже существующего обработчика 'onClientResourceStart'. Это не новый обработчик события, код предназначен заменить то, что мы уже имеем.

addEventHandler("onClientResourceStart", getResourceRootElement(getThisResource()), 
	function ()
		-- создаем окно входа и его компоненты
		createLoginWindow()

		-- выводим игроку краткое приветствие
                outputChatBox("Добро пожаловать на мой MTA:SA сервер, пожалуйста, залогиньтесь.")

		-- если GUI был создан успешно, показать его игроку
	        if (wdwLogin ~= nil) then
			guiSetVisible(wdwLogin, true)
		else
			-- если GUI не был создан успешно, сообщаем игроку об этом
			outputChatBox("Возникла непредвиденная ошибка и GUI входа не был создан.")
	        end 

		-- активируем курсор игрока (чтобы он мог выбирать компонентф и кликать по ним)
	        showCursor(true)
		-- отдаем контроль над клавиатурой GUI, позволяя игрокам (например) нажимать 'T', не открывая при этом одновременно чат
	        guiSetInputEnabled(true)
	end
)	

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

Программирование кнопки

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

Обнаружение клика

Когда игрок кликает по какому-либо компоненту GUI, по отношении к этому компоненту срабатывает событие "onClientGUIClick". Это позволяет нам с легкостью отслеживать различные клики по тем элементам GUI, которые мы желаем использовать. Например, мы можем прикрепить событие ко кнопке btnLogin для отслеживания кликов по ней:

-- прикрепляем событие onClientGUIClick к btnLogin и с него переключение на функцию 'clientSubmitLogin'
addEventHandler("onClientGUIClick", btnLogin, clientSubmitLogin, false)

Заметьте, что последний аргумент - "false". Это говорит о том, что срабатывание события произойдет только именно от btnLogin, но не в результате его распространения вверх или вниз по дереву. Использование "true" при прикреплении к элементам gui повлечет за собой срабатывание события при клике по любому из элементов одной ветви.

Теперь эта строка кода может быть добавлена внутрь функции createLoginWindow. Распространенной ошибкой является пробовать и прикреплять события к несуществующим элементам GUI, так что всегда проверяйте, прикрепили ли вы свои события уже после создания элемента gui (в данном случае, кнопки):

Обратите внимание, что сейчас мы допишем код для уже существующей функции 'createLoginWindow'.

function createLoginWindow()
	-- создаем все наши элементы GUI
	...

	-- теперь добавим событие onClientGUIClick к свежесозданной кнопке
	addEventHandler("onClientGUIClick", btnLogin, clientSubmitLogin, false)

Управление кликом

Теперь мы можем обнаружить клик игрока по кнопке, и нам нужно написать код, отвечающий за происходящее после него. В нашем обработчике события onClientGUIClick мы распорядились переключаться на функцию clientSubmitLogin при клике по btnLogin. Следовательно, теперь мы можем воспользоваться функцией clientSubmitLogin для контроля над происходящим после клика по кнопке:

-- создаем функцию и определяем параметры 'button' и 'state'
-- (которые автоматически передаются событием onClientGUIClick)
function clientSubmitLogin(button,state)
	-- если клик по кнопке был произведен левой кнопкой мыши, и положение кнопки мыши - "вверху" (отпущена)
	if button == "left" and state == "up" then
		-- отдаем контроль над клавиатурой обратно игре (позволяя игрокам передвигаться, писать в чат и т.д.)
		guiSetInputEnabled(false)
		-- прячем окно и все его компоненты
		guiSetVisible(wdwLogin, false)
		-- прячем курсор мыши
		showCursor(false)
	end
end

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

Вызов сервера

Вызов сервера производится с помощью triggerServerEvent. Это позволяет вам вызывать указанное серверное событие из клиента. С помощью triggerClientEvent можно сделать и наоборот. В данном случае мы будем использовать функцию triggerServerEvent для вызова созданного нами сами серверного события, названного "submitLogin", которое затем проконтроллирует респавн игрока на серверной стороне.

Примите во внимание, что сейчас мы просто дописываем код для уже существующей функции 'clientSubmitLogin'. Функция ниже не является новой, а предназначена заменить то, что мы уже имеем.

function clientSubmitLogin(button,state)
	if button == "left" and state == "up" then
		-- получаем текст, введенный в поле 'username'
		local username = guiGetText(edtUser)
		-- получаем текст, введенный в поле 'password'
		local password = guiGetText(edtPass)

		-- если и username, и password существуют
		if username ~= "" and password ~= "" then
			-- вызвать серверное событие 'submitLogin', передав ему username и password
			triggerServerEvent("submitLogin", getRootElement(), username, password)

			-- прячем gui, курсор и возвращаем управление игроку
			guiSetInputEnabled(false)
			guiSetVisible(wdwLogin, false)
			showCursor(false)
		else
			-- иначе выводим игроку сообщение, но не отсылаем информацию на сервер
			-- и не прячем gui
			outputChatBox("Пожалуйста, введите логин и пароль.")
		end
	end
end

Создание серверного события

На данный момент мы имеем весь нужный код для клиентской стороны, так что откройте ваш серверный файл 'script.lua' (из введения в скриптинг), либо любой другой подходящий для работы серверный файл.

На стороне сервера надо сделать так, чтобы игрок спавнился, как только залогинится. Итак, для начала нам понадобится объявить свое событие, которым мы ранее пользовались на клиенте. Это можно сделать с помощью связки addEvent и addEventHandler.

-- создаем функцию loginHandler с параметрами username и password (переданные от gui на клиенте)
function loginHandler(username,password)

end

-- объявляем свое событие и позволяем ему быть вызванным из клиента ('true')
addEvent("submitLogin",true)
-- добавляем обработчик события, чтобы при вызове submitLogin происходило переключение на функцию loginHandler
addEventHandler("submitLogin",root,loginHandler)

Залогинивание

Теперь у нас есть функция, которая вызывается через наше собственное событие 'submitLogin', теперь можно начать работать над процессом залогинивания и респавна игрока, задействовав функцию 'loginHandler':

function loginHandler(username,password)
	-- проверяем username и password на правильность
	if username == "user" and password == "apple" then
		-- игрок успешно залогинился, так что спавним его
		if (client) then
			spawnPlayer(client, 1959.55, -1714.46, 10)
			fadeCamera(client, true)
                        setCameraTarget(client, client)
			outputChatBox("Добро пожаловать на мой сервер.", client)
		end
	else
		-- если username или password неправильны, выводим игроку соответствующее сообщение
		outputChatBox("Неправильные логин и пароль. Пожалуйста, переподсоединитесь и попробуйте еще раз.",client)
        end			
end

addEvent("submitLogin",true)
addEventHandler("submitLogin",root,loginHandler)

Для целей данного руководства была использована очень простая система хранения логинов/паролей. Более вменяемыми альтернативами являются встроенная система аккаунтов и база данных MySQL.

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


Наконец, не забудьте включить новый файл gui.lua в meta.xml главного ресурса и отметить его как клиентский скрипт:

<script src="client/gui.lua" type="client" />


Теперь у нас есть базовое окно авторизации, проверяющее логин и пароль игрока по нажатии на кнопку залогинивания. Если они верны, игрок автоматически заспавнится.

Для дальнейшей помощи по GUI, посетите страницу с руководствами по GUI.