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

D2

Администратор
Регистрация
19 Фев 2025
Сообщения
4,380
Реакции
0

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

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

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

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

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

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

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

Перед тем как приступить к разработке логики анти-отдачи, необходимо подготовить проект, внести некоторые правки и улучшить его базу. В первую очередь, для удобства следует вынести переменные в отдельный конфигурационный файл. Для этого нужно создать файл config.json и записать в него все переменные, которые ранее назначались в начале кода.
Код: Скопировать в буфер обмена
Код:
{
   "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: Скопировать в буфер обмена
Код:
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: Скопировать в буфер обмена
Код:
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: Скопировать в буфер обмена
Код:
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. Для начала нужно создать два ключа в конфиге, которые будут использоваться в будущей функции антиотдачи.
Код: Скопировать в буфер обмена
Код:
"anti_recoil_px": 1,
"anti_recoil_time_sleep": 0.001

После добавления ключей в конфиг можно приступать к написанию функции антиотдачи. Называться она будет anti_recoil, и в ней нужно будет прописать цикл while True. Внутри цикла нужно добавить проверку на нажатие левой кнопки мыши. Если левая кнопка мыши нажата, то двигать курсор по y на количество пикселей, указанных в ключе из конфига anti_recoil_px.
Python: Скопировать в буфер обмена
Код:
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: Скопировать в буфер обмена
Код:
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: Скопировать в буфер обмена
Код:
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: Скопировать в буфер обмена
Код:
def game_initialization():
   if load_config("game") == "default":
       default.start_game()
P.S. default.start_game означает, что будет вызываться функция start_game из файла default, но пока этой функции нет, и к ней нужно будет вернуться позже.

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

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

Теперь же это будет выглядеть так:
Python: Скопировать в буфер обмена
Код:
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_. Это сделано для того, чтобы абсолютно каждый параметр был отдельно настраиваемым для каждой игры.
Код: Скопировать в буфер обмена
Код:
"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: Скопировать в буфер обмена
Код:
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: Скопировать в буфер обмена
Код:
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: Скопировать в буфер обмена
Код:
input_data = """


"""
В данную переменную будут записываться макросы X7 в следующем формате:
Python: Скопировать в буфер обмена
Код:
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: Скопировать в буфер обмена
Код:
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: Скопировать в буфер обмена
Код:
elif parts[0] == 'MoveR':
   x, y = map(int, parts[1:])
   macros.append((x, y, delay))

После того как в словарь были записаны данные, нужно взять этот словарь и записать его в txt файл, используя file.writelines.
Python: Скопировать в буфер обмена
Код:
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, и, к сожалению, не все из них хорошо работают. Но на первое время этого будет достаточно.

Теперь можно приступить к реализации новой логики анти-отдачи, и первым шагом нужно добавить новые ключи со значениями в конфигурационный файл.
Код: Скопировать в буфер обмена
Код:
"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: Скопировать в буфер обмена
Код:
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: Скопировать в буфер обмена
Код:
# Основной цикл антиотдачи
while True:
   for x, y, delay in macros_dict[load_config("cs2_macros_gun")]:

Затем, внутри цикла for нужно умножать x и y на число, указанное в ключе из конфигурационного файла, в котором указано значение для усиления или уменьшения силы отдачи для конкретного оружия.
Python: Скопировать в буфер обмена
Код:
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: Скопировать в буфер обмена
Код:
anti_recoil_x += x
anti_recoil_y += y

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

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

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

И затем нужно вне функции назначить этим переменным значение 0.
Python: Скопировать в буфер обмена
Код:
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: Скопировать в буфер обмена
Код:
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: Скопировать в буфер обмена
Код:
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: Скопировать в буфер обмена
Код:
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: Скопировать в буфер обмена
Код:
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 (перед этим, разумеется, нужно Скачать
View hidden content is available for registered users!
 
Сверху Снизу