Новый nape. Приложение сил к телам и raycast

Новый nape:
1. Новый nape. Hello world
2. Новый nape. Обработка событий
3. Новый nape. Testbed. И немного теории
4. Новый nape. Создание тел, материалы, фильтрация столкновений
5. Новый nape. Соединение тел
6. Новый nape. Приложение сил к телам и raycast (Его вы сейчас читаете)

Все примеры кода в Testbed: Скачать проект для FlashDevelop.

Приложение сил и импульсов к телам.

Как мы можем повлиять на тело в nape? Можно просто указать ему новое положение или скорость — но такое воздействие лежит за пределами физической симуляции, можно прицепить к нему constraint — это вполне в рамках физики, но не всегда подходит, есть еще один вариант, и его мы не рассматривали, — можно приложить к телу силу или импульс.

Для начала разберемся, что такое сила и импульс. Школьный курс наверное помнят все, но не будем вдаваться в детали, для нас важно следующее: с точки зрения nape приложить силу — это тоже самое, что приложить импульс равный величине силы, умноженной на длительность одного шага физической симуляции. Т.е. с практической точки зрения, если есть некая длительно действующая сила, то нужно прикладывать к телу силу (например, если вы решили сделать например эффект ветра, то просто каждый шаг прикладываете к нужным телам соответствующую силу), а если возникло какое-то мгновенное событие (например, взрыв), то нужно прикладывать импульс. Если следовать этому правилу, то можно менять длительность шага симуляции, и это не приведет к необходимости менять размеры прикладываемых сил и импульсов.

В nape у класса Body есть шесть функций:
applyLocalForce
applyRelativeForce
applyWorldForce
applyLocalImpulse
applyRelativeImpulse
applyWorldImpulse

Первые три функции прикладывают силу, а следующие три — импульс. В остальном эти функции отличаются только тем, как им передаются координаты точки приложения силы: в функции со словом local в названии передается точка в локальных координатах тела, в функции со словом relative в названии передается точка в «относительных координатах», т.е. начало координат совпадает с положением тела, а оси направлены как глобальной системе координат, и, наконец, функции с названием, содержащим world, принимают глобальные координаты. Сила или импульс всегда передаются в глобальных координатах.

В Testbed я добавил тест с эффектами ветра и взрыва, а также добавил ракету. Там используются описанные функции. Эффект ветра и взрыва не учитывают размер попавших зону их действия предметов, а должны бы. В остальном надеюсь там все очевидно.

Код. Ракета, тут ИМХО все очевидно, смотрим где находится условный двигатель, где — центр ракеты, проводим линию получаем направление силы, прикладываем ее в точке крепления условного двигателя.
var rocketForceVec:Vec2 = rocket.position.sub(rocketEngine.worldCOM);
rocketForceVec.muleq(rocketForce.value / rocketForceVec.length);
rocket.applyWorldForce(rocketForceVec, rocketEngine.worldCOM);

Ветер. Перебираем все тела касающиеся сенсора, задающего область действия ветра и прикладываем к ним силу.
for (var i:int = 0; i < windSensor.arbiters.length; ++i)
{
	var arb:Arbiter = windSensor.arbiters.at(i);
	if (arb.isSensorArbiter())
	{
		var body:Body = arb.body1 == windSensor ? arb.body2 : arb.body1;
		if (body.isDynamic())
			body.applyRelativeForce(new Vec2(windForce.value, 0));
	}
}

Взрыв. Пробегаем по телам касающимся сенсора, отвечающего за взрыв (а можно было бы пробегать и по всем телам вообще или воспользоваться функцией space.bodiesInCircle) и прикладываем к ним импульс направленный от центра взрыва и обратно пропорциональный квадрату расстояния.
for (i = 0; i < explosionSensor.arbiters.length; ++i)
{
	arb = explosionSensor.arbiters.at(i);
	if (arb.isSensorArbiter())
	{
		body = arb.body1 == explosionSensor ? arb.body2 : arb.body1;
		if (body.isDynamic())
		{
			var impulse:Vec2 = body.worldCOM.sub(explosionPoint);
			impulse.muleq(explosionCoefficient.value / Math.max(100, impulse.lsq(), 2));
			body.applyLocalImpulse(impulse);
		}
	}
}


Raycast

В nape есть функции для поиска тел, пересекаемых заданным лучом. Очень полезно, если делать что-нибудь лазерное :-)

Таких функций всего две, обе в классе Space: rayCast и rayMultiCast. Первая нужна, если нужно найти первое пересечение, а вторая — если нужно найти все пересечения.

Немного кода для ясности:
var ray:Ray = new Ray(laserPoint.worldCOM, Vec2.fromPolar(1, laser.rotation));
ray.maxDistance = 300;
var rayResult:RayResult = space.rayCast(ray, false, new InteractionFilter(1, 0x10));

if (rayResult != null)
{
	var resultPt:Vec2 = ray.at(rayResult.distance);
	var body:Body = rayResult.shape.body;
	body.applyWorldForce(ray.direction.mul(1000), resultPt);
}

Вначале создается луч, при этом указывается точка начала и вектор направления, также я задал максимальную длину, можно этого не делать, тогда ограничения по длине не будет. Далее вызывается собственно функция rayCast: первый параметр — луч, второй — нужно ли учитывать пересечения изнутри наружу фигуры, а третий — фильтр столкновений, пересечение луча будет только с телом, с которым бы столкнулось другое тело с таким фильтром. Ну и в качестве демонстрации я прикладываю силу в этой точке к найденному телу.

RayMultiCast отличается тем, что возвращает список из всех найденных точек столкновения, отсортированный по удаленности. Я написал небольшой пример разрезания тел на куски лучом с использованием этой функции. Общая идея в том, что мы находим две точки пересечения с одним и тем же полигоном, а потом отбираем вершины лежащие выше и ниже луча (для этого надо знать, что такое скалярное произведение, и что по-английски оно зовется dot product, а также что в nape его выполняет функция Vec2.dot()) И из них и точек пересечения получаем два новых полигона.

var rayResults:RayResultList = space.rayMultiCast(ray, true, rayFilter);
if (rayResults.length > 1)
{
var polygon:Polygon = rayResults.at(0).shape as Polygon;
if (polygon != null)
{
	var pt0:Vec2 = ray.at(rayResults.at(0).distance);
	var pt1:Vec2 = null;
	for (var i:int = 1; i < rayResults.length; ++i)
	{
		if (rayResults.at(i).shape == polygon)
		{
			pt1 = ray.at(rayResults.at(i).distance);
			break;
		}
	}
	if (pt1 != null)
	{
		var normal:Vec2 = new Vec2( -(pt1.y - pt0.y), pt1.x - pt0.x);
		var vertN:int = polygon.worldVerts.length;
		
		function side(n:int):Boolean
		{
			return normal.dot(polygon.worldVerts.at(n).sub(pt0)) > 0;
		}
		
		//find first vertice over pt0-pt1
		//iterate while vertices are over 
		for (i = 0; side(i) == true; i = (i + 1) % vertN) {}
		//iterate while vertices are below, stop when vertice is again over
		for (; side(i) == false; i = (i + 1) % vertN) {}
		
		//get distance from pt0 and from pt1 to edge (i)-(i-1). intersection0 - current intersection, intersection1 - next intersection
		var distPt0:Number = Math.abs(polygon.worldVerts.at(i).sub(pt0).dot(polygon.edges.at((i - 1 + vertN) % vertN).worldNormal));
		var distPt1:Number = Math.abs(polygon.worldVerts.at(i).sub(pt1).dot(polygon.edges.at((i - 1 + vertN) % vertN).worldNormal));
		var intersection0:Vec2;
		var intersection1:Vec2;
		if (distPt0 < distPt1)
		{
			intersection0 = pt0;
			intersection1 = pt1;
		}
		else
		{
			intersection0 = pt1;
			intersection1 = pt0;
		}
		
		//create slices
		for (var n:int = 0; n < 2; ++n)
		{
			var verts:Array = [n == 0 ? intersection0 : intersection1];
			for (; side(i) == (n == 0); i = (i + 1) % vertN)
				verts.push(polygon.worldVerts.at(i));
			verts.push(n == 0 ? intersection1 : intersection0);
			
			var newB:Body = new Body(BodyType.DYNAMIC);
			newB.shapes.add(new Polygon(verts, polygon.material, polygon.filter, polygon.cbType));
			newB.align();
			newB.space = space;
			
		}
		polygon.body.space = null;
	}
}


Смотрим результат. Найдите тесты с названиями Test 3 и Test 4.
  • +22

Комментарии (3)

+1
Вся серия уроков по nape замечательная, спасибо!
+1
+1
Если решу пересесть на nape — всё будет гораздо легче чем могло бы...:)
0
Спасибо, камрад, помог освежить знания после работы с первым нейпом годичной давности =)
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.