Godot 4.x 中的拖放功能
控制节点接口
使用它
全局拖放
我当时在开发一个库存系统,因此必须让玩家在无聊的时候可以玩“库存俄罗斯方块”游戏。我之前在不同的平台上做过拖放功能,知道其中存在一些潜在的特殊情况和问题,会耗费我大量的时间和精力。为了找到适用于 Godot 的拖放解决方案,我搜索了一下,结果发现搜索引擎提供的几乎都是一些老旧的、手动实现的方案。
但我最终还是找到了 Godot 内置的解决方案,而我一直努力避免的就是钻研 DIY/NIH 之类的无底洞。太好了!
下面我快速讲解一下,这样你就不用自己摸索了:
控制节点接口
Control节点有 3 个虚拟私有函数,您可以重写这些函数,从而启用拖放功能:
# Control that can be dragged from
func _get_drag_data(at_position:Vector2)->Variant
# Control that can be dragged to
func _can_drop_data(at_position:Vector2, data:Variant)->bool
func _drop_data(at_position:Vector2, data:Variant)->void
文档:_get_drag_data、_can_drop_data、_drop_data
拖放机制也是按这个顺序工作的:
_get_drag_data- 返回可以从当前位置拖动的数据Control。在我的例子中,如果一个槽位中有物品,那么我将返回该物品,否则返回空值null。_can_drop_dataControl该方法会持续在鼠标位置下方调用,传递相对鼠标位置和data,并返回该物品是否data可以被接受。这里我会检查当前物品是否适合鼠标位置的物品栏网格,以及当前栏位是否与物品类型兼容(武器栏位用于武器,背包栏位用于背包,等等)。_drop_data- 最后一步调用与之前相同_can_drop_data,只是这里我们接受丢弃操作,并且应该处理从之前的容器中移除物品,并将其添加到当前容器Control(或您计划对物品执行的任何其他操作data)。
为了处理拖放操作的显示预览,我们使用func set_drag_preview(control:Control)->void:(文档)
不出所料,这将使用Control您传递的节点作为拖放的显示图标,将其添加到场景树中,并在拖动完成后将其销毁。
force_drag(data:Variant, preview:Control)->void文档中还提供了一个函数,可以通过编程方式触发拖放操作。如果您是从事件_drop_data处理程序中调用此函数,请使用 `call_deferredif` 语句在下一帧中调用它,因为当前的拖放操作仍在进行中。我通常在将项目拖放到已有项目的槽位时使用此函数,这样可以先对前一个项目触发拖放操作,从而加快切换速度。
使用它
它界面简洁,基本用法也很直接——创建一些数据,这些数据将从某个位置拖拽过来Control,检查这些数据,并可能将这些数据接收到一个Control可以拖拽到的位置。
我遇到的最后一个问题是如何检测用户是否结束了拖拽操作,但当前页面Control却不接受这种操作。这时我们可以回归Node基本方法,利用预览的生命周期Control!完整的生命周期如下所示:
- 用户点击和拖动,Godot 调用:
_get_drag_data - 我们称之为:
set_drag_preview - 用户拖动鼠标到屏幕上
Control,Godot 调用:_can_drop_data - 用户松开拖拽操作,结果
_can_drop_data属实,Godot 调用:_drop_data preview_control.tree_exiting发出信号
为了简化这个生命周期,我使用了一个单独的对象来管理拖拽操作:
class_name ItemDrag
signal drag_completed(data:ItemDrag)
var source: Control = null
var destination: Control = null
var item: Item
var preview: Control
func _init(_source: Control, _item: Item, _preview: Control):
self.source = _source
self.item = _item
self.preview = _preview
self.preview.tree_exiting.connect(_on_tree_exiting)
func _on_tree_exiting()->void:
drag_completed.emit(self)
使用此类,其他函数的示例可以是:
func remove_item(item:Item)->void:
inventory.remove_item(item)
func _get_drag_data(at_position:Vector2)->Variant:
var item := inventory.item_at(at_position)
if item == null: return null
var drag_data = ItemDrag.new(self, item, _create_item_preview(item))
set_drag_preview(drag_data.preview)
return drag_data
func _can_drop_data(at_position:Vector2, data:Variant)->bool:
if !data is ItemDrag: return false
var drag_data := data as ItemDrag
# Check if the item can fit in the inventory at this position
return !inventory.intersects_at(drag_data.item, at_position)
func _drop_data(at_position:Vector2, data:Variant)->void:
if !data is ItemDrag: return
var drag_data := data as ItemDrag
drag_data.destination = self
if drag_data.source: drag_data.source.remove_item(drag_data.item)
inventory.add_item_at(drag_data.item, at_position)
某些功能可以通过ItemDrag类来强制执行,特别是赋予诸如添加/删除项目之类的source更destination强大的类型,以确保它们具有处理/添加/删除项目的接口。但定义这组函数是处理完整的拖放操作流程的良好开端。
全局拖放
除了上述Control基于 GUI 的方法之外,Godot 还在生命周期中提供了一些事件钩子,Viewport并Control提供了查询任何拖放操作状态的函数:
# Viewport
func gui_is_dragging()->bool
func gui_get_drag_data()->Variant
func gui_is_drag_successful()->bool
# Control
func is_drag_successful()->bool
前两个应该很明显——第一个可以查询是否在任何时候发生了拖放操作,第二个可以获取data我们在_get_drag_data回调中返回的相同内容Control(或者data包含的任何内容force_drag,或者其他方式,拖放操作是如何启动的)。
最后两个参数可以与func _notification(what:int)->void处理程序一起使用,以便在拖放操作开始或结束时接收全局通知,以及在拖放操作成功结束时接收通知:
func _notification(what:int)->void:
if what == Node.NOTIFICATION_DRAG_BEGIN:
# Drag data is available (populated by our _get_drag_data() function for example)
var data = get_viewport().gui_get_drag_data()
# Use the drag data
if what == Node.NOTIFICATION_DRAG_END:
# Drag data is no longer available and has been disposed already
print("Drag ended. Success: ", get_viewport().gui_is_drag_successful())
这些功能或许有用,但与拖放操作的上下文关系不大。某些用例可供相关的拖放接收者根据拖放数据突出显示自身。
有了这些工具,任何拖放场景都能轻松应对,希望这能帮您省去编写自定义拖放功能的麻烦。祝您在使用这些信息时一切顺利,取得成功!
P.
文章来源:https://dev.to/pdeveloper/godot-4x-drag-and-drop-5g13