Как сделать пазл для андроид

Разработка игры Пазлы на С++ для 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 ), как показано на рисунке:

В коде это реализовано так:

Задача немного осложняется тем, что:

  • для крайних фрагментов программа вместо крючков, использует прямые;
  • крючки могут быть «выгнутыми» и «вогнутыми», при этом выгнутые — увеличивают размеры фрагмента, а это важное при дальнейшей его заливке содержимым изображения;
  • у смежных фрагментов имеются общие крючки и они должны иметь одинаковую форму, однако если у одного она выгнута, то у второго — вогнута.
Читайте также:  Связанные таблицы sqlite android studio

В связи с этим, формируются две матрицы из горизонтальных и вертикальных крючков, которые необходимо соединить в контуры. Если до сих пор эти данные было удобно хранить в виде векторов опорных точек, то теперь нужно по ним построить плавную кривую ( QPainterPath ), для этого применяется стандартная функция кубической интерполяции cubicTo :

Полученные кривые соединяются в контур, для этого предварительно выполняется их смещение:

Итак, мы построили множество контуров, по которым теперь нужно разрезать изображение.

3 Как вырезать фигурный фрагмент изображения?

Лучший способ вырезать фигурную часть изображения — это залить контур нужной формы некоторым цветом и наложить на изображение маску. Тут основная проблема в том, что для наложения маски нам нужен QPixmap , а нарисовать контур с прозрачным фоном — можно на QImage :

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

Тут описаны не все технические детали, однако, при необходимости, вы можете задать вопрос на нашем форуме.

Значительная часть работы была выполнена Брюхановой Ульяной, в рамках диплома.

Литература

  1. Страница приложения на Google Play.
  2. Репозиторий с исходным кодом проекта.
  3. Разработка казуальных игр с помощью Qt Framework.
  4. Шаблон проектирования Декоратор. Пример использования.
  5. Э. Гамма Приемы объектно-ориентированного проектирования. Паттерны проектирования / Э. Гамма, Р. Хелм, Р. Джонсон, Д. Влиссидес. – СПб.: Питер, 2009. – 366 с.
  6. Влиссидес Джон. Применение шаблонов проектирования. Дополнительные штрихи. : Пер. англ. М.: Издательский дом «Вильямс»

Источник

Создаем пазл для 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];
>
>
>

Читайте также:  Windows icon pack для андроид

* 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 .

Читайте также:  Whatsapp android no root

Внедряем методы, ответственные за перемещение фрагмента. Их будет два: «(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 .

Остальное оставляю вам. Те, кому заканчивать лень, могут просто загрузить исходный код. 🙂 Спасибо за внимание.

Исходный код к уроку можно скачать здесь.

Источник

Оцените статью