更新自研插件
This commit is contained in:
@@ -76,7 +76,7 @@ Chair_PC_04 ← 模型实例,仍然引用 res://.../chair.
|
|||||||
| 设置 | 默认 | 说明 |
|
| 设置 | 默认 | 说明 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `auto_add_collision` | `true` | 自动处理拖进场景的模型 |
|
| `auto_add_collision` | `true` | 自动处理拖进场景的模型 |
|
||||||
| `collision_shape` | `Convex Hull` | `Convex Hull` / `Trimesh` / `Box` / `Sphere` / `Capsule` / `Cylinder` |
|
| `collision_shape` | **`Trimesh`** | `Convex Hull` / `Trimesh` / `Box` / `Sphere` / `Capsule` / `Cylinder` |
|
||||||
| `collision_body` | `StaticBody3D` | `StaticBody3D` / `RigidBody3D` / `Area3D` / `CharacterBody3D` |
|
| `collision_body` | `StaticBody3D` | `StaticBody3D` / `RigidBody3D` / `Area3D` / `CharacterBody3D` |
|
||||||
| `skip_meshes_with_collision` | `false` | 跳过已经位于物理体下的网格(模型自带碰撞时打开) |
|
| `skip_meshes_with_collision` | `false` | 跳过已经位于物理体下的网格(模型自带碰撞时打开) |
|
||||||
| `collision_layer` / `collision_mask` | `1` | 碰撞层 / 掩码 |
|
| `collision_layer` / `collision_mask` | `1` | 碰撞层 / 掩码 |
|
||||||
@@ -85,9 +85,33 @@ Chair_PC_04 ← 模型实例,仍然引用 res://.../chair.
|
|||||||
|
|
||||||
### 形状怎么选
|
### 形状怎么选
|
||||||
|
|
||||||
- **Convex Hull**(默认)—— 每个网格一个凸包。性能好,可用于会移动的物体;凹形会被"填平"。
|
| 形状 | 精度 | 代价 |
|
||||||
- **Trimesh** —— 逐面精确,不做凸包简化。**只能用在静态物体上**,顶点多时开销较大。
|
| --- | --- | --- |
|
||||||
- **Box / Sphere / Capsule / Cylinder** —— 最省性能。这几种形状会**按每个网格的实际位置自动偏移居中**。
|
| **Trimesh**(默认) | 逐三角面精确,索引一个不差,不做任何简化 | 顶点多时开销大;**只能用于静态物体** |
|
||||||
|
| **Convex Hull** | 凸包,凹形被"填平"(椅子坐垫下方、脚轮之间会变实心) | 可用于移动物体,开销可控 |
|
||||||
|
| **Box / Sphere / Capsule / Cylinder** | 单个包围体,最粗 | 最省性能 |
|
||||||
|
|
||||||
|
**默认给 Trimesh,是因为静态场景物件本来就该用最精确的形状。** 精度不足的典型症状是"碰撞比模型胖"——那是凸包把凹陷填平了,换成 Trimesh 即可。
|
||||||
|
|
||||||
|
这几种基础形状会**按每个网格的实际位置自动偏移居中**,不会全部堆在原点。
|
||||||
|
|
||||||
|
> **Trimesh 与移动刚体不兼容。** `ConcavePolygonShape3D` 是静态专用形状,Godot 不会用它驱动物理模拟,而且**失效是静默的**。为避免踩坑,构建时会自动降级为凸包并打印告警 —— 需要会移动的碰撞体时,请把 `collision_shape` 换成凸包或基础形状。
|
||||||
|
|
||||||
|
### 怎么确认当前用的是哪种形状
|
||||||
|
|
||||||
|
在场景里选中 `CollisionBody` 下的 `CollisionShape3D`,看检视面板里的 **Shape** 属性:
|
||||||
|
|
||||||
|
| 看到 | 说明 |
|
||||||
|
| --- | --- |
|
||||||
|
| `ConcavePolygonShape3D` | **Trimesh** —— 逐面精确 |
|
||||||
|
| `ConvexPolygonShape3D` | **凸包** —— 凹形已被填平 |
|
||||||
|
| `BoxShape3D` / `SphereShape3D` / … | 基础形状 |
|
||||||
|
|
||||||
|
用凸包时,"碰撞比模型胖"是必然的,不是精度不足:凸包**无法表达凹形**,增加网格密度也不会改善。
|
||||||
|
|
||||||
|
### 升级提示
|
||||||
|
|
||||||
|
`collision_shape` 的默认值在 2.0 内定稿时从 `Convex Hull` 改成了 `Trimesh`。如果你之前跑过更早的版本,启动时插件会检测到"这个值仍是旧默认值"并自动迁移,同时打印一条日志。**已经生成的碰撞体不会跟着变** —— 要重新执行一次「移除选中模型的碰撞」→「给选中模型加碰撞」。
|
||||||
|
|
||||||
### 模型自带碰撞时
|
### 模型自带碰撞时
|
||||||
|
|
||||||
@@ -120,7 +144,7 @@ MeshColliderLib.remove_collision(model_instance) # bool
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 两个必须知道的限制
|
## 三个必须知道的限制
|
||||||
|
|
||||||
**1. 碰撞体不会跟着动画变形。**
|
**1. 碰撞体不会跟着动画变形。**
|
||||||
碰撞是按模型**静止姿态的几何**烘焙的,不是按骨骼运行时变形后的形状。这是静态烘焙的固有性质,游戏里通常也是用胶囊/凸包代替。
|
碰撞是按模型**静止姿态的几何**烘焙的,不是按骨骼运行时变形后的形状。这是静态烘焙的固有性质,游戏里通常也是用胶囊/凸包代替。
|
||||||
@@ -132,12 +156,18 @@ MeshColliderLib.remove_collision(model_instance) # bool
|
|||||||
|
|
||||||
> 这一条正是 1.0 本地化方案想解决的问题,但它的代价(1–3× 存储 + 静默过期)不值得。原生 `-col` 方案没有这个问题,因为它把碰撞生成放在导入流程里 —— 这也是我建议优先考虑它的原因。
|
> 这一条正是 1.0 本地化方案想解决的问题,但它的代价(1–3× 存储 + 静默过期)不值得。原生 `-col` 方案没有这个问题,因为它把碰撞生成放在导入流程里 —— 这也是我建议优先考虑它的原因。
|
||||||
|
|
||||||
|
**3. 模型的世界尺寸太小,物理会不准。**
|
||||||
|
Godot 的物理按"1 单位 = 1 米"调参。如果模型导入后只有几厘米大(某些 FBX 会这样),碰撞虽然几何正确,但**碰撞检测会变得不可靠** —— 表现为穿透、抖动、站在物体上却下滑。
|
||||||
|
|
||||||
|
这不是碰撞体的问题,改形状类型也没用。正确做法是**把模型调制到真实比例**:在导入设置里调 `Scale`,或给模型的父节点设 `scale`,让它变成真实尺寸(一把椅子约 1 米、一个角色约 1.7 米)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 已知取舍
|
## 已知取舍
|
||||||
|
|
||||||
- **检测靠轮询**(0.4 秒一次)。Godot 没有暴露"模型被拖进来"的信号,这是唯一可靠的做法。
|
- **检测靠轮询**(0.4 秒一次)。Godot 没有暴露"模型被拖进来"的信号,这是唯一可靠的做法。
|
||||||
- **已验证的 API 兼容性**:本插件只使用在 Godot 4.7.2 上实测过的 API。踩过的坑包括 `MeshInstance3D.skeleton` 是 `NodePath` 而不是 `Skeleton3D`、`ClassDB.can_instantiate()` 才是判断节点能否容纳子节点的正确方法、以及 GDScript 不允许 `const` 字典的值引用其他常量。
|
- **变换在注入前一次性快照**。所有网格的世界变换在**模型确实处于场景树中**时解析好,再交给构建函数。这有两个原因:刚体是**故意游离**的(由撤销系统负责挂载),而 `global_transform` 在树外会打印 `Condition "!is_inside_tree()" is true` 并返回单位阵;更重要的是,依赖"读取瞬间节点仍在树里"会让碰撞在边界情况下静默烘焙到原点。快照把这件事变成确定的,缺了变换的网格会被跳过而不是猜一个。
|
||||||
|
- **已验证的 API 兼容性**:本插件只使用在 Godot 4.7.2 上实测过的 API。踩过的坑包括 `MeshInstance3D.skeleton` 是 `NodePath` 而不是 `Skeleton3D`、`ClassDB.can_instantiate()` 才是判断节点能否容纳子节点的正确方法、以及 GDScript 不允许 `const` 字典的值引用其他常量(所以库里的枚举默认值写成字面量索引)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,32 @@ const BODY_TYPE_NAMES: PackedStringArray = [
|
|||||||
"static", "rigid", "area", "character",
|
"static", "rigid", "area", "character",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
## Resolve a requested shape/body pair into one that actually works.
|
||||||
|
##
|
||||||
|
## A `ConcavePolygonShape3D` (Trimesh) is a static-only shape: Godot will not
|
||||||
|
## drive a moving RigidBody3D / CharacterBody3D from one, and the failure is
|
||||||
|
## silent. Rather than let that through, fall back to a convex hull and say so.
|
||||||
|
## Returns `{ shape, body, changed, reason }`.
|
||||||
|
static func resolve_shape_and_body(requested_shape: ShapeType,
|
||||||
|
requested_body: BodyType) -> Dictionary:
|
||||||
|
var out := {
|
||||||
|
"shape": requested_shape,
|
||||||
|
"body": requested_body,
|
||||||
|
"changed": false,
|
||||||
|
"reason": "",
|
||||||
|
}
|
||||||
|
if requested_shape != ShapeType.TRIMESH:
|
||||||
|
return out
|
||||||
|
if requested_body == BodyType.STATIC or requested_body == BodyType.AREA:
|
||||||
|
return out
|
||||||
|
out["shape"] = ShapeType.CONVEX_HULL
|
||||||
|
out["changed"] = true
|
||||||
|
out["reason"] = ("Trimesh collision is static-only, so %s cannot use it; "
|
||||||
|
+ "fell back to Convex Hull. Pick a convex shape type for moving bodies, "
|
||||||
|
+ "or use StaticBody3D.") % BODY_TYPE_NAMES[requested_body]
|
||||||
|
return out
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Path helpers
|
# Path helpers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -106,6 +132,32 @@ static func is_model_instance(node: Node) -> bool:
|
|||||||
# Collision generation
|
# Collision generation
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
## The transform that maps a mesh's vertices into the space of `body_global`.
|
||||||
|
##
|
||||||
|
## Resolved from `body_global` (a plain Transform3D) rather than by reading
|
||||||
|
## `body.global_transform` off the body node, because the body is deliberately
|
||||||
|
## detached while its shapes are baked - and reading `global_transform` on a node
|
||||||
|
## outside the tree makes Godot log `Condition "!is_inside_tree()" is true` and
|
||||||
|
## return an identity transform.
|
||||||
|
static func mesh_space_in(body_global: Transform3D, mi: MeshInstance3D) -> Transform3D:
|
||||||
|
return body_global.affine_inverse() * mi.global_transform
|
||||||
|
|
||||||
|
|
||||||
|
## Build the `mesh_spaces` map consumed by `build_collision_body`.
|
||||||
|
##
|
||||||
|
## MUST be called while `model` is inside the scene tree: global transforms do
|
||||||
|
## not resolve otherwise, and every shape would silently end up baked at the
|
||||||
|
## origin. Keys are mesh instance ids.
|
||||||
|
static func collect_mesh_spaces(model: Node, body_global: Transform3D,
|
||||||
|
exclude_existing_bodies: bool = false) -> Dictionary:
|
||||||
|
var meshes: Array[MeshInstance3D] = []
|
||||||
|
_collect_meshes(model, meshes, exclude_existing_bodies)
|
||||||
|
var spaces: Dictionary = {}
|
||||||
|
for mi in meshes:
|
||||||
|
spaces[mi.get_instance_id()] = mesh_space_in(body_global, mi)
|
||||||
|
return spaces
|
||||||
|
|
||||||
|
|
||||||
## Build a physics body with baked collision shapes for `model`, and return it
|
## Build a physics body with baked collision shapes for `model`, and return it
|
||||||
## DETACHED, or `null` if there was nothing to add.
|
## DETACHED, or `null` if there was nothing to add.
|
||||||
##
|
##
|
||||||
@@ -113,10 +165,14 @@ static func is_model_instance(node: Node) -> bool:
|
|||||||
## tree (the editor plugin does so through the undo system, so an injection can
|
## tree (the editor plugin does so through the undo system, so an injection can
|
||||||
## be reverted with Ctrl+Z). Intended to be added as a child of `model`.
|
## be reverted with Ctrl+Z). Intended to be added as a child of `model`.
|
||||||
##
|
##
|
||||||
## `model` must be inside the scene tree: global transforms do not resolve on a
|
## `body_global` is the transform the body will end up with once parented - for
|
||||||
## detached node, which would silently bake every shape at the origin. The body
|
## the usual "body as a child of the model instance" case that is simply
|
||||||
## itself is deliberately never parented here, because its own transform stays
|
## `model.global_transform`. Shapes are baked relative to it, so the body's own
|
||||||
## at identity and the shapes are already baked into that space.
|
## transform stays at identity.
|
||||||
|
##
|
||||||
|
## `mesh_spaces` must come from `collect_mesh_spaces`, gathered while the model
|
||||||
|
## was still in the tree. A mesh missing from it is skipped rather than guessed
|
||||||
|
## at, because a wrong transform is worse than a missing shape.
|
||||||
##
|
##
|
||||||
## `exclude_existing_bodies` skips meshes that already sit under a physics body.
|
## `exclude_existing_bodies` skips meshes that already sit under a physics body.
|
||||||
## Turn it on for models that ship their own collision (Godot's `-col` suffixes).
|
## Turn it on for models that ship their own collision (Godot's `-col` suffixes).
|
||||||
@@ -126,8 +182,13 @@ static func is_model_instance(node: Node) -> bool:
|
|||||||
## constant expressions, and a qualified enum member is not reliably accepted -
|
## constant expressions, and a qualified enum member is not reliably accepted -
|
||||||
## the same restriction that rules out `const` dictionaries referencing other
|
## the same restriction that rules out `const` dictionaries referencing other
|
||||||
## constants. Keep these literals in step with the enum order above.
|
## constants. Keep these literals in step with the enum order above.
|
||||||
|
##
|
||||||
|
## `shape_type` is subject to `resolve_shape_and_body` and may be corrected (with
|
||||||
|
## a warning) if the requested combination cannot work.
|
||||||
static func build_collision_body(
|
static func build_collision_body(
|
||||||
model: Node,
|
model: Node,
|
||||||
|
mesh_spaces: Dictionary,
|
||||||
|
body_global: Transform3D = Transform3D.IDENTITY,
|
||||||
shape_type: ShapeType = 0, # ShapeType.CONVEX_HULL
|
shape_type: ShapeType = 0, # ShapeType.CONVEX_HULL
|
||||||
body_type: BodyType = 0, # BodyType.STATIC
|
body_type: BodyType = 0, # BodyType.STATIC
|
||||||
exclude_existing_bodies: bool = false,
|
exclude_existing_bodies: bool = false,
|
||||||
@@ -139,10 +200,15 @@ static func build_collision_body(
|
|||||||
return null
|
return null
|
||||||
if _find_added_body(model) != null:
|
if _find_added_body(model) != null:
|
||||||
return null # already has collision from this library
|
return null # already has collision from this library
|
||||||
if not model.is_inside_tree():
|
|
||||||
push_warning("[MeshCollider] %s is not inside the scene tree; cannot bake "
|
# Enforce the static-only rule for Trimesh here, at the single choke point, so
|
||||||
% model.name + "collision without resolved global transforms.")
|
# neither the plugin nor a script caller can accidentally ask for a concave
|
||||||
return null
|
# shape on a moving body.
|
||||||
|
var resolved := resolve_shape_and_body(shape_type, body_type)
|
||||||
|
if resolved["changed"]:
|
||||||
|
push_warning("[MeshCollider] " + String(resolved["reason"]))
|
||||||
|
shape_type = resolved["shape"]
|
||||||
|
body_type = resolved["body"]
|
||||||
|
|
||||||
var meshes: Array[MeshInstance3D] = []
|
var meshes: Array[MeshInstance3D] = []
|
||||||
_collect_meshes(model, meshes, exclude_existing_bodies)
|
_collect_meshes(model, meshes, exclude_existing_bodies)
|
||||||
@@ -162,7 +228,11 @@ static func build_collision_body(
|
|||||||
|
|
||||||
var added := 0
|
var added := 0
|
||||||
for mi in meshes:
|
for mi in meshes:
|
||||||
var shape := build_shape_for_mesh(mi, body as Node3D, shape_type)
|
var key := mi.get_instance_id()
|
||||||
|
if not mesh_spaces.has(key):
|
||||||
|
continue # no resolved transform for this mesh; don't guess
|
||||||
|
var to_local: Transform3D = mesh_spaces[key]
|
||||||
|
var shape := build_shape_for_mesh(mi, to_local, shape_type)
|
||||||
if shape == null:
|
if shape == null:
|
||||||
continue
|
continue
|
||||||
var cs := CollisionShape3D.new()
|
var cs := CollisionShape3D.new()
|
||||||
@@ -211,8 +281,20 @@ static func add_collision(
|
|||||||
exclude_existing_bodies: bool = false,
|
exclude_existing_bodies: bool = false,
|
||||||
collision_layer: int = 1,
|
collision_layer: int = 1,
|
||||||
collision_mask: int = 1) -> Node:
|
collision_mask: int = 1) -> Node:
|
||||||
var body := build_collision_body(model, shape_type, body_type,
|
if model == null or not is_instance_valid(model):
|
||||||
exclude_existing_bodies, collision_layer, collision_mask)
|
return null
|
||||||
|
if not (model is Node3D):
|
||||||
|
return null
|
||||||
|
if not model.is_inside_tree():
|
||||||
|
push_warning("[MeshCollider] %s is not inside the scene tree; collision "
|
||||||
|
% model.name + "cannot be baked without resolved global transforms.")
|
||||||
|
return null
|
||||||
|
|
||||||
|
# Resolve every mesh transform up front, while tree membership is certain.
|
||||||
|
var model_xform: Transform3D = (model as Node3D).global_transform
|
||||||
|
var spaces := collect_mesh_spaces(model, model_xform, exclude_existing_bodies)
|
||||||
|
var body := build_collision_body(model, spaces, model_xform, shape_type,
|
||||||
|
body_type, exclude_existing_bodies, collision_layer, collision_mask)
|
||||||
if body == null:
|
if body == null:
|
||||||
return null
|
return null
|
||||||
model.add_child(body, true)
|
model.add_child(body, true)
|
||||||
@@ -325,24 +407,24 @@ static func _unique_child_name(parent: Node, wanted: String) -> String:
|
|||||||
# Shape construction
|
# Shape construction
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
## Build one shape resource for `mi`, baked into the coordinate space of
|
## Build one shape resource for `mi`, baked into the space described by
|
||||||
## `body`.
|
## `to_local`.
|
||||||
|
##
|
||||||
|
## `to_local` comes from `mesh_space_in`, resolved while the model was known to
|
||||||
|
## be inside the scene tree. Passing it in - rather than reading
|
||||||
|
## `mi.global_transform` here - is what keeps this function from depending on
|
||||||
|
## tree membership at bake time.
|
||||||
##
|
##
|
||||||
## Returns null when no shape can be produced. The `shape_type` default is a
|
## Returns null when no shape can be produced. The `shape_type` default is a
|
||||||
## literal enum index for the reason given on `build_collision_body`.
|
## literal enum index for the reason given on `build_collision_body`.
|
||||||
static func build_shape_for_mesh(mi: MeshInstance3D, body: Node3D,
|
static func build_shape_for_mesh(mi: MeshInstance3D, to_local: Transform3D,
|
||||||
shape_type: ShapeType = 0) -> Shape3D: # ShapeType.CONVEX_HULL
|
shape_type: ShapeType = 0) -> Shape3D: # ShapeType.CONVEX_HULL
|
||||||
if mi == null or body == null:
|
if mi == null:
|
||||||
return null
|
return null
|
||||||
var mesh := mi.mesh
|
var mesh := mi.mesh
|
||||||
if mesh == null or mesh.get_surface_count() == 0:
|
if mesh == null or mesh.get_surface_count() == 0:
|
||||||
return null
|
return null
|
||||||
|
|
||||||
# Bake into the body's space. `global_transform` is the anchor even for a
|
|
||||||
# skinned mesh: the mesh's own node transform carries the armature scale and
|
|
||||||
# offset, which is where the geometry is actually drawn.
|
|
||||||
var to_local := body.global_transform.affine_inverse() * mi.global_transform
|
|
||||||
|
|
||||||
match shape_type:
|
match shape_type:
|
||||||
ShapeType.TRIMESH:
|
ShapeType.TRIMESH:
|
||||||
return _trimesh_shape(mi, to_local)
|
return _trimesh_shape(mi, to_local)
|
||||||
|
|||||||
@@ -119,16 +119,31 @@ func _inject(model: Node, source: String = "") -> void:
|
|||||||
if source.is_empty():
|
if source.is_empty():
|
||||||
source = String(model.scene_file_path)
|
source = String(model.scene_file_path)
|
||||||
|
|
||||||
|
# Everything below reads `global_transform`, which only resolves inside the
|
||||||
|
# tree. Without this check Godot logs
|
||||||
|
# `Condition "!is_inside_tree()" is true. Returning: Transform3D()` and the
|
||||||
|
# shapes would be baked at the origin.
|
||||||
|
if not model.is_inside_tree():
|
||||||
|
push_warning("[MeshCollider] %s is not in the scene tree; skipped."
|
||||||
|
% model.name)
|
||||||
|
return
|
||||||
|
|
||||||
var skip_existing := Settings.get_bool(Settings.KEY_SKIP_EXISTING)
|
var skip_existing := Settings.get_bool(Settings.KEY_SKIP_EXISTING)
|
||||||
if skip_existing and Collider.has_any_collision(model):
|
if skip_existing and Collider.has_any_collision(model):
|
||||||
print("[MeshCollider] %s already has collision; skipped." % model.name)
|
print("[MeshCollider] %s already has collision; skipped." % model.name)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Built detached on purpose: the editor's undo system owns every tree change
|
# Resolve the mesh transforms first, while tree membership is certain, then
|
||||||
# in this plugin, so inserting the body here would mean undoing a change we
|
# build the body from that snapshot. The body itself is built detached on
|
||||||
# had already applied behind undo's back.
|
# purpose: the editor's undo system must own every tree change in this
|
||||||
|
# plugin, so inserting it here would mean undoing a change that had already
|
||||||
|
# been applied behind undo's back.
|
||||||
|
var model_xform: Transform3D = (model as Node3D).global_transform
|
||||||
|
var spaces := Collider.collect_mesh_spaces(model, model_xform, skip_existing)
|
||||||
var body := Collider.build_collision_body(
|
var body := Collider.build_collision_body(
|
||||||
model,
|
model,
|
||||||
|
spaces,
|
||||||
|
model_xform,
|
||||||
Settings.shape_type(),
|
Settings.shape_type(),
|
||||||
Settings.body_type(),
|
Settings.body_type(),
|
||||||
skip_existing,
|
skip_existing,
|
||||||
|
|||||||
@@ -18,6 +18,15 @@ const KEY_MASK := ROOT + "collision_mask"
|
|||||||
const KEY_EXTENSIONS := ROOT + "model_extensions"
|
const KEY_EXTENSIONS := ROOT + "model_extensions"
|
||||||
const KEY_SKIP_DIRS := ROOT + "skip_dirs"
|
const KEY_SKIP_DIRS := ROOT + "skip_dirs"
|
||||||
|
|
||||||
|
## The shape this plugin shipped as its default before 2.0 finalised.
|
||||||
|
##
|
||||||
|
## `install()` only writes a default when the setting does not exist yet, so a
|
||||||
|
## project that already ran an earlier build keeps the old value forever - which
|
||||||
|
## is exactly what happened when this default moved from Convex Hull to Trimesh.
|
||||||
|
## A stored value still equal to this legacy default is treated as "untouched"
|
||||||
|
## and migrated.
|
||||||
|
const LEGACY_SHAPE_DEFAULT := 0 # MeshCollider.ShapeType.CONVEX_HULL
|
||||||
|
|
||||||
## NOTE: a function rather than a `const` dictionary on purpose. GDScript
|
## NOTE: a function rather than a `const` dictionary on purpose. GDScript
|
||||||
## requires a const dictionary's *values* to be constant expressions, so a
|
## requires a const dictionary's *values* to be constant expressions, so a
|
||||||
## PackedStringArray cannot be built inside one ("isn't a constant expression").
|
## PackedStringArray cannot be built inside one ("isn't a constant expression").
|
||||||
@@ -36,7 +45,7 @@ static func project_info() -> Dictionary:
|
|||||||
KEY_SHAPE: {
|
KEY_SHAPE: {
|
||||||
"name": KEY_SHAPE,
|
"name": KEY_SHAPE,
|
||||||
"type": TYPE_INT,
|
"type": TYPE_INT,
|
||||||
"default": 0, # MeshCollider.ShapeType.CONVEX_HULL
|
"default": 1, # MeshCollider.ShapeType.TRIMESH
|
||||||
"hint": PROPERTY_HINT_ENUM,
|
"hint": PROPERTY_HINT_ENUM,
|
||||||
"hint_string": "Convex Hull,Trimesh,Box,Sphere,Capsule,Cylinder",
|
"hint_string": "Convex Hull,Trimesh,Box,Sphere,Capsule,Cylinder",
|
||||||
},
|
},
|
||||||
@@ -106,6 +115,9 @@ static func _info(key: String) -> Dictionary:
|
|||||||
|
|
||||||
## Register every setting with ProjectSettings, so the values appear in the UI
|
## Register every setting with ProjectSettings, so the values appear in the UI
|
||||||
## and carry a usable default even before the user touches anything.
|
## and carry a usable default even before the user touches anything.
|
||||||
|
##
|
||||||
|
## Existing values are never overwritten, with one narrow exception: the shape
|
||||||
|
## default this plugin changed after shipping, migrated below.
|
||||||
static func install() -> void:
|
static func install() -> void:
|
||||||
var all := _all_info()
|
var all := _all_info()
|
||||||
for key in all:
|
for key in all:
|
||||||
@@ -117,6 +129,26 @@ static func install() -> void:
|
|||||||
# reported as a modified project setting.
|
# reported as a modified project setting.
|
||||||
ProjectSettings.set_initial_value(key, info["default"])
|
ProjectSettings.set_initial_value(key, info["default"])
|
||||||
|
|
||||||
|
# `install()` only writes a default when the setting is absent, so a project
|
||||||
|
# that already ran an earlier build would keep the old default forever. Only
|
||||||
|
# a value still equal to that old default is treated as untouched and
|
||||||
|
# migrated; a genuinely chosen value is left alone.
|
||||||
|
var stored := int(ProjectSettings.get_setting(KEY_SHAPE,
|
||||||
|
all[KEY_SHAPE]["default"]))
|
||||||
|
var wanted := int(all[KEY_SHAPE]["default"])
|
||||||
|
if stored != LEGACY_SHAPE_DEFAULT or wanted == LEGACY_SHAPE_DEFAULT:
|
||||||
|
return
|
||||||
|
ProjectSettings.set_setting(KEY_SHAPE, wanted)
|
||||||
|
print(("[MeshCollider] collision_shape was still the old default "
|
||||||
|
+ "(Convex Hull) - updated to '%s'. Set it back in Project Settings "
|
||||||
|
+ "> mesh_collider if that was intentional.")
|
||||||
|
% MeshCollider.SHAPE_TYPE_NAMES[wanted])
|
||||||
|
# Only the migration is persisted. Writing on every install would spill all of
|
||||||
|
# the plugin's defaults into project.godot for no benefit.
|
||||||
|
var err := ProjectSettings.save()
|
||||||
|
if err != OK:
|
||||||
|
push_warning("[MeshCollider] could not save project settings (err %d)" % err)
|
||||||
|
|
||||||
|
|
||||||
static func get_bool(key: String) -> bool:
|
static func get_bool(key: String) -> bool:
|
||||||
return bool(ProjectSettings.get_setting(key, _info(key)["default"]))
|
return bool(ProjectSettings.get_setting(key, _info(key)["default"]))
|
||||||
|
|||||||
Reference in New Issue
Block a user