Análisis del código fuente de Godot 4: colisión

La función de colisión debe ser una función central, que puede generar automáticamente los datos correspondientes, como el objeto objetivo que ingresa y sale del área de detección del objeto.

Según la configuración de atributos, todos los objetos que pueden colisionar tienen tales atributos: Capa, Máscara.

En Godot 4, las propiedades Capa y Máscara en la propiedad Colisión son parámetros importantes para definir el filtrado de colisión. Permiten controlar qué objetos se pueden detectar para colisionar con ese nodo.

  1. Capa:

    • La capa es un atributo que tienen todos los nodos y se utiliza para agrupar nodos en diferentes capas. La capa es un conjunto de máscaras de bits (máscara de bits), cada bit representa una capa de colisión específica. A cada objeto se le puede asignar una o más capas de colisión. Al asignar objetos a capas de colisión específicas, es posible definir el grupo lógico al que pertenecen
    • Cada nodo puede pertenecer a una o más capas. Puede seleccionar una o más capas en la sección Capa del panel de propiedades del nodo.
    • La propiedad Capa define la capa a la que pertenece el nodo. Por defecto, los nodos pertenecen a la capa base.
    • Agrupar nodos en una escena con diferentes capas le permite realizar solo la detección de colisiones con nodos en capas específicas.
  2. Mascarilla:

    • La máscara también es un atributo que tienen todos los nodos, que se utiliza para especificar el interés del nodo en la colisión. La máscara también es un conjunto de máscaras de bits que se utilizan para indicar con qué objetos de capa de colisión puede colisionar un objeto. A cada objeto se le puede asignar una máscara de colisión. Al configurar la máscara de colisión, puede definir qué objetos en la capa de colisión chocan con el objeto
    • Cada nodo tiene un valor de máscara. La máscara se puede configurar en la propiedad Colisión en el panel de propiedades del nodo.
    • La propiedad Máscara define en qué nodos de capas está interesado este nodo, y el nodo realizará la detección de colisiones con otros nodos en estas capas.
    • El valor de la máscara de cada nodo es un número entero de 32 bits y cada bit representa una capa. 0 significa que no está interesado en esta capa, 1 significa que está interesado en esta capa. Los valores de máscara se pueden establecer y comprobar mediante operaciones bit a bit como AND bit a bit y OR bit a bit.

Mediante el uso de las propiedades Capa y Máscara, puede controlar de manera flexible la detección de colisiones para que solo los nodos en capas específicas interactúen entre sí. Por ejemplo, puede configurar un nodo para que solo colisione con los nodos que pertenecen a una determinada capa e ignore los nodos de otras capas.

Cabe señalar que para que dos nodos realicen la detección de colisiones, su Capa y Máscara deben cumplir ciertas condiciones al mismo tiempo. Específicamente, el valor de Capa de un nodo debe incluirse en el valor de Máscara de otro nodo, y el valor de Capa de otro nodo debe incluirse en el valor de Máscara de este nodo.

Al configurar correctamente las propiedades de Capa y Máscara, se puede crear un sistema de filtrado de colisiones sofisticado y flexible en Godot 4 para lograr varios efectos físicos complejos y mecanismos de juego.

Supongamos que hay dos capas de colisión, una es "Jugador" y la otra es "Enemigo". Tengo un personaje de jugador y algunos enemigos, y quiero asegurarme de que se produzcan colisiones entre el jugador y los enemigos, pero no entre los enemigos.

  • Para el objeto del jugador: asígnelo a la capa de colisión "Jugador" y establezca su Máscara de colisión en la máscara de bits de "Enemigo". Esto hará que el jugador solo choque con objetos en la capa "Enemigo".

  • Para objetos enemigos: asígnelos a la capa de colisión "Enemigo" y establezca su Máscara de colisión en la máscara de bits "Jugador". Esto hará que los enemigos solo choquen con objetos en la capa "Jugador".

Para los objetos enemigos, asígnelos a la misma capa de colisión y configure la máscara de colisión correspondiente para asegurarse de que no colisionen entre sí.

De esta forma, la interacción de las colisiones se puede controlar con mayor precisión en escenas complejas, lo que hace que la lógica de colisión sea más clara y manejable.

Desde la perspectiva de la comprensión, la lógica es complicada de decir, pero la implementación técnica es muy simple.

Tanto la capa como la máscara deben ser números enteros de 32 bits. El objeto A tiene atributos de capa y máscara, y el objeto B también tiene atributos de capa y máscara.

Si A.Layer y B.Mask no son cero, A está a punto de chocar con B

Si A.Mask y B.Layer no son cero, entonces B está a punto de chocar con A

Todavía es un poco confuso, piénsalo de nuevo, los siguientes son mis propios pensamientos, no necesariamente correctos:

De hecho, tanto Capa como Máscara son conceptos lógicos, virtuales. En una escena real, tanto A como B ocupan posiciones espaciales (3D) o posiciones planas (2D) correspondientes. Durante el funcionamiento, al menos uno de A y B está activo y sus posiciones relativas pueden cambiar. Hay superposiciones. En este momento, el motor Godot lo sabe porque puede calcular en tiempo real. Entonces, Godot encuentra que dos objetos se superponen, ¿qué debo hacer?

Luego verifique la Capa y la Máscara de A y B.

  • Si A.Layer y B.Mask no son cero, entonces B puede detectar A, activar la señal de entrada del cuerpo de B y el parámetro real es A.
  • Si B.Layer & A.Mask no es cero, entonces A puede detectar B, desencadenar la señal ingresada en el cuerpo de A y el parámetro real es B.

De hecho, la declaración anterior no es rigurosa, porque la señal body_entered no debe enviarse continuamente, sino que tiene un estado de entrada y salida. Juntos debe ser:

  • A y B mantienen su propio body_map respectivamente y conocen la lista de objetos que se superponen consigo mismos. Este trabajo no se puede entregar al motor de Godot, está tan ocupado que es mejor ocuparse de sus propios asuntos.
  • Se puede ver en el código fuente que A y B completan la detección de las banderas de estado de entrada y salida. Es decir, atraviesan todos los objetos monitoreados cuerpos_monitorizados y ven su propio estado E->valor. Si es 0, no hay ningún problema, el objeto ya no se supervisa. Si >0, es AREA_BODY_ADDED, lo que significa entrar. Si <0, es AREA_BODY_REMOVED, lo que significa salir.
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 el indicador body_in en función de si es AREA_BODY_ADDED. Si es body_in, desencadenar tree_entered, tree_exiting señales, si el objeto está en la escena de trabajo, desencadenar body_entered, el parámetro es nodo. Por cierto, la señal body_shape_entered también se activa, usémosla.
  • Si body_in es falso, active tree_entered, tree_exiting. Si el objeto está en la escena de trabajo, active 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();
}

En este punto, es hora de prestar atención a los cuerpos_monitorizados, que se mantienen en add_body_to_query y 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();
	}
}

Pero esto no ve el papel de las propiedades Capa y Máscara. Luego vuelva a consultar. Finalmente encontrado en la clase GodotCollisionObject2D:

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

Efectivamente, era consistente con la conjetura. La llamada específica a collides_with está en algunas funciones de configuración.

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

No hay necesidad de ir más lejos. En cuanto a cuándo se llaman estas funciones de configuración, puede usar un proyecto real para depurar y ver.

La idea central principal es comprender el problema de colisión de configuración de capa y máscara.

Supongo que te gusta

Origin blog.csdn.net/drgraph/article/details/132100947
Recomendado
Clasificación