在前篇文章中,我们介绍了构建一个自动化游戏脚本的基本思路。这一篇中,我们将简要介绍一些基础的自动化操作。在了解它们以后,读者就可以通过组织这些方法,自己构建一个简单的基本自动化逻辑了。
模拟点击
Python自动化操作当然离不开pyautogui库。我们先安装pyautogui。
pip install pyautogui
随后在程序中导入
import pyautogui
在脚本的开发中,我们主要用到的函数有pyautogui.click(),这是构建自动化脚本最基础的函数,可以用来点击需要的坐标点位。例如:
pyautogui.click(500,800)
我们让程序点击了x=500,y=800的位置。你可以分别传入x和y两个参数,也可以把(x, y)的元组作为一个参数传入click函数,这两者都是合法的。注意:显示器的坐标原点是显示器左上角顶点。水平向右为x轴正方向,竖直向下为y轴正方向。最大坐标取决于你显示器的分辨率,我们可以用pyautogui.size()来获得它(记住这个方法,后面我们会再次用到)。例如我的显示器是3840*2160分辨率,那么
print(pyautogui.size())
输出的信息为
Size(width=3840, height=2160)
好了,在掌握了click函数以后,如果想要实现一系列有且仅有固定位置、固定顺序的点击操作,你就已经可以使用click函数和time.sleep()来实现了——不过,用连点器也可以实现完全相同的效果。
pyautogui库除了click函数,也有很多功能相类似的函数,它们都是为了实现自动化而设计的。例如 moveTo() 控制鼠标移动,scroll() 实现滚轮滚动,dragRel() 实现相对拖拽,以及操控键盘按键的keyUp()和keyDown()。在此不一一列举,实际开发时,可查阅pyautogui官方文档/源码,或向大语言模型AI询问,进而得到你所需要的功能。
坐标从何而来?
现在,我们会面临一个直接的问题:坐标从何而来?我们介绍了指定鼠标指针点击某点的click方法,我们又应该如何获得需要的某点的坐标?除了前文所述那种“仅包含固定位置、固定顺序的点击操作”,在不同分辨率的设备上,是否就意味着我们需要重新去测得每一个点的坐标?
首先回答第一个问题。
print(pyautogui.position())
pyautogui的position()函数可以获取鼠标指针当前所在位置的坐标,我们只需要再加上sleep函数,就能确保我们将指针移动到所需位置上后,得到该点的坐标。
例如,我们要获取打开部落战页面的坐标:
我们只需要在time.sleep的时间内,把鼠标指针移动到图标上,等待程序测算并输出,就可以了。
当然,这还不够。在不同分辨率的设备上,如果某些对象(例如特定的按钮)的位置是以近似等比例缩放来放置的,那么我们可以记录下这个目标对象的相对坐标,作为静态数据写入代码中,再在每次运行时计算它们的真实坐标——在大部分情况下,这种方法应该是表现良好的。计算相对坐标是个自然的想法,且在第0篇中亦有所介绍,有阅读过的读者一定不陌生。
- P.S.虽然《部落冲突》游戏内大部分图标的相对位置都不受分辨率、DPI影响,但我还是建议你不要在极端情况下使用!
相对坐标
相对坐标=(真实坐标/分辨率)
我们分别将x坐标除以显示器的宽(pyautogui.size()返回的第0个参数),将y坐标除以显示器的高(pyautogui.size()返回的第1个参数),就能得到某一点的相对坐标。
# 计算某点的相对坐标
time.sleep(3) # 确保鼠标在后续代码执行前移动到所需位置上
width, height = pyautogui.size()
x, y = pyautogui.position()
relative_x = x / width
relative_y = y / height
print(relative_x, relative_y)
当然,更进一步,我们可以直接一步到位写成:循环监听键盘,每按下一次空格键,就计算并输出当前坐标的相对值,按下esc则退出程序。同时将输出写成特定格式,以便后续直接填入存储所有程序运行需要的相对坐标的config.py文件中。代码如下:
# get_location.py
# 导入必要的模块
import keyboard # 记得pip安装该模块
import pyautogui
x_size, y_size = pyautogui.size()
# 定义一个函数,当按下空格键时,打印当前鼠标指针的坐标
def print_mouse_position():
# 获取鼠标指针的坐标
x, y = pyautogui.position()
x_relative, y_relative = x / x_size, y / y_size
# 打印相对坐标
print(f"(round(x * {x_relative}), round(y * {y_relative})),") # 这里是便于后续填入config.py文件的存储坐标字典locations中,我们随后便可以看到
# 注册一个监听器,当按下空格键时,调用函数
keyboard.on_press_key("space", lambda _: print_mouse_position())
# 保持监听状态,直到按下esc键退出
keyboard.wait("esc")
存储程序运行所需的相对坐标如下:
# config.py
x, y = pyautogui.size() # 获取屏幕分辨率,在后续运行时逐个计算乘上相对坐标后的实际坐标值
locations = \
{
"settings": # 游戏设置相关的坐标
{
"open_settings_page": (round(x * 0.96015625), round(y * 0.724537037037037)),
"more_settings": (round(x * 0.69921875), round(y * 0.899537037037037)),
"snow_point": (round(x * 0.7140625), round(y * 0.775462962962963)),
},
"train_page": # 改版后,coc已经取消了造兵时间,所有的部队训练都在一个页面下即时修改
{
"delete_1": (round(x * 0.9651041666666667), round(y * 0.2351851851851852)),
"delete_2": (round(x * 0.7651041666666667), round(y * 0.48055555555555557)),
"delete_3": (round(x * 0.965625), round(y * 0.4824074074074074)),
"delete_4": (round(x * 0.7651041666666667), round(y * 0.7444444444444445)),
"edit_troop": (round(x * 0.6911458333333333), round(y * 0.3402777777777778)),
"edit_spell": (round(x * 0.6002604166666666), round(y * 0.5856481481481481)),
"exit": (round(x * 0.96484375), round(y * 0.08935185185185185)),
},
"attack": # 进攻页面的一些坐标
{
"attack_button": (round(x * 0.0625), round(y * 0.8796296296296296)),
"find_a_match": (round(x * 0.7708333333333333), round(y * 0.6055555555555556)),
"troop_1": (round(x * 0.1075520833333333), round(y * 0.9055555555555556)),
"troop_2": (round(x * 0.1817708333333333), round(y * 0.9055555555555556)),
"troop_3": (round(x * 0.2666666666666667), round(y * 0.9055555555555556)),
"troop_4": (round(x * 0.3455729166666667), round(y * 0.9055555555555556)),
"troop_5": (round(x * 0.4325520833333333), round(y * 0.9055555555555556)),
"troop_6": (round(x * 0.5078125), round(y * 0.9055555555555556)),
"troop_7": (round(x * 0.58203125), round(y * 0.9055555555555556)),
"troop_8": (round(x * 0.6596354166666667), round(y * 0.9055555555555556)),
"troop_9": (round(x * 0.73203125), round(y * 0.9055555555555556)),
"troop_10": (round(x * 0.81015625), round(y * 0.9055555555555556)),
"before_scroll": # 在游戏进攻时,我们会先移动到左上,后续再往下滑动(利用pyautogui.scroll())因此这部分是scroll前的坐标
{
"village_up": (round(x * 0.5299479166666667), round(y * 0.0796296296296296)),
"village_left": (round(x * 0.1364583333333333), round(y * 0.6055555555555556)),
"village_right": (round(x * 0.9263020833333333), round(y * 0.6078703703703704))
},
"after_scroll": # scroll后的坐标,记录的是村庄的几个端点,方便下兵
{
"village_down_left": (round(x * 0.4869791666666667), round(y * 0.7967592592592593)),
"village_down_right": (round(x * 0.55390625), round(y * 0.8023148148148148)),
"village_left": (round(x * 0.12421875), round(y * 0.2856481481481481)),
"village_right": (round(x * 0.9481770833333333), round(y * 0.2662037037037037))
},
"spell": # 曾经记录的几个法术施放位置,当然后续已经荒废了,在此留作示例
{
"row_1_start": (round(x * 0.5479166666666667), round(y * 0.2625)),
"row_1_end": (round(x * 0.2755208333333333), round(y * 0.6273148148148148)),
"row_2_start": (round(x * 0.55625), round(y * 0.4027777777777778)),
"row_2_end": (round(x * 0.37395833333333334), round(y * 0.6430555555555556)),
"row_3_start": (round(x * 0.6341145833333334), round(y * 0.47638888888888886)),
"row_3_end": (round(x * 0.42630208333333336), round(y * 0.7532407407407408)),
},
"resource_region": (round(x * 0.05), round(y * 0.135),
round(x * 0.1041666666666667), round(y * 0.1574074074074074)),
"rgb_point": (round(x * 0.5), round(y * 0.82)),
"next": (round(x * 0.9125), round(y * 0.7222222222222222)),
"return": (round(x * 0.5), round(y * 0.8888888888888889)),
},
"clan": # 部落页面的几个坐标
{
"open_chatting_page": (round(x * 0.01875), round(y * 0.4305555555555556)),
"request_donation": (round(x * 0.05), round(y * 0.5185185185185185)),
"edit_donation": (round(x * 0.3328125), round(y * 0.5185185185185185)),
},
"clan_war": # 部落战的几个坐标,不过……已经没用了?
{
"start": (round(x * 0.35572916666666665), round(y * 0.3907407407407407)),
"end": (round(x * 0.75), round(y * 0.9398148148148148)),
},
"donation": # 捐兵相关的一些坐标
{
"troop_drag_left": (round(x * 0.4359375), round(y * 0.30277777777777776)),
"troop_drag_right": (round(x * 0.8658854166666666), round(y * 0.30787037037037035)),
"troop_area": (round(x * 0.615625), round(y * 0.39675925925925926)),
"spell_area": (round(x * 0.6161458333333333), round(y * 0.6935185185185185)),
},
"reload": (round(x * 0.296875), round(y * 0.5828703703703704)), # 长时间无操作,重新进入游戏的按钮坐标
}
- 注:实际上,在上述这么多的坐标数据中,有些已逐渐被更优的cv方法取代了,比如我们后面很快就要介绍的cv2.matchTemplate方法。
根据相对值×实际值的思路,以此类推,除了坐标可以使用一个相对的比例,有时需要在屏幕上拖动/滚动一定距离,也可以使用一个比例,运行时乘上真实的分辨率数据。这是一个很实用的技巧。
需要注意的是,上述方法仅用于游戏运行中那些我们能够提前知道出现时机、出现位置的固定坐标。对于会位置会变动、或出现时机不固定的对象,我们同样需要更复杂的cv方法,这将会在后续系列教程中讲到(点个关注,谢谢喵(୨୧• ᴗ •͈)◞︎ᶫᵒᵛᵉ ♡)。