Godot 4 Source Code Analysis- Collision

The collision function should be a core function, which can automatically generate corresponding data, such as the target object entering and leaving the detection area of ​​the object.

Based on the attribute settings, all objects that can collide have such attributes: Layer, Mask.

In Godot 4, the Layer and Mask properties in the Collision property are important parameters for defining collision filtering. They allow control over which objects can be detected for collision with that node.

  1. Layer:

    • Layer is an attribute that all nodes have and is used to group nodes into different layers. Layer is a set of bit masks (bitmask), each bit represents a specific collision layer. Each object can be assigned one or more collision layers. By assigning objects to specific collision layers, it is possible to define the logical group they belong to
    • Each node can belong to one or more layers. You can select one or more layers in the Layer section of the node's properties panel.
    • The Layer property defines the layer the node belongs to. By default, nodes belong to the Base Layer.
    • Grouping nodes in a scene with different layers allows you to only perform collision detection with nodes on specific layers.
  2. Mask:

    • Mask is also an attribute that all nodes have, which is used to specify the node's interest in collision. Mask is also a set of bitmasks used to indicate which collision layer objects an object can collide with. Each object can be assigned a collision mask. By setting the collision mask, you can define which objects in the collision layer collide with the object
    • Each node has a mask value. The mask can be set under the Collision property in the node's properties panel.
    • The Mask property defines which layers' nodes this node is interested in, and the node will do collision detection with other nodes in these layers.
    • The mask value of each node is a 32-bit integer, and each bit represents a layer. 0 means not interested in this layer, 1 means interested in this layer. Mask values ​​can be set and checked using bitwise operations such as bitwise AND and bitwise OR.

By using the Layer and Mask properties, you can flexibly control collision detection so that only nodes in specific layers interact with each other. For example, you can set a node to only collide with nodes belonging to a certain layer, and ignore nodes of other layers.

It should be noted that in order for two nodes to perform collision detection, their Layer and Mask need to meet certain conditions at the same time. Specifically, the Layer value of one node must be included in the Mask value of another node, and the Layer value of another node must be included in the Mask value of this node.

By properly setting the Layer and Mask properties, a sophisticated and flexible collision filtering system can be created in Godot 4 to achieve various complex physical effects and game mechanisms.

Suppose there are two collision layers, one is "Player" and the other is "Enemy". I have a player character and some enemies, and want to make sure that collisions happen between the player and the enemies, but not between the enemies.

  • For the player object: assign it to the "Player" collision layer and set its Collision Mask to the bitmask of "Enemy". This will make the player only collide with objects in the "Enemy" layer.

  • For enemy objects: Assign them to the "Enemy" collision layer and set their Collision Mask to the "Player" bitmask. This will make enemies only collide with objects in the "Player" layer.

For enemy objects, assign them to the same collision layer and set the corresponding collision mask to ensure that they will not collide with each other.

In this way, the interaction of collisions can be controlled more finely in complex scenes, making collision logic clearer and more manageable.

From the perspective of understanding, the logic is complicated to say, but the technical implementation is very simple

Both Layer and Mask should be 32-bit integers. Object A has Layer and Mask attributes, and object B also has Layer and Mask attributes.

If A.Layer & B.Mask are non-zero, A is about to collide with B

If A.Mask & B.Layer are non-zero, then B is about to collide with A

It's still a bit confusing, think about it again, the following are my own thoughts, not necessarily correct:

In fact, both Layer and Mask are logical concepts, virtual. In a real scene, both A and B occupy corresponding spatial positions (3D) or plane positions (2D). During operation, at least one of A and B is active, and their relative positions may change. There are overlaps. At this time, the godot engine knows because it can calculate in real time. So, godot finds that two objects overlap, what should I do?

Then check the Layer and Mask of A and B.

  • If A.Layer & B.Mask are non-zero, then B can detect A, trigger B's body_entered signal, and the actual parameter is A.
  • If B.Layer & A.Mask is non-zero, then A can detect B, trigger A's body_entered signal, and the actual parameter is B.

In fact, the above statement is not rigorous, because the body_entered signal should not be sent continuously, but has an entry and exit state. Together it should be:

  • A and B maintain their own body_map respectively, and know the list of objects that overlap with themselves. This work cannot be handed over to the godot engine, it is so busy, it is better to take care of your own affairs
  • From the source code, it can be seen that A and B complete the detection of the state flags of entry and exit. That is to say, traverse all the monitored objects monitored_bodies and check their own?? state E->value. If it is 0, there is nothing wrong. The object is no longer monitored. If >0, it is AREA_BODY_ADDED, which means entering. If <0, it is AREA_BODY_REMOVED, which means leave.
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();
		}
	}
}
  • Determine the body_in flag based on whether it is AREA_BODY_ADDED. If it is body_in, trigger tree_entered, tree_exiting signals, if the object is in the working scene, trigger body_entered, the parameter is node. By the way, the body_shape_entered signal is also triggered, let's use it.
  • If body_in is false, trigger tree_entered, tree_exiting. If the object is in the working scene, trigger 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();
}

At this point, it is time to pay attention to monitored_bodies, which is maintained in add_body_to_query and remove_body_from_query


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();
	}
}

But this does not see the role of the Layer and Mask properties. Then check back. Finally found in the GodotCollisionObject2D class:

_FORCE_INLINE_ bool collides_with(GodotCollisionObject2D *p_other) const {
    return p_other->collision_layer & collision_mask;
}

Sure enough, it was consistent with the guess. The specific call to collides_with is in some setup functions.

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;
}

There's no need to go any further. As for when these setup functions are called, you can use an actual project to debug and see.

The main core idea is to understand the Layer and Mask configuration problem of collision.

Guess you like

Origin blog.csdn.net/drgraph/article/details/132100947