HUD это всё, что висит поверх игры: часы, координаты, счётчик FPS, ватермарка. HUD-элемент от скрипта полноправный житель этого интерфейса: его таскают мышкой в редакторе HUD, настраивают правой кнопкой, включают и выключают. Позиция и настройки сохраняются в аккаунте. Добавляется он одной строкой и дальше ничем не отличается от встроенных.
Создавай элемент в теле скрипта, оно выполняется один раз при загрузке. Функция Hud(...). Когда скрипт выгрузят, элемент исчезнет сам, убирать за собой не надо.
el = Hud("MyHud", width=100, height=20, x=8, y=8)
@el.render
def draw(d):
d.rounded_rect(0, 0, d.w, d.h, 6, Color(0, 0, 0, 180))
d.text("Привет", 6, 6, size=7, color=Color(255, 255, 255))| Параметр hud.add | По умолчанию | Что это |
|---|---|---|
name | обязателен | Имя. Видно в редакторе HUD, по нему же ищут через hud.find |
icon | "hud/player" | Иконка в списке элементов (hud/world, hud/potion, hud/handcuffs, logo, cube и другие) |
width / height | 80 / 20 | Размер на старте. Дальше его можно менять из кода |
x / y | 8 / 8 | Где появится в первый раз. Потом пользователь передвинет мышкой |
showing | True | Показывать ли сразу |
render | None | Функция рисования, то же самое, что декоратор @el.render |
layout | None | Функция сборки дерева виджетов, вместо render |
visible_when | None | Условие, при котором элемент вообще показывается |
Элементом можно рулить и из кода:
| Свойство / метод | Что делает |
|---|---|
el.x / el.y | Позиция, можно прочитать и записать |
el.width / el.height | Размер, можно прочитать и записать |
el.showing | Показан сейчас или нет |
el.name | Имя элемента, только читать |
el.pos(x, y) / el.size(w, h) | Задать позицию и размер |
el.show() / el.hide() / el.remove() | Показать, спрятать, удалить совсем |
Найти элементы: hud.find(name) по имени, hud.all() все скриптовые элементы клиента, hud.mine() только те, что создал твой скрипт, hud.remove(name) удалить.
В режиме render ты рисуешь всё сам, каждый кадр. Функция получает d, контекст рисования, ту самую кисточку. И вот главное: координаты в нём относительные. Точка (0, 0) это левый верхний угол самого элемента, а не экрана. Поэтому куда бы пользователь ни утащил элемент, твой код рисования не меняется ни на строчку.
| Метод контекста | Что рисует |
|---|---|
d.rect(x, y, w, h, color) | Прямоугольник. Если не задать w и h, займёт весь элемент |
d.rounded_rect(x, y, w, h, radius=6, color) | Прямоугольник со скруглёнными углами |
d.border(x, y, w, h, radius=6, thickness=1, color) | Рамка без заливки |
d.shadow(x, y, w, h, radius=6, softness=12, color) | Мягкая тень |
d.client_rect(...) | Фон в стиле клиента. Без аргументов заливает весь элемент |
d.text(value, x, y, size=7, weight="medium", color, outline, outline_width=1) | Текст. outline это цвет обводки вокруг букв, outline_width её ширина в пикселях |
d.centered_text(...) / d.right_text(...) | Текст по центру и прижатый вправо, с теми же аргументами |
d.icon(name, x, y, size=8, color) | Иконка клиента по имени |
d.item(stack, x, y, size=1) | Предмет из игры |
d.font(weight, size) | Достать шрифт, если нужен отдельно |
d.size(w, h) | Поменять размер самого элемента. Так подгоняют его под содержимое |
Чтобы надпись читалась на любом фоне, дайте ей обводку: outline это цвет контура вокруг букв, outline_width его ширина в пикселях (до 8). Цвет обводки лучше брать непрозрачным: у полупрозрачного контур накладывается сам на себя и выходит плотнее, чем вы задумали.
d.text("HP 20", 6, 6, size=8, weight="semibold",
color=Color(255, 255, 255), outline=Color(0, 0, 0), outline_width=1)Что ещё лежит в d: d.width и d.w, d.height и d.h это размер элемента; d.alpha прозрачность, которую сейчас хочет клиент (например, элемент плавно появляется); d.drag анимация перетаскивания; d.mouse_x и d.mouse_y положение курсора; d.delta доля тика. Каждый метод возвращает сам d, так что вызовы можно лепить цепочкой.
Разберём пример целиком: HUD с координатами и FPS. Ширина считается по самой длинной строке, элемент подгоняет себя под текст через d.size().
FG = Color(255, 255, 255)
DIM = Color(255, 255, 255, 140)
el = Hud("Coords", icon="hud/world", width=96, height=26, x=8, y=8)
def _lines():
p = world.self() # наш игрок; None значит мы ещё не в мире
if p is None:
return ["-, -, -", "0 fps"]
coords = "%d, %d, %d" % (int(p.getX()), int(p.getY()), int(p.getZ()))
return [coords, "%d fps" % int(mc.getCurrentFps())]
@el.render
def draw(d):
top, bottom = _lines()
# меряем обе строки, берём ту, что длиннее, плюс поля по краям
w = max(client.font_width("semibold", 8, top),
client.font_width("medium", 7, bottom)) + 14
d.size(w, 26)
# фон клиента: он сам учтёт прозрачность и анимацию перетаскивания
d.client_rect()
d.text(top, 7, 5, size=8, weight="semibold", color=FG)
d.text(bottom, 7, 15, size=7, color=DIM)Второй режим это layout. Тут ты ничего не рисуешь руками, а описываешь дерево виджетов: строки, колонки, тексты, иконки. Размеры, отступы и итоговую ширину элемента движок посчитает сам. Это тот же конструктор интерфейса, что и в меню, подробности в разделе про UI-компоненты.
Функция получает ui и собирает дерево. Текст можно передать функцией, тогда он обновляется сам, а дерево трогать не надо. В этом вся суть: дерево строится редко, а живые числа подтягиваются из функций каждый кадр.
import time
FG = Color(207, 215, 251)
BG = Color(9, 9, 11, 204)
DIV = Color(255, 255, 255, 20) # цвет разделителя между секциями
el = Hud("Bar", icon="logo", x=8, y=8)
def _fps():
return "%d fps" % int(mc.getCurrentFps())
def _name():
return str(mc.getSession().getUsername())
def build(ui):
with ui.row(align="center", gap=0, radius=6, bg=BG):
sections = (lambda: "Rockstar", _name, lambda: time.strftime("%H:%M"), _fps)
for i, provider in enumerate(sections):
if i > 0:
ui.column(width=1, height=7, bg=DIV) # тонкая палочка-разделитель
with ui.row(align="center", pad=(5, 7)):
# передаём функцию, а не строку, и значение обновляется само
ui.text(provider, size=7, weight="medium", color=FG)
el.layout(build)Бинды, зелья и список персонала в клиенте устроены одинаково: сверху шапка со значком, под ней строки-клавиши. Такой же элемент собирается одной функцией. Ты отдаёшь только строки, а лесенку по ширине, появление и уход строк, прижатие к своей стороне экрана клиент сделает сам.
binds = hud.list("Мои бинды", icon="keyboard", x=8, y=40)
@binds.rows
def rows():
return [[m.getName(), m.getKeyName()] for m in client.modules.all()
if m.isEnabled() and m.getKey() > 0]Строка это список ячеек. Ячейка бывает просто текстом, а бывает словарём, и тогда у неё есть настройки.
| Ключ | Что делает |
|---|---|
text | Текст на клавише |
icon | Значок клиента в квадратике |
item | Слот с предметом: стек из inventory.stack(slot), название вроде totem_of_undying или None для пустого слота. Количество и полоска прочности рисуются сами |
size | Сторона квадратной ячейки (значок, эффект, слот). По умолчанию 10, для слота инвентаря берут 18 |
effect | Иконка эффекта: объект из effects или его имя, например speed |
dot | Точка состояния: цвет или True для зелёной |
color | Цвет текста, значка или иконки |
background | Цвет подложки под ячейкой |
fill | Эта ячейка забирает всю свободную ширину строки |
width, min_width | Точная и минимальная ширина ячейки |
align | Выравнивание текста: start, center, end |
Если fill нигде не указан, растягивается первая текстовая ячейка. Поэтому пара из имени и клавиши сразу выглядит биндом, а иконка с именем и временем строкой зелья.
pots = hud.list("Зелья", icon="hud/potion", x=8, y=80)
@pots.rows
def rows():
return [[{"effect": e}, e.label, e.time] for e in effects.list()]Пустой список прячется сам, как клиентские: в игре его не видно, а в открытом чате он остаётся, иначе его нечем было бы подвинуть мышью. Нужен всегда видимый элемент, зови hide_empty(False). Рядом лежат title(text) для подписи в шапке, header(False) чтобы шапки не было вовсе, uniform(True) для одинаковой длины строк и sort(False), если строки должны идти в том порядке, в каком их вернула функция.
Строка, собранная из одних слотов, это уже сетка. Инвентарь 9 на 3 получается из трёх таких строк, и рисовать его руками не нужно.
inv = hud.list("Инвентарь", icon="hud/player", sort=False)
@inv.rows
def rows():
return [{"key": "main%d" % start,
"cells": [{"item": inventory.stack(s), "size": 18} for s in range(start, start + 9)]}
for start in (9, 18, 27)]Пустой слот, у которого предмета нет, всё равно занимает своё место в сетке, поэтому ряды не разъезжаются.
HUD-элементу можно навесить свои настройки. Они появятся в окошке по правому клику в редакторе HUD и сохранятся в конфиге. Конструкторы те же, что у модулей, только вызываются прямо на элементе.
| Вызов | Что за настройка |
|---|---|
el.checkbox("Shadow") | Галочка вкл/выкл: .get(), .set(b), .toggle() |
el.slider("Size") | Ползунок: .min(n).max(n).step(n).set(n), читать через .get() |
el.mode("Style") | Выбрать один вариант из списка: .add(name).select(name), .get() |
el.select("Elements") | Выбрать сразу несколько: .add(name), .select(name), .draggable(), .isSelected(name) |
el.button("Reset") | Кнопка: .action(fn) задаёт, что делать по нажатию |
el.range("Bounds") | Ползунок с двумя ручками, от и до: .min().max().first().second(), .get() |
el.color("Accent") | Выбор цвета: .color(r, g, b[, a]).alpha(True), .get() |
import time
el = Hud("Clock", width=60, height=18)
show_sec = el.checkbox("Секунды")
accent = el.color("Цвет").color(207, 215, 251)
@el.render
def draw(d):
fmt = "%H:%M:%S" if show_sec.get() else "%H:%M"
d.client_rect()
d.centered_text(time.strftime(fmt), d.w / 2, 5, size=8, color=accent.get())visible_when(fn) (то же самое, что show_when) задаёт условие показа. Клиент дёргает эту функцию сам; вернула False, элемент не рисуется. Так прячут HUD, когда открыто меню, когда мы не в игре или когда модуль выключен.
mod = Module("MyHud", "Visuals")
el = Hud("MyHud", width=80, height=18)
el.visible_when(lambda: mod.isEnabled() and world.ingame())Теперь про signature(fn). Он нужен только в режиме layout. Собирать дерево виджетов каждый кадр дорого, поэтому клиент строит его один раз и запоминает. Но как ему понять, что пора пересобрать?
Для этого ты даёшь функцию-сигнатуру. Она возвращает строку, которая описывает устройство дерева. Клиент зовёт её каждый кадр и сравнивает со старой: строка изменилась, дерево пересобирается. Это как штрихкод: он не про содержимое коробки, а про то, что за коробка.
Правило простое: в сигнатуру идёт всё, что меняет состав или порядок виджетов. Вот HUD-бар, у которого пользователь сам выбирает секции и их порядок:
import time
FG = Color(207, 215, 251)
BG = Color(9, 9, 11, 204)
DIV = Color(255, 255, 255, 20)
def _coords():
p = world.self()
if p is None:
return "0, 0, 0"
return "%d, %d, %d" % (int(p.getX()), int(p.getY()), int(p.getZ()))
# что за секции бывают и откуда каждая берёт своё значение
ELEMENTS = [
("Brand", lambda: "Rockstar"),
("Username", lambda: str(mc.getSession().getUsername())),
("Time", lambda: time.strftime("%H:%M")),
("FPS", lambda: "%d fps" % int(mc.getCurrentFps())),
("Coords", _coords),
]
PROV = {label: fn for label, fn in ELEMENTS}
el = Hud("Neobar", width=116, height=17, x=8, y=8)
# мультивыбор с перетаскиванием: пользователь решает, что показывать и в каком порядке
sel = el.select("Elements").draggable().min(0)
for label, _ in ELEMENTS:
sel.add(label)
for label in ("Brand", "Username", "Time"):
sel.select(label)
def build(ui):
with ui.row(align="center", gap=0, radius=6, bg=BG):
first = True
for label in list(sel.getValues()): # getValues() отдаёт порядок, который выставил игрок
if not sel.isSelected(label):
continue
if not first:
ui.column(width=1, height=7, bg=DIV) # разделитель
with ui.row(align="center", gap=4, pad=(5, 7)):
ui.text(PROV[label], size=7, weight="medium", color=FG)
first = False
def signature():
# порядок секций плюс то, какие включены. Изменилось, дерево пересоберётся
return "|".join(list(sel.getValues())) + "#" + ",".join(sorted(sel.getSelected()))
el.layout(build)
el.signature(signature)Скрипту видны не только свои элементы, но и весь HUD клиента: hud.elements отдаёт список словарей, где у каждого элемента есть координаты, размер и состояние анимаций. Это те же числа, которыми HUD рисует себя, так что вокруг чужого таргет-худа или списка биндов можно рисовать что угодно.
| Ключ | Что внутри |
|---|---|
name / title / icon | Ключ имени, переведённое имя и значок |
x, y, width, height | Где стоит и какого размера |
right, bottom, center_x, center_y | Посчитанные края и центр |
alpha | Прозрачность появления: 0 спрятан, 1 на месте |
scale | Масштаб кадра: элемент всплывает не только прозрачностью |
appear / visible / select / drag | Отдельные анимации: появление, видимость, выделение, перетаскивание |
showing / dragging / script | Включён игроком, тащат прямо сейчас, создан скриптом |
Читаются такие данные и точкой, и скобками: el.alpha это то же самое, что el["alpha"]. Внутри обычный словарь Python, так что работают и get(), и перебор, и проверка "name" in el.
Рисовать вокруг них дают два события: hud_render кладёт слой под элементы, а post_hud_render поверх них. Один элемент можно найти по имени: hud.info("targethud").
Элементы клиента можно и включать: hud.show(name), hud.hide(name) и hud.toggle(name) делают ровно то же, что галка в модуле Интерфейс, и так же запоминают выбор в настройках игрока. Имя берётся то же, что у hud.info: ключ, его последний кусок или видимое название. Вместо имени можно передать свой элемент.
mod = Module("CleanHud", "Visuals")
EXTRA = ("hud.keybinds", "hud.effects")
@events.module_toggled
def toggled(event):
if event.getModule().getName() != "CleanHud":
return
# пока модуль включён, лишние элементы спрятаны
for name in EXTRA:
if event.isState():
hud.hide(name)
else:
hud.show(name)
def flip_target():
hud.toggle("targethud")
Button(mod, "Цель").action(flip_target)| Метод | Что делает |
|---|---|
hud.show(name) | Включает элемент. False, если элемента с таким именем нет |
hud.hide(name) | Выключает элемент |
hud.toggle(name) | Переключает и отдаёт новое состояние: True, False или None, если имени нет |
hud.move(name, x, y) | Ставит элемент в точку. Координаты те же, что в hud.info |
mod = Module("HudBackdrop", "Visuals")
COL = Color(120, 190, 255)
@events.hud_render
def draw(event):
if not mod.isEnabled():
return
ctx = event.getContext()
for el in hud.elements:
if el.alpha <= 0.02:
continue
# подложка гаснет и сжимается вместе с элементом
w = el.width * el.scale
h = el.height * el.scale
x = el.center_x - w / 2
y = el.center_y - h / 2
ctx.drawRoundedRect(x - 4, y - 4, w + 8, h + 8, border(9), COL.withAlpha(40 * el.alpha))