Files
wow/player_movement.py

285 lines
13 KiB
Python
Raw Normal View History

2026-03-18 09:04:37 +08:00
import math
import time
import logging
import pyautogui
from player_position import PlayerPosition
# 游戏朝向约定:正北=0°正西=90°正南=180°正东=270°逆时针递增
# 游戏坐标约定人物面朝正北按w一秒坐标y会增加0.1 反之减少0.1 。如果人物面朝正东按w一秒坐标x会增加0.1 如果人物面朝正西按w一秒坐标x会减少0.1 。
# 按键w前进按键a左转按键d右转按键s后退 (转向时最好按0.5s就停止,以免转向过度)
# 当前人物的坐标和朝向根据player_position.py模块获取
class PlayerMovement:
MOVE_SPEED = 0.1 # 每秒移动距离(坐标单位)
TURN_RATE = 216.0 # 每秒转向角度(度);实测约 216°/s0.5s ≈ 108°
MAX_TURN_DURATION = 0.5 # 原地转向单次最长按键时间(秒),避免转向过度
SMOOTH_TURN_DURATION = 0.15 # 边走边转时每次按键时长(秒)
ANGLE_TOLERANCE = 5.0 # 朝向允许误差(度)
COARSE_TURN_THRESHOLD = 90.0 # 超过此角度差则停下来原地粗对齐,再继续行走
HEADING_CHECK_INTERVAL = 5 # 每隔几次位置读取才做一次朝向校正
STUCK_CHECK_COUNT = 3 # 连续多少次坐标无明显位移则判定为卡死
STUCK_MOVE_THRESHOLD = 0.05 # 每次检测周期内最小期望位移(坐标单位)
# 实测方向:按 a 朝向减小(右转),按 d 朝向增大(左转)
# 游戏朝向为顺时针:北=0°, 东=90°, 南=180°, 西=270°
def __init__(self):
self.player_position = PlayerPosition()
self.logger = logging.getLogger(__name__)
def get_required_heading(self, current_x: float, current_y: float,
target_x: float, target_y: float) -> float:
"""计算从当前位置朝向目标位置所需的游戏朝向角度。
实测游戏规律
朝向 0° y 减小朝向 180° y 增加
朝向 90° x 增加朝向 270° 西x 减小
dx_actual sin(heading)dy_actual -cos(heading)
因此目标朝向 = atan2(dx, -dy)
验证需北移(dy>0) atan2(0,-1)=180° 需东移(dx>0) atan2(1,0)=90°
"""
dx = target_x - current_x
dy = target_y - current_y
if dx == 0 and dy == 0:
return 0.0
return math.degrees(math.atan2(dx, -dy)) % 360
def turn_to_heading(self, target_heading: float, max_attempts: int = 10) -> bool:
"""转向到目标朝向。
按键 a 左转朝向增大按键 d 右转朝向减小
每次按键不超过 MAX_TURN_DURATION 多次微调直到误差在 ANGLE_TOLERANCE
Args:
target_heading: 目标朝向角度0~360
max_attempts: 最大调整次数
Returns:
bool: 是否成功对准目标朝向
"""
for attempt in range(max_attempts):
current_heading = self.player_position.get_heading_with_retry()
if current_heading is None:
self.logger.warning("无法获取当前朝向,转向中止")
return False
diff = (target_heading - current_heading) % 360
if diff <= self.ANGLE_TOLERANCE or diff >= (360 - self.ANGLE_TOLERANCE):
self.logger.info(f"朝向已对齐:当前 {current_heading:.1f}°,目标 {target_heading:.1f}°")
return True
# 实测d 使朝向增大a 使朝向减小(与按键名称相反)
# diff ≤ 180需增大朝向 → 按 ddiff > 180需减小朝向 → 按 a
if diff <= 180:
key, angle_to_turn = 'd', diff
else:
key, angle_to_turn = 'a', 360 - diff
turn_duration = min(angle_to_turn / self.TURN_RATE, self.MAX_TURN_DURATION)
self.logger.info(
f"[转向 {attempt + 1}] 当前 {current_heading:.1f}° → 目标 {target_heading:.1f}°,"
f"'{key}' {turn_duration:.2f}s需转 {angle_to_turn:.1f}°)"
)
pyautogui.keyDown(key)
time.sleep(turn_duration)
pyautogui.keyUp(key)
time.sleep(0.15) # 等待游戏刷新朝向
self.logger.warning(f"转向失败:超过最大尝试次数 {max_attempts}")
return False
def move_forward(self, duration: float):
"""按住 w 键向前移动指定时间。
Args:
duration: 移动时间
"""
self.logger.info(f"向前移动 {duration:.2f}s")
pyautogui.keyDown('w')
time.sleep(duration)
pyautogui.keyUp('w')
def _escape_stuck(self):
"""卡死脱困组合拳:后退 + 随机转向 + 跳跃。
当连续多次检测到坐标无明显位移时调用尝试脱离障碍物
"""
import random
self.logger.warning("检测到角色卡死,执行脱困动作")
# 1. 松开前进键,后退 0.8s
pyautogui.keyUp('w')
pyautogui.keyDown('s')
time.sleep(0.8)
pyautogui.keyUp('s')
# 2. 随机左转或右转 0.3~0.5s
turn_key = random.choice(['a', 'd'])
turn_dur = random.uniform(0.3, 0.5)
pyautogui.keyDown(turn_key)
time.sleep(turn_dur)
pyautogui.keyUp(turn_key)
# 3. 跳跃(按空格)同时向前冲 0.5s
pyautogui.keyDown('w')
pyautogui.press('space')
time.sleep(0.5)
pyautogui.keyUp('w')
self.logger.info("脱困动作完成,重新开始前进")
def move_to(self, target_x: float, target_y: float,
position_tolerance: float = 0.5, max_iterations: int = 100) -> bool:
"""平滑移动人物到目标坐标(边走边转向)。
策略
1. 起步前若角度偏差 > COARSE_TURN_THRESHOLD先原地粗对齐
2. 按住 w 持续前进每次读取朝向后同时按 a/d 微调方向边走边转
3. 若行走中出现大角度偏差绕过障碍等短暂停步原地修正后继续
4. 到达判定半径内松开 w结束移动
Args:
target_x, target_y: 目标坐标
position_tolerance: 到达判定半径坐标单位默认 0.5
max_iterations: 最大循环次数防止死循环
Returns:
bool: 是否成功到达目标位置
"""
self.logger.info(f"开始平滑移动到目标位置 ({target_x}, {target_y})")
# 起步前先判断是否已在目标范围内,避免重复调用时原地乱转
init_pos = self.player_position.get_position_with_retry()
if init_pos is None:
self.logger.warning("无法获取当前坐标,移动中止")
return False
if (abs(target_x - init_pos[0]) <= position_tolerance and
abs(target_y - init_pos[1]) <= position_tolerance):
self.logger.info(f"已在目标位置 ({target_x}, {target_y}) 范围内,无需移动")
return True
# 起步前粗对齐:角度偏差过大时先原地转向,避免一开始就走偏
init_heading = self.player_position.get_heading_with_retry()
if init_heading is not None:
init_required = self.get_required_heading(init_pos[0], init_pos[1], target_x, target_y)
init_diff = (init_required - init_heading) % 360
init_abs_diff = init_diff if init_diff <= 180 else 360 - init_diff
if init_abs_diff > self.COARSE_TURN_THRESHOLD:
self.logger.info(f"起步角度差 {init_abs_diff:.1f}°,先原地粗对齐")
if not self.turn_to_heading(init_required):
self.logger.warning("起步粗对齐失败,移动中止")
return False
# 按住 w 开始持续前进
pyautogui.keyDown('w')
# 卡死检测:记录最近 STUCK_CHECK_COUNT 次坐标,用于判断是否停滞
recent_positions = []
try:
for iteration in range(max_iterations):
# 每隔 HEADING_CHECK_INTERVAL 次才读一次朝向并校正
# 读取当前位置OCR 耗时期间 w 持续按住,角色一直在走)
current_pos = self.player_position.get_position_with_retry()
if current_pos is None:
self.logger.warning("无法获取当前坐标,移动中止")
return False
current_x, current_y = current_pos
dx = target_x - current_x
dy = target_y - current_y
distance = math.sqrt(dx ** 2 + dy ** 2)
self.logger.info(
f"[平滑 {iteration + 1}] 当前 ({current_x}, {current_y})"
f"目标 ({target_x}, {target_y}),剩余距离 {distance:.2f}"
)
# 卡死检测:维护滑动窗口,比较最新与最旧坐标的位移
recent_positions.append((current_x, current_y))
if len(recent_positions) > self.STUCK_CHECK_COUNT:
recent_positions.pop(0)
if len(recent_positions) == self.STUCK_CHECK_COUNT:
oldest_x, oldest_y = recent_positions[0]
moved = math.sqrt((current_x - oldest_x) ** 2 + (current_y - oldest_y) ** 2)
if moved < self.STUCK_MOVE_THRESHOLD:
self._escape_stuck()
recent_positions.clear()
pyautogui.keyDown('w')
# x 和 y 都在容差范围内即视为到达
if abs(dx) <= position_tolerance and abs(dy) <= position_tolerance:
self.logger.info(f"已到达目标位置 ({target_x}, {target_y})")
return True
# 接近目标时松开 w 停下,再做一次静止判定,避免冲过头后死循环
if distance <= position_tolerance * 2:
pyautogui.keyUp('w')
time.sleep(0.2)
final_pos = self.player_position.get_position_with_retry()
if final_pos is not None:
fdx = target_x - final_pos[0]
fdy = target_y - final_pos[1]
if abs(fdx) <= position_tolerance and abs(fdy) <= position_tolerance:
self.logger.info(f"已到达目标位置 ({target_x}, {target_y})")
return True
# 静止后仍未到达,重新按住 w 继续前进
pyautogui.keyDown('w')
# 每隔 HEADING_CHECK_INTERVAL 次读一次朝向,减少 OCR 频率
if iteration % self.HEADING_CHECK_INTERVAL != 0:
continue
current_heading = self.player_position.get_heading_with_retry()
if current_heading is None:
continue
required_heading = self.get_required_heading(current_x, current_y, target_x, target_y)
diff = (required_heading - current_heading) % 360
abs_diff = diff if diff <= 180 else 360 - diff
if abs_diff > self.COARSE_TURN_THRESHOLD:
# 偏差过大:停步原地粗修正,再重新按住 w
pyautogui.keyUp('w')
self.logger.info(f"偏差 {abs_diff:.1f}° 过大,停步原地修正")
if not self.turn_to_heading(required_heading):
self.logger.warning("修正转向失败,移动中止")
return False
pyautogui.keyDown('w')
elif abs_diff > self.ANGLE_TOLERANCE:
# 小幅偏差:边走边转,同时按住 w 和转向键
# diff ≤ 180 需增大朝向 → ddiff > 180 需减小朝向 → a
turn_key = 'd' if diff <= 180 else 'a'
turn_time = min(abs_diff / self.TURN_RATE, self.SMOOTH_TURN_DURATION)
self.logger.info(
f"边走边转:按 '{turn_key}' {turn_time:.2f}s偏差 {abs_diff:.1f}°)"
)
pyautogui.keyDown(turn_key)
time.sleep(turn_time)
pyautogui.keyUp(turn_key)
else:
# 方向已对齐,让角色多走一段再重新检测,减少 OCR 频率
time.sleep(1)
finally:
pyautogui.keyUp('w')
self.logger.warning(f"移动失败:超过最大迭代次数 {max_iterations}")
return False
def move_to_position(self, target_x: float, target_y: float,
tolerance: float = 0.5, max_iterations: int = 100) -> bool:
"""移动人物到目标坐标combat_engine 对外接口)。
Args:
target_x, target_y: 目标坐标
tolerance: 到达判定半径坐标单位默认 0.5
max_iterations: 最大循环次数
Returns:
bool: 是否成功到达目标位置
"""
return self.move_to(target_x, target_y,
position_tolerance=tolerance,
max_iterations=max_iterations)