更新自研插件
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
@tool
|
||||
extends EditorPlugin
|
||||
|
||||
## Mesh Collider - editor integration.
|
||||
##
|
||||
## Drag an FBX/GLB into a scene and the plugin adds a physics body with baked
|
||||
## collision shapes to that instance. Nothing is written to disk and no copy of
|
||||
## the model is made: the added nodes belong to the scene you are editing, so the
|
||||
## model file stays referenced exactly once.
|
||||
##
|
||||
## Everything the plugin does goes through the editor's undo system, so Ctrl+Z
|
||||
## reverts an injection and Ctrl+Shift+Z re-applies it.
|
||||
|
||||
const Collider := preload("mesh_collider.gd")
|
||||
const Settings := preload("mesh_collider_settings.gd")
|
||||
|
||||
const MENU_ADD := "Mesh Collider: 给选中模型加碰撞 / Add collision to selected"
|
||||
const MENU_REMOVE := "Mesh Collider: 移除选中模型的碰撞 / Remove collision"
|
||||
const MENU_SETTINGS := "Mesh Collider: 设置 / Settings"
|
||||
|
||||
## How often the edited scene is scanned for freshly dropped models. Detection
|
||||
## needs polling because Godot exposes no "a model was dragged in" signal.
|
||||
const SCAN_INTERVAL_SEC := 0.4
|
||||
|
||||
var _scan_accum := 0.0
|
||||
var _needs_scan := false
|
||||
## Instances already handled this session (instance id -> true), so a model is
|
||||
## processed once and never re-processed while the editor stays open.
|
||||
var _handled: Dictionary = {}
|
||||
|
||||
|
||||
func _enter_tree() -> void:
|
||||
Settings.install()
|
||||
add_tool_menu_item(MENU_ADD, _add_pressed)
|
||||
add_tool_menu_item(MENU_REMOVE, _remove_pressed)
|
||||
add_tool_menu_item(MENU_SETTINGS, _open_settings)
|
||||
get_tree().node_added.connect(_on_node_added)
|
||||
_needs_scan = true
|
||||
print("[MeshCollider] ready. Settings live in Project Settings > mesh_collider.")
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
if get_tree() != null and get_tree().node_added.is_connected(_on_node_added):
|
||||
get_tree().node_added.disconnect(_on_node_added)
|
||||
remove_tool_menu_item(MENU_ADD)
|
||||
remove_tool_menu_item(MENU_REMOVE)
|
||||
remove_tool_menu_item(MENU_SETTINGS)
|
||||
|
||||
|
||||
func _on_node_added(node: Node) -> void:
|
||||
# Cheap pre-filter: only a 3D node can be a model root worth scanning for.
|
||||
if node is Node3D:
|
||||
_needs_scan = true
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
_scan_accum += delta
|
||||
if not _needs_scan and _scan_accum < SCAN_INTERVAL_SEC:
|
||||
return
|
||||
_scan_accum = 0.0
|
||||
_needs_scan = false
|
||||
if Settings.get_bool(Settings.KEY_AUTO):
|
||||
_scan_scene()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Detection
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Walk the edited scene for model instances that have not been handled yet.
|
||||
func _scan_scene() -> void:
|
||||
var scene_root := get_editor_interface().get_edited_scene_root()
|
||||
if scene_root == null:
|
||||
return
|
||||
var found: Array[Node] = []
|
||||
_collect_model_instances(scene_root, found)
|
||||
for node in found:
|
||||
if _handled.has(node.get_instance_id()):
|
||||
continue
|
||||
_handle(node)
|
||||
|
||||
|
||||
func _collect_model_instances(node: Node, out: Array[Node]) -> void:
|
||||
for child in node.get_children():
|
||||
if child is Node3D and _is_configured_model_instance(child):
|
||||
out.append(child)
|
||||
_collect_model_instances(child, out)
|
||||
|
||||
|
||||
## A model instance we should act on: instantiated straight from a configured
|
||||
## model file, and not already carrying one of our bodies.
|
||||
func _is_configured_model_instance(node: Node) -> bool:
|
||||
if node.scene_file_path.is_empty():
|
||||
return false
|
||||
if not Settings.is_configured_model(node.scene_file_path):
|
||||
return false
|
||||
if not ResourceLoader.exists(node.scene_file_path):
|
||||
return false # not imported yet
|
||||
return not Collider.has_injected_collision(node)
|
||||
|
||||
|
||||
func _handle(node: Node) -> void:
|
||||
if not is_instance_valid(node):
|
||||
return
|
||||
_handled[node.get_instance_id()] = true
|
||||
_inject(node)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Injection
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Add collision to `model` through the undo system.
|
||||
func _inject(model: Node, source: String = "") -> void:
|
||||
if not (model is Node3D):
|
||||
return
|
||||
if Collider.has_injected_collision(model):
|
||||
return
|
||||
if source.is_empty():
|
||||
source = String(model.scene_file_path)
|
||||
|
||||
var skip_existing := Settings.get_bool(Settings.KEY_SKIP_EXISTING)
|
||||
if skip_existing and Collider.has_any_collision(model):
|
||||
print("[MeshCollider] %s already has collision; skipped." % model.name)
|
||||
return
|
||||
|
||||
# Built detached on purpose: the editor's undo system owns every tree change
|
||||
# in this plugin, so inserting the body here would mean undoing a change we
|
||||
# had already applied behind undo's back.
|
||||
var body := Collider.build_collision_body(
|
||||
model,
|
||||
Settings.shape_type(),
|
||||
Settings.body_type(),
|
||||
skip_existing,
|
||||
Settings.get_int(Settings.KEY_LAYER),
|
||||
Settings.get_int(Settings.KEY_MASK))
|
||||
if body == null:
|
||||
print("[MeshCollider] %s: no usable mesh geometry found." % model.name)
|
||||
return
|
||||
|
||||
var scene_root := get_editor_interface().get_edited_scene_root()
|
||||
var undo := get_undo_redo()
|
||||
undo.create_action("Mesh Collider: add collision to %s" % model.name)
|
||||
undo.add_do_method(model, "add_child", body, true)
|
||||
# `owner` can only be assigned once the body is inside the tree - without it
|
||||
# Godot would not serialise the body or its shapes into the saved scene.
|
||||
undo.add_do_method(Collider, "prepare_for_scene", body, scene_root)
|
||||
undo.add_do_reference(body)
|
||||
undo.add_undo_method(model, "remove_child", body)
|
||||
undo.commit_action()
|
||||
|
||||
get_editor_interface().mark_scene_as_unsaved()
|
||||
print("[MeshCollider] %s (%s): added %s with %d shape(s)"
|
||||
% [model.name, source.get_file(), body.get_class(), Collider.count_shapes(body)])
|
||||
|
||||
|
||||
## Remove the collision this plugin added to `model`, undoably.
|
||||
func _remove(model: Node) -> void:
|
||||
if not is_instance_valid(model):
|
||||
return
|
||||
var body := _find_added_body(model)
|
||||
if body == null:
|
||||
print("[MeshCollider] %s has no collision added by this plugin." % model.name)
|
||||
return
|
||||
var scene_root := get_editor_interface().get_edited_scene_root()
|
||||
var undo := get_undo_redo()
|
||||
undo.create_action("Mesh Collider: remove collision from %s" % model.name)
|
||||
undo.add_do_method(model, "remove_child", body)
|
||||
undo.add_undo_method(model, "add_child", body, true)
|
||||
undo.add_undo_method(body, "set_owner", scene_root)
|
||||
# Keep the object alive so redo can put the same body back.
|
||||
undo.add_undo_reference(body)
|
||||
undo.commit_action()
|
||||
get_editor_interface().mark_scene_as_unsaved()
|
||||
print("[MeshCollider] %s: collision removed." % model.name)
|
||||
|
||||
|
||||
func _find_added_body(model: Node) -> Node:
|
||||
if not is_instance_valid(model):
|
||||
return null
|
||||
for child in model.get_children():
|
||||
if child.has_meta(Collider.META_OWNED):
|
||||
return child
|
||||
return null
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Menu actions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _add_pressed() -> void:
|
||||
var targets := _selected_models()
|
||||
for model in targets:
|
||||
_handled[model.get_instance_id()] = true
|
||||
_inject(model)
|
||||
if targets.is_empty():
|
||||
print("[MeshCollider] 先选中一个拖入的 FBX/GLB 实例 / "
|
||||
+ "select a model instance first")
|
||||
|
||||
|
||||
func _remove_pressed() -> void:
|
||||
var targets := _selected_models()
|
||||
for model in targets:
|
||||
_remove(model)
|
||||
if targets.is_empty():
|
||||
print("[MeshCollider] 先选中一个模型实例 / select a model instance first")
|
||||
|
||||
|
||||
## Selected nodes that are model instances. Copied into a fresh array, because
|
||||
## injection modifies the tree while the selection array is still live.
|
||||
func _selected_models() -> Array[Node]:
|
||||
var out: Array[Node] = []
|
||||
var selection := get_editor_interface().get_selection().get_selected_nodes()
|
||||
for node in selection:
|
||||
if node is Node3D and not String(node.scene_file_path).is_empty() \
|
||||
and Settings.is_configured_model(node.scene_file_path):
|
||||
out.append(node)
|
||||
return out
|
||||
|
||||
|
||||
func _open_settings() -> void:
|
||||
# The settings are ordinary project settings; point the user at them rather
|
||||
# than inventing a second place to configure the same values.
|
||||
get_editor_interface().call_deferred("set_main_screen_editor", "Project")
|
||||
print("[MeshCollider] settings are under Project Settings > mesh_collider")
|
||||
Reference in New Issue
Block a user