SpriteKit — Автослалом (начало) — Часть 15

Очень долго придумывал пример по SpriteKit, рассматривал темы космических кораблей, вертолетов и воздушных шаров, но таких примеров в сети много и повторяться не хочется. Тут вспомнилась древняя игра Электроника Автослалом:
autoslalom
Внизу находится автомобиль, который может занимать одну из трех позиций — слева,в центре и справа. На автомобиль сверху надвигаются барьеры. Игрок должен перемещать автомобиль, чтобы избежать столкновения с препятствиями. За каждый пройденный барьер +1 балл. Чем больше очков, тем выше скорость. В оригинале есть два вида сложности и три жизни, у нас этого не будет. И само собой сделаем все красивее, чем в оригинале.

1.Создадим проект и сделаем фон
1.1. Создайте обычный проект Single View Application, добавьте в него SpriteKit, в сториборде у главного UIView поменяйте класс на SKView. Подробнее описывать эти действия не буду, поскольку уже писал про них на этом сайте. Проверьте чтобы UIViewController был под 4-х дюймовый экран (iphone5 и выше). Поскольку вся графика у меня нарисована именно под 4-х дюймовый экран, поэтому и приложение будем делать под iphone5 и выше. Оставьте в настройке Device Orientation только Portrait.

1.2. Создадим сразу класс GameScene, наследник от SKScene. Добавьте в GameScene.h:

#import

а в ViewController.h добавьте

#import "GameScene.h"

1.3. Дооформим ViewController.m, добавим метод

- (void)viewWillAppear:(BOOL)animated
{
    GameScene* gamePlay = [[GameScene alloc] initWithSize:self.view.frame.size];
    SKView *spriteView = (SKView *) self.view;
    [spriteView presentScene: gamePlay];
}

1.4. В GameScene.m добавим метод

- (void)didMoveToView: (SKView *) view
{
    if (!createdFlag)
    {
        //сюда будем добавлять код для этого метода
        
        
        createdFlag=YES;
    }
}

и в GameScene.h добавим переменную createdFlag

@interface GameScene : SKScene
{
    BOOL createdFlag;
}
@end

1.5. Скачайте этот as_bg1.zip архив с фоном, облаками и солнцем. Перенесите все картинки в Images.xcassets и не забудьте их там перетащить в группу ретина.

1.6. В didMoveToView добавьте вызов метода

[self sceneBackGround];

1.7. И теперь добавьте в GameScene.m сам метод sceneBackGround

-(void)sceneBackGround
{
    //тут будем заполнять класс фона
}

1.8. Заполняем sceneBackGround, сначала сделаем голубой фон

SKSpriteNode *blueBg=[SKSpriteNode spriteNodeWithColor:[UIColor colorWithRed:0.42f green:0.73f blue:0.99f alpha:1.0f] size:self.frame.size];
[blueBg setPosition:CGPointMake(self.frame.size.width/2, self.frame.size.height/2)];
[self addChild:blueBg];

Запустите и проверьте на работоспособность
1scr

1.9. Добавим солнце и заставим лучи солнца вращаться. Добавьте код в метод sceneBackGround

SKSpriteNode *sun=[SKSpriteNode spriteNodeWithImageNamed:@"sun"];
[sun setPosition:CGPointMake(self.frame.size.width/2-28, self.frame.size.height-140)];
[sun setSize:CGSizeMake(self.frame.size.width+150, self.frame.size.width+150)];
[sun runAction:[SKAction repeatActionForever:[SKAction rotateByAngle: (-2*M_PI) duration:40.5]]];
[self addChild:sun];

Запустите и проверьте на работоспособность. Фон голубой и солнце должно вращаться
2scr

1.10. Теперь сделаем плывущие по небу облака. Для этого добавим пару методов в GameScene.m

- (SKSpriteNode *)newCloudNode:(int)y
{
    SKSpriteNode *cloud=[SKSpriteNode spriteNodeWithImageNamed:@"cloud"];
    [cloud setAnchorPoint:CGPointMake(0, 0)];
    [cloud setPosition:CGPointMake([self randomFloatBetween:-100 and:-35], y)];
    [cloud setSize:CGSizeMake(35, 23)];
    [cloud setName:@"cloud"];
    [cloud setScale:[self randomFloatBetween:0.5 and:1.3]];
    
    /////////////
    SKAction *moving = [SKAction sequence:@[
                                            [SKAction moveByX:self.size.width+105 y:0.0 duration:[self randomFloatBetween:10 and:25]],
                                            [SKAction runBlock:^{
        [cloud setPosition:CGPointMake(-33, y)];
        [cloud setScale:[self randomFloatBetween:0.5 and:1.3]];
    }]
                                            ]];
    
    [cloud runAction: [SKAction repeatActionForever:moving]];
    
    return cloud;
}


-(float) randomFloatBetween:(float) min and:(float) max
{
    float random =  ((rand()%RAND_MAX)/(RAND_MAX*1.0))*(max-min)+min;
    return random;
}

Тут все просто. Облака плывут слева на право, на разной высоте и разного размера.

1.11. Теперь добавим вызов новых методов в sceneBackGround

[self addChild:[self newCloudNode:self.size.height-30]];
[self addChild:[self newCloudNode:self.size.height-70]];
[self addChild:[self newCloudNode:self.size.height-100]];
[self addChild:[self newCloudNode:self.size.height-130]];

Запускайте и проверяйте. Облака должны плыть.
3scr

1.12. И добавим для фона последний штрих в sceneBackGround

SKSpriteNode *mainBg=[SKSpriteNode spriteNodeWithImageNamed:@"bg"];
[mainBg setPosition:CGPointMake(self.frame.size.width/2, self.frame.size.height/2)];
[self addChild:mainBg];

Запускайте и проверяйте. Вот что должно получится:
4scr

1.13. Добавим метод с кнопкой Играть в GameScene.m

-(void)showPlayButton
{
    SKSpriteNode *playBtn=[SKSpriteNode spriteNodeWithImageNamed:@"play"];
    [playBtn setSize:CGSizeMake(100, 100)];
    [playBtn setPosition:CGPointMake(self.size.width/2, self.size.height/3)];
    [playBtn setName:@"PLAYBTN"];
    [playBtn setAlpha:0];
    [playBtn setZPosition:1];
    [playBtn runAction:[SKAction fadeAlphaTo:1 duration:0.3]];
    [self addChild:playBtn];
}

И добавим вызов этого метода в didMoveToView

[self showPlayButton];

Должно получиться так:
5scr

1.14. Теперь вставим в GameScene.m обработчик нажатий

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [touches anyObject];
    CGPoint location = [touch locationInNode:self];

    SKNode *node = [self nodeAtPoint:location];
    
    if ([node.name isEqualToString:@"PLAYBTN"]) {
        NSLog(@"PLAY PRESSED");
    }
}

Запустите приложение и нажмите кнопку Play. В консоле должно выскочить PLAY PRESSED

2. Эффект движения
2.1. При нажатии на Play по моей задумке, игра должна приходить в движение. По обочинам, вдоль дороги должны двигаться деревья, кустарники и фонари. Одновременно с движением они должны увеличиваться. Т.е. двигаться они должны типа как из далека. Казалось бы это не трудно сделать на спрайтките, что-то вроде того:

SKSpriteNode *tree=[SKSpriteNode spriteNodeWithImageNamed:@"derevo"];
[tree setPosition:startPoint];
[tree setScale:0.2];

SKAction *mov=[SKAction moveTo:finishPoint duration:2];
SKAction *sca=[SKAction scaleTo:1 duration:2];
SKAction *gr=[SKAction group:@[mov, sca]];
    
[tree runAction:gr];
[self addChild:tree];

Работать будет безобразно. Потому что скорость движения одинаковая, и спрайт увеличивается в это время. К концу анимации будет эффект замедления. А должно быть все наоборот. Советую вам попробовать сделать то о чем я написал, чтобы убедиться и понять.
Проанализировав ситуацию, я понял, что скорость спрайта должна быть переменной на всем пути. В начале пути медленно, а к концу анимации ускоряться. Поняв это, я попробовал

[mov setTimingMode:SKActionTimingEaseOut];

Но увы, эффект не настолько хорош и быстр. Поэтому пришлось использовать возможность написать кастомный SKAction.
Вот метод, добавьте его в GameScene.m:

-(SKAction*)perspectiveAction:(NSDictionary*)propertyDict
{
    SKAction *forFade=[SKAction fadeAlphaTo:1 duration:0.5];

    CGFloat dur=[[propertyDict objectForKey:@"DURATION"] floatValue];
    
    SKAction *scale=[SKAction scaleTo:[[propertyDict objectForKey:@"SCALETO"] floatValue] duration:dur];
    
    CGFloat x1=[[propertyDict objectForKey:@"X1"] floatValue]; //Начало X
    CGFloat y1=[[propertyDict objectForKey:@"Y1"] floatValue]; //Начало Y
    
    CGFloat x2=[[propertyDict objectForKey:@"X2"] floatValue]; //Конец X
    CGFloat y2=[[propertyDict objectForKey:@"Y2"] floatValue]; //Конец Y
    
    CGFloat dTot=sqrt( (x2-x1)*(x2-x1) + (y2 - y1)*(y2 - y1) )/dur;
    
    CGFloat v1=[[propertyDict objectForKey:@"V1"] floatValue]*dTot;
    CGFloat v2=[[propertyDict objectForKey:@"V2"] floatValue]*dTot;
    
    SKAction *myAct=[SKAction customActionWithDuration:dur actionBlock:^(SKNode *node, CGFloat elapsedTime) {
        
        CGFloat t = elapsedTime;
        
        CGFloat v=((abs(v2-v1))/(dur/t))+v2;
        
        CGFloat d=v*t;
        
        CGFloat x3=x1+d*(x2-x1) / sqrt( (x2-x1)*(x2-x1) + (y2 - y1)*(y2 - y1) );
        CGFloat y3=y1+d*(y2-y1) / sqrt( (x2-x1)*(x2-x1) + (y2 - y1)*(y2 - y1) );
        
        [node setPosition:CGPointMake(x3, y3)];
        
    }];
    
    SKAction *gr=[SKAction group:@[myAct, scale]];
    
    SKAction *goHome=[SKAction group:@[[SKAction fadeAlphaTo:0 duration:0],[SKAction moveTo:CGPointMake([[propertyDict objectForKey:@"X1"] floatValue], [[propertyDict objectForKey:@"Y1"] floatValue]) duration:0],[SKAction scaleTo:0.2 duration:0]]];
    
    
    SKAction *act=[SKAction sequence:@[forFade, gr, goHome]];
    
    return act;
}

Чтобы написать этот экшн я исписал много обычной бумаги. Вот краткая схема:
6scr
Итак, на входе имеем Точку начала A(x1,y1) и точку окончания движения B(x2,y2). Также имеем начальную скорость V1 и скорость окончания V2, изначально скорость задаем от 0 до 1. Также нам известно время, за которое должна произойти анимация и текущее время от начала анимации. По последнему параметру мы как раз и вычисляем, какая скорость должна быть у спрайта в этом месте пути. После того как скорость известна, вычисляем расстояние и координаты текущей точки и перемещаем туда спрайт. Параллельно высчитываем его «ЗУМ», т.к. спрайт еще и увеличиваться должен. Также чтобы вас не смущало, вначале добавлена полусекундная анимация-появление. Чтобы спрайт не резко появлялся, а плавно увеличивалась его альфа. И в конце добалена анимация goHome, которая возвращает спрайт вверх, делает его невидимым и спрайт ожидает дальнейших действий. В общем, если набросать все на бумаге, это не так сложно.

2.2. Скачайте этот as_roadside архив. Рисунки перенесите в Images.xcassets. Тут находятся Куст, Дерево, Биллборд, Фонарь левый и Фонарь правый.

2.3. Добавим метод начала игры в GameScene.m:

-(void)startGame
{
    
}

И вызов этого метода добавим в нажатие кнопки Play, вместо NSLog(@»PLAY PRESSED»); должно получиться так:

if ([node.name isEqualToString:@"PLAYBTN"]) {
   [self startGame];
   [node removeFromParent]; //удаление кнопки Play, ведь игра началась и она больше не нужна
}

2.4. Добавим метод, который добавляет на обочины нашей дороги движение:

-(void)addRoadsideObjects
{
    NSDictionary *propL=[[NSDictionary alloc] initWithObjectsAndKeys:
                         [NSNumber numberWithFloat:1.5],@"DURATION",
                         [NSNumber numberWithFloat:1],@"SCALETO",
                         [NSNumber numberWithFloat:90],@"X1",
                         [NSNumber numberWithFloat:433],@"Y1",
                         [NSNumber numberWithFloat:-90],@"X2",
                         [NSNumber numberWithFloat:100],@"Y2",
                         [NSNumber numberWithFloat:1],@"V1",
                         [NSNumber numberWithFloat:0.05],@"V2",
                         nil];
    SKAction *lPerspectiveAction=[self perspectiveAction:propL];
    
    
    
    NSDictionary *propR=[[NSDictionary alloc] initWithObjectsAndKeys:
                         [NSNumber numberWithFloat:1.5],@"DURATION",
                         [NSNumber numberWithFloat:1],@"SCALETO",
                         [NSNumber numberWithFloat:172],@"X1",
                         [NSNumber numberWithFloat:433],@"Y1",
                         [NSNumber numberWithFloat:380],@"X2",
                         [NSNumber numberWithFloat:100],@"Y2",
                         [NSNumber numberWithFloat:1],@"V1",
                         [NSNumber numberWithFloat:0.05],@"V2",
                         nil];
    SKAction *rPerspectiveAction=[self perspectiveAction:propR];
    
    
    CGPoint startPointLeft=CGPointMake([[propL objectForKey:@"X1"] floatValue],[[propL objectForKey:@"Y1"] floatValue]);
    CGPoint startPointRight=CGPointMake([[propR objectForKey:@"X1"] floatValue],[[propR objectForKey:@"Y1"] floatValue]);
    
    NSArray *imageNamesArray=[[NSArray alloc] initWithObjects:@"lfonar",@"kust",@"lfonar",@"derevo",@"lfonar",@"kust",@"lfonar",@"billboard", nil];
    
    
    SKAction *mainGrLeft=[SKAction repeatActionForever:lPerspectiveAction];
    SKAction *mainGrRight=[SKAction repeatActionForever:rPerspectiveAction];
    
    
    
    float i=0.0;
    for (NSString *str in imageNamesArray){
        
        SKSpriteNode *lObject=[SKSpriteNode spriteNodeWithImageNamed:str];
        [lObject setPosition:startPointLeft];
        [lObject setAlpha:0];
        [lObject setScale:0.20];
        [lObject setName:@"ROADSIDE"];
        [lObject runAction:[SKAction sequence:@[[SKAction waitForDuration:i], mainGrLeft]] withKey:@"ROADSIDE_ACTION"];
        [lObject setName:@"ROADSIDE_OBJECT"];
        [self addChild:lObject];
        
        SKSpriteNode *rObject=[SKSpriteNode spriteNodeWithImageNamed:str];
        [rObject setPosition:startPointRight];
        [rObject setAlpha:0];
        [rObject setScale:0.20];
        [rObject setName:@"ROADSIDE"];
        [rObject setName:@"ROADSIDE_OBJECT"];
        [rObject runAction:[SKAction sequence:@[[SKAction waitForDuration:i], mainGrRight]] withKey:@"ROADSIDE_ACTION"];

        [self addChild:rObject];
        
        i=i+0.25;
    };
}

и добавим вызов этого метода в startGame:

-(void)startGame
{
    [self addRoadsideObjects];
}

Должно быть все понятно тут, создаем два Dictionary для настроек кастомных SKAction, и применяем кастомные экшены к спрайтам которые создаются через цикл. Всего два направления — левая обочина и правая.
Запускайте и жмите Play. Play должен исчезнуть, а деревья, кусты и фонари должны появиться:
7scr

3. Добавим машину снизу
3.1 Вот архив as_car с картинками авто в трех положениях. Распакуйте и добавьте в проект.

3.2 Далее в startGame допишите добавления свайпов:

-(void)startGame
{
    [self addRoadsideObjects];
    
    UISwipeGestureRecognizer *swL = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(lSwipe:)];
    UISwipeGestureRecognizer *swR = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(rSwipe:)];
    
    [swL setDirection:UISwipeGestureRecognizerDirectionLeft];
    [swR setDirection:UISwipeGestureRecognizerDirectionRight];
    
    [self.view addGestureRecognizer:swL];
    [self.view addGestureRecognizer:swR];
}

и добавим обработчики свайпов:

-(void)lSwipe:(UISwipeGestureRecognizer *)tap
{
    NSLog(@"LEFT");
}

-(void)rSwipe:(UISwipeGestureRecognizer *)tap
{
    NSLog(@"RIGHT");
}

Запустите и попробуйте свайпать влево и вправо. В консоли должны появляться надписи.

3.3 Добавьте несколько глобальных переменных в GameScene.h:

SKSpriteNode *car;
int statecar;
int score;
SKTexture *carLeftTexture;
SKTexture *carCenterTexture;
SKTexture *carRightTexture;

Это спрайт самого автомобиля, его текущее состояние (statecar) и текстуры для трех режимов автомобиля.

3.4 Теперь вернемся в GameScene.m и метод для подготовки текстур:

-(void)prepareTextures
{
    carLeftTexture=[SKTexture textureWithImageNamed:@"car_left"];
    carCenterTexture=[SKTexture textureWithImageNamed:@"car_center"];
    carRightTexture=[SKTexture textureWithImageNamed:@"car_right"];
}

сделаем вызов этого метода из didMoveToView:

[self prepareTextures];

3.5 Напишем методы для работы с автомобилем:

- (SKSpriteNode *)newCarNode:(int)type
{
    car=[SKSpriteNode spriteNodeWithTexture:carCenterTexture];
    [car setSize:CGSizeMake(50, 120)];
    [car setScale:0.8];
    [car setName:@"CAR"];

    statecar=2;
    [self changeStateCar:statecar sprite:car];
    [car setZPosition:1];
    return car;
}


-(void)changeStateCar:(int)type sprite:(SKSpriteNode*)sprite
{
    if (type==1) {
        //left position
        [sprite setTexture:carLeftTexture];
        [sprite setPosition:CGPointMake(50, 120)];
        [sprite setSize:CGSizeMake(84, 119)];
        [sprite setScale:0.8];
    }else if (type==2){
        //center position
        [sprite setTexture:carCenterTexture];
        [sprite setPosition:CGPointMake(150, 120)];
        [sprite setSize:CGSizeMake(74, 119)];
        [sprite setScale:0.8];
    }else if (type==3){
        //right position
        [sprite setTexture:carRightTexture];
        [sprite setPosition:CGPointMake(255, 120)];
        [sprite setSize:CGSizeMake(85, 119)];
        [sprite setScale:0.8];
    }
    
}

Добавим вызов метода в startGame:

-(void)startGame
{
    score=0;
    [self addRoadsideObjects];
    
    UISwipeGestureRecognizer *swL = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(lSwipe:)];
    UISwipeGestureRecognizer *swR = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(rSwipe:)];
    
    [swL setDirection:UISwipeGestureRecognizerDirectionLeft];
    [swR setDirection:UISwipeGestureRecognizerDirectionRight];
    
    [self.view addGestureRecognizer:swL];
    [self.view addGestureRecognizer:swR];
    
    [self addChild: [self newCarNode:2]];


}

И исправим обработчики свайпов:

-(void)lSwipe:(UISwipeGestureRecognizer *)tap
{
    //NSLog(@"LEFT");
    if (statecar>1) {
        statecar=statecar-1;
        [self changeStateCar:statecar sprite:car];
    }
    
}

-(void)rSwipe:(UISwipeGestureRecognizer *)tap
{
    //NSLog(@"RIGHT");
    if (statecar<3) {
        statecar=statecar+1;
        [self changeStateCar:statecar sprite:car];
    }
}

Запускайте, жмите Play, проверяйте - машина должна перемещаться по свайпам:
8scr

В следующей части добавим барьеры, физику и геймовер.

По всем вопросам пишите в комменты.

P.S. Теперь можете следить за публикациями на сайте через социалки:
Facebook.com
Vk.com
Twitter.com

  • petruska

    Киньте пжл сайт откуда брали картинки