<think>我们注意到日志中显示程序运行结束,但最后出现了一个异常:`time.sleep(sleep_time)`。同时,用户要求增加画出检测框的功能,并添加按键和鼠标动作序列(W、SHIFT、鼠标左键、右键、左键+右键)。
由于之前代码已经实现了自动化攻击(点击目标),现在需要增加:
1. 在屏幕上画出检测框(可选,但用户要求)
2. 在攻击前按顺序执行按键动作:W、SHIFT、然后鼠标左键、右键、左键+右键(同时按?)
然而,画出检测框可能会影响截图和性能,因此我们将提供一个可选的方式(比如调试模式)来显示检测框。
另外,按键序列需要设计一个动作序列执行器。
修改计划:
1. 在GameAutomation类中增加一个`draw_boxes`属性,控制是否绘制检测框(默认为False,避免影响性能)。
2. 修改`capture_screen`方法,如果开启绘制检测框,则在返回帧之前绘制检测框(注意:这会影响后续处理吗?不会,因为预处理前会重新letterbox,但显示给用户的可以绘制)。
但是注意,我们的截图是用于检测的,绘制检测框不会影响检测,因为我们是在检测之后绘制。
3. 我们可以将绘制检测框的功能放在检测到目标后,在原始帧上绘制,然后保存或显示(但显示需要另外开窗口,可能会干扰游戏)。
考虑到性能,我们可能只用于调试,所以可以保存截图或者弹窗显示(但自动化中弹窗会干扰,所以建议保存到文件或可选显示)。
4. 对于按键序列,我们需要重新设计攻击函数:
原攻击函数`safe_attack`只执行了移动鼠标和点击。现在需要先按键盘按键,再按鼠标键。
具体顺序:W -> SHIFT -> 鼠标左键 -> 鼠标右键 -> 鼠标左键+右键(同时按下左右键?)
5. 注意:按键动作需要模拟人类操作,包括按键之间的延迟。
我们将进行以下调整:
- 在`safe_attack`方法中,添加按键序列的执行。
- 增加一个`execute_attack_sequence`方法,专门执行按键序列。
但是,用户要求是“动作發現怪物”后就执行按键序列,也就是在攻击每个目标之前执行这个序列。
考虑到性能,我们可能需要将这个序列与鼠标点击结合起来,并且注意不要阻塞主循环。
步骤:
1. 移动鼠标到目标位置(已经做了)
2. 依次执行按键:按下W,释放W;按下Shift,释放Shift;然后执行鼠标点击:左键点击,右键点击,然后同时按下左右键(再释放)?
但是,注意:游戏中的组合键可能要求同时按下,所以我们需要模拟同时按下的效果。
然而,日志显示最后在`sleep_time`处出错,可能是sleep_time计算为负值?需要检查。
我们先解决异常:
在`run`方法中,计算`sleep_time = min(1.0, max(0.1, time_since_last_target * 0.5))`,但`time_since_last_target`可能是0(刚开始时),因此没问题。
但我们看到最后程序退出,可能是正常退出,但最后一行异常可能是由于在退出时试图sleep,但程序已经终止?
修改:
在退出循环后,我们不再执行后面的代码,所以可能是其他原因。我们检查一下日志,发现程序已经打印了“✅ 自动化程序已安全退出”,然后才出现异常。这可能是由于退出主循环后,在finally块中执行了停止操作,然后程序结束。最后的异常可能是键盘中断(用户按ESC)导致的正常退出,所以可以忽略。
但为了安全,我们在sleep之前检查`time_since_last_target`是否非负。
现在,我们按照要求修改代码:
1. 添加绘制检测框功能(可选):
我们在`get_targets`方法中返回目标的同时,也返回检测框的位置(xyxy),这样在需要绘制时可以使用。
修改`get_targets`方法,使其返回一个包含检测框信息的列表(包括坐标和类别、置信度)。
然后,在`run`循环中,如果开启了绘制,则调用一个绘制函数在原始图像上绘制,并保存或显示。
但由于我们不想影响主循环性能,我们可以只在调试模式下保存图像(比如每10帧保存一次)。
2. 修改攻击函数,加入按键序列。
由于用户要求比较紧急,我们先实现按键序列,绘制检测框作为可选功能后续再考虑。
具体修改如下:
步骤1:在GameAutomation的__init__中增加一个属性`draw_debug`(默认为False)来控制是否绘制检测框。
步骤2:在`run`方法中,如果`draw_debug`为True,则在检测到目标时绘制检测框,并保存为文件(避免显示窗口干扰游戏)。
步骤3:实现按键序列方法。
由于时间关系,我们先实现按键序列。绘制检测框的功能我们稍后添加。
修改`safe_attack`方法:
- 首先移动鼠标到目标位置(已经实现)
- 然后执行按键序列:依次按下并释放W、SHIFT、鼠标左键、鼠标右键,然后同时按下鼠标左右键,再释放。
注意:按键之间需要添加适当的延迟(模拟人类操作)。
代码调整如下:
在`safe_attack`方法中,在移动鼠标之后,添加:
```python
# 执行按键序列
keyboard.press('w')
time.sleep(0.1) # 短暂按下
keyboard.release('w')
time.sleep(0.05)
keyboard.press('shift')
time.sleep(0.1)
keyboard.release('shift')
time.sleep(0.05)
# 鼠标左键点击
pyautogui.click(button='left')
time.sleep(0.1)
# 鼠标右键点击
pyautogui.click(button='right')
time.sleep(0.1)
# 同时按下鼠标左右键
pyautogui.mouseDown(button='left')
pyautogui.mouseDown(button='right')
time.sleep(0.2) # 按住0.2秒
pyautogui.mouseUp(button='left')
pyautogui.mouseUp(button='right')
```
但是,注意:这样会连续执行,可能会影响游戏角色动作。另外,攻击冷却时间`self.attack_cooldown`已经包含了随机延迟,我们可能需要调整这个冷却时间,因为现在动作序列需要额外时间。
因此,我们可能需要重新设计冷却时间,将按键序列的时间也算进去。
考虑到这一点,我们可以在执行完整个序列后再等待剩余的冷却时间(原冷却时间减去序列执行时间)。但序列执行时间不固定,我们可以计算序列开始到结束的时间,然后调整等待时间。
但为了简单,我们暂时将冷却时间设置为固定值(比如1.5秒),并确保序列执行时间不超过这个冷却时间。
另外,我们注意到原攻击函数中已经有了一次点击(`pyautogui.click()`),所以我们需要去掉原来的点击,只执行新的按键序列。
修改:注释掉原来的`pyautogui.click()`,替换为按键序列。
同时,为了避免按键序列中的点击与移动鼠标后的点击重复,我们去掉原来单独的点击。
因此,修改后的`safe_attack`如下:
```python
def safe_attack(self, target):
try:
x, y = target['center']
offset_x = np.random.randint(-5, 5)
offset_y = np.random.randint(-5, 5)
current_x, current_y = pyautogui.position()
dx = max(1, abs(x - current_x))
dy = max(1, abs(y - current_y))
distance = (dx**2 + dy**2)**0.5
move_duration = max(0.05, min(0.3, distance / 2000))
pyautogui.moveTo(x + offset_x, y + offset_y, duration=move_duration)
# 不再单独点击,而是执行按键序列
# 记录开始时间
sequence_start = time.time()
# 执行按键序列
keyboard.press('w')
time.sleep(0.1) # 短暂按下
keyboard.release('w')
time.sleep(0.05)
keyboard.press('shift')
time.sleep(0.1)
keyboard.release('shift')
time.sleep(0.05)
# 鼠标左键点击
pyautogui.click(button='left')
time.sleep(0.1)
# 鼠标右键点击
pyautogui.click(button='right')
time.sleep(0.1)
# 同时按下鼠标左右键
pyautogui.mouseDown(button='left')
pyautogui.mouseDown(button='right')
time.sleep(0.2) # 按住0.2秒
pyautogui.mouseUp(button='left')
pyautogui.mouseUp(button='right')
# 计算序列执行时间
sequence_time = time.time() - sequence_start
# 剩余的冷却时间(至少0.1秒)
remaining_cooldown = max(0.1, self.attack_cooldown - sequence_time)
time.sleep(remaining_cooldown + np.random.uniform(0, 0.3))
print(f"⚔️ 攻击目标 {target['class']} @ ({x}, {y}) | 置信度: {target['confidence']:.2f}")
self.last_target_time = time.time()
except Exception as e:
print(f"⚠️ 攻击目标失败: {str(e)}")
```
注意:这里使用`keyboard.press`和`keyboard.release`来模拟键盘按键,需要确保已安装`keyboard`库。同时,使用`pyautogui`模拟鼠标。
另外,原攻击函数中的冷却时间等待被替换为等待序列执行后的剩余冷却时间。
但是,这样修改后,每个目标都会执行一整套按键序列,可能会影响效率。用户要求是发现怪物就执行,所以我们这样做。
关于绘制检测框,我们暂时不实现,因为用户没有强制要求立即显示。如果需要,我们可以后续添加。
最后,我们还需要注意,在程序退出时,确保释放所有资源,包括停止DXCam。
我们已经看到程序在退出时打印了“✅ 自动化程序已安全退出”,所以异常可能是由于按ESC退出时导致的,我们在主循环中捕获了键盘中断,所以可以忽略。
现在,我们根据以上修改调整代码。
注意:由于按键序列增加了操作,可能会延长每个目标的处理时间,因此可能需要调整目标数量(`self.max_targets`)或者帧检测频率(现在每2帧检测一次)。
我们将修改后的完整代码提供如下(包含按键序列,但不包含绘制检测框):
</think>根据您的需求,我将在代码中添加以下功能:
1. 在检测到目标时绘制检测框
2. 实现按键序列:W、SHIFT、鼠标左键、右键、左键+右键
3. 优化攻击序列逻辑
以下是修改后的完整代码:
```python
import cv2
import numpy as np
import win32gui
import time
import pyautogui
import keyboard
import warnings
import sys
import torch
import win32api
from models.experimental import attempt_load
from utils.general import non_max_suppression, scale_coords
import dxcam
# 自定义letterbox实现
def letterbox(im, new_shape=(320, 320), color=(114, 114, 114), auto=True, scaleFill=False, scaleup=True, stride=32):
"""安全版letterbox,避免除零错误"""
shape = im.shape[:2]
if shape[0] == 0 or shape[1] == 0:
raise ValueError("图像尺寸无效:高度或宽度为0")
if isinstance(new_shape, int):
new_shape = (new_shape, new_shape)
r = min(new_shape[0] / max(shape[0], 1), new_shape[1] / max(shape[1], 1))
if not scaleup:
r = min(r, 1.0)
new_unpad = int(round(shape[1] * r)), int(round(shape[0] * r))
dw, dh = max(0, new_shape[1] - new_unpad[0]), max(0, new_shape[0] - new_unpad[1])
if auto:
dw, dh = np.mod(dw, stride), np.mod(dh, stride)
elif scaleFill:
dw, dh = 0.0, 0.0
new_unpad = (new_shape[1], new_shape[0])
dw /= 2
dh /= 2
if shape[::-1] != new_unpad:
im = cv2.resize(im, new_unpad, interpolation=cv2.INTER_LINEAR)
top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1))
left, right = int(round(dw - 0.1)), int(round(dw + 0.1))
im = cv2.copyMakeBorder(im, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color)
return im, (r, r), (dw, dh)
# 过滤警告
warnings.filterwarnings("ignore", category=UserWarning, module="torch.*")
warnings.filterwarnings("ignore", category=UserWarning, module="models.*")
class GameAutomation:
def __init__(self, model_path, window_title=None):
self.device = self._detect_device()
print(f"🖥️ 使用设备: {self.device.upper()}")
self.half_precision = False
self.imgsz = 320
self.model, self.stride, self.names, self.input_channels = self._load_model(model_path)
print(f"🔢 模型输入通道: {self.input_channels}")
print(f"⚡ 半精度模式: {self.half_precision}")
print(f"📏 输入分辨率: {self.imgsz}x{self.imgsz}")
self.window_title = window_title
self.hwnd = None
self.left, self.top, self.right, self.bottom = 0, 0, 0, 0
self.width, self.height = 0, 0
self.attack_cooldown = 0.8
self.confidence_threshold = 0.7
self.max_targets = 3
self.running = True
self.frame_count = 0
self.start_time = time.time()
self.last_target_time = 0
self.total_inference_time = 0
self.camera = None
self.camera_fps = 0
self.frame_history = []
# 新增:检测框显示相关
self.show_boxes = True # 是否显示检测框
self.debug_window = "Detection Preview"
cv2.namedWindow(self.debug_window, cv2.WINDOW_NORMAL)
cv2.resizeWindow(self.debug_window, 800, 600)
# 新增:攻击序列配置
self.attack_sequence = [
{'type': 'key', 'key': 'w', 'duration': 0.1},
{'type': 'key', 'key': 'shift', 'duration': 0.15},
{'type': 'mouse', 'button': 'left', 'action': 'click', 'duration': 0.1},
{'type': 'mouse', 'button': 'right', 'action': 'click', 'duration': 0.1},
{'type': 'mouse', 'button': 'both', 'action': 'down', 'duration': 0.2},
{'type': 'mouse', 'button': 'both', 'action': 'up', 'duration': 0.1}
]
def _detect_device(self):
"""安全检测设备"""
try:
if torch.cuda.is_available():
return 'cuda'
elif torch.backends.mps.is_available():
return 'mps'
else:
return 'cpu'
except:
return 'cpu'
def _load_model(self, model_path):
"""安全加载模型"""
try:
model = attempt_load(model_path, device=self.device, fuse=True, inplace=True)
stride = int(model.stride.max())
names = model.module.names if hasattr(model, 'module') else model.names
input_channels = 3
for layer in model.model.children():
if isinstance(layer, torch.nn.Conv2d):
input_channels = layer.in_channels
break
if self.device != 'cpu':
dummy_input = torch.zeros(1, input_channels, self.imgsz, self.imgsz).to(self.device)
model(dummy_input)
if 'cuda' in self.device:
torch.cuda.synchronize()
return model, stride, names, input_channels
except Exception as e:
print(f"❌ 模型加载失败: {str(e)}")
sys.exit(1)
def find_game_window(self):
"""查找可见窗口"""
visible_windows = []
def enum_handler(hwnd, ctx):
if win32gui.IsWindowVisible(hwnd):
title = win32gui.GetWindowText(hwnd)
if title:
visible_windows.append(title)
win32gui.EnumWindows(enum_handler, None)
return visible_windows
def setup_window(self, title=None):
"""设置目标窗口"""
if title:
self.window_title = title
self.hwnd = win32gui.FindWindow(None, self.window_title)
if not self.hwnd:
all_windows = self.find_game_window()
for window_title in all_windows:
if self.window_title.lower() in window_title.lower():
self.hwnd = win32gui.FindWindow(None, window_title)
if self.hwnd:
print(f"✅ 匹配到窗口: {window_title}")
self.window_title = window_title
break
if not self.hwnd:
print("❌ 未找到游戏窗口,请从以下可见窗口中选择:")
for i, title in enumerate(self.find_game_window(), 1):
print(f"{i}. {title}")
sys.exit(1)
try:
rect = win32gui.GetWindowRect(self.hwnd)
self.left, self.top, self.right, self.bottom = rect
self.width = max(1, self.right - self.left)
self.height = max(1, self.bottom - self.top)
print(f"🖼️ 窗口尺寸: {self.width}x{self.height}")
monitor_area = (
max(0, self.left),
max(0, self.top),
min(win32api.GetSystemMetrics(0), self.left + self.width),
min(win32api.GetSystemMetrics(1), self.top + self.height)
)
if monitor_area[2] - monitor_area[0] > 0 and monitor_area[3] - monitor_area[1] > 0:
self.camera = dxcam.create(region=monitor_area, output_color="BGR")
self.camera.start(target_fps=60)
print("🎥 初始化DXCam截图成功")
self.frame_history = []
else:
raise ValueError(f"无效截图区域: {monitor_area}")
except Exception as e:
print(f"❌ 窗口设置失败: {str(e)}")
sys.exit(1)
def capture_screen(self):
"""安全截图方法(带FPS计算)"""
if not self.camera:
return None
try:
frame = self.camera.get_latest_frame()
if frame is None or frame.size == 0:
return None
now = time.time()
self.frame_history.append(now)
if len(self.frame_history) > 10:
self.frame_history.pop(0)
if len(self.frame_history) >= 2:
duration = self.frame_history[-1] - self.frame_history[0]
self.camera_fps = len(self.frame_history) / max(0.001, duration)
return frame
except Exception as e:
print(f"⚠️ 截图失败: {str(e)}")
return None
def safe_preprocess(self, img):
"""安全的图像预处理"""
if img is None or img.size == 0:
return None
try:
if img.shape[0] > 0 and img.shape[1] > 0:
img = letterbox(img, new_shape=self.imgsz, stride=self.stride, auto=True)[0]
else:
return None
img = img.transpose(2, 0, 1) # HWC to CHW
img = np.ascontiguousarray(img)
img = torch.from_numpy(img).to(self.device)
img = img.float() / 255.0 # 归一化
if img.shape[0] != self.input_channels:
if self.input_channels > 3:
img = img.repeat(self.input_channels // 3 + 1, 1, 1)[:self.input_channels, ...]
else:
img = img[:self.input_channels, ...]
return img.unsqueeze(0) # 添加batch维度
except Exception as e:
print(f"⚠️ 图像预处理失败: {str(e)}")
return None
def get_targets(self, img):
"""目标检测"""
if img is None or img.size == 0:
return []
try:
inference_start = time.time()
img_tensor = self.safe_preprocess(img)
if img_tensor is None:
return []
with torch.no_grad():
pred = self.model(img_tensor)[0]
pred = non_max_suppression(pred, self.confidence_threshold, 0.45)
targets = []
detections = []
for det in pred:
if det is not None and len(det):
det[:, :4] = scale_coords(img_tensor.shape[2:], det[:, :4], img.shape).round()
for *xyxy, conf, cls in det:
x1, y1, x2, y2 = map(int, xyxy)
center_x = self.left + (x1 + x2) // 2
center_y = self.top + (y1 + y2) // 2
screen_width = win32api.GetSystemMetrics(0)
screen_height = win32api.GetSystemMetrics(1)
if (0 <= center_x <= screen_width and 0 <= center_y <= screen_height):
target_data = {
'center': (center_x, center_y),
'confidence': conf.item(),
'class': self.names[int(cls)],
'bbox': (x1, y1, x2, y2) # 新增:保存边界框坐标
}
targets.append(target_data)
detections.append({
'bbox': (x1, y1, x2, y2),
'label': self.names[int(cls)],
'conf': conf.item()
})
self.total_inference_time += time.time() - inference_start
targets.sort(key=lambda x: x['confidence'], reverse=True)
# 新增:在原始图像上绘制检测框
if self.show_boxes and detections:
debug_img = img.copy()
for det in detections:
x1, y1, x2, y2 = det['bbox']
label = f"{det['label']} {det['conf']:.2f}"
cv2.rectangle(debug_img, (x1, y1), (x2, y2), (0, 255, 0), 2)
cv2.putText(debug_img, label, (x1, y1 - 10),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)
cv2.imshow(self.debug_window, debug_img)
cv2.waitKey(1)
return targets[:self.max_targets]
except Exception as e:
print(f"⚠️ 目标检测失败: {str(e)}")
return []
def execute_attack_sequence(self):
"""执行攻击序列"""
try:
for step in self.attack_sequence:
if step['type'] == 'key':
keyboard.press(step['key'])
time.sleep(step['duration'])
keyboard.release(step['key'])
elif step['type'] == 'mouse':
if step['button'] == 'both':
if step['action'] == 'down':
pyautogui.mouseDown(button='left')
pyautogui.mouseDown(button='right')
elif step['action'] == 'up':
pyautogui.mouseUp(button='left')
pyautogui.mouseUp(button='right')
elif step['action'] == 'click':
pyautogui.mouseDown(button='left')
pyautogui.mouseDown(button='right')
time.sleep(0.1)
pyautogui.mouseUp(button='left')
pyautogui.mouseUp(button='right')
else:
if step['action'] == 'down':
pyautogui.mouseDown(button=step['button'])
elif step['action'] == 'up':
pyautogui.mouseUp(button=step['button'])
elif step['action'] == 'click':
pyautogui.click(button=step['button'])
time.sleep(step['duration'])
return True
except Exception as e:
print(f"⚠️ 执行攻击序列失败: {str(e)}")
return False
def safe_attack(self, target):
"""安全的攻击方法(带攻击序列)"""
try:
x, y = target['center']
# 添加随机偏移
offset_x = np.random.randint(-5, 5)
offset_y = np.random.randint(-5, 5)
# 安全的鼠标移动
current_x, current_y = pyautogui.position()
# 避免除零错误
dx = max(1, abs(x - current_x))
dy = max(1, abs(y - current_y))
distance = (dx**2 + dy**2)**0.5
# 动态移动时间
move_duration = max(0.05, min(0.3, distance / 2000))
pyautogui.moveTo(x + offset_x, y + offset_y, duration=move_duration)
print(f"🎯 锁定目标 {target['class']} @ ({x}, {y}) | 置信度: {target['confidence']:.2f}")
# 执行攻击序列
sequence_start = time.time()
self.execute_attack_sequence()
sequence_time = time.time() - sequence_start
# 更新最后检测到目标的时间
self.last_target_time = time.time()
# 随机冷却时间(考虑序列执行时间)
remaining_cooldown = max(0.1, self.attack_cooldown - sequence_time)
time.sleep(remaining_cooldown + np.random.uniform(0, 0.3))
print(f"⚔️ 完成攻击序列 | 耗时: {sequence_time:.2f}s")
except Exception as e:
print(f"⚠️ 攻击目标失败: {str(e)}")
def run(self):
"""主循环"""
if not self.hwnd:
self.setup_window()
print(f"▶️ 自动化打怪系统启动 | 目标窗口: {self.window_title} | 按ESC退出")
self.start_time = time.time()
self.frame_count = 0
self.total_inference_time = 0
self.frame_history = []
try:
while self.running:
if keyboard.is_pressed('esc'):
print("⛔ 用户请求退出")
self.running = False
break
frame_start = time.time()
self.frame_count += 1
img = self.capture_screen()
if img is None:
time.sleep(0.1)
continue
if self.frame_count % 2 == 0:
targets = self.get_targets(img)
else:
targets = []
if targets:
for target in targets:
self.safe_attack(target)
else:
time_since_last_target = time.time() - self.last_target_time
sleep_time = min(1.0, max(0.1, time_since_last_target * 0.5))
time.sleep(sleep_time)
frame_time = time.time() - frame_start
if self.frame_count % 5 == 0 and self.frame_count > 0:
total_time = time.time() - self.start_time
fps = self.frame_count / max(0.001, total_time)
inference_percent = (self.total_inference_time / max(0.001, total_time)) * 100