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

Ursina引擎多人游戏报错Assertion failed: !is_empty()求助

Ursina多人联机游戏断言错误问题排查

在开发多客户端联机游戏时,当接入超过2个客户端或频繁断开重连,会触发错误:

Assertion failed: !is_empty() at line 961 of panda/src/pgraph/nodePath.cxx

问题根源分析

服务端问题

  1. 重复定义update函数:
    代码中存在两个update函数,后定义的会覆盖前一个,导致原本的旋转信息更新逻辑失效,同时可能打乱网络事件处理的执行时机。
  2. 客户端断开处理逻辑错误:
    • 自定义的clientdisconnect事件中,del clients_pos[0]会无条件删除列表第一个元素,不管断开的是哪个客户端,直接导致客户端ID与clients_pos、clientplayers的索引完全不匹配,后续操作会访问错误的元素或已销毁的实体。
    • 递减last_id会导致ID重复分配,多个客户端使用同一ID操作同一个实体,销毁后再次访问就会触发断言错误。
    • 注释的onClientDisconnected事件中,clients_pos.pop(clientplayers[ID])是错误的,应该按索引ID删除,而非传入实体对象。
  3. 玩家位置更新逻辑隐患:
    xycoords事件中直接使用客户端传来的Id作为列表索引,当ID与列表索引不匹配时(比如断开后列表元素被删除),会访问越界或已销毁的实体。

客户端问题

  1. 错误修改服务端传来的位置列表:
    xandyotherplayers事件中使用Content.pop(Id)会直接修改原列表,导致后续位置信息错乱,应该改为通过索引访问自身位置,而非删除元素。
  2. 玩家实体管理混乱:
    • 玩家数量减少时仅销毁otherplayers[0],无法对应到实际断开的玩家,导致实体与位置信息不匹配。
    • counter变量的使用逻辑不严谨,当otherplayers长度与位置列表长度不一致时,会出现索引越界。
  3. 变量名错误:
    全局声明的other_players未被使用,实际代码中用的是otherplayers,可能引发未定义错误。
  4. 高频网络请求:
    每一帧都发送位置信息,会导致网络拥堵,加重服务端负担。

修正后的服务端代码

from ursinanetworking import *
from ursina import *

server = UrsinaNetworkingServer('192.168.1.34', 22625)
App = Ursina()

clientplayers = []
clients_pos = []
client_names = []
last_id = 0
nb_clients = 0

ground = Entity(model='ground.obj', scale=(10,1,10), color=color.white, texture='white_cube',texture_scale=(10,10)) 
ec = EditorCamera(rotation_smoothing=10, enabled=1, rotation=(30,30,0))
rotation_info = Text(position=window.top_left)

def update():
    rotation_info.text = str(int(ec.rotation_y)) + '\n' + str(int(ec.rotation_x))
    server.process_net_events()  # 合并到一个update函数

@server.event
def onClientConnected(Client):
    global last_id, nb_clients
    print(f'{Client} joined the game!')
    # 分配唯一ID,不重复使用
    client_id = last_id
    Client.send_message("ID", client_id)
    
    # 初始化玩家实体和位置
    clientplayers.append(Entity(model="person.obj", position=(0,5,0), color=color.red, collider='sphere'))
    clients_pos.append((0,5,0))
    client_names.append(str(Client))
    
    nb_clients += 1
    last_id += 1

@server.event
def onClientDisconnected(Client):
    global nb_clients
    # 从Client对象中提取ID
    client_id_str = str(Client).replace("Client ", "")
    if not client_id_str.isdigit():
        print(f"Invalid client ID: {Client}")
        return
    client_id = int(client_id_str)
    
    # 检查ID是否在有效范围内
    if 0 <= client_id < len(clientplayers):
        destroy(clientplayers[client_id])
        # 将该位置标记为无效,避免索引错乱
        clients_pos[client_id] = None
        print(f"Client {client_id} left the game")
        nb_clients -= 1

@server.event
def xycoords(Client, Content):
    x, y, z, client_id = Content
    # 验证ID有效性
    if 0 <= client_id < len(clientplayers) and clients_pos[client_id] is not None:
        clientplayers[client_id].position = (x, y, z)
        clients_pos[client_id] = (x, y, z)
    
    # 过滤掉无效位置,发送给客户端
    valid_positions = [pos for pos in clients_pos if pos is not None]
    Client.send_message("xandyotherplayers", (client_id, valid_positions))

App.run()

修正后的客户端代码

from ursinanetworking import *
from ursina.prefabs.first_person_controller import FirstPersonController

client = UrsinaNetworkingClient('192.168.1.34', 22625)
App = Ursina()

player_id = 0
other_players = []
last_valid_positions = []

ground = Entity(model="ground.obj", scale=(10,1,10), color=color.white, texture="white_cube", texture_scale=(10,10), collider="mesh")
player = FirstPersonController(origin_x=0, origin_z=0, origin_y=5, mouse_sensivity=Vec2(40,40), jump_height=2, collider='cube')

# 控制位置发送频率,避免高频请求
position_send_timer = 0
SEND_INTERVAL = 0.05  # 每0.05秒发送一次

@client.event
def onConnectionEtablished():
    print('Client successfully connected!')

@client.event
def onConnectionError(Reason):
    print('Client failed to connect, the reason is : ' + Reason)

@client.event
def ID(Content):
    global player_id
    player_id = int(Content)
    print(f"Assigned ID: {player_id}")

@client.event
def xandyotherplayers(Content):
    global other_players, last_valid_positions
    own_id, valid_positions = Content
    
    # 同步其他玩家数量:创建或销毁实体
    while len(other_players) < len(valid_positions) - 1:
        other_player = Entity(model="person.obj", color=color.white, texture="white_cube", texture_scale=(10,2), collider='sphere')
        other_players.append(other_player)
    
    while len(other_players) > len(valid_positions) - 1:
        destroy(other_players.pop())
    
    # 更新其他玩家位置
    idx = 0
    for i, pos in enumerate(valid_positions):
        if i != own_id and pos is not None:
            if idx < len(other_players):
                other_players[idx].position = pos
                idx += 1
    
    last_valid_positions = valid_positions

def update():
    global position_send_timer
    position_send_timer += time.dt
    
    # 达到间隔再发送位置
    if position_send_timer >= SEND_INTERVAL:
        client.send_message("xycoords", (player.x, player.y, player.z, player_id))
        position_send_timer = 0
    
    client.process_net_events()
    
    # 死亡重置
    if player.y <= -20:
        player.position = (0, 5, 0)
    
    # 退出逻辑
    if held_keys["q"]:
        client.disconnect()
        application.quit()

App.run()

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.16 05:01:01