
Ursina 的 on_click 回调不触发,通常是因为作用域问题:当 nav 在函数内部定义且未正确声明为全局变量时,on_click 绑定的函数无法访问或修改预期的外部状态变量。
ursina 的 `on_click` 回调不触发,通常是因为作用域问题:当 `nav` 在函数内部定义且未正确声明为全局变量时,`on_click` 绑定的函数无法访问或修改预期的外部状态变量。
在 Ursina 中,on_click 是一个事件回调机制,它会在鼠标点击实体时异步触发(由引擎内部的输入循环调用),而非在你定义 Entity 的那一刻同步执行。因此,该回调函数必须能稳定访问到你期望共享的状态变量(如 nav)。你遇到的问题根源在于 Python 的作用域规则:
在第二个版本中,nav = [None, 0, 0] 被定义在 runner() 函数内部 —— 这使其成为一个局部变量。尽管 click() 函数内使用了 global nav,但它所声明的 global nav 指向的是模块级(即文件顶层)的 nav,而此时顶层并未定义 nav,导致 global nav 实际引用了一个未初始化或被覆盖的变量,甚至可能引发 UnboundLocalError 或静默失败。
✅ 正确做法是:将共享状态变量(如 nav)声明在模块顶层(global scope),并在所有需要读写它的函数中显式使用 global 声明:
from ursina import *
import math
# ✅ 正确:在模块顶层定义 nav,确保所有函数可一致访问
nav = [None, 0, 0]
def click():
global nav
if mouse.hovered_entity and hasattr(mouse.hovered_entity, 'x'):
# 安全获取位置,避免 AttributeError
nav[0] = (
mouse.hovered_entity.x,
mouse.hovered_entity.y + mouse.hovered_entity.scale_y / 2,
mouse.hovered_entity.z
)
nav[1] = 1 # 启动移动
nav[2] = 0 # 重置停止标志
def normalize_direction_vector(x_a, y_a, x_b, y_b):
global nav
dx, dy = x_b - x_a, y_b - y_a
magnitude = math.sqrt(dx**2 + dy**2)
if magnitude < 0.001: # 防除零
nav[2] = 1
return [0, 0]
i = dx / magnitude
j = dy / magnitude
if magnitude < 0.5: # target_threshold
nav[2] = 1
return [i, j]
def runner():
# ⚠️ 移除此处的 nav = [...]!否则会遮蔽顶层全局变量
movement_speed = 0.1
rotation_speed = 40
camera_distance = 15
control_mode = 0
app = Ursina(fullscreen=True, development_mode=True)
player = Entity(model='cube', color=color.white, position=(0, 0, 0))
# 创建 10×10 绿色地面方块,启用碰撞与点击
for i in range(10):
for j in range(10):
Entity(
model='cube',
color=color.green,
position=(i, -1, j),
scale=(1, 1, 1),
collider='box',
on_click=click # ✅ 直接传函数名,不加括号
)
camera.position = (0, 15, -15)
camera.look_at(player)
# 主循环(保持 while True 结构,符合你的需求)
while True:
if held_keys['escape']:
break
# 恢复上一帧悬停方块颜色(避免残留黑色)
try:
if hasattr(hovered_entity := mouse.hovered_entity, 'color'):
hovered_entity.color = color.green
except:
pass
# WASD 移动(可选)
if control_mode:
player.x += (held_keys['d'] - held_keys['a']) * movement_speed
player.z += (held_keys['w'] - held_keys['s']) * movement_speed
# 点击移动逻辑
elif nav[1] and not nav[2]: # 正在移动且未到达
target_x, _, target_z = nav[0]
dir_vec = normalize_direction_vector(player.x, player.z, target_x, target_z)
player.x += dir_vec[0] * movement_speed
player.z += dir_vec[1] * movement_speed
elif nav[2]: # 到达目标,重置导航
nav[:] = [None, 0, 0] # ✅ 清空原列表对象(非重新赋值)
# 相机跟随与视角控制
player.look_at((mouse.x * 5 + player.x, 0, mouse.y * 5 + player.z))
camera.position = (player.x - camera_distance, player.y + camera_distance, player.z - camera_distance)
camera.look_at((mouse.x * 5 + player.x, 0, mouse.y * 5 + player.z))
# 悬停变黑效果
if mouse.hovered_entity:
mouse.hovered_entity.color = color.black
player.rotation_y += rotation_speed * time.dt # 推荐用 time.dt 替代固定旋转
app.step()
runner()? 关键注意事项:
-
on_click=click必须传函数对象(无括号),而非on_click=click()(这会立即执行并绑定返回值); - 所有修改
nav的函数(包括click和normalize_direction_vector)都必须包含global nav,且nav必须在模块顶层初始化; - 使用
nav[:] = [...]可原地更新列表内容,避免因nav = [...]重新赋值导致其他闭包或引用失效; - 建议添加
hasattr或try/except防御性检查,避免mouse.hovered_entity为None时崩溃; - 若需更健壮的点击逻辑(如射线检测精度、UI 层级过滤),可配合
camera.world_to_screen()或自定义raycast(),但当前collider="box"+on_click已满足基础需求。
遵循以上结构,你的点击移动功能即可稳定运行,同时保留 while True 主循环风格。

















