<think>我们之前已经实现了基于asyncio和WebSocket的多人RPG游戏服务器,现在需要补全客户端代码。客户端使用Pygame来显示游戏世界,包括地图、玩家、NPC,并处理用户输入(移动、攻击、聊天等)。我们需要完成以下功能:1.绘制游戏世界(根据服务器发送的地图数据,但注意:在之前的服务器代码中,地图是固定的,所以客户端可以直接使用同样的地图定义,或者由服务器发送地图数据。但为了简单起见,我们让客户端也使用同样的地图生成函数。注意:在真实游戏中,服务器应该发送地图数据,但这里为了简化,客户端直接复制了服务器的地图数据定义)2.绘制玩家和NPC(根据服务器广播的位置和状态)3.处理键盘输入:WASD移动,空格攻击,回车发送聊天消息4.显示聊天框和消息5.显示玩家的状态(血条、等级等)注意:由于之前服务器代码中,地图数据没有发送给客户端,我们将在客户端中复制同样的地图生成函数。但在实际应用中,地图数据应由服务器发送。我们将按照以下步骤完成客户端:1.初始化Pygame,设置窗口和颜色2.复制服务器的地图数据定义(生成同样的地图)3.在游戏循环中绘制地图、玩家和NPC4.处理用户输入事件,并发送相应的命令到服务器5.接收并处理服务器消息,更新游戏状态6.绘制聊天框和消息,以及状态栏由于之前给出的客户端代码框架已经实现了连接服务器的逻辑,我们需要补全游戏主循环和绘制逻辑。我们将修改客户端代码,添加以下内容:-地图绘制函数-玩家和NPC的绘制-输入处理:移动、攻击(选择目标)、聊天-聊天框的显示和输入-状态显示注意:攻击功能需要选择目标,我们这里采用简单的邻近目标选择(当玩家在某个目标周围时,按下空格攻击最近的目标)具体实现:1.地图绘制:我们使用一个二维数组表示地图,每个格子用一种颜色填充。我们将格子放大以便观察(比如每个格子40x40像素)。2.玩家和NPC:用不同颜色的圆形表示。玩家自己用蓝色,其他玩家用绿色,NPC用红色,重要NPC(如龙)用其他颜色。3.输入处理:-移动:WASD键按下时,发送移动命令(dx,dy),注意要限制移动速度(避免一次移动过远)-聊天:当按下回车键时,弹出聊天输入框,输入消息后发送-攻击:按下空格时,计算周围最近的玩家或NPC(在攻击范围内)并发送攻击命令4.聊天框:在屏幕底部显示一个矩形框,显示最近的聊天消息。5.状态栏:在屏幕顶部显示玩家自己的状态(血条、等级等)。我们将在客户端类中添加以下方法:-`draw_map`:绘制地图-`draw_players`:绘制玩家-`draw_npcs`:绘制NPC-`draw_chat_box`:绘制聊天框-`draw_hud`:绘制状态栏并完善主循环。由于客户端需要同时处理Pygame事件和接收服务器消息,我们将使用异步循环。注意:Pygame的事件循环是阻塞的,而我们需要同时等待服务器消息,因此我们将使用`asyncio`来同时处理两个循环。但是,由于Pygame本身不是异步的,我们将采用以下方式:在每次游戏循环中,我们使用`asyncio.get_event_loop().run_until_complete(asyncio.sleep(0))`来处理异步事件(非阻塞方式),或者使用`pygame.time.Clock`来控制帧率,并在每一帧中处理异步事件。我们之前已经在`receive_messages`方法中启动了异步任务来接收消息,所以只需要在主循环中处理异步事件队列即可(使用`asyncio.sleep(0)`来让出控制权)。具体步骤:1.在客户端初始化时,我们已连接到服务器并启动了接收消息的任务。2.在游戏主循环中:whileTrue:处理Pygame事件(退出、按键等)使用`asyncio.get_event_loop().run_until_complete(self.process_network_messages())`来处理网络消息(但注意:接收消息已经在另一个任务中处理,并更新了客户端状态,所以这里我们只需要绘制最新状态即可)-但实际上,因为接收消息的任务是独立运行的,我们需要考虑线程安全(但这里都在同一个线程的事件循环中,所以没问题)。3.我们使用一个独立的异步方法`process_network_messages`可能并不必要,因为我们已经在另一个任务中接收消息并更新了客户端状态。所以我们每帧只需要绘制当前状态即可。4.但要注意:Pygame的主循环不能阻塞,否则会导致接收消息延迟。所以我们每帧中处理事件后,让出控制权给事件循环,以便处理网络消息。因此,我们在主循环中添加一个步骤:短暂地让出控制权给异步事件循环。我们可以这样:foreventinpygame.event.get():#处理事件...#处理异步网络事件(让出控制权,允许异步任务运行)awaitasyncio.sleep(0)#但注意:主循环是同步的,所以我们需要调整主循环为异步?然而,这需要我们将主循环也放在异步函数中。由于初始连接时我们已经使用了事件循环,我们可以调整主循环为异步函数。所以,我们重构一下:将游戏主循环改为异步函数`run_game`,然后在初始化连接后调用`asyncio.get_event_loop().run_until_complete(self.run_game())`。步骤:-将游戏主循环重构成异步函数(使用`asyncdefrun_game(self):`)-在循环中,使用`awaitasyncio.sleep(0.016)`来控制帧率(每秒60帧)并同时让出控制权但是,由于我们使用Pygame,需要及时处理事件和绘制,我们可以这样:asyncdefrun_game(self):whileTrue:#处理事件foreventinpygame.event.get():#退出事件ifevent.type==QUIT:return#处理其他事件...#处理键盘持续按下(移动)keys=pygame.key.get_pressed()ifkeys[K_w]:awaitself.move(0,-0.1)ifkeys[K_s]:awaitself.move(0,0.1)ifkeys[K_a]:awaitself.move(-0.1,0)ifkeys[K_d]:awaitself.move(0.1,0)#处理攻击(按空格)ifkeys[K_SPACE]:awaitself.attack_nearest()#处理聊天输入(按回车)ifkeys[K_RETURN]:ifnotself.chat_input_active:#避免重复触发self.chat_input_active=Trueself.current_chat_message=""#如果聊天框激活,处理聊天输入ifself.chat_input_active:#处理文本输入事件foreventinpygame.event.get(KEYDOWN):#...处理输入和回车发送#清空屏幕self.screen.fill((0,0,0))#绘制地图self.draw_map()#绘制玩家和NPCself.draw_players()self.draw_npcs()#绘制聊天框self.draw_chat_box()#绘制状态栏self.draw_hud()#更新屏幕pygame.display.flip()#控制帧率,并让出控制权给事件循环self.clock.tick(60)awaitasyncio.sleep(0)注意:由于在异步函数中,我们可以在发送网络消息时使用`awaitself.websocket.send(...)`。但是,上面的键盘连续按下处理会导致移动速度过快,我们需要加入一个移动冷却机制(比如每次移动后暂停一段时间,或者用时间增量来控制)。这里为了简单,我们每次移动发送一个小的位移(0.1),然后由服务器处理。由于我们让服务器每0.5秒广播一次状态,所以移动的更新频率由客户端决定,但服务器会验证位置。另外,攻击功能我们需要在客户端选择目标:找到离自己最近且在攻击范围内的目标。我们将实现`attack_nearest`方法:计算玩家当前位置与所有其他玩家和NPC的距离(除了自己),找到最近的并且在攻击范围内(比如距离小于1.5)的目标,然后发送攻击命令。最后,聊天框的实现需要处理文本输入,我们将使用Pygame事件处理输入字符。由于代码较长,我们开始补全客户端代码:注意:由于我们之前定义的服务器地图数据是15x15,所以我们客户端也要同样定义(复制服务器中的地图生成函数)。由于之前服务器代码中地图生成函数`generate_map`返回一个二维列表,我们在客户端中同样定义这个函数。另外,为了保持与服务器一致的NPC数据,我们在客户端中暂时不生成NPC,而是等待服务器广播(因为NPC位置会由服务器更新并广播)。我们开始写完整的客户端代码(包括之前已有的部分和新增部分):</think>
# 基于Asyncio和Pygame的多人RPG游戏客户端完整实现
下面我将基于之前的服务器代码,补全一个功能完善的多人在线RPG游戏客户端代码。客户端使用Pygame作为渲染引擎,与服务器通过WebSocket进行通信。
## 完整客户端代码实现
```python
import asyncio
import websockets
import json
import pygame
import sys
from pygame.locals import *
import math
import uuid
# 颜色定义
BACKGROUND = (40, 40, 60)
PLAYER_SELF = (65, 105, 225)
PLAYER_OTHER = (50, 205, 50)
NPC_COLOR = (220, 20, 60)
TEXT_COLOR = (255, 255, 255)
MAP_GRID = (70, 70, 90)
HEALTH_BAR = (220, 20, 60)
HEALTH_BG = (50, 50, 50)
CHAT_BG = (0, 0, 0, 180)
class GameClient:
def __init__(self):
pygame.init()
self.screen_width, self.screen_height = 800, 600
self.screen = pygame.display.set_mode((self.screen_width, self.screen_height))
pygame.display.set_caption("多人RPG游戏")
self.clock = pygame.time.Clock()
self.font = pygame.font.SysFont(None, 24)
self.small_font = pygame.font.SysFont(None, 18)
# 游戏状态
self.player_id = None
self.name = ""
self.position = (0, 0)
self.health = 100
self.max_health = 100
self.level = 1
self.experience = 0
self.gold = 50
self.inventory = {}
self.equipped = {}
# 其他实体
self.players = {}
self.npcs = {}
# 地图相关
self.map_size = (15, 15)
self.tile_size = 40
self.camera_offset = [0, 0]
# UI 状态
self.chat_messages = []
self.chat_input_active = False
self.chat_input_text = ""
self.selected_target = None
# 网络连接
self.websocket = None
self.connected = False
def render_text(self, text, font, color, position):
"""渲染文本到屏幕上"""
text_surface = font.render(text, True, color)
self.screen.blit(text_surface, position)
return text_surface.get_size()
def render_centered_text(self, text, font, color, y_position):
"""在屏幕中间水平居中渲染文本"""
text_surface = font.render(text, True, color)
text_rect = text_surface.get_rect(center=(self.screen_width // 2, y_position))
self.screen.blit(text_surface, text_rect)
def render_player(self, player, color, is_self=False):
"""渲染玩家"""
# 转换世界坐标到屏幕坐标
screen_x = player["position"][0] * self.tile_size - self.camera_offset[0]
screen_y = player["position"][1] * self.tile_size - self.camera_offset[1]
# 渲染玩家
pygame.draw.circle(self.screen, color, (int(screen_x), int(screen_y)), 10)
# 渲染名称
name_size = self.render_text(player["name"], self.small_font, color, (screen_x - 20, screen_y - 25))
# 渲染血条
health_percent = player["health"] / player["max_health"]
health_width = 40
pygame.draw.rect(self.screen, HEALTH_BG, (screen_x - health_width // 2, screen_y + 15, health_width, 5))
pygame.draw.rect(self.screen, HEALTH_BAR, (screen_x - health_width // 2, screen_y + 15,
int(health_width * health_percent), 5))
# 如果选中目标,绘制目标标记
if self.selected_target == player["id"]:
pygame.draw.circle(self.screen, (255, 255, 0), (int(screen_x), int(screen_y - 20)), 5, 1)
return screen_x, screen_y
def render_npc(self, npc_id, npc):
"""渲染NPC"""
# 转换世界坐标到屏幕坐标
screen_x = npc["position"][0] * self.tile_size - self.camera_offset[0]
screen_y = npc["position"][1] * self.tile_size - self.camera_offset[1]
# 渲染NPC
pygame.draw.rect(self.screen, NPC_COLOR, (screen_x - 10, screen_y - 10, 20, 20))
# 渲染名称
self.render_text(npc["name"], self.small_font, NPC_COLOR, (screen_x - 20, screen_y - 30))
# 渲染血条
health_percent = npc["health"] / npc["max_health"]
health_width = 40
pygame.draw.rect(self.screen, HEALTH_BG, (screen_x - health_width // 2, screen_y + 15, health_width, 5))
pygame.draw.rect(self.screen, HEALTH_BAR, (screen_x - health_width // 2, screen_y + 15,
int(health_width * health_percent), 5))
# 如果选中目标,绘制目标标记
if self.selected_target == npc_id:
pygame.draw.circle(self.screen, (255, 255, 0), (int(screen_x), int(screen_y - 15)), 7, 1)
return screen_x, screen_y
def render_map(self):
"""渲染游戏地图"""
# 绘制网格背景
for x in range(0, self.screen_width, self.tile_size):
pygame.draw.line(self.screen, MAP_GRID, (x, 0), (x, self.screen_height), 1)
for y in range(0, self.screen_height, self.tile_size):
pygame.draw.line(self.screen, MAP_GRID, (0, y), (self.screen_width, y), 1)
# 渲染NPC
for npc_id, npc in self.npcs.items():
self.render_npc(npc_id, npc)
# 渲染其他玩家
for player_id, player in self.players.items():
if player_id != self.player_id:
self.render_player(player, PLAYER_OTHER)
# 渲染自己
if self.player_id in self.players:
self.render_player(self.players[self.player_id], PLAYER_SELF, True)
def render_hud(self):
"""渲染游戏界面上的HUD元素"""
# 左上角状态面板
pygame.draw.rect(self.screen, (30, 30, 50, 200), (10, 10, 200, 100))
health_percent = self.health / self.max_health
health_width = 150
pygame.draw.rect(self.screen, HEALTH_BG, (70, 20, health_width, 15))
pygame.draw.rect(self.screen, HEALTH_BAR, (70, 20, int(health_width * health_percent), 15))
self.render_text(f"HP: {self.health}/{self.max_health}", self.font, TEXT_COLOR, (20, 20))
self.render_text(f"等级: {self.level}", self.font, TEXT_COLOR, (20, 45))
self.render_text(f"金币: {self.gold}", self.font, TEXT_COLOR, (20, 70))
# 左下角聊天框
pygame.draw.rect(self.screen, CHAT_BG, (10, self.screen_height - 140, 300, 130))
for i, message in enumerate(self.chat_messages[-5:]):
self.render_text(message["text"], self.small_font, TEXT_COLOR,
(15, self.screen_height - 135 + i * 20))
# 聊天输入框
if self.chat_input_active:
pygame.draw.rect(self.screen, (40, 40, 70), (10, self.screen_height - 30, 300, 25))
self.render_text(">" + self.chat_input_text, self.font, TEXT_COLOR, (15, self.screen_height - 28))
# 右下角操作提示
self.render_text("WASD: 移动", self.small_font, TEXT_COLOR, (self.screen_width - 150, self.screen_height - 100))
self.render_text("空格: 攻击", self.small_font, TEXT_COLOR, (self.screen_width - 150, self.screen_height - 80))
self.render_text("回车: 聊天", self.small_font, TEXT_COLOR, (self.screen_width - 150, self.screen_height - 60))
self.render_text("点击: 选择目标", self.small_font, TEXT_COLOR, (self.screen_width - 150, self.screen_height - 40))
def center_camera_on_player(self):
"""将摄像机中心对准玩家角色"""
if self.player_id in self.players:
player_pos = self.players[self.player_id]["position"]
self.camera_offset[0] = player_pos[0] * self.tile_size - self.screen_width // 2
self.camera_offset[1] = player_pos[1] * self.tile_size - self.screen_height // 2
def get_entity_at_position(self, screen_pos):
"""获取指定屏幕位置的实体"""
for npc_id, npc in self.npcs.items():
npc_pos = (npc["position"][0] * self.tile_size - self.camera_offset[0],
npc["position"][1] * self.tile_size - self.camera_offset[1])
distance = math.sqrt((screen_pos[0] - npc_pos[0])**2 + (screen_pos[1] - npc_pos[1])**2)
if distance < 20:
return npc_id, "npc"
for player_id, player in self.players.items():
player_pos = (player["position"][0] * self.tile_size - self.camera_offset[0],
player["position"][1] * self.tile_size - self.camera_offset[1])
distance = math.sqrt((screen_pos[0] - player_pos[0])**2 + (screen_pos[1] - player_pos[1])**2)
if distance < 15 and player_id != self.player_id:
return player_id, "player"
return None, None
async def connect_to_server(self):
"""连接到游戏服务器"""
# 获取玩家名称
self.name = "玩家" + str(uuid.uuid4())[:5] # 自动生成随机玩家名
try:
self.websocket = await websockets.connect("ws://localhost:8765")
# 登录服务器
await self.websocket.send(json.dumps({
"action": "login",
"name": self.name
}))
# 启动消息接收任务
asyncio.create_task(self.receive_messages())
self.connected = True
print(f"已连接到服务器,玩家名: {self.name}")
except Exception as e:
print(f"连接服务器失败: {e}")
self.connected = False
async def receive_messages(self):
"""接收服务器消息"""
while self.connected:
try:
message = await self.websocket.recv()
data = json.loads(message)
self.handle_server_message(data)
except websockets.ConnectionClosed:
print("与服务器的连接已关闭")
self.connected = False
break
def handle_server_message(self, data):
"""处理来自服务器的消息"""
msg_type = data["type"]
if msg_type == "login_success":
self.player_id = data["player_id"]
self.position = tuple(data["position"])
self.health = data["health"]
self.max_health = data.get("max_health", 100)
self.level = data.get("level", 1)
self.inventory = data.get("inventory", {})
self.equipped = data.get("equipped", {})
self.players[self.player_id] = {
"id": self.player_id,
"name": self.name,
"position": self.position,
"health": self.health,
"max_health": self.max_health,
"level": self.level
}
elif msg_type == "world_update":
self.players = data["players"]
self.npcs = data["npcs"]
# 更新自己的状态
if self.player_id in self.players:
player_data = self.players[self.player_id]
self.health = player_data["health"]
self.max_health = player_data["max_health"]
self.level = player_data["level"]
elif msg_type == "player_joined":
print(f"新玩家加入: {data['name']}")
elif msg_type == "player_left":
player_id = data["player_id"]
if player_id in self.players:
del self.players[player_id]
print(f"玩家离开: {player_id}")
elif msg_type == "player_moved":
player_id = data["player_id"]
if player_id in self.players:
self.players[player_id]["position"] = tuple(data["position"])
elif msg_type == "combat_result":
print(f"战斗结果: {data['attacker_id']} 攻击了 {data['target_id']}, 伤害: {data['damage']}")
elif msg_type == "chat_message":
self.chat_messages.append({
"player_id": data["player_id"],
"name": data["name"],
"text": data["message"]
})
print(f"[聊天] {data['name']}: {data['message']}")
async def send_move(self, dx, dy):
"""发送移动命令到服务器"""
if self.connected:
await self.websocket.send(json.dumps({
"action": "move",
"dx": dx,
"dy": dy
}))
async def send_attack(self, target_id):
"""发送攻击命令到服务器"""
if self.connected:
await self.websocket.send(json.dumps({
"action": "attack",
"target_id": target_id
}))
async def send_chat(self, message):
"""发送聊天消息到服务器"""
if self.connected:
await self.websocket.send(json.dumps({
"action": "chat",
"message": message
}))
self.chat_messages.append({
"player_id": self.player_id,
"name": self.name,
"text": message
})
async def run(self):
"""运行游戏主循环"""
await self.connect_to_server()
if not self.connected:
self.render_centered_text("连接服务器失败,按ESC退出", self.font, TEXT_COLOR, self.screen_height // 2)
pygame.display.flip()
return
running = True
while running:
dt = self.clock.tick(60) / 1000.0 # 计算增量时间
# 处理事件
for event in pygame.event.get():
if event.type == QUIT:
running = False
elif event.type == KEYDOWN:
# 处理聊天输入
if self.chat_input_active:
if event.key == K_RETURN:
if self.chat_input_text:
await self.send_chat(self.chat_input_text)
self.chat_input_active = False
self.chat_input_text = ""
elif event.key == K_BACKSPACE:
self.chat_input_text = self.chat_input_text[:-1]
elif event.key != K_RETURN: # 确保回车键不会输入到文本中
self.chat_input_text += event.unicode
else:
# 普通键盘操作
if event.key == K_RETURN:
self.chat_input_active = True
elif event.key == K_SPACE and self.selected_target:
await self.send_attack(self.selected_target)
elif event.key == K_ESCAPE:
running = False
elif event.type == MOUSEBUTTONDOWN:
# 鼠标点击选择目标
if event.button == 1: # 左键
entity_id, entity_type = self.get_entity_at_position(event.pos)
if entity_id:
self.selected_target = entity_id
else:
self.selected_target = None
# 处理持续按键(移动)
keys = pygame.key.get_pressed()
move_speed = 5.0 * dt
if keys[K_w]:
await self.send_move(0, -move_speed)
if keys[K_s]:
await self.send_move(0, move_speed)
if keys[K_a]:
await self.send_move(-move_speed, 0)
if keys[K_d]:
await self.send_move(move_speed, 0)
# 更新摄像机位置
self.center_camera_on_player()
# 渲染
self.screen.fill(BACKGROUND)
self.render_map()
self.render_hud()
# 如果正在显示聊天框,绘制聊天输入提示
if self.chat_input_active:
self.render_centered_text("输入聊天消息,按回车发送", self.font, TEXT_COLOR, 30)
# 如果未连接或登录,显示状态
if not self.connected:
self.render_centered_text("连接已断开,按ESC退出", self.font, TEXT_COLOR, self.screen_height // 2)
elif not self.player_id:
self.render_centered_text("登录服务器...", self.font, TEXT_COLOR, self.screen_height // 2)
pygame.display.fl