崩铁自动小助手ASR开发实录

崩铁小助手ASR

天下苦二游上班坐牢久矣。方舟有MAA造福大众,免去日常之苦,能让我专心于关卡,但是米家游戏就不行了,于是就有了这个崩铁小助手——AutoStarRail的想法。

功能计划

目前初步计划就是能够实现每天自动清体力,领日常奖励,让我不用操心每天还得上线清体力的事情。最后实现的界面如下,大概和方舟的maa差不多。
![[Pasted image 20240621134224.png]]

但是为了防止崩铁全屏运行时难以观察运行信息,所以又做了个始终在前台的message窗口,用于实现实时显示自动化脚本的信息。(窗口可放置在任意位置)
在这里插入图片描述

经过测试,选择好要刷的本(经验\钱、行迹、突破素材、仪器这些都没问题)后能够自动导航至目标副本,然后识别体力,刷到没体力为止。
演示视频如西瓜视频:从重复劳动中解脱-崩铁自动日常小助手
抖音:从重复劳动中解脱-崩铁自动日常小助手
完整开源代码见 AutoStarRail,欢迎大家star。

功能实现

操作的模拟

操作上使用vgamepad创建虚拟手柄来对游戏进行操作。虽然这样会多一个虚拟设备,但是由于手柄操作时对选中部件的高亮,能够更容易的识别当前选中的东西,并进行精确的操作。


class Gamepad:
    def __init__(self,pigeon = None):
        # 初始化一个手柄
        self.pigeon = pigeon
        self.gamepad = vgamepad.VX360Gamepad()
        # 初始化手柄状态
        self.reset_gamepad()


    def reset_gamepad(self):
        self.gamepad.reset()#键位扳机摇杆全部重置成初始状态
        self.gamepad.update()
    # gamepad 操作
    def click_button(self,button,duration=0.15):
        self.gamepad.press_button(button)
        self.gamepad.update()
        time.sleep(duration + random.randint(0,int(0.05*100))/100)
        self.gamepad.release_button(button)
        self.gamepad.update()
        if self.pigeon:
            self.pigeon("click " + button_mapping[button])

    def press_button(self,button):
        self.gamepad.press_button(button)
        self.gamepad.update()
        if self.pigeon:
            self.pigeon("press " + button_mapping[button])

    def release_button(self,button):
        self.gamepad.release_button(button)
        self.gamepad.update()

    def LEFT_TRIGGER(self,value):
        self.gamepad.left_trigger_float(value)
        # 左扳机轴 value改成0.0到1.0之间的浮点值,可以精确到小数点后5位
        self.gamepad.update()

    def RIGHT_TRIGGER(self,value):    
        self.gamepad.right_trigger_float(value)
        # 右扳机轴 value改成0.0到1.0之间的浮点值,可以精确到小数点后5位
        self.gamepad.update()

    def LEFT_JOYSTICK(self,theta,amplitude,ran_theta = 2*np.pi/25,ran_amp = 1/25): #x_value, y_value):
        theta = theta + random.randint(0,ran_theta*100)/100
        amplitude = amplitude + random.randint(0,ran_amp*100)/100
        x_value = 1.414*amplitude * np.cos(theta)
        y_value = 1.414*amplitude * np.sin(theta)
        self.gamepad.left_joystick_float(x_value, y_value)
        # 左摇杆XY轴  x_values和y_values改成-1.0到1.0之间的浮点值,可以精确到小数点后5位
        self.gamepad.update()
        if self.pigeon:
            self.pigeon("" + "Left joystick")
    
    def RIGHT_JOYSTCIK(self,theta,amplitude,ran_theta = 2*np.pi/25,ran_amp = 1/25):
        theta = theta + random.randint(0,int(ran_theta*100))/100
        amplitude = amplitude + random.randint(0,int(ran_amp*100))/100
        x_value = amplitude * np.cos(theta)
        y_value = amplitude * np.sin(theta)
        self.gamepad.right_joystick_float(x_value, y_value)
        # 右摇杆XY轴  x_values和y_values改成-1.0到1.0之间的浮点值,可以精确到小数点后5位
        self.gamepad.update()
        if self.pigeon:
            self.pigeon("" + "Left joystick")

    def joystick_movement(self, theta=0, duration=0.5, amplitude=1):
        start_time = time.time()
        while time.time() - start_time < duration:
            # 时间-角度序列
            theta_time = theta * (time.time() - start_time) / duration

            # 幅度
            amplitude_time = amplitude * (time.time() - start_time) / duration

            self.RIGHT_JOYSTCIK(theta_time, amplitude_time)

            time.sleep(0.01)

窗口的识别

这部分涉及到图像的一些识别。为了减少计算资源的消耗,本文主要使用paddleocr识别字符来定位。少部分地方用到了矩形框的识别。

游戏窗口识别

脚本启动时应当先识别当前有没有打开启动器、或游戏,再决定是否需要打开游戏。
对于老版本而言由于启动器和游戏名称都为“崩坏:星穹铁道”,因此无法仅从名称上判断窗口是哪个,还需要进一步判断是启动器还是游戏。可以通过窗口上是否有启动器上独有的字符判断是否为游戏。
因此,窗口检测的流程如下图所示。

检查星穹铁道窗口
存在星穹铁道
检查特征字符
当前是旧启动器
游戏已启动
点击打开游戏
不存在星穹铁道
检查米哈游启动器窗口
不存在任何启动器
搜寻启动器并打开
启动了米哈游启动器

其相关代码在start_game.py中,该部分代码能够实现自动识别当前是否有游戏窗口,如果无窗口则逐步实现打开游戏。

副本导航

该部分代码放置在daily_tasks.py中。
导航的第一步是打开星际和平指南,该步较为简单,直接使用虚拟手柄打开轮盘,然后拨到对应位置即可。

def open_star_guide(self):

	# 打开星际和平指南
	
	self.gp.press_button(LEFT_SHOULDER)
	
	self.gp.joystick_movement(np.pi * 1 / 4, duration= 1) # 移动到指南
	
	time.sleep(0.8)
	
	self.gp.joystick_movement(amplitude=0)
	
	self.gp.release_button(LEFT_SHOULDER)
	
	self.pigeon("打开星际和平指南")
	
	time.sleep(0.9)

随后需要进一步识别星际和平指南的页面,以及寻找对应的副本。流程如下:

经验/武器经验/信用点
行迹材料
突破材料
遗器
打开星际和平指南
领取日常奖励
每日实训
清体力
生存索引
拟造花萼金
拟造花萼赤
凝滞虚影
侵蚀隧洞
和平指南页面识别

对于和平指南的页面,如每日实训、生存索引的识别,只需通过ocr识别有无对应字符即可找到该页面

def find_page(self,tag = "生存索引"):

# 已经打开星际和平指南后,通过手柄切换标签找到对应的page

	if TargetDetector(self.window,self.gp).search_button(tag, RIGHT_SHOULDER):
	
		self.pigeon("找到" + tag)
	
	else:
	
		self.pigeon("未找到" + tag)

其中TargetDetect中的search_button为递归寻找,直到满足条件。

def search_button(self, btn_text, action):
	
	"""
	
	查找相应按钮
	
	:param btn_text: 目标按钮名称
	
	:param action: 找不到按钮对应操作
	
	:return:
	
	"""
	
	figure, _, _, _, _ = self.win_action.get_screenshot(self.window)
	
	find, _ = self.findText(figure,btn_text)
	
	if find:
		
		print(f'找到{btn_text}')
		
		return True
	
	else:
		
		self.gp.click_button(action)
		
		time.sleep(0.2 + random.randint(0, 10) / 100)
		
		return self.search_button(btn_text, action)
页面中高亮位置的寻找

在确定找到页面后,我们需要识别出当前高亮的标签(如拟造花萼、侵蚀隧洞)是哪个。
此处我们可以先使用OCR识别有无目标文字,如果没有,说明当前页面不存在目标标签,需要继续翻页。如果存在,通过灰度阈值识别目标文字所在区域的灰度是否是选中的灰度,如果是则退出寻找,否则继续寻找。

范围内
范围外
查询目标文字
按下down_button
计算目标文字所在区域灰度
结束查找
def find_highlight(self, btn_text, action=None): 
	""" 
	寻找按钮的高亮状态。
	通过截图并查找按钮文本,判断按钮是否处于高亮状态。如果按钮未高亮,则点击按钮并重试。 
	主要用于自动化测试中对按钮状态的判断和操作。 
	参数: 
	btn_text (str): 按钮的文本内容,用于查找按钮。 
	action (function, optional): 当按钮未高亮时执行的操作,默认为None。可以是一个函数,该函数会在按钮未高亮时被调用。 
	返回: 
	bool: 如果按钮处于高亮状态,则返回True;否则返回False。 
	""" 
	# 截取窗口的屏幕快照 
	# 转到灰度上看灰度值。先截取字附近的区域 
	figure, _, _, _, _ = self.win_action.get_screenshot(self.window) # 在屏幕快照中查找按钮文本 
	find, pos = self.findText(figure, btn_text) 
	# 如果按钮未找到 
	if not find: # 没找到
		
		self.gp.click_button(action)
		
		time.sleep(0.2 + random.randint(0, 10) / 100)
		
		return self.find_highlight(btn_text, action)
	
	  
	
	ave_gray = self.get_average_gray_value(figure,pos)
	
	if ave_gray < 100: # 高亮-黑色
	
		print(f'找到{btn_text}')
		
		return True
	
	else:
	
		self.gp.click_button(action)
		
		time.sleep(0.2 + random.randint(0, 10) / 100)
		
		return self.find_highlight(btn_text, action)
右侧具体副本的寻找

使用手柄的话,右侧选择的副本会有一个橙色的矩形框作为高亮,因此识别矩形框就知道我们当前选中的是哪个了。
使用HSV颜色空间对橙色进行区分的效果并不理想。因此还是采用了矩形边框识别。

    def detect_dungeon_boxes(self,image,text):
        # 可以找出当前高亮的选择区域
        gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
        edges = cv2.Canny(gray, threshold1=100, threshold2=200)
        contours, _ = cv2.findContours(edges.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
        min_area_threshold = 10  # 设定最小面积阈值
        height, width = image.shape[:2]  # 获取图像高度和宽度
        area_ratio = 0.2
        target_area = height * width * area_ratio  # 计算目标最大面积
        max_area = 0
        for contour in contours:
            area = cv2.contourArea(contour)
            if area > max_area and area < target_area:
                max_area = area
                max_contour = contour


        for contour in contours:
            area = cv2.contourArea(contour)
            if area > min_area_threshold:
                # 计算边界框
                x, y, w, h = cv2.boundingRect(contour)

                    # 绘制矩形
                cv2.rectangle(image, (x, y), (x+w, y+h), (0, 255, 0), 2)
        # 如果找到了符合条件的轮廓
        if max_contour is not None:
            # 获取边界框
            x, y, w, h = cv2.boundingRect(max_contour)
            # 截取该矩形区域
            cropped_image = image[y:y+h, x:x+w]
            find, pos = self.findText(cropped_image,text)
            # 显示截取的图像
            # cv2.imshow('Cropped Rectangle', cropped_image)
            # cv2.waitKey(0)
            # cv2.destroyAllWindows()
            return find
        else:
            # print("Not found.")
            return False
    def find_dungeon(self,btn_text, action = None):
        figure, _, _, _, _ = self.win_action.get_screenshot(self.window)
        # self.gp.click_button(action)
        if self.detect_dungeon_boxes(figure,btn_text):
            print('已选中:' + btn_text)
        else:
            self.gp.click_button(action)
            time.sleep(0.2 + random.randint(0, 10) / 100)
            return self.find_dungeon(btn_text, action)

未完待续

GUI、消息窗口、邮件等功能以及当前版本未实现的功能待后续更新。

开源地址

  • 9
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

豆沙粽子好吃嘛!

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值