iPhone: Создание игр для iOS (iPhone и iPad)
GameDev.ru / Сообщества / iPhone / Форум / Массив полиморфных объектов

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

Коля_РазработчикПостоялецwww5 июня 201219:19#0
Я занимаюсь программированием игры и сейчас нахожусь на стадии оптимизации рабочего кода.
Хотелось бы услышать совет от гуру 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];

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

flaberПостоялецwww5 июня 201219:41#1
я обычно делаю NSDictionary в котором ключь это тип объекта (аля enum { eRocket, eChuck};) а значение это NSMutableArray.
Кроме того у меня обычно два таких NSDictionary один с активными объектами другой с не активными это позволяет избижать перебора объектов при поиске неактивного и упрощает код (на мой взгляд).
alcoSHoLiKПостоялецwww5 июня 201222:52#2
Раз нужно иметь инстансы разных классов, созданные заранее, следовательно, надо заранее создать достаточное количество инстансов каждого типа. 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) не требует изменений.

Коля_РазработчикПостоялецwww5 июня 201223:39#3
Если я правильно понял, то самый простой вариант сводится к использованию двумерных массивов на основе NSDictionary, который служит для хранения и оптимизации доступа к массивам, созданных для отдельных типов пуль? Где каждый класс используется в качестве ключа доступа к массиву? И для наиболее эффективной обработки пуль, массивы активных и не активных пуль обмениваются данными.
alcoSHoLiKПостоялецwww6 июня 20128:26#4
Да, на мой взгляд это довольно простая и эффективная реализация. При большом количестве пуль, возможно, получишь выигрыш от замены _activeBullets на сет или связанный список. Однако прежде чем делать любые оптимизации, стоит прогнать игру через профайлер и убедиться в том, что это действительно узкое место в коде.
Коля_РазработчикПостоялецwww6 июня 201212:00#5
Я инициализирую 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];

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

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

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

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

alcoSHoLiKПостоялецwww6 июня 201223:22#7
Коля_Разработчик
Не могу понять, почему в качестве ключа используется [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
Коля_РазработчикПостоялецwww9 июня 201216:22#8
да опечатка. разобрался со всем, кроме того как получить классы в ран-тайме. xcode не находит objc_getClassList.
Там специальный include/framework нужен?
alcoSHoLiKПостоялецwww9 июня 201217:03#9
Коля_Разработчик
#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

Коля_РазработчикПостоялецwww9 июня 201221:00#10
так можно получить все классы протокола. но классов 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);

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

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

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

Коля_РазработчикПостоялецwww10 июня 201222:38#12
все, код упростил, всем спасибо. следующий вопрос, как метод с 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];
    }
  
}

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

/ Форум / iPhone: Создание игр / Программирование

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

2001—2018 © GameDev.ru — Разработка игр