更新自研插件
This commit is contained in:
@@ -30,15 +30,24 @@
|
||||
|
||||
## 用法
|
||||
|
||||
**拖进去就行。** 插件会轮询当前编辑的场景,发现新拖入的模型实例就自动加碰撞,并且整个操作走编辑器的撤销系统 —— **Ctrl+Z 可以撤销,Ctrl+Shift+Z 可以重做**。
|
||||
**拖进去就行。** 把 FBX / GLB 拖进场景,插件会弹窗问一句要不要加碰撞:
|
||||
|
||||
菜单里还有三个手动操作:
|
||||
| 弹窗里的选择 | 结果 |
|
||||
| --- | --- |
|
||||
| **添加碰撞** | 给这一批模型加上碰撞体,走撤销系统 —— **Ctrl+Z 可以撤销,Ctrl+Shift+Z 可以重做** |
|
||||
| **不添加** | 什么都不做,模型留在场景里 |
|
||||
| 勾上**不再询问**再选 | 记住这次的选择,以后不再弹窗(想恢复询问,去 项目设置 → mesh_collider 打开 `ask_before_adding`) |
|
||||
|
||||
一次拖入多个模型只弹**一个**弹窗,确认后整批处理。不想被问就把 `ask_before_adding` 关掉,改由 `auto_add_collision` 决定。
|
||||
|
||||
菜单里还有四个手动操作:
|
||||
|
||||
| 菜单项 | 作用 |
|
||||
| --- | --- |
|
||||
| 扫描场景中的模型 | 列出场景里所有还没加碰撞的模型(`auto_add_collision` 打开时直接加) |
|
||||
| 给选中模型加碰撞 | 对当前选中的模型实例执行一次 |
|
||||
| 移除选中模型的碰撞 | 移除本插件加的那个碰撞体(同样可撤销) |
|
||||
| 设置 | 跳到项目设置页 |
|
||||
| 打开项目设置 | 跳到项目设置页 |
|
||||
|
||||
---
|
||||
|
||||
@@ -73,36 +82,34 @@ Chair_PC_04 ← 模型实例,仍然引用 res://.../chair.
|
||||
|
||||
## 设置
|
||||
|
||||
设置有两个入口,值完全同步:
|
||||
设置只有**一个入口**:**项目设置 → mesh_collider**(菜单里的『打开项目设置』会直接跳过去)。分节名显示为 **Mesh Collider**,每一项的名字由 Godot 按 key 自动美化:
|
||||
|
||||
- **右侧停靠栏的 `MC` 标签页**(检查器那一列)—— 装了插件就有;
|
||||
- **项目设置 → mesh_collider** —— 想和其他项目设置一起看时用。
|
||||
```
|
||||
Ask Before Adding [✓ 启用]
|
||||
Auto Add Collision [ 启用]
|
||||
Collision Shape [Trimesh ▾]
|
||||
Collision Body [StaticBody3D ▾]
|
||||
Skip Meshes with Collision [ 启用]
|
||||
碰撞层 [1][2][3][4] … [32] ← Godot 原生的层勾选网格
|
||||
碰撞遮罩 [1][2][3][4] … [32]
|
||||
Model Extensions fbx,glb,gltf,…
|
||||
```
|
||||
|
||||
> **关于位置。** 你希望它出现在「检查器 / 信号 / 分组」那一排。那三个标签属于 Godot **内建的检查器容器**,插件 API 没有向它注入标签页的接口,所以做不到完全并排。可达的最接近位置是**右列停靠栏**,面板会和检查器出现在同一列里。`MC` 之上悬停会显示完整名称。
|
||||
>
|
||||
> **关于悬浮说明。** 面板**没有**使用 `EditorInspector`,而是手写表单 —— 因为 `EditorInspector` 仅在少数列出的枚举类型上把 `hint_string` 当 tooltip,勾选框和数值框悬停时什么都不显示,做不到"每个设置项都有说明"。手写之后每个设置项的标签和控件都能带 tooltip。
|
||||
- 改动**立即写入**,插件下一次扫描就读到,没有"应用"这一步;
|
||||
- 保存交给编辑器(退出或保存项目时落盘)—— 唯一的例外是下面那个"不再询问";
|
||||
- **没有逐项说明**:项目设置的每一行只有名字和控件,Godot 没给插件留"显示名 / 描述"的位置。每项的含义看下面这张表。
|
||||
|
||||
面板不使用 `EditorInspector`,而是逐项构建控件,但值仍然读写**同一批 ProjectSettings 键**,所以和项目设置页不会漂移。改动**立即写入**,插件下一次扫描就读到,没有"应用"这一步。
|
||||
|
||||
页面底部三个按钮:
|
||||
|
||||
| 按钮 | 作用 |
|
||||
| --- | --- |
|
||||
| 恢复默认值 | 把插件拥有的**全部**设置改回默认值 |
|
||||
| 保存到 project.godot | 立即落盘(不点的话,编辑器退出或保存项目时也会写) |
|
||||
| 在项目设置中打开 | 跳到项目设置页 |
|
||||
> 早期版本还有一个右列停靠面板(`MC` 标签页),只是把同一批键换了种画法,现已删掉:两处 UI 意味着两处要维护的布局问题,而设置本来就该在一个地方看。顺带一提,项目设置里 `collision_layer` / `collision_mask` 用的是 Godot 原生的 32 层勾选网格,比面板里的数字输入框好用。
|
||||
|
||||
| 设置 | 默认 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `ask_before_adding` | **`true`** | 拖入模型时**弹窗询问**是否添加碰撞 |
|
||||
| `auto_add_collision` | `false` | 不询问时直接添加;仅在 `ask_before_adding` 关闭后生效 |
|
||||
| `always_add_without_asking` | `false` | 由弹窗的「不再询问」写入 |
|
||||
| `collision_shape` | **`Trimesh`** | `Convex Hull` / `Trimesh` / `Box` / `Sphere` / `Capsule` / `Cylinder` |
|
||||
| `collision_body` | `StaticBody3D` | `StaticBody3D` / `RigidBody3D` / `Area3D` / `CharacterBody3D` |
|
||||
| `skip_meshes_with_collision` | `false` | 跳过已经位于物理体下的网格(模型自带碰撞时打开) |
|
||||
| `collision_layer` / `collision_mask` | `1` | 碰撞层 / 掩码 |
|
||||
| `model_extensions` | `fbx glb gltf dae obj blend …` | 识别为模型的后缀 |
|
||||
| `skip_dirs` | `addons/`, `.godot/` | 永不扫描的目录 |
|
||||
|
||||
### 拖入模型时的三种行为
|
||||
|
||||
@@ -110,13 +117,13 @@ Chair_PC_04 ← 模型实例,仍然引用 res://.../chair.
|
||||
|
||||
| `ask_before_adding` | `auto_add_collision` | 拖入模型时 |
|
||||
| --- | --- | --- |
|
||||
| **`true`**(默认) | 忽略 | **弹窗询问**,包含「不再询问」勾选框和设置入口提示 |
|
||||
| **`true`**(默认) | 忽略 | **弹窗询问**,带「不再询问」勾选框 |
|
||||
| `false` | `true` | 直接添加碰撞,不问 |
|
||||
| `false` | `false` | 什么都不做,只能从 项目 > 工具 手动执行 |
|
||||
|
||||
弹窗里的「不再询问」按你本次的选择记忆:选「添加碰撞」→ 以后直接添加;选「不添加」→ 以后不再添加也不再询问。两种情况都会把 `ask_before_adding` 关掉,想恢复询问就在 `MC` 面板里重新打开。
|
||||
弹窗里的「不再询问」按你本次的选择记忆:选「添加碰撞」→ 以后直接添加;选「不添加」→ 以后不再添加也不再询问。两种情况都会把 `ask_before_adding` 关掉,想恢复询问就去 **项目设置 → mesh_collider** 把它重新打开。
|
||||
|
||||
一次拖入多个模型只会弹**一个**弹窗,确认后整批处理。
|
||||
> 这一项是唯一会**主动落盘**的设置(`ProjectSettings.save()`),因为"不再询问"必须活过这次编辑器会话。其余设置不主动落盘,交给编辑器自己保存。
|
||||
|
||||
### 形状怎么选
|
||||
|
||||
@@ -200,9 +207,11 @@ Godot 的物理按"1 单位 = 1 米"调参。如果模型导入后只有几厘
|
||||
|
||||
## 已知取舍
|
||||
|
||||
- **检测靠轮询**(0.4 秒一次)。Godot 没有暴露"模型被拖进来"的信号,这是唯一可靠的做法。
|
||||
- **检测靠轮询**(0.4 秒一次)。Godot 没有暴露"模型被拖进来"的信号 —— 编辑器自己在内部处理这次 drop,不通知任何人 —— 所以只能盯着场景。为了不重蹈"每帧扫全场景"的覆辙,轮询有两道闸:先用 `SceneTree.get_node_count()`(O(1))判断整棵树有没有增删,**没变就直接跳过扫描**;变了也只扫当前编辑的场景。编辑器空转时这个定时器不做任何事。
|
||||
- **基线不等于"新拖入"**。切换场景时,场景里已有的模型会被记进"已知"集合,不弹窗 —— 否则每开一个场景都会被问一遍。代价是:在场景刚打开后的 0.4 秒内拖入的模型会被当成基线,不弹窗(用菜单手动处理即可)。
|
||||
- **撤销/重做会当成新拖入**。Ctrl+Z 删掉模型再 Ctrl+Shift+Z 放回来,节点实例 id 变了,插件会再问一次。功能上没错,只是多问一句。
|
||||
- **变换在注入前一次性快照**。所有网格的世界变换在**模型确实处于场景树中**时解析好,再交给构建函数。这有两个原因:刚体是**故意游离**的(由撤销系统负责挂载),而 `global_transform` 在树外会打印 `Condition "!is_inside_tree()" is true` 并返回单位阵;更重要的是,依赖"读取瞬间节点仍在树里"会让碰撞在边界情况下静默烘焙到原点。快照把这件事变成确定的,缺了变换的网格会被跳过而不是猜一个。
|
||||
- **已验证的 API 兼容性**:本插件只使用在 Godot 4.7.2 上实测过的 API。踩过的坑包括 `MeshInstance3D.skeleton` 是 `NodePath` 而不是 `Skeleton3D`、`ClassDB.can_instantiate()` 才是判断节点能否容纳子节点的正确方法、以及 GDScript 不允许 `const` 字典的值引用其他常量(所以库里的枚举默认值写成字面量索引)。
|
||||
- **已验证的 API 兼容性**:本插件只使用在 Godot 4.7.2 上实测过的 API。踩过的坑包括 `MeshInstance3D.skeleton` 是 `NodePath` 而不是 `Skeleton3D`、`ClassDB.can_instantiate()` 才是判断节点能否容纳子节点的正确方法、GDScript 不允许 `const` 字典的值引用其他常量(所以库里的枚举默认值写成字面量索引)、以及 `AcceptDialog.add_child()` 会把子控件**铺满**内容区而不是排在正文下面(所以弹窗的正文和勾选框要自己装进一个 `VBoxContainer`)。
|
||||
|
||||
---
|
||||
|
||||
@@ -212,10 +221,9 @@ Godot 的物理按"1 单位 = 1 米"调参。如果模型导入后只有几厘
|
||||
demo_project/
|
||||
├─ addons/mesh_collider/ ← 插件本体(复制这个目录即可)
|
||||
│ ├─ plugin.cfg
|
||||
│ ├─ mesh_collider_plugin.gd 编辑器集成(检测 / 注入 / 撤销 / 停靠面板注册)
|
||||
│ ├─ mesh_collider_plugin.gd 编辑器集成(检测 / 拖入弹窗 / 注入 / 撤销)
|
||||
│ ├─ mesh_collider.gd 核心逻辑(纯逻辑,不依赖编辑器)
|
||||
│ ├─ mesh_collider_settings.gd 项目设置 + 面板用的 label / tooltip 文本
|
||||
│ ├─ mesh_collider_panel.gd 右侧 `MC` 标签页的设置面板
|
||||
│ ├─ mesh_collider_settings.gd 项目设置 + 每项的 label / tooltip 文本
|
||||
│ └─ README.md
|
||||
├─ assets/models/ 测试模型(GLB + 带骨骼动画的 FBX)
|
||||
└─ tests/ 辅助脚本
|
||||
|
||||
@@ -60,9 +60,6 @@ enum BodyType {
|
||||
CHARACTER, ## CharacterBody3D - player/NPC.
|
||||
}
|
||||
|
||||
const SHAPE_TYPE_NAMES: PackedStringArray = [
|
||||
"convex_hull", "trimesh", "box", "sphere", "capsule", "cylinder",
|
||||
]
|
||||
const BODY_TYPE_NAMES: PackedStringArray = [
|
||||
"static", "rigid", "area", "character",
|
||||
]
|
||||
@@ -119,15 +116,6 @@ static func is_imported_model(path: String) -> bool:
|
||||
return FileAccess.file_exists(path + ".import")
|
||||
|
||||
|
||||
## True when `node` is a root node instantiated directly from a model file.
|
||||
static func is_model_instance(node: Node) -> bool:
|
||||
if node == null or not is_instance_valid(node):
|
||||
return false
|
||||
if node.scene_file_path.is_empty():
|
||||
return false
|
||||
return is_imported_model(node.scene_file_path)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Collision generation
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -612,27 +600,3 @@ static func _cylinder_shape(mi: MeshInstance3D, to_local: Transform3D) -> Cylind
|
||||
shape.height = h
|
||||
shape.set_meta(META_SHAPE_OFFSET, box.get_center())
|
||||
return shape
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Diagnostics
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## A one-line description of the collision under `root`, for log output.
|
||||
static func describe_collision(root: Node) -> String:
|
||||
var shapes := count_shapes(root)
|
||||
if shapes == 0:
|
||||
return "no collision"
|
||||
var kinds: Dictionary = {}
|
||||
var stack: Array[Node] = [root]
|
||||
while not stack.is_empty():
|
||||
var n: Node = stack.pop_back()
|
||||
if n is CollisionShape3D and (n as CollisionShape3D).shape != null:
|
||||
var k := (n as CollisionShape3D).shape.get_class()
|
||||
kinds[k] = int(kinds.get(k, 0)) + 1
|
||||
for c in n.get_children():
|
||||
stack.append(c)
|
||||
var parts: Array[String] = []
|
||||
for k in kinds:
|
||||
parts.append("%s x%d" % [k, kinds[k]])
|
||||
return "%d shape(s): %s" % [shapes, ", ".join(parts)]
|
||||
|
||||
@@ -1 +1 @@
|
||||
uid://byxlq5nu6745l
|
||||
uid://deylj2mv7ob5h
|
||||
|
||||
@@ -11,26 +11,55 @@ extends EditorPlugin
|
||||
## Every change goes through the editor's undo system, so Ctrl+Z reverts an
|
||||
## injection and Ctrl+Shift+Z re-applies it.
|
||||
##
|
||||
## SCOPE - this plugin touches no editor UI beyond its own menu entries.
|
||||
## SCOPE - a tool menu and one confirmation dialog. No dock panel, no overlay.
|
||||
##
|
||||
## There is no dock panel, no dialog, no overlay and no per-frame work. Earlier
|
||||
## versions had all four, and each of them caused trouble that could not be
|
||||
## reproduced or diagnosed from here:
|
||||
## Earlier versions did more, and every extra piece caused trouble that could not
|
||||
## be reproduced or diagnosed from here:
|
||||
##
|
||||
## * a per-frame scan of the whole edited scene (heavy on a large scene), and a
|
||||
## SceneTree.node_added hook that made it run even more often;
|
||||
## * `AcceptDialog` / `PopupPanel` / a hand-built overlay, every one of which
|
||||
## opened thousands of pixels too tall;
|
||||
## * a dock panel whose wrapping labels widened its dock column and pushed the
|
||||
## editor's own docks and the viewport out of the window.
|
||||
## * a dock panel, which brought its own layout problems and only ever mirrored
|
||||
## Project Settings > mesh_collider anyway.
|
||||
##
|
||||
## So discovery is now explicit (a menu action) and reports to the Output panel,
|
||||
## and all configuration lives in Project Settings > mesh_collider. Nothing here
|
||||
## participates in the editor's layout.
|
||||
## So configuration has exactly one home - Project Settings > mesh_collider, with
|
||||
## the menu's "打开项目设置" to get there - and the scene is watched on a 0.4 s
|
||||
## timer whose walk is skipped outright whenever the tree has not changed at all
|
||||
## (see `POLL_SECONDS`). The one dialog left is given an explicit size instead of
|
||||
## being allowed to size itself to its label.
|
||||
|
||||
const Collider := preload("mesh_collider.gd")
|
||||
const Settings := preload("mesh_collider_settings.gd")
|
||||
|
||||
## Seconds between two looks at the edited scene.
|
||||
##
|
||||
## Godot gives a plugin no signal for "a model was just dragged in" - the editor
|
||||
## handles the drop internally and never tells anyone - so the scene has to be
|
||||
## watched. An earlier version of this plugin scanned every frame and made the
|
||||
## editor sluggish on a large scene; this one is bounded two ways:
|
||||
##
|
||||
## * `SceneTree.get_node_count()` is O(1), and an unchanged count means nothing
|
||||
## was added or removed anywhere, so the walk is skipped outright - an idle
|
||||
## editor does no work at all;
|
||||
## * the walk only ever visits the edited scene.
|
||||
const POLL_SECONDS := 0.4
|
||||
|
||||
var _timer: Timer = null
|
||||
## Instance ids of the model instances already present in the edited scene.
|
||||
## Anything in here has been seen, and will not be asked about again.
|
||||
var _known: Dictionary = {}
|
||||
## The scene the entries in `_known` belong to.
|
||||
var _scene_root: Node = null
|
||||
var _last_node_count := -1
|
||||
|
||||
var _dialog: ConfirmationDialog = null
|
||||
var _message: Label = null
|
||||
var _remember: CheckBox = null
|
||||
## Models waiting for the dialog's answer. A further drop while the dialog is
|
||||
## open joins this batch instead of opening a second dialog.
|
||||
var _pending: Array[Node] = []
|
||||
|
||||
const MENU_SCAN := "Mesh Collider: 扫描场景中的模型 / Scan scene for models"
|
||||
const MENU_ADD := "Mesh Collider: 给选中模型加碰撞 / Add collision to selected"
|
||||
const MENU_REMOVE := "Mesh Collider: 移除选中模型的碰撞 / Remove collision"
|
||||
@@ -43,8 +72,15 @@ func _enter_tree() -> void:
|
||||
add_tool_menu_item(MENU_ADD, _add_pressed)
|
||||
add_tool_menu_item(MENU_REMOVE, _remove_pressed)
|
||||
add_tool_menu_item(MENU_SETTINGS, _open_settings)
|
||||
|
||||
_timer = Timer.new()
|
||||
_timer.wait_time = POLL_SECONDS
|
||||
_timer.autostart = true
|
||||
_timer.timeout.connect(_poll)
|
||||
add_child(_timer)
|
||||
|
||||
print("[MeshCollider] ready. All actions are under 项目 > 工具; settings are in "
|
||||
+ "Project Settings > mesh_collider. No dock panel, no dialogs.")
|
||||
+ "Project Settings > mesh_collider.")
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
@@ -52,6 +88,13 @@ func _exit_tree() -> void:
|
||||
remove_tool_menu_item(MENU_ADD)
|
||||
remove_tool_menu_item(MENU_REMOVE)
|
||||
remove_tool_menu_item(MENU_SETTINGS)
|
||||
if _timer != null:
|
||||
_timer.stop()
|
||||
_timer.queue_free()
|
||||
_timer = null
|
||||
if _dialog != null:
|
||||
_dialog.queue_free()
|
||||
_dialog = null
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -107,7 +150,8 @@ func _scan_pressed() -> void:
|
||||
_add_all(pending)
|
||||
else:
|
||||
print("[MeshCollider] 未改动场景。选中模型后用 项目 > 工具 > "
|
||||
+ "『给选中模型加碰撞』处理,或打开设置里的『扫描后自动添加』。")
|
||||
+ "『给选中模型加碰撞』处理,或把 项目设置 → mesh_collider 里的 "
|
||||
+ "auto_add_collision 打开。")
|
||||
|
||||
|
||||
func _add_all(pending: Array[Node]) -> void:
|
||||
@@ -220,6 +264,158 @@ func _find_added_body(model: Node) -> Node:
|
||||
return null
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Drag-in detection
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _poll() -> void:
|
||||
var root: Node = get_editor_interface().get_edited_scene_root()
|
||||
if root != _scene_root:
|
||||
# A different scene is open. Whatever we knew belonged to the old one, and
|
||||
# what is already in the new one is taken as the baseline: opening a scene
|
||||
# is not "dragging a model in", and prompting for every model of a freshly
|
||||
# opened scene would be intolerable.
|
||||
_scene_root = root
|
||||
_known.clear()
|
||||
if root != null:
|
||||
var ignored: Array[Node] = []
|
||||
_collect_new_models(root, ignored)
|
||||
return
|
||||
if root == null:
|
||||
return
|
||||
|
||||
# Nothing was added or removed anywhere in the editor, so the scene cannot
|
||||
# have gained a model. This is the O(1) check that makes the timer free when
|
||||
# the editor is idle.
|
||||
var count := get_tree().get_node_count()
|
||||
if count == _last_node_count:
|
||||
return
|
||||
_last_node_count = count
|
||||
|
||||
var fresh: Array[Node] = []
|
||||
_collect_new_models(root, fresh)
|
||||
if fresh.is_empty():
|
||||
return
|
||||
|
||||
if Settings.get_bool(Settings.KEY_ASK):
|
||||
_ask(fresh)
|
||||
elif Settings.get_bool(Settings.KEY_AUTO):
|
||||
_add_all(fresh)
|
||||
# Neither: asking was turned off and auto is off too, so a drop is left alone
|
||||
# and stays reachable under 项目 > 工具.
|
||||
|
||||
|
||||
## Record every model instance under `node` we have not seen before, and append
|
||||
## the ones that still need collision to `out`. Recording happens whether or not
|
||||
## a model is acted on, so a model the user declined is not offered twice.
|
||||
func _collect_new_models(node: Node, out: Array[Node]) -> void:
|
||||
for child in node.get_children():
|
||||
if child is Node3D and not _known.has(child.get_instance_id()):
|
||||
_known[child.get_instance_id()] = true
|
||||
if _is_pending_model(child):
|
||||
out.append(child)
|
||||
_collect_new_models(child, out)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The drag-in dialog
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Ask about a batch of freshly dragged-in models. The answer applies to the whole
|
||||
## batch, so dropping five files asks once.
|
||||
func _ask(models: Array[Node]) -> void:
|
||||
if _dialog == null:
|
||||
_build_dialog()
|
||||
if _dialog.visible:
|
||||
_pending.append_array(models)
|
||||
return
|
||||
_pending = models.duplicate()
|
||||
_remember.button_pressed = false
|
||||
_message.text = _ask_text(_pending)
|
||||
# An explicit width, because a dialog that sizes itself to its label is how an
|
||||
# earlier version of this plugin ended up opening thousands of pixels tall.
|
||||
_dialog.popup_centered(Vector2i(440, 0))
|
||||
|
||||
|
||||
func _ask_text(models: Array[Node]) -> String:
|
||||
if models.size() == 1:
|
||||
return ("拖入的模型『%s』还没有碰撞体。\n现在给它加上吗?"
|
||||
% models[0].name)
|
||||
return ("拖入的 %d 个模型还没有碰撞体。\n现在给它们都加上吗?" % models.size())
|
||||
|
||||
|
||||
func _build_dialog() -> void:
|
||||
_dialog = ConfirmationDialog.new()
|
||||
_dialog.title = "Mesh Collider"
|
||||
_dialog.ok_button_text = "添加碰撞"
|
||||
_dialog.cancel_button_text = "不添加"
|
||||
_dialog.exclusive = true
|
||||
# Godot draws control text through the editor's translation domain, which
|
||||
# would rewrite our own strings with whatever the editor's locale calls them.
|
||||
# Off for the dialog, so it always says what this file says.
|
||||
_dialog.auto_translate_mode = Node.AUTO_TRANSLATE_MODE_DISABLED
|
||||
|
||||
# The message and the checkbox go into a container of our own rather than into
|
||||
# `dialog_text` plus a bare `add_child`. An `AcceptDialog` stretches any extra
|
||||
# child over its whole content area instead of laying it out under the label,
|
||||
# so the checkbox ends up drawn on top of the message.
|
||||
#
|
||||
# The cost is one empty line: `dialog_text` stays empty but the dialog's own
|
||||
# label still reserves its height, and hiding it does not help - AcceptDialog
|
||||
# computes its minimum size without looking at visibility.
|
||||
var body := VBoxContainer.new()
|
||||
body.name = "Body"
|
||||
body.add_theme_constant_override("separation", 10)
|
||||
|
||||
_message = Label.new()
|
||||
_message.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
_message.custom_minimum_size = Vector2(360, 0)
|
||||
_message.auto_translate_mode = Node.AUTO_TRANSLATE_MODE_DISABLED
|
||||
body.add_child(_message)
|
||||
|
||||
_remember = CheckBox.new()
|
||||
_remember.text = "不再询问"
|
||||
_remember.auto_translate_mode = Node.AUTO_TRANSLATE_MODE_DISABLED
|
||||
_remember.tooltip_text = ("记住这次的选择:选『添加碰撞』以后直接加,"
|
||||
+ "选『不添加』以后不再加也不再问。\n"
|
||||
+ "想恢复询问,去 项目设置 → mesh_collider 把 ask_before_adding 重新打开。")
|
||||
body.add_child(_remember)
|
||||
|
||||
_dialog.add_child(body)
|
||||
add_child(_dialog)
|
||||
_dialog.confirmed.connect(_on_ask_confirmed)
|
||||
_dialog.canceled.connect(_on_ask_canceled)
|
||||
|
||||
|
||||
func _on_ask_confirmed() -> void:
|
||||
_finish_ask(true)
|
||||
|
||||
|
||||
func _on_ask_canceled() -> void:
|
||||
_finish_ask(false)
|
||||
|
||||
|
||||
func _finish_ask(accepted: bool) -> void:
|
||||
var batch := _pending
|
||||
_pending = []
|
||||
if accepted:
|
||||
_add_all(batch)
|
||||
if not _remember.button_pressed:
|
||||
return
|
||||
# "不再询问" is an explicit decision that has to outlive the editor session, so
|
||||
# this one is written out now; everything else is left to the editor's own
|
||||
# save. Two settings are enough to remember it - stop asking, and let
|
||||
# `auto_add_collision` carry the answer that was given.
|
||||
Settings.set_value(Settings.KEY_ASK, false)
|
||||
Settings.set_value(Settings.KEY_AUTO, accepted)
|
||||
var err := ProjectSettings.save()
|
||||
if err == OK:
|
||||
print("[MeshCollider] 已记住选择:%s,以后不再询问。"
|
||||
% ("拖入模型时直接加碰撞" if accepted else "拖入模型时不加碰撞"))
|
||||
else:
|
||||
push_warning("[MeshCollider] 保存设置失败,错误码 %d。" % err)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Menu actions
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -253,7 +449,22 @@ func _selected_models() -> Array[Node]:
|
||||
return out
|
||||
|
||||
|
||||
## Open Project Settings > mesh_collider.
|
||||
##
|
||||
## 4.7 has no API for this. `EditorInterface` offers nothing, and the obvious
|
||||
## `set_main_screen_editor("Project")` fails outright - "The editor name
|
||||
## 'Project' was not found" - because Project Settings is a dialog, not one of
|
||||
## the main screens. So the editor's own dialog node is looked up by class and
|
||||
## popped up. If a future editor renames or moves it, this degrades to printing
|
||||
## where the settings live rather than breaking.
|
||||
func _open_settings() -> void:
|
||||
get_editor_interface().call_deferred("set_main_screen_editor", "Project")
|
||||
var stack: Array[Node] = [get_tree().root]
|
||||
while not stack.is_empty():
|
||||
var node: Node = stack.pop_back()
|
||||
if node.get_class() == "ProjectSettingsEditor":
|
||||
node.call("popup_centered_clamped", Vector2i(1100, 760))
|
||||
return
|
||||
for child in node.get_children():
|
||||
stack.append(child)
|
||||
print("[MeshCollider] 设置项在 项目设置 > mesh_collider / "
|
||||
+ "settings are under Project Settings > mesh_collider")
|
||||
|
||||
@@ -1 +1 @@
|
||||
uid://dkjs4o4ligbxn
|
||||
uid://t4x7mtjp7wtj
|
||||
|
||||
@@ -24,13 +24,11 @@ const BODY_COUNT := 4
|
||||
const SHAPE_NAMES: PackedStringArray = [
|
||||
"convex_hull", "trimesh", "box", "sphere", "capsule", "cylinder",
|
||||
]
|
||||
const BODY_NAMES: PackedStringArray = [
|
||||
"static", "rigid", "area", "character",
|
||||
]
|
||||
const DEFAULT_MODEL_EXTENSIONS: PackedStringArray = [
|
||||
"fbx", "glb", "gltf", "dae", "obj", "blend", "md2", "md3", "vrm",
|
||||
]
|
||||
|
||||
const KEY_ASK := ROOT + "ask_before_adding"
|
||||
const KEY_AUTO := ROOT + "auto_add_collision"
|
||||
|
||||
const KEY_SHAPE := ROOT + "collision_shape"
|
||||
@@ -39,7 +37,6 @@ const KEY_SKIP_EXISTING := ROOT + "skip_meshes_with_collision"
|
||||
const KEY_LAYER := ROOT + "collision_layer"
|
||||
const KEY_MASK := ROOT + "collision_mask"
|
||||
const KEY_EXTENSIONS := ROOT + "model_extensions"
|
||||
const KEY_SKIP_DIRS := ROOT + "skip_dirs"
|
||||
|
||||
## The shape this plugin shipped as its default before 2.0 finalised.
|
||||
##
|
||||
@@ -58,22 +55,25 @@ const LEGACY_SHAPE_DEFAULT := 0 # MeshCollider.ShapeType.CONVEX_HULL
|
||||
## reason applied one level out: they must not depend on another script's enum
|
||||
## resolving. Keep them in step with MeshCollider's enum order.
|
||||
##
|
||||
## Each entry carries two kinds of text, and they are NOT interchangeable:
|
||||
## * `hint_string` - consumed by ProjectSettings / the built-in inspector. An
|
||||
## enum uses it for its option list, so it cannot hold prose.
|
||||
## * `label` / `tooltip` - consumed by this plugin's own settings panel, which
|
||||
## labels each row and shows the tooltip on hover.
|
||||
## `hint_string` is the one piece of text an entry carries, and its meaning
|
||||
## depends on the type: an enum uses it for its option list, an array for its
|
||||
## element type, and a bool for a short description. Only the first two are
|
||||
## visible - Project Settings renders a bool as a bare checkbox labelled 启用 and
|
||||
## has no slot for a per-setting description, so what each setting does is
|
||||
## documented in README.md instead.
|
||||
static func project_info() -> Dictionary:
|
||||
return {
|
||||
KEY_ASK: {
|
||||
"name": KEY_ASK,
|
||||
"type": TYPE_BOOL,
|
||||
"default": true,
|
||||
"hint_string": "Ask before adding collision to a dragged-in model",
|
||||
},
|
||||
KEY_AUTO: {
|
||||
"name": KEY_AUTO,
|
||||
"type": TYPE_BOOL,
|
||||
"default": false,
|
||||
"hint_string": "Add collision to every pending model found by a scan",
|
||||
"label": "扫描后自动添加",
|
||||
"tooltip": "打开后,点『扫描场景中的模型』会直接把找到的模型全部加上碰撞。\n"
|
||||
+ "关闭(默认):扫描只列出结果,由你决定是否处理。\n"
|
||||
+ "插件不做任何后台轮询,只有你点按钮时才扫描。",
|
||||
},
|
||||
KEY_SHAPE: {
|
||||
"name": KEY_SHAPE,
|
||||
@@ -81,10 +81,6 @@ static func project_info() -> Dictionary:
|
||||
"default": 1, # MeshCollider.ShapeType.TRIMESH
|
||||
"hint": PROPERTY_HINT_ENUM,
|
||||
"hint_string": "Convex Hull,Trimesh,Box,Sphere,Capsule,Cylinder",
|
||||
"label": "碰撞形状",
|
||||
"tooltip": "Trimesh:逐三角面精确,零简化。静态物体的最佳选择。\n"
|
||||
+ "Convex Hull:凸包,凹形会被填平,可用于会移动的物体。\n"
|
||||
+ "Box / Sphere / Capsule / Cylinder:单个包围体,最省性能。",
|
||||
},
|
||||
KEY_BODY: {
|
||||
"name": KEY_BODY,
|
||||
@@ -92,37 +88,24 @@ static func project_info() -> Dictionary:
|
||||
"default": 0, # MeshCollider.BodyType.STATIC
|
||||
"hint": PROPERTY_HINT_ENUM,
|
||||
"hint_string": "StaticBody3D,RigidBody3D,Area3D,CharacterBody3D",
|
||||
"label": "刚体类型",
|
||||
"tooltip": "碰撞体挂在哪种物理节点下。\n"
|
||||
+ "StaticBody3D:静态场景物件。\n"
|
||||
+ "RigidBody3D:参与物理模拟的道具。\n"
|
||||
+ "Area3D:只做触发检测,不阻挡。\n"
|
||||
+ "CharacterBody3D:玩家/NPC,移动由脚本接管。",
|
||||
},
|
||||
KEY_SKIP_EXISTING: {
|
||||
"name": KEY_SKIP_EXISTING,
|
||||
"type": TYPE_BOOL,
|
||||
"default": false,
|
||||
"hint_string": "Skip meshes already under a physics body",
|
||||
"label": "跳过已有碰撞的网格",
|
||||
"tooltip": "打开后,已经位于物理体下方的网格不再重复生成碰撞。\n"
|
||||
+ "模型自带碰撞时(例如用 -col 后缀导入)应该打开。",
|
||||
},
|
||||
KEY_LAYER: {
|
||||
"name": KEY_LAYER,
|
||||
"type": TYPE_INT,
|
||||
"default": 1,
|
||||
"hint": PROPERTY_HINT_LAYERS_3D_PHYSICS,
|
||||
"label": "碰撞层",
|
||||
"tooltip": "生成的碰撞体属于哪些层。用掩码表示,1 = 第 1 层。",
|
||||
},
|
||||
KEY_MASK: {
|
||||
"name": KEY_MASK,
|
||||
"type": TYPE_INT,
|
||||
"default": 1,
|
||||
"hint": PROPERTY_HINT_LAYERS_3D_PHYSICS,
|
||||
"label": "碰撞掩码",
|
||||
"tooltip": "生成的碰撞体会和哪些层发生碰撞。用掩码表示。",
|
||||
},
|
||||
KEY_EXTENSIONS: {
|
||||
"name": KEY_EXTENSIONS,
|
||||
@@ -132,17 +115,6 @@ static func project_info() -> Dictionary:
|
||||
]),
|
||||
"hint": PROPERTY_HINT_ARRAY_TYPE,
|
||||
"hint_string": "String",
|
||||
"label": "识别为模型的后缀",
|
||||
"tooltip": "逗号分隔,不带点。只有这些后缀的文件会被当作模型处理。",
|
||||
},
|
||||
KEY_SKIP_DIRS: {
|
||||
"name": KEY_SKIP_DIRS,
|
||||
"type": TYPE_PACKED_STRING_ARRAY,
|
||||
"default": PackedStringArray(["res://addons/", "res://.godot/"]),
|
||||
"hint": PROPERTY_HINT_ARRAY_TYPE,
|
||||
"hint_string": "String",
|
||||
"label": "永不扫描的目录",
|
||||
"tooltip": "逗号分隔。这些目录下的模型不会被自动处理。",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -226,19 +198,6 @@ static func set_value(key: String, value) -> void:
|
||||
ProjectSettings.set_setting(key, value)
|
||||
|
||||
|
||||
## Restore every setting this plugin owns back to its registered default.
|
||||
## Returns the number of settings that actually changed.
|
||||
static func reset_to_defaults() -> int:
|
||||
var all := _all_info()
|
||||
var changed := 0
|
||||
for key in all:
|
||||
var info: Dictionary = all[key]
|
||||
if ProjectSettings.get_setting(key) != info["default"]:
|
||||
ProjectSettings.set_setting(key, info["default"])
|
||||
changed += 1
|
||||
return changed
|
||||
|
||||
|
||||
## The configured shape type, clamped to the valid enum range.
|
||||
##
|
||||
## Returns a plain int rather than `MeshCollider.ShapeType` on purpose; see
|
||||
@@ -266,17 +225,3 @@ static func is_configured_model(path: String) -> bool:
|
||||
if String(e).to_lower().trim_prefix(".") == ext:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
## True when `path` is inside a directory the plugin must never scan.
|
||||
static func should_skip(path: String) -> bool:
|
||||
var p := path.replace("\\", "/")
|
||||
for dir in get_strings(KEY_SKIP_DIRS):
|
||||
var d := String(dir).replace("\\", "/")
|
||||
if d.is_empty():
|
||||
continue
|
||||
if not d.ends_with("/"):
|
||||
d += "/"
|
||||
if p.begins_with(d):
|
||||
return true
|
||||
return false
|
||||
|
||||
@@ -1 +1 @@
|
||||
uid://brsdbmd42dcqs
|
||||
uid://cev4c1qt4vtm
|
||||
|
||||
Reference in New Issue
Block a user