Делаем конвейер в Box2D - туториал

Добрый вечер, друзья. Сегодня, как и обещал, я хочу рассказать как делать логический конвейер в Box2D 2.1a.

Для начала обрисуем что же такое «логический конвейер». Этот термин, я придумал вчера.
Для его реализации нам не требуется большое количество тел и джоинтов, как для «физического», хватит и одного.
конвейеры
Alert: Тутор довольно увесистый.

Оба имеют свои плюсы и минусы, каким пользоваться — зависит от ситуации.

Физический:
+Красиво, реалистично провисает.
-ресурсоёмко, если у вас много физ. объектов, то такой конвейер всё убьет.
-голючит при натяжении, может сломаться.

Логический:
+быстро работает, не требует много ресурсов на эмуляцию.
+настраиваемый, можно сделать даже несколько областей разной скорости и направления на одном.
+не ломается.
-не так реалистично.

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

Теория — смело пропускайте, если не интересно.

Сам не люблю теорию, потому опишу как работает метод очень коротко.
Каждый раз, когда сталкиваются два тела, трение заставляет их скорости сравняться. Результат зависит от веса тел, их скорости и площади поверхности. Этот метод эмулирует движение одного/обоих контактирующих тел, а всё остальное ложится на плечи физическому движку. Можете представить это как будто там конвейер (так логичнее), или одно их тел действительно движется, а второе на нём лежит.
Кстати, сначала я думал применять импульс к точку прикосновения, но ничего хорошего их этого не вышло.

Процесс пошёл — модифицируем код

Как сделать физический конвейер из revolute-джоинтов и динамических тел вы нуже няка уже знаете, потому сейчас мы будем делать логический. Стоит также предупредить, что в процессе, я буду вносить изменения в код движка… опять. *evil laugh*

Это затронет такие классы:
Box2D\Dynamics\Contacts\b2Contact.as
Box2D\Dynamics\Contacts\b2ContactConstraint.as
Box2D\Dynamics\Contacts\b2ContactSolver.as

Итак, в b2Contact.as нужно добавить новое свойство surfaceVelocityModifier и его сеттер.
А так же занулять его на каждом update.

	// Искусственная поверхностная скорость. (для одной итерации)
	var surfaceVelocityModifier:Number = 0;
	
	/** Изменяем относительную скорость двух "фикстур", перпендикулярную нормали контакта
	 * Работает только в текущей итерации, или суб-итерации для (bullet=true)
	 */
	public function SetTangentSpeed(Velocity:Number):void
	{
		surfaceVelocityModifier = Velocity;
	}

... обрыв

	b2internal function Update(listener:b2ContactListener) : void
	{
		surfaceVelocityModifier = 0;

... обрыв


Открываем b2ContactConstraint.as и добавляем переменную surfaceVelocityModifier:

	public var manifold:b2Manifold; // существующий код
						   // Искусственная поверхностная скорость.
	public var surfaceVelocityModifier:Number; //добавляемый код


Теперь будем потрошить b2ContactSolver.as. Добавим присвоение в ф-цию Initialize()

			cc.type = manifold.m_type; // существующий код
			cc.surfaceVelocityModifier = contact.surfaceVelocityModifier; //добавляемый код

И пересчёт скорости в ф-цию SolveVelocityConstraints()

				dvX = vB.x - wB * ccp.rB.y - vA.x + wA * ccp.rA.y; // существующий код
				dvY = vB.y + wB * ccp.rB.x - vA.y - wA * ccp.rA.x; // существующий код
				
				// Вычисляем скорость с учётом нашего модификатора
				dvX -= c.surfaceVelocityModifier * tangentX;        //добавляемый код
				dvY -= c.surfaceVelocityModifier * tangentY;        //добавляемый код


Полпути — используем наработку

Чтобы потешить себя чувством достижения, можно немного переделать стандартный breakable-тестбед:

Обновите страницу, если не видно флешки.

Надо всего лишь добавить следующее в PreSolve:
override public function PreSolve(contact:b2Contact, oldManifold:b2Manifold):void 
	{
		contact.SetTangentSpeed(-5);
		super.PreSolve(contact, oldManifold);
	}

Коробки двигаются вправо как будто под ними есть конвейер с лентой, движущейся по часовой стрелке. Кстати, разрешаю по швырять коробку в разные стороны и убедиться, что всё ведёт себя реалистично, вы всего лишь отнимаем от скорости движения объекта, потому касаясь пола, коробка не движется равномерно. Это так же значит, что изменяя трение, вы получите другой результат, всё как надо.

Хотя вы заметите, что теперь все поверхности обладают горизонтальной скоростью, направленной вправо (включая стороны динамических квадратов), но это ничего, на данном этапе цель была просто наконец увидеть, что оно хоть как-то работает.

Идём дальше
Конвейер теперь есть, но он, как говорится, sick as a brick. Следующим шагом будет добавить ему «ума». Для этого мы пополним b2Fixture несколькими свойствами, которые будут использоваться для просчёта скорости в PreSolve(). Их можно хранить и в UserData, но она использует динамическую структуру, которую потом можно забыть занулить, не говоря уже о (малом) падении производительности.

Box2D\Dynamics\b2Fixture.as
Box2D\Dynamics\b2FixtureDef.as

Следующий код можно добавить в b2Fixture.as:

...Обрыв
		//Функция Create()
		m_density = def.density; // Существующий код

		// Добавляемый код
		surfaceVelocity = def.surfaceVelocity;
		if (def.minAngle == 0 && def.maxAngle == 0) {
			controlAngle = false;
		} else {
			controlAngle = true;
			minAngle = def.minAngle;
			maxAngle = def.maxAngle;
		}
...Обрыв		
	// Добавляемые переменные - переменные класса
	public var controlAngle:Boolean = false;
	public var minAngle:Number = 0;
	public var maxAngle:Number = 0;
	public var surfaceVelocity:Number = 0;

...Дальше - существующие переменные.


B2FixtureDef является всего-лишь контейнером для передачи параметров b2Fixture. Мы добавляем фичу в движок, потому всё должно работать, как задумано автором.

Итак, b2FixtureDef.as

	// В Конструктор
	minAngle = maxAngle = surfaceVelocity = 0;
...Обрыв
	// В конец
	 /**
	 * Минимальный угол для искусственной поверхностной скорости.
	 */
	public var minAngle:Number;
	
	/**
	 * Максимальный угол для искусственной поверхностной скорости.
	 */
	public var maxAngle:Number;
	
	/**
	 * Значение искусственной поверхностной скорости.
	 */
	public var surfaceVelocity:Number;
...Обрыв


Тут потребуется некоторое объяснение
Углы

Мой метод вычисляет углы при помощи точки на теле и арктангенса atan2(y,x).
Это значит 2 вещи:
Область поворачивается вместе с телом.
(Если мы сделаем конвейер с рабочей верхней плоскостью и повернём его на 180, то она станет нижней)
Сверху угол отрицательный и (принадлежит) (0;-180), а снизу положительный (0;180).

Чтобы иметь максимальный контроль, мы будем использовать PreSolve() в контактлисенере. Это сделает конвейер довольно гибким в настройке. Сразу скажу, что следующий код можно добавить как в ваш ContactListener, так и в сам b2ContactListener, который вы наследуете. Решайте сами. Я добавил в свой.

ContactListener

		override public function PreSolve(contact:b2Contact, oldManifold:b2Manifold):void 
		{
			
			var fixtureA:b2Fixture = contact.GetFixtureA();
			var fixtureB:b2Fixture = contact.GetFixtureB();
			
			//Нужно для вычисления точки контакта и угла.
			var worldManifold:b2WorldManifold;
			var surfaceVelocityModifier:Number = 0;
			var localNormal:b2Vec2;
			var angle:Number;
			var manifold:b2Manifold;
			var worldPoint:b2Vec2;
			var localPoint:b2Vec2;
			
			//Работаем с манифолдом и векторами, если это надо.
			if (fixtureA.surfaceVelocity != 0) 
			{
				if (!fixtureA.controlAngle)
				{
					//Если контроль угла не задан, то незачем проводить вычисления
					surfaceVelocityModifier += fixtureA.surfaceVelocity;	
					
				} else {
					//Нужно для получения количества точек соприкосновения
					manifold = contact.GetManifold();
					
					//Если соприкосновение есть
					if (manifold.m_pointCount)
					{
						//Берём первую точку соприкосновения 
	//(можно сделать for и проверять все точки, но это затратно и в моём случае не нужно)

						worldManifold = new b2WorldManifold();
						contact.GetWorldManifold(worldManifold);
						worldPoint = worldManifold.m_points[0];
						
						//Находим точку на теле и угол
						localPoint = fixtureA.GetBody().GetLocalPoint(worldPoint);
						angle = Math.atan2( localPoint.y, localPoint.x );
						if (fixtureA.minAngle < angle && fixtureA.maxAngle > angle)
							surfaceVelocityModifier += fixtureA.surfaceVelocity;
					}
				}
			}
			
			if (fixtureB.surfaceVelocity != 0) 
			{
				if (!fixtureB.controlAngle)
				{
					surfaceVelocityModifier += fixtureB.surfaceVelocity;	
					
				} else {
					manifold = contact.GetManifold();
					
					if (manifold.m_pointCount)
					{
						worldManifold = new b2WorldManifold();
						contact.GetWorldManifold(worldManifold);
						worldPoint = worldManifold.m_points[0];
						
						localPoint = fixtureB.GetBody().GetLocalPoint(worldPoint);
						angle = Math.atan2( localPoint.y, localPoint.x );
						if (fixtureB.minAngle < angle && fixtureB.maxAngle > angle)
							surfaceVelocityModifier += fixtureB.surfaceVelocity;
					}
				}
			}
			
			//Применяем результирующую скорость.
			contact.SetTangentSpeed( surfaceVelocityModifier );
		}


Основная работа закончена. Теперь осталось создать конвейер. Для этого в процессе создания тела всего-лишь надо передать в b2FixtureDef нужную информацию:

var fixtureDef:b2FixtureDef = new b2FixtureDef();
fixtureDef.friction = x;
...
...Ваш обычный код создания b2FixtureDef
...
fixtureDef.maxAngle = conveyorAngle * deg_to_rad;  //Если надо ограничивать область,
fixtureDef.minAngle = conveyorAngle2 * deg_to_rad; // то надо задать оба угла. В радианах.
fixtureDef.surfaceVelocity = conveyorVelocity; // Скорость конвейера.
...


Теперь вы сможете контролировать скорость поверхности каждого шейпа в отдельности, но если тело некоторое время не будет двигаться, то оно заснёт… (видно в первом примере) и тогда этот способ перестанет работать. Можно задать SetSleepingAllowed(false) для динамических тел, это решит проблему.

Замечу, что задавать углы — ваша работа, и для разных размеров поверхностей тел он будет варьироваться. Можно переписать этот метод для автоматического вычисления угла в зависимости от нужной поверхности, если нужно, но туториал и так затянулся, так что сейчас об этом говорить мы не будем.

Всё готово — теперь ваш Бокс оборудован тангенциальной скоростью

Говоря о тангенциальной скорости, при помощи этого метода можно контроллировать движение персонажей, но их надо будет закрепить (fixedRotation). Не знаю зачем, но так же можно использовать его для колёс, но их тоже надо будет сделать невращающимися, но в этом случае есть одна оговорка: они будут достигать максимальной скорости довольно быстро.
Естественно для визуальной стороны придётся нарисовать и анимировать конвейер/колесо вручную и подогнать по скорости.
Зная как обращаться с ContactListener'ом, можно изменить много чего, например метод вычисления угла, или сделать градиент плоскостной скорости, включаем фантазию. Кстати, движение зависит от трения, если его увеличить, тела будут быстрее достигать максимальной скорости конвейера.

Вот вам стиральная машинка:

Обновите страницу, если не видно флешки.

Многобуков
Это всё на сегодня, спасибо за внимание и терпение, если дошли до сюда.

PS: Идея пошла отсюда: www.box2d.org/forum/viewtopic.php?f=3&t=2861
PPS: Если есть ошибки в тексте, или что-то не работает — обязательно пишите.
PPPS: Первая картинка отсюда: www.arcadecanvas.com/games/conveyor

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

+2
А хитро! Твой лучший эксперимент с боксом, добавлю в свою копилку модификаций )
+3
На последнюю флешку смотрю уже 5 минут.
+1
Я восхищаюсь )
+5
Немного странно что в статье нет и намека на автора алгоритма или источник:
www.iforce2d.net/b2dtut/conveyor-belts

Зато есть ссылка на источник какой-то там картинки, расстановка приоритетов, по-моему, страдает у тебя.
0
Ссылки на «источник какой-то там картинки» нет. Она была в видео.

намек на автора алгоритма

Да, это стоит сказать. Идея — mayobutter, зародилась здесь: www.box2d.org/forum/viewtopic.php?f=3&t=2861 Реализация моя.
Сходства со статьей по ссылке вижу, но он не является использованным материалом.
0
Нашёл ссылку на топик mayobutter в статье iforce2d. Теперь это выглядит как оправдание.
Заркуа, запостить картинку не сложно, а теперь, будь добр, сравни статьи.
+4
Про указание источника картинки я имел ввиду
PPPS: Первая картинка отсюда: www.arcadecanvas.com/games/conveyor

Реализации различаются только способом хранения предельных углов, там через юзердату, у тебя добавлены дополнительные свойства в b2Fixture. Названия переменных тоже совпадают, именно это и натолкнуло на мысль, что именно это и есть оригинал алгоритма, который лег в основу написания статьи. Если алгоритм твой, то респект — молодец, но из-за такой степени схожести и различия дат публикаций, теоретически могут возникнуть недопонимания с Chris Campbell — iforce2d.net, исходники и так распространяются под лицензией zlib, которая разрешает делать с исходниками все, что заблагорассудится, кроме двух вещей:
— Запрещается утверждать, что это вы написали оригинальный продукт;
— Изменённые версии не должны выдаваться за оригинальный продукт;
+5
Господа, я не пытаюсь тут что-то продать. Это туториал, его я написал сам. Обещание выполнено, надеюсь, люди, поставившие плюс посту с видео чему-то научились.

Код поверхностной скорости давно входит в v2.3 box2d (правда, он выглядит по-другому и написан на другом языке), авторство мне не нужно.
Я не буду патентовать метод нахождения угла, который, хочу заметить, более надежен, чем предложенный iforce. Не буду, чтобы не возникало таких идиотских ситуаций с тем, кому он придет в голову независимо от меня.
+7
MidnightOne спасибо, отличный туториал.
Не понимаю некоторых товарищей, человек постарался, потратил свое личное время, а вместо адекватного обсуждения статьи в комментах какие-то картинки. Напишите лучше тоже что-нибудь полезное;)
+2
MidnightOne, спасибо за урок
0
Спасибо, пригодилось.
0
Спасибо, интересно.
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.