iPhoneФорумПрограммирование

Массив полиморфных объектов

#0
19:19, 5 июня 2012

Я занимаюсь программированием игры и сейчас нахожусь на стадии оптимизации рабочего кода.
Хотелось бы услышать совет от гуру Objective C. У меня есть родительский класс:

@protocol BulletProtocol
-(void) shoot;
@end

@interface Bullet <BulletProtocol> {
BOOL active;
}
...
@end

И, к примеру, 2 дочерних:

@interface Chuck: Bullet
...
@end

@interface Rocket: Bullet
...
@end

и есть контролер-класс:

@interface BulletsManager
...
@end

Так вот вопрос такой - как эффективнее всего организовать управление массивом пуль, NSMutableArray *bullets, который создан заранее.
Этот массив по идее может содержать любые дочерние классы от класса Bullet. Примерно так выглядит инициализация массива:

        bullets = [[NSMutableArray alloc] initWithCapacity: max_bullets];
        for (int i = 0; i < max_bullets; i++)
            [bullets addObject: [[Bullet alloc] initWithGame: game]];

Изначально, все пули в массиве созданы в памяти, но не существуют в игре. То есть существуют "активные" и "не активные" пули. Чтобы запустить
полет пули (сделать ее активной) нужно вызвать метод [bullets[objectAtIndex: i] shoot] - у дочерних классов он разный (виртуализированный). При попадании
пуля становится не активной, и может быть использована заного. Как правильно использовать массив bullets, чтобы, к примеру, при выстреле из RocketLauncher
в bullets добавлялась Rocket, а когда Rocket становилась не активной, ее можно было заменить на другой класс Chuck.

Rocket *rocket = [[Rocket alloc] init];
[bullets replaceObjectAtIndex: i withObject: rocket];

Такой метод мне не подходит, т.к хотелось бы чтобы объекты не создавались в процессе игры.

#1
19:41, 5 июня 2012

я обычно делаю NSDictionary в котором ключь это тип объекта (аля enum { eRocket, eChuck};) а значение это NSMutableArray.
Кроме того у меня обычно два таких NSDictionary один с активными объектами другой с не активными это позволяет избижать перебора объектов при поиске неактивного и упрощает код (на мой взгляд).

#2
22:52, 5 июня 2012

Раз нужно иметь инстансы разных классов, созданные заранее, следовательно, надо заранее создать достаточное количество инстансов каждого типа. BulletsManager тогда будет выбирать нужные инстансы в зависимости от переданного типа. В качестве контейнера для этого дела можно использовать NSDictionary, как заметил flaber.

typedef Class BulletType;

@interface BulletManager {
  NSDictionary   *_bulletPools;
  NSMutableArray *_activeBullets;
}
- (Bullet *)activateBulletWithType:(BulletType)klass;
- (void)update:(float)dt;
@end

@implementation BulletsManager

- (id)init
{
    if ((self = [super init])) {
        // Используем класс в качестве ключа
        _bulletPools = [[NSDictionary alloc] initWithObjectsAndKeys:
            [NSMutableArray array], [BulletRocket class],
            [NSMutableArray array], [BulletChuck class],
            nil];

        // ...
        // Заполняем массивы в _bulletPools нужным количеством инстансов пуль
        // каждого типа
        // ...

        _activeBullets = [[NSMutableArray alloc] init];
    }
    return self;
}

- (void)dealloc
{
    [_bulletPools release];
    [_activeBullets release];
    [super dealloc];
}

- (Bullet *)activateBulletWithType:(BulletType)klass
{
    NSMutableArray *pool = [_bulletPools objectForKey:klass];
    if (pool.count == 0) {
        // Не создали достаточно инстансов сразу, создаем новый сейчас
        Bullet *bullet = [[klass alloc] init];
        [_activeBullets addObject:bullet];
        [bullet release];
        return bullet;
    }

    Bullet *bullet = [pool lastObject];
    [_activeBullets addObject:bullet];
    [pool removeLastObject];
    return bullet;
}

- (void)update:(float)dt
{
    for (unsigned i = 0; i < _activeBullets.count; /* empty */) {
        Bullet *bullet = [_activeBullets objectAtIndex:i];
        [bullet update:dt];

        // Внимательно с индексами: после удаление инстанса из _activeBullets,
        // индекс увеличивать не надо
        if ([bullet hasSurvedItsPurpose]) {
            NSMutableArray *pool = [_bulletPools objectForKey:[bullet class]];
            [pool addObject:bullet];
            [_activeBullets removeObjectAtIndex:i];
        } else {
            ++i;
        }
    }
}
@end
// Пример использования
BulletManager *bmg;
Bullet *b = [bmg activateBulletWithType:[BulletRocket class]];
[b shoot];
// ...
[bmg update:dt];

Простор для оптимизации остается:

  • Хранить тип пули в инстансах числом. Тогда можно _bulletPools сделать сишным массивом и использовать enum-тип для выбора пулов.
  • Реализовать легковесное копирование для пуль, тогда _activeBullets можно будет сделать сетом и избежать лишних перемещений объектов в цикле в методе -[BulletsManager update:]
  • Если у тебя в сумме меньше нескольких сотен пуль, то заморачиваться со скоростью стоит. Я бы порекомендовал сначала сделать так, чтобы работало безотказно, потом уже оптимизировать. С помощью функций или макросов можно обеспечить простое изменение реализации в будущем, например:

    BulletType bullet_rocket_type() { return [BulletRocket class]; }
    BulletType bullet_chuck_type() { return [BulletChuck class]; }
    
    BulletsManager *bmg;
    Bullet *bullet [bmg activateBulletWithType:bullet_rocket_type()];
    // ...
    
    
    // ***
    // Затем оптимизируем _bulletPools внутри BulletManager, заменяя его на массив
    // и переопределяем функции
    // ***
    
    // Вместо предыдущего typedef Class BulletType; ставим этот
    typedef enum {
        BulletTypeRocket,
        BulletTypeChuck,
    } BulletType;
    
    BulletType bullet_rocket_type() { return BulletTypeRocket; }
    BulletType bullet_chuck_type() { return BulletTypeChuck; }

    При этом ни интерфейс BulletManager, ни клиентский код (который использует методы BulletsManager) не требует изменений.

    #3
    23:39, 5 июня 2012

    Если я правильно понял, то самый простой вариант сводится к использованию двумерных массивов на основе NSDictionary, который служит для хранения и оптимизации доступа к массивам, созданных для отдельных типов пуль? Где каждый класс используется в качестве ключа доступа к массиву? И для наиболее эффективной обработки пуль, массивы активных и не активных пуль обмениваются данными.

    #4
    8:26, 6 июня 2012

    Да, на мой взгляд это довольно простая и эффективная реализация. При большом количестве пуль, возможно, получишь выигрыш от замены _activeBullets на сет или связанный список. Однако прежде чем делать любые оптимизации, стоит прогнать игру через профайлер и убедиться в том, что это действительно узкое место в коде.

    #5
    12:00, 6 июня 2012

    Я инициализирую NSDictionary так:

          bonuses_pools = [[NSDictionary alloc] initWithObjectsAndKeys:
                            [NSMutableArray array], [WeaponBonus class],
                            [NSMutableArray array], [HealthBonus class],
                            [NSMutableArray array], [TimeSlowBonus class],
                            [NSMutableArray array], [DevastationBonus class],
                            [NSMutableArray array], [FiredTyresBonus class],
                            nil];
           
            NSMutableArray *p = [bonuses_pools objectForKey: [WeaponBonus class]];
            for (int i = 0; i < max_weapon_bonuses; i++)
                p addObject: [[Weapon alloc] initWithGame: inGame];

    Но тогда для каждого класса приходится вручную писать создание. Можно как-нибудь упростить?

    #6
    14:39, 6 июня 2012

    Если все бонусы реализуют какой-то общий протокол, то первую часть можно решить так. С помощью objc_getClassList получаешь массив всех объявленных классов. Далее с помощью class_conformsToProtocol смотришь, какие классы реализуют протокол бонуса, таким образом получая массив классов бонусов. А дальше уже заполняешь свой bonuses_pools с помощью этого массива.

    Заполнение собственно массива для каждого бонуса решить можно так. В том же протоколе объявить классовый метод, который возвращает соответствующий этому бонусу класс. Для WeaponBonus это будет Weapon, для HealthBonus будет Health и т.д. Далее важно, чтобы у каждого класса (Weapon, Health) был одинаковый инициализатор, тот же initWithGame: . Как ты это сделаешь не сильно важно - протокол или сам заручишься, что в каждом классе будет такой метод.

    Собственно и все. Теперь все можно сделать в рантайме автоматически.

    #7
    23:22, 6 июня 2012

    Коля_Разработчик
    Не могу понять, почему в качестве ключа используется [WeaponBonus class], а объекты создаются класса Weapon. Предположим, что это опечатка, тогда создание можно и нужно упростить. Список классов бонусов можно получить в рантайме, как описал __SaM__, либо можно создать статичный массив с классами. Для простоты, предположим что наш список классов хранится в NSArray.

    // наш массив с классами бонусов
    NSArray *bonusClasses = ...;
    
    NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
    for (Class klass in bonusClasses) {
        unsigned count = [klass maxCount];
        NSMutableArray *pool = [[NSMutableArray alloc] initWithCapacity:count];
        for (unsigned i = 0; i < count; ++i) {
            BaseBonusClass *instance = [[klass alloc] initWitGame:inGame];
            [pool addObject:instance];
            [instance release];
        }
        [dict setObject:pool forKey:klass];
        [pool release];
    }
    
    // При желании можно присвоить [dict copy], если очень нужно иметь неизменяемый словарь
    bonuses_pools = dict;

    Метод класса maxCount легко определить в базовом классе бонусов и переопределять для подклассов, где это необходимо.

    @interface BaseBonusClass
    + (unsigned)maxCount;
    @end
    
    @implementation BaseBonusClass
    + (unsigned)maxCount
    {
        return max_bonus_count;
    }
    @end
    #8
    16:22, 9 июня 2012

    да опечатка. разобрался со всем, кроме того как получить классы в ран-тайме. xcode не находит objc_getClassList.
    Там специальный include/framework нужен?

    #9
    17:03, 9 июня 2012

    Коля_Разработчик
    #import <objc/runtime.h>

    Все файлы рантайма можно найти по пути такого вида:
    /Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS5.0.sdk/usr/include/objc/
    или
    /Applications/Xcode.app/Contents/Developer/<то же самое>

    λ ls /Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS5.0.sdk/usr/include/objc/
    message.h        objc-api.h       objc-auto.h      objc-exception.h objc-sync.h      objc.h           runtime.h
    #10
    21:00, 9 июня 2012

    так можно получить все классы протокола. но классов 2000. использование такого приема повторяется на стадии инициализации около 20 раз, т.е 40000 итераций.

           Protocol *protocol = @protocol(BonusProtocol);
            
            int numberOfClasses = objc_getClassList(NULL, 0);
            Class *classList = malloc(numberOfClasses * sizeof(Class));
            numberOfClasses = objc_getClassList(classList, numberOfClasses);
            
            for (int idx = 0; idx < numberOfClasses; idx++) 
            {
                Class class = classList[idx];
                if (class_getClassMethod(class, @selector(conformsToProtocol:)) && [class conformsToProtocol:protocol])
                {
                    NSLog(@"%@", NSStringFromClass(class));
                }
            }
            free(classList);

    Стоит ли оно того? или все-таки лучше вручную массивы классов вписать?

    #11
    22:11, 9 июня 2012

    Что значит 20 раз повторяется? Ты для 20 разных протоколов ищешь классы таким образом? Сделай все в одном цикле за раз, а там уже смотри, медленно это или нет.

    Стоит оно того или нет это уже тебе самому решать. Если оно не замедляет существенно загрузку игры, то можно и пожертвовать миллисекундами задержки при инициализации ради того, что не придется десяток массивов поддерживать, если я правильно тебя понял.

    #12
    22:38, 10 июня 2012

    все, код упростил, всем спасибо. следующий вопрос, как метод с n количеством параметров передать как аргумент.
    Есть сейчас такой код:

        NSMutableArray *pool = [elements objectForKey: inClass];
        
        if (pool.count == 0) {
            Bonus *bonus = [[inClass alloc] initWithGame: game];
            [bonus activatePosition: inPosition  // вот этот метод нужно передавать как аргумент
                         Parameters: inParameters];
            [active_elements addObject: bonus];
            [bonus release];
        }
        else {
            Bonus *bonus = [pool lastObject];
            [bonus activatePosition: inPosition
                         Parameters: inParameters];
            [active_elements addObject: bonus];
            [pool removeLastObject];
        }


    Хочу заменить на такой:

    [self addElement: inClass 
                 Selector: /* передаем метод */ ];

    И в родительском классе такой вот метод:

    -(void) addElement: (Class) inClass
              Selector: (SEL) inSelector
    {
    
        if (pool.count == 0) {
            
            CollectionElement *element = [[inClass alloc] initWithGame: game];
            [element performSelector: inSelector]; // вызываем переданный метод
            [active_elements addObject: element];
            [element release];
        }
        else {
            CollectionElement *element = [pool lastObject];
            [element performSelector: inSelector]; // вызываем переданный метод
            [active_elements addObject: element];
            [pool removeLastObject];
        }
      
    }
    #13
    0:36, 11 июня 2012

    Проблемы с получением селектора не вижу - @selector(methodWithArg1:arg2:arg3:) и все. Загвоздка тут только в вызове селектора. Тут два решения:
    1. Т.к. performSelector не поддерживает несколько аргументов, то обычно все аргументы засовывают в массив и передают единственным аргументом.
    2. Использовать NSInvocation.

    iPhoneФорумПрограммирование

    Тема в архиве.