Шайбу вбросим в iOS восемь

в 14:15, , рубрики: Alex Ovechkin, game development, hockey, John Tavares, mobile development, table, Блог компании Papa Buba Diop, разработка под iOS, метки: , , ,

Прежде чем создавать казуальную игру для iOS, хорошо бы ответить на вопрос: — А зачем?
Вариантов три:

  • Срубить денег;
  • Порадовать родственников;
  • Хрен его знает, но мысль жжет организм изнутри.

image

Думаю, в ближайшие годы, правильный ответ — третий.
А, не буду спорить и учить — расскажу, как я делаю приложения.
Гуру разработки молча нажимают плюс и уходят в сторону. Остальные следуют за мной, чтобы вспомнить школу и настольный хоккей.
И да, уникальность топика, что в каждом предложении слова начинаются разными буквами.
В статье девять картинок и пол-минуты видео.

1. Идея

От неё зависит выбор инструментов. Мои идеи, как пользователи смартфонов — незамысловатые. Потому — никаких лишних инструментов для разработки, кроме библиотеки воспроизведения звуков. Ресурсы заимствую из сети. Мелодии, дизайн, изображения, примеры кода. Все уже создано до нас. Авторское право — как в хоккее или футболе. Допустим, Месси придумал новый финт. Вы можете его скопировать, не отчисляя автору денег.
Ладно. В качестве примера игры я выбрал настольный хоккей.

Очень личное

80-ые годы. На мех-мате эта забава была популярна. Мы играли в умывалке ФДС-6. Похвастаюсь звездным моментом — толпа народу соревновалась в ночь. Я зашел, дождался очереди, победил местного чемпиона 10-0 и скромно удалился. Если честно — у меня два брата и шестнадцать лет азартной практики.

2. Начало проекта

image
Рисунок 2. В Xcode завожу новый проект.

Кстати, у меня толстые пальцы. Значит, для игры подходит лишь iPad режим. Это существенно облегчает работу. Кроме того, запрещаю вращение экрана. Только портрет. Долой пейзажи, это хоккей, а не балет.
Выбираю название Hockey 2015. Названия из мира спорта — прибыльны. Насчет денег, я, конечно, слукавил. Хочется, чтобы будущий опус принес удовлетворение как душевное, так и материальное. Я уже писал здесь о своей игре Biathlon 2014. Каждый год — почти тысяча долларов навара. Покупают в дни чемпионата мира, олимпиады, кубковых этапов. Кто? Норвежцы, германцы, чехи, французы, русские, итальянцы.
Думаю, мой будущий хоккей ждет что-то похожее всего лишь из-за названия.

Выбираю иконку. Не так важно, как имя. Главное — чтобы самому нравилось.
image
Рисунок 3. Кто угадает, что это за хоккеист, тому- приз 1 доллар.

В результате, получается заготовка будущей игры, которая отображает ярко-серый экран. Проект имеет два основных файла, которые я могу редактировать.

  • ViewController.xib
  • ViewController.m

А не буду, плохой тон. Хороший тон не полениться и создать другой класс вида ViewController. Например, с именем PlayViewController.

image
Рисунок 4. Добавляю новый контроллер PlayViewController.

Проект имеет два новых файла, которые я всегда могу редактировать.

  • PlayViewController.xib
  • PlayViewController.m

3. Статичные изображения

Неподвижные изображения размещаю с помощью редактора в файле PlayViewController.xib.
Вначале их надо создать, украсть, позаимствовать. Фотографии хоккейного поля, игроков, вратарей мне прислал Milfgard. В Мосигре их есть. За что я без спросу разместил именной лейбл в центре поля. Шайба — из сети. Фотографии лиц хоккеистов — nhl.com. Кнопки — от дизайнеров Зептолаб. Хоккейные звуки — из приложения Ice Rage.
Теперь можно разместить изображения в редакторе xib-файлов. Это просто — все картинки отображается элементом UIImageView.

image
Рисунок 5. Хоккейное поле и табло в редакторе Xcode.

Если в дальнейшем потребуется анимация статично заданных элементов — не беда. Каждому элементу можно присвоить имя. В этом случае элемент можно программно двигать, гасить, трансформировать, что угодно.

Пример, объявляем элемент scoreBoard (черное табло на картинке) в файле PlayViewController.m

   IBOutlet UIImageView *scoreBoard;

Ключевое слово IBOutlet означает, что в редакторе XIB любому элементу можно присвоить идентификатор scoreBoard.
Присваиваю мышкой в редакторе.

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

  scoreBoard.center = CGPointMake(384, -1000);

Напоминаю, 384 — центр экрана iPad по ширине, -1000 — что-то вне устройства. После исполнения команды табло улетит вверх за границы экрана.

Если требуется двигать группу картинок — их надо объединить. Завести элемент типа UIView, чтобы переместить в него группу картинок и надписей.
image
Рисунок 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];
 

image
Рисунок 7. На корте появился хоккеист.

В дальнейшем я его буду перемещать и вращать. Вот так.

    player.center = CGPointMake(xnew, ynew); // перемещаю в точку (xnew=100, ynew=440)
    player.transform = CGAffineTransformMakeRotation(alpha); // кручу хоккеиста на угол (alpha=2.0) в радианах
 

image
Рисунок 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 пикселов) — клюшка.

Функция ниже, комментарии внутри.

puckMoving

       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

класс 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 тестеров на стадии ревизии приложения.

Не заходите, реклама

У кого есть iPad — шлите Apple ID, можете играть прямо завтра. Сегодня у меня хоккей, извините.

Спасибо, что дочитали.

Автор: PapaBubaDiop

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js