You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

游戏技能范围边界计算优化及相机角度适配问询

问题描述

我正在开发一款游戏的技能范围计算脚本,当前脚本用椭圆标识技能范围(如Image 1所示),但实际需要实现的是Image 2那种非圆非椭圆的技能范围边界。我找不到当前脚本的问题所在,而且游戏支持相机角度调整,推测需要引入三维(z轴)相关信息,但不知道具体怎么实现。希望优化脚本以实现正确的范围计算,还要适配不同相机角度(如Image 3-5所示)。现有脚本如下:

import cv2
import numpy as np
import pyautogui
import win32gui


window_name = "Game Window"
hwnd = win32gui.FindWindow(None, window_name)
window_rect = win32gui.GetWindowRect(hwnd)
screen_width = window_rect[2]
screen_height = window_rect[3]

# Range in pixels.
SKILL_RANGE_LIMIT = 380

# Defines the center (the caster is in the center of the screen).
center_x = screen_width / 2
center_y = screen_height / 2

# Defines the color of the circle (in BGR format).
circle_color = (0, 255, 0)  # Green

while True:
    # Gets the current mouse position.
    mouse_x, mouse_y = pyautogui.position()

    # Gets the caster position (it's actually the center of the screen).
    caster_x, caster_y = screen_width // 2, screen_height // 2

    # Calculates the distance between the caster and the target.
    distance = np.sqrt((mouse_x - caster_x) ** 2 + (mouse_y - caster_y) ** 2)

    # Checks if the mouse is within the skill range limit.
    if distance <= SKILL_RANGE_LIMIT:
        print("Inside")

    else:
        print("Outside")

        # Calculates the normalized direction vector from the caster to the max range.
        dx = mouse_x - caster_x
        dy = mouse_y - caster_y
        direction = np.array([dx, dy]) / distance

        # Calculates the new mouse position at the edge of the limit.
        new_x = int(caster_x + direction[0] * SKILL_RANGE_LIMIT)
        new_y = int(caster_y + direction[1] * SKILL_RANGE_LIMIT)

        # Moves the mouse to the new position.
        pyautogui.moveTo(new_x, new_y, _pause=False)

    screenshot = np.array(pyautogui.screenshot(region=window_rect))

    # Draws the skill range limit ellipse.
    img = cv2.ellipse(screenshot, (caster_x, caster_y), (SKILL_RANGE_LIMIT, SKILL_RANGE_LIMIT), 0, 0, 360, (0, 255, 0), 2)

    cv2.imshow(window_name, img)

    # Waits for a key press to exit the script.
    if cv2.waitKey(1) & 0xFF == ord("q"):
        break

# Cleans up the window.
cv2.destroyAllWindows()
问题分析与优化方案

核心问题

当前脚本直接用2D屏幕距离和椭圆绘制,完全忽略了3D世界到2D屏幕的透视投影逻辑。游戏中的技能范围是3D空间内的区域,相机角度变化时,3D范围会被投影成不同的2D形状,绝非简单椭圆能匹配。

优化步骤

1. 引入3D到2D的投影转换

技能范围是3D世界中的区域(比如圆形、扇形、自定义多边形),需要先定义3D范围,再通过相机参数投影到屏幕:

  • 需获取游戏相机参数:包括相机位置、朝向、FOV(视野角)、近远裁剪面;若无法直接获取,可通过游戏内已知3D点(如角色位置、地面标记)反向计算投影矩阵。
  • 用投影矩阵将3D范围的顶点转换为屏幕坐标,再连接这些坐标绘制边界。

2. 替换距离判断逻辑

当前的2D屏幕距离判断不符合3D世界实际范围,需改为:

  • 将鼠标屏幕坐标转换为3D射线(通过相机反投影)。
  • 判断射线与技能3D范围的交点,或计算角色到射线与地面交点的3D距离,再和技能范围阈值比较。

3. 自定义范围的绘制实现

如果技能是自定义形状(如扇形、矩形):

  • 在3D世界中定义该形状的顶点。
  • 将每个顶点投影为屏幕坐标。
  • 使用cv2.polylines()连接坐标点,绘制准确边界。

4. 适配相机角度变化

每次相机角度改变时,重新计算投影矩阵,再重新投影3D顶点到屏幕,更新绘制的边界。

示例修改代码(核心逻辑框架)

import cv2
import numpy as np
import pyautogui
import win32gui

window_name = "Game Window"
hwnd = win32gui.FindWindow(None, window_name)
window_rect = win32gui.GetWindowRect(hwnd)
screen_width = window_rect[2] - window_rect[0]
screen_height = window_rect[3] - window_rect[1]

# 角色在3D世界中的位置(需从游戏获取或校准)
caster_world_pos = np.array([0.0, 0.0, 0.0])
# 技能在3D世界中的实际范围(比如10米)
skill_range_3d = 10.0
# 相机参数(需根据游戏实际情况调整或动态获取)
camera_params = {
    "pos": np.array([5.0, 5.0, 3.0]),
    "target": np.array([0.0, 0.0, 0.0]),
    "fov": 60.0,
    "aspect_ratio": screen_width / screen_height,
    "near": 0.1,
    "far": 100.0
}

def get_projection_matrix(cam_params):
    # 计算透视投影矩阵
    fov_rad = np.radians(cam_params["fov"])
    tan_half_fov = np.tan(fov_rad / 2)
    proj_mat = np.zeros((4,4))
    proj_mat[0,0] = 1 / (cam_params["aspect_ratio"] * tan_half_fov)
    proj_mat[1,1] = 1 / tan_half_fov
    proj_mat[2,2] = -(cam_params["far"] + cam_params["near"]) / (cam_params["far"] - cam_params["near"])
    proj_mat[2,3] = -2 * cam_params["far"] * cam_params["near"] / (cam_params["far"] - cam_params["near"])
    proj_mat[3,2] = -1
    return proj_mat

def get_view_matrix(cam_pos, target_pos):
    # 计算视图矩阵(相机朝向转换)
    forward = target_pos - cam_pos
    forward = forward / np.linalg.norm(forward)
    right = np.cross(np.array([0.0, 1.0, 0.0]), forward)
    right = right / np.linalg.norm(right)
    up = np.cross(forward, right)
    view_mat = np.eye(4)
    view_mat[0:3, 0] = right
    view_mat[0:3, 1] = up
    view_mat[0:3, 2] = -forward
    view_mat[0:3, 3] = -np.dot(right, cam_pos), -np.dot(up, cam_pos), np.dot(forward, cam_pos)
    return view_mat

def world_to_screen(world_pos, view_mat, proj_mat, screen_size):
    # 将3D世界坐标转换为屏幕坐标
    homogenous_pos = np.array([world_pos[0], world_pos[1], world_pos[2], 1.0])
    view_pos = np.dot(view_mat, homogenous_pos)
    proj_pos = np.dot(proj_mat, view_pos)
    if proj_pos[3] == 0:
        return None
    ndc_pos = proj_pos / proj_pos[3]
    screen_x = (ndc_pos[0] + 1) * screen_size[0] / 2
    screen_y = (1 - ndc_pos[1]) * screen_size[1] / 2
    return (int(screen_x), int(screen_y))

def generate_skill_range_3d_points(caster_pos, range_radius, num_points=32):
    # 生成3D技能范围顶点(可替换为自定义形状的顶点生成逻辑)
    points = []
    for i in range(num_points):
        angle = 2 * np.pi * i / num_points
        x = caster_pos[0] + range_radius * np.cos(angle)
        z = caster_pos[2] + range_radius * np.sin(angle)
        y = caster_pos[1]
        points.append(np.array([x, y, z]))
    return points

while True:
    mouse_x, mouse_y = pyautogui.position()
    win_mouse_x = mouse_x - window_rect[0]
    win_mouse_y = mouse_y - window_rect[1]

    # 计算当前相机的视图和投影矩阵
    view_mat = get_view_matrix(camera_params["pos"], camera_params["target"])
    proj_mat = get_projection_matrix(camera_params)

    # 转换3D技能范围顶点到屏幕坐标
    skill_world_points = generate_skill_range_3d_points(caster_world_pos, skill_range_3d)
    skill_screen_points = []
    for wp in skill_world_points:
        sp = world_to_screen(wp, view_mat, proj_mat, (screen_width, screen_height))
        if sp is not None:
            skill_screen_points.append(sp)
    skill_screen_points = np.array(skill_screen_points, np.int32)

    # 鼠标范围判断逻辑(需替换为真实的3D射线检测)
    in_range = False

    if in_range:
        print("Inside")
    else:
        print("Outside")
        # 鼠标边界移动逻辑(需基于3D范围投影实现)

    # 绘制技能范围
    screenshot = np.array(pyautogui.screenshot(region=window_rect))
    if len(skill_screen_points) > 2:
        cv2.polylines(screenshot, [skill_screen_points], isClosed=True, color=(0,255,0), thickness=2)
    cv2.imshow(window_name, screenshot)

    if cv2.waitKey(1) & 0xFF == ord("q"):
        break

cv2.destroyAllWindows()

关键注意事项

  • 相机参数获取:若无法直接从游戏API获取,可通过校准实现——记录游戏内两个已知3D坐标点的屏幕位置,反向求解投影和视图矩阵。
  • 自定义范围适配:如果技能是扇形、矩形等非圆形,直接修改generate_skill_range_3d_points函数,生成对应形状的3D顶点即可。
  • 性能优化:投影计算和顶点转换可缓存,仅在相机参数或技能范围变化时重新计算,避免每帧重复运算。

内容的提问来源于stack exchange,提问作者KenOaza

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 20:28:09