Предисловие
Как можно развлечься в новогодние праздники? Играть в компьютерные игры? Нет! Лучше написать бота, который будет делать это за вас, а сами пойти лепить снеговика и пить глинтвейн.Когда-то в школьные годы меня увлекла одна из популярных ММОРПГ - Lineage 2. В игре можно вступать в кланы, группы, дружить и сражаться с соперниками, но в целом игра наполнена однообразными действиями: прохождением квесты и фарм (сбор ресурсов, получение опыта).
В итоге я решил, что бот должен решать одну задачу: фармить.
Для управления будут использоваться эмулированные щелчки мыши и нажатия клавиш клавиатуры, а для пространственной ориентации — компьютерное зрение с использованием языка программирования Python. В общем, создание бота для Л2 — дело не новое и готовых существует довольно много.
Они разделены на 2 основные группы: те, которые внедряются в работу клиента и кликеры.
Первые — жесткий чит; в плане игры использовать их слишком неспортивно.
Второй вариант более интересен, учитывая, что его можно применить к любой другой игре с некоторыми доработками, и реализация будет интереснее.
Те кликеры, которые я нашел, по разным причинам не работали, либо работали нестабильно.
Внимание: вся информация здесь представлена исключительно в образовательных целях.
Специально для разработчиков игр, чтобы помочь им лучше бороться с ботами.
Итак, приступим к делу.
Работа с окном
Здесь все просто.Мы будем работать со скриншотами из окна игры.
Для этого определяем координаты окна.
Работаем с окном с помощью модуля win32gui. Нужное окно мы определим по названию – «Lineage 2».
Код методов получения положения окна
Получаем изображение нужного окна с помощью ImageGrab:def get_window_info(): # set window info window_info = {} win32gui.EnumWindows(set_window_coordinates, window_info) return window_info # EnumWindows handler # sets L2 window coordinates def set_window_coordinates(hwnd, window_info): if win32gui.IsWindowVisible(hwnd): if WINDOW_SUBSTRING in win32gui.GetWindowText(hwnd): rect = win32gui.GetWindowRect(hwnd) x = rect[0] y = rect[1] w = rect[2] - x h = rect[3] - y window_info['x'] = x window_info['y'] = y window_info['width'] = w window_info['height'] = h window_info['name'] = win32gui.GetWindowText(hwnd) win32gui.SetForegroundWindow(hwnd)
def get_screen(x1, y1, x2, y2):
box = (x1 + 8, y1 + 30, x2 - 8, y2)
screen = ImageGrab.grab(box)
img = array(screen.getdata(), dtype=uint8).
reshape((screen.size[1], screen.size[0], 3))
return img
Теперь поработаем с контентом.
Искать монстра
Самое интересное.Те реализации, которые я нашел, меня не устроили.
Например, в одной из популярных и даже платных это сделано через игровой макрос.
И «игрок» должен написать макрос типа «/target Monster Name Bla Bla» для каждого типа монстров.
В нашем случае мы будем следовать такой логике: в первую очередь мы найдем на экране весь белый текст. Белый текст может быть не только именем монстра, но и именем самого персонажа, именем NPC или других игроков.
Поэтому нам необходимо навести курсор на объект и если появится подсветка с нужным нам рисунком, то мы можем атаковать цель.
Вот исходное изображение, с которым мы будем работать:
Закрасим наше имя черным цветом, чтобы оно не мешало, и превратим картинку в черно-белую.
Исходное изображение имеет формат RGB — каждый пиксель представляет собой массив из трех значений от 0 до 255, когда ч/б — это одно значение.
Таким образом мы существенно сократим объем данных: img[210:230, 350:440] = (0, 0, 0)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
Найдите все белые объекты (это белый текст с именами монстров) ret, threshold1 = cv2.threshold(gray, 252, 255, cv2.THRESH_BINARY)
Морфологические преобразования:
- Мы будем фильтровать по прямоугольнику 50х5. Этот прямоугольник работал лучше всего.
- Удаление шума внутри прямоугольников с текстом (по сути, закрашивание всего, что между буквами, в белый цвет)
- Еще раз убираем шум путем размытия и растягивания с помощью фильтра.
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (50, 5))
closed = cv2.morphologyEx(threshold1, cv2.MORPH_CLOSE, kernel)
closed = cv2.erode(closed, kernel, iterations=1)
closed = cv2.dilate(closed, kernel, iterations=1)
Нахождение середины получившихся пятен
(_, centers, hierarchy) = cv2.findContours(closed, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
Работает, но можно сделать круче (например, для монстров, имена которых не видны, потому что они далеко) — используя TensorFlow Object Detection, например здесь , но когда-нибудь в следующей жизни.
Теперь наводим курсор на найденного монстра и смотрим, появилось ли выделение с помощью метода cv2.matchTemplate. Остается только нажать ЛКМ и кнопку атаки.
Плакать
С поиском монстра мы разобрались, бот уже может находить цели на экране и наводить на них мышку.Чтобы атаковать цель, нужно щелкнуть левой кнопкой мыши и нажать «атака» (можно привязать атаку к кнопке «1»).
Щелчок правой кнопкой мыши необходим для поворота камеры.
На сервере, где я тестировал бота, я вызывал клик через AutoIt, но по каким-то причинам это не сработало.
Как оказалось, игры защищены от автокликеров разными способами:
- поиск процессов, эмулирующих клики
- запись кликов и определение цвета объекта, на который нажимает бот
- определение шаблонов кликов
- идентификация бота по частоте кликов
(Было бы здорово, если бы кто-нибудь мог сказать мне, как именно).
Мы попробовали некоторые фреймворки, которые умеют кликать (в том числе pyautogui, robot framework и что-то еще), но ни один из вариантов не сработал.
Проскочила идея построить устройство, которое будет нажимать кнопку (кто-то даже это делал).
Похоже, вам нужно нажать как можно сильнее.
В результате я начал подумывать о написании собственного драйвера.
В Интернете был найден способ решения проблемы: USB-устройство, которое можно запрограммировать на отправку нужного сигнала — Digispark.
Ждать несколько недель с Алиэкспресс не хочется, поэтому поиск продолжился.
В конце концов было найдено замечательная библиотека C Нашел для нее и Python-обертка Моя библиотека не запустилась на Python 3.6 — у меня возникла ошибка нарушения прав доступа или что-то в этом роде.
Поэтому мне пришлось перейти на Python 2.7, там все работало как часы.
Движение курсора
Библиотека может отправлять любые команды, в том числе куда двигать мышкой.Но это похоже на телепортацию курсора.
Нам нужно сделать движение курсора плавным, чтобы нас не забанили.
По сути задача сводится к перемещению курсора из точки А в точку Б с помощью обертки AutoHotPy. Вам действительно нужно помнить математику? После некоторых раздумий я наконец решил загуглить.
Оказалось, что ничего изобретать не нужно — задачу решает алгоритм Брезенхема, один из старейших алгоритмов компьютерной графики:
Вы можете взять это прямо из Википедии.
Логика работы
Все инструменты есть, осталось самое простое — написать скрипт.- Если монстр жив, продолжаем атаковать
- Если цели нет, найдите цель и начните атаковать.
- Если мы не смогли найти цель, немного развернёмся
- Если 5 раз не удалось никого найти, отойдите в сторону и начните заново.
В общих чертах: с помощью паттерна OpenCV находим элемент управления, показывающий состояние здоровья цели, берём полоску высотой в один пиксель и считаем в процентах, сколько закрашено красным.
Код метода для получения уровня здоровья жертвы def get_targeted_hp(self):
"""
return victim's hp
or -1 if there is no target
"""
hp_color = [214, 24, 65]
target_widget_coordinates = {}
filled_red_pixels = 1
img = get_screen(
self.window_info["x"],
self.window_info["y"],
self.window_info["x"] + self.window_info["width"],
self.window_info["y"] + self.window_info["height"] - 190
)
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
template = cv2.imread('img/target_bar.png', 0)
# w, h = template.shape[::-1]
res = cv2.matchTemplate(img_gray, template, cv2.TM_CCOEFF_NORMED)
threshold = 0.8
loc = np.where(res >= threshold)
if count_nonzero(loc) == 2:
for pt in zip(*loc[::-1]):
target_widget_coordinates = {"x": pt[0], "y": pt[1]}
# cv2.rectangle(img, pt, (pt[0] + w, pt[1] + h), (255, 255, 255), 2)
if not target_widget_coordinates:
return -1
pil_image_hp = get_screen(
self.window_info["x"] + target_widget_coordinates['x'] + 15,
self.window_info["y"] + target_widget_coordinates['y'] + 31,
self.window_info["x"] + target_widget_coordinates['x'] + 164,
self.window_info["y"] + target_widget_coordinates['y'] + 62
)
pixels = pil_image_hp[0].
tolist()
for pixel in pixels:
if pixel == hp_color:
filled_red_pixels += 1
percent = 100 * filled_red_pixels / 150
return percent
Теперь бот понимает, сколько ХП у жертвы и жива ли она еще.
Базовая логика готова, вот как она выглядит сейчас в действии: Для тех, кто занят, я ускорил процесс.
Прекратить работу
Вся работа с курсором и клавиатурой осуществляется через объект autohotpy, работу которого можно остановить в любой момент, нажав кнопку ESC. Проблема в том, что все время бот занят выполнением цикла, отвечающего за логику действий персонажа, а обработчики событий объекта и autohotpy не начинают слушать события, пока цикл не завершится.Программу невозможно остановить даже с помощью мыши, потому что ею управляет бот и перемещает курсор туда, куда ему нужно.
Нас это не устраивает, поэтому пришлось разделить бота на 2 потока: прослушивание событий и выполнение логики действий персонажа.
Давайте создадим 2 темы # init bot stop event
self.bot_thread_stop_event = threading.Event()
# init threads
self.auto_py_thread = threading.Thread(target=self.start_auto_py, args=(auto_py,))
self.bot_thread = threading.Thread(target=self.start_bot, args=(auto_py, self.bot_thread_stop_event, character_class))
# start threads
self.auto_py_thread.start()
self.bot_thread.start()
и теперь присоединяем обработчик к ESC: auto_py.registerExit(auto_py.ESC, self.stop_bot_event_handler)
при нажатии ESC устанавливаем событие self.bot_thread_stop_event.set()
и в цикле символьной логики проверяем, установлено ли событие: while not stop_event.is_set():
Теперь спокойно остановите бота кнопкой ESC.
Заключение
Казалось бы, зачем тратить время на продукт, который не приносит никакой практической пользы? По сути, с точки зрения компьютерного зрения компьютерная игра — это почти то же самое, что и реальность, заснятая на камеру, и возможности для применения в ней огромны.Отличный пример описан в статье о подводных роботах, которые они стреляют в лосося лазером .
Статья также может помочь разработчикам игр в борьбе с ботами.
Что ж, я познакомился с Python, прикоснулся к компьютерному зрению, написал свой первый слабоумный искусственный интеллект и получил массу удовольствия.
Надеюсь, вам это тоже было интересно.
P.S. Ссылка к репозиторий Теги: #python #бот #AI #Lineage 2 #компьютерное зрение #python #программирование #Разработка игр
-
Шанют, Октава
19 Oct, 24 -
Другие Браузеры Добрались До Антарктиды
19 Oct, 24