Разработка → Создание простого бота для онлайн-игры world of warcraft

rednaxi 6 февраля 2011 в 22:00 51,2k
Думаю, тема ботов не оставляет равнодушным ни одного игрока в онлайн-игры. Кого-то они раздражают, кто-то ими интересуется, а кто-то их использует. Существует и некоторое количество людей, довольно маленькое относительно остальных трех групп — это люди, которые этих ботов разрабатывают.
Я предлагаю присоединиться к этой небольшой касте людей и посмотреть изнутри процесс разработки бота.

Предыстория


Как-то раз в выходные я зашел за своего персонажа в world of warcraft. Делать было в игре нечего — все рейдовые боссы уже убиты, друзей для похода на арену нет, остается только выполнение квестов и неспешная добыча золота. Квесты я не очень люблю и свое свободное время в игре провожу обычно возле аукциона — с помощью специальных аддонов скупаю что подешевле и потом продаю подороже, выигрывая на разнице в цене.

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

В качестве инструмента для реализации был выбран Auto It, но потом оказалось что задача не такая тривиальная, как выглядит на первый взгляд, и пришлось подключать дополнительные инструменты. Сразу скажу, что никакие «грязные» методы вроде чтения памяти процесса или перехвата траффика я не использовал, только autoit для имитации кликов мышки и клавиатуры и чтения цветов пикселей с экрана.



Предупреждение


Все описанные действия вы производите на свой страх и риск, правилами игры это запрещено и может привести к перманентному бану аккаунта.

Передвижение


Собственно, наверное самая интересная тема при разработке бота — это реализация его перемещения по миру. Сначала я думал что проблем с этим не возникнет — путь у нас простой: аукцион это здание, в котором на такой площадке стоят NPC, с которыми необходимо поговорить чтобы начать торговлю, а почтовый ящик стоит прямо на выходе из аукциона, немного слева. Персонаж поэтому все время ходит как бы буквой «Г», туда и обратно



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

Но отчаиваться я не стал и начал думать что делать дальше. Казалось бы следующее решение — каким то образом определять где в данный момент находится персонаж, и в зависимости от этого расчитывать куда ему дальше двигаться. Только каким образом получить данные о позиции и угле поворота персонажа без чтения памяти процесса игры? Анализировать целиком картинку или какие то отдельные ее части довольно сложно, скриншот выхода из аукциона:


После недолгих раздумий было придумано решение проблемы: надо писать аддон для WoW, который будет получать данные о текущем положении персонажа (координаты и поворот персонажа) и давать команды, куда надо двигать персонажа. Внешне это должно выглядеть как цветной «семафор» который будет отображать необходимое действие: двигаться вперед, вправо или влево, или поворачиваться. Когда персонаж приходит в необходимую точку семафор должен символизировать что персонаж пришел на место и необходимо выполнять следующее действие. Так как у меня персонаж перемещается между двумя точками, то и семафоворов я решил делать 2: один будет командовать по дороге к аукционеру, а второй — по дороге к почте. С алгоритмом вроде разобрались, далее будут примеры кода (xml — шаблоны аддонов, lua — собственно исходники аддона, и autoit), местами кривоватые и грязноватые, но думаю простительно: на этих языках писал первый раз, и еще до конца не разобрался.

Для реализации семафоров пришлось изучить процесс разработки аддонов для WoW, он очень хорошо описан в WoWwiki

Самая первая версия семафора, она показывала только повернут ли я лицом к аукционеру в данный момент, и подсвечивала зеленым соответствующие блоки:


В качестве основы для своего аддона я взял hello world! аддон, описанный в воввики.
Изначально он не имел никаких графических элементов, соответственно понадобилось добавить в него вывод прямоугольников, а также повесить обработчик . В итоге мой HelloWorld.xml стал выглядеть примерно так:
<Ui xmlns="http://www.blizzard.com/wow/ui/" 
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
 xsi:schemaLocation="http://www.blizzard.com/wow/ui/ 
 ..\..\FrameXML\UI.xsd">
  <Script File="HelloWorld.lua"/>
    <!-- Фрейм - контейнер для семафора -->
   <Frame
       name="myTabContainerFrame"
       toplevel="true"
       frameStrata="DIALOG"
       movable="true"
       enableMouse="true"
       hidden="false"
       parent="UIParent">
       <!-- размер фрейма -->
       <Size>
           <AbsDimension x="20" y="120"/>
       </Size>
       <!-- положение - в левом верхнем углу экрана, отступы от краев экрана 20 пикселей -->
       <Anchors>
           <Anchor point="TOPLEFT">
             <Offset><AbsDimension x="20" y="-20"/></Offset>
           </Anchor>
       </Anchors>
       <!-- вложенные фреймы -->
       <Frames>     
        <!-- фрейм - семафор для передвижения к аукциону -->
        <Frame name="myGoToAucFrame">
            <!-- размер - половина родительского фрейма, ниже будет семафор для почты -->
            <Size>
                <AbsDimension x="20" y="60"/>
            </Size>
            <!-- положение - находится в левом верхнем углу родителя -->
            <Anchors>
                <Anchor point="TOPLEFT" />
            </Anchors>        
            <Frames>
            <!-- первый элемент семафора - показывает на месте ли я нахожусь -->
           <Frame name="myTabPage1" hidden="false">
               <!-- находится в левом верхнем углу -->
               <Anchors>
                   <Anchor point="TOPLEFT" />
               </Anchors>
               <!-- размер - ширина как у родителя, высота небольшая -->
                <Size>
                    <AbsDimension x="20" y="10"/>
                </Size>               
                <!-- содержимое фрейма -->
               <Layers>
                   <Layer level="ARTWORK">
                       <!-- текст - нужен чтобы проще было писать скрипты autoit --> 
                       <FontString inherits="GameFontNormal" text="*">
                           <Anchors>
                               <Anchor point="TOPLEFT" relativeTo="$parent" />
                           </Anchors>
                       </FontString>
                       <!-- текстура - ее цвет мы будем менять -->
					<Texture name="PlayerAucViewTrue">
                        <Size>
                            <AbsDimension x="20" y="20" />
                        </Size>
                        <Anchors>
                            <Achor point="BOTTOM" relativePoint="BOTTOM" relativeTo="UIParent" />
                        </Anchors>
				    </Texture>                       
                   </Layer>
               </Layers>
               <Frames>
               </Frames> 
           </Frame>
           <!-- элемент семафора, который показывает что нужно повернуть налево -->
           <Frame name="myGoToAucFrame2" hidden="false">
               <Anchors>
                   <!-- сдвинут на 10 пикселей вниз, чтобы быть ниже предыдущего фреймом -->
                   <Anchor point="TOPLEFT">
                        <Offset><AbsDimension x="0" y="-10"/></Offset>
                   </Anchor>
               </Anchors>
               <!-- размеры - ширина в два раза меньше - справа будет фрейм, сигнализирующий о повороте направо -->
                <Size>
                    <AbsDimension x="10" y="10"/>
                </Size>               
               <Layers>
                   <Layer level="ARTWORK">
                       <FontString inherits="GameFontNormal" text="<">
                           <Anchors>
                               <Anchor point="TOPLEFT" relativeTo="$parent">
                                   <Offset>
                                       <AbsDimension x="0" y="0"/>
                                   </Offset>
                               </Anchor>
                           </Anchors>
                       </FontString>
					<Texture name="PlayerAucViewLeft">
                        <Size>
                            <AbsDimension x="20" y="20" />
                        </Size>
                        <Anchors>
                            <Achor point="BOTTOM" relativePoint="BOTTOM" relativeTo="UIParent" />
                        </Anchors>
				    </Texture>                       
                   </Layer>
               </Layers>
               <Frames>
               </Frames> 
           </Frame>           
          <!-- далее добавляем необходимое количество фреймов по аналогии -->
           </Frames>
          </Frame>
         
       </Frames>
     <!-- обработчики событий - при загрузке аддона и при обновлении данных в игре -->
	<Scripts>
		<OnLoad>
		Semafor_Onload();
		</OnLoad>
        <OnUpdate>
        CheckPosition();
        </OnUpdate>
	</Scripts>
   </Frame>
</Ui> 


Соответственно в HelloWorld.lua необходимо написать соответствующие обработчики. Для того, чтобы узнать, на какой угол в данный момент повернут персонаж, используется функция GetPlayerFacing, которая возвращает угол относительно севера в радианах. Экспериментальным путем было выяснено, что когда персонаж смотрит прямо на аукционера — это угол 5.42 радиана. Но повернуть персонажа точно на этот угол довольно сложно, поэтому допускаем небольшой разброс (5.35 — 5.5 радиана).
function Semafor_Onload()
    print("Hi!");
end

function CheckPosition()
	local facing = GetPlayerFacing();   
    -- проверим куда смотрит персонаж и отрисуем в какую сторону ему надо поворачиваться чтобы смотреть на аукционера
    if(facing <= 5.50 and facing >= 5.35) then
      PlayerAucViewTrue:SetTexture(0,1,0); -- установка цвета текстуры, цвет в формате RGB. 1,0,0 - красный, 0,1,0 - зеленый
      PlayerAucViewLeft:SetTexture(1,0,0);
      PlayerAucViewRight:SetTexture(1,0,0);
    elseif ((facing > 5.50))then
      PlayerAucViewTrue:SetTexture(1,0,0);
      PlayerAucViewLeft:SetTexture(1,0,0);
      PlayerAucViewRight:SetTexture(0,1,0);    
    elseif (facing < 5.35) then
      PlayerAucViewTrue:SetTexture(1,0,0);
      PlayerAucViewLeft:SetTexture(0,1,0);
      PlayerAucViewRight:SetTexture(1,0,0);    
    end
end


Теперь напишем скрипт на autoit, который будет смотреть какой цвет в данный момент у семафора, и поворачивать персонажа в необходимую сторону, и заодно создадим каркас нашего приложения для автоматической торговли на аукционе. Опытным путем было выяснено что цвет пикселя 65280 — это зеленый.
Global $WinName = "World of Warcraft"
Global $state = "stop" 
Opt("PixelCoordMode", 2) ;Отсчет координат пикселей от левого верхнего угла клиентской части окна
Opt("MouseCoordMode", 2) ;Отсчет координат мыши от левого верхнего угла клиентской части окна
HotKeySet("{NUMPAD1}", "GoRotate")
HotKeySet("{NUMPAD3}", "_Exit")

WinActivate($WinName)
WinWaitActive($WinName)	

While 1
	sleep(10)
	Running(); бесконечный вызов этой функции, которая делает необходимое в данный момент действие
WEnd

Func _Exit()
    Exit
EndFunc

Func GoRotate()
	$state = "rotating" 
EndFunc

Func  Running()
	Switch $state
	Case  "rotating" 
		Rotating()
	EndSwitch
EndFunc

Func Rotating()
	While $state = "rotating"
		;определим угол поворота, надо ли поворачиваться, координаты точек, в которых берем цвет, взяты вручную в paintе со скриншота с семафором
		$angleOkColor = PixelGetColor(32,24)
		$angleLeftColor = PixelGetColor(29,36)
		$angleRightColor = PixelGetColor(40,32)
			
		
		if $angleOkColor = 65280 Then
			$state = "starttrading" ;если все ок значит мы на месте - начинаем торговлю
		ElseIf $pxAngleLeftColor = 65280 Then; поворот налево
				Send("{LEFT down}"); жмем кнопку "влево" и засыпаем пока не погаснет сигнал семафора, поворот налево - плавный, иногда поворачивает лишнего из-за этого
				While PixelGetColor(29,36) = 65280
					sleep(2)
				Wend
				Send("{LEFT up}")
		ElseIf $pxAngleRightColor = 65280 Then ; поворот направо - не плавный, чтобы более точно поворачивать
				Send("{RIGHT down}");
				sleep(20);
				Send("{RIGHT up}");	
		EndIf		
	WEnd
EndFunc


Всё, сохраняем аддон, запускаем WoW, запускаем скрипт AutoIt и смотрим на персонажа, который сам поворачивается лицом в нужное направление. Но кроме поворотов надо также реализовать перемещение персонажа вперед-назад и вправо-влево, чтобы он мог приходить к аукционеру от почтового ящика. Удачно, что на пути персонажа нет никаких препятствий, иначе задача была бы сложнее.

А так просто добавляем в наш xml дополнительные фреймы для еще 5 кнопок семафора: положение Ок!, двигаться вперед, двигаться назад, двигаться вправо и двигаться влево, и в lua файле дописываем код, который будет показывать, куда нам небходимо двигаться в данный момент. Эталонные координаты где нам нужно находится мы знаем. Казалось бы тоже все просто, но не тут то было — как мы видели по карте, и уже выяснили когда делали повороты, движение у нас идет под углом к северу. Т.е. при движении у нас постоянно меняются обе координаты персонажа. Считать при этом куда двигаться дальше не очень удобно, поэтому воспользуемся известными со школьного курса формулами для поворота системы координат на заданный угол (в нашем случае — 5.42 радиана)



Теперь при движении к/от аукционера и вправо-влево у нас будет меняться только одна координата. Допишем соответствущий код в наш lua файл и он примет примерно следующий вид:
function Semafor_Onload()
    print("Hi! All done!");
end

function CheckPosition()
	local facing = GetPlayerFacing();
    SetMapToCurrentZone();
	local posX, posY = GetPlayerMapPosition("player");
    
    -- переходим в систему координат относительно аукциона (повернем координаты на угол 5.42 радиан)
    newPosX = posX*math.cos(5.42) + posY*math.sin(5.42);
    newPosY = -posX*math.sin(5.42) + posY*math.cos(5.42);
    newPosX = -newPosX; -- не люблю отрицательные числа :)
   
    
    -- проверим куда смотрит персонаж и отрисуем в какую сторону ему надо поворачиваться чтобы смотреть на аукционера
    if(facing <= 5.50 and facing >= 5.35) then
      PlayerAucViewTrue:SetTexture(0,1,0);
      PlayerAucViewLeft:SetTexture(1,0,0);
      PlayerAucViewRight:SetTexture(1,0,0);
    elseif ((facing > 5.50 and facing < 6.5))then
      PlayerAucViewTrue:SetTexture(1,0,0);
      PlayerAucViewLeft:SetTexture(1,0,0);
      PlayerAucViewRight:SetTexture(0,1,0);    
    elseif (facing < 5.35) then
      PlayerAucViewTrue:SetTexture(1,0,0);
      PlayerAucViewLeft:SetTexture(0,1,0);
      PlayerAucViewRight:SetTexture(1,0,0);    
    end
    -- проверим позицию персонажа и определим куда бежать чтобы попасть к аукционеру
    if (newPosX <= 0.207 and newPosY <=0.889 and newPosY >=0.8875) then
      PlayerGoForvard:SetTexture(1,0,0);
      PlayerGoBack:SetTexture(1,0,0);
      PlayerGoLeft:SetTexture(1,0,0);
      PlayerGoRight:SetTexture(1,0,0);
      PlayerOnAuc:SetTexture(0,1,0);      
    elseif (newPosY > 0.889) then
      PlayerGoForvard:SetTexture(1,0,0);
      PlayerGoBack:SetTexture(1,0,0);
      PlayerGoLeft:SetTexture(0,1,0);
      PlayerGoRight:SetTexture(1,0,0);
      PlayerOnAuc:SetTexture(1,0,0);    
    elseif (newPosY < 0.8875) then
      PlayerGoForvard:SetTexture(1,0,0);
      PlayerGoBack:SetTexture(1,0,0);
      PlayerGoLeft:SetTexture(1,0,0);
      PlayerGoRight:SetTexture(0,1,0);
      PlayerOnAuc:SetTexture(1,0,0);      
    elseif (newPosX > 0.207) then
      PlayerGoForvard:SetTexture(0,1,0);
      PlayerGoBack:SetTexture(1,0,0);
      PlayerGoLeft:SetTexture(1,0,0);
      PlayerGoRight:SetTexture(1,0,0);
      PlayerOnAuc:SetTexture(1,0,0);       
    end
end


Все, теперь у нас получился примерно вот такой семафор:

Осталось дописать по аналогии скрипт AutoIt чтобы когда загораются сигналы семафора он посылал соответствующие кнопки в игру. Единственное с чем я столкнулся — он не дает посылать нормально буквенные кнопки (A/D), поэтому пришлось забиндить стрейф на F6/F7.

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

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

Сегодня в 12 часов я поставил бота крутиться по аукциону, за время его работы (около 9 часов) потребовалось 3 раза вмешаться в его работу — один раз застрял в торчащих элементах стены, после этого я слегка переписал алгоритм бега в сторону почты, и 2 раза промахивался мимо почтового ящика — надо поправить это, пока руки не дошли.
Прибыль за день:

Неплохо при условии что я в процессе не участвовал совершенно :)

Upd:
вторая часть
Проголосовать:
+94
Сохранить: