Flask Socket-IO中玩家对象错误批量添加问题及根因定位
编辑:已确认问题并非出自Flask-SocketIO。问题在于我的Player类使用了默认字典,所有对象实例都会修改同一个默认字典而非创建新字典。待系统允许后将标记为已解决。
问题背景
我正尝试使用Flask-SocketIO作为客户端与服务器的通信接口,构建一个小型自定义文本RPG引擎。目前需要实现:玩家进入房间时将Player对象添加至Room对象的contents属性,离开房间时移除该对象。但当多个客户端连接服务器后,一旦多名玩家开始移动,所有玩家都会被添加至任意玩家进入的房间。我猜测这与我使用Flask-SocketIO事件处理将客户端命令传递给玩家/房间方法的方式有关,但不确定具体原因。我感觉每次客户端发送数据时,都会触发方法为所有玩家执行添加至房间contents的操作,但我的其他方法并未出现此类重复问题。
玩家移动操作无报错,发言和查看功能均正常运行。我目前有些困惑,非常感谢任何帮助或建议。
连接事件
client_list = [] #当前已连接的客户端列表 world = objects.World() #实例化World类,用于存储所有房间、玩家和角色 @socketio.on('connect') def connect(): active_player = current_user.accounts.filter(PlayerAccount.is_active == True).first() #获取活跃玩家信息 if not active_player.player_info: #检查该活跃玩家是否为新玩家 player = objects.Player(id=active_player.id, name=active_player.player_name, description="A newborn player, fresh to the world.", account=active_player.user_id) print(f'id = {player.id}, name = {player.name}, description = {player.description}, health = {player.health}, level = {player.level}, stats = {player.stats}, location = {player.location}, inventory = {player.inventory}') #创建新玩家对象 active_player.player_info = dill.dumps(player) #序列化玩家对象并写入活跃玩家信息 active_player.save() #将序列化数据保存至玩家数据库 else: player = dill.loads(active_player.player_info) #加载序列化的玩家数据 username = player.name location = player.location player.session_id = request.sid client_list.append(player.session_id) world.players.update({player.id: player}) print(f'客户端列表为 {client_list}') print(f'已连接玩家为 {world.players}') session['player_id'] = player.id join_room(location) print(player.location) print(player, world.rooms[location].name, world.rooms[location].contents['Players']) socketio.emit('event', {'message': f'{username} has connected to the server'})
客户端命令事件及方法路由函数
@socketio.event def client(data): current_player = events.world.players[session.get('player_id')] current_room = events.world.rooms[current_player.location] content = { 'player': current_player, 'room': current_room, 'command': data['command'], 'data': data['data'], } if content['command'] == 'say': say(content['player'], content['data']) if content['command'] in ['move', 'go', 'north', 'south', 'east', 'west', 'n', 's', 'e', 'w']: if not content['data']: content['data'] = content['command'] if content['data'] == 'n': content['data'] = 'north' if content['data'] == 's': content['data'] = 'south' if content['data'] == 'e': content['data'] = 'east' if content['data'] == 'w': content['data'] = 'west' move(player=content['player'], direction=content['data'], room=content['room']) if content['command'] == 'look' or content['command'] == 'l': look(player=content['player'], data=content['data'], room=content['room']) if content['command'] == 'test': test(content['player'], content['data']) if content['command'] == 'save': save(content['player'], content['data']) def say(player, data): player.speak(data) def move(player, direction, room): player.move(direction=direction, room=room) def look(player, room, data=''): player.look(data=data, room=room)
World、Room及Player对象类
class World(): def __init__(self) -> None: with open('app/data/room_db.pkl', 'rb') as dill_file: rooms = dill.load(dill_file) self.rooms = rooms self.players = {} def world_test(self): print(f'World初始化完成,包含房间:{self.rooms}') socketio.emit('event', {'message': self.rooms['0,0'].description}) def world_save(self): with open('app/data/world_db.pkl', 'wb') as dill_file: dill.dump(self, dill_file) socketio.emit('event', {'message': 'world saved'}) def room_save(self): with open('app/data/room_db.pkl', 'wb') as dill_file: dill.dump(self.rooms, dill_file) socketio.emit('event', {'message': 'rooms saved'}) #世界中所有可交互对象的基类 class Entity(): def __init__(self, name, description) -> None: self.name = name #实体的简称 self.description = description #所有实体都需要支持被查看的描述信息 #当前为测试函数,所有实体都需要实现查看自身描述的方法 def describe(self): pass #房间类,房间应包含所有其他对象(NPC、物品、玩家及其他新增对象) class Room(Entity): id = itertools.count() def __init__(self, name, description, position, exits, icon, contents={'NPCs': {}, 'Players': {}, 'Items': {}}) -> None: super().__init__(name, description) self.id = next(Room.id) self.position = position #房间在网格系统中的坐标,用于角色移动时定位 self.exits = exits #与当前房间相连的房间列表,通常为南北东西方向,可扩展支持特殊移动 self.icon = icon #世界地图上的图标,由两个ASCII字符组成(如"/\\"代表山脉) self.contents = contents #存储房间内所有NPC、玩家、物品的字典,值会随角色移动、NPC生成、物品移动而修改 def describe_contents(self, caller): output = '' print('测试') print(f'房间内玩家:{self.contents["Players"]}') return output #所有具备自主行动能力、能影响世界的实体的基类 default_stats = { 'strength': 10, 'endurance': 10, 'intelligence': 10, 'wisdom': 10, 'charisma': 10, 'agility': 10 } class Character(Entity): def __init__(self, name, description, health=100, level=1, location='0,0', stats=default_stats, deceased=False, inventory = []) -> None: super().__init__(name, description) self.health = health #所有角色都具备生命值 self.level = level #所有角色都具备等级 self.location = location #所有角色都具备当前位置,反映所在房间,移动时会被引用 self.stats = stats #所有角色都具备属性面板 self.deceased = deceased #角色存活状态标识,若为True则物品可被掠夺 self.inventory = inventory #角色背包中的物品列表,可能改为分类字典 #玩家控制的角色类,用于与世界交互。暂未确定是否与模型层混合,或直接序列化类存入数据库 class Player(Character): def __init__(self, id, account, name, description, health=100, level=1, location='0,0', stats=default_stats, deceased=False, inventory=[]) -> None: super().__init__(name, description, health, level, location, stats, deceased, inventory) self.id = id self.account = account #与玩家角色关联的用户账号 self.session_id = '' #会话ID,用于向指定玩家发送消息,会话ID对每个连接唯一,需在连接时赋值 def connection(self): events.world.rooms[self.location].contents['Players'].update({self.id: self}) def disconnection(self): pass def look(self, data, room): if data == '': socketio.emit('event', {'message': self.location}, to=self.session_id) socketio.emit('event', {'message': room.description}, to=self.session_id) socketio.emit('event', {'message': room.describe_contents(self)}, to=self.session_id) else: socketio.emit('event', {'message': 'this will eventually be a call to a class\'s .description to return a look statement.'}, to=self.session_id) def speak(self, data): socketio.emit('event', {'message': f'{self.name} says "{data}"'}, room=self.location, include_self=False) socketio.emit('event', {'message': f'You say "{data}"'}, to=self.session_id) def move(self, direction, room): if direction not in room.exits: socketio.emit('event', {'message': 'You can\'t go that way.'}, to=self.session_id) return leave_room(self.location) socketio.emit('event', {'message': f'{self.name} moves towards the {direction}'}, room=self.location) if self.id in room.contents['Players']: print(f"{room.contents['Players'][self.id].name} 已从 {room.name} 移除,对象ID: {id(room)}") print(events.world) del room.contents['Players'][self.id] print(room.contents) lat = int(self.location[:self.location.index(',')]) lon = int(self.location[self.location.index(',')+1:]) if direction == 'n' or direction == 'north': lon += 1 socketio.emit('event', {'message': 'You move towards the north'}, to=self.session_id) if direction == 's' or direction == 'south': lon -= 1 socketio.emit('event', {'message': 'You move towards the south'}, to=self.session_id) if direction == 'e' or direction == 'east': lat += 1 socketio.emit('event', {'message': 'You move towards the east'}, to=self.session_id) if direction == 'w' or direction == 'west': lat -= 1 socketio.emit('event', {'message': 'You move towards the west'}, to=self.session_id) new_location = f'{lat},{lon}' came_from = [i for i in events.world.rooms[new_location].exits if events.world.rooms[new_location].exits[i]==self.location] socketio.emit('event', {'message': f'{self.name} arrives from the {came_from[0]}'}, room=new_location) socketio.sleep(.5) self.location = new_location join_room(self.location) events.world.rooms[self.location].contents['Players'][self.id] = self socketio.emit('event', {'message': events.world.rooms[self.location].description}, to=self.session_id)
内容的提问来源于stack exchange,提问作者Thomas Makurat
相关产品推荐
相关产品推荐

