Я занимаюсь программированием игры и сейчас нахожусь на стадии оптимизации рабочего кода.
Хотелось бы услышать совет от гуру 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];
Такой метод мне не подходит, т.к хотелось бы чтобы объекты не создавались в процессе игры.
я обычно делаю NSDictionary в котором ключь это тип объекта (аля enum { eRocket, eChuck};) а значение это NSMutableArray.
Кроме того у меня обычно два таких NSDictionary один с активными объектами другой с не активными это позволяет избижать перебора объектов при поиске неактивного и упрощает код (на мой взгляд).
Раз нужно иметь инстансы разных классов, созданные заранее, следовательно, надо заранее создать достаточное количество инстансов каждого типа. 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];
Простор для оптимизации остается:
Если у тебя в сумме меньше нескольких сотен пуль, то заморачиваться со скоростью стоит. Я бы порекомендовал сначала сделать так, чтобы работало безотказно, потом уже оптимизировать. С помощью функций или макросов можно обеспечить простое изменение реализации в будущем, например:
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) не требует изменений.
Если я правильно понял, то самый простой вариант сводится к использованию двумерных массивов на основе NSDictionary, который служит для хранения и оптимизации доступа к массивам, созданных для отдельных типов пуль? Где каждый класс используется в качестве ключа доступа к массиву? И для наиболее эффективной обработки пуль, массивы активных и не активных пуль обмениваются данными.
Да, на мой взгляд это довольно простая и эффективная реализация. При большом количестве пуль, возможно, получишь выигрыш от замены _activeBullets на сет или связанный список. Однако прежде чем делать любые оптимизации, стоит прогнать игру через профайлер и убедиться в том, что это действительно узкое место в коде.
Я инициализирую 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];
Но тогда для каждого класса приходится вручную писать создание. Можно как-нибудь упростить?
Если все бонусы реализуют какой-то общий протокол, то первую часть можно решить так. С помощью objc_getClassList получаешь массив всех объявленных классов. Далее с помощью class_conformsToProtocol смотришь, какие классы реализуют протокол бонуса, таким образом получая массив классов бонусов. А дальше уже заполняешь свой bonuses_pools с помощью этого массива.
Заполнение собственно массива для каждого бонуса решить можно так. В том же протоколе объявить классовый метод, который возвращает соответствующий этому бонусу класс. Для WeaponBonus это будет Weapon, для HealthBonus будет Health и т.д. Далее важно, чтобы у каждого класса (Weapon, Health) был одинаковый инициализатор, тот же initWithGame: . Как ты это сделаешь не сильно важно - протокол или сам заручишься, что в каждом классе будет такой метод.
Собственно и все. Теперь все можно сделать в рантайме автоматически.
Коля_Разработчик
Не могу понять, почему в качестве ключа используется [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
да опечатка. разобрался со всем, кроме того как получить классы в ран-тайме. xcode не находит objc_getClassList.
Там специальный include/framework нужен?
Коля_Разработчик
#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
так можно получить все классы протокола. но классов 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);
Стоит ли оно того? или все-таки лучше вручную массивы классов вписать?
Что значит 20 раз повторяется? Ты для 20 разных протоколов ищешь классы таким образом? Сделай все в одном цикле за раз, а там уже смотри, медленно это или нет.
Стоит оно того или нет это уже тебе самому решать. Если оно не замедляет существенно загрузку игры, то можно и пожертвовать миллисекундами задержки при инициализации ради того, что не придется десяток массивов поддерживать, если я правильно тебя понял.
все, код упростил, всем спасибо. следующий вопрос, как метод с 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]; } }
Проблемы с получением селектора не вижу - @selector(methodWithArg1:arg2:arg3:) и все. Загвоздка тут только в вызове селектора. Тут два решения:
1. Т.к. performSelector не поддерживает несколько аргументов, то обычно все аргументы засовывают в массив и передают единственным аргументом.
2. Использовать NSInvocation.
Тема в архиве.