четверг, 10 января 2013 г.

Основы. Статья 5. Работа со слоями и касаниями в cocos2d

Основы cocos2d. Статья 5. Работа со слоями и касаниями в cocos2d

Итак, к этому моменту мы изучили сцены и работу с ними.
Как мы помним, каждая сцена состоит из "слоев", в которых располагаются конкретные игровые объекты и, соответственно, игровая логика.
Сам слой - это узел, который умеет принимать от устройства данные о касаниях пользователя и показания акселерометра.
Предлагаю узнать больше о слоях в этой статье.

CCLayer и работа с ним

Как я уже говорил, каждая сцена состоит из слоев - давайте разберемся, как добавляются слои в сцену.
Делается это достаточно просто:
+(id) scene {
CCScene* scene = [CCScene node];
CCLayer* backgroundLayer = [HelloWorldBackground node]; 
[scene addChild: backgroundLayer];
CCLayer* layer = [HelloWorld node]; 
[scene addChild:layer];
CCLayer* userInterfaceLayer = [HelloWorldUserInterface node]; [scene addChild: userInterfaceLayer];
return scene; 


Как мы помним, сцена обычно создается в статическом методе (id)scene одного из классов-слоев.
В нашем случае, мы создаем в этом методе сначала саму сцену, потом три слоя - слой фона (background layer), обычный слой (layer) и слой для интерфейса (userInterfaceLayer).
Эти слои приведены просто в качестве примера (в реальности каждый из слоев должен быть написан вами :) ).
Каждый слой после создания мы добавляем в сцену, передавая этот слой в методе addChild.
Отрисовка слоев на сцене происходит в том порядке, в котором они добавлены - то есть сначала будет нарисован фон, потом (сверху) объекты обычного слоя и "сверху" слой интерфейса.
Примерно так:

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

Хочется, кстати, напомнить, что в сцену не обязательно добавлять именно класс, происходящий от CCLayer. Можно добавить любой класс, происходящий от CCNode - фактически, если вам нужно просто объеденить несколько узлов в "группу", но при этом для этой группы вам не нужен ввод касаний или показания акселерометра (например, фоновый слой не использует эти данные), то можно вместо CCScene использовать CCNode.

Зачем может понадобиться несколько слоев?
Ответов много. Все зависит от конкретной игры. Например, в отдельном слое удобно держать "прокручивающийся" фон, не затрагивая другие, статичные узлы.
Также, есть интересный момент. Все трансформации (вращение, масштабирование, перемещение), применяемые к слою, автоматически применяются ко всем входящим в него узлам, поэтому если вам нужно оперировать одновременно группой узлов, возможно, имеет смысл объединить их в одну группу.

Еще один важный момент.
В принципе, не важно, сколько слоев вы используете в сцене - каждый слой берет ресурсов столько же, сколько обычный CCNode (ну, чуть больше, из-за доп. возможностей по распознаванию ввода). Но вот тех слоев, которые занимаются распознаванием ввода (жестов и акселерометра), должно быть минимум, более того настоятельно рекомендую, чтобы только один слой работал на распознавание. Если данные о касаниях или акселерометре нужны объектам из других слоев, можно реализовать их передачу другими методами (паттерн проектирования Listener (Слушатель), обычные вызовы performSelector или, например, встроенный в cocos2d класс CCSheduler - его мы обсудим в других статьях).

Обработка касаний

Чтобы ваш слой обрабатывал касания, его нужно об этом попросить.
Делается это просто:
self.isTouchEnabled = YES; 
Обычно это делается в классе init. Ну и, конечно, это свойство можно в любой момент поменять.
Как только мы попросим слой обрабатывать касания, cocos2d будет вызывать у нашего слоя 4 метода:
  • -(void)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event - вызывается, когда пользователь начал касание
  • -(void)ccTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event - вызывается каждый раз, когда пользователь переместил палец (или пальцы) по экрану
  • -(void)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event - вызывается, когда касания закончились
  • -(void)ccTouchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event - этот метод вызывается редко и почти всегда из него можно просто вызывать ccTouchesEnded
Во многих случаях, вам захочется знать, где именно произошло нажатие.
Для того, чтобы получить точку в координатах OpenGL, нужно выполнить следующий код:
-(CGPoint) locationFromTouches:(NSSet *)touches {
UITouch *touch = [touches anyObject];
CGPoint touchLocation = [touch locationInView: [touch view]]; return [[CCDirector sharedDirector] convertToGL:touchLocation];

}

передав туда touches. 
Обратите внимание на 1ую строку - вызов anyObject сгодится нам только если у нас одно касание.
Во второй строке мы получаем точку в координатах UIKit.
Если вам нужны координаты, принятые в UIKit, то возвращайте из этого метода touchLocation (которую вы получили во второй строке), а не конвертированные в GL координаты.

Опытные разработчики, конечно, обратили внимание, что по умолчанию слой получает все те же самые события, что и стандартный Apple-овский класс UIResponder.
Cocos2d также поддерживает таргетированные (целевые) обработчики нажатий, правда, в отличие от вышеприведенных методов, таргетированные обработчики работают только с одним касанием, а не с набором. Если касаний несколько, то обработчик разделяет их и обрабатывает по отдельности - в некоторых случаях, это упрощает вашу работу. Важнее всего здесь то, что вы получаете возможность редактировать очередь нажатий, указывая, например, что вы обработали то или иное касание и не хотите, чтобы оно передавалось в остальные слои.
Таким образом удобно проверять, находится ли то или иное касание в определенной зоне экрана - если да, то можно отметить его как обработанное и избавиться от проверок в других слоях.
Словом - этому можно найти хорошее применение :)

Чтобы добавить таргетированный обработчик, вставьте в ваш класс следующий код:

-(void) registerWithTouchDispatcher {
[[CCTouchDispatcher sharedDispatcher] addTargetedDelegate:self priority:INT_MIN+1
swallowsTouches:YES];

Важно - не оставляйте этот метод (registerWithTouchDispatcher) пустым - иначе ваш слой не будет получать вообще никаких сообщений.
Для того, чтобы не потерять обработчик по умолчанию, добавьте в начало вызов [super registerWithTouchDispatcher].

Итак, если вы добавите вышеуказанный код, то теперь будете cocos2d также будет вызывать четыре метода, но немножко другие:
-(BOOL) ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event {} 
-(void) ccTouchMoved:(UITouch *)touch withEvent:(UIEvent *)event {} 
-(void) ccTouchEnded:(UITouch *)touch withEvent:(UIEvent *)event {} 
-(void) ccTouchCancelled:(UITouch *)touch withEvent:(UIEvent *)event {} 

То есть теперь, вместо NSSet с набором касаний мы получаем одно касание UITouch.
Важно, что начало касания - это теперь метод, возвращающий BOOL.
Если из него вернуть YES, это будет значить, что вы не хотите, чтобы это касание передавалось другим обработчикам с более низким приоритетом - то есть вы "поглощаете" это касание в текущем обработчике (и кроме него, оно никуда больше не поступит).

В cocos2d нет встроенных распознавателей жестов, поэтому, если вам необходима эта функциональность - ее придется писать самостоятельно.
Благо, в интернете много информации на эту тему :)


На этом я заканчиваю сегодняшнюю статью. В следующей мы поговорим о другой интересной возможности слоев - о получении данных от акселерометров. Будет интересно! ;)



Иллюстрации и примеры кода взяты из книги Learn cocos2d Game Development with iOS 5.
Посетите также блог автора книги ("Learn Cocos2d", Steffen Itterheim)

2 комментария:

  1. Молодец, что делаешь уроки. А то русского ничего в интернете практически нету. Но, опять таки на этих уроках далеко не уедешь. Было бы не плохо, если бы добавил урок про структуру движка подробный. А так молодец. Респект.

    ОтветитьУдалить
    Ответы
    1. Благодарю!
      Сейчас я в основном придерживаюсь того, что дается в книжке, хочу основы раскрыть.

      Как только основы будут покрыты, у меня появится возможность "копнуть глубже" - тогда обязательно напишу более подробные детали. Тут, как говорится, всему свое время :)

      Удалить