Содержание статьи
Мы будем управлять ботом так же, как это делает обычный игрок: отправлять ему команды нажатия клавиш (WASD и других). Получать информацию от бота будем через встроенную в игру функцию сохранения скриншотов. Например, отправили боту команду нажать F12 — получили снимок того, что он видит в данный момент. Проанализировали изображение, отправили следующую команду — и так по кругу.
Важный момент: у нас не будет цели делать в игре что‑то конкретное. Мы просто подготовим основу и заодно изучим принципы работы с машинным зрением.
Для распознавания объектов мы будем использовать модели YOLO (You Only Look Once). Наша задача — обучить модель находить булыжник и двигаться по дорожке, вымощенной им.
Обучение будет проходить в два этапа.
- Первый этап. Обучим модель YOLOv8n находить наш объект на снимках. Эта модель сможет определить, содержится ли на изображении искомый объект, и выделить его с помощью ограничивающего прямоугольника (bounding box).
- Второй этап. Подготовим обучающий материал для второй модели — YOLOv8-seg, которая сможет обводить объекты по контуру. Это даст нам возможность анализировать их размер, форму и положение. Это именно то, что нужно для точной ориентации бота в пространстве.
Что нам понадобится
-
Обычный компьютер. Мы используем легковесные модели, с которыми справится даже средний по мощности ПК. Весь код был написан и протестирован на машине со следующими характеристиками:
- процессор — восьмиядерный AMD Ryzen 7 5700U с картой Radeon;
- оперативная память — 8 Гбайт.
-
Свободное место на диске. Потребуется около 10 Гбайт:
- 400–500 Мбайт для хранения снимков (зависит от их количества);
- ~400 Мбайт для скачивания предобученных моделей;
- ~8,6 Гбайт для установки библиотек Python;
- 38,8 Кбайт для нашего кода!
- Хороший фильм или даже сериал. Обучение модели занимает несколько часов, так что стоит запастись терпением и развлечениями.
- Операционная система. Я использовал Ubuntu, но подойдет любой дистрибутив Linux. Нам понадобятся несколько дополнительных программ, которые, скорее всего, уже есть в официальных репозиториях твоего дистра.
Ставим приложения для работы с буфером обмена:
sudo apt install xclip xsel
И для работы с окнами:
sudo apt install wmctrl xdotool
Тестировать я буду на игре Luanti (также известна под старым названием Minetest) — она попроще, чем Minecraft, и есть в стандартных репозиториях. Однако попутно дам рекомендации для работы с Minecraft — отличия будут минимальными.
Мы воспользуемся своим набором текстур для игры, чтобы наш бот заработал на любом сервере. Скачать текстурки можно из репозитория проекта.
В качестве языка возьмем Python — как один из наиболее популярных и имеющий множество удобных библиотек.
Весь процесс можно разбить на три основных этапа: подготовка, обучение, использование.
Подготовка
У нас будет много разных файлов, поэтому, чтобы ничего не потерять, создадим каталог для проекта.
Поскольку нам нужно будет ставить модули Python, сразу сделаем виртуальное окружение.
python -m venv minebot
Теперь попробуем покомандовать нашим ботом. Для этого воспользуемся библиотекой PyAutoGUI. С ее помощью мы можем отправлять окну в фокусе нажатие клавиш, движение мыши и нажатие ее кнопок.
Активируем наше виртуальное окружение:
source minebot/bin/activate
Установим библиотеки.
pip install pyautogui pyperclip opencv-python numpy ultralytics
Команды управления ботом я собрал в одном файле.
luanti/controller.py
import pyautoguiimport pyperclipimport timeimport mathclass LuantiController: """Класс для управления персонажем в Luanti с использованием библиотеки PyAutoGUI.""" def __init__(self, window_width, window_height): """Инициализирует контроллер, получает размеры экрана.""" self.screen_width = window_width self.screen_height = window_height self.fov = 72 # Угол обзора в градусах def take_screenshot(self): """Делает скриншот (нажатие F12).""" pyautogui.press('f12') time.sleep(0.1) def _move(self, key, duration): """Вспомогательная функция для перемещения.""" pyautogui.keyDown(key) time.sleep(duration) pyautogui.keyUp(key) def move_forward(self, duration): """Перемещает персонажа вперед (нажатие W).""" self._move('w', duration) def move_backward(self, duration): """Перемещает персонажа назад (нажатие S).""" self._move('s', duration) def move_left(self, duration): """Перемещает персонажа влево (нажатие A).""" self._move('a', duration) def move_right(self, duration): """Перемещает персонажа вправо (нажатие D).""" self._move('d', duration) def jump(self): """Прыжок (нажатие пробела).""" pyautogui.keyDown('space') time.sleep(0.1) pyautogui.keyUp('space') def run(self, direction, duration): """Бег в указанном направлении.""" pyautogui.keyDown('e') time.sleep(0.1) if direction == 'forward': self._move('w', duration) elif direction == 'backward': self._move('s', duration) elif direction == 'left': self._move('a', duration) elif direction == 'right': self._move('d', duration) pyautogui.keyUp('e') def crouch_and_move(self, direction, duration): """Перемещение при приседании.""" pyautogui.keyDown('shift') time.sleep(0.1) if direction == 'forward': self._move('w', duration) elif direction == 'backward': self._move('s', duration) elif direction == 'left': self._move('a', duration) elif direction == 'right': self._move('d', duration) pyautogui.keyUp('shift') def open_inventory(self): """Открывает инвентарь (нажатие I).""" pyautogui.press('i') def close_inventory(self): """Закрывает инвентарь (нажатие Esc).""" pyautogui.press('esc') def change_camera(self, clicks): """Меняет вид камеры (нажатие C).""" for _ in range(clicks): pyautogui.press('c') time.sleep(0.5) def select_inventory_slot(self, slot_number): """Выбирает слот в инвентаре (нажатие клавиш 1–8).""" if 1 <= slot_number <= 8: pyautogui.press(str(slot_number)) else: print("Недопустимый номер слота: " + str(slot_number)) def place_block(self): """Ставит блок (правый клик мыши).""" pyautogui.click(button='right') def mine_block(self): """Ломает блок (удержание левого клика мыши).""" pyautogui.mouseDown(button='left') time.sleep(2) # Удерживаем кнопку мыши 2 секунды pyautogui.mouseUp(button='left') def send_chat_message(self, message): """Отправляет сообщение в чат с поддержкой кириллицы.""" # Открываем чат pyautogui.press('t') time.sleep(0.1) # Копируем текст в буфер обмена pyperclip.copy(message) # Вставляем текст из буфера обмена (Ctrl + V) pyautogui.hotkey('ctrl', 'v') # Отправляем сообщение pyautogui.press('enter') def _calculate_pixels_for_degrees(self, degrees): """Вычисляет количество пикселей для сдвига мыши, необходимое для имитации поворота на заданный угол.""" pixels_per_degree = self.screen_width / self.fov return int(degrees * pixels_per_degree) def turn(self, degrees_x, degrees_y): """Поворачивает персонажа на указанное количество градусов.""" pixel_offset_x = self._calculate_pixels_for_degrees(degrees_x) pixel_offset_y = self._calculate_pixels_for_degrees(degrees_y) pyautogui.moveRel(pixel_offset_x, pixel_offset_y, duration=0.0) time.sleep(0.05) def drop_item(self): """Выбрасывает предмет (нажатие Q).""" pyautogui.press('q') time.sleep(0.1)
Управление в Minecraft будет отличаться несколькими клавишами, код файла можно взять в репозитории проекта.
Дальше — демонстрационный код, который поочередно отправляет все команды боту, чтобы проверить, как он их выполняет.
Вот как это работает.
Мы подключаем к нашему файлу команды управления ботом из предыдущего листинга.
Важно: при подключении нужно указывать размеры окна игры! Если размер неверный, то бот будет неправильно рассчитывать углы для поворота. Для упрощения и сокращения кода в этом файле они указаны вручную, ты можешь их исправить на свои значения. Далее мы будем определять эти параметры автоматически. Сейчас же нам надо проверить, что мы можем управлять персонажем.
Зайди в игру и найди ровную поверхность, запусти experiment_1.
в терминале и кликай по окну с игрой, чтобы оно получило фокус.
Для Minecraft нужно будет закомментировать одну строку и раскомментировать другую — они помечены в коде.
experiment_1.py
import pyautoguiimport timeimport cv2import osimport datetimeimport re# Для Luantifrom luanti.controller import LuantiController as Controller# Для Minecraft# from minecraft.controller import MinecraftController as Controller# Лучше сделать размер окна игры такого размераwindow_width, window_height = 640, 640def run_bot_actions(controller): """Выполняет все действия бота.""" normal_move_time = 2 run_move_time = 1 methods = [ ("устанавливает направление взгляда", lambda: ( controller.turn(0, 90), controller.turn(0, -25) ) ), ("идет вперед", lambda: controller.move_forward(normal_move_time)), ("идет назад", lambda: controller.move_backward(normal_move_time)), ("идет влево", lambda: controller.move_left(normal_move_time)), ("идет вправо", lambda: controller.move_right(normal_move_time)), ("прыгает", controller.jump), ("бежит вперед", lambda: controller.run("forward", run_move_time)), ("бежит назад", lambda: controller.run("backward", run_move_time)), ("бежит влево", lambda: controller.run("left", run_move_time)), ("бежит вправо", lambda: controller.run("right", run_move_time)), ("приседает и идет вперед", lambda: controller.crouch_and_move("forward", normal_move_time)), ("приседает и идет назад", lambda: controller.crouch_and_move("backward", normal_move_time)), ("приседает и идет влево", lambda: controller.crouch_and_move("left", normal_move_time)), ("приседает и идет вправо", lambda: controller.crouch_and_move("right", normal_move_time)), ("открывает инвентарь", controller.open_inventory), ("закрывает инвентарь", controller.close_inventory), ("меняет камеру", lambda: controller.change_camera(3)), ("выбирает слот 8 и ставит блок", lambda: ( controller.select_inventory_slot(8), controller.place_block() ) ), ("выбирает слот 5 и копает блок", lambda: ( controller.select_inventory_slot(5), controller.mine_block() ) ), ("выбирает слот 1 и кушает :)", lambda: ( controller.select_inventory_slot(1), time.sleep(0.2), pyautogui.mouseDown(button='left'), time.sleep(0.1), pyautogui.mouseUp(button='left') ) ), ("поворачивается на 10 градусов вправо", lambda: controller.turn(10, 0)), ("поворачивается на 10 градусов влево", lambda: controller.turn(-10, 0)), ("поворачивается на 10 градусов влево",lambda: controller.turn(-10, 0)), ("поворачивается на 10 градусов вправо",lambda: controller.turn(10, 0)), ("Делает скриншот", controller.take_screenshot), ("пишет в чат", lambda: controller.send_chat_message("Тест пройден, мои поздравления!")), ] for description, method in methods: print(f"[БОТ] {description}") if isinstance(method, tuple): for action in method: action() else: method() time.sleep(0.2)if __name__ == "__main__": print("Начинаем тестирование всех методов...") print("Пожалуйста, переключите фокус на окно с игрой...") time.sleep(5) if window_width is not None and window_height is not None: controller = Controller(window_width, window_height) run_bot_actions(controller) print(f"[БОТ] работу закончил.")
Если бот зашевелился, значит, есть контроль над ним и можно двигаться дальше.
Продолжение доступно только участникам
Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».
Присоединяйся к сообществу «Xakep.ru»!
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее