first commit
This commit is contained in:
284
player_movement.py
Normal file
284
player_movement.py
Normal file
@@ -0,0 +1,284 @@
|
||||
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°/s(0.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:需增大朝向 → 按 d;diff > 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 需增大朝向 → d;diff > 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)
|
||||
Reference in New Issue
Block a user