Прежде чем создавать казуальную игру для iOS, хорошо бы ответить на вопрос: — А зачем?
Вариантов три:
- Срубить денег;
- Порадовать родственников;
- Хрен его знает, но мысль жжет организм изнутри.
Думаю, в ближайшие годы, правильный ответ — третий.
А, не буду спорить и учить — расскажу, как я делаю приложения.
Гуру разработки молча нажимают плюс и уходят в сторону. Остальные следуют за мной, чтобы вспомнить школу и настольный хоккей.
И да, уникальность топика, что в каждом предложении слова начинаются разными буквами.
В статье девять картинок и пол-минуты видео.
1. Идея
От неё зависит выбор инструментов. Мои идеи, как пользователи смартфонов — незамысловатые. Потому — никаких лишних инструментов для разработки, кроме библиотеки воспроизведения звуков. Ресурсы заимствую из сети. Мелодии, дизайн, изображения, примеры кода. Все уже создано до нас. Авторское право — как в хоккее или футболе. Допустим, Месси придумал новый финт. Вы можете его скопировать, не отчисляя автору денег.
Ладно. В качестве примера игры я выбрал настольный хоккей.
2. Начало проекта
Рисунок 2. В Xcode завожу новый проект.
Кстати, у меня толстые пальцы. Значит, для игры подходит лишь iPad режим. Это существенно облегчает работу. Кроме того, запрещаю вращение экрана. Только портрет. Долой пейзажи, это хоккей, а не балет.
Выбираю название Hockey 2015. Названия из мира спорта — прибыльны. Насчет денег, я, конечно, слукавил. Хочется, чтобы будущий опус принес удовлетворение как душевное, так и материальное. Я уже писал здесь о своей игре Biathlon 2014. Каждый год — почти тысяча долларов навара. Покупают в дни чемпионата мира, олимпиады, кубковых этапов. Кто? Норвежцы, германцы, чехи, французы, русские, итальянцы.
Думаю, мой будущий хоккей ждет что-то похожее всего лишь из-за названия.
Выбираю иконку. Не так важно, как имя. Главное — чтобы самому нравилось.
Рисунок 3. Кто угадает, что это за хоккеист, тому- приз 1 доллар.
В результате, получается заготовка будущей игры, которая отображает ярко-серый экран. Проект имеет два основных файла, которые я могу редактировать.
- ViewController.xib
- ViewController.m
А не буду, плохой тон. Хороший тон не полениться и создать другой класс вида ViewController. Например, с именем PlayViewController.
Рисунок 4. Добавляю новый контроллер PlayViewController.
Проект имеет два новых файла, которые я всегда могу редактировать.
- PlayViewController.xib
- PlayViewController.m
3. Статичные изображения
Неподвижные изображения размещаю с помощью редактора в файле PlayViewController.xib.
Вначале их надо создать, украсть, позаимствовать. Фотографии хоккейного поля, игроков, вратарей мне прислал Milfgard. В Мосигре их есть. За что я без спросу разместил именной лейбл в центре поля. Шайба — из сети. Фотографии лиц хоккеистов — nhl.com. Кнопки — от дизайнеров Зептолаб. Хоккейные звуки — из приложения Ice Rage.
Теперь можно разместить изображения в редакторе xib-файлов. Это просто — все картинки отображается элементом UIImageView.
Рисунок 5. Хоккейное поле и табло в редакторе Xcode.
Если в дальнейшем потребуется анимация статично заданных элементов — не беда. Каждому элементу можно присвоить имя. В этом случае элемент можно программно двигать, гасить, трансформировать, что угодно.
Пример, объявляем элемент scoreBoard (черное табло на картинке) в файле PlayViewController.m
IBOutlet UIImageView *scoreBoard;
Ключевое слово IBOutlet означает, что в редакторе XIB любому элементу можно присвоить идентификатор scoreBoard.
Присваиваю мышкой в редакторе.
Теперь я могу сдвинуть картинку глубоко наверх, чтобы оно не загораживало хоккейное поле.
scoreBoard.center = CGPointMake(384, -1000);
Напоминаю, 384 — центр экрана iPad по ширине, -1000 — что-то вне устройства. После исполнения команды табло улетит вверх за границы экрана.
Если требуется двигать группу картинок — их надо объединить. Завести элемент типа UIView, чтобы переместить в него группу картинок и надписей.
Рисунок 6. Итак, я освободил поле от лишних элементов.
Теперь разместим здесь хоккеистов, чтобы научить их двигаться.
4. Трансформируемые изображения
Размещаем одного хоккеиста программно в файле PlayViewController.m
UIImageView *player = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"player_1.png"]];
float rPlayer = 150.0;
player.frame = CGRectMake(0, 0, rPlayer, rPlayer);
player.center = CGPointMake(100, 500);
[self.view addSubview:player];
Рисунок 7. На корте появился хоккеист.
В дальнейшем я его буду перемещать и вращать. Вот так.
player.center = CGPointMake(xnew, ynew); // перемещаю в точку (xnew=100, ynew=440)
player.transform = CGAffineTransformMakeRotation(alpha); // кручу хоккеиста на угол (alpha=2.0) в радианах
Рисунок 8. Хоккеист уехал в другое место и развернулся.
Хоккеистов много, а я один. Размещаем все 12 игроков с помощью массива.
NSMutableArray *players;
players = [[NSMutableArray alloc] init];
shadows = [[NSMutableArray alloc] init];
for (int k=0; k<12; k++) {
UIImageView *p = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"player_1"]];
p.frame = CGRectMake(0, 0, cellDx, cellDx);
float x = xp[k];
float y = yp[k];
p.center = CGPointMake(x, y);
p.transform = CGAffineTransformMakeRotation(ap[k]);
[players addObject:p];
p = [players objectAtIndex:k]; // так можем получить доступ к любому из 12 хоккеистов
[self.view addSubview:p];
}
5. Численные методы в хоккее
Хоккей, как жизнь, зависит от времени.
Необходимо завести счетчик временем. Событие, которое программа будет вызывать 3000 раз за минуту.
NSTimer *pauseTimer;
time = 0;
deltaTime = 1.0/50.0;
pauseTimer = [NSTimer scheduledTimerWithTimeInterval:deltaTime target:self selector:@selector(timerFunction) userInfo:nil repeats:YES];
- (void) timerFunction
{
// мы здесь бываем 50 раз в секунду
time = time + deltaTime;
[self renderPuck];
[self renderPlayers];
[self puckMoving:deltaTime];
}
Итак, все что мне осталось — расписать функцию puckMoving. В ней проверю столкновение шайбы и хоккеистов. Математически шайба задается окружностью радиусом 21 пиксел. Каждый хоккеист есть две окружности — побольше (радиус 25 пикселов) тело. Поменьше (радиус 5 пикселов) — клюшка.
Функция ниже, комментарии внутри.
for (int k=1; k<33; k++) {
if ( [self checkCollision:i With:k] ) {
[self resolve:i With:k];
}
}
-(int) checkCollision:(int) k1 With:(int) k2
{
float d2 = [self distance2:k1 With:k2];
float dr = rp2[k1] + rp2[k2];
return ( d2 < dr*dr ? 1 : 0);
}
-(float) distance2:(int) k1 With:(int) k2
{
float dxx = xp2[k1] - xp2[k2];
float dyy = yp2[k1] - yp2[k2];
return dxx*dxx + dyy*dyy;
}
-(void) resolve:(int) k1 With:(int) k2
{
float x1 = xp2[k1];
float y1 = yp2[k1];
float x2 = xp2[k2];
float y2 = yp2[k2];
float u2 = up2[k2];
float v2 = vp2[k2];
float u1 = up2[k1];
float v1 = vp2[k1];
Vector *b1Velocity = [Vector alloc];
[b1Velocity initX:u1 initY:v1];
Vector *b2Velocity = [Vector alloc];
[b2Velocity initX:u2 initY:v2 ];
float b1Mass = mp2[k1];
float b2Mass = mp2[k2];
Vector *vv = [Vector alloc];
[vv initX:x1-x2 initY:y1-y2];
float distance = [vv magnitude];
float min_distance = rp2[k1] + rp2[k2];
if (distance < min_distance) {
[vv mulScalar: ((0.1+min_distance-distance)/(distance)) ];
x1 += vv.x;
y1 += vv.y;
xp2[k1] = x1;
yp2[k1] = y1;
}
Vector *lineOfSight = [Vector alloc];
[lineOfSight initX:x1-x2 initY:y1-y2];
Vector *v1Prime = [b1Velocity vectorProjectionOnto:lineOfSight];
Vector *v2Prime = [b2Velocity vectorProjectionOnto:lineOfSight];
Vector *v1Prime2 = [Vector alloc];
[v1Prime2 copyVector:v2Prime];
[v1Prime2 mulScalar:(2*b2Mass)];
[v1Prime2 addVector:[v1Prime getMulScalar:(b1Mass - b2Mass)] ];
[v1Prime2 mulScalar:(1.0/(b1Mass + b2Mass))];
Vector *v2Prime2 = [Vector alloc];
[v2Prime2 copyVector:v1Prime];
[v2Prime2 mulScalar:(2*b1Mass)];
[v2Prime2 subVector: [v2Prime getMulScalar:(b1Mass - b2Mass)] ];
[v2Prime2 mulScalar:(1.0/(b1Mass + b2Mass))];
[v1Prime2 subVector:(v1Prime)];
[v2Prime2 subVector:(v2Prime)];
[b1Velocity addVector:v1Prime2];
[b2Velocity addVector:v2Prime2];
float a = 0.999;
up2[k1] = a*b1Velocity.x + (1.0-a)*b2Velocity.x;
vp2[k1] = a*b1Velocity.y + (1.0-a)*b2Velocity.y;
a = 1.0 - a;
// NSLog(@"new speed %f", hypotf(u, v)) );
}
Внутри функции используется класс Vector
#import "Vector.h"
@implementation Vector
@synthesize x,y;
-(void) initX:(float) setX initY:(float) setY {
x = setX;
y = setY;
}
-(void) copyVector:(Vector*) v{
x = v.x;
y = v.y;
}
-(void) addVector:(Vector*) v {
x += v.x;
y += v.y;
}
-(void) subVector:(Vector*) v {
x -= v.x;
y -= v.y;
}
-(void) mulScalar:(float) f {
x *= f;
y *= f;
}
-(float) magnitude {
return sqrt( x*x + y*y );
}
-(float) magnitude2 {
return x*x + y*y;
}
-(Vector *)getMulScalar:(float) f {
Vector *v = [Vector alloc];
[v initX:x*f initY:y*f];
return v;
}
-(float) scalarProjectionOnto:(Vector*) v {
return (x* v.x + y*v.y)/ [v magnitude];
}
-(Vector *) vectorProjectionOnto:(Vector*) v {
Vector *res = [v getUnitVector];
[res mulScalar: [self scalarProjectionOnto:v]];
return res;
}
-(Vector *) getUnitVector {
float len = [ self magnitude];
Vector *res = [Vector alloc];
[res initX:x initY:y];
if (len>0) {
len = 1.0/len;
[res mulScalar:len];
}
return res;
}
@end
При численном моделировании движения шайбы есть одна хитрость. 50 раз в секунду не хватает для точности моделирования. Я применяю элементарный трюк — удесятеряю число вызовов функции puckMoving, соответственно разбивая временной шаг на десять.
- (void) timerFunction
{
// мы здесь бываем 50 раз в секунду
time = time + deltaTime;
[self renderPuck];
[self renderPlayers];
for (int k=0; k<10; k++) [self puckMoving:deltaTime/10.0];
}
Смотрим, что получилось
6. Статистика знает все
Русские и американцы любят статистику. Поэтому я организовал турнир шести лучших команд мира.
Реальные игроки с nhl.com, голы, шайбы, очки, все звезды турнира.
7. Тестирование
Apple допускает 1000 тестеров на стадии ревизии приложения.
Спасибо, что дочитали.
Автор: PapaBubaDiop