Введение
В данной статье будет создана простая игра под iOS с использованием cocos2d фреймворка. Ниже, я бы хотел поделиться своими познаниями в виде проекта. Я настоятельно рекомендую скачать исходники проекта (ссылка на BitBucket) в силу того, что в процессе написания поста мог что-то пропустить.
Установка cocos2d
В этом пункте, я не буду подробно останавливаться на процессе уставновки cocos2d. Стоит лишь привести 3 сслыки на статьи и видео урок:
- ссылка на статью c хабра по уставновки cocos2d;
- ссылка на родной сайт cocos2d;
- видео — урок на английском языке.
Добавляем главного героя: первые строчки кода
Создание нового проекта и добавление текстуры главного героя
После того, как процесс установки прошел гладко, создаем новый проект в Xcode:Shift+N+Cmd или же File-New Project->cocos2d v2.x->cocos2d iOS. Называем проект RobotWars и сохраняем в любую папку.
После выполения нехитрых манипуляций, у вас должна появиться следующая картинка:
картинка кликабельна
В качестве текстуры (как для главного героя, так и для пуль, и врагов) будем использовать рисунок симпатишного зеленого мячика. Загружаем текстуру и добавляем в исходный проект: 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]
— инициализация значения этикетки числом типа int;
fontName:@"Arial"
fontSize:10.0];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]; }
— удаляем подстрелянных мобов.
Ссылки на полезные статьи
- iPhone and iPad game development with cocos2d
- описание почти такого же проекта на английском языке
- как использовать файлы из проекта Sneaky Joystic
Спасибо за прочтение данной статьи!
С удовольствием прочитаю любую критику в комментариях.
Автор: getbraine