衝突機能は、対象物体の検出領域への出入りなど、対応するデータを自動的に生成できるコア機能である必要があります。
属性設定に基づいて、衝突できるすべてのオブジェクトには、レイヤー、マスクなどの属性が与えられます。
Godot 4 では、Collision プロパティの Layer プロパティと Mask プロパティは、衝突フィルタリングを定義するための重要なパラメータです。これらにより、そのノードとの衝突を検出できるオブジェクトを制御できます。
-
層:
- レイヤはすべてのノードが持つ属性で、ノードを異なるレイヤにグループ化するために使用されます。レイヤはビット マスク (ビットマスク) のセットであり、各ビットは特定の衝突レイヤを表します。各オブジェクトには 1 つ以上の衝突レイヤーを割り当てることができます。オブジェクトを特定のコリジョン レイヤーに割り当てることで、オブジェクトが属する論理グループを定義できます。
- 各ノードは 1 つ以上のレイヤーに属することができます。ノードのプロパティ パネルの [レイヤー] セクションで 1 つ以上のレイヤーを選択できます。
- Layer プロパティは、ノードが属するレイヤーを定義します。デフォルトでは、ノードはベース レイヤに属します。
- シーン内のノードを異なるレイヤーでグループ化すると、特定のレイヤー上のノードとのみ衝突検出を実行できるようになります。
-
マスク:
- マスクはすべてのノードが持つ属性でもあり、衝突に対するノードの関心を指定するために使用されます。マスクは、オブジェクトがどの衝突レイヤー オブジェクトと衝突できるかを示すために使用されるビットマスクのセットでもあります。各オブジェクトには衝突マスクを割り当てることができます。衝突マスクを設定することで、衝突レイヤー内のどのオブジェクトがオブジェクトと衝突するかを定義できます。
- 各ノードにはマスク値があります。マスクは、ノードのプロパティ パネルの Collision プロパティで設定できます。
- Mask プロパティは、このノードがどのレイヤーのノードに関係するかを定義し、ノードはこれらのレイヤー内の他のノードとの衝突検出を行います。
- 各ノードのマスク値は 32 ビットの整数で、各ビットがレイヤーを表します。0 はこのレイヤーに興味がないことを意味し、1 はこのレイヤーに興味があることを意味します。マスク値は、ビットごとの AND やビットごとの OR などのビットごとの演算を使用して設定および確認できます。
Layer および Mask プロパティを使用すると、特定のレイヤー内のノードのみが相互作用するように衝突検出を柔軟に制御できます。たとえば、特定のレイヤーに属するノードとのみ衝突し、他のレイヤーのノードを無視するようにノードを設定できます。
2 つのノードが衝突検出を実行するには、それらのレイヤーとマスクが同時に特定の条件を満たす必要があることに注意してください。具体的には、あるノードの Layer 値が別のノードの Mask 値に含まれ、別のノードの Layer 値がこのノードの Mask 値に含まれる必要があります。
Layer および Mask プロパティを適切に設定することで、Godot 4 で洗練された柔軟な衝突フィルタリング システムを作成し、さまざまな複雑な物理的効果やゲーム メカニズムを実現できます。
2 つの衝突レイヤーがあり、1 つは「プレイヤー」、もう 1 つは「敵」であるとします。プレイヤー キャラクターといくつかの敵があり、プレイヤーと敵の間で衝突が発生するが、敵の間では衝突が発生しないようにしたいと考えています。
-
プレイヤー オブジェクトの場合: 「プレイヤー」コリジョン レイヤーに割り当て、そのコリジョン マスクを「敵」のビットマスクに設定します。これにより、プレイヤーは「Enemy」レイヤー内のオブジェクトのみと衝突するようになります。
-
敵オブジェクトの場合: オブジェクトを「Enemy」コリジョン レイヤーに割り当て、コリジョン マスクを「Player」ビットマスクに設定します。これにより、敵は「プレイヤー」レイヤー内のオブジェクトとのみ衝突します。
敵オブジェクトの場合は、それらを同じ衝突レイヤーに割り当て、対応する衝突マスクを設定して、それらが互いに衝突しないようにします。
このようにして、複雑なシーンで衝突の相互作用をより細かく制御できるようになり、衝突ロジックがより明確になり、管理しやすくなります。
理解するという観点から見ると、ロジックは複雑ですが、技術的な実装は非常に簡単です
Layer と Mask は両方とも 32 ビット整数である必要があります。オブジェクト A には Layer 属性と Mask 属性があり、オブジェクト B にも Layer 属性と Mask 属性があります。
A.Layer と B.Mask がゼロ以外の場合、A は B と衝突しようとしています。
A.Mask と B.Layer がゼロ以外の場合、B は A と衝突しようとしています。
まだ少し混乱していますが、もう一度考えてみてください。以下は私自身の考えであり、必ずしも正しいとは限りません。
実際、レイヤーとマスクはどちらも論理的な概念であり、仮想的です。実際のシーンでは、A と B は両方とも、対応する空間位置 (3D) または平面位置 (2D) を占めます。動作中、A と B の少なくとも 1 つがアクティブになり、それらの相対位置が変化する可能性があります。オーバーラップがあります。この時点で、Godot エンジンはリアルタイムで計算できるため、それを認識します。それで、ゴドーは 2 つのオブジェクトが重なっていることに気づきました。どうすればよいでしょうか?
次にAとBのレイヤーとマスクを確認します。
- A.Layer と B.Mask がゼロ以外の場合、B は A を検出し、B の body_entered 信号をトリガーでき、実際のパラメーターは A です。
- B.Layer と A.Mask がゼロ以外の場合、A は B を検出し、A の body_entered 信号をトリガーでき、実際のパラメーターは B になります。
実際、body_entered 信号は継続的に送信されるべきではなく、開始状態と終了状態があるため、上記のステートメントは厳密ではありません。組み合わせると次のようになります。
- A と B はそれぞれ独自の body_map を維持し、自分たちと重複するオブジェクトのリストを知っています。この作業は Godot エンジンに引き渡すことはできません。とても忙しいので、自分のことは自分でやった方が良いです。
- ソース コードから、A と B が開始と終了の状態フラグの検出を完了していることがわかります。つまり、すべての監視対象オブジェクトのmonitored_bodies を走査し、自身の状態 E->value を確認します。 0、何も問題ありません。オブジェクトは監視されなくなりました。>0 の場合、AREA_BODY_ADDED となり、入ることを意味します。<0 の場合は AREA_BODY_REMOVED、つまり離れることを意味します。
void GodotArea2D::call_queries() {
if (!monitor_callback.is_null() && !monitored_bodies.is_empty()) {
if (monitor_callback.is_valid()) {
Variant res[5];
Variant *resptr[5];
for (int i = 0; i < 5; i++) {
resptr[i] = &res[i];
}
for (HashMap<BodyKey, BodyState, BodyKey>::Iterator E = monitored_bodies.begin(); E;) {
if (E->value.state == 0) { // Nothing happened
HashMap<BodyKey, BodyState, BodyKey>::Iterator next = E;
++next;
monitored_bodies.remove(E);
E = next;
continue;
}
res[0] = E->value.state > 0 ? PhysicsServer2D::AREA_BODY_ADDED : PhysicsServer2D::AREA_BODY_REMOVED;
res[1] = E->key.rid;
res[2] = E->key.instance_id;
res[3] = E->key.body_shape;
res[4] = E->key.area_shape;
HashMap<BodyKey, BodyState, BodyKey>::Iterator next = E;
++next;
monitored_bodies.remove(E);
E = next;
Callable::CallError ce;
Variant ret;
monitor_callback.callp((const Variant **)resptr, 5, ret, ce);
if (ce.error != Callable::CallError::CALL_OK) {
ERR_PRINT_ONCE("Error calling event callback method " + Variant::get_callable_error_text(monitor_callback, (const Variant **)resptr, 5, ce));
}
}
} else {
monitored_bodies.clear();
monitor_callback = Callable();
}
}
if (!area_monitor_callback.is_null() && !monitored_areas.is_empty()) {
if (area_monitor_callback.is_valid()) {
Variant res[5];
Variant *resptr[5];
for (int i = 0; i < 5; i++) {
resptr[i] = &res[i];
}
for (HashMap<BodyKey, BodyState, BodyKey>::Iterator E = monitored_areas.begin(); E;) {
if (E->value.state == 0) { // Nothing happened
HashMap<BodyKey, BodyState, BodyKey>::Iterator next = E;
++next;
monitored_areas.remove(E);
E = next;
continue;
}
res[0] = E->value.state > 0 ? PhysicsServer2D::AREA_BODY_ADDED : PhysicsServer2D::AREA_BODY_REMOVED;
res[1] = E->key.rid;
res[2] = E->key.instance_id;
res[3] = E->key.body_shape;
res[4] = E->key.area_shape;
HashMap<BodyKey, BodyState, BodyKey>::Iterator next = E;
++next;
monitored_areas.remove(E);
E = next;
Callable::CallError ce;
Variant ret;
area_monitor_callback.callp((const Variant **)resptr, 5, ret, ce);
if (ce.error != Callable::CallError::CALL_OK) {
ERR_PRINT_ONCE("Error calling event callback method " + Variant::get_callable_error_text(area_monitor_callback, (const Variant **)resptr, 5, ce));
}
}
} else {
monitored_areas.clear();
area_monitor_callback = Callable();
}
}
}
- body_in フラグは AREA_BODY_ADDED かどうかに基づいて決定します。body_in の場合、トリガー Tree_entered、tree_exiting シグナル、オブジェクトが作業シーンにある場合、トリガー body_entered、パラメーターはノードです。ちなみに、body_shape_entered シグナルもトリガーされるので、それを使ってみましょう。
- body_in が false の場合、tree_entered、tree_exiting をトリガーします。オブジェクトが作業シーンにある場合、body_exited、body_shape_exited をトリガーします
void Area2D::_body_inout(int p_status, const RID &p_body, ObjectID p_instance, int p_body_shape, int p_area_shape) {
bool body_in = p_status == PhysicsServer2D::AREA_BODY_ADDED;
ObjectID objid = p_instance;
Object *obj = ObjectDB::get_instance(objid);
Node *node = Object::cast_to<Node>(obj);
HashMap<ObjectID, BodyState>::Iterator E = body_map.find(objid);
if (!body_in && !E) {
return; //does not exist because it was likely removed from the tree
}
lock_callback();
locked = true;
if (body_in) {
if (!E) {
E = body_map.insert(objid, BodyState());
E->value.rid = p_body;
E->value.rc = 0;
E->value.in_tree = node && node->is_inside_tree();
if (node) {
node->connect(SceneStringNames::get_singleton()->tree_entered, callable_mp(this, &Area2D::_body_enter_tree).bind(objid));
node->connect(SceneStringNames::get_singleton()->tree_exiting, callable_mp(this, &Area2D::_body_exit_tree).bind(objid));
if (E->value.in_tree) {
emit_signal(SceneStringNames::get_singleton()->body_entered, node);
}
}
}
E->value.rc++;
if (node) {
E->value.shapes.insert(ShapePair(p_body_shape, p_area_shape));
}
if (!node || E->value.in_tree) {
emit_signal(SceneStringNames::get_singleton()->body_shape_entered, p_body, node, p_body_shape, p_area_shape);
}
} else {
E->value.rc--;
if (node) {
E->value.shapes.erase(ShapePair(p_body_shape, p_area_shape));
}
bool in_tree = E->value.in_tree;
if (E->value.rc == 0) {
body_map.remove(E);
if (node) {
node->disconnect(SceneStringNames::get_singleton()->tree_entered, callable_mp(this, &Area2D::_body_enter_tree));
node->disconnect(SceneStringNames::get_singleton()->tree_exiting, callable_mp(this, &Area2D::_body_exit_tree));
if (in_tree) {
emit_signal(SceneStringNames::get_singleton()->body_exited, obj);
}
}
}
if (!node || in_tree) {
emit_signal(SceneStringNames::get_singleton()->body_shape_exited, p_body, obj, p_body_shape, p_area_shape);
}
}
locked = false;
unlock_callback();
}
この時点で、add_body_to_query と Remove_body_from_query で維持されるmonitored_bodies に注意を払う時が来ました。
void GodotArea2D::add_body_to_query(GodotBody2D *p_body, uint32_t p_body_shape, uint32_t p_area_shape) {
BodyKey bk(p_body, p_body_shape, p_area_shape);
monitored_bodies[bk].inc();
if (!monitor_query_list.in_list()) {
_queue_monitor_update();
}
}
void GodotArea2D::remove_body_from_query(GodotBody2D *p_body, uint32_t p_body_shape, uint32_t p_area_shape) {
BodyKey bk(p_body, p_body_shape, p_area_shape);
monitored_bodies[bk].dec();
if (!monitor_query_list.in_list()) {
_queue_monitor_update();
}
}
ただし、これには Layer プロパティと Mask プロパティの役割が含まれていません。その後、もう一度確認してください。最後に GodotCollisionObject2D クラスで見つかりました。
_FORCE_INLINE_ bool collides_with(GodotCollisionObject2D *p_other) const {
return p_other->collision_layer & collision_mask;
}
案の定、それは推測と一致しました。collides_with への特定の呼び出しは、一部のセットアップ関数内にあります。
bool GodotAreaPair2D::setup(real_t p_step) {
bool result = false;
if (area->collides_with(body) && GodotCollisionSolver2D::solve(body->get_shape(body_shape), body->get_transform() * body->get_shape_transform(body_shape), Vector2(), area->get_shape(area_shape), area->get_transform() * area->get_shape_transform(area_shape), Vector2(), nullptr, this)) {
result = true;
}
process_collision = false;
has_space_override = false;
if (result != colliding) {
if ((int)area->get_param(PhysicsServer2D::AREA_PARAM_GRAVITY_OVERRIDE_MODE) != PhysicsServer2D::AREA_SPACE_OVERRIDE_DISABLED) {
has_space_override = true;
} else if ((int)area->get_param(PhysicsServer2D::AREA_PARAM_LINEAR_DAMP_OVERRIDE_MODE) != PhysicsServer2D::AREA_SPACE_OVERRIDE_DISABLED) {
has_space_override = true;
} else if ((int)area->get_param(PhysicsServer2D::AREA_PARAM_ANGULAR_DAMP_OVERRIDE_MODE) != PhysicsServer2D::AREA_SPACE_OVERRIDE_DISABLED) {
has_space_override = true;
}
process_collision = has_space_override;
if (area->has_monitor_callback()) {
process_collision = true;
}
colliding = result;
}
return process_collision;
}
bool GodotBodyPair2D::setup(real_t p_step) {
check_ccd = false;
if (!A->interacts_with(B) || A->has_exception(B->get_self()) || B->has_exception(A->get_self())) {
collided = false;
return false;
}
collide_A = (A->get_mode() > PhysicsServer2D::BODY_MODE_KINEMATIC) && A->collides_with(B);
collide_B = (B->get_mode() > PhysicsServer2D::BODY_MODE_KINEMATIC) && B->collides_with(A);
report_contacts_only = false;
if (!collide_A && !collide_B) {
if ((A->get_max_contacts_reported() > 0) || (B->get_max_contacts_reported() > 0)) {
report_contacts_only = true;
} else {
collided = false;
return false;
}
}
//use local A coordinates to avoid numerical issues on collision detection
offset_B = B->get_transform().get_origin() - A->get_transform().get_origin();
_validate_contacts();
const Vector2 &offset_A = A->get_transform().get_origin();
Transform2D xform_Au = A->get_transform().untranslated();
Transform2D xform_A = xform_Au * A->get_shape_transform(shape_A);
Transform2D xform_Bu = B->get_transform();
xform_Bu.columns[2] -= offset_A;
Transform2D xform_B = xform_Bu * B->get_shape_transform(shape_B);
GodotShape2D *shape_A_ptr = A->get_shape(shape_A);
GodotShape2D *shape_B_ptr = B->get_shape(shape_B);
Vector2 motion_A, motion_B;
if (A->get_continuous_collision_detection_mode() == PhysicsServer2D::CCD_MODE_CAST_SHAPE) {
motion_A = A->get_motion();
}
if (B->get_continuous_collision_detection_mode() == PhysicsServer2D::CCD_MODE_CAST_SHAPE) {
motion_B = B->get_motion();
}
bool prev_collided = collided;
collided = GodotCollisionSolver2D::solve(shape_A_ptr, xform_A, motion_A, shape_B_ptr, xform_B, motion_B, _add_contact, this, &sep_axis);
if (!collided) {
oneway_disabled = false;
if (A->get_continuous_collision_detection_mode() == PhysicsServer2D::CCD_MODE_CAST_RAY && collide_A) {
check_ccd = true;
return true;
}
if (B->get_continuous_collision_detection_mode() == PhysicsServer2D::CCD_MODE_CAST_RAY && collide_B) {
check_ccd = true;
return true;
}
return false;
}
if (oneway_disabled) {
return false;
}
if (!prev_collided) {
if (shape_B_ptr->allows_one_way_collision() && A->is_shape_set_as_one_way_collision(shape_A)) {
Vector2 direction = xform_A.columns[1].normalized();
bool valid = false;
for (int i = 0; i < contact_count; i++) {
Contact &c = contacts[i];
if (c.normal.dot(direction) > -CMP_EPSILON) { // Greater (normal inverted).
continue;
}
valid = true;
break;
}
if (!valid) {
collided = false;
oneway_disabled = true;
return false;
}
}
if (shape_A_ptr->allows_one_way_collision() && B->is_shape_set_as_one_way_collision(shape_B)) {
Vector2 direction = xform_B.columns[1].normalized();
bool valid = false;
for (int i = 0; i < contact_count; i++) {
Contact &c = contacts[i];
if (c.normal.dot(direction) < CMP_EPSILON) { // Less (normal ok).
continue;
}
valid = true;
break;
}
if (!valid) {
collided = false;
oneway_disabled = true;
return false;
}
}
}
return true;
}
これ以上進む必要はありません。これらの設定関数がいつ呼び出されるのかについては、実際のプロジェクトを使用してデバッグして確認できます。
主な中心となるアイデアは、衝突のレイヤーとマスクの構成問題を理解することです。