четверг, 7 февраля 2013 г.

in app purchase cocos2d-x (покупки внутри приложения)


В этой статье я расскажу как написать код для  IN APP PURCHASE в COCOS 2d-x. (разработка   под MacOS на XCode 4.5)


Чтобы не было реплик, типа статья украдена и т.д. скажу:
Эта статья написана мной, она является результатом моих поисков решения проблемы с in app purchase для cocos2d-x. Естественно я перечитал массу западных форумов, потратил не один день,  в моей статье используются куски кода с этих форумов, но в отличии от них, эти "кусочки" правильно соеденены и работают, + добавленный мной код.


Как это работает... В интернете куча статей, про то как работает биллинг, однако постараюсь все-же написать пару слов об этом, чтобы было понятно.
Если вы хотите продавать контент (далее "продукты") внутри своей игры, то придется писать in app purchase. Задача не совсем сложная, но литературы на русском языке практически нет, а на английском, в основном, статьи для cocos2d objective-c и все они разрозненные и практически не работают.

Нам надо реализацию на C++, а не на Objective-C. Компилятор Objective-C и среда XCode позволяют писать на C++, но файл с реализацией класса должен иметь расширение .mm
Я нарисовал схему, взаимодействия и наследования классов, для реализации In app purchase:


StoreKit - apple framework для реализации IAP. Ее мы будем использовать.
IAPHelper_objc - это пользовательскй класс, который взаимодействует с StoreKit, для этого он реализует два протокола <SKProductsRequestDelegateSKPaymentTransactionObserver>,  мы используем синглтон данного класса. Написан он на objective c.
IAPWrapper - это класс - мост, между IAPHelper_objc и нашим C++ классом, который реализует ф-ции магазина в приложении. В моей игре этот класс (CMyShop) унаследован от CCLayer.
IAPCallback - это виртуальный класс, написан на C++. От него, так же, будет унаследован наш класс магазин (CMyShop) для получения ответов от сервера.
Как все работает...
Пользователь заходит в наш виртуальный магазин (объект класса CMyShop), в нем через объект класса IAPWrapper отсылаются запросы на сервер apple. Когда приходят асинхронные ответы от сервера, то вызываются соответствующие методы нашего класса- магазина, т.к. он реализует методы виртуального класса IAPCallback. В этих методах мы и обрабатываем покупки, неудачи и т.д.

Он сказал поехали и запил водой...

Шаг 1.Начнем отсюда: http://habrahabr.ru/post/84359/ В этой статье не плохо описан сам принцип биллинга и то, как в консоли разработчика создать запись о своей игре, добавить тестовых пользователей и "продукты", которые будем продавать. Без этого ничего не заработает.

Шаг 2. Надеюсь с первым шагом проблем не возникло, можно продолжать...
ИТАК... опишу бизнес процесс...
за биллинг отвечает библиотека StoreKit, которая  входит в набор стандартных библиотек для разработки под iOS, от компании Apple.
Значит ее необходимо добавить в свой проект: в Project Navigator, кликните на самом верхнем элементе дерева, это ваш проект. На панели редактора откроется окно с информацией и установками вашего проекта. Идем на вкладку Summary, прокручиваем окно вниз, находим окошко Linked Frameworks and Libraries, там внизу нажимаем на "+", добавляем StoreKit.framework.

Для осуществления биллинга я создал, (больше украл) несколько классов:

1) IAPHelper_objc -  это класс, который, непосредственно, взаимодействует с StoreKit.
Когда пользователь в вашей игре пытается что-то купить, storekit отправляет запрос на сервер apple, там он обрабатывается и возвращается ответ... За все это отвечает IAPHelper_objc.
IAPHelper_objc -  написан на objective_c.  Он фактически посылает запросы на сервер и получает ответы, а также "покупает продукты". Для всех нас, естественно, очевидно, что получать ответы от сервера класс должен асинхронно. А раз асинхронно, значит должен быть реализован механизм обратного вызова ф-ций...  Для реализации этой функциональности наш IAPHelper_objc должен имплементировать два протокола (это понятие obj-c, а на простом языке, это интерфейсы):

<SKProductsRequestDelegateSKPaymentTransactionObserver>


SKProductsRequestDelegate - это протокол для отправки запросов и получения ответов от сервера.
В нем объявлен всего один метод, этот метод вызывается после получения ответа от сервера.

- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response 
в этом ответе, в переменной,  response  приходит список доступных продуктов.

SKProductsRequest,  SKProductsResponse - эти типы, запроса и ответа, описаны в библиотеке StoreKit.


SKPaymentTransactionObserver - это протокол для осуществления покупок.

класс IAPHelpet_objc лучше  поместить в подгруппу ios  нашего проекта.

Создание синглтона IAPHelper_objc должно быть в методе didFinishLaunchingWithOptions,класса AppController. Он находится в группе ios, нашего проекта.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 
{
  ...
 // In-app purchases: the helper will be notified when the product purchase transactions come in
    [[SKPaymentQueue defaultQueue] addTransactionObserver:[IAPHelper_objc sharedHelper]];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(productPurchased:)      name:kProductPurchasedNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector: @selector(productPurchaseFailed:) name:kProductPurchaseFailedNotification object: nil];
}
Вот заголовок  класса IAPHelper_objc.h
//
//
//  IAPHelper_objc.h
//  Truck Adventure
//
//

#ifndef Truck_Adventure_IAPHelper_objc_h
#define Truck_Adventure_IAPHelper_objc_h

#import <foundation foundation.h>
#import "StoreKit/StoreKit.h"
#include "IAPCallback.h"

#define kProductsLoadedNotification         @"ProductsLoaded"
#define kProductPurchasedNotification       @"ProductPurchased"
#define kProductPurchaseFailedNotification  @"ProductPurchaseFailed"

@interface IAPHelper_objc : NSObject  {
    NSSet * _productIdentifiers;
    NSArray * _products;
    NSMutableSet * _purchasedProducts;
    SKProductsRequest * _request;
    iOSBridge::Callbacks::IAPCallback* latestIAPCallback;
}

@property (retain) NSSet *productIdentifiers;
@property (retain) NSArray * products;
@property (retain) NSMutableSet *purchasedProducts;
@property (retain) SKProductsRequest *request;

- (void)requestProducts;
- (id)initWithProductIdentifiers:(NSSet *)productIdentifiers;
- (bool)canMakePayments;
- (void)requestProductsWithCallback:(iOSBridge::Callbacks::IAPCallback*)callback;
- (void)buyProductIdentifier:(NSString *)productIdentifier;
- (int)getProductIndexByIdentifier:(NSString*)productIdentifier;
+(id) sharedHelper;
//- (void)buyProductIdentifier:(NSString *)productIdentifier;

@end

#endif

А вот и реализация
//
//  IAPHelper_objc.mm
//  Truck Adventure
//
//

#import "IAPHelper_objc.h"
#include "Const.h"

@interface IAPHelper_objc () 
@end

@implementation IAPHelper_objc 

@synthesize productIdentifiers = _productIdentifiers;
@synthesize products = _products;
@synthesize purchasedProducts = _purchasedProducts;
@synthesize request = _request;


+ (IAPHelper_objc *)sharedHelper
{
    static dispatch_once_t once;
    static IAPHelper_objc * sharedHelper;
    dispatch_once(&once,
        ^{
                NSSet * productIdentifiers = [NSSet setWithObjects:
                                              @"IDProduct1", // наш продукт 1
                                              @"IDProduct2", //  наш продукт 2
                                            nil];
                sharedHelper = [[self alloc] initWithProductIdentifiers:productIdentifiers];
        }
    );
    return sharedHelper;
}

- (id)initWithProductIdentifiers:(NSSet *)productIdentifiers{
 latestIAPCallback = nil;
    
 if ((self = [super init])) {
  _productIdentifiers = [productIdentifiers retain];
        
  // Check for previously purchased products
  NSMutableSet * purchasedProducts = [NSMutableSet set];
  for (NSString * productIdentifier in _productIdentifiers)
  {
   BOOL productPurchased = [[NSUserDefaults standardUserDefaults] boolForKey:productIdentifier];
   if (productPurchased)
   {
    [purchasedProducts addObject:productIdentifier];
    NSLog(@"Previously purchased: %@", productIdentifier);
   }
   NSLog(@"Not purchased: %@", productIdentifier);
  }
        
  self.purchasedProducts = purchasedProducts;
 }
 return self;
}

- (bool)canMakePayments
{
 return [SKPaymentQueue canMakePayments];
}

- (void)requestProductsWithCallback:(iOSBridge::Callbacks::IAPCallback*)callback
{
 latestIAPCallback = callback;
 [self requestProducts];
}

-(void) setNullCallback
{
    latestIAPCallback = nil;
}
- (void)requestProducts
{
 self.request = [[[SKProductsRequest alloc] initWithProductIdentifiers:_productIdentifiers] autorelease];
 _request.delegate = self;
 [_request start];
    
}

// productsRequest это ф-ция обратного вызова (протокол SKProductsRequestDelegate) , которая начинает работать после получения ответа от сервера
// запрос к серверу отправляет ф-ция requestProduct
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
    
    NSLog(@"Received products results...");
    self.products = response.products;
    self.request = nil;
    
    [[NSNotificationCenter defaultCenter] postNotificationName:kProductsLoadedNotification object:_products];
    
    if (self.products.count>0)
    {
        CCArray* items = CCArray::create();
        items->retain();
        
        NSArray * skProducts = response.products;
        for (SKProduct * skProduct in skProducts) {
            NSLog(@"Найден продукт: %@ %@ %0.2f",
                  skProduct.productIdentifier,
                  skProduct.localizedTitle,
                  skProduct.price.floatValue);
            iOSBridge::Callbacks::IAPItem* item = new iOSBridge::Callbacks::IAPItem();
            item->identification = skProduct.productIdentifier.UTF8String;
            item->name = std::string("noname");
            item->localizedDescription = skProduct.localizedDescription.UTF8String;
            item->localizedTitle = skProduct.localizedTitle.UTF8String;
            items->addObject(item);
        }
        if (latestIAPCallback)
            latestIAPCallback->productsDownloaded(items);
    }
    else if (latestIAPCallback)
            latestIAPCallback->productDonwloadFailed();
}

- (void)productsLoaded:(NSNotification *)notification {
    [NSObject cancelPreviousPerformRequestsWithTarget:self];
}

- (void)recordTransaction:(SKPaymentTransaction *)transaction {
    // Optional: Record the transaction on the server side...
}

- (void)provideContent:(NSString *)productIdentifier {
    
    NSLog(@"Toggling flag for: %@", productIdentifier);
    [[NSUserDefaults standardUserDefaults] setBool:TRUE forKey:productIdentifier];
    [[NSUserDefaults standardUserDefaults] synchronize];
    [_purchasedProducts addObject:productIdentifier];
    
    //[[NSNotificationCenter defaultCenter] postNotificationName:kProductPurchasedNotification object:productIdentifier];
    
}

- (void)completeTransaction:(SKPaymentTransaction *)transaction {
    
    NSLog(@"completeTransaction...");
    
    [self recordTransaction: transaction];
   // 
    [self provideContent: transaction.payment.productIdentifier];
    [[SKPaymentQueue defaultQueue] finishTransaction: transaction];
    NSString *product = transaction.payment.productIdentifier;
    std::string stdproduct =  std::string([product UTF8String]);
    if (latestIAPCallback) 
        latestIAPCallback->productPurchased(stdproduct);
    else
        myconst->bonus+=10000; //  завершаем покупку, если вышли из магазина
}

- (void)restoreTransaction:(SKPaymentTransaction *)transaction {
    
    NSLog(@"restoreTransaction...");
    
    [self recordTransaction: transaction];
    [self provideContent: transaction.originalTransaction.payment.productIdentifier];
    [[SKPaymentQueue defaultQueue] finishTransaction: transaction];
    NSString *product = transaction.payment.productIdentifier;
    std::string stdproduct =  std::string([product UTF8String]);
    if (latestIAPCallback)
        latestIAPCallback->productRestored(stdproduct);
}

- (void)failedTransaction:(SKPaymentTransaction *)transaction {
    
    if (transaction.error.code != SKErrorPaymentCancelled)
    {
        NSLog(@"Transaction error: %@", transaction.error.localizedDescription);
    }
    
    //[[NSNotificationCenter defaultCenter] postNotificationName:kProductPurchaseFailedNotification object:transaction];
    
    [[SKPaymentQueue defaultQueue] finishTransaction: transaction];
    NSString *product = transaction.payment.productIdentifier;
    std::string stdproduct =  std::string([product UTF8String]);
    if (latestIAPCallback)
        latestIAPCallback->productCanceled(stdproduct);
}

- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
{
    for (SKPaymentTransaction *transaction in transactions)
    {
        switch (transaction.transactionState)
        {
            case SKPaymentTransactionStatePurchased:
                [self completeTransaction:transaction];
                break;
            case SKPaymentTransactionStateFailed:
                [self failedTransaction:transaction];
                break;
            case SKPaymentTransactionStateRestored:
                [self restoreTransaction:transaction];
            default:
                break;
        }
    }
}

- (void)buyProductIdentifier:(NSString *)productIdentifier
{
    NSLog(@"Покупаем %@...", productIdentifier);
    
    for (SKProduct * product in _products)
    {
        if ([product.productIdentifier isEqualToString:productIdentifier])
        {
            SKPayment * payment = [SKPayment paymentWithProduct:product];
            [[SKPaymentQueue defaultQueue] addPayment:payment];
            break;
        }
    }
}

- (int)getProductIndexByIdentifier:(NSString*)productIdentifier
{
    NSLog(@"Ищем индекс для %@...", productIdentifier);
    int index=0;
    bool bFound = NO;
    for (SKProduct * product in _products)
    {
        if ([product.productIdentifier isEqualToString:productIdentifier]) {
            bFound=YES;
            break;
        }
        index++;
    }
    if (!bFound) index=-1;
    return index;
}


- (void)dealloc
    {
        [_productIdentifiers release];
        _productIdentifiers = nil;
        [_products release];
        _products = nil;
        [_purchasedProducts release];
        _purchasedProducts = nil;
        [_request release];
        _request = nil;
        [super dealloc];
    }
        
@end




не буду умничать, я сам не вникал в некоторые места этого кода. Разберетесь сами, если будет нужно. Просто скопируйте код класса в свой проект. Не забудьте изменить в методе sharedHelper ID-шники ваших продуктов, созданных на шаге 1. В данном случае - это IDProduct1 и IDProduct2.

Дальше смотрим класс IAPCallback, как уже упоминалось раньше, он написан на C++

#ifndef xxx_IAPCallback_h
#define xxx_IAPCallback_h

#include "cocos2d.h"


using namespace cocos2d;

namespace iOSBridge
{
 namespace Callbacks
 {
  class IAPItem : public CCObject
  {
        public:
   std::string identification;
   std::string name;
   std::string localizedTitle;
   std::string localizedDescription;
   float price;
  };
        
  class IAPCallback
  {
  public:
            // Callback когда загружена инфа о продуктах с itunes сервера
     virtual void productsDownloaded(CCArray* products) = 0;
           
            // Callback когда не загружена инфа о продуктах с itunes сервера
     virtual void productDonwloadFailed() = 0;
           
            // Callback когда транзакция успешна
            virtual void productPurchased(std::string productID)=0;
            
            // Callback когда транзакция восстановлена
            virtual void productRestored(std::string productID)=0;
            
            // Callback когда транзакция неуспешна
            virtual void productCanceled(std::string productID)=0;
            
    };
        

 }
}

#endif
Стоит отметить класс IAPItem - это запись об одном продукте, унаследован он CCObject, поэтому его можно использовать для работы с  CCArray.
В IAPCallback объявлено несколько виртуальных методов,  реализация которых в классе - потомке "магазин" (CMyShop) . Думаю назначение методов понятно из комментов.  Они вызываются асинхронно, при возникновении событий во время биллинга.

Дальше смотрим IAPWrapper. Наш мостик между C++ и Objective c. Для отправки запросов на сервер в классе - магазине, вызываются методы объекта класса IAPWrapper. А в этих методах вызовы перенапрявляются синглтону IAPHelper_objc. Файл имплементации IAPWrapper должен иметь расширение mm. Кстати как и файл имплементации CMyShop.
Вот объявление класса:




//
//  IAPWrapper.h
//  Truck Adventure
//
//
//

#ifndef __Truck_Adventure__IAPWrapper__
#define __Truck_Adventure__IAPWrapper__

#import "IAPHelper_objc.h"
#import "IAPCallback.h"

class IAPWrapper
{
public:
    // А можно ли покупать ? Некоторые юзеры выключают внутренние покупки.
    bool canMakePayments();
    void requestProducts(iOSBridge::Callbacks::IAPCallback* callback);
    void setNullCallback();
    void buyProductIdentifier(const std::string& productID);
    int getProductIndexByIdentifier(const std::string& productID);
};

#endif /* defined(__Truck_Adventure__IAPWrapper__) */

Что здесь скажешь... смотрите реализацию
//
//  IAPWrapper.mm
//  Truck Adventure
//
//
//

#include "IAPWrapper.h"

bool IAPWrapper::canMakePayments()
{
    return [[IAPHelper_objc sharedHelper] canMakePayments];
}

void IAPWrapper::requestProducts(iOSBridge::Callbacks::IAPCallback* callback)
{
    [[IAPHelper_objc sharedHelper] requestProductsWithCallback: callback];
}

void IAPWrapper::setNullCallback()
{
    [[IAPHelper_objc sharedHelper] setNullCallback ];
}


void IAPWrapper::buyProductIdentifier(const std::string& productID)
{
    NSString *nsID = [NSString stringWithCString:productID.c_str()
                                encoding:[NSString defaultCStringEncoding]];
    
    [[IAPHelper_objc sharedHelper] buyProductIdentifier:nsID];
}

int IAPWrapper::getProductIndexByIdentifier(const std::string& productID)
{
    NSString *nsID = [NSString stringWithCString:productID.c_str()
                                        encoding:[NSString defaultCStringEncoding]];
    int index = [[IAPHelper_objc sharedHelper] getProductIndexByIdentifier:nsID];
    return index;
}

Обратите внимание на метод requestProducts(iOSBridge::Callbacks::IAPCallback* callback). Сюда передаем ссыклу на наш класс- магазин, потомок IAPCallback. Теперь посмотрим класс CMyShop, я оставил только то, что относится к статье. В любом случае, вы сами будете разрабатывать свой магазин внутри игры. Объявление класса:
//
//  Shop.h
//  Truck Adventure
//
//
//

#ifndef __Truck_Adventure__Shop__
#define __Truck_Adventure__Shop__

#include "cocos2d.h"
#include "Const.h"
#include "CarGarage.h"
#include "IntroLayer.h"
#include "Utils.h"
#include "IAPCallback.h"
#include "IAPWrapper.h"


using namespace cocos2d;

class TAShop : public CCLayer, public iOSBridge::Callbacks::IAPCallback
{
public:
    TAShop();
    ~TAShop();
    static cocos2d::CCScene* scene();
private:
    ...

    void onEnter()
    ...
    CCArray* mProducts;
    CCArray* mStoreKitProducts;

    // IN APP PURCHASE
    void requestServer();
    void productsDownloaded(CCArray* products);
    void productDonwloadFailed();
    // Callback когда транзакция успешна
    void productPurchased(std::string productID);
    // Callback когда транзакция восстановлена
    void productRestored(std::string productID);
    // Callback когда транзакция неуспешна
    void productCanceled(std::string productID);
    // здесь покупка
    void dealMe(std::string productID);
    ...
};

#endif /* defined(__Truck_Adventure__Shop__) */

Вот реализация:
...
void TAShop::onEnter()
{
    ...
    // запрос продуктов
    requestServer(); // можно вызвать и в конструкторе
    CCLayer::onEnter();
}

void TAShop::requestServer()
{
    // запрос продуктов
    IAPWrapper* IAP = new IAPWrapper::IAPWrapper();
    IAP->requestProducts(this); //передаем указатель на себя для Callback вызовов своих методов.
    delete IAP;
}

void TAShop::productsDownloaded(CCArray* products)
{

   // ответ от сервера со списком продуктов получен
   ...
}

void TAShop::productDonwloadFailed()
{
    // нет ответа от сервера
    ...
}


// Callback когда транзакция успешна
void TAShop::productPurchased(std::string productID)
{
    CCLog("SUCCESS TRANSACTION: Product = %s", productID.c_str());
}
// Callback когда транзакция восстановлена
void TAShop::productRestored(std::string productID)
{

    CCLog("RESTORE TRANSACTION: Product = %s", productID.c_str());
}
// Callback когда транзакция неуспешна
void TAShop::productCanceled(std::string productID)
{

    CCLog("CANCELED TRANSACTION: Product = %s", productID.c_str());
}
// купить продукт
void TAShop::dealMe(std::string productID)
{
   ...
   IAPWrapper* IAP = new IAPWrapper();
   iOSBridge::Callbacks::IAPItem* item = 
        (iOSBridge::Callbacks::IAPItem*)mStoreKitProducts->objectAtIndex(productIndex); // индекс продукта в массиве
   IAP->buyProductIdentifier(item->identification);
   delete IAP;
}
Ну вот и все. Надеюсь, что эта статья поможет вам реализовать IAP.
Вот архив:
http://yadi.sk/d/2DRID8Ej4gwWi

понедельник, 21 января 2013 г.

Cocos2d-x Жрем память....


Если вы в тупике по поводу жора памяти в cocos2d-x, возможно эта статья вам поможет!

С недавнего времени начал писать под iOS, выбрал cocos2d-x. Скачал с http://www.cocos2d-x.org версию движка 2.0.4. Прочитал не мало инфы про него, прочитал англоязычную книжку cocos2d. И решил пора начинать... До недавнего времени я программировал на JAVA для OS Android. Использовал бесплатный движок AndEngine. Написал пару игрушек, первая - Охота Мюнгхаузена (первый блин комом) , вторая Truck advernture. Вот собственно: Мои игры.
Так вот, к чему все это... К тому, что вторая моя игра попала в топ на www.amazon.com. И начала приносить доход. Это и подтолкнуло меня переписать ее для iOS, чтобы опубликовать на AppStore. Все было хорошо, игрушка писалась, потихоньку, я почти закончил проект.  Пишу ее в XCode под MAC OS.  Но возникла проблема, приложение начало падать. Причем XCode ничего не говорил по этому поводу, кроме Finished Running Truck Adventure... Несколько дней прогонял ее в профайле (инструменты для разработчика, можно запустить свой проект для профайлинга), нашел пару незначительных ликов по 5 кб. В Allocations память, как будто и не девалась никуда... В Leaks, как уже говорил, нашел пару ликов... В общем продолжалось это неделю, так нечего и не сдвинулось с места. Перечитал кучу форумов,  ответа так и не нашел...
Но, буквально, вчера случайно наткнулся на один пост: http://stackoverflow.com/questions/13231570/is-it-normal-that-my-cocos2d-app-increase-real-memory-usage-every-second/13234008#13234008,  Там у человека та же проблема... Он натолкнул меня на мысль в профайле запустить Activity Monitor, до этого анализировал только память и лики...  

Вот оно!! В Activity Monitor видно, как мой процесс каждые 2 секунды отъедает по 200 кб. физической памяти!!!! непонятно куда!!!?? 

И начались пляски с бубном, закоментил всё в стартовом слое, жор так и не уменьшился.
В общем не буду долго писать, что я только не предпринимал, все в бестолку!
В архиве с движком также идут сэмплы. Очень полезные примеры... Так вот запускаю TestCpp - это демка возможностей, от авторов движка, с исходниками, в Activity Monitor память как вкопанная, ничего нигде не растет :)  Копирую свой стартовый слой в проект TestCpp,  подправил код запуска, теперь TestCpp стартует мой CCLayer... Все нормально, память не растет. Значит дело не в моем коде, а настройках проекта, я так полагаю. Несколько часов сравнивал настройки своего проекта и TestCpp, ничего из этого не вышло!
Но я решил свою проблему. Удалил все с демо проекта TestCpp и добавил туда файлы с классами и ресурсами, со своего проблемного проекта... Test Cpp переименовал в Truck Adventure вот так:http://stackoverflow.com/questions/5043066/change-name-of-iphone-app-in-xcode-4
И все в прядке.
Надеюсь вам поможет моя статья )))

среда, 2 января 2013 г.

Пример использования CCSAXParser для cocos2d-x



-->           Каждый программист, который хочет написать более-менее приличную игру, рано или поздно сталкивается с вопросом загрузки различных данных, к примеру игровых уровней.
Естественно, данный вопрос коснулся и меня. С недавнего времени я решил оставить AndEngine и перешел на Cocos2d-x, т.к. купил себе MAC BOOK PRO и скачал XCode. И глупо было-бы ограничиться написанием игр только под android. Тем более Cocos2d-x позволяет писать мультиплатформенные приложения для iOS, Android, Win32, MacOS не вдаваясь в подробности реализации API OpenGL.  Cocos2d-x это порт знаменитого движка Cocos2d. 2D-X написан на С++, соответственно и игры с применением этого движка пишутся на C++. Так как C++ был моим любимым языком со студенческих лет, то никаких сомнений не осталось! Этот движок - то что нужно для меня!

Итак, в этой статье я хочу рассказать, как использовать класс CCSAXParser. Этот класс написан нашим программистом Максимом Аксеновым и был включен в состав движка.
SAX парсинг - это последовательное чтение XML с вызовами CALLBACK ф-ций. Таким образом он является событийным парсером. События возникают каждый раз при чтении очередного элемента XML файла. Когда элемент поступает на вход парсера, он вызывает ф-цию обратного вызова OnStartElement. В своем обработчике данного события можно выполнить необходимые проверки и обычно получить массив атрибутов элемента. Когда на вход парсера поступает закрывающий тэг соотв. элемента, вызывается событие OnEndElement. В общем, об SAX  парсере написано много статей в интернете и, при желании, можно получить более подробную информацию об этом способе разбора XML.  После долгих поисков примеров применения SAX парсера в интернете, именно для Cocos2d-x, я ничего так и не нашел.  Мне повезло писать SAX парсинг ранее на JAVA, когда я писал  игру Truck adventure для android, таким образом, я уже имел опыт написания своего обработчика для SAX парсера. 
Итак начнем.
Вот как объявлены классы для парсинга в кокосе:
class CC_DLL CCSAXDelegator
{
public:
    virtual void startElement(void *ctx, const char *name, const char **atts) = 0;
    virtual void endElement(void *ctx, const char *name) = 0;
    virtual void textHandler(void *ctx, const char *s, int len) = 0;
};

class CC_DLL CCSAXParser
{
    CCSAXDelegator*    m_pDelegator;
public:
    CCSAXParser();
    ~CCSAXParser(void);

    bool init(const char *pszEncoding);
    bool parse(const char* pXMLData, unsigned int uDataLength);
    bool parse(const char *pszFile);
    void setDelegator(CCSAXDelegator* pDelegator);


    static void startElement(void *ctx, const CC_XML_CHAR *name, const CC_XML_CHAR **atts);
    static void endElement(void *ctx, const CC_XML_CHAR *name);
    static void textHandler(void *ctx, const CC_XML_CHAR *name, int len);
};
 


Из этого объявления понятно,  что наш класс, в котором  будет происходить парсинг, должен быть унаследован от CCSAXDelegator. А переопределенные нами методы startElement, endElement и textHandler и есть необходимые обработчики событий. Т.е. когда парсер во входных данных встретит начало элемента, соответственно будет вызвана ф-ция startElement(void *ctx, const CC_XML_CHAR *name, const CC_XML_CHAR **atts),
где name - это имя пришедшего  XML элемента, а в массиве atts - содержатся все атрибуты элемента, причем, сначала идет название атрибута, а потом его значение (key, value).
Соответственно, при закрывающем теге элемента будет вызвана ф-ция endElement

Теперь приведу примеры XML  и своего кода парсинга.


Мой XML файл имеет следующий формат:

<?xml version="1.0" encoding="utf-8"?> <landscape>
   
<level number="1-1" length="10000">
    <back backb="back_b" backf="back_f" />
    <danger name="danger_brevno1" x="5522" y="356" angle="0"
        type="revolute" x_axis="-1" y_axis="-1" speed="1" backgroup="true"
        torque="30000" motor="true" center_r="true"  w="241" h="241"/>
    <element name="danger_brevno5" x="9875" y="383" angle="0"  w="25" h="225"/>
    <element name="danger_brevno4" x="3472" y="273" angle="0"  w="269" h="25"/>
    <element name="tree1" x="7717" y="357" angle="0"  w="100" h="122"/>
    ...

</level>
...
</landscape>
 

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

class LandLoader: public CCSAXDelegator
{
public:
    LandLoader(CCLayer* _gameLayer,
               CCLayer* _backFrontLayer,
               CCLayer* _backBackLayer,
               b2World* _world,
               DangerObjectManager* _dom);
    ~LandLoader();
    void loadLevel(string namelevel);
private:
    const char* TAG_LEVEL     ="level";
    const char* TAG_NUMBER     ="number";
    const char* TAG_LENGTH    ="length";
    const char* TAG_NAME    ="name";
    const char* TAG_X         ="x";
    const char* TAG_Y         ="y";
    const char* TAG_W         ="w";
    const char* TAG_H         ="h";
    const char* TAG_ELEMENT ="element";
    const char* TAG_VULKAN     ="vulkan";
    const char* TAG_SLOWCPU ="slowcpu";
    const char* TAG_FAKE     ="fake";
    const char* TAG_ANGLE     ="angle";
    const char* TAG_BACK     ="back";
    const char* TAG_BACKB     ="backb";
    const char* TAG_BACKF     ="backf";
   
    // danger
    const char* TAG_DANGER="danger";
    const char* TAG_DANGERD="danger_dyn";
    const char* TAG_DANGERDB="danger_dyn_box";
    const char* TAG_DANGERDC="danger_dyn_circle";
    const char* TAG_TYPE = "type";
    const char* TAG_CENTERR ="center_r";
    const char* TAG_MOTOR="motor";
    const char* TAG_TORQUE="torque";
    const char* TAG_SPEED="speed";
    const char* TAG_X_ANCHOR="x_anchor";
    const char* TAG_Y_ANCHOR="y_anchor";
    const char* TAG_X_AXIS="x_axis";
    const char* TAG_Y_AXIS="y_axis";
    const char* TAG_X_LEFT="x_left";
    const char* TAG_Y_LEFT="y_left";
    const char* TAG_X_RIGHT="x_right";
    const char* TAG_Y_RIGHT="y_right";
    const char* TAG_BACKGROUP="backgroup";
    // bonus
    const char* TAG_BONUS  ="bonus";
    const char* TAG_BONUSTYPE  ="type";
    // finish
    const char* TAG_FINISH  ="finish";
    // check point
    const char* TAG_CHECKPOINT  ="checkpoint";
    // car part
    const char* TAG_CARPART="carpart";
    const char* TAG_TRUCK="truck";
    CCLayer*   gameLayer;
    CCLayer*   backBackLayer;
    CCLayer*   backFrontLayer;
    b2World*   world;
    DangerObjectManager* dangerManager;
    CCSAXParser* parser;
    bool bInLevel=false;
    void initBorder();
    //**************
   
    void createElement(CCLayer* landLayer,string name, float pX, float pY);
    void createDangerDynamic(CCLayer* landLayer,string name,float pX, float pY);
    void createBackBBackF(CCLayer* backBackLayer, CCLayer* backForeLayer);
    // parsing xml
    void startElement(void *ctx, const char *name, const char **atts);
    void endElement(void *ctx, const char *name);
    void textHandler(void *ctx, const char *s, int len);
   
    // get atts Key,Value
    const char* getValue(const char* szKey,const char** atts);
};



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

В конструкторе класса создаем парсер:

LandLoader::LandLoader(CCLayer* _gamelayer,CCLayer* _backFrontLayer,
                       CCLayer* _backBackLayer,b2World* _world,DangerObjectManager* _dom)
{

...
    parser = new CCSAXParser();
    parser->setDelegator(this);
};

 
В этом методе получаем полный путь к файлу уровней и начинаем процесс парсинга.
 
void LandLoader::loadLevel(string namelevel)
{
    const char* fname = CCFileUtils::sharedFileUtils()->fullPathFromRelativePath("landlite.xml");
    bInLevel=false;
    parser->parse(fname);
}


В процессе парсинга, с помощью данного метода, получаем значение нужного атрибута из массива атрибутов.  Написан он криво и может быть зациклен. Я привожу здесь пример без оптимизации данного метода. Перепишите его для себя сами.

const char* LandLoader::getValue(const char *szKey, const char **atts)
{
    const char* res = NULL;
    if (atts!=NULL)
    {
        bool bflag=false;
        int i=0;
        while (!bflag)
        {
            if (atts[i]!=NULL)
            {
                const char* key = atts[i];
                const char* value = atts[i+1];
                i+=2;
                if (strcmp(szKey, key)==0)
                {
                    res = value;
                    bflag=true;
                }
            }
        }
    }
    return res;
}

 

Далее идут необходимые нам методы:
Этот метод устанавливает флаг bInLevel, если на вход пришел тег TAG_LEVEL с атрибутом TAG_NUMBER  = "1-1" Это частный случай, только для примера.   
TAG_LEVEL, TAG_NUMBER - это константы, объявленные  в описании класса.

void LandLoader::startElement(void *ctx, const char *name, const char **atts)
{
   
    if (strcmp(name, TAG_LEVEL)==0&&!bInLevel)
    {
       if (strcmp(getValue(TAG_NUMBER,atts),"1-1")==0)
        {
            bInLevel = true;
            int nWidth = atoi(getValue(TAG_LENGTH,atts));
            Const::me()->levelWidth = nWidth;
            initBorder();
        }
    }
    else
    // back
    if (strcmp(name, TAG_BACK)==0&&bInLevel)
    {

        const char* backB = getValue(TAG_BACKB,atts);
        const char* backf = getValue(TAG_BACKF,atts);

    }
    else
    // element
    if (strcmp(name, TAG_ELEMENT)==0&&bInLevel)
    {
        float pY=0;
        float pX=atoi(getValue(TAG_X, atts));
        const char* value = getValue(TAG_Y,atts);
        if (strcmp(value,"bottom")==0) pY = Const::me()->SCREEN_HEIGHT-100;
        else pY = atof(value);
        const char* szname = getValue(TAG_NAME,atts);
        string name(szname);
        createElement(gameLayer,name,pX,pY);
    }
    else
        // DANGER DYNAMIC
        if (
            (strcmp(name,TAG_DANGERD)==0||
             strcmp(name,TAG_DANGERDB)==0||
             strcmp(name,TAG_DANGERDC)==0)&&bInLevel
        )
        {

            float pY=0;
            float pX=atoi(getValue(TAG_X, atts));
            const char* value = getValue(TAG_Y,atts);
            if (strcmp(value,"bottom")==0) pY = Const::me()->SCREEN_HEIGHT-100;
            else pY = atof(value);
            const char* szname = getValue(TAG_NAME,atts);
            string name(szname);
            createDangerDynamic(gameLayer,name,pX,pY);
        }
}
void LandLoader::endElement(void *ctx, const char *name){
    if (strcmp(name, TAG_LEVEL)==0&&bInLevel)
    {
        dangerManager->createDDs(gameLayer);
        bInLevel=false;
    }
}


 
void LandLoader::textHandler(void *ctx, const char *s, int len)
{
    CCLog("text handler");
}


Я думаю, кому надо разберутся в данном коде. И надеюсь, что эта статья поможет Вам написать свои парсеры для своих нужд. 

С наилучшими пожеланиями )





суббота, 13 октября 2012 г.

Поиск утечек памяти в приложениях Android



Поиск источников, инициализирующих сборщика мусора.

В моей игре Truck adventure во время игрового процесса в LogCat-е постоянно проскакивала строка о том что работет сборщик мусора.  В эти моменты наблюдались тормоза, что портило впечатление от игрового процесса. Я несколько дней бороздил в бескрайних просторах интернета, в надежде найти помощь в решении этой проблемы. Установил MAT, настроил чтобы после HPROF дампа он автоматически запускался. С помощью MAT  мне удалось найти несколько проблемных мест в своем приложении. В интернете много чего про него написано.
MAT надо использовать, если у Вас в приложении происходит постоянная утечка памяти, т.е. размер кучи растет со временем. Это грозит OutOfMmory exception! Недавно я пару дней оптимизировал свое приложение с помощью MAT и нашел несколько утечек памяти. Мне удалось их устранить и теперь во время игры размер кучи не меняется )
Лень сейчас об этом подробно писать, но как будет время обязательно напишу пост как я искал утечки с помощью MAT.

Но MAT мне не помог в поиске куска кода, из за которого постоянно работал GC, возможно не хватает опыта работы с ним.

Тогда я  поочередно комментировал блоки кода, которые работали в главном цикле. Т.к. в AndEngine весь игровой процесс необходимо реализовывать в onUpdate (в такте движка), я просто комментил один за одним блоки.
К примеру: Машина в моей игре, это экземпляр класса TheCar. Он созается после инициализации сцены, кнопок и загрузки всех элементов уровня.
1. Выключил "машинку", т.е. она не создавалась и не добавлялась на игровую сцену. Жор памяти продолжался.
2. Выключил MainHUD(на котором нарисованы элементы управления) , жор памяти не уменьшился.
Значит не машинка, не основной HUD не влияют на постоянный запуск сборщика мусора.
3. Остался 3-й блок, который учавствует в игровом процесс - это физический мир.
В игре я использую Box2d.  Во время игры, при возникновении контакта происходит вызов переопределенного обработчика события  beginContact.   В нем происходит проверка, что с чем  контактирует и соответствующие, дальнейшие вызовы.

вот как он выглядит:


@Override
public void beginContact(final Contact contact)
{
     Body checkedBody    = contact.getFixtureA().getBody();
     Body contactPartner = contact.getFixtureB().getBody();
     ...
}


Проблема была в нем. Когда я писал проверку контактов, то написал одну вспомогательную ф-цию checkContact , которая принимала произвольное число аргументов. Мне было удобно ее использовать.

К примеру:
произошло событие,  вызвался метод
я получил указатели на checkedBody и contactPartner и с помощью этой ф-ции мог выполнить проверку:
if (checkContact(checkedBody, contactPartner, "danger", "carwheel","carbasket","carcabin")
{
     ... здесь должна развалиться машинка ...
}
т.е.  произошел контакт объекта типа danger с  колесом, или кузовом, или кабиной машины

Обработчик вызывался на каждом такте. Соотвтественно dalvik постоянно создавала в памяти большие массивы строк из за этого и происходил жор!

Вот кусок кода, как не надо писать ф-ции в приложениях для ОС Android:
Красным выделены проблемные места!

private boolean checkContact(Body checkedBody, Body contactPartner, String cb, String ...strings )
{
boolean bFlag=false;
for (int i=0;i<strings.length;i++)
{
final String cn_cp = contactPartner.getUserData().getClass().getName().toLowerCase();
final String cn_cb = checkedBody.getUserData().getClass().getName().toLowerCase();
if (cn_cp.contains(strings[i].toLowerCase())&&cn_cb.contains(cb)) 
{
bFlag=true;
break;
}
}
return bFlag;
}


Т.е. видно, что на вход может передаваться произвольное количество строк... В этом и проблема!
Вообще все параметры в методах класса очень желательно объявлять final.
А вот код проверки, который работает на данный момент и весьма успешно!

...

final String cbName = checkedBody.getUserData().getClass().getName();
final String cpName = contactPartner.getUserData().getClass().getName();
if (
cbName.contains("Bonus")&&
(
     cpName.contains("Carwheel")||cpName.contains("Carcabin")||
     cpName.contains("Carframe")||cpName.contains("Carbasket")||
             cpName.contains("Caraxle")
) &&!GameManager.mTheCar.mIsDestroyed
)
{

        ... произошел контакт машинки и бонуса! ...
return;
}
...

И кстати сказать, - не используйте в своих играх, в главном цикле, ф-ции работы со строками, которые в результате создают копию строки. Даже uppercase, lowercase, concat и т.д. создают в памяти копию. Память не утекает, т.к. сборщик мусора удалит исходную строку. Но выполнение сборки во время игрового процесса недопустимо. 


воскресенье, 30 сентября 2012 г.

Очистка мира-box2d andengine



Andengine.
Удаление объектов world-box2d должно производиться в такте andengine.
Вот код.
сначала удаляются все тела, потом соединения


public class MyPhysicsWorld extends FixedStepPhysicsWorld {

...

public void clean() {
  boolean bFlagDestroy=true
  clearForces();
  clearPhysicsConnectors();
  Iterator<Body> allMyBodies = getBodies();
  while (allMyBodies.hasNext()) {
     bFlagDestroy=true;
     final Body currentBody = allMyBodies.next();
     destroyBody(currentBody);
  }
  Iterator<Joint> allMyJoints = getJoints();
  while (allMyJoints.hasNext()) {
     final Joint currentJoint = allMyJoints.next();
     destroyJoint(currentJoint);
  }
  reset();
}

...

 @Override
 public void onUpdate(float pSecondsElapsed) {
   if (mClean) {
      mClean=false;
      clean1();
   }
   super.onUpdate(pSecondsElapsed);
 }


}

Удаление спрайтов со сцены andengine


AndEngine, GLES 1.  Удаление спрайтов со сцены
Удаление спрайтов со сцены должно производиться в такте движка. Почему имеено в такте ?  Потому что  вызов Scene.attachChild(entity) добавляет entity  в
SmartList<IEntity>  класса Entity, от которого наследован класс  Scene.  Во время каждого такта сцена пробегает по списку и вызывает onUpdate для каждого элемента списка.  (Кстати, элемент может отказаться от получения данного уведомления путем вызова setIgnoreUpdate(true)).
Если мы удалим спрайт со сцены вне такта, то сцена об этом знать не будет и при следующем пробеге вывалится exception index out of bounds, т.к. список стал короче.

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

public class GameScene extends Scene {

public GameScene() {

}

public void setClean()
{
     mClean=true;
}

... 

public void clean() 
{
  this.clearTouchAreas();
  while (getChildCount() > 0) {
      Entity layer = (Entity) getChild(0);
      while (layer.getChildCount() > 0) {
           Entity obj = (Entity) layer.getChild(0);
           while (obj.getChildCount() > 0) {
              Entity sg_child = (Entity) obj.getChild(0);
              obj.detachChild(sg_child);
              sg_child.setIgnoreUpdate(true);
              sg_child = null;
           }
           layer.detachChild(obj);
           obj.setIgnoreUpdate(true);
           obj = null;
         }
         detachChild(layer);
         layer.setIgnoreUpdate(true);
         layer = null;
    }
}



@Override
protected void onManagedUpdate(float pSecondsElapsed) {
   if (mClean)
   {
      mClean=false;
      clean();
   }
   super.onManagedUpdate(pSecondsElapsed);
}

}