Разработка игры Пазлы на С++ для Android
В статье описывается разработка очередной игрушки на С++, Qt для Android. В игре вы можете собирать пазлы из своих собственных картинок (любых изображений, имеющихся на телефоне).
Приложение с открытым исходным кодом, без рекламы, весит 8-10 Мб. Можно загрузить с google play, а можно — посмотреть исходники.
Трудно закончить первое приложение. Дальше процесс идет проще, так как у вас накапливаются наработки. Так, в этот раз я активно пользовался заготовками, краткие аннотации к которым приведены в статье «Разработка казуальных игр с помощью Qt Framework«. В частности, с их помощью выводится справка о программе, реализовано иерархическая система окон, отображается часть игровой сцены и выполняется ее масштабирование жестами пальцев.
Код, выполняющий открытие изображений на телефоне я заимствовал отсюда и оттуда. Просмотреть другим способом содержимое SD-карты/внутренней памяти не получилось (у каждого производителя телефонов в этом плане есть свои заморочки). Код по первой ссылке прекрасно работал на моем телефоне (Asus), но отказался работать на аппаратах товарищей (Huawei и Samsung).
Что есть в этой статье:
Игра реализована с помощью графического фреймворка Qt. Фрагменты пазла наследуют класс QGraphicsItem и помещаются на сцену ( QGraphicsScene ), за отображение (прокрутку и масштабирование) отвечает QGraphicsView . На заднем фоне отображается сетка, составленная из QGraphicsRectItem , которые задают «точки привязки» для элементов пазла.
1 Применимость шаблона проектирования Декоратор
Если вы не слышали о таком шаблоне проектирования — загляните туда.
Фрагмент пазла должен:
- отображать нужную картинку;
- отображать рамку при выделении мышью (пальцем);
- позволять себя перетаскивать;
- поворачиваться на 90 градусов при клике (если не имел место факт перетаскивания);
- устанавливаться в «правильную» позицию (после установки — игнорировать все события мыши).
Всю эту функциональность можно реализовать в одном единственном классе, но это плохое решение, нарушающее, как минимум, Single Responsibility Principle. Нужно попытаться распределить эти обязанности между несколькими классами. Паттерн «Декоратор» может показаться подходящим решением, ведь он:
- является гибкой альтернативой порождению подклассов с целью расширения функциональности [5];
- смешивать функциональность — отдельно реализуем отображение, выделение, перетаскивание и … смешиваем.
- добавлять и удалять функциональность к уже существующим объектам прямо во время выполнения (было бы здорово после «установки» элемента пазла — удалить функциональность перетаскивания и поворота);
рис. 1 Диаграмма классов шаблона Декоратор
Выглядит здорово, но в нашем случае работать не будет. Очень важная особенность, которую я не нашел у GoF [5] и поэтому пишу этот раздел — декораторы должны быть независимы друг от друга. Очевидно, если между декораторами есть зависимости, то «смешивать их как угодно» не получится. В нашем случае сразу несколько декораторов должны бы были обрабатывать события мыши, но:
- в каком порядке им это делать?
- при отпускании фигуры ( mouseReleaseEvent ) важно «имел ли место факт перетаскивания», т.к. от этого зависит необходимость вращения фигуры. А это явная зависимость от декоратора, выполняющего это перетаскивание;
- для проверки корректности установки фрагмента пазла, нужно получить информацию о его повороте (зависимость от декоратора вращения).
В момент проектирования эти зависимости не были так очевидны. В итоге, я отказался от «гибкой альтернативой порождению подклассов» и реализовал такую иерархию (с удовольствием приму предложения по рефакторингу):
Вывод: если вы хотите применить Декоратор — посмотрите внимательно, не появятся ли у конкретных декораторов взаимные зависимости.
Если вам понравился этот раздел — вы будете в восторге от «Дополнительных штрихов» Влиссидеса [6].
2 Как нарисовать фигурные контуры фрагментов?
Итак, исходное изображение разрезается на фигурные фрагменты:
Программа генерирует матрицу из кривых, как показано на приведенной картинке, а кривые генерируются случайным образом для каждого открываемого изображения. Для этого, программа хранит набор «опорных точек», описывающих один «крючек» фрагмента пазла, для каждой из них генерируется случайное смещение в небольшой окрестности (радиуса d ), как показано на рисунке:
В коде это реализовано так:
Задача немного осложняется тем, что:
- для крайних фрагментов программа вместо крючков, использует прямые;
- крючки могут быть «выгнутыми» и «вогнутыми», при этом выгнутые — увеличивают размеры фрагмента, а это важное при дальнейшей его заливке содержимым изображения;
- у смежных фрагментов имеются общие крючки и они должны иметь одинаковую форму, однако если у одного она выгнута, то у второго — вогнута.
В связи с этим, формируются две матрицы из горизонтальных и вертикальных крючков, которые необходимо соединить в контуры. Если до сих пор эти данные было удобно хранить в виде векторов опорных точек, то теперь нужно по ним построить плавную кривую ( QPainterPath ), для этого применяется стандартная функция кубической интерполяции cubicTo :
Полученные кривые соединяются в контур, для этого предварительно выполняется их смещение:
Итак, мы построили множество контуров, по которым теперь нужно разрезать изображение.
3 Как вырезать фигурный фрагмент изображения?
Лучший способ вырезать фигурную часть изображения — это залить контур нужной формы некоторым цветом и наложить на изображение маску. Тут основная проблема в том, что для наложения маски нам нужен QPixmap , а нарисовать контур с прозрачным фоном — можно на QImage :
Теперь, чтобы маска наложилась в правильное место, нужно вырезать соответствующий прямоугольник из исходного изображения. Затем, мы сможем сформировать QGraphicsPixmapItem, который и будем перемещать по графической сцене:
Тут описаны не все технические детали, однако, при необходимости, вы можете задать вопрос на нашем форуме.
Значительная часть работы была выполнена Брюхановой Ульяной, в рамках диплома.
Литература
- Страница приложения на Google Play.
- Репозиторий с исходным кодом проекта.
- Разработка казуальных игр с помощью Qt Framework.
- Шаблон проектирования Декоратор. Пример использования.
- Э. Гамма Приемы объектно-ориентированного проектирования. Паттерны проектирования / Э. Гамма, Р. Хелм, Р. Джонсон, Д. Влиссидес. – СПб.: Питер, 2009. – 366 с.
- Влиссидес Джон. Применение шаблонов проектирования. Дополнительные штрихи. : Пер. англ. М.: Издательский дом «Вильямс»
Источник
Создаем пазл для iPhone
Почему бы не представить в магазине приложений свой собственный пазл — как это сделали мы! В этом уроке я поэтапно расскажу о создании такого приложения. Итоговый результат будет выглядеть примерно так, как на фото. Чашку с кофе — и можно приступать.
Как настоящие программисты, для начала остановимся на том, что такое slider puzzle и как его реализовать. Наверное, все помнят детскую игру «пятнашки», где фишки с цифрами нужно было выстроить по порядку. В нашем случае это будут разрозненные фрагменты изображения, которые собираются в единое целое (их на один меньше, чтобы кусочки можно было перемещать). Теперь подумаем, что понадобится, чтобы воплотить такой проект в жизнь.
Для начала потребуется изображение, которое мы разделим на фрагменты. Разместим их в беспорядке, чтобы после снова собрать. Правда перед этим нужно как-то запомнить, где должен находиться тот или иной фрагмент. Для этого введем новый класс, который будет содержать как оригинальное, так и текущее положение каждого фрагмента в матрице (под матрицей понимается сетка, на которой формируется рисунок). Так мы сможем определить, собрал пользователь пазл или нет (сравнив для каждого фрагмента текущее положение с исходным). Следующая задача — определить разрешенные перемещения. Для этой цели заменим один из фрагментов пустым. На его место разрешается передвинуть соседний фрагмент. Ну вот, в принципе, и все. Если я что-то упустил, разберемся по ходу дела.
Итак, перечислим все, что необходимо сделать:
- разбить изображение;
- привязать каждую часть изображения к определенному фрагменту пазла (отвечающему за хранение его исходной и текущей позиции);
- перемешать беспорядочно все фрагменты (запускаем n-ный цикл, во время которого случайно выбранный фрагмент перемещается на место пустого);
- фиксируем касание пользователем фрагментов пазла; если перемещение разрешено, меняем местами пустой фрагмент с выбранным и проверяем, вернулось ли изображение к исходному состоянию.
Начнем? Откройте XCode и создайте приложение windows based. (Здесь я буду останавливаться в основном на логике. Детали по настройкам можно получить, загрузив исходный код либо обратившись к предыдущим урокам).
Как обычно, нам понадобится новый контроллер «UIViewController«. Создайте его и присвойте соответствующее имя. Теперь отыщите среди своих файлов подходящее изображение (по размерам чуть меньше представления).
Первая задача — разделить изображение на части. Создаем новый метод «initPuzzle:(NSString *) imagePath» — он разобьёт рисунок на отдельные фрагменты. Параллельно добавьте две константы, определяющие общее число фрагментов:
#define NUM_HORIZONTAL_PIECES 3
#define NUM_VERTICAL_PIECES 3
-( void ) initPuzzle:(NSString *) imagePath <
UIImage *orgImage = [UIImage imageNamed:imagePath];
if ( orgImage == nil ) <
return ;
>
tileWidth = orgImage.size.width/NUM_HORIZONTAL_PIECES;
tileHeight = orgImage.size.height/NUM_VERTICAL_PIECES;
for ( int x=0; x for ( int y=0; y // освобождаем ресурсы
[tileImage release];
CGImageRelease( tileImageRef );
// добавляем к представлению
[self.view insertSubview:tileImageView atIndex:0];
[tileImageView release];
>
>
>
* This source code was highlighted with Source Code Highlighter .
Запускаем приложение — на экране iPhone появляется изображение, уже поделенное на 9 фрагментов. Это сделал метод «GFImageCreateWithImageInRect» (Core Graphics), который принимает ссылку на изображение и прямоугольник, а возвращает ссылку на обрезанное изображение (в данном случае, по форме прямоугольника). Имея ссылку, приступаем к созданию экземпляра «UIImage«.
Как уже упоминалось выше, для каждого фрагмента запоминается исходная позиция (чтобы определить окончание сборки пазла), а также текущее положение по отношению к сетке. Для этой цели расширим класс «UIImageView» и добавим еще два свойства. Дополнительно можно немного раздвинуть фрагменты, чтобы они больше напоминали стандартый пазл, и добавить пустой участок, открыв возможность перемещения.
Для начала внесем в заголовочный файл константы с промежутками вместе с переменными, отвечающими за позиции фрагментов (включая пустой).
В итоге заголовочный файл должен выглядеть примерно так:
#define NUM_HORIZONTAL_PIECES 3
#define NUM_VERTICAL_PIECES 3
#define TILE_SPACING 4
@ interface SliderController : UIViewController <
CGFloat tileWidth;
CGFloat tileHeight;
NSMutableArray *tiles;
CGPoint blankPosition;
>
@property (nonatomic,retain) NSMutableArray *tiles;
@end
* This source code was highlighted with Source Code Highlighter .
Заполнить пробелы в классе реализации предлагаю самостоятельно.
Теперь у нас есть заполнитель для фрагментов и пустого места — можно переходить к отображению отдельного фрагмента. Расширим класс «UIImageView» (рассмотренным выше способом) и добавим новые свойства.
@ interface Tile : UIImageView <
CGPoint originalPosition;
CGPoint currentPosition;
>
@property (nonatomic,readwrite) CGPoint originalPosition;
@property (nonatomic,readwrite) CGPoint currentPosition;
@end
@implementation Tile
@synthesize originalPosition;
@synthesize currentPosition;
— ( void ) dealloc
<
[self removeFromSuperview];
[super dealloc];
>
@end
* This source code was highlighted with Source Code Highlighter .
В комментариях к данному коду упомяну только, что после освобождения объекта мы удаляем его из родительского уровня. Объясняется это тем, что мы имеем дело с массивом фрагментов. Когда мы его отбрасываем (освобождаем), каждый из фрагментов должен удалить себя из представления.
Вернемся к методу «-(void) initPuzzle:(NSString *) imagePath» и внесем ряд корректировок:
- пропускать «пустой» фрагмент;
- к каждому фрагменту добавлять позицию в сетке;
- увеличить расстояние между фрагментами.
-( void ) initPuzzle:(NSString *) imagePath <
UIImage *orgImage = [UIImage imageNamed:imagePath];
if ( orgImage == nil ) <
return ;
>
[self.tiles removeAllObjects];
tileWidth = orgImage.size.width/NUM_HORIZONTAL_PIECES;
tileHeight = orgImage.size.height/NUM_VERTICAL_PIECES;
blankPosition = CGPointMake( NUM_HORIZONTAL_PIECES-1, NUM_VERTICAL_PIECES-1 );
for ( int x=0; x for ( int y=0; y if ( blankPosition.x == orgPosition.x && blankPosition.y == orgPosition.y ) <
continue ;
>
CGRect frame = CGRectMake(tileWidth*x, tileHeight*y,
tileWidth, tileHeight );
CGImageRef tileImageRef = CGImageCreateWithImageInRect( orgImage.CGImage, frame );
UIImage *tileImage = [UIImage imageWithCGImage:tileImageRef];
CGRect tileFrame = CGRectMake((tileWidth+TILE_SPACING)*x, (tileHeight+TILE_SPACING)*y,
tileWidth, tileHeight );
Tile *tileImageView = [[Tile alloc] initWithImage:tileImage];
tileImageView.frame = tileFrame;
tileImageView.originalPosition = orgPosition;
tileImageView.currentPosition = orgPosition;
// освобождаем русурсы
[tileImage release];
CGImageRelease( tileImageRef );
[tiles addObject:tileImageView];
// добавляем к представлению
[self.view insertSubview:tileImageView atIndex:0];
[tileImageView release];
>
>
>
* This source code was highlighted with Source Code Highlighter .
Для начала очищаем массив, потом указываем пустую позицию последней в сетке. Для каждого фрагмента создаем описывающую его положение точку, привязывая ее к свойствам «originalPosition» и «currentPosition«. Перед обработкой фрагмента проверяем, соответствует ли его позиция пустому положению. В случае подтверждения пропускаем фрагмент. Чуть не забыл — и добавляем его в массив фрагментов.
Закончив с этим, переходим к следующему этапу проекта. Теперь необходимо беспорядочно разместить фрагменты на экране, чтобы пользователю пришлось поломать голову над тем, как собрать изображение обратно. Запустив n-ное количество циклов, будем случайным образом выбирать один из фрагментов рядом с пустым, меняя их местами. Для этого сначала определим разрешенные перемещения, что легко выполнит приведенный ниже фрагмент кода:
#define SHUFFLE_NUMBER 100
typedef enum <
NONE = 0,
UP = 1,
DOWN = 2,
LEFT = 3,
RIGHT = 4
> ShuffleMove;
* This source code was highlighted with Source Code Highlighter .
Здесь заданы n (количество случайных перемещения фрагментов) и тип «enum«, с помощью которого будут различаться разрешенные и некорректные ходы.
Первый метод «validMove:(Tile *) tile» принимает фрагмент и возвращает enum «ShuffleMove«, определяя, может ли перемещаться указанный фрагмент и в каком направлении. Для этого проверяется позиция фрагмента по отношению к пустому. Если указанный фрагмент соседствует с пустым, он может встать на его место.
-(ShuffleMove) validMove:(Tile *) tile <
// пустая точка над текущим фрагментом
if ( tile.currentPosition.x == blankPosition.x && tile.currentPosition.y == blankPosition.y+1 ) <
return UP;
>
// пустая точка под текущим фрагментом
if ( tile.currentPosition.x == blankPosition.x && tile.currentPosition.y == blankPosition.y-1 ) <
return DOWN;
>
// пустая точка слева от текущего фрагмента
if ( tile.currentPosition.x == blankPosition.x+1 && tile.currentPosition.y == blankPosition.y ) <
return LEFT;
>
// пустая точка справа от текущего фрагмента
if ( tile.currentPosition.x == blankPosition.x-1 && tile.currentPosition.y == blankPosition.y ) <
return RIGHT;
>
return NONE;
>
* This source code was highlighted with Source Code Highlighter .
Внедряем методы, ответственные за перемещение фрагмента. Их будет два: «(movePiece:(Tile *) tile withAnimation:(BOOL) animate)» определит, в каком направлении может двигаться фрагмент, и передаст задачу собственно перемещения следующему методу — «movePiece:(Tile *) tile inDirectionX:(NSInteger) dx inDirectionY:(NSInteger) dy withAnimation:(BOOL) animate)«. Второй из методов рассчитывает разницу в координатах x и y (в зависимости от того, как именно по отношению к перемещаемому фрагменту расположен пустой) и на основании ее вычисляет новое положение, меняя местами значения «currentPosition» и «blankPosition«. Если «animate» является истиной, заключаем параметры положения в операторы анимации.
-( void ) movePiece:(Tile *) tile withAnimation:(BOOL) animate <
switch ( [self validMove:tile] ) <
case UP:
[self movePiece:tile
inDirectionX:0 inDirectionY:-1 withAnimation:animate];
break ;
case DOWN:
[self movePiece:tile
inDirectionX:0 inDirectionY:1 withAnimation:animate];
break ;
case LEFT:
[self movePiece:tile
inDirectionX:-1 inDirectionY:0 withAnimation:animate];
break ;
case RIGHT:
[self movePiece:tile
inDirectionX:1 inDirectionY:0 withAnimation:animate];
break ;
default :
break ;
>
>
-( void ) movePiece:(Tile *) tile inDirectionX:(NSInteger) dx inDirectionY:(NSInteger) dy withAnimation:(BOOL) animate <
tile.currentPosition = CGPointMake( tile.currentPosition.x+dx,
tile.currentPosition.y+dy);
blankPosition = CGPointMake( blankPosition.x-dx, blankPosition.y-dy );
int x = tile.currentPosition.x;
int y = tile.currentPosition.y;
if ( animate ) <
[UIView beginAnimations: @»frame» context:nil];
>
tile.frame = CGRectMake((tileWidth+TILE_SPACING)*x, (tileHeight+TILE_SPACING)*y,
tileWidth, tileHeight );
if ( animate ) <
[UIView commitAnimations];
>
>
* This source code was highlighted with Source Code Highlighter .
Последним шагом создаем метод «shuffle«, который, как уже упоминалось выше, будет выполнять цикл количество раз, соответствующее «SHUFFLE_NUMBER«, хаотично перемещая фрагменты, для которых разрешено движение.
-( void ) shuffle <
NSMutableArray *validMoves = [[NSMutableArray alloc] init];
srandom(time(NULL));
for ( int i=0; i // выясняем, какие фрагменты могут перемещаться
for ( Tile *t in tiles ) <
if ( [self validMove:t] != NONE ) <
[validMoves addObject:t];
>
>
// случайным образом выбираем фрагмент для перемещения
NSInteger pick = random()%[validMoves count];
//NSLog(@»shuffleRandom using pick: %d from array of size %d», pick, [validMoves count]);
[self movePiece Tile *)[validMoves objectAtIndex:pick] withAnimation:NO];
>
[validMoves release];
>
* This source code was highlighted with Source Code Highlighter .
Ничего нового — делаем то, что и намечали. Для выбора разрешенного к перемещению фрагмента циклически перемещаемся между всеми, занося в массив те, что могут двигаться. Рассмотрев все фрагменты, случайным образом выбираем один и сдвигаем.
Осталось только вызвать нужный метод. К нижней части метода «initPuzzle(NSString *) imagePath» добавьте следующую строку:
* This source code was highlighted with Source Code Highlighter .
ОК. Теперь наши фрагменты отображаются на экране, причем в беспорядке. Осталось добавить интерактивности, чтобы пользователь мог их перемещать. Для этого зафиксируем касание и определим фрагмент, который нажал пользователь. Если фрагмент разрешен к перемещению, двигаем его.
Для начала внедрим вспомогательный метод, который будет возвращать привязанный к касанию пользователя фрагмент.
-(Tile *) getPieceAtPoint:(CGPoint) point <
CGRect touchRect = CGRectMake(point.x, point.y, 1.0, 1.0);
for ( Tile *t in tiles ) <
if ( CGRectIntersectsRect(t.frame, touchRect) ) <
return t;
>
>
return nil;
>
* This source code was highlighted with Source Code Highlighter .
Теперь, располагая информацией по касанию, определим, на каком фрагменте щелкнул пользователь. Отменяем метод «touchesEnded» и перемещаем выбранный фрагмент.
— ( void )touchesEnded:(NSSet *)touches withEvent:(UIEvent *) event <
UITouch *touch = [touches anyObject];
CGPoint currentTouch = [touch locationInView:self.view];
Tile *t = [self getPieceAtPoint:currentTouch];
if ( t != nil ) <
[self movePiece:t withAnimation:YES];
>
>
* This source code was highlighted with Source Code Highlighter .
Вот и все — перед вами собственный пазл. Само собой, еще нужно определить момент окончания игры. Добавьте к коду приведенный ниже метод и обращайтесь к нему каждый раз, когда метод «touchesEnded» перемещает фрагмент.
-(BOOL) puzzleCompleted <
for ( Tile *t in tiles ) <
if ( t.originalPosition.x != t.currentPosition.x || t.originalPosition.y != t.currentPosition.y ) <
return NO;
>
>
return YES;
>
* This source code was highlighted with Source Code Highlighter .
Остальное оставляю вам. Те, кому заканчивать лень, могут просто загрузить исходный код. 🙂 Спасибо за внимание.
Исходный код к уроку можно скачать здесь.
Источник