Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

Sapper: Royal Engineer

Anna | 2.07.2014 | нет комментариев
В данном посте я расскажу «историю» разработки и публикации первой нашей игры: как рисовался дизайн, как разрабатывали, с какими сложностями столкнулись, отчего StackOverflow отличнее Apple Dev Forums и т.д.
Игра делалась с целью образования механизмов взаимодействия с дизайнером, для дальнейшего убыстрения разработки на больше трудных играх, следственно судите сурово (на столько, насколько это допустимо).Картинки для привлечения внимания:
imageimage

imageimage

С чего всё началось?

Так получилось, что на новой работе, по определенным причинам, пришлось заниматься разработкой игр для автоматов. Работал я с гениальным дизайнером, тот, что дюже крепко хотел разрабатывать другие игры (на мобильные платформы, под PC, XBox и т.д). Вот мы с ним и решили параллельно разработать что-то увлекательное, но в то же время не слишком трудное, Дабы наша разработка не затянулась на 4-5-6 месяцев. Ни я, ни он не были готовы к такому затяжному прыжку.

Сапёр был не той идеей за которую мы хотели с самого начала взяться.

Вот, за что мы хотели браться, но рады дюже, что своевременно правильно оценили наши силы:

image

image

image

image

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

На StackOverflow я видел, что дюже энергично поддерживается Cocos2D самим автором (LearnCocos2D), но мне дюже хотелось испробовать именно SpriteKit позже презентации Apple и показа игры Adventure. Огорчил меня правда тот факт, что в XCode встроена визуализация частиц, а я XCode хочу реже открывать.

Инструменты

На исходных этапах связка XCode AppCode, Photoshop.
Потом только AppCode и Photoshop.

Про SpriteKit дозволено посмотреть тут либо тут.

Дизайн (ретина, не ретина, 4 и 5 iPhone)

Я сразу был за то, Дабы 4 iPhone мы не поддерживали и не парились с еще кучей изображений, которых у нас и так было довольно из-за особенностей локализации приложения. Раз отказались от 4 и каждого, что ниже, значит рисовать нужно только под ретину — чудесно!

Вот, скажем, как выглядел наш 1-й вариант:

Дальше мы задавались приблизительно такими вопросами:

  • Должна ли быть реклама на игровом экране и закрывать игровое поле?
  • Рассматривать ли при отрисовке фонового изображения размеры и расположение рекламного блока?
  • Должна ли быть кнопка «Меню» либо же кнопка «Назад»?
  • Что показывать позже того, как пользователь выиграл либо проиграл?
  • и т.д.

Следующие теснее версии выглядели приблизительно так:

Давайте дизайнеру свободу, но контролируйте. Дело в том, что шрифта, тот, что он применял для надписей затраченного времени и кол-ва бомб на поле, нет в стандартном списке, значит нужно искать в интернете. Но это еще ничего, дело в том, что нужно еще обнаружить _правильный_ шрифт с учетом того, что за надписью находится фоновое изображение с 4 серыми цифрами с определенными расстояниями между ними, а в большинстве случаев мы сталкивались с тем, что при изменении надписи с «111» до «888» ширина текстовой надписи (UILabel) изменялась и менялось само расстояние между символами, что нас не устраивало… впрочем, необходимый шрифт был обнаружен, известность Всевышнему, напротив пришлось бы делать 10 изображений, позиционировать их и обновлять счетчик соответствующим образом. Казалось бы, примитивный шрифт, но увы, не всё так легко (в разделе «Разработка» расскажу, что еще увлекательного было со шрифтом этим).

Со спрайтами задач не было никаких. Огромнее каждого нам доставлял наслаждение вот данный переключатель:

Три вещи над которыми дизайнер дольше каждого работал:

  • Фоновое изображение основного экрана
  • Фоновое изображение экрана настроек
  • Анимация взрыва бомбы

Анимация взрыва бомбы содержит порядка 40 кадров (на скриншоте ниже два типа взрыва).

Столкнулись мы с дизайнером еще с одной проблемкой — позиционирование элементов и указание позиций. Для него это абсолютно не твердо, пиксель налево, пиксель вправо — не имеет значения… он рисует всё без линеек, уж такой вот творческий человек :)
Меня данный вариант вовсе не устраивал, потому что позиционировать мне как-то нужно, а значит необходимы хоть какие-то координаты/размеры.

Получилось как-то так:

Комфортно, но что-то тут не так, меня не покидает такое чувство.

Звуки

Со звуками тоже пришлось разбираться дизайнеру. Мы обнаружили подходящий сайт www.freesound.org/ и применяли некоторые звуки (вовсе без обработки не получилось — обрезание, фильтрация):

  • Взрыв
  • Откапывание
  • Нажатие на всякий «кнопочный» элемент

Разработка

Начиналось всё со сторибордов:

Закончилось:

Проектирование, проектирование, проектирование… херня полная. Создаешь каркас, а дальше на него начинаешь наворачивать функционал, логику, графику и т.д. От начала разработки до конца у меня конструкция плана не менялась, но схемы взаимодействия и логики работы контроллеров — уйма раз.

Начнем с основного экрана. Две кнопки, по тапу осуществляется переход на экран игры и в настройки. Барабанная дробь… по тапу еще проигрывается звук, а значит тут либо SystemSound, либо AVAudioPlayer (либо еще что-то), а значит необходима предзагрузка, а значит необходим еще какой-то класс, тот, что бы отвечал за предзагрузку всех звуков и их воспроизведение. Так и получилось — BGAudioPreloader.

@interface BGResourcePreloader : NSObject <AVAudioPlayerDelegate>

  (instancetype)shared;

// предзагружает аудио файл и готовит его к проигрыванию
- (void)preloadAudioResource:(NSString *)name;

// возвращает аудиопроигрыватель для воспроизведения аудио файла с именем name и
// растяжением type
// nil - если звуки отключены
- (AVAudioPlayer *)playerFromGameConfigForResource:(NSString *)name;

// возвращает аудиопроигрыватель для воспроизведения аудио файла с именем name и
// растяжением type. Не зависит от настроек звука
- (AVAudioPlayer *)playerForResource:(NSString *)name;

@end

Реализация вот такая:

//
//  BGResourcePreloader.m
//  Miner
//
//  Created by AndrewShmig on 4/5/14.
//  Copyright (c) 2014 Bleeding Games. All rights reserved.
//

#import "BGResourcePreloader.h"
#import "BGSettingsManager.h"

@implementation BGResourcePreloader
{
    NSMutableDictionary *_data;
}

#pragma mark - Class methods

static BGResourcePreloader *shared;

  (instancetype)shared
{
    static dispatch_once_t once;

    dispatch_once(&once, ^{
        shared = [[self alloc] init];
        shared->_data = [[NSMutableDictionary alloc] init];
    });

    return shared;
}

#pragma mark - Instance methods

- (void)preloadAudioResource:(NSString *)name
{
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        NSString *soundPath = [[NSBundle mainBundle]
                                         pathForResource:name
                                                  ofType:nil];
        NSURL *soundURL = [NSURL fileURLWithPath:soundPath];
        AVAudioPlayer *player = [[AVAudioPlayer alloc]
                                                initWithContentsOfURL:soundURL
                                                                error:nil];
        [player prepareToPlay];

        _data[name] = player;
    });
}

- (AVAudioPlayer *)playerFromGameConfigForResource:(NSString *)name
{
    //    звуки отключены
    if ([BGSettingsManager sharedManager].soundStatus == BGMinerSoundStatusOff)
        return nil;

    return [self BGPrivate_playerForResource:name];
}

- (AVAudioPlayer *)playerForResource:(NSString *)name
{
    return [self BGPrivate_playerForResource:name];
}

#pragma mark - AVAudioDelegate

- (void)audioPlayerBeginInterruption:(AVAudioPlayer *)player
{
    [player stop];
    player.currentTime = 0.0;
}

#pragma mark - Private method

- (AVAudioPlayer *)BGPrivate_playerForResource:(NSString *)name
{
    return (AVAudioPlayer *) _data[name];
}

@end

На основном экране огромнее ничего увлекательного.

Переходим к экрану настроек.

У нас здесь сразу UISegmentedControl (схожий) и переключатель (UIButton).
Перед тем, как писать велосипед с собственным UISegmentedControl я дюже скрупулезно порыл StackOverflow и осознал, что отличнее не наследоваться, а писать всё-таки велосипед… ничего трудного, но кое-какие особенности есть (механизм работы переключателя таков, что даже водя пальцев по нему, опция активая изменяется и зависит не только от того, где вы подняли палец, но и от того, где теперь ваш палец находится).

Основная обработка метаморфозы состояния переключателя выглядит дальнейшим образом:

#pragma mark - Touches

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self updateSegmentedControlUsingTouches:touches];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self updateSegmentedControlUsingTouches:touches];
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self updateSegmentedControlUsingTouches:touches];
}

#pragma mark - Private method

- (void)updateSegmentedControlUsingTouches:(NSSet *)touches
{
    UITouch *touch = [touches anyObject];
    CGPoint touchPoint = [touch locationInView:self];

    for (NSUInteger i = 0; i < _selectedSegments.count; i  ) {
        CGRect rect = ((UIImageView *) _selectedSegments[i]).frame;

        if (CGRectContainsPoint(rect, touchPoint)) {

            if (self.selectedSegmentIndex != i) {
                //    проигрываем звук нажатия - однажды и только на новом
                //                значении
                [[[BGResourcePreloader shared]
                                       playerFromGameConfigForResource:@"buttonTap.mp3"]
                                       play];
            }

            self.selectedSegmentIndex = i;

            break;
        }
    }

    [_target performSelector:_action
                  withObject:@(_selectedSegmentIndex)];
}

Вопросов не появляется.

Любимый наш элемент — переключатель. Сперва он работал как обыкновенная кнопка, но меня это непрерывно бесило потому, что ощущения не те и хочется Ощущать, что он подлинный и работает так, как это делает подлинный.

В результате получил дальнейший код для переключения между состояниями:

#pragma mark - Touches

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
//    не нужно обрабатывать это нажатие
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    BGLog();

    [self updateActiveRegionUsingTouches:touches];

    if ((self.isOn && self.activeRegion == BGUISwitchLeftRegion) ||
            (!self.isOn && self.activeRegion == BGUISwitchRightRegion)) {

        [super touchesMoved:touches withEvent:event];
        [self playSwitchSound];

        [_target performSelector:_action withObject:self];

        self.on = !self.on;
    }
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    BGLog();

    [self updateActiveRegionUsingTouches:touches];

    if ((self.isOn && self.activeRegion == BGUISwitchLeftRegion) ||
            (!self.isOn && self.activeRegion == BGUISwitchRightRegion)) {

        [super touchesEnded:touches withEvent:event];
        [self playSwitchSound];
        [_target performSelector:_action withObject:self];

        self.on = !self.on;
    }
}

- (void)updateActiveRegionUsingTouches:(NSSet *)touches
{
    UITouch *touch = [touches anyObject];
    CGPoint touchPoint = [touch locationInView:self];
    CGRect leftRect = CGRectMake(0, 0, self.bounds.size.width / 2, self.bounds.size.height);
    CGRect rightRect = CGRectMake(self.bounds.size.width / 2, 0, self.bounds.size.width / 2, self.bounds.size.height);

    if (CGRectContainsPoint(leftRect, touchPoint)) {
        _activeRegion = BGUISwitchLeftRegion;
    } else if (CGRectContainsPoint(rightRect, touchPoint)) {
        _activeRegion = BGUISwitchRightRegion;
    } else {
        _activeRegion = BGUISwitchNoneRegion;
    }
}

При всяком изменении состояния мы сберегаем новое значение настроек в администраторе настроек. Администратор настроек (тот, что в релизнутом приложении) получился дерьмовым, потом я написал новейший, но пока не заменил.

Вот начальный код нового администратора настроек:

static NSString* const kBGSettingManagerUserDefaultsStoreKeyForMainSettings = @"kBGSettingsManagerUserDefaultsStoreKeyForMainSettings";
static NSString* const kBGSettingManagerUserDefaultsStoreKeyForDefaultSettings = @"kBGSettingsManagerUserDefaultsStoreKeyForDefaultSettings";

// Class allows to work with app settings in a simple and flexible way.
@interface BGSettingsManager : NSObject

// Delimiters for setting paths. Defaults to "." (dot) character.
@property (nonatomic, readwrite, strong) NSCharacterSet *pathDelimiters;
// Boolean value which specifies if exception should be thrown if settings path
// doesn't exist or they are incorrect. Defaults to YES.
@property (nonatomic, readwrite, assign) BOOL throwExceptionForUnknownPath;

  (instancetype)shared;

// creates default settings which are not used as main settings until
// resetToDefaultSettings method is called
// example: [[BGSettingsManager shared] createDefaultSettingsFromDictionary:@{@"user":@{@"login":@"Andrew", @"password":@"1234"}}]
- (void)createDefaultSettingsFromDictionary:(NSDictionary *)settings;
// resets main settings to default settings
- (void)resetToDefaultSettings;
// clears/removes all settings - main and default
- (void)clear;

// adding new setting value for settingPath
// example: [... setValue:@YES forSettingsPath:@"user.personalInfo.married"];
- (void)setValue:(id)value forSettingsPath:(NSString *)settingPath;

// return setting value with specified type
- (id)valueForSettingsPath:(NSString *)settingsPath;
- (BOOL)boolValueForSettingsPath:(NSString *)settingsPath;
- (NSInteger)integerValueForSettingsPath:(NSString *)settingsPath;
- (NSUInteger)unsignedIntegerValueForSettingsPath:(NSString *)settingsPath;
- (CGFloat)floatValueForSettingsPath:(NSString *)settingsPath;
- (NSString *)stringValueForSettingsPath:(NSString *)settingsPath;
- (NSArray *)arrayValueForSettingsPath:(NSString *)settingsPath;
- (NSDictionary *)dictionaryValueForSettingsPath:(NSString *)settingsPath;
- (NSData *)dataValueForSettingsPath:(NSString *)settingsPath;

@end

Часть с реализацией:

//
// Copyright (C) 4/27/14  Andrew Shmig ( andrewshmig@yandex.ru )
// Russian Bleeding Games. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//

#import "BGSettingsManager.h"

@implementation BGSettingsManager
{
    NSMutableDictionary *_defaultSettings;
    NSMutableDictionary *_settings;
}

#pragma mark - Class methods

  (instancetype)shared
{
    static dispatch_once_t once;
    static BGSettingsManager *shared;

    dispatch_once(&once, ^{
        shared = [[self alloc] init];
        shared->_pathDelimiters = [NSCharacterSet characterSetWithCharactersInString:@"."];
        shared->_throwExceptionForUnknownPath = YES;

        [shared BGPrivateMethod_loadExistingSettings];
    });

    return shared;
}

#pragma mark - Instance methods

- (void)createDefaultSettingsFromDictionary:(NSDictionary *)settings
{
    _defaultSettings = [self BGPrivateMethod_deepMutableCopy:settings];

    [self BGPrivateMethod_saveSettings];
}

- (void)resetToDefaultSettings
{
    _settings = [_defaultSettings mutableCopy];

    [self BGPrivateMethod_saveSettings];
}

- (void)clear
{
    _settings = [NSMutableDictionary new];
    _defaultSettings = [NSMutableDictionary new];

    [self BGPrivateMethod_saveSettings];
}

- (void)setValue:(id)value forSettingsPath:(NSString *)settingPath
{
    NSArray *settingsPathComponents = [settingPath componentsSeparatedByCharactersInSet:self
            .pathDelimiters];
    __block id currentNode = _settings;

    [settingsPathComponents enumerateObjectsUsingBlock:^(id pathComponent,
                                                         NSUInteger idx,
                                                         BOOL *stop) {

        id nextNode = currentNode[pathComponent];

        BOOL nextNodeIsNil = (nextNode == nil);
        BOOL nextNodeIsDictionary = [nextNode isKindOfClass:[NSMutableDictionary class]];
        BOOL lastPathComponent = (idx == [settingsPathComponents count] - 1);

        if ((nextNodeIsNil || !nextNodeIsDictionary) && !lastPathComponent) {

            [currentNode setObject:[NSMutableDictionary new]
                            forKey:pathComponent];
        } else if (idx == [settingsPathComponents count] - 1) {

if ([value isKindOfClass:[NSNumber class]])
                currentNode[pathComponent] = [value copy];
            else
                currentNode[pathComponent] = [value mutableCopy];
        }

        currentNode = currentNode[pathComponent];
    }];

    [self BGPrivateMethod_saveSettings];
}

- (id)valueForSettingsPath:(NSString *)settingsPath
{
    NSArray *settingsPathComponents = [settingsPath componentsSeparatedByCharactersInSet:self
            .pathDelimiters];
    __block id currentNode = _settings;
    __block id valueForSettingsPath = nil;

    [settingsPathComponents enumerateObjectsUsingBlock:^(id obj,
                                                         NSUInteger idx,
                                                         BOOL *stop) {

//        we have a nil node for a path component which is not the last one
//        or a node which is not a leaf node
        if ((nil == currentNode && idx != [settingsPathComponents count]) ||
                (currentNode != nil && ![currentNode isKindOfClass:[NSDictionary class]])) {

            [self BGPrivateMethod_throwExceptionForInvalidSettingsPath];
        }

        NSString *key = obj;
        id nextNode = currentNode[key];

        if (nil == nextNode) {
            *stop = YES;
        } else {
            if (![nextNode isKindOfClass:[NSMutableDictionary class]])
                valueForSettingsPath = nextNode;
        }

        currentNode = nextNode;
    }];

    return valueForSettingsPath;
}

- (BOOL)boolValueForSettingsPath:(NSString *)settingsPath
{
    return [[self valueForSettingsPath:settingsPath] boolValue];
}

- (NSInteger)integerValueForSettingsPath:(NSString *)settingsPath
{
    return [[self valueForSettingsPath:settingsPath] integerValue];
}

- (NSUInteger)unsignedIntegerValueForSettingsPath:(NSString *)settingsPath
{
    return (NSUInteger) [[self valueForSettingsPath:settingsPath] integerValue];
}

- (CGFloat)floatValueForSettingsPath:(NSString *)settingsPath
{
    return [[self valueForSettingsPath:settingsPath] floatValue];
}

- (NSString *)stringValueForSettingsPath:(NSString *)settingsPath
{
    return (NSString *) [self valueForSettingsPath:settingsPath];
}

- (NSArray *)arrayValueForSettingsPath:(NSString *)settingsPath
{
    return (NSArray *) [self valueForSettingsPath:settingsPath];
}

- (NSDictionary *)dictionaryValueForSettingsPath:(NSString *)settingsPath
{
    return (NSDictionary *) [self valueForSettingsPath:settingsPath];
}

- (NSData *)dataValueForSettingsPath:(NSString *)settingsPath
{
    return (NSData *) [self valueForSettingsPath:settingsPath];
}

- (NSString *)description
{
    return [_settings description];
}

#pragma mark - Private methods

- (void)BGPrivateMethod_saveSettings
{
    [[NSUserDefaults standardUserDefaults]
                     setValue:_settings
                       forKey:kBGSettingManagerUserDefaultsStoreKeyForMainSettings];
    [[NSUserDefaults standardUserDefaults]
                     setValue:_defaultSettings
                       forKey:kBGSettingManagerUserDefaultsStoreKeyForDefaultSettings];

    [[NSUserDefaults standardUserDefaults] synchronize];
}

- (void)BGPrivateMethod_loadExistingSettings
{
    id settings = [[NSUserDefaults standardUserDefaults]
                                   valueForKey:kBGSettingManagerUserDefaultsStoreKeyForMainSettings];
    id defaultSettings = [[NSUserDefaults standardUserDefaults]
                                          valueForKey:kBGSettingManagerUserDefaultsStoreKeyForDefaultSettings];

    _settings = (settings ? settings : [NSMutableDictionary new]);
    _defaultSettings = (defaultSettings ? defaultSettings : [NSMutableDictionary new]);
}

- (NSMutableDictionary *)BGPrivateMethod_deepMutableCopy:(NSDictionary *)settings
{
    NSMutableDictionary *deepMutableCopy = [settings mutableCopy];

    [settings enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
        if ([obj isKindOfClass:[NSDictionary class]])
            deepMutableCopy[key] = [self BGPrivateMethod_deepMutableCopy:obj];
        else
            deepMutableCopy[key] = obj;
    }];

    return deepMutableCopy;
}

- (void)BGPrivateMethod_throwExceptionForInvalidSettingsPath
{
    if (self.throwExceptionForUnknownPath)
        [NSException raise:@"Invalid settings path."
                    format:@"Some of your setting path components may intersect incorrectly or they don't exist."];
}

@end

Применять дюже легко и, как я потом узнал и осознал, комфортно:

//    CODE -- begin
    BGSettingsManager *settingsManager = [BGSettingsManager shared];

    [settingsManager createDefaultSettingsFromDictionary:@{
            @"user": @{
                    @"info":@{
                            @"name": @"Andrew",
                            @"surname": @"Shmig",
                            @"age": @22
                    }
            }
    }];

    [settingsManager resetToDefaultSettings];

    [settingsManager setValue:@" 7 920 930 87 56"
              forSettingsPath:@"user.info.contacts.phone"];

    NSLog(@"%@", settingsManager);

    [settingsManager clear];

    NSLog(@"%@", settingsManager);
//    CODE - end

В консоли получим такой итог:

2014-04-30 23:45:03.842 BGUtilityLibrary[13730:70b] {
    user =     {
        info =         {
            age = 22;
            contacts =             {
                phone = " 7 920 930 87 56";
            };
            name = Andrew;
            surname = Shmig;
        };
    };
}
2014-04-30 23:45:03.847 BGUtilityLibrary[13730:70b] {
}

Переходим к игровому экрану. Данный экран стал для меня по-настоящему «багряным»… дело в том, что в самом начале, при нажатии на кнопку «Играть», происходил переход на игровой экран и там, в viewDidLoad способе генерировалось и заполнялось поле (SKScene), но задержка была такой огромный, что пришлос

Источник: programmingmaster.ru

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB