What's new
Runion

This is a sample guest message. Register a free account today to become a member! Once signed in, you'll be able to participate on this site by adding your own topics and posts, as well as connect with other members through your own private inbox!

Улучшение нейроаима, добавление анти отдачи и новых механик

OverlordGameDev

Light Weight
Депозит
$0

Предисловие​

В этой статье будет показано продолжение разработки нейросети: будет переписана структура проекта, добавлены новые механики и улучшены старые.

Нововведения в проекте​

В данной статье будет реализована анти-отдача в связке с нейронной сетью, которая была разработана в предыдущих статьях. Анти-отдача будет разделена на несколько типов:
  1. Стандартная — это анти-отдача, которая работает путём смещения курсора по оси Y вниз по прямой.
  2. Кастомная — этот тип анти-отдачи будет работать, используя точные координаты и смещая курсор как по осям X, так и Y с конкретными таймингами. По сути, данный метод будет аналогичен макросам для мышек X7, и каждый макрос будет настроен на определённую игру и конкретное оружие. В данной статье для примера выбрана игра CS2. В связи с усложнённой логикой отдачи в игре CS2, также потребуется изменить функцию наведения и функцию детекции, чтобы добавить возможность выбора объекта для наведения — например, контр-террориста, террориста или обоих.
Кроме того, в статье будет представлен простой интерфейс для управления настройками нейронной сети.

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

Также будет написана логика для функции триггер-бота.

Перенос настроек в конфиг файл​

Перед тем как приступить к разработке логики анти-отдачи, необходимо подготовить проект, внести некоторые правки и улучшить его базу. В первую очередь, для удобства следует вынести переменные в отдельный конфигурационный файл. Для этого нужно создать файл config.json и записать в него все переменные, которые ранее назначались в начале кода.
Код: Скопировать в буфер обмена
Code:
{
   "screen_width": 1366,
   "screen_height": 768,
   "fov_width": 500,
   "fov_height": 500,
   "aim_step": 2,
   "aim_time_sleep": 0.001,
   "aim_target": 0.2
}

Теперь вместо переменных в коде нужно написать новую функцию с названием load_config, которая будет принимать три аргумента:
  1. Первый аргумент: ключ (в нём будет находиться ключ из файла конфига, например aim_step).
  2. Второй аргумент: max_retries (это максимальное количество попыток обращения к конфигу. Это нужно для будущего интерфейса, если в интерфейсе заменилось значение и передалось в конфиг, в этот момент программа может запросить доступ к значению из конфига, а интерфейс не успел его передать. Поэтому может возникнуть ошибка и краш всей программы. max_retries позволит при неудачной попытке попробовать снова определённое количество раз).
  3. Третий аргумент: delay (это пауза перед следующей попыткой обращения к конфигу после неудачной попытки).
Python: Скопировать в буфер обмена
def load_config(key=None, max_retries=3, delay=1):

Далее нужно назначить переменную attempt, которая будет равняться нулю. Это нужно также для предотвращения краша программы. Затем нужно создать цикл while, который будет срабатывать, если attempt меньше max_retries.
Python: Скопировать в буфер обмена
while attempt < max_retries:

Внутри цикла нужно установить блок try-except. В блоке try будет попытка открыть и загрузить в переменную config данные из файла конфига. После этого, внутри блока try, должно быть условие if, которое будет проверять, если в переменную load_config был передан ключ, то функция должна возвращать данные конкретного ключа из переменной config, в которую ранее были загружены все данные из файла конфига. Если же ключ не был передан при запросе к переменной, функция должна возвращать все данные из переменной config, а не конкретный ключ.
Python: Скопировать в буфер обмена
Code:
try:
   with open("config.json", 'r') as file:
       config = json.load(file)


   if key:
       return config.get(key)
   return config

В except, если выдало ошибку о неудачном получении данных из конфига или файл конфига был не найден, сначала добавляется +1 к attempt. Затем следует проверка if на то, что если attempt больше или равен max_retries, то выдается ошибка RuntimeError с уведомлением о том, что было много попыток использовать конфиг-файл. В else (если attempt не больше max_retries) выводится обычный print о том, что не удалось использовать конфиг. После этого следует пауза длиной в значение из переменной delay. Затем будет новая попытка подключения, и так будет продолжаться, пока не будет успешный запрос или количество попыток не превысит максимально разрешенное количество.
Python: Скопировать в буфер обмена
Code:
except (FileNotFoundError, json.JSONDecodeError) as e:
   attempt += 1
   if attempt >= max_retries:
       raise RuntimeError(f"Не удалось загрузить конфигурацию после {max_retries} попыток. Ошибка: {e}")
   else:
       print(f"Ошибка при загрузке конфигурации: {e}. Попытка {attempt} из {max_retries}.")
       time.sleep(delay)  # Задержка перед повторной попыткой

С переменной обращения к конфигу закончено. Теперь в коде множество ошибок из-за того, что не найдены переменные, так как они были удалены ранее. Чтобы исправить это, вместо вызова переменных нужно вызывать функцию обращения к конфигу, передавая в неё ключ, из которого нужно получить данные. Например, если есть ошибка в строке, которая получает центр экрана по оси X путем деления ширины экрана на 2, нужно заменить эту строку на вызов функции load_config, передавая ключ для ширины экрана и вычисляя центр экрана.
Python: Скопировать в буфер обмена
center_x = screen_width // 2

Нужно заменить screen_width на вызов функции load_config с ключом screen_width.
Python: Скопировать в буфер обмена
center_x = load_config("screen_width") // 2

Так нужно проделать со всеми ошибками в коде, связанными с отсутствием переменных. Но на самом деле вычисление center_x, а также center_y и fov_x и fov_y также не понадобятся в коде, так как в дальнейшем в коде нужно будет получать значения из этих переменных. Даже если вызывать, допустим, ту же переменную center_x в каком-нибудь цикле, её значение не будет изменяться, даже если в конфиге по факту уже другое значение. Это связано с тем, что при обращении к переменной center_x она не будет каждый раз вызывать load_config, а просто будет использовать значение, которое получило впервые. Поэтому ещё раз, нужно удалить это:
Python: Скопировать в буфер обмена
Code:
center_x = screen_width // 2
center_y = screen_height // 2
fov_x = center_x - fov_width // 2
fov_y = center_y - fov_height // 2

После удаления нужно заменить все использования center_x на следующее:
Python: Скопировать в буфер обмена
load_config("screen_width") // 2

center_y на это:
Python: Скопировать в буфер обмена
load_config("screen_height") // 2

fov_x на это:
Python: Скопировать в буфер обмена
load_config("screen_width") // 2 - load_config("fov_width") // 2

fov_y на это:
Python: Скопировать в буфер обмена
load_config("screen_height") // 2 - load_config("fov_height") // 2

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

Базовая система анти отдачи​

Теперь, когда все переменные заменены на вызов получения данных из конфига, можно приступать к первой версии антиотдачи, которая будет просто смещать курсор вниз по y. Для начала нужно создать два ключа в конфиге, которые будут использоваться в будущей функции антиотдачи.
Код: Скопировать в буфер обмена
Code:
"anti_recoil_px": 1,
"anti_recoil_time_sleep": 0.001

После добавления ключей в конфиг можно приступать к написанию функции антиотдачи. Называться она будет anti_recoil, и в ней нужно будет прописать цикл while True. Внутри цикла нужно добавить проверку на нажатие левой кнопки мыши. Если левая кнопка мыши нажата, то двигать курсор по y на количество пикселей, указанных в ключе из конфига anti_recoil_px.
Python: Скопировать в буфер обмена
Code:
def anti_recoil():
   while True:
       if 0 > win32api.GetKeyState(win32con.VK_LBUTTON):
           win32api.mouse_event(win32con.MOUSEEVENTF_MOVE, 0, int(load_config("anti_recoil_px")), 0, 0)
           time.sleep(load_config("anti_recoil_time_sleep"))
Цикл в данном случае нужен для того, чтобы функция антиотдачи всегда работала и постоянно проверяла нажатие левой кнопки мыши. Однако, если вызвать эту функцию, она будет постоянно занимать главный поток, что не является желаемым поведением. Из данной ситуации есть два выхода.
  1. Выход: убрать цикл и вызывать функцию каждый раз в конце функции детекции. В таком случае функция не будет занимать поток на постоянной основе; будет происходить итерация цикла детекции, вызов функции наводки, затем вызов функции антиотдачи. После этого антиотдача сместит курсор на указанное количество пикселей по y, и начнется новая итерация цикла детекции. Данный метод плох тем, что пока будет работать функция антиотдачи, цикл функции детекции не начнется, и хотя смещение занимает не так много времени, все же это лишняя задержка, которой можно избежать. Кроме того, постоянный вызов функции или постоянная проверка на нажатие в потоке детекции также вредят скорости работы нейронной сети.
  2. Выход: оставить цикл while True, и вызвать функцию антиотдачи в отдельном потоке при запуске игры. Таким образом, не потребуется постоянно вызывать функцию и занимать поток детекции. Этот способ хорош тем, что функция будет вызвана лишь один раз, и соответственно реакция на нажатие будет быстрее, нежели в первом способе, где придется ждать, пока дойдет очередь до вызова функции. Поскольку будет использоваться отдельный поток, функция антиотдачи не будет конфликтовать с функцией детекции.
Соответственно, из этих двух вариантов будет выбран второй, и чтобы его реализовать, нужно будет в if __name__ == "__main__": вызвать функцию антиотдачи в отдельном потоке.
Python: Скопировать в буфер обмена
Code:
if __name__ == "__main__":
   anti_recoil_thread = threading.Thread(target=anti_recoil)
   anti_recoil_thread.start()
   screenshot()

На этом этапе нейросеть с простой антиотдачей закончена, и её вполне хватит для некоторых игр с несложной отдачей.

Нейросеть для CS2​

Теперь можно начинать разработку нейросети для CS2, и есть два пути её реализации:
  1. Путь: Нейросеть для CS2 будет отличаться от текущей версии полностью переписанной логикой антиотдачи, переписанной логикой наведения и переписанной логикой детекции. В связи с этими изменениями можно просто добавлять условие if в текущем коде, в котором будет проверяться выбранная игра. Таких условий придётся делать достаточно много, и некоторые из них нужно будет делать в цикле, что, хотя и немного, но скажется на скорости работы программы.
  2. Путь: Вынести обе версии нейросети в отдельные файлы, а в основном файле проекта написать функцию для проверки выбранной игры. Допустим, если выбрана игра CS2, то запускать нейросеть из файла с логикой для игры CS2. Если выбрана другая игра, то соответственно запускать логику из файла для этой другой игры. С таким способом суммарное количество кода увеличится по сравнению с первым вариантом разработки, но учитывая, что логика для каждой игры будет разделена на отдельные файлы, это является плюсом, так как позволит в дальнейшем намного проще добавлять логику для отдельных игр с различными механиками. Также количество строк без использования условий в каждой функции значительно повысит читаемость кода. Плюсом будет также то, что если одна из версий нейросети сломается при изменении, это не затронет остальные логики. Таким образом, я считаю, что данный вариант разработки позволит сделать код более структурированным и модульным, поэтому выбран именно он.

Изменение структуры проекта​

Перед созданием логики нейросети конкретно для игры CS2 потребуется изменить структуру проекта. Все версии нейросетей будут разделены на отдельные файлы и не будут храниться в основном файле. Поэтому для начала нужно создать папку для файлов с логикой для каждой игры. Основная папка для файлов нейросетей будет называться game_modules. В этой папке для каждой логики будет своя папка: для игры CS2 будет одноимённая папка, для уже реализованной версии нейросети будет папка с названием default, поскольку это стандартная версия, которая подойдёт для многих игр. Теперь внутри папок cs2 и default нужно создать одноимённые Python-файлы.

Возможность выбора версии нейросети​

После подготовки папок и файлов можно приступать к написанию кода. Для начала нужно скопировать весь код и перенести его в файл default.py, а в основном файле нужно удалить весь код и добавить такую же функцию загрузки конфига, как и в стандартной нейросети, которая только что была перенесена в файл default.py.
Python: Скопировать в буфер обмена
Code:
def load_config(key=None, max_retries=3, delay=1):
   attempt = 0


   while attempt < max_retries:
       try:
           with open("config.json", 'r') as file:
               config = json.load(file)


           if key:
               return config.get(key)
           return config


       except (FileNotFoundError, json.JSONDecodeError) as e:
           attempt += 1
           if attempt >= max_retries:
               raise RuntimeError(f"Не удалось загрузить конфигурацию после {max_retries} попыток. Ошибка: {e}")
           else:
               print(f"Ошибка при загрузке конфигурации: {e}. Попытка {attempt} из {max_retries}.")
               time.sleep(delay)  # Задержка перед повторной попыткой

Далее нужно добавить в конфиг новый ключ, который потребуется в дальнейшем для определения игры.
Код: Скопировать в буфер обмена
"game": "default",

Теперь нужно вернуться обратно в главный файл и написать ещё одну функцию. Эта функция будет проверять ключ из конфига, и в зависимости от того, какое значение будет указано, будет запускать нужную копию нейросети. Пока что реализована только простая версия, поэтому проверка будет только для неё.
Python: Скопировать в буфер обмена
Code:
def game_initialization():
   if load_config("game") == "default":
       default.start_game()
P.S. default.start_game означает, что будет вызываться функция start_game из файла default, но пока этой функции нет, и к ней нужно будет вернуться позже.

После написания функции определения игры нужно вызывать эту функцию при запуске, и для этого в главном файле нужно написать соответствующую логику.
Python: Скопировать в буфер обмена
Code:
if __name__ == "__main__":
   game_initialization()

Теперь можно вернуться к default.start_game. Для того чтобы нейросеть для игры запустилась, нужно зайти в файл default и создать функцию start, которая будет запускать функцию создания скриншотов и антиотдачи. Раньше это делала эта часть кода:
Python: Скопировать в буфер обмена
Code:
if __name__ == "__main__":
   anti_recoil_thread = threading.Thread(target=anti_recoil)
   anti_recoil_thread.start()
   screenshot()

Теперь же это будет выглядеть так:
Python: Скопировать в буфер обмена
Code:
def start_game():
   screenshot_thread = threading.Thread(target=screenshot)
   screenshot_thread.start()
   anti_recoil_thread = threading.Thread(target=anti_recoil)
   anti_recoil_thread.start()
P.S. Запуск функции создания скриншотов было решено сделать в отдельный поток для того, чтобы в дальнейшем, при большой структуре проекта, ничего не мешало, если вдруг что-то будет использовать основной поток на постоянной основе. Теперь можно запустить главный файл, и при его запуске будет вызвана функция инициализации игры. Эта функция проверяет ключ game, и если он совпадает с названием default, то будет вызвана функция start_game из файла default.py. В свою очередь, внутри функции start_game будет вызвана функция антиотдачи и функция создания скриншотов, внутри которой в цикле while создаются скриншоты и вызывается функция детекции, в которую передается скриншот.

Подготовка нейросети для cs2​

Теперь можно начать писать логику для нейросети cs2. Для начала можно скопировать код стандартной нейросети и вставить его в Python-файл cs2.py. Затем нужно добавить новые ключи со значениями в конфиг. Половина ключей и их значения идентичны стандартной версии нейронной сети, поэтому можно просто скопировать их и дописать в начале каждого ключа приписку cs2_. Это сделано для того, чтобы абсолютно каждый параметр был отдельно настраиваемым для каждой игры.
Код: Скопировать в буфер обмена
Code:
"cs2_screen_width": 1366,
"cs2_screen_height": 768,
"cs2_fov_width": 250,
"cs2_fov_height": 250,
"cs2_aim_step": 1,
"cs2_aim_time_sleep": 0.001,
"cs2_aim_target": 0.38,

После добавления ключей в конфиг нужно в самом файле cs2.py изменить все упоминания старых ключей на новые с припиской cs2, но не считая ключей anti_recoil_px и anti_recoil_time_sleep, так как в дальнейшем они не понадобятся в функции антиотдачи. Тем не менее, для проверки нейронной сети они пока должны использовать значения для простой нейронной сети.

После изменения вызываемых из конфига ключей нужно перейти в основной файл проекта и добавить в функцию инициализации игры еще одно условие для проверки ключа на название cs2.
Python: Скопировать в буфер обмена
Code:
def game_initialization():
   if load_config("game") == "default":
       default.start_game()
   if load_config("game") == "cs2":
       cs2.start_game()

Теперь можно указать в конфиге внутри ключа game значение cs2 и запустить нейросеть. При запуске нейросети в консоль дважды будет выводиться уведомление об инициализации CUDA. Это связано с тем, что инициализация CUDA и модели обучения присутствуют в обеих версиях нейронной сети и находятся вне какой-либо функции. Если код находится вне функции, он инициализируется при запуске программы, что приводит к повторной инициализации. Из этой дилеммы есть два выхода:
  1. Перенести инициализацию CUDA и инициализацию модели обучения в отдельный файл, в котором перед инициализацией будет проверяться ключ game, и инициализировать конкретный файл с обученной моделью для конкретной игры.
  2. Оставить в каждой из версий нейронной сети инициализацию CUDA и модели обучения для конкретной игры без проверок ключей, но вынести это в отдельную функцию, которую можно вызывать в самом начале функции скриншота.
Оба варианта приемлемы, но было решено использовать второй вариант, так как он более удобен для меня. Вот как это теперь выглядит на примере версии нейронной сети для cs2:
Python: Скопировать в буфер обмена
Code:
def cuda_initialization():
   global model
   # Определение устройства (GPU или CPU)
   device = 'cuda' if torch.cuda.is_available() else 'cpu'
   if torch.cuda.is_available():
       print("Используется CUDA")
   else:
       print("Используется CPU")


   model = YOLO(os.path.abspath("models\\cs2_model.pt"))
   model.to(device)
   print("Запущена нейронная сеть для игры CS2")




def screenshot():
   cuda_initialization()
   camera = dxcam.create()


   while True:
       frame = camera.grab(region=(load_config("cs2_screen_width") // 2 - load_config("cs2_fov_width") // 2,
                                   load_config("cs2_screen_height") // 2 - load_config("cs2_fov_height") // 2,
                                   load_config("cs2_screen_width") // 2 - load_config("cs2_fov_width") // 2 +
                                   load_config("cs2_fov_width"), load_config("cs2_screen_height") // 2 -
                                   load_config("cs2_fov_height") // 2 + load_config("cs2_fov_height")))
       if frame is None:
           continue
       detection(frame)
P.S. Переменная model в функции инициализации CUDA сделана глобальной, так как она используется в других функциях и должна быть для них доступна.

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

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

Программа для конвертации макросов​

Python: Скопировать в буфер обмена
Code:
input_data = """


"""
В данную переменную будут записываться макросы X7 в следующем формате:
Python: Скопировать в буфер обмена
Code:
input_data = """Delay 20 ms
LeftUp 1
Delay 20 ms
LeftDown 1
Delay 20 ms
MoveR 0 6
Delay 20 ms
MoveR 0 6
Delay 20 ms
MoveR 0 6
Delay 20 ms
MoveR 0 6
"""

Затем нужно создать пустой список, в котором в дальнейшем будет находиться конвертированный макрос, а также создать переменную Delay = 0 (в дальнейшем значение будет меняться).
Python: Скопировать в буфер обмена
Code:
macros = []


delay = 0

После этого нужно создать цикл for, который будет проходить по каждой строке в переменной с макросом x7.
Python: Скопировать в буфер обмена
for line in input_data.split('\n'):

Внутри цикла нужно создать проверку на то, что строка из переменной не пустая.
Python: Скопировать в буфер обмена
if line.strip():

Далее, внутри if нужно брать полученную строку и разделить её на элементы, используя пробелы. Затем добавить проверку на то, что если первый элемент — это строка Delay, то нужно взять второй элемент (второй элемент — это число, например 20 ms) и разделить его на 1000, чтобы получить миллисекунды в формате, который понимает Python, и записать полученное значение в переменную.

Затем нужно добавить похожую проверку, но на слово MoveR. Если первый элемент — это MoveR, то остальные элементы — это координаты. Нужно взять эти координаты и преобразовать их в int, затем записать их в переменные x и y. Далее нужно записать получившиеся переменные x, y, и delay в пустой словарь macros.
Python: Скопировать в буфер обмена
Code:
elif parts[0] == 'MoveR':
   x, y = map(int, parts[1:])
   macros.append((x, y, delay))

После того как в словарь были записаны данные, нужно взять этот словарь и записать его в txt файл, используя file.writelines.
Python: Скопировать в буфер обмена
Code:
with open('макрос.txt', 'w') as file:
   file.writelines(str(macros))

На этом написание конвертера макросов закончено. В итоге, если конвертировать макрос x7, пример которого был выше, получится что-то подобное:
[(0, 8, 0.098), (5, 21, 0.097), (-5, 26, 0.098), (0, 23, 0.1)]

Теперь нужно хранить этот макрос, чтобы в дальнейшем вызывать его в коде. Для этого был создан файл с названием cs2_macros_list.py, который помещен в папку cs2, где находится логика нейросети для одноименной игры. Внутри этого файла нужно создать переменную, в которой будет находиться макрос. Название переменной будет соответствовать оружию, для которого этот макрос предназначен.
ak_47 = [(0, 8, 0.098), (5, 21, 0.097), (-5, 26, 0.098), (0, 23, 0.1)]

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

Теперь можно приступить к реализации новой логики анти-отдачи, и первым шагом нужно добавить новые ключи со значениями в конфигурационный файл.
Код: Скопировать в буфер обмена
Code:
"cs2_macros_gun": "ak_47",
"cs2_macros_ak_47_adjustment": 0.25,
"cs2_macros_famas_adjustment": 0.4,
"cs2_macros_m4a1s_adjustment": 0.3,
"cs2_macros_m4a4_adjustment": 0.3,
"cs2_macros_mac_10_adjustment": 0.35,
"cs2_macros_sg_553_adjustment": 0.5
Первый ключ нужен для определения выбранного оружия, остальные ключи необходимы для увеличения или уменьшения значений макросов по x и y на значение из этих ключей.

P.S. Так как макросы являются довольно примитивными, результат их использования на разных системах с разным DPI, разрешением и чувствительностью мыши в игре может сильно варьироваться. У кого-то макрос будет стрелять в точку, у кого-то выше, у кого-то ниже. Поэтому будет возможность уменьшать или увеличивать силу смещения по координатам, что позволит каждому настроить макросы под себя и достичь хорошего результата.

Продвинутая анти отдача​

Теперь можно переписывать функцию анти-отдачи. Для начала нужно удалить все из функции и первым делом создать пустой словарь, в котором в дальнейшем будут храниться макросы. Затем нужно создать цикл for, который будет получать все переменные и их значения из cs2_macros_list.py и записывать их в переменную weapon.
Python: Скопировать в буфер обмена
Code:
macros_dict = {}


for weapon in dir(cs2_macros_list):

Затем нужно записывать эти данные (переменные и их значения в виде координат макроса) в ранее созданный словарь.
Python: Скопировать в буфер обмена
macros_dict[weapon] = getattr(cs2_macros_list, weapon)
P.S. getattr(cs2_macros_list, weapon) возвращает значение переменной weapon из файла cs2_macros_list, то есть возвращает координаты и записывает их в словарь, где ключом является название переменной weapon.

После этого нужно создать цикл while, чтобы функция анти-отдачи работала постоянно. Внутри цикла while нужно создать цикл for, в котором будет браться ключ из словаря macros_dict с названием оружия, указанным в конфигурационном файле из cs2_macros_gun (названия ключей в файле с макросами такие же, как названия из конфигурационного файла). Из этого ключа в файле с макросами будут браться значения x, y, и delay.
Python: Скопировать в буфер обмена
Code:
# Основной цикл антиотдачи
while True:
   for x, y, delay in macros_dict[load_config("cs2_macros_gun")]:

Затем, внутри цикла for нужно умножать x и y на число, указанное в ключе из конфигурационного файла, в котором указано значение для усиления или уменьшения силы отдачи для конкретного оружия.
Python: Скопировать в буфер обмена
Code:
x = round(x * load_config(f"cs2_macros_{load_config('cs2_macros_gun')}_adjustment"))
y = round(y * load_config(f"cs2_macros_{load_config('cs2_macros_gun')}_adjustment"))
P.S. Допустим, что выбрано оружие ak_47. В цикле while будет браться макрос, который хранится в ключе ak_47. Затем x и y будут умножаться на значение из ключа в конфиге под названием cs2_macros_ak_47_adjustment. То есть получается, что "cs2_macros_{load_config('cs2_macros_gun')}_adjustment" это cs2_macros_ak_47_adjustment. Таким образом, если выбрано конкретное оружие, макрос и переменная для умножения всегда будут подходить друг к другу.

Далее, в том же цикле for нужно добавить проверку на нажатие левой кнопки мыши.
Python: Скопировать в буфер обмена
if 0 > win32api.GetKeyState(win32con.VK_LBUTTON):

Далее нужно создать 2 переменные, к которым будут прибавляться x и y (позже будет объяснено, для чего это нужно).
Python: Скопировать в буфер обмена
Code:
anti_recoil_x += x
anti_recoil_y += y

После этого нужно указать паузу, используя значение из delay, которое было получено из макроса.
Python: Скопировать в буфер обмена
time.sleep(delay)

Затем, в else нужно обнулять значения переменных. else будет срабатывать, когда левая кнопка мыши будет отпущена.
Python: Скопировать в буфер обмена
Code:
else:
   anti_recoil_x = 0
   anti_recoil_y = 0
   break

Теперь в начале функции нужно сделать эти переменные глобальными.
Python: Скопировать в буфер обмена
global anti_recoil_x, anti_recoil_y

И затем нужно вне функции назначить этим переменным значение 0.
Python: Скопировать в буфер обмена
Code:
anti_recoil_x = 0
anti_recoil_y = 0


def anti_recoil():
   global anti_recoil_x, anti_recoil_y

Объяснение логики стрельбы в cs2 и новой анти отдачи​

Для чего нужна вся эта работа с переменной? Дело в том, что стрельба в CS2 не совсем стандартная. Обычно в шутерах при стрельбе прицел уходит вверх вслед за пулями, но в CS2 прицел всегда остается в центре, а пули летят выше. То есть, если бы использовалась система анти-отдачи и наводки, как в стандартной нейросети, то нейросеть бы наводила прицел на объект, а пули летели бы выше, над головой игрока. Поэтому в версии нейросети для CS2 прицел не будет просто наводиться на центр объекта; он будет наводиться на центр объекта, который будет смещаться относительно координат из макроса. Допустим, чтобы центр объекта оказался в нужной позиции, нужно передвинуть мышь на 5 пикселей по x и на 1 пиксель по y. При нажатии левой кнопки мыши курсор смещается на эти координаты, и происходит выстрел. Затем, не отпуская левую кнопку мыши, происходит второй выстрел, но курсор остается в центре объекта, а пуля летит на 5 пикселей выше центра объекта. Чтобы пули летели в центр объекта, а не выше на 5 пикселей, нужно сместить центр объекта по y на 5 пикселей ниже. Таким образом, центр объекта по y для программы будет ниже на 5 пикселей от реального центра, но пули будут стрелять в реальный центр.

Учитывая все вышесказанное, переменные нужны для передачи координат макроса при нажатии левой кнопки мыши. Эти координаты будут прибавляться к чистым координатам для перемещения прицела. Если левая кнопка мыши отпущена, функция наведения не будет работать, а значения переменных, передаваемых из анти-отдачи, обнуляются. При повторном нажатии левой кнопки мыши цикл повторится, и макрос начнет передавать свои координаты по новой, начиная сначала.

Также текущая функция анти-отдачи отличается от стандартной тем, что в самой функции нет перемещения курсора. Если курсор пытался бы смещаться по координатам в функции анти-отдачи и также смещаться на эти же координаты в функции наводки, это бы ухудшило точность. Поэтому в текущей реализации анти-отдача работает только тогда, когда обнаружен объект, так как функция наводки вызывается именно тогда, когда есть обнаруженные объекты. Бывает так, что вдалеке нейросеть не распознает объекты, и в итоге даже анти-отдача не сработает, но в CS2 это не критично, так как графика в игре не сложная, эффектов мало, а карты не самые длинные. Поэтому такое бывает очень редко и не критично.

По сути, в нейросетях, где стандартная отдача, а не как в CS2, когда при стрельбе прицел поднимается, анти-отдача не нужна, так как при поднятии прицела выше центра объекта нейросеть сама его вернет назад. Однако проблема в том, что скорость стрельбы, как правило, в играх выше скорости обработки и детекции скриншотов нейросетью. Соответственно, пока нейросеть поймет, что прицел стал выше, уже успеет произойти выстрел и после него еще один выстрел. Добавив функцию анти-отдачи в отдельном потоке, скорость обработки данных нейросетью компенсируется скоростью работы отдельной функции, задачей которой является просто перемещение курсора по заготовленным координатам. Использовать такой метод, как в CS2, для других нейросетей считаю бессмысленным, так как во-первых, в других играх случаев, когда нейросеть не определяет объект, может быть гораздо больше, и в такие моменты выручает хотя бы анти-отдача, а во-вторых, скорость срабатывания анти-отдачи отдельно от функции наводки компенсирует скорость детекции.

После того как была закончена функция анти-отдачи и пояснено, для чего и почему вся её логика устроена именно так, можно перейти к изменению функции наводки. Вот её текущая реализация, которая ничем не отличается от функции наводки из первой статьи:
Python: Скопировать в буфер обмена
Code:
def aim(dx, dy):
   if 0 > win32api.GetKeyState(win32con.VK_LBUTTON):
       aim_step_x = dx / load_config("cs2_aim_step")
       aim_step_y = dy / load_config("cs2_aim_step")


       for i in range(load_config("cs2_aim_step")):
           win32api.mouse_event(win32con.MOUSEEVENTF_MOVE, int(aim_step_x), int(aim_step_y), 0, 0)
           time.sleep(load_config("cs2_aim_time_sleep"))

А вот её новая реализация:
Python: Скопировать в буфер обмена
Code:
def aim(dx, dy):
   dx += anti_recoil_x
   dy += anti_recoil_y
 
   if 0 > win32api.GetKeyState(win32con.VK_LBUTTON):
       aim_step_x = dx / load_config("cs2_aim_step")
       aim_step_y = dy / load_config("cs2_aim_step")


       for i in range(load_config("cs2_aim_step")):
           win32api.mouse_event(win32con.MOUSEEVENTF_MOVE, int(aim_step_x), int(aim_step_y), 0, 0)
           time.sleep(load_config("cs2_aim_time_sleep"))
Как видно, к координатам смещения курсора прибавляются значения из функции анти-отдачи.

На этом написание логики наводки и анти-отдачи готово, и теперь можно добавить возможность переключения объектов для детекции, то есть контр-террористов или террористов.

Выбор объектов для детекции​

Первым делом понадобится создать новый ключ со значениями в конфиге.
Код: Скопировать в буфер обмена
"cs2_obj_detection": [0],

Если значение [0], то будет наводиться на объект, который был пронумерован в датасете первым. Если [1], то на второй объект. Если [0,1], то будет наводиться на первый и второй объект, и если будут еще объекты в датасете, то получится [0,1,2,3] и т.д. Далее нужно перейти к функции детекции и немного её дополнить. Раньше в функции детекции брался первый обнаруженный объект, и далее шли вычисления центра объекта и т.д. Теперь будут браться все обнаруженные объекты, используя цикл for, и внутри цикла каждый из объектов будет проверяться через условие if на соответствие id объекта с id, указанным в конфиге. Если объект соответствует id, указанному в конфиге, тогда будет происходить логика вычисления центра объекта относительно экрана и т.д.

Было:
Python: Скопировать в буфер обмена
Code:
def detection(frame):
   results = model.predict(frame, verbose=False)[0]


   if len(results.boxes) > 0:
       box = results.boxes[0]
       box_cord = box.xyxy[0].tolist()
       center_x_obj = (box_cord[0] + box_cord[2]) / 2
       center_y_obj = (box_cord[1] + box_cord[3]) / 2
       center_x_obj += load_config("cs2_screen_width") // 2 - load_config("cs2_fov_width") // 2
       center_y_obj += load_config("cs2_screen_height") // 2 - load_config("cs2_fov_height") // 2
       center_y_obj -= (box_cord[3] - box_cord[1]) * load_config("cs2_aim_target")
       dx = center_x_obj - load_config("cs2_screen_width") // 2
       dy = center_y_obj - load_config("cs2_screen_height") // 2
       aim(dx, dy)

Стало:
Python: Скопировать в буфер обмена
Code:
def detection(frame):
   results = model.predict(frame, verbose=False)[0]


   if len(results.boxes) > 0:
       for box in results[0].boxes:
           if box.cls[0].item() in load_config("cs2_obj_detection"):
               box_cord = box.xyxy[0].tolist()
               center_x_obj = (box_cord[0] + box_cord[2]) / 2
               center_y_obj = (box_cord[1] + box_cord[3]) / 2
               center_x_obj += load_config("cs2_screen_width") // 2 - load_config("cs2_fov_width") // 2
               center_y_obj += load_config("cs2_screen_height") // 2 - load_config("cs2_fov_height") // 2
               center_y_obj -= (box_cord[3] - box_cord[1]) * load_config("cs2_aim_target")
               dx = center_x_obj - load_config("cs2_screen_width") // 2
               dy = center_y_obj - load_config("cs2_screen_height") // 2
               aim(dx, dy)

На этом написание самой нейросети закончено, и что получается в итоге:
  1. Переписана структура всего проекта, что делает код более модульным и лёгким в плане добавления нового функционала.
  2. Возможность менять значения прямо на лету, не перезапуская каждый раз проект.
  3. Добавлена простая анти-отдача.
  4. Добавлена более продвинутая механика анти-отдачи для игры CS2.
  5. Возможность переключать детекцию с одного объекта на другой.
  6. Дополнительная программа для конвертации макросов от мышек x7 в формат для текущего проекта.

Написание интерфейса​

Теперь можно реализовать интерфейс для управления настройками под каждую игру, чтобы это было ещё проще и быстрее. На Python я знаком только с Eel и Flask, также одним из вариантов разработки для меня был Windows Forms на C#. Eel мне показался более сложным, нежели Flask, а писать интерфейс на Windows Forms в принципе показалось плохой идеей, потому что, ну, потому что зачем? Поэтому был выбран Flask.

Что такое Flask​

Что такое Flask, мне кажется, знают многие, и особо рассказывать об этом не вижу смысла, поэтому скажу кратко: это фреймворк для Python, который позволяет писать веб-часть приложения на HTML+JS и взаимодействовать с Python частью.

Подготовка проекта​

Перед началом написания кода нужно подготовить проект. Для этого нужно создать папку templates, в которой будут храниться HTML-файлы. В ней нужно создать файл index.html. Затем нужно создать Python-файл с названием flask_initialization.py, в котором будет происходить инициализация Flask, а также замена значений из конфига значениями с веб-части интерфейса.

Python часть интерфейса​

После подготовки проекта можно приступать к написанию кода. Сначала будет написана логика на Python, а затем уже веб-часть интерфейса. Для начала нужно зайти в файл flask_initialization.py и импортировать Flask (перед этим, разумеется, нужно скачать библиотеку Flask через pip install).
Python: Скопировать в буфер обмена
from flask import Flask, render_template, request, jsonify

Далее нужно инициализировать Flask.
Python: Скопировать в буфер обмена
app = Flask(__name__)

Теперь нужно указать маршрут для страницы интерфейса.
Python: Скопировать в буфер обмена
Code:
@app.route('/')
def index():
   return render_template('index.html')

P.S. Если указать в @app.route('/') слеш, то при открытии обычного адреса сайта откроется конкретно страница index. Если указать /index, то страница интерфейса откроется только при переходе на https://адрес_сайта/index.

Далее нужна функция load_config, точно такая же, как и в файлах нейросетевой логики.
Python: Скопировать в буфер обмена
Code:
def load_config(key=None, max_retries=3, delay=1):
   attempt = 0


   while attempt < max_retries:
       try:
           with open("config.json", 'r') as file:
               config = json.load(file)


           if key:
               return config.get(key)
           return config


       except (FileNotFoundError, json.JSONDecodeError) as e:
           attempt += 1
           if attempt >= max_retries:
               raise RuntimeError(f"Не удалось загрузить конфигурацию после {max_retries} попыток. Ошибка: {e}")
           else:
               print(f"Ошибка при загрузке конфигурации: {e}. Попытка {attempt} из {max_retries}.")
               time.sleep(delay)
P.S. json.load(f) считывает данные из конфига.

Далее нужна функция с маршрутом для Flask, работающая с GET-запросами. Эта функция необходима для того, чтобы в дальнейшем веб-часть могла обращаться к ней и получать значения всех параметров из конфигурационного файла для их отображения в веб-интерфейсе.
Python: Скопировать в буфер обмена
Code:
@app.route('/get_config', methods=['GET'])
def get_config():
   return jsonify(load_config()), 200
Получается, что при запросе к этой функции она возвращает результат работы функции load_config, в логике которой уже происходит получение данных из конфигурационного файла.

Теперь нужна функция с маршрутом для Flask, работающая с POST-запросами. В эту функцию будут передаваться значения из веб-интерфейса, и затем эти значения будут заменять собой значения в конфигурационном файле.
Python: Скопировать в буфер обмена
Code:
@app.route('/update_config', methods=['POST'])
def update_config():

Внутри функции нужно инициализировать переменную, которая будет вызывать функцию load_config для открытия конфига и чтения из него данных.
Python: Скопировать в буфер обмена
config_data = load_config()

Затем нужно назначить каждому ключу из конфига новое значение, которое будет получено из веб-части интерфейса.
Python: Скопировать в буфер обмена
Code:
config_data['game'] = request.json['game']
config_data['screen_width'] = int(request.json['screen_width'])
config_data['screen_height'] = int(request.json['screen_height'])
config_data['fov_width'] = int(request.json['fov_width'])
config_data['fov_height'] = int(request.json['fov_height'])
config_data['aim_step'] = int(request.json['aim_step'])
config_data['aim_time_sleep'] = float(request.json['aim_time_sleep'])
config_data['aim_target'] = float(request.json['aim_target'])
config_data['anti_recoil_px'] = int(request.json['anti_recoil_px'])
config_data['anti_recoil_time_sleep'] = float(request.json['anti_recoil_time_sleep'])
config_data['cs2_screen_width'] = int(request.json['cs2_screen_width'])
config_data['cs2_screen_height'] = int(request.json['cs2_screen_height'])
config_data['cs2_fov_width'] = int(request.json['cs2_fov_width'])
config_data['cs2_fov_height'] = int(request.json['cs2_fov_height'])
config_data['cs2_aim_step'] = int(request.json['cs2_aim_step'])
config_data['cs2_aim_time_sleep'] = float(request.json['cs2_aim_time_sleep'])
config_data['cs2_aim_target'] = float(request.json['cs2_aim_target'])
config_data['cs2_obj_detection'] = request.json['cs2_obj_detection']
config_data['cs2_macros_gun'] = request.json['cs2_macros_gun']
config_data['cs2_macros_ak_47_adjustment'] = float(request.json['cs2_macros_ak_47_adjustment'])
config_data['cs2_macros_famas_adjustment'] = float(request.json['cs2_macros_famas_adjustment'])
config_data['cs2_macros_m4a1s_adjustment'] = float(request.json['cs2_macros_m4a1s_adjustment'])
config_data['cs2_macros_m4a4_adjustment'] = float(request.json['cs2_macros_m4a4_adjustment'])
config_data['cs2_macros_mac_10_adjustment'] = float(request.json['cs2_macros_mac_10_adjustment'])
config_data['cs2_macros_sg_553_adjustment'] = float(request.json['cs2_macros_sg_553_adjustment'])

P.S. request.json означает, что берутся данные из запроса с веб-интерфейса. То есть Flask отправляет на Python часть JSON-объекта, внутри которого находятся ключи со значениями для каждой из функций. Допустим, request.json['cs2_aim_time_sleep'] берет из полученного от веб-части JSON ключ с названием cs2_aim_time_sleep, и его значение записывается в ключ в JSON-файле конфига, то есть в config_data['cs2_aim_time_sleep'].

Далее нужно перезаписать данные в конфиге.
Python: Скопировать в буфер обмена
Code:
with open('config.json', 'w') as f:
   json.dump(config_data, f, indent=4)
P.S. json.dumps перезаписывает данные.

Теперь, после перезаписи данных в конфиге, нужно возвращать серверу ответ. В данном случае возвращаться будут обновленные данные config_data.
Python: Скопировать в буфер обмена
return jsonify(config_data), 200

Веб часть интерфейса​

С частью логики на Python закончено, и можно приступить к написанию веб-части на HTML. Так как файл HTML был заготовлен ранее, этап его создания можно пропустить. При открытии файла будет уже базовый шаблон (если файл был создан в среде PyCharm).
HTML: Скопировать в буфер обмена
Code:
<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <title>Configuration</title>
</head>
<body>


</body>
</html>

Все объекты для управления параметрами будут находиться внутри body, и первое, что нужно сделать, это создать форму, в которой будут находиться все остальные объекты.
HTML: Скопировать в буфер обмена
Code:
<form id="config-form">


</form>
P.S. Очень важно обратить внимание на то, что у объекта формы указан id. В дальнейшем этот id понадобится для логики на JavaScript.

Теперь в этой форме нужно расположить объекты для всех значений из конфига, которые нужно редактировать. Все эти объекты однотипны, но несколько из них стоит рассмотреть подробнее. Например, для объекта с выбором игры будут использоваться выпадающие списки с готовыми вариантами, такие объекты обычно называют селекторами.
HTML: Скопировать в буфер обмена
Code:
<select id="game" name="game">
   <option value="cs2">CS2</option>
   <option value="default">DEFAULT</option>
</select>

Как видно в коде выше, у селектора также есть id. id в принципе должен быть у каждого объекта, к которому планируется потом обращаться. Также внутри селектора находятся объекты <option>, которые составляют выпадающий список. В value указано значение, а после value находится текст, который виден на странице.

Кроме того, есть объект, отображающий текст на странице.
HTML: Скопировать в буфер обмена
<label for="game">Game:</label>

P.S. Это просто текст; таких объектов в HTML достаточно на любой вкус, и перечислять все эти виды текста нет смысла.

В данной реализации интерфейса также присутствует такой тип объектов, как input типа number. Это числовой объект, в который можно вводить числа. Вводить их можно как с помощью клавиатуры, так и с помощью встроенных в этот объект кнопок повышения и уменьшения значения.

HTML: Скопировать в буфер обмена
<input type="number" step="0.001" id="aim_time_sleep" name="aim_time_sleep">
P.S. Также у этого объекта добавлен параметр step. Этот параметр отвечает за то, на сколько будет увеличиваться или уменьшаться значение, если будут использованы встроенные в этот объект кнопки.

Также можно рассмотреть еще один объект типа селектор. Он абсолютно такой же, как и рассмотренный выше, за исключением значений в value.
HTML: Скопировать в буфер обмена
Code:
<select id="cs2_obj_detection" name="cs2_obj_detection">
   <option value="[0]">CT</option>
   <option value="[1]">T</option>
   <option value="[0, 1]">CT, T</option>
</select>
В данном объекте значения находятся в квадратных скобках, так как нужно, чтобы эти данные записывались в конфиг в виде массива чисел, а не просто чисел. Для того чтобы они были в виде массива, в дальнейшем в JavaScript будет отдельная строка кода, которая будет обрабатывать это.

С разбором объектов закончено, теперь нужно написать логику для взаимодействия веб-части с частью логики на Python. Чтобы в HTML вписать JavaScript-код, нужно указать, что это именно JavaScript.
HTML: Скопировать в буфер обмена
Code:
<script>
 
</script>

Первой функцией, которая будет написана на JavaScript, будет функция загрузки данных из конфига и запись полученных значений в объекты на странице.
JavaScript: Скопировать в буфер обмена
async function loadConfig() {
P.S. Данная функция асинхронна, так что она не будет блокировать никакую другую логику на веб-странице.

Далее, внутри этой функции нужно отправить запрос к функции get_config, которая находится в Python-файле инициализации Flask (у этой функции указан маршрут /get_config, принимающий запросы типа GET).
JavaScript: Скопировать в буфер обмена
const response = await fetch('/get_config');

Далее нужно получать ответ от этой функции обратно на веб-страницу. Напомню, что в ответ на веб-страницу идут текущие данные из конфига. После получения данных они записываются в переменную config.
JavaScript: Скопировать в буфер обмена
const config = await response.json();

Далее нужно назначать каждому объекту страницы с конкретным id значение из полученного от Python ответа, в котором находится ключ с таким же названием, как и id объекта на странице.
JavaScript: Скопировать в буфер обмена
document.getElementById('game').value = config.game || '';
P.S. || '' означает, что если в полученном ответе значение для нужного объекта пустое, то в объект на странице будет записан пустой массив.

Далее идут однотипные строки, как та, что указана выше, для каждого объекта на странице, за исключением объекта, где находится именно массив чисел.
JavaScript: Скопировать в буфер обмена
document.getElementById('cs2_obj_detection').value = JSON.stringify(config.cs2_obj_detection || []);
P.S. JSON.stringify означает, что данные будут конвертированы в строку, так как для объекта на странице нужна именно строка, а не массив, как в конфиге.

С функцией получения данных из конфига закончено, теперь нужно написать функцию для обновления данных в конфиге.
JavaScript: Скопировать в буфер обмена
async function updateConfig() {
P.S. Функция, как и первая, асинхронна, и поэтому не будет блокировать поток.

Далее внутри функции нужно создать JavaScript-объект с названием data. Затем в конкретные ключи этого объекта назначаются значения из объектов на странице с конкретным id (именно для этого к каждому объекту был назначен id).
JavaScript: Скопировать в буфер обмена
game: document.getElementById('game').value,

Далее идут опять же однотипные строки для каждого объекта на странице, за исключением объекта селектора с массивом данных. Так как эта функция создана для того, чтобы отправлять данные на Python-часть для записи в конфиг, а не наоборот, как было в предыдущей функции, то здесь нужно конвертировать значение не в строку, а наоборот — из строки в массив.
JavaScript: Скопировать в буфер обмена
cs2_obj_detection: JSON.parse(document.getElementById('cs2_obj_detection').value),

Далее нужно отправить JavaScript-объект на Python-часть по маршруту /update_config.
Python: Скопировать в буфер обмена
Code:
await fetch('/update_config', {
 
});

Внутри нужно указать тип запроса, а именно POST.
JavaScript: Скопировать в буфер обмена
method: 'POST',

Далее нужно указать, что данные отправляются в формате JSON.
JavaScript: Скопировать в буфер обмена
Code:
headers: {
   'Content-Type': 'application/json'
},

Теперь нужно указать тело запроса (данные, которые будут отправлены).
JavaScript: Скопировать в буфер обмена
body: JSON.stringify(data)
P.S. JSON.stringify преобразует JavaScript-объект data в строку формата JSON.

С функцией отправки данных на Python-часть закончено. Теперь нужно сделать так, чтобы данные на странице обновлялись при открытии и перезапуске страницы, подхватывая данные из конфига. Для этого нужно создать событие, которое будет вызываться при открытии страницы.
JavaScript: Скопировать в буфер обмена
Code:
document.addEventListener("DOMContentLoaded", function() {
 
});
P.S. DOMContentLoaded как раз и означает, что код будет запускаться при открытии страницы.

Далее нужно вызывать первую написанную в JavaScript функцию, а именно, функцию load_config, чтобы отправлять запрос на Python и получать свежие данные из конфига.
JavaScript: Скопировать в буфер обмена
loadConfig();

Теперь нужно создать константную переменную, в которую будет записан id объекта form.
JavaScript: Скопировать в буфер обмена
const form = document.getElementById("config-form");

Далее нужно вызвать событие, которое будет срабатывать при взаимодействии с объектом input внутри объекта form (const form).
JavaScript: Скопировать в буфер обмена
form.addEventListener('input', updateConfig);
P.S. Если произошло взаимодействие, будет вызываться функция updateConfig из JavaScript.

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

Trigger bot​

Для начала нужно обозначить несколько переменных в конфиге со значениями.
Код: Скопировать в буфер обмена
Code:
"cs2_trigger_bot": true,
"cs2_trigger_bot_zone": 1,
"cs2_trigger_bot_time_sleep": 0.15,
С первым и третьим параметром все понятно, а насчет второго — это значение, которое будет прибавляться к размерам бокса объекта в зоне которого начнет стрелять программа. Добавил этот параметр, потому что показалось, что стандартный размер не совсем удобен, и когда персонаж стоит боком, триггер-бот срабатывает хуже.

Теперь нужно создать функцию trigger_bot, в которую будут передаваться координаты бокса объекта из функции детекции.
Python: Скопировать в буфер обмена
def trigger_bot(box_cord):
P.S. box_cord — это box.xyxy[0].tolist().

Как устроена система координат​

В предыдущей статье был показан код с работой над этими координатами бокса, но не было объяснено, что это за стороны и как они выглядят. Причина этому проста — я сам не до конца понимал, как это устроено, поэтому не решился высказывать ложные объяснения. Сейчас же все стало понятнее.

box.xyxy[0].tolist() — это список координат объекта. Первые xy — это левая верхняя точка бокса, вторые xy — это правая нижняя точка бокса. Я в графики не умею, поэтому нарисовал, как смог:
1726193084239.png


Что стоит держать в уме, разбираясь в моем недо-графике и в принципе в этих координатах:
  1. Начальная координата по x и y идет из левого верхнего угла экрана.
  2. Верх y — это визуально низ экрана, а низ y — это визуально верх экрана.
  3. Когда речь идет о верхней точке бокса, это означает нижнюю точку y, и аналогично с нижней точкой бокса, которая является верхней точкой y, то есть с большим значением y.
Когда теория стала понятнее, нужно будет применить полученные знания на практике. Внутри функции нужно добавить проверку на то, чтобы центр fov по xy был внутри бокса объекта.
Python: Скопировать в буфер обмена
Code:
if ((box_cord[0] - load_config("cs2_trigger_bot_zone")) < (load_config("fov_width") // 2)
       < (box_cord[2] + load_config("cs2_trigger_bot_zone")) and
       (box_cord[1] - load_config("cs2_trigger_bot_zone")) < (load_config("fov_height") // 2)
       < (box_cord[3] + load_config("cs2_trigger_bot_zone"))):

В данном коде присутствует вычисление центра fov, чтобы он и был внутри объекта при проверке.

Вычисление центра fov по x: load_config("fov_width") // 2
Вычисление центра fov по y: load_config("fov_height") // 2

Также в данном коде используется переменная из конфига для увеличения бокса во все стороны:
  • box_cord[0] - load_config("cs2_trigger_bot_zone") смещает левую точку x бокса влево.
  • box_cord[2] + load_config("cs2_trigger_bot_zone") смещает правую точку x бокса вправо.
  • box_cord[1] - load_config("cs2_trigger_bot_zone") смещает верхнюю точку y бокса вверх, то есть уменьшает значение координат.
  • box_cord[3] + load_config("cs2_trigger_bot_zone") смещает нижнюю точку y бокса вниз, то есть увеличивает значение координат, тем самым снижает в нижнюю часть экрана.
Теперь, держа это все в уме, можно приступить к расчетам, чтобы понять, находится ли центр fov по xy внутри бокса или нет. Чтобы выяснить, находится ли центр fov по y внутри бокса, нужно, чтобы значение центра fov по y было больше значения верхнего угла бокса. Если значение больше, значит, fov по y находится ниже верхнего угла бокса. В то же время значение fov по y должно быть меньше значения нижнего угла бокса, то есть визуально выше нижней грани бокса. Похожим методом вычисляется, находится ли центр fov по x. То есть центр fov по x должен быть больше значения левой стороны бокса, а значит правее, и также центр fov по x должен быть меньше значения правой стороны бокса, а значит левее.

Далее в функции идет самое простое — просто эмуляция нажатия левой кнопки мыши и затем отпускание левой кнопки мыши, а затем пауза, значение которой также хранится в переменной, созданной ранее в конфиге. Пауза нужна для того, чтобы при использовании триггер-бота нейронка не зажимала, а тапала по пульке, тем самым гасила отдачу, если это штурмовая винтовка, или не стреляла бесконечно с АВП, пока АВП не вошло по новой в зум.
Python: Скопировать в буфер обмена
Code:
# Нажатие левой кнопки мыши
win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0)
# Отпускание левой кнопки мыши
win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, 0, 0, 0, 0)


# Задержка
time.sleep(load_config("cs2_trigger_bot_time_sleep"))

Теперь нужно вызвать эту функцию в конце функции детекции при условии, что переменная cs2_trigger_bot в конфиге стоит true.

Для этого можно добавить следующий код в конец функции детекции:
Python: Скопировать в буфер обмена
Code:
if load_config("cs2_trigger_bot"):
   trigger_bot(box_cord)

Обновление интерфейса​

С функцией триггер бота завершено, и теперь можно вывести новые параметры для редактирования на страницу управления. В Python-файле, в котором происходит инициализация Flask и работа с конфигом, нужно перейти в функцию update_config и вписать переменные триггер бота, которые были внесены в файл конфига.
Python: Скопировать в буфер обмена
Code:
config_data['cs2_trigger_bot'] = bool(request.json['cs2_trigger_bot'])
config_data['cs2_trigger_bot_zone'] = int(request.json['cs2_trigger_bot_zone'])
config_data['cs2_trigger_bot_time_sleep'] = float(request.json['cs2_trigger_bot_time_sleep'])
Хочу обратить внимание, что переменная cs2_trigger_bot типа булеан, такого типа еще не было в коде, так что это важно. Также сейчас была изменена функция, которая принимает запрос от веб-части, в котором находятся данные; данная функция их принимает и записывает в конфиг, но пока что веб-часть ничего не отправляет, и ее также нужно дополнить.

Для начала нужно создать объекты на странице, в которых будут храниться значения.
HTML: Скопировать в буфер обмена
Code:
<select id="cs2_trigger_bot" name="cs2_trigger_bot">
   <option value="true">TRUE</option>
   <option value="false">FALSE</option>
</select><br>
<label for="cs2_trigger_bot_zone">CS2 Trigger Bot Zone:</label>
<input type="number" step="1" id="cs2_trigger_bot_zone" name="cs2_trigger_bot_zone"><br>
<label for="cs2_trigger_bot_time_sleep">CS2 Trigger Bot Time Sleep:</label>
<input type="number" step="0.001" id="cs2_trigger_bot_time_sleep" name="cs2_trigger_bot_time_sleep"><br>
Все эти объекты были рассмотрены ранее. Стоит лишь заметить, что у cs2_trigger_bot_zone параметр step установлен на единицу, так как это количество пикселей, на которое увеличивается зона работы триггер-бота, и соответственно пиксель не может быть нецелым числом.

После того как были добавлены объекты, нужно дополнить функцию загрузки конфига, чтобы передать значения в объекты.
JavaScript: Скопировать в буфер обмена
Code:
document.getElementById('cs2_trigger_bot').value = config.cs2_trigger_bot || '';
document.getElementById('cs2_trigger_bot_zone').value = config.cs2_trigger_bot_zone || '';
document.getElementById('cs2_trigger_bot_time_sleep').value = config.cs2_trigger_bot_time_sleep || '';

Далее нужно дополнить функцию отправки данных из объектов на Python часть кода, чтобы данные записались в конфиг.
JavaScript: Скопировать в буфер обмена
Code:
cs2_trigger_bot: document.getElementById('cs2_trigger_bot').value === 'true',
cs2_trigger_bot_zone: document.getElementById('cs2_trigger_bot_zone').value,
cs2_trigger_bot_time_sleep: document.getElementById('cs2_trigger_bot_time_sleep').value,
В данном коде тоже ничего нового, за исключением первой строки. === 'true' проверяет значение из объекта на то, является ли оно true. Если является, то записывается true; если не является, то false. Это сделано потому, что значение в селекторе по умолчанию текстовое, а данная проверка в любом случае переведет значение в тип булевый. Проще говоря, этот механизм по сути превращает строку 'true' в булевое true, а всё остальное (включая 'false') — в булевое false. Это делается автоматически благодаря сравнению === 'true'.
На этом обновление веб-части закончено.

Небольшая фишка интерфейса​

Хочется также отметить небольшую фишку Flask: чтобы её использовать, нужно перейти в главный файл проекта и в параметрах запуска Flask указать такой IP-адрес.
Python: Скопировать в буфер обмена
host="0.0.0.0", port="228"

Вот как выглядит полная строка:
Python: Скопировать в буфер обмена
flask_thread = threading.Thread(target=lambda: app.run(host="0.0.0.0", port="228", debug=True, use_reloader=False))

Теперь, когда IP-адрес страницы установлен на четыре нуля, при запуске нейронной сети в консоль будут выводиться два адреса страницы, например, 127.0.0.1 и 192.168. и т.д. Если ваш мобильный телефон или любое другое устройство с браузером подключено к той же сети, что и ПК, вы сможете зайти на панель софта и редактировать настройки прямо с него. Это полезная фишка, когда нужно настроить конфиг, а каждый раз сворачивать игру не хочется.

Вывод​

Пожалуй, на этом код и статья завершены. Возможно, материал получился сложным или я плохо объяснил некоторые моменты, но все же надеюсь, что все понятно, так как я старался донести все простым языком, включая правильный расчет антиотдачи при механике стрельбы в CS2. На том же GitHub я не смог найти проектов с логикой для учета анти отдачи при стрельбе; обычно в проектах антиотдача либо отсутствует, либо реализована простым способом, аналогично нашему проекту. Поэтому надеюсь, что информация окажется полезной.

P.S Если кто-то действительно дочитал всю статью и есть замечания по коду, с удовольствием выслушаю ваши предложения по оптимизации, так как считаю, что языки программирования я практически не знаю и с многим не знаком. Буду рад узнать что-то новое и улучшить нейросеть.

О следующей статье​

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

Видео презентация работы софта​

Что касается видео-презентации работы софта. У меня возникли некоторые сложности с аккаунтом Google, поэтому залить ролик на YouTube не получится. Было принято решение создать канал, на котором будет опубликован ролик работы софта.

Канал, в котором находится ролик с работой софта - https://t.me/DungeonAIChannel

Статья в виде документа:​

Также, если кому-то удобнее читать статью в более отредактированном формате, вот ссылка на документ: https://docs.google.com/document/d/14WG2L-3PXS6GS81SLjeTxw9kIxTHoSy7_119dh9twjk/edit?usp=sharing

Пояснения к эффективности софта​

P.S. Отдельно хочется прокомментировать пару моментов из видеоролика.

В катке на первом месте, скорее всего, был читер, так как в предыдущей игре он набил 120 килов на 10 смертей, и большинство смертей было именно от него.

Чаще всего промахи были из-за плохих макросов, и они просто не могли полностью законтрить отдачу. На калаше отдача начинает уходить в стороны примерно после 10 патронов.

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

На записи видны моменты, когда мышь доводилась медленно. Это связано с датасетом, который состоит всего из 2000 скриншотов, что крайне мало. Из-за плохого датасета определение объектов прерывалось, в результате чего мышь каждые несколько миллисекунд переставала доводиться и не работала антиотдача.

Вследствие всего вышесказанного, хочу подытожить. Большинство косяков во время стрельбы были связаны с плохим качеством датасета и макросов. Считаю, что в ролике было достаточно моментов, когда было видно, что нейронка зарешала, учитывая что лично я играю намного хуже, чем показано в ролике и явно бы не смог 80% убийств совершить в голову.

P.S. Если у кого-то есть макросы для мышек X7, буду рад, если поделитесь в комментариях.

Сделано OverlordGameDev специально для форума XSS.IS
 
Top