Пишем простую игру под iOS с использованием cocos2d

в 17:18, , рубрики: cocos2d, game development, game-dev, ios development, разработка под iOS, метки: , ,

Введение

В данной статье будет создана простая игра под iOS с использованием cocos2d фреймворка. Ниже, я бы хотел поделиться своими познаниями в виде проекта. Я настоятельно рекомендую скачать исходники проекта (ссылка на BitBucket) в силу того, что в процессе написания поста мог что-то пропустить.

Установка cocos2d

В этом пункте, я не буду подробно останавливаться на процессе уставновки cocos2d. Стоит лишь привести 3 сслыки на статьи и видео урок:

Добавляем главного героя: первые строчки кода

Создание нового проекта и добавление текстуры главного героя

После того, как процесс установки прошел гладко, создаем новый проект в Xcode:Shift+N+Cmd или же File-New Project->cocos2d v2.x->cocos2d iOS. Называем проект RobotWars и сохраняем в любую папку.
После выполения нехитрых манипуляций, у вас должна появиться следующая картинка:
Пишем простую игру под iOS с использованием cocos2d
картинка кликабельна

В качестве текстуры (как для главного героя, так и для пуль, и врагов) будем использовать рисунок симпатишного зеленого мячикаimage. Загружаем текстуру и добавляем в исходный проект: File -> Add files to project ->Место где лежит загруженная текстура. После того, как вы добавили к проекту файл ball.png, последний у вас появится прям под названием вашего проекта.

Добавление текстуры главного героя на слой

Для того, что бы текстура появилась на экране, нужно добавить на слой (layer). Сначала объявим две переменные в интерфейсе класса HelloWorldLayer:

@interface HelloWorldLayer : CCLayer <GKAchievementViewControllerDelegate, GKLeaderboardViewControllerDelegate>
{
//for our hero
   CCSprite* robot;
//for screen size of iOS device
   CGSize screenSize;
}

Потом заменяем тело метода -(id)init на следующее:

// on "init" you need to initialize your instance
-(id) init
{
	// always call "super" init
	// Apple recommends to re-assign "self" with the "super's" return value
	if( (self=[super init]) ) 
     {    
        screenSize=[[CCDirector sharedDirector] winSize];    
        robot=[CCSprite spriteWithFile:@"ball.png"];
        robot.position=ccp(screenSize.width-10,screenSize.height/2);
        [self addChild:robot];    
    }
	return self;
}

Немного комментариев к коду:

  • robot=[CCSprite spriteWithFile:@"ball.png"] — создание и инициализация объекта картинкой мячика;
  • screenSize=[[CCDirector sharedDirector] winSize] — получение размера экрана;
  • robot.position=ccp(screenSize.width-10,screenSize.height/2); — установка точки отображения текстуры;
  • [self addChild:robot] — отображение на текущем слое текстуры.

После компиляции и запуска проекта на эмуляторе устройства, вы должны получить, что текстура будет «прижата» к середине правой стороны устройства.

Добавляем labels и джойстики

Добавляем labels

Начнем создание игрового интерфейса с самого простого — с добавления labels. Для этого, добавляем новые переменные в объявление интерфейса:

@interface HelloWorldLayer : CCLayer <GKAchievementViewControllerDelegate, GKLeaderboardViewControllerDelegate>
{
    Player* robot;
    CGSize screenSize;
    //variables below you must add. 
    CCLabelTTF* labelForNumberOfUsedBullets;
    CCLabelTTF* labelForNumberOfShotEnemies;
    CCLabelTTF* labelForNumberOfHelthRemained;
    CCLabelTTF* labelForStringDisplayNumberOfUsedBullets;
    CCLabelTTF* labelForStringDisplayNumberOfShotEnemies;
    CCLabelTTF* labelForStringDisplayNumberOfHealthRemained;
}

А в implementation (в теле условного перехода) пишем следующее:

        labelForNumberOfShotEnemies = [CCLabelTTF labelWithString:[NSString stringWithFormat:@"%d",0] 
                                                         fontName:@"Arial" 
                                                         fontSize:10.0];
        [labelForNumberOfShotEnemies setPosition: CGPointMake(screenSize.width-50, screenSize.height-10)];
        [self addChild:labelForNumberOfShotEnemies];
        labelForStringDisplayNumberOfShotEnemies = [CCLabelTTF labelWithString:@"Number of shot enemies" 
                                                                      fontName:@"Arial" 
                                                                      fontSize:10.0];
        [labelForStringDisplayNumberOfShotEnemies setPosition: CGPointMake(screenSize.width-116, screenSize.height-10)];
        [self addChild:labelForStringDisplayNumberOfShotEnemies];
        
        labelForNumberOfUsedBullets = [CCLabelTTF labelWithString:[NSString stringWithFormat:@"%d",numberOfShotBullets] 
                                                         fontName:@"Arial" 
                                                         fontSize:10.0];
        [labelForNumberOfUsedBullets setPosition: CGPointMake(screenSize.width-50, screenSize.height-20)];
        [self addChild:labelForNumberOfUsedBullets];
        labelForStringDisplayNumberOfUsedBullets = [CCLabelTTF labelWithString:@"Number of used bullets" 
                                                         fontName:@"Arial" 
                                                         fontSize:10.0];
        [labelForStringDisplayNumberOfUsedBullets setPosition: CGPointMake(screenSize.width-113, screenSize.height-20)];
        [self addChild:labelForStringDisplayNumberOfUsedBullets];
        
        labelForNumberOfHelthRemained = [CCLabelTTF labelWithString:[NSString stringWithFormat:@"%d",numberOfShotBullets] 
                                                         fontName:@"Arial" 
                                                         fontSize:10.0];
        [labelForNumberOfHelthRemained setPosition: CGPointMake(screenSize.width-50, screenSize.height-30)];
        [self addChild:labelForNumberOfHelthRemained];
        labelForStringDisplayNumberOfHealthRemained = [CCLabelTTF labelWithString:@"Number of health remained" 
                                                                      fontName:@"Arial" 
                                                                      fontSize:10.0];
        [labelForStringDisplayNumberOfHealthRemained setPosition: CGPointMake(screenSize.width-120, screenSize.height-30)];
        [self addChild:labelForStringDisplayNumberOfHealthRemained];

Комментарии к коду:

  • CCLabelTTF* labelForNumberOfSomething — объявление переменной типа «этикетка»;
  • labelForNumberOfShotEnemies = [CCLabelTTF labelWithString:[NSString stringWithFormat:@"%d",0]
    fontName:@"Arial"
    fontSize:10.0];
    — инициализация значения этикетки числом типа int;
  • labelForStringDisplayNumberOfShotEnemies = [CCLabelTTF labelWithString:@"Number of shot enemies"
    fontName:@"Arial"
    fontSize:10.0]
    — инициализация значения этикетки строчкой;
  • [labelForStringDisplayNumberOfShotEnemies setPosition: CGPointMake(screenSize.width-120, screenSize.height-30)]; — уставнока точки размещения этикетки (в данном случаей этикетка будет размещена вверху справа);

Вы можете спросить: «А почему товарищ топик-стартер не использовал конкатенацию строчек и вместо этого использовал две этикетки для хранения данных?». Как мне кажется, вышло маленькая, но все же оптимизация: не нужно постоянно создавать новую строку, достаточно просто изменить значение одной этикетки. Хотя, любые доводы в пользу того или иного решения будут приниматься.

Добавляем джойстики стрельбы и движения на экран

В своем проекте я использовал классы проекта SneakyJoystic и SneakyButtom. Скачиваем, распаковываем в папку с проектом. Для того, что бы отобразить джойстики на экране, добавляем следующие файлы в проект:
SneakyButton.h,SneakyButton.m, SneakyButtonSkinnedBase.h, SneakyButtonSkinnedBase.m, SneakyJoystick.h, SneakyJoystick.m, SneakyJoystickSkinnedBase.h, SneakyJoystickSkinnedBase.m.
После этого, добавлям текстуры для джойстика в исходный проект (шаги анлогичны тем, что и при добавлении текстуры для главного героя).
Далее, добавляем новые сущности в описание интерфейса:

    SneakyJoystick* leftJoystick;
    SneakyJoystick* rightJoystick;

В implementation (после -(id)init ) добавляем следующие методы:

-(void)addLeftJoystic
{
    SneakyJoystickSkinnedBase *joystickbase = [[[SneakyJoystickSkinnedBase alloc] init] autorelease];
    joystickbase.backgroundSprite = [CCSprite spriteWithFile:@"down.png"];
    joystickbase.thumbSprite = [CCSprite spriteWithFile:@"joystic.png"];
    joystickbase.joystick = [[SneakyJoystick alloc] initWithRect: CGRectMake(0, 0, 120,128)];
    joystickbase.position = ccp(55,55);
    [self addChild:joystickbase];                                           
    leftJoystick = [joystickbase.joystick retain];
}
-(void)addRightJoystic
{
    SneakyJoystickSkinnedBase *joystickbase = [[[SneakyJoystickSkinnedBase alloc] init] autorelease];
    joystickbase.backgroundSprite = [CCSprite spriteWithFile:@"down.png"];
    joystickbase.thumbSprite = [CCSprite spriteWithFile:@"joystic.png"];
    joystickbase.joystick = [[SneakyJoystick alloc] initWithRect: CGRectMake(0, 0, 40,48)];
    joystickbase.position = ccp(screenSize.width-55,55);
    [self addChild:joystickbase];                                           
    rightJoystick = [joystickbase.joystick retain];
}

Далее, внутри -(id)init в теле условного оператора, добавляем следующие строчки:

     [self addLeftJoystic];
     [self addRightJoystic];

После компиляции проекта, в правом и левом углах экрана должны появиться два джойстика. Разберем более детально новый код:

  • joystickbase.backgroundSprite — текстура, лежащая в основании джойстика;
  • joystickbase.thumbSprite — движимая текстура;
  • [self addLeftJoystic]; — обертка, которая позволяет добавить джойстик на слой HelloWorldLayer.

Движение главного героя

На данный момент, в проект добавлены 2 джойстика. Пусть leftJoystick используется для управления перемещением главного героя, а rightJoystick — для изменения вектора стрельбы и управлением событием «есть ли стрельба». Сказано, сделано.
Для управления перемещением создаем новый метод, код которого помещаем под -(id)init. Тело метода приведено ниже:

-(void)update:(ccTime)dt
{                         
    CGPoint schaledVelocity=ccpMult(leftJoystick.velocity, 240);
    CGPoint newposition=ccp(robot.position.x+dt*schaledVelocity.x,robot.position.y+schaledVelocity.y*dt);
    positionOfTheRobot=newposition;
    [robot setPosition:newposition];
}

Потом добавляем вызов данного метода в теле условного оператора:

    [self schedule:@selector(update:) interval:0.01];

Разбор написанного кода:

  • CGPoint schaledVelocity=ccpMult(leftJoystick.velocity, 240) — скалярное произведение скорости джойстика на вектор (240,24). Я изменил данное свойство для того, что бы движение было более плавными. Кто скажет, почему именно две координаты, тому я подарю печеньку=)
  • CGPoint newposition=ccp(robot.position.x+dt*schaledVelocity.x,robot.position.y+schaledVelocity.y*dt); — преобразовываем скорость в новые координаты;
  • [robot setPosition:newposition]; — перемещаем героя на новую позицию;
  • [self schedule:selector(update:) interval:0.01]; — вызываем метод update каждую 0.01 секунду для того, что вовремя обновлять позицию героя в зависимости от позиции джойстика.

Откомпилируйте данный проект, мышкой сэмулируйте касание на джойстик — в итоге герой должен плавно перемещаться по экрану.

Стрельба главного героя

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

-(CGPoint)returnPointWhereToShoot:(CGPoint)theCurrentPositionOfTheRobot and:(float)theJoystickAngle
{   
    CGPoint bufresult;
    
    bufresult.x=theCurrentPositionOfTheRobot.x+5000 * cos(theJoystickAngle*0.017453292519);
    bufresult.y=theCurrentPositionOfTheRobot.y+5000 * sin(theJoystickAngle*0.017453292519);
    
    return bufresult;
}

Добавляем методы для анимации движения пули (что бы быть как Рэмбо=)):

-(void)isReachedBottom: (id)sender
{
    CCSprite *sprite=(CCSprite *)sender;
    [self removeChild:sprite cleanup:YES];
}
-(void)animateBullets
{

    CCSprite* someBullet=[CCSprite spriteWithFile:@"ball.png"];
//start point for bullet animation
    someBullet.position=ccp(robot.position.x,robot.position.y);
    [self addChild:someBullet];
    
    //detect the finish line 
    CGPoint whereToMove;
    whereToMove=[self returnPointWhereToShoot:robot.position 
                                          and:rightJoystick.degrees];
    //start animating bullets
    CCMoveTo* moveTarget = [CCMoveTo actionWithDuration:5
                                               position:whereToMove];
    
    CCCallFuncN* actionForTargetMoveDidFinish = [CCCallFuncN actionWithTarget:self 
                                                                     selector:@selector(isReachedBottom:)];
    
    [someBullet runAction:[CCSequence actions:moveTarget, actionForTargetMoveDidFinish, nil]];  
    [labelForNumberOfUsedBullets setString:[NSString stringWithFormat:@"%d",numberOfShotBullets]];
    [arrayOfBullets addObject:someBullet];
}

Комментарии к коду:

  •     whereToMove=[self returnPointWhereToShoot:robot.position 
                                              and:rightJoystick.degrees];
    

    — определяем куда стрелять;

  •         CCMoveTo* moveTarget = [CCMoveTo actionWithDuration:5
                                                   position:whereToMove];
    

    — определяем как быстро и куда пуля будет лететь;

  •         CCCallFuncN* actionForTargetMoveDidFinish = [CCCallFuncN actionWithTarget:self 
                                                                         selector:@selector(isReachedBottom:)];    
    

    — делаем call-back функции isReachedBottom (попросту говоря, мы удаляем пулю с экрана по завершению ее полета).

В заключении, нужно определить повернул ли пользователь джойстик для стрельбы. Если да, то вызываем метод-(void)animateBullets. Код, который обрабатывает данное событие, нужно вставить под -(id)init:

-(void)isJoystickActivated:(ccTime)dt
{
    if(rightJoystick.velocity.x!=0&&rightJoystick.velocity.y!=0)
    {
        numberOfShotBullets++;
        [self animateBullets];
    }
}

Вызов данного кода нужно вставить в тело условного оператора:

    [self schedule:@selector(isJoystickActivated:) interval:0.1];

Добавляем врагов и анимацию их движения

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

Метод для определения случаной точки для отображения структуры врага:

-(void)spawnEnemies:(ccTime)dt
{
    int randomX= arc4random() % (int)(screenSize.width) ;
    int randomY= arc4random() % (int)(screenSize.height);
    [self addEnemyAtX:randomX Y:randomY];
}

Метод для отображения врага в заданной точке:

-(void)addEnemyAtX:(int)x Y:(int)y 
{
    CCSprite *enemy = [CCSprite spriteWithFile:@"ball.png"];
    enemy.position = ccp(x, y);
    [self addChild:enemy];
    [self animateEnemy:enemy];
    [arrayOfEnemies addObject:enemy];
}

Методы для анимации движения врага в сторону героя:

- (void) enemyMoveFinished:(id)sender 
{
    CCSprite *enemy = (CCSprite *)sender;
    
    [self animateEnemy: enemy];
}
- (void) animateEnemy:(CCSprite*)enemySprite
{
    // speed of the enemy
    ccTime actualDuration = 0.3;
    CGPoint diff = ccpSub(robot.position,enemySprite.position);
    float angleRadians = atanf((float)diff.y / (float)diff.x);
    float angleDegrees = CC_RADIANS_TO_DEGREES(angleRadians);
    float cocosAngle = -1 * angleDegrees;
    if (diff.x < 0) 
    {
        cocosAngle += 180;
    }
    enemySprite.rotation = cocosAngle;
    // Create the actions
    CCMoveTo* actionMove = [CCMoveBy actionWithDuration:actualDuration
                                        position:ccpMult(ccpNormalize(ccpSub(robot.position,enemySprite.position)), 10)];
    CCCallFuncN* actionMoveDone = [CCCallFuncN actionWithTarget:self
                                             selector:@selector(enemyMoveFinished:)];
    [enemySprite runAction:
    [CCSequence actions:actionMove, actionMoveDone, nil]];
}

Комментарии к двум последним методам:

  • комбинация CCCallFuncN* actionMoveDone = [CCCallFuncN actionWithTarget:self
    selector:selector(enemyMoveFinished:)]
    и [self animateEnemy: enemy]; позволяет создать рекурсию, с помощью которой враг будет двигаться даже когда его текущая позиция будет совпадать с позицией героя;
  • ccpMult(ccpNormalize(ccpSub(robot.position,enemySprite.position)), 10) задает относительное положение точки конечного движения, что позволяет врагу двигать в сторону героя, даже тогда когда последний движется по экрану.

Для того, что бы заработала анимация врагов, нужно добавить следующий метод в тело условного оператора -(id)init:

     [self schedule:@selector(spawnEnemies:) interval:1.0];

Убит ли врга?

Для определения подстрелян ли враг, воспользуемся пересечением описанного вокруг пули прямоугольника с описанным вокруг врага прямоугольником. Далее, в двойном цикле пройдемся по всем пулям и врагам и проанализируем с чем было пересечение. Для этого добавим 4 массива в описание интерфейса класса HelloWorldLayer.h:

    NSMutableArray *arrayOfBullets;
    NSMutableArray* arrayOfEnemies;
    NSMutableArray* arrayForShotEnemies;
    NSMutableArray* arrayForEnemiesWhichKilledHero;

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

-(void)checkCollisionOfEnemyWithBullet:(ccTime)dt
{
    for (CCSprite* anEnemy in arrayOfEnemies) 
    {
        for(CCSprite* aBullet in arrayOfBullets)
        {
            CGRect rectForBullet=CGRectMake(aBullet.position.x-(aBullet.contentSize.width)/2, 
                                            aBullet.position.y-(aBullet.contentSize.height)/2,
                                            aBullet.contentSize.width, 
                                            aBullet.contentSize.height);
            
            CGRect rectForEnemy=CGRectMake(anEnemy.position.x-(anEnemy.contentSize.width)/2, 
                                           anEnemy.position.y-(anEnemy.contentSize.height)/2,
                                           anEnemy.contentSize.width, 
                                           anEnemy.contentSize.height);
            
            if (CGRectIntersectsRect(rectForEnemy, rectForBullet)) 
            {
                [self removeChild:anEnemy cleanup:YES];
                [arrayForShotEnemies addObject:anEnemy];
                [labelForNumberOfShotEnemies setString:[NSString stringWithFormat:@"%d",[arrayForShotEnemies count]]];
            }
        }
    }
    for (CCSprite* anEnemy in arrayOfEnemies) 
    {
        CGRect rectForEnemy=CGRectMake(anEnemy.position.x-(anEnemy.contentSize.width)/2, 
                                       anEnemy.position.y-(anEnemy.contentSize.height)/2,
                                       anEnemy.contentSize.width, 
                                       anEnemy.contentSize.height);
        
        CGRect rectForRobot=CGRectMake(robot.position.x-(robot.contentSize.width)/2, 
                                       robot.position.y-(robot.contentSize.height)/2,
                                       robot.contentSize.width, 
                                       robot.contentSize.height);
        
        if (CGRectIntersectsRect(rectForEnemy, rectForRobot)) 
        {
            [arrayForEnemiesWhichKilledHero addObject:anEnemy];
            [labelForNumberOfHelthRemained setString:[NSString stringWithFormat:@"%d",[arrayForEnemiesWhichKilledHero count]]];
            NSLog(@"number of hero kills %d", [arrayForEnemiesWhichKilledHero count]);
        }
    }
    //remove enemies from created array of enemies
    for (CCSprite* anyShotEnemy in arrayForShotEnemies) 
    {
        [arrayOfEnemies removeObject:anyShotEnemy];
    }
    for (CCSprite* anyKiller in arrayForEnemiesWhichKilledHero) 
    {
        [arrayOfEnemies removeObject:anyKiller];
    }
}

Комментарии к коду:

  •             CGRect rectForBullet=CGRectMake(aBullet.position.x-(aBullet.contentSize.width)/2, 
                                                aBullet.position.y-(aBullet.contentSize.height)/2,
                                                aBullet.contentSize.width, 
                                                aBullet.contentSize.height);
    

    — создание описанного квадрата вокруг пули;

  • if (CGRectIntersectsRect(rectForEnemy, rectForBullet)) 
                {
                    [self removeChild:anEnemy cleanup:YES];
                    [arrayForShotEnemies addObject:anEnemy];
                    [labelForNumberOfShotEnemies setString:[NSString stringWithFormat:@"%d",[arrayForShotEnemies count]]];
                }
    

    — определяем было ли пересечение, если да то убираем спрайт врага, записываем его в массив подстрелянных мобов и обновляем этикетку со статитистикой убийств;

  •     for (CCSprite* anyShotEnemy in arrayForShotEnemies) 
        {
            [arrayOfEnemies removeObject:anyShotEnemy];
        }
    

    — удаляем подстрелянных мобов.

Ссылки на полезные статьи

Спасибо за прочтение данной статьи!

С удовольствием прочитаю любую критику в комментариях.

Автор: getbraine

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


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