Это кирпичики, из которых складывается любая графика: цвет, скругление углов, шрифт и картинка. Они одинаковые везде: и в render_2d, и в HUD, и в меню, и в 3D. Разберись один раз, дальше во всех разделах будет легче.
Цвет делает функция Color. Ей дают три числа: сколько красного, зелёного и синего, каждое от 0 до 255. Можно добавить четвёртое, альфу. Это непрозрачность: 255 значит цвет плотный, 0 совсем прозрачный, невидимый. Если альфу не написать, берётся 255.
red = Color(255, 0, 0) # красный: r, g, b
glass = Color(0, 0, 0, 180) # чёрный, но сквозь него немного видно
accent = Color.from_hex("#567cfc") # из hex, как в фигме; можно с альфой: "#567cfcaa"
argb = Color.from_int(0xFFFF0000) # из одного числа вида 0xAARRGGBB
magenta = Color.from_hsb(0.85, 1.0, 1.0) # оттенок, насыщенность, яркость; каждое 0..1
white = ColorRGBA.WHITE # готовые цвета:
black = ColorRGBA.BLACK # WHITE, BLACK, RED, GREEN, BLUE, YELLOWГотовый цвет можно подкрутить. Важно: он не меняется на месте, методы возвращают новый цвет, а старый остаётся целым.
| Метод | Что делает |
|---|---|
c.withAlpha(120) | Тот же цвет, но с другой альфой (0..255) |
c.mulAlpha(0.5) | Умножить нынешнюю альфу: 0.5 сделает вдвое прозрачнее. Удобно для затухания |
c.mix(other, 0.5) | Смешать с другим цветом. 0 остался этот, 1 стал другой, 0.5 ровно посередине |
c.toHex() | Строка вида "#rrggbbaa" |
Хороший приём: вынеси все цвета в начало скрипта и дай им имена. Тогда меню и HUD выглядят как одно целое, а перекрасить тему можно правкой пары строк. Именно так написаны все рабочие скрипты клиента.
BG = Color(12, 13, 15, 255) # фон окна
CARD = Color(16, 17, 20, 255) # карточка внутри окна
ACCENT = Color(86, 124, 252, 255) # акцент, им подсвечиваем главное
TEXT = Color(255, 255, 255, 255) # основной текст
MUTED = TEXT.withAlpha(140) # подписи: тот же белый, только потише
HOVER = ACCENT.mix(TEXT, 0.2) # акцент, чуть подбелённый, для наведенияРадуга получается сама собой из from_hsb: крутим оттенок по кругу вместе со временем, а насыщенность и яркость держим на месте.
import time
def rainbow(offset=0.0):
# % 1.0 заворачивает оттенок обратно в начало, цикл замыкается
hue = (time.time() * 0.2 + offset) % 1.0
return Color.from_hsb(hue, 0.65, 1.0)
@events.render_2d
def draw(event):
event.getContext().drawRoundedRect(10, 10, 80, 6, border(3), rainbow())У клиента своя палитра, и игрок меняет её акцент в настройках. Объект theme отдаёт эти цвета скрипту где угодно: в HUD-элементе, в 2D-рисовании, в 3D. Раньше они были доступны только внутри лейаута, и нарисованный вручную элемент выпадал из темы.
theme.accent() # акцент клиента
theme.text() # цвет текста
theme.background() # фон панелей
theme.second() # фон второго уровня
theme.outline() # линии и рамки
theme.on_accent() # цвет текста ПОВЕРХ акцентной заливки
theme.color("accent") # то же самое по имениЦвета живые: игрок сменил акцент, и следующий кадр рисуется уже новым. Поэтому брать их надо в момент отрисовки, а не запоминать в переменную при загрузке скрипта.
el = Hud("Themed", width=120, height=22)
@el.render
def draw(ctx):
ctx.size(120, 22)
ctx.rounded_rect(0, 0, 120, 22, radius=6, color=theme.background())
ctx.rect(0, 20, 120, 2, theme.accent())
ctx.text("в цветах клиента", 8, 6, size=7, color=theme.text())| Метод | Что это |
|---|---|
theme.color(name) | accent, text, background, second, outline, shadow, on_accent, flat, separator |
theme.readable(color) | Читаемый текст для своей заливки: светлый или тёмный |
theme.name() / theme.dark() | Имя темы и признак тёмной |
theme.set_accent(color) | Сменить акцент клиента целиком |
Скруглённым фигурам мало одного числа, им нужен набор радиусов на все четыре угла. Такой набор делают двумя функциями:
border(r) ставит один радиус сразу на все углы;border4(tl, tr, br, bl) задаёт углы по часовой стрелке от верхнего левого: верхний левый, верхний правый, нижний правый, нижний левый.@events.render_2d
def draw(event):
ctx = event.getContext()
dark = Color(20, 20, 24, 220)
ctx.drawRoundedRect(10, 10, 60, 24, border(8), dark) # все углы одинаково
ctx.drawRoundedRect(80, 10, 60, 24, border4(12, 0, 12, 0), dark) # скруглены два угла по диагонали
ctx.drawRoundedRect(150, 10, 60, 24, border(12), dark) # радиус в половину высоты даёт таблеткуfont(weight, size) собирает шрифт для методов рисования текста. weight это насколько буквы жирные, size размер в пикселях интерфейса. Для HUD обычно берут 6-9, для заголовков в меню 10-15.
| Вес | Когда брать |
|---|---|
regular | Обычный текст |
medium | Рабочая лошадка. Бери его, если не уверен |
semibold | Названия модулей, важные подписи |
bold | Заголовки, вкладки |
roundbold | Жирный со скруглёнными буквами: крупные цифры, лого |
noto | Умеет почти любые символы: иероглифы, эмодзи, редкие языки |
@events.render_2d
def draw(event):
ctx = event.getContext()
title = font("bold", 12)
body = font("medium", 8)
ctx.drawText(title, "Rockstar", 20, 20, Color(255, 255, 255))
ctx.drawText(body, "python scripting", 20, 36, Color(255, 255, 255, 150))Чтобы что-то поставить по центру или подогнать подложку под надпись, надо знать её размеры. Их считает клиент, по тому же весу и размеру, что у шрифта:
text = "Target: Steve"
w = client.font_width("medium", 8, text) # сколько пикселей займёт строка в ширину
h = client.font_height("medium", 8) # и в высоту
@events.render_2d
def draw(event):
ctx = event.getContext()
ctx.drawRoundedRect(10, 10, w + 12, h + 8, border(5), Color(0, 0, 0, 170))
ctx.drawText(font("medium", 8), text, 16, 14, Color(255, 255, 255))Ассет это любой файл, который нужен скрипту: картинка, шрифт. Свои картинки клади рядом со скриптом, например в scripts/assets/. Дальше assets.image(path) (у неё есть короткие имена image и texture) прочитает файл и вернёт пропуск на текстуру. Этот пропуск понимают drawTexture, drawRoundedTexture, ui.image и render3d.billboard.
| Вызов | Что делает |
|---|---|
assets.image("assets/logo.png") | Твоя картинка с диска. Путь считается от папки scripts/ |
assets.image("https://site.ru/logo.png") | Она же, но по ссылке. Качается один раз |
assets.resource("icons/logo.png") | Картинка из ресурсов самого клиента |
assets.ttf("MyFont", "assets/Inter.ttf", 9) | Свой шрифт прямо из ttf или otf, файлом или ссылкой |
assets.svg("моё/сердце", "assets/heart.svg") | Свой значок из svg, файлом, ссылкой или текстом |
assets.download(url, callback) | Любой другой файл по ссылке: вернёт путь к локальной копии |
assets.scripts_dir() | Путь до папки со скриптами |
mod = Module("MyFont", "Visuals")
# файл лежит в scripts/assets/Inter.ttf; размер тут в тех же единицах, что у font("medium", 8)
inter = assets.ttf("Inter", "assets/Inter.ttf", 9)
@events.render_2d
def draw(event):
if mod.isEnabled():
event.getContext().drawText(inter, "свой шрифт", 12, 12, Color(255, 255, 255))mod = Module("LogoHud", "Visuals")
logo = assets.image("assets/logo.png") # файл лежит в scripts/assets/logo.png
@events.render_2d
def draw(event):
if not mod.isEnabled():
return
ctx = event.getContext()
ctx.drawRoundedRect(10, 10, 40, 40, border(10), Color(0, 0, 0, 160))
ctx.drawRoundedTexture(logo, 14, 14, 32, 32, border(8), Color(255, 255, 255))def img(path):
try:
return assets.image(path)
except Exception:
return None # файла нет, просто живём без картинки
avatar = img("assets/avatar.png")
@events.render_2d
def draw(event):
if avatar is None:
return
event.getContext().drawRoundedTexture(avatar, 10, 10, 16, 16, border(4))У клиента есть свой набор значков, они рисуются по имени: d.icon("hud/potion"), ui.icon("category/combat"). Свой значок добавляется туда же: assets.svg(имя, файл) разбирает svg, и дальше имя работает во всех тех же местах.
# файл лежит в scripts/assets/heart.svg
assets.svg("моё/сердце", "assets/heart.svg")
el = Hud("Здоровье", width=60, height=20)
@el.render
def draw(d):
d.icon("моё/сердце", 4, 4, size=12, color=Color(255, 80, 80))
d.text(str(int(mc.player.getHealth())), 20, 6, size=8)Имя ты придумываешь сам, и оно общее на все скрипты, поэтому бери с префиксом: короткое star может занять сосед. Значок можно дать и текстом, прямо из кода скрипта, через assets.svg_text(имя, текст): у скрипта с маркета файлов рядом нет, приезжает только код.
Значок это вектор: он масштабируется на любой размер без мыла и рисуется тем же способом, что и текст, то есть почти бесплатно.
Везде, где ждут путь к картинке или шрифту, можно написать ссылку. Клиент скачает файл один раз и положит его в свою папку, поэтому со второго запуска ассет берётся с диска и интернет уже не нужен. Это удобно, когда скрипт продаётся или просто раздаётся одним файлом: картинки не нужно класть рядом.
logo = image("https://site.ru/logo.png") # картинка
inter = assets.ttf("Inter", "https://site.ru/Inter.ttf", 9) # шрифт
def ready(path):
client.msg("файл лежит тут: " + path)
assets.download("https://site.ru/data.json", ready) # любой другой файл| Что | Как ведёт себя |
|---|---|
| Картинка | Пропуск на текстуру возвращается сразу, картинка появляется, когда дойдёт. Рисовать можно с первого кадра |
| Шрифт | До загрузки текст рисуется запасным начертанием, потом само встаёт своё. Со второго запуска сразу своё |
| Другой файл | assets.download вернёт путь, если файл уже скачан, иначе None, а путь придёт в колбэк |