Pull to refresh

Определение столкновений 2d-объектов в Marmalade SDK

Reading time10 min
Views7.8K
Marmalade Привет всем! Хочу поделиться своим опытом разработки приложений под Marmalade SDK.

Хочу напомнить, что Мармелад — это замечательный инструмент для создания мультиплатформенных приложений. Можно написать приложения, как под телевизоры LG TV, так и под мобильные устройства (на базе Android, Bada, iOS, Symbian и WindowsMobile).
В данной статье вы найдёте:
  • процесс создания приложения под Marmalade SDK;
  • определение столкновений (collision detection) выпуклых многоугольников, при помощи построения проекций;
  • определение реакции на столкновение (вывод объекта по минимальному пути).


Немного предисловия. Однажды, для одного проекта, мне потребовалось реализовать проверку на столкновения различных объектов. И я был очень удивлён тем фактом, что, не смотря на то, что практически ни одна игра не обходится без проверок на столкновение и пересечение объектов, не так-то просто найти уже готовые решения и простые примеры для такой задачи. Тем более с более-менее ясными объяснениями. Пришлось реализовывать самостоятельно.

Заранее хочу предупредить, что весь код, приведенный в статье, написан на C++.

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

Итак, давайте начнем с простого – создания Marmalade-проекта.

В Мармеладе всё начинается с файла mkb, поэтому рассмотрим его. Создадим в папке с проектом файл test2d.mkb и папку source. В файле mkb напишем следующее:

#!/usr/bin/env mkb
files {
[Source]
(source)
main.cpp
entity.cpp
entity.h
}
subprojects {
iw2d
}


В теге «files» описываем список файлов нашего проекта. У нас будет всего три файла, которые находятся в папке «source» (на это указывает содержимое круглых скобок). В теге «subprojects» мы перечислим библиотеки Мармелада, которые мы будем использовать. В данном случае у нас всего ода библиотека для работы с 2d графикой.

Создадим три пустых файла (main.cpp, entity.cpp и entity.h) в папке «source», после чего запустим test2d.mkb. В результате Мармелад сгенерирует необходимые данные для среды разработки (в моём случае это папка «build_test2d_vc10» для Visual Studio), а так же папку «data» с настройками проекта.

Проект создан, теперь приступим к написанию кода.

Первичная задача состоит в следующем:
  • создадим несколько объектов на сцене;
  • реализуем управление одним из объектов (управление будет осуществляться клавиатурой: стрелки — движение, 1 и 2 — поворот);
  • реализуем проверку на столкновения управляемого объекта с остальными объектами сцены, а так же обеспечим адекватную реакцию на столкновения.


image

Итак, рассмотрим код файла main.cpp, в котором мы осуществим инициализацию, создадим необходимые фигуры и объекты, реализуем главный цикл приложения, а так же осуществим очистку данных при завершении работы приложения.

		#include "iw2d.h"
		#include "iwArray.h"
		#include "entity.h"

		#include "s3eKeyboard.h"
		#include "s3eDevice.h"
		#include <time.h>
		#include <s3e.h>

		#define FPS 30

		#define MOVE_SPEED 100

		#define OBJECT_DIMENSION 50

		/***************************************************************/

		int main(int argc, char* argv[])
		{
			s3eResult result = S3E_RESULT_SUCCESS;

		/* Инициализируем модуль для работы с 2D графикой */
			Iw2DInit();

		/* Узнаём размеры экрана */ 
			uint32 width = Iw2DGetSurfaceWidth();
			uint32 height = Iw2DGetSurfaceHeight();

		/* Вычисляем размер объектов "стен" */
			int wall_size = MAX(width, height);

		/* Список всех объектов */
			CIwArray<CEntity*> arr_objects;
				
		/* Описываем 3 фигуры различной формы */
		/* Все фигуры выпуклые */
			CIwSVec2 m_Verts1[5];
			CIwSVec2 m_Verts2[4];
			CIwSVec2 m_Verts3[4];

		/* Фигура 1 */
			m_Verts1[0] = CIwVec2(-OBJECT_DIMENSION, -OBJECT_DIMENSION);
			m_Verts1[1] = CIwVec2(OBJECT_DIMENSION, -OBJECT_DIMENSION);
			m_Verts1[2] = CIwVec2(OBJECT_DIMENSION, 0);
			m_Verts1[3] = CIwVec2(0, OBJECT_DIMENSION);
			m_Verts1[4] = CIwVec2(-OBJECT_DIMENSION, OBJECT_DIMENSION);

		/* Фигура 2 */
			m_Verts2[0] = CIwVec2(-OBJECT_DIMENSION/2, OBJECT_DIMENSION + OBJECT_DIMENSION/2);
			m_Verts2[1] = CIwVec2(OBJECT_DIMENSION/2, OBJECT_DIMENSION/2);
			m_Verts2[2] = CIwVec2(OBJECT_DIMENSION/2, -OBJECT_DIMENSION/2);
			m_Verts2[3] = CIwVec2(-OBJECT_DIMENSION/2, -OBJECT_DIMENSION/2);

		/* Фигура 3: стена */
			m_Verts3[0] = CIwVec2(-5, wall_size/2);
			m_Verts3[1] = CIwVec2(5, wall_size/2);
			m_Verts3[2] = CIwVec2(5, -wall_size/2);
			m_Verts3[3] = CIwVec2(-5, -wall_size/2);

		/* Создаём объект, которым будем управлять */
		/* Устанавливаем цвет, положение и массив вершин */
			CEntity *main_object = new CEntity(CIwSVec2(width/2, height/2), &m_Verts2[0], 4, 0xFFff0000);
			arr_objects.append(main_object);

		/* Создадим два неподвижных объекта одинаковой формы в различных местах, и повернём их. */
			CEntity *tmp_object = new CEntity(CIwSVec2(width/2, height/2), &m_Verts1[0], 5, 0xFF0000ff);
			tmp_object->SetAngle(IW_ANGLE_PI/3);
			arr_objects.append(tmp_object);

			tmp_object = new CEntity(CIwSVec2(50, 50), &m_Verts1[0], 5, 0xFF0000ff);
			tmp_object->SetAngle(IW_ANGLE_PI/2);
			arr_objects.append(tmp_object);

		/* Создадим 4 "стены" */
			tmp_object = new CEntity(CIwSVec2(0, height/2), &m_Verts3[0], 4, 0xFFff0000);
			arr_objects.append(tmp_object);

			tmp_object = new CEntity(CIwSVec2(width, height/2), &m_Verts3[0], 4, 0xFFff0000);
			arr_objects.append(tmp_object);

			tmp_object = new CEntity(CIwSVec2(width/2, 0), &m_Verts3[0], 4, 0xFFff0000);
			tmp_object->SetAngle(IW_ANGLE_PI/2);
			arr_objects.append(tmp_object);

			tmp_object = new CEntity(CIwSVec2(width/2, height), &m_Verts3[0], 4, 0xFFff0000);
			tmp_object->SetAngle(IW_ANGLE_PI/2);
			arr_objects.append(tmp_object);


		/* Вычислим время между кадрами */
			int time_between_frames = 1000/FPS;

		/* Запомним текущее время */
			uint32 timer = (uint32)s3eTimerGetMs();

			while(result == S3E_RESULT_SUCCESS) {

		/* Обновим события клавиатуры */
				s3eKeyboardUpdate();
			    
		/* Проверим запрос на закрытие приложения. При необходимости прерываем цикл. */
				if (s3eDeviceCheckQuitRequest())
					break;

		/* Проверим сколько прошло времени с предыдущего такта. Необходимо для равномерного движения объектов */
				uint32 now_time = (uint32)s3eTimerGetMs();
				int delta = now_time - timer;

				if (delta < 0) delta = 0;

		/* Сбрасываем скорость движения объекта и скорость повората */
				main_object->Move(CIwSVec2::g_Zero);
				main_object->Rotate(0);

		/* Устанавливаем скорость движение объекта и скорость поворота в зависимости от нажатых клавиш */
				if ( (s3eKeyboardGetState(s3eKeyLeft) & S3E_KEY_STATE_DOWN) ) {
					main_object->Move(CIwSVec2(-MOVE_SPEED, 0));
				} else if ( (s3eKeyboardGetState(s3eKeyRight) & S3E_KEY_STATE_DOWN) ) {
					main_object->Move(CIwSVec2(MOVE_SPEED, 0));
				} else if ( (s3eKeyboardGetState(s3eKeyUp) & S3E_KEY_STATE_DOWN) ) {
					main_object->Move(CIwSVec2(0, -MOVE_SPEED));
				} else if ( (s3eKeyboardGetState(s3eKeyDown) & S3E_KEY_STATE_DOWN) ) {
					main_object->Move(CIwSVec2(0, MOVE_SPEED));
				} 
			
				if ( (s3eKeyboardGetState(s3eKey1) & S3E_KEY_STATE_DOWN) ) {
						main_object->Rotate(-IW_ANGLE_PI / 2);
				} else if ( (s3eKeyboardGetState(s3eKey2) & S3E_KEY_STATE_DOWN) ) {
						main_object->Rotate(IW_ANGLE_PI / 2);
				} 
				
		/* Очищаем экран белым цветом*/
				Iw2DSurfaceClear(0xffffffff);

		/* Обновляем все объекты */
				for(int i=0; i < (int)arr_objects.size(); ++i) {
					arr_objects[i]->Update(delta);
				}

		/* Устанавливаем цвет главной фигуры (зеленый). При обнаружении столкновения меняем на черный. */
				main_object->SetColour(0xff00ff00);

		/* Устанавливаем число попыток выхода главного объекта, при обнаружении столкновений. */
				int try_count = 10;

				bool b_collide;
				do{
					b_collide = false;

		/* Сравниваем управляемый объект со всеми */
					for(int i = 0; i < (int)arr_objects.size(); ++i) {
		/* Не проверяем столкновение с самим собой */
						if(arr_objects[i] != main_object){
		/* Проверяем столкновение. Если vec_reaction не нулевой, значит столкновение было. */
							CIwVec2 vec_reaction = main_object->CollideRect(arr_objects[i], true);
							if(!vec_reaction.IsZero()){
								main_object->SetColour(0xff000000);
								b_collide = true;
							}
						}
					}
					--try_count;
		/* Проверяем на столкновения до тех пор, пока объект "соприкасается" с другими, но не больше чем количества попыток. */
				} while(b_collide && try_count > 0);

		/* Рисуем все объекты */
				for(int i=0; i < (int)arr_objects.size(); ++i) {
					arr_objects[i]->Render();
				}

		/* Выведем всё на экран */
				Iw2DSurfaceShow();

		/* Вычисляем количество времени до следующего кадра и ждём. */
				int32 wait_time = (uint32)time_between_frames - ((uint32)s3eTimerGetMs() - now_time);
					
				if(wait_time<0)wait_time = 0;

				timer = now_time;
				s3eDeviceYield(wait_time);
			}

		/* Удаляем все объекты из памяти */
			for(int i=0; i < (int)arr_objects.size(); ++i) {
				delete arr_objects[i];
			}
		/* Очищаем список */
			arr_objects.clear_optimised();
				
		/* Прерываем работу модуля для работы с 2d графикой. */
			Iw2DTerminate();

		    return 0;
		}
		


Вроде бы ничего сложного. Теперь давайте рассмотрим сами объекты, в которых будет реализованы следующие возможности:
  • объект имеет форму и цвет;
  • объект может изменять положение (двигаться) и вращаться;
  • объект может реагировать на столкновения с другими объектами;
  • объект может рисовать себя;

Давайте рассмотрим описание класса наших объектов (файл entity.h). В нём достаточно наглядно описываются возможности объекта.

		#pragma once

		#include "iw2d.h"
		#include "iwArray.h"
		#include "IwMath.h"

		class CEntity{
		private:
			CIwMat2D m_MatLocal;

		/* Угол поворота фигуры */
			iwangle angle;

		/* Скорость поворота фигуры */
			iwangle angle_velocity;

		/* Скорость передвижения фигуры */
			CIwSVec2 m_MoveVelocity;

		/* Количество вершин */
			uint32 m_NumPoints;

		/* Вершины фигуры */
			CIwSVec2 *m_Points;

		/* Модифицированные вершины */
			CIwVec2 *m_PointsMod;

		/* Оси проецирования*/
			CIwVec2 *m_Axis;
				
		/* Цвет фигуры */
			uint32 colour;

		public:
		/* Конструктор */
			CEntity(CIwSVec2 position, CIwSVec2 *verts, int num_verts, uint32 colour = 0xff000000);

		/* Деструктор */
			virtual ~CEntity();

		/* Обновление фигуры */
			void Update(int speed);

		/* Рисование фигуры */
			void Render();

		/* Устанавливаем угол поворота фигуры */
			void SetAngle(iwangle angle) {this->angle = angle % IW_ANGLE_PI;};

		/* Устанавливаем скорость поворота фигуру */
			void Rotate(iwangle rot_angle) { angle_velocity = rot_angle; }
				
		/* Устанавливаем скорость передвижения фигуры */
			void Move(CIwSVec2 move_velocity) { m_MoveVelocity = move_velocity; }

		/* Устанавливаем цвет фигуры */
			void SetColour(int colour){this->colour = colour;}

		/* Отдаём указатель на модифицированные вершины */
			const CIwVec2 *GetVerts(){ return m_PointsMod; }

		/* Количество вершин */
			int GetNumVerts() {return m_NumPoints;}

		/* Подсчитываем столкновение вершин. Возвращаем минимальный путь выхода одной фигуры из другой */
			CIwVec2 CollideRect(CEntity *other, bool b_uncollide = false);
		private:
		/* Обновляем все вершины */
			void UpdateVerts();

		/* Проверка на пересечение двух объектов */
		/* Возвращает проекцию с минимальным пересечением и размер пересечения */
		/* b_revert - меняет направление пересечения. */
			bool TestOverlaps(CEntity *other, CIwVec2 &min_axis, int &min_t, int b_revert);

		/* Метод возвращает минимальную и максимальную точку объекта на проекцию */
			void GetInterval(const CIwVec2 *verts, int count, CIwVec2 axis, int &min, int &max);
		};
		


Давайте рассмотрим саму реализацию класса. Особое внимание следует обратить на методы:CollideRect и TestOverlaps.

		#include "entity.h"

		CEntity::CEntity(CIwSVec2 position, CIwSVec2 *verts, int num_verts, uint32 colour ): m_MatLocal(CIwMat2D::g_Identity), m_MoveVelocity(CIwSVec2::g_Zero), angle(0), angle_velocity(0) {
			this->colour = colour;
			this->m_NumPoints = num_verts;

			m_Points = NULL;
			m_PointsMod = new CIwVec2[m_NumPoints];
			m_Axis = new CIwVec2[num_verts];

			m_MatLocal.t = position;
			m_Points = verts;
		}


		CEntity::~CEntity() {
			if(m_PointsMod)delete m_PointsMod;
			delete m_Axis;
		}
			 
		void CEntity::Update(int speed) {
			angle += (angle_velocity * speed) / 1000;
			angle = angle % IW_ANGLE_2PI;
			m_MatLocal.SetRot(angle, false);

			if(!m_MoveVelocity.IsZero()) {
				m_MatLocal.t += ( m_MoveVelocity * speed) / 1000;
			}
			UpdateVerts();
		}

		void CEntity::UpdateVerts() {
			if(!m_Points) return;
		/* Обновляем координаты вершин */
			for(uint32 i = 0; i < m_NumPoints; ++i) {
				m_PointsMod[i] = m_MatLocal.TransformVec(m_Points[i]);
			}

		/* Строим оси */
			for(uint32 i = 0; i < m_NumPoints; ++i) {
		/* Строим вектор между двумя вершинами */
				m_Axis[i] = m_PointsMod[(i + 1) % m_NumPoints] - m_PointsMod[i]; 
					
		/* Нормализуем его*/
				m_Axis[i].Normalise();

		/* И берем правую нормаль */
				m_Axis[i] = CIwVec2(-m_Axis[i].y, m_Axis[i].x); 
		    }
		}

		void CEntity::Render() {
		/* Устанавливаем цвет фигуры */
			Iw2DSetColour(colour);

			if(!m_Points)return;

		/* Устанавливаем матрицу поворота и сдвига */
			Iw2DSetTransformMatrix(m_MatLocal);

		/* Рисуем фигуру */
			Iw2DFillPolygon(m_Points, m_NumPoints);

		/* Рисуем центр фигуры*/
			Iw2DSetColour(0xff00ffff);
			Iw2DFillArc(CIwSVec2::g_Zero, CIwSVec2(5,5), 0, IW_ANGLE_2PI);
		}

		CIwVec2 CEntity::CollideRect(CEntity *other, bool b_uncollide) {
			/* Минимальная глубина вхождения, и направление для выхода из вхождения */
			int min_t(0);
			CIwVec2 min_axis(CIwFVec2::g_Zero);
				

		/* В начале проверяем пересечения с осями проекций одного объекта, потом другого */
		/* Проверка двойная т.к. каждый объект знает только свои оси, на которые необходимо строить проекции */
		/* Если хотя бы по одной проекции нет пересечения, то считаем, что фигуры не пересекаются. */
			if(!TestOverlaps(other, min_axis, min_t, false))return CIwVec2::g_Zero;
			if(!other->TestOverlaps(this, min_axis, min_t, true))return CIwVec2::g_Zero;

		/* Если это необходимо, выводим объект по минимальному пути */
			if(b_uncollide){
				CIwVec2 fvec = min_axis * min_t;

		/* Применяем сдвиг */
				m_MatLocal.t += fvec;

		/* Обновляем вершины. */
				UpdateVerts();
			}

			return  min_axis * min_t;
		}


		bool CEntity::TestOverlaps(CEntity *other, CIwVec2 &min_axis, int &min_t, int b_revert) {
		/* Получаем вершины объекта, с которым проверяем столкновение */
			const CIwVec2 *other_corner = other->GetVerts();
			int other_corner_count  =  other->GetNumVerts();

		/* Проверяем столкновение с каждой осью проекции */
			for (uint32 i = 0; i < m_NumPoints; ++i) {
				int aMin;
			        int aMax;
			        int bMin;
			        int bMax;

		/* Получаем максимальную и минимальную точки на оси проекции для каждого объекта*/
				GetInterval(m_PointsMod, m_NumPoints, m_Axis[i], aMin, aMax);
				GetInterval(other_corner, other_corner_count, m_Axis[i], bMin, bMax);

		/* Проверяем, пересекаются ли объекты на данной проекции */
				if ((aMax <= bMin) || (bMax <= aMin)) {
		/* Значит не пересекаются. Выходим */
					return false;
				}


		/* Вычисляем глубину вхождения. И запоминаем, если оно меньше, чем минимальное на момент проверки */
		/* Это необходимо для того что бы узнать минимальный путь выхода одного объекта из другого */
				int t = 0;
				if(aMax >= bMin) {
					t = bMin - aMax ;
					if(min_axis.IsZero() || ABS(min_t) > ABS(t)){
						min_t = t * (b_revert?-1:1);
						min_axis = m_Axis[i];
					}
				}
					
				if(bMax >= aMin) {
					t =  bMax - aMin;
					if(min_axis.IsZero() || ABS(min_t) > ABS(t)){
						min_t = t * (b_revert?-1:1);
						min_axis = m_Axis[i];
					}
				}
		    }

		    return true;
		}

		void CEntity::GetInterval(const CIwVec2 *verts, int count, CIwVec2 axis, int &min, int &max) {
			min = max = axis.Dot(verts[0]);
			for (int i = 1; i < count; i++) {
				int value = axis.Dot(verts[i]);
				min = MIN(min, value);
				max = MAX(max, value);
			}
		}
		


Ну вот и всё, наше приложение готово. Теперь откомпилируем его и посмотрим результаты.



Хочу заметить, что приложение достаточно простое и работает с некоторой погрешностью. Что бы её убрать, можно, например, использовать флоаты. Так же не мешало бы добавить грубую, но более быструю, первичную проверку на столкновения. Так же можно изменить реакцию объектов на столкновения (например что бы объекты отскакивали при столкновении) и многое другое.

Любая критика, замечания, дополнения и исправления приветствуются.

При написании данной статьи, очень полезным оказался один ресурс, посвящённый алгоритмам определения столкновений. С хорошими наглядными flash-примерами.
Tags:
Hubs:
Total votes 22: ↑20 and ↓2+18
Comments8

Articles