1. 项目概述:基于智谱大模型+DQN强化学习的《上古卷轴5》自动化控制
这个项目实现了一个能够自动玩《上古卷轴5》的AI系统,核心思路是将智谱大模型的视觉理解能力与DQN强化学习算法相结合。系统通过实时截图获取游戏画面,由大模型分析当前场景并生成动作决策,再通过DQN算法优化决策策略,最终将动作指令发送到游戏窗口执行。
整个系统包含三个关键模块:
- 游戏控制模块(gamecontrol.py):负责精确控制游戏角色动作
- 屏幕捕捉模块(screemcatch.py):实时获取游戏画面
- AI决策模块(AiHowtodo.py):结合大模型和DQN进行智能决策
2. 游戏控制模块深度解析
2.1 窗口与进程管理
游戏控制的核心在于精确获取和操作游戏窗口。我们使用Windows API通过窗口句柄(hwnd)和进程ID(pid)来定位和控制游戏:
python复制def get_game_hwnd_and_pid():
global game_hwnd, game_pid
game_hwnd = None
game_pid = 0
def enum_windows_callback(hwnd, lParam):
title = win32gui.GetWindowText(hwnd).lower()
game_keywords = ["skyrim", "上古卷轴", "the elder scrolls"]
if any(keyword in title for keyword in game_keywords):
if win32gui.IsWindowVisible(hwnd):
game_hwnd = hwnd
game_pid = win32process.GetWindowThreadProcessId(hwnd)[1]
return True
win32gui.EnumWindows(enum_windows_callback, None)
return game_hwnd, game_pid
关键技巧:窗口枚举时添加了中文和英文的游戏名称匹配,确保能识别各种语言版本的游戏窗口。同时检查窗口可见性(IsWindowVisible)避免匹配到后台窗口。
2.2 输入模拟的精准控制
游戏输入模拟面临两个主要挑战:
- 输入法干扰:中文输入法会导致按键失效
- 窗口焦点丢失:后台窗口无法接收输入
解决方案是通过Windows API实现硬件级输入模拟:
python复制def simulate_game_key(key_code, press_time=0.2):
# 临时开启大写锁定强制英文输入
caps_lock_state = win32api.GetKeyState(0x14)
if not (caps_lock_state & 1):
win32api.keybd_event(0x14, 0, 0, 0)
win32api.keybd_event(0x14, 0, win32con.KEYEVENTF_KEYUP, 0)
time.sleep(0.05)
# 硬件级按键模拟
scan_code = win32api.MapVirtualKey(key_code, 0)
win32api.keybd_event(key_code, scan_code, 0, 0)
# 长按期间持续检查窗口焦点
start_time = time.time()
while time.time() - start_time < press_time:
if game_hwnd and win32gui.GetForegroundWindow() != game_hwnd:
win32gui.SetForegroundWindow(game_hwnd)
time.sleep(0.1)
win32api.keybd_event(key_code, scan_code, win32con.KEYEVENTF_KEYUP, 0)
# 恢复大写锁定状态
if not (caps_lock_state & 1):
win32api.keybd_event(0x14, 0, 0, 0)
win32api.keybd_event(0x14, 0, win32con.KEYEVENTF_KEYUP, 0)
time.sleep(0.05)
实战经验:按键持续时间(press_time)限制在0.1-10秒之间,既保证了动作执行的灵活性,又避免了过长按键导致的游戏卡顿。测试发现0.2秒是最佳的基础动作时长。
2.3 动作执行系统
游戏支持7种基础动作,每种动作都映射到特定的按键或鼠标操作:
| 动作名称 | 按键映射 | 默认时长(秒) | 虚拟键码 |
|---|---|---|---|
| 攻击(attack) | 鼠标左键 | 0.2 | - |
| 拾取(pickup) | E键 | 0.15 | 0x45 |
| 前进(move_forward) | W键 | 2.0 | 0x57 |
| 后退(move_back) | S键 | 2.0 | 0x53 |
| 左移(move_left) | A键 | 2.0 | 0x41 |
| 右移(move_right) | D键 | 2.0 | 0x44 |
| 跳跃(jump) | 空格键 | 0.25 | 0x20 |
动作执行前会强制激活游戏窗口并提升进程优先级:
python复制def force_activate_game_window():
for _ in range(3): # 重试3次
try:
win32gui.ShowWindow(game_hwnd, win32con.SW_RESTORE)
current_thread = win32api.GetCurrentThreadId()
game_thread = win32process.GetWindowThreadProcessId(game_hwnd)[0]
user32.AttachThreadInput(current_thread, game_thread, True)
user32.SetForegroundWindow(game_hwnd)
user32.SetActiveWindow(game_hwnd)
user32.SetFocus(game_hwnd)
user32.AttachThreadInput(current_thread, game_thread, False)
# 移动鼠标到窗口中心
rect = win32gui.GetWindowRect(game_hwnd)
center_x = (rect[0] + rect[2]) // 2
center_y = (rect[1] + rect[3]) // 2
win32api.SetCursorPos((center_x, center_y))
return True
except Exception as e:
print(f"激活窗口失败:{e}")
time.sleep(0.1)
return False
3. 屏幕捕捉模块实现细节
3.1 游戏窗口区域获取
使用pygetwindow库定位游戏窗口,并考虑窗口边框的偏移量:
python复制def get_game_window_region():
for window_name in GAME_WINDOW_NAMES:
try:
windows = gw.getWindowsWithTitle(window_name)
if windows:
game_win = windows[0]
if not game_win.isMinimized:
# 修正窗口边框偏移
left = game_win.left + 8
top = game_win.top + 32
width = game_win.width - 16
height = game_win.height - 40
# 校验最小尺寸
width = max(800, width)
height = max(600, height)
return {"top": top, "left": left, "width": width, "height": height}
except Exception:
continue
return None
避坑指南:不同Windows主题的窗口边框厚度不同,这里采用固定偏移值(左+8,上+32,右+16,下+40)可以兼容大多数情况。如果遇到截图不准确,可以调整这些偏移参数。
3.2 高效截图实现
使用mss库进行高效截图,相比PIL.ImageGrab性能提升明显:
python复制def capture_screen(screenshot_count):
game_region = get_game_window_region()
if not game_region:
return None
# 使用mss截取并转换为numpy数组
with mss.mss() as sct:
img = sct.grab(game_region)
img_np = np.array(img)[:, :, :3] # 去除alpha通道
# 保存截图供调试
img_name = f"skyrim_{int(time.time())}_{screenshot_count}.png"
save_path = os.path.join(SAVE_DIR, img_name)
mss.tools.to_png(img.rgb, img.size, output=save_path)
return img_np
性能对比:
- PIL.ImageGrab: ~200ms/帧
- mss: ~50ms/帧
4. AI决策与DQN训练系统
4.1 动作模式设计
系统支持两种动作模式:
- 单动作模式(ACTION_MODE="single"):每次决策执行一个基础动作
- 组合动作模式(ACTION_MODE="combo"):每次决策生成1-5个连续动作
python复制# 在AiHowtodo.py中配置
ACTION_MODE = "combo" # 或 "single"
MAX_COMBO_ACTIONS = 3 # 组合动作最大数量
MIN_ACTION_DURATION = 0.1 # 最短动作时长
MAX_ACTION_DURATION = 5.0 # 最长动作时长
4.2 智谱大模型视觉决策
大模型接收游戏截图,输出动作决策:
python复制def llm_vision_decision(img_np):
# 将numpy数组转换为PIL图像
img_pil = Image.fromarray(img_np)
# 调用智谱大模型API
response = zhipuai.model_api.invoke(
model="visual_glm",
prompt="分析游戏场景并给出最佳动作",
images=[img_pil]
)
# 解析响应,生成动作序列
if ACTION_MODE == "single":
action_name = parse_single_action(response)
duration = calculate_duration(action_name)
return (action_name, duration), ACTION_MAP[action_name], extract_state(img_np)
else:
combo_actions = parse_combo_actions(response)
action_codes = [ACTION_MAP[action] for action, _ in combo_actions]
return combo_actions, action_codes, extract_state(img_np)
4.3 DQN强化学习实现
DQN算法通过经验回放和双网络结构来稳定训练:
python复制class DQNAgent:
def __init__(self, state_dim=10, action_dim=8, gamma=0.95, lr=0.001):
self.state_dim = state_dim
self.action_dim = action_dim
self.gamma = gamma # 折扣因子
self.epsilon = 1.0 # 探索率
self.epsilon_min = 0.01
self.epsilon_decay = 0.995
# 主网络和目标网络
self.model = self.build_model()
self.target_model = self.build_model()
self.update_target_network()
# 经验回放缓冲区
self.buffer = ReplayBuffer(capacity=10000)
def build_model(self):
model = Sequential()
model.add(Dense(64, input_dim=self.state_dim, activation='relu'))
model.add(Dense(64, activation='relu'))
model.add(Dense(self.action_dim, activation='linear'))
model.compile(loss='mse', optimizer=Adam(lr=self.lr))
return model
def update_target_network(self):
self.target_model.set_weights(self.model.get_weights())
def act(self, state):
if np.random.rand() <= self.epsilon:
return random.randrange(self.action_dim)
q_values = self.model.predict(state.reshape(1, -1))
return np.argmax(q_values[0])
def update(self, action):
if len(self.buffer) < 32: # 最小批次大小
return 0
# 从缓冲区采样
states, actions, rewards, next_states, dones = self.buffer.sample(32)
# 计算目标Q值
targets = self.model.predict(states)
next_q_values = self.target_model.predict(next_states)
targets[range(32), actions] = rewards + self.gamma * np.max(next_q_values, axis=1) * (1 - dones)
# 训练主网络
history = self.model.fit(states, targets, epochs=1, verbose=0)
loss = history.history['loss'][0]
# 衰减探索率
if self.epsilon > self.epsilon_min:
self.epsilon *= self.epsilon_decay
return loss
4.4 奖励函数设计
奖励函数根据前后状态变化计算,鼓励有益的游戏行为:
python复制def compute_reward(action, prev_state, curr_state):
# 基础奖励
reward = 0
# 根据动作类型给予不同权重
if action == ACTION_MAP["attack"]:
# 攻击成功会改变敌人状态
reward += (curr_state[ENEMY_HP_IDX] - prev_state[ENEMY_HP_IDX]) * 10
elif action == ACTION_MAP["pickup"]:
# 成功拾取物品会增加背包物品数
reward += (curr_state[ITEM_COUNT_IDX] - prev_state[ITEM_COUNT_IDX]) * 5
elif action in MOVEMENT_ACTIONS:
# 移动类动作根据位置变化给予奖励
distance = np.linalg.norm(curr_state[POS_IDX:POS_IDX+2] - prev_state[POS_IDX:POS_IDX+2])
reward += distance * 2
# 惩罚无效动作
if np.array_equal(prev_state, curr_state):
reward -= 1
return reward
5. 系统集成与实战测试
5.1 主控制流程
系统通过键盘监听启动/停止自动控制:
python复制def on_press(key):
global recording, training
try:
if key.char == 'o': # 启动
recording = True
training = True
print("启动自动控制...")
elif key.char == 'p': # 停止
recording = False
training = False
print("停止自动控制")
except AttributeError:
pass
listener = keyboard.Listener(on_press=on_press)
listener.start()
5.2 完整工作流程
- 截图获取当前游戏状态
- 大模型分析画面并生成动作决策
- 执行动作并观察结果
- DQN根据结果更新策略
- 等待固定间隔后重复
python复制while True:
if recording and training:
# 1. 截图
img_np = capture_screen()
if img_np is None:
time.sleep(CAPTURE_INTERVAL)
continue
# 2. AI决策
if ACTION_MODE == "single":
(action_name, duration), action_code, curr_state = llm_vision_decision(img_np)
else:
combo_actions, action_codes, curr_state = llm_vision_decision(img_np)
# 3. 执行动作
if ACTION_MODE == "single":
control.execute_game_action(action_code, duration)
else:
execute_combo_actions(action_codes, [a[1] for a in combo_actions])
# 4. DQN训练
if agent is not None and prev_state is not None:
reward = compute_reward(action_code, prev_state, curr_state)
agent.buffer.add(prev_state, action_code, reward, curr_state, False)
loss = agent.update(action_code)
prev_state = curr_state
time.sleep(CAPTURE_INTERVAL)
else:
time.sleep(0.1)
5.3 性能优化技巧
-
截图间隔:CAPTURE_INTERVAL设置为10秒是平衡决策质量和性能的折中方案。测试发现小于5秒会导致动作过于频繁,大于15秒则反应迟钝。
-
动作时长:移动类动作默认2秒,既能保证有效位移,又不会因过长导致卡顿。实际项目中可以通过分析游戏角色移动速度来动态调整。
-
目标网络更新:每10个episode更新一次目标网络,既保证了训练稳定性,又不会带来太大计算开销。
-
经验回放:缓冲区大小设为10000,批次大小32,这是经过测试在8GB内存机器上的最优配置。
6. 常见问题与解决方案
6.1 游戏窗口无法识别
症状:系统提示"未找到游戏窗口"
排查步骤:
- 确认游戏已启动且为窗口化/无边框模式
- 检查游戏窗口标题是否包含在GAME_WINDOW_NAMES列表中
- 以管理员身份运行Python脚本
解决方案:
python复制# 在screemcatch.py中扩展窗口名匹配
GAME_WINDOW_NAMES = [
"上古卷轴5",
"The Elder Scrolls V",
"Skyrim",
"Skyrim Special Edition",
"Skyrim VR", # VR版本
"老滚5", # 中文俗称
"TESV" # 缩写
]
6.2 输入模拟失效
症状:按键/鼠标动作未在游戏中生效
可能原因:
- 中文输入法干扰
- 游戏窗口失去焦点
- 防作弊系统拦截
解决方案:
- 在控制面板中将系统默认输入法设为英文
- 游戏设置中关闭"独占全屏"模式
- 添加管理员权限检查:
python复制def check_admin():
try:
return ctypes.windll.shell32.IsUserAnAdmin()
except:
return False
if not check_admin():
ctypes.windll.shell32.ShellExecuteW(None, "runas", sys.executable, " ".join(sys.argv), None, 1)
sys.exit()
6.3 DQN训练不稳定
症状:奖励值波动大,策略不收敛
优化方案:
- 调整奖励函数权重
- 增加状态特征维度
- 使用Double DQN或Dueling DQN等改进算法
python复制# 改进的DQN实现
class DoubleDQNAgent(DQNAgent):
def update(self, action):
# ... 采样代码同上 ...
# Double DQN修改
next_actions = np.argmax(self.model.predict(next_states), axis=1)
next_q_values = self.target_model.predict(next_states)
targets[range(32), actions] = rewards + self.gamma * next_q_values[range(32), next_actions] * (1 - dones)
# ... 其余代码同上 ...
7. 项目扩展方向
7.1 多模态输入增强
当前仅使用视觉输入,可以加入:
- 游戏内存读取获取精确状态数据
- 音频分析检测战斗/对话等事件
- OCR识别游戏界面文字信息
7.2 分层强化学习
将决策分为:
- 高层策略:制定长期目标(如"前往某个地点")
- 底层控制:执行具体动作(移动、战斗等)
7.3 模仿学习
录制人类玩家操作作为示范数据,结合强化学习进行预训练,加速收敛。
python复制def load_human_demonstrations():
# 加载人类玩家的(状态,动作)对
demonstrations = []
with open('human_play.json') as f:
data = json.load(f)
for episode in data:
states = episode['states']
actions = episode['actions']
demonstrations.extend(zip(states, actions))
return demonstrations
# 在DQN训练前进行模仿学习
demonstrations = load_human_demonstrations()
for state, action in demonstrations:
agent.buffer.add(state, action, 1.0, state, True) # 给予正奖励
这个项目展示了如何将大语言模型与强化学习结合来实现游戏自动化控制,其中的技术思路也可以应用于其他需要视觉理解和决策自动化的场景。通过持续优化DQN算法和扩展状态特征,系统的表现可以不断提升,最终实现接近人类水平的游戏操作能力。
