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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 10:57:02