Пишем автокликер для рыбалки в Adventure Quest 3D (AQ3D) (Часть 3)

Закрыто
Admin
Главный админ
Сообщения: 214
Зарегистрирован: 28 окт 2021 14:01

Пишем автокликер для рыбалки в Adventure Quest 3D (AQ3D) (Часть 3)

Сообщение Admin »

В предыдущих частях было рассмотрено, как работают различные механизмы в автокликере. Ссылка на вторую часть. Теперь же полученные знания будут применены на практике. Остановились мы на поиске зоны 3 (См. изображения в первой статье). У нас уже имеются все данные для решения этой задачи. Поэтому пора приступать к скриптингу. Попутно я буду комментировать некоторые, важные на мой взгляд, моменты. Если непонятно как работают вызываемые функции, значит вы не читали руководство пользователя. Хотя должны были это сделать в первую очередь.

В связи с тем, что зона 3 имеет рандомный угол наклона, не получиться искать ее как изображение. Автокликер умеет искать только постоянные изображения без каких-либо трансформаций. Поэтому в данном случае необходимо использовать распознавание цвета пикселя. Искать конкретный цвет мы не можем, потому что, во-первых, цвет полоски индикатора градиентный, а, во-вторых, зона 3 принимает цвет, в зависимости от редкости рыбы. Единственное, что можно использовать, так это тот факт, что цвет зоны 3 значительно светлее основного фона индикатора 2. Автокликер понимает цвета только в целочисленном формате. Это сделано во избежание дополнительных конвертаций из строки HEX-формата. Кроме того, целые числа гораздо нагляднее выглядят во время их сравнения. Важно помнить только одно правило: чем цвет темнее, тем больше его значение. В скрипте вы можете использовать все 16777216 оттенков цветов, которые поддерживаются смартфонами. К слову, это число равно значению черного цвета. Белый цвет это 1.

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

Код: Выделить всё

int frameColor = 13422296;
Выбираем самый темный цвет рамки, чтоб она определилась наверняка. В любом случае цвет остального фона индикатора темнее.

Задаем центральную точку нашей окружности (читай первую и вторую часть).

Код: Выделить всё

Point cntr = Point.get(1640, 565);
Включаем захват экрана в режиме максимальной производительности. Потому что реагировать на изменение индикатора нужно как можно быстрее. И добавляем задержку в 2 секунды. Это необходимо в данном случае для того чтоб успеть подвигать экран. Потому что работать сначала будем со скриншотом. В игре этого делать не нужно. Там экран и так постоянно перерисовывается.

Код: Выделить всё

startScreenCapture(2);
sleep(2000);
Объявляем переменную, куда будем помещать значение цвета пикселя на котором остановились в текущей итерации цикла.

Код: Выделить всё

int color;

color = getColor(x, y);
Цвет пикселя сравниваем в условном ветвлении. Если текущий пиксель светлее, чем самый темный цвет рамки зоны 3, значит мы точно попали где-то в начало зоны. А значит необходимо запомнить координаты этой точки. Но сейчас нам это не нужно. Вместо запоминания координат, будет устанавливаться метка. Потому что неизвестно, работает ли вообще наше условие. Кроме того, цикл прерывается оператором break. Потому что сейчас важно проверить только корректность обнаружения начала зоны 3.

Код: Выделить всё

if(color < frameColor ){
  setMark(x, y);
  break;
}
Почему я не пишу сразу весь скрипт, который будет искать начало и конец зоны 3. Потому что чем больше кода написано, тем сложнее выявить ошибки в нем. Если писать скрипт небольшими частями и тестировать каждый шаг, можно легко понять в каком месте появилась ошибка и почему нарушилась его работа.
В результате получился вот такой скрипт

Код: Выделить всё

int frameColor = 13422296;

Point cntr = Point.get(1640, 565);
int r = 127;

double pi = 3.1416;
double from = pi * 0.45;
double to = pi * 1.38;

startScreenCapture(2);
sleep(2000);

int x;
int y;
int color;

for (; from < to; from += 0.05)
{
  x = (int)(cntr.x + Math.cos(from) * r);
  y = (int)(cntr.y - Math.sin(from) * r);
  
  color = getColor(x, y);
  
  if (color < frameColor)
  {
    setMark(x, y);
    break;
  }
}
Запускаем этот скрипт на скриншоте игры, быстро двигаем окно запуска скрипта, для перерисовки экрана системой. И получаем следующий результат.
Изображение

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

Код: Выделить всё

log("" + color);
Сохраняем, запускаем. После остановки скрипта, открываем журнал отладки и видим значение 13345228. При этом в скрипте мы ожидаем, что самый светлый будет именно на рамке 13422296. Нужно исключить попадание более светлого оттенка в условие. Для этого копируем значение, которое получили в журнале и вставляем вместо старого значения цвета рамки. Теперь автокликер будет искать цвет, который светлее этого значения, пропуская светлые пиксели, которые незаметны глазу на полосе 2.
Запускаем скрипт и получаем необходимый результат. Начало зоны определяется корректно.
Изображение Изображение

Теперь можно переписать скрипт так, чтобы найденная точка не отмечалась меткой, но сохранялась в память. Ведь нам необходимо будет определить еще и конец зоны 3. А метка на экране может быть установлена только одна.
Сделать это крайне просто.
Объявим переменные, в которых будем хранить координаты начала и конца зоны 3.

Код: Выделить всё

Point startP  = Point.get();
Point endP  = Point.get();
А условие, в котором раньше устанавливалась метка перепишем следующим образом

Код: Выделить всё

if(color < frameColor){
  startP.x = x;
  startP.y = y;
}
Теперь у нас есть координаты начала зоны 3, но чтоб получить координаты конца, нужно еще напрячь голову.

Давайте рассмотрим внимательно индикатор. Начало зоны мы определяем во время перехода цвета от темного к светлому. Логично, что конец нужно искать на переходе от светлого к темному. Вот только есть проблема. Цвет зоны 3 может быть темнее, чем сама рамка, например, синий. То есть следующее условие не даст нужный нам результат.

Код: Выделить всё

if(color > frameColor){
  endP.x = x;
  endP.y = y;
}
Необходимо как-то объяснить скрипту, когда он должен искать начало, а когда конец. Для этого лучше всего использовать так называемый флаг. Флаг это булевая переменная(читай руководство/гугл). Так как она может принимать только два значения “true” либо “false” ее использовать удобнее всего. Хотя можно было бы использовать и целочисленную переменную. Как флаг, может быть либо поднятый либо опущен, так и переменная хранит в себе значение “да” или “нет”. Для себя мы решаем, например, так, если флаг опущен, значит автокликер должен искать начало зоны 3, если поднят - конец.
Для начала объявим переменную-флаг.

Код: Выделить всё

boolean found;
По умолчанию она создается со значением “false”. То есть флаг опущен.
Теперь перепишем условное ветвление внутри цикла.

Код: Выделить всё

if (!found)
{
  if (color < frameColor)
  {
    startP.x = x;
    startP.y = y;
    found = true;
  }
}
else
{
}
Оно означает, если флаг не поднят, проверяем чтоб цвет был светлее чем frameColor. Если он светлее - устанавливаем координаты начала зоны 3 и поднимаем флаг, чтоб со следующей итерации цикла, автокликер начал искать конец полоски. Если же флаг находится в поднятом состоянии то выполнение кода сразу переходит к ветвлению else и начинает выполняться код между его фигурных скобок. Но пока что здесь пусто.
Для теста можно вставить такой код в ветвление else

Код: Выделить всё

setMark(startP.x, startP.y);
break;
Выполнив скрипт, можно понять, что флаг работает корректно.

Теперь осталось придумать как найти конец зоны 3. Имеем следующие данные: полоска может быть темнее чем рамка по ее краям, но светлее чем цвет зоны 2. Я не придумал ничего лучше, чем запустить скрипт несколько раз в отладочном варианте и найти самый темный цвет для фона зоны 3. После чего начать сравнивать это значение с цветом фона зоны 2.
Для поиска самого темного значения цвета определим переменную и инициализируем ее цветом фона рамки, который получили в начале этой статьи. Позже это значение будет скорректировано.

Код: Выделить всё

int darkestBack = 13422296;
Во ветвление else запишем еще одно условие

Код: Выделить всё

if(color > darkestBack ){
   log("" + color);
   setMark(x, y);
   break;
}
Оно значит, если цвет текущего пикселя темнее чем самый темный цвет зоны 3, то выполняем код в фигурных скобках. Запускаем, находим самый темный цвет, когда клюнула рыба синего качества и… понимаем что ничего не работает.
Дело в том, что индикатор прозрачный и в некоторых местах цвет становится темно синий, в то время когда зона 2 может быть светло серой. Из-за этого, метка устанавливалась далеко за пределами зоны 3.

Честно признаться, я был расстроен. Неужели придется оставить все как есть и пропускать ловлю синей рыбы… Забросив на какое-то время создание скрипта, я не мог выкинуть его с головы. И в какой-то момент меня осенило! Ведь можно же сначала найти переход от темного к светлому с одной стороны индикатора, после этого начать второй цикл, который начнет обход с противоположного края зоны 2 и найдет начало зоны 3 со своей стороны. При этом не нужно никаких флагов, дополнительных проверок цвета и тд.
Вот какой вид принял скрипт на данном этапе.

Код: Выделить всё

int frameColor = 11830383;

Point startP = Point.get();
Point endP = Point.get();

Point cntr = Point.get(1640, 565);
int r = 127;

double pi = 3.1416;
double from = pi * 0.45;
double to = pi * 1.38;
double step = 0.05;

double tFrom = from;
double tTo = to;

startScreenCapture(2);
sleep(2000);

int x;
int y;

int color;

while (tFrom < tTo){

  x = (int)(cntr.x + Math.cos(tFrom) * r);
  y = (int)(cntr.y - Math.sin(tFrom) * r);

  color = getColor(x, y);

  if (color < frameColor)
  {
    startP.x = x;
    startP.y = y;
    tFrom = to;
    tTo = from;
    break;
  }

  tFrom += step;
}

while (tFrom > tTo){

  x = (int)(cntr.x + Math.cos(tFrom) * r);
  y = (int)(cntr.y - Math.sin(tFrom) * r);

  color = getColor(x, y);

  if (color < frameColor)
  {
    endP.x = x;
    endP.y = y;
    break;
  }

  tFrom -= step;
}

setMark(startP);
sleep(1000);
setMark(endP);
sleep(1000);
removeMark();
Как видите здесь переработана логика. Проверка флага удалена, но появились две дополнительные переменные tFrom и tTo. Они нужны для того, чтоб не изменять в цикле изначальное значение границ обхода. То есть, помещенные в них значения меняются, но изначальные from и to остаются незатронутыми.
После нахождения начала зоны 3, меняем местами начало и конец обхода.

Код: Выделить всё

tFrom = to;
tTo = from;
Во втором цикле вместо приращения значения угла, уменьшает его. Потому что движемся в обратном направлении.

Код: Выделить всё

tFrom -= step;
В конце скрипта устанавливаем метку, сперва на начале зоны 3, а после - в конце. Запускаем скрипт и с удовольствием наблюдаем, как все красиво определяется.

С логикой пока что закончили. Теперь немного геометрии. Нам необходимо нажать на кнопку вылова, когда рыбка будет находится, в идеале, напротив центра зоны 3. Расстояние, на котором движется рыба вокруг центра нашей окружности всегда постоянно. Осталось найти точку по которой проходит рыба, и в которой будем проверять изменение цвета. С теми данными, которыми мы уже располагаем, это можно выполнить с помощью формул.

Определим координаты белой стрелки под рыбкой 1 с помощью пипетки, после этого переходим в онлайн калькулятор для определения длины вектора. Вводим координаты центра индикатора, а также, полученной ранее точки, в соответствующие поля и, вуаля! Теперь у нас есть расстояние от центра индикатора до рыбки. Так как это расстояние постоянно, больше его вычислять не потребуется.
Добавим переменную длины вектора в скрипт.

Код: Выделить всё

int vectorLen = 165;
Теперь необходимо определить направление, на котором будем искать координату стрелки под рыбкой. Оно зависит, конечно же, от расположения центральной точки зоны 3. Потому что, как раз над этой точкой мы должны отследить рыбу 1.
Центральную точку получаем по формуле.

Код: Выделить всё

midP.x = (int)((startP.x + endP.x) * 0.5);
midP.y = (int)((startP.y + endP.y) * 0.5);
Координаты начала плюс координаты конца отрезка делим на 2.

Дальше находим вектор с помощью максимально простой формулы

Код: Выделить всё

int vX = midP.x - cntr.x;
int vY = midP.y - cntr.y;
Теперь у нас имеется вектор, длина отрезка и начальная точка, которой является центральная точка нашего индикатора. Подставляем все эти значения в формулу, найденную с помощью гугла, и получаем координаты точки, через которую будет проходить иконка рыбы.

Код: Выделить всё

double norm = Math.sqrt(vX*vX + vY*vY);
double dX = vX / norm;
double dY = vY / norm;

Point fishP = Point.get();

fishP.x = (int)(cntr.x + dX * vectorLen);
fishP.y = (int)(cntr.y + dY * vectorLen);
Общий скрипт приобрел следующий вид:

Код: Выделить всё

int frameColor = 11830383;

Point startP = Point.get();
Point endP = Point.get();
Point midP = Point.get();

Point cntr = Point.get(1640, 565);
int r = 127;

int vectorLen = 165;

double pi = 3.1416;
double from = pi * 0.45;
double to = pi * 1.38;
double step = 0.05;

double tFrom = from;
double tTo = to;

startScreenCapture(2);
sleep(2000);

int x;
int y;

int color;

while (tFrom < tTo)
{

  x = (int)(cntr.x + Math.cos(tFrom) * r);
  y = (int)(cntr.y - Math.sin(tFrom) * r);

  color = getColor(x, y);

  if (color < frameColor)
  {
    startP.x = x;
    startP.y = y;
    tFrom = to;
    tTo = from;
    break;
  }

  tFrom += step;
}

while (tFrom > tTo)
{

  x = (int)(cntr.x + Math.cos(tFrom) * r);
  y = (int)(cntr.y - Math.sin(tFrom) * r);

  color = getColor(x, y);

  if (color < frameColor)
  {
    endP.x = x;
    endP.y = y;
    break;
  }

  tFrom -= step;
}

midP.x = (int)((startP.x + endP.x) * 0.5);
midP.y = (int)((startP.y + endP.y) * 0.5);

int vX = midP.x - cntr.x;
int vY = midP.y - cntr.y;

double norm = Math.sqrt(vX * vX + vY * vY);
double dX = vX / norm;
double dY = vY / norm;

Point fishP = Point.get();

fishP.x = (int)(cntr.x + dX * vectorLen);
fishP.y = (int)(cntr.y + dY * vectorLen);

setMark(startP);
sleep(1000);
setMark(endP);
sleep(1000);
setMark(midP);
sleep(1000);
setMark(fishP);
sleep(1000);
removeMark();
В конце скрипта добавляем ещё одну установку метки, в только что найденной точке, и убеждаемся, что скрипт отрабатывает корректно.

Смотрите видео выполнения скрипта на скриншоте из игры.

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

Не страшно, если изображение получилось некрасивым. Для автокликера это не имеет никакого значения. Ему главное, чтоб фрагмент изображения был уникален и его легко можно было отличить от остальных объектов на экране.
Чтоб еще сильнее упростить жизнь автокликеру, зададим зону на экране, в которой следует искать данный фрагмент.

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

Код: Выделить всё

Image indicator = Image.load("fish");
Где fish это название файла изображения, которое задается при сохранении фрагмента.

Зададим зону, в которой нужно искать индикатор

Код: Выделить всё

Point ltInd = Point.get(1479,460);
Point rbInd = Point.get(1798,682);
Где ltInd это верхняя левая точка прямоугольника, который ограничивает зону поиска, а rbInd - нижняя правая.

Укажем точку, куда нужно нажимать для перезапуска рыбалки.

Код: Выделить всё

Point pool = Point.get(1079,537);
Определим самый темный цвет стрелки под рыбкой. Но, который значительно светлее волн на воде.

Код: Выделить всё

int fishArrowColor = 8019676;
Наличие изображения индикатора проверяем следующим образом

Код: Выделить всё

if(hasImg(indicator, ltInd, rbInd))
После того, как вычислили точку, по которой будет проходить стрелка, запускаем бесконечный цикл. В котором есть несколько условных ветвлений.

Код: Выделить всё

while(!EXIT){
if(getColor(1762,599) < 16653488){
  if(getColor(fishP) < fishArrowColor){
     click(cntr);
     sleep(3000);
     click(pool);
     break;
   }
}else{
     click(cntr);
     sleep(3000);
     click(pool);
     break;
}

 sleep(10);
}
Здесь мы сначала проверяем не вышло ли у нас время индикатора 5. Если время заканчивается, во избежание ситуации, когда рыбалка закончилась автоматически, а новая не началась, прерываем текущую рыбалку и запускаем новую самостоятельно. Иначе проверяем цвет в точке, где должна проплывать рыбка 1. Если цвет точки стал светлее, чем самый темный цвет стрелки, значит, вероятнее всего, изображение рыбы находится в нужном месте. Нажимаем на кнопку вылова и запускаем рыбалку по новой.

Финальный скрипт имеет следующий вид:

Код: Выделить всё

Image indicator = Image.load("fish");
Point ltInd = Point.get(1479, 460);
Point rbInd = Point.get(1798, 682);
Point pool = Point.get(1079, 537);
 
int fishArrowColor = 8019676;
 
int frameColor = 11830383;
 
Point startP = Point.get();
Point endP = Point.get();
Point midP = Point.get();
 
Point cntr = Point.get(1640, 565);
int r = 127;
 
int vectorLen = 165;
 
double pi = 3.1416;
double from = pi * 0.45;
double to = pi * 1.38;
double step = 0.1;
 
double tFrom = from;
double tTo = to;
 
startScreenCapture(2);
sleep(1000);
 
int x;
int y;
 
int color;
 
while (!EXIT)
{ 
  if (hasImg(indicator, ltInd, rbInd))
  {
    while (tFrom < tTo)
    {
      x = (int)(cntr.x + Math.cos(tFrom) * r);
      y = (int)(cntr.y - Math.sin(tFrom) * r);
 
      color = getColor(x, y);
 
      if (color < frameColor)
      {
        startP.x = x;
        startP.y = y;
        tFrom = to;
        tTo = from;
        break;
      }
 
      tFrom += step;
    }
 
    while (tFrom > tTo)
    {
      x = (int)(cntr.x + Math.cos(tFrom) * r);
      y = (int)(cntr.y - Math.sin(tFrom) * r);
 
      color = getColor(x, y);
 
      if (color < frameColor)
      {
        endP.x = x;
        endP.y = y;
        tFrom = from;
        tTo = to;
        break;
      }
 
      tFrom -= step;
    }
 
    midP.x = (int)((startP.x + endP.x) * 0.5);
    midP.y = (int)((startP.y + endP.y) * 0.5);
 
    int vX = midP.x - cntr.x;
    int vY = midP.y - cntr.y;
 
    double norm = Math.sqrt(vX * vX + vY * vY);
    double dX = vX / norm;
    double dY = vY / norm;
 
    Point fishP = Point.get();
 
    fishP.x = (int)(cntr.x + dX * vectorLen);
    fishP.y = (int)(cntr.y + dY * vectorLen);
 
    while (!EXIT)
    {
      if (getColor(1762, 599) < 16653488)
      {
        if (getColor(fishP) < fishArrowColor)
        {
          click(cntr);
          sleep(3000);
          click(pool);
          break;
        }
      }
      else
      {
        click(cntr);
        sleep(3000);
        click(pool);
        break;
      }
 
      sleep(10);
    }
  }
  sleep(200);
}
Тест на белой луже из 49 рыбами показал следующий результат:
40 пойманных рыб суммарно, из которых 8 зеленого качества, 2 синего и 1 фиолетового.
Зум камеры и fov рекомендуется устанавливать таким образом, чтоб камера была максимально высоко над персонажем. Это поможет избавиться от лишних бликов в зоне индикатора рыбалки.

Из недостатков данного скрипта можно отметить следующее, иногда автокликер срабатывает на слишком светлые волны, считая, что это стрелка рыбы. Также периодически он не может выловить рыбу, потому что она может проплывать между проверками цвета в нужной точке. Но в целом, результат 40/49 я считаю очень успешным, при том, что после запуска скрипта и рыбалки один раз, больше вообще не нужно прикасаться к экрану.

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

В качестве тренировки, попробуйте добавить поиск изображения индикатора лужи. Чтоб когда рыба в луже закончилась, автокликер подавал звуковое/вибро уведомление. Подсказка: целесообразнее всего проверять наличие индикатора лужи после перезапуска рыбалки. То есть, нажали по луже, подождали несколько секунд, проверили наличие индикатора. Если не нашли, включаем оповещение и прерываем скрипт путем установки переменной EXIT в состояние true.

Хочу напоследок сказать несколько слов о том, почему я показал пример скрипта, который не работает, как ожидалось. Во-первых, использование флага, иногда бывает незаменимо, поэтому о нем необходимо знать. Во-вторых, вы должны понимать, что даже разработчик автокликера не может сесть, и за 15 минут написать сразу рабочий скрипт, если этот скрипт должен проверять кучу состояний экрана и выполнять множество действий. Поэтому не нужно отчаиваться и забрасывать скриптинг, со словами: “Это не мое”, только потому, что у вас скрипт не заработал с первого раза. Например, на создание финальной версии скрипта, который был представлен выше в этой статье, было полностью потрачено два моих вечера. Так что задумайтесь, достаточно ли вы приложили усилий, перед тем как забрасывать скрипт. Если зашли в тупик во время скриптинга, отдохните, прежде чем продолжать, и попробуйте решить проблему, используя другой подход к решению задачи. Возможно даже полностью меняя логику скрипта, как это было в моем случае.

На этом у меня все. Желаю терпения и успехов в скриптинге!
Закрыто