TowerDefence #11. Вражеские волны

В сегодняшнем уроке мы разберем принцип работы вражеских волн и реализуем простую структуру данных для создания вражеских волн.

Если вы читаете этот урок, значит вы решили создать свою игру, а если вы решили создать свою игру — это значит, что вы играли в другие игры, и без меня отлично знаете, что такое вражеские волны с точки зрения игрока. Но когда речь заходит о создании вражеской волны в своей игре, то на вражеские волны приходится смотреть под новым углом с точки зрения разработчика.

Что такое вражеская волна?

Если смотреть с точки зрения игрока — то это просто новая пачка врагов, которая сейчас будет размазана в пух и прах. А если посмотреть с точки зрения разработчика — то это уже не просто пачка врагов, а нечто большое. Каждая вражеская волна — это, как минимум, набор разного типа врагов в заданном количестве, выходящих в атаку с определенным временным интервалом, и это только базовая основа. Давайте рассмотрим более детально то, какая информация нам понадобится для создания вражеской волны:

  1. Временной интервал между вражескими волнами, то есть перед началом очередной вражеской волны должна быть некая произвольная временная задержка.
  2. Чтобы волны были интересными, у нас должна быть возможность задавать разного вида противников в одной вражеской волне, а так же возможность указывать для каждого вида количество юнитов.
  3. Помимо типа и количества противников было бы здорово указывать и их порядок выхода.
  4. Так же между появлением врагов нужно задавать временной интервал, чтобы они не появлялись сразу в большом количестве, а выходили поочередно.

Таким образом получается так, что вражеская волна — это некий объект, который живет какое-то время и выполняет функции базы, которая как бы создает и отправляет в бой противников. Конечно, в случае с TD понятие базы будет абстрактным, но принцип реализации будет именно таким. Давайте приступим к её созданию.

EnemyWave.as

Создаем новый класс EnemyWave.as и размещаем его в пакете (папке) com.towerdefence. В классе у нас будет одна публичная переменная:

// Задержка перед началом вражеской волны
public var startDelay:Number = 0;

А так же несколько приватных переменных необходимых для внутренней работы вражеской волны:

// Указатель на игровой мир
private var _universe:Universe;
// Список врагов в волне
private var _enemies:Array = [];
// ?ндекс текущего врага в волне
private var _enemyIndex:int = 0;
// Указатель на текущий объект с информацией о враге
private var _enemy:Object = null;
// Флаг, определяющий выполняется ли волна сейчас
private var _isStarted:Boolean = false;
// Пройденный интервал между респауном врагов 
private var _interval:Number = 0;

В конструкторе класса напишем строчку:

_universe = Universe.getInstance();

Дальше первым делом мы напишем публичный метод addEnemy(), который будет добавлять в волну вид врага:

public function addEnemy(kind:uint, count:int, respawnInterval:Number):void
{
  _enemies[_enemies.length] = { kind:kind, 
    count:count,
    respawn:respawnInterval };
}

Здесь мы создаем новый объект с тремя полями kind — вид врага, count —количество юнитов указанного типа и respawnInterval — временной интервал между появлением юнитов данного типа. Объект с информацией о типе врага тут же добавляется в массив. Теперь напишем метод startWave(), который будет запускать работу волны:

public function startWave():void
{
  if (!_isStarted)
  {
    _enemyIndex = 0;
    _interval = startDelay;
    _isStarted = true;
  }
}

Если волна еще не запущена, то мы сбрасываем текущий индекс врага на нулевую позицию, задаем стартовый интервал и устанавливаем флаг работы волны. Теперь можно перейти к написанию метода update(), который будет вызываться из игрового мира и постоянно обрабатывать действия волны, так же как это реализовано для самих юнитов и врагов с их методами update():

public function update(delta:Number):void
{
  if (_isStarted)
  {
    // Уменьшаем текущий интервал
    _interval -= 10 * delta;

    // Если интервал закончился,
    // вызываем следующего врага
    if (_interval <= 0)
    {
      // Если врага вывать не удалось,
      // значит волна закончилась,
      // сбрасываем флаг работы волны
      if (!nextEnemy())
      {
        _isStarted = false;
      }
    }
  }
}

Код получился достаточно простым, и я думаю, что из комментариев его принцип работы должен быть понятен: мы все время уменьшаем интервал со скоростью 10 единиц в секунду. ? тут я хочу обратить ваше внимание на использование delta времени — это необходимо, чтобы временной интервал между появлением врагов был синхронизирован с игровым процессом. К реальному времени появление врагов привязывать не стоит, так как, если вдруг, по какой-то причине, на компьютере игрока игра начнет притормаживать, то может получиться так, что враги начнут появляться быстрее, чем будет прекращаться движение предыдущей волны. То есть, время игры не будет соответсвовать времени появлению врагов и таким образом весь баланс игры просто развалится.

Если вы еще не обратили внимание на метод nextEnemy() в условие — сделайте это сейчас :) Этот метод вызывает следующего врага из списка врагов текущей волны, если следующего врага удалось вызвать, метод возвращает true, а если по какой-то причине следующий враг не вышел (в нашем случае это может случиться только, если очередь врагов закончилась), то метод вернет значение false. Таким образом мы вызываем метод nextEnemy() и если он возвращает false, значит работа волны закончилась. Теперь посмотрим на код метода nextEnemy():

private function nextEnemy():Boolean
{
  // Если список врагов не закончен
  if (_enemyIndex < _enemies.length)
  {
    // ?звлекаем информацию о следующем виде врага
    if (_enemy == null)
    {
      var o:Object = _enemies[_enemyIndex];
      // Копируем информацию о виде
      // и количестве единиц врагов
      // в новый объект.
      _enemy = { kind:o.kind,
        count:o.count,
        respawn:o.respawn };
    }
                
    // Создаем единицу врага
    _universe.newEnemy();
                
    // Устанавливаем задержку до
    // появления следующей единицы врага.
    _interval = _enemy.respawn;
                
    // Уменьшаем количество вышедших врагов
    _enemy.count--;
    if (_enemy.count <= 0)
    {
      // Если все единицы врагов для 
      // текущего вида созданы, переходим
      // к следующему виду врагов.
      _enemyIndex++;
      _enemy = null;
    }
                
    return true;
  }
            
  return false;
}

При каждом вызове метода мы проверяем наличие объекта в _enemy, если он равен null, значит нужно извлечь новый вид и количество единиц из списка врагов. После чего мы сразу же вызываем публичный метод игрового мира Universe.newEnemy(); —этот метод пока остался у нас в оригинальном виде без указания типа врага, позже мы этот момент доработаем. Далее по коду мы устанавливаем новый интервал между появлением единиц и уменьшаем количество вышедших юнитов на один. Если количество юнитов равно нулю, значит все враги данного типа вышли и можно переключиться на следующий вид врагов в списке: увеличиваем индекс врага и сбрасываем указатель на объект с информацией о враге, чтобы при следующем вызове этого метода была выполнена инициализация следующего вида врага из списка. Вот собственно и все, осталось добавить геттер, который позволит нам отслеживать завершение работы волны в игровом мире, и можно будет протестировать работу класса.

public function get isFinished():Boolean
{
  return !_isStarted;
}

Геттер работает просто: если волна не запущена, значит она закончена :)

EnemyWave в игре

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

Открываем класс Universe.as и первым делом добавляем тройку новых приватных переменных:

// Список всех вражеских волн на текущем уровне
private var _waves:Array = [];
// ?ндекс текущей вражеской волны в списке
private var _waveIndex:int = 0;
// Указатель на экземпляр класса текущей вражеской волны
private var _currentWave:EnemyWave = null;

Теперь перейдем к конструктору класса Universe и добавим инициализацию пары тестовых вражеских волн:

// Временная инициализация двух вражеских волн
var wave:EnemyWave = new EnemyWave();
wave.startDelay = 10;
wave.addEnemy(0, 2, 3);
wave.addEnemy(1, 3, 3);
_waves.push(wave);
            
wave.startDelay = 20;
wave.addEnemy(0, 4, 2);
wave.addEnemy(2, 2, 2);
_waves.push(wave);
_waveIndex = 0;

Здесь мы просто создаем два экземпляра класса EnemyWave, устанавливаем стартовую задержку перед началом действия волны, потом добавляем по паре видов врагов и сохраняем волну в список волн. Далее необходимо добавить обработку вражеских волн, для этого создадим метод updateWaves(), который будем вызывать в методе enterFrameHandler() каждый кадр. Метод updateWaves() будет выглядеть следующим образом:

private function updateWaves(delta:Number):void
{
  // Обновление текущей вражеской волны
  if (_currentWave == null)
  {
    // Если волна не задана, выбираем новую
    // из списка по индексу
    _currentWave = _waves[_waveIndex] as EnemyWave;
    _currentWave.startWave();
  }
  else
  {
    // Обновляем вражескую волну
    _currentWave.update(_deltaTime);
    if (_currentWave.isFinished)
    {
      // Если текущая вражеская волна закончилась,
      // увеличиваем индекс волны и сбрасываем
      // текущую волну. Если все волны в списке
      // были выполнены, то переходим в начало 
      // списка всех волн и процесс выполняется
      // по кругу.
      _waveIndex = (_waveIndex + 1 >= _waves.length) ? 0 : _waveIndex + 1;
      _currentWave = null;
    }
  }
}

Добавляем вызов метода updateWaves() в методе enterFrameHandler() и можно протестировать работу наших вражеских волн. Если все сделано правильно, то в двух случайных точках будут появляться враги с заданным нами интервалом, волны будут идти по кругу. Теперь если все работает правильно, можно приступать к доработке.

Типы врагов

В одном из прошлых уроков в качестве домашнего задания я просил вас добавить новых врагов. Не знаю, сделали вы это или нет, но они нам сейчас очень понадобятся. Если вы помните, то в качестве идентификатора вида врага мы используем константы определенные в классе EnemyBase.as, до сегодняшнего времени у нас там была всего одна константа: ENEMY_SOLDIER, а теперь я добавил новых врагов: джип и танк, поэтому я добавил туда две новые константы:

public static const KIND_JEEP:int = 1;
public static const KIND_TANK:int = 2;

А так же добавил два новых класса: EnemyJeep.as и EnemyTank.as — оба класса унаследованы от EnemyBase.as и практически ничем не отличаются от EnemySoldier.as. Основные отличия только в скорости движения, количестве жизни, внешнем виде, ну и другие незначительные изменения. Так же для новых боевых единиц техники я сделал плавные развороты, чтобы их движение выглядело более интересным и живым. Как это сделано я не буду здесь описывать, так что если вам это интересно, не забудьте заглянуть в исходники и посмотреть на изменения.

Чтобы добавить использование новых видов врагов в игре нам необходимо немного изменить код в классе Universe.as. Первым делом добавим кэши для хранения джипов и танков — это две новые публичные переменные в классе Universe:

public var cacheEnemyJeep:SimpleCache;
public var cacheEnemyTank:SimpleCache;

?нициализация кэшев в конструкторе класса должна выглядить так:

cacheEnemyJeep = new SimpleCache(EnemyJeep, 10);
cacheEnemyTank = new SimpleCache(EnemyTank, 10);

А теперь изменим метод newEnemy() следующим образом:

public function newEnemy(kind:int):void
{
  // Создание врага в зависимости от указанного типа
  var enemy:EnemyBase = null;
  switch (kind)
  {
    case EnemyBase.KIND_SOLDIER :
      enemy = cacheEnemySoldier.get() as EnemySoldier;
    break;
                
    case EnemyBase.KIND_JEEP :
      enemy = cacheEnemyJeep.get() as EnemyJeep;
    break;
                
    case EnemyBase.KIND_TANK :
      enemy = cacheEnemyTank.get() as EnemyTank;
    break;
  }
            
  if (enemy != null)
  {
    // Выбираем случайную стартовую точку и финишную
    var startPos:Avector = _startPoints[Amath.random(1, _startPoints.length) - 1];
    var finishPos:Avector = _finishPoints[Amath.random(1, _finishPoints.length) - 1];

    // ?нициализируем юнита с выбранными точками
    enemy.init(startPos.x, startPos.y, finishPos.x, finishPos.y);
  }
}

Теперь метод в качестве параметра принимает значение вида врага (kind), и в зависимости от указанного вида врага достает из кэша экземпляр врага и инициализирует его. Теперь код инициализации вражеских волн можно переделать следующим образом:

var wave:EnemyWave = new EnemyWave();
wave.startDelay = 10;
wave.addEnemy(EnemyBase.KIND_JEEP, 5, 6);
wave.addEnemy(EnemyBase.KIND_SOLDIER, 10, 2);
_waves.push(wave);
            
wave.startDelay = 30;
wave.addEnemy(EnemyBase.KIND_JEEP, 4, 6);
wave.addEnemy(EnemyBase.KIND_TANK, 3, 14);
wave.addEnemy(EnemyBase.KIND_JEEP, 2, 6);
_waves.push(wave);
_waveIndex = 0;

При добавлении нового вида врага мы теперь явно указываем его вид, используя константы из класса EnemyBase, а не какие-нибудь абстрактные числа, таким образом код стал намного понятнее.

Дальше необходимо вернуться в класс EnemyWave и при вызове метода newEnemy() передавать вид врага из волны. Откройте класс EnemyWave, перейдите в метод nextEnemy() и найдите строку:

_universe.newEnemy();

? замените её на строку:

_universe.newEnemy(_enemy.kind);

Еще не забудьте, что до этого момента новых врагов мы могли вызывать клавишой пробел, которая обрабатывалась в классе Game.as. Откройте класс Game и закомментируйте содержимое метода keyDownHandler(). Теперь вновь можно протестировать волны и, если вы позаботились о новых классах EnemyJeep и EnemyTank, а так же о внешнем виде новых боевых едениц, то вы в полной мере сможете насладиться разнообразием врагов, выходящих в атаку с заданным нами интервалом. Сейчас уже можно поиграться немного со скоростью движения врагов, с их количеством жизни, а так же со скоростью атаки единственной пока башни и её уроном, чтобы попробовать уже найти хоть какой-то баланс.

Домашнее задание

Как вы могли заметить, сейчас враги по-прежнему появляются в случайных точках и бегут в случайные точки, и это на самом деле полное безобразие. Чтобы устранить этот беспорядок, сегодняшним домашним заданием будет задача расширить функционал вражеских волн и добавить возможность задавать для врагов точную точку появления и точку цели. Задача простая, но немножко придется поковыряться в коде.

Кстати, первым делом не забудьте определиться:

  • Вы хотите задавать точку появления/точку цели для каждого вида врага в волне отдельно?
  • ?ли может быть следует задавать точку появления/точку цели для всей волны целиком?

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

Заключение

Вражеские волны получились отличными, и в очередной раз все получилось не так сложно, как казалось изначально. Основной вывод из сегодняшнего урока можно сделать такой: вражеская волная — это небольшой вражеский заводик с коротенькой конвеерной лентой, которая производит юнитов и отправляет их в бой!

Но в текущей реализации создавать и тестировать вражеские волны не удобно —нужно менять код и перекомпилировать всю игру, поэтому в следующем уроке мы добавим возможность менять настройки волн на лету, а так же постараемся вынести информацию о волнах на время разработки во внешние .xml файлы. ? если одно другому мешать не будет, то постараемся добавить поддержку уровней.

Ссылка на исходники — CS4, *.zip, 180кб.

Результат

Атака врагов происходит автоматически и в бесконечной цикличности, поэтому просто ставьте башни и смотрите на бесконечный отстрел вражеских единиц :)

Содержание

  1. Вступление
  2. Структура игры
  3. Карта проходимости
  4. Первый враг
  5. Готовимся к поиску пути
  6. Поиск пути
  7. Редактор уровней
  8. Движение врагов
  9. Первая башня
  10. Кэширование объектов
  11. Полоса жизни
  12. Вражеские волны
  13. Загрузка вражеских волн
  14. Продолжение следует...

 

 

Ну прям таки таки бери код, ставь свою графику и на FGL))))

Komizart
15 Октября 2011
— 20:45
#

Антон, спасибо за урок! Возник вопрос по производительности, она уж слишком хороша :) В дальнейшем в игру будет добавлено не так много динамических объектов, единственное, что может убить производительность, это добавление анимации врагам, башням. Так ли сильно скажется это на фпс? Если нет, то нафиг нужен растровый рендер? :) Ведь ТД по сути самый нуждающийся в большом количестве анимированных объектов жанр, нет?

Я вот пробовал просто добавить 50 растровых анимаций - ощутимо тормозило. Решил уже разбираться в растеризации, а тут посмотрел твой урок - и как-то все неожиданно здорово :) Вот и возник вопрос.

TheJoker
15 Октября 2011
— 21:50
#

2 TheJoker
На производительность влияет
-размер карты (поиск пути)
-Кол-во юнитов одновременно
-Постоянные вычисления и слушатели
- Задний фон, менюшки и пр.
Посматри на кол-во юнитов в буфере, увидишь что у нас пара сотен объектов макс (если башен настроить много), а с этим флеш справляется и без растрового рендера. Вызови пару тысяч и увидишь что будет )) ?ли расширь функционал.

Marcus
16 Октября 2011
— 21:34
#

@TheJoker, да, анимация скажется на производительности. Но как сильно — это будет зависеть непосредственно от самой анимации. В данном примере вектор работает достаточно шустро лишь потому что графика примитивна. Если заменить текущий вектор на более детальный и сложный (например, нарисованный кисточкой), то все может начать прилично тормозить. Поэтому графику наподобие той как я демонстрировал для CTC TD надо обязательно растеризировать.

Векторная графика так же может сильно влиять на производительность игры даже если она не имеет анимации. Причиной тому может быть большое количество точек. Например, когда я нарисовал этот дом, и вставил в окно туториала его векторым, то при открытии окна туториала с этим домом, fps игры проседал вдвое! :)

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

Ant.Karlov
16 Октября 2011
— 21:47
#

Можно немного расширить логику.
В одном из прототипов я делал еще следующее
- признак ключевого персонажа (при убийстве которого запускается следующая волна)
- ключевая точка, при достижении которой персонажем запускается следующая волна.

Для того чтобы сгенерировать волны - я ставил отслеживания по клавиатуре (например нажатие на 1 - простой юнит, 2 - бронированный), так же при нажатии на Q например следующему персонажу присваивался признак ключевого, а W - позволял поставить ключевую точку.

Все это писал в логи.

Для создания уровня достаточно запустить игру - и начать кнопками создавать волны, после чего опгрейдишь башни и создаещь следующую волну....

логи парсиш в excel и там же фомулами делаешь код :)

dron
17 Октября 2011
— 07:09
#

Привет, Антон.
возник вопрос: спрайт это что? растр или вектор?

приветкакдела
17 Октября 2011
— 22:25
#

Блин, наверно это был дурацкий вопрос.
но не понимаю - есть у спрайта объект "графикс", в котором мы можем все что угодно нарисовать - все это хранится в векторном виде, или само растеризуется?

я так понял, что в результате растеризации у нас должен получиться экземпляр БитмапДаты, из котороый можно сделать битмап или врисовать ее куда нибудь еще.

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

приветкакдела
17 Октября 2011
— 22:30
#

в graphics хранится только векторная информация, а растр хранится только в BitmapData.

BitmapData есть только у Bitmap, так что все остальное (Sprite, Shape, MovieClip) по дефолту векторное.

Вращать BitmapData не нужно (как вы это вообще собирались делать?), вместо этого вращать нужно Bitmap, связанный с этим BitmapData.

BuxomBerry
18 Октября 2011
— 10:01
#

@приветкакдела, в геймдеве под спрайтом подразумевается графическое представление какого-либо объекта, в 3D спрайты — это плоские объекты. В своих уроках под спрайтами я так же подразумеваю графические сущности тех или иных объектов.

Флеш сам не умеет растерезировать векторную графику полноценно, он только может делать временный растровый кэш векторной графики чтобы наводить дополнительную оптимизацию производительности (см. опцию cache as bitmap), но этого как правило не достаточно для сложной векторной графики и анимации.

BitmapData — это матрица пикселей с информацией о них и она не имеет стандартных свойств для трансформации изображения. Поэтому следует использовать класс Bitmap в связке с BitmapData. Bitmap — это аналог Sprite, работающий исключительно с растровой картинкой.

Ant.Karlov
18 Октября 2011
— 11:02
#

Привет Антон, как всегда - увлекательное чтиво :)
Мне очень понравился стиль снега (тайлов) и задника в твоей игре Santa Is Coming, и в связи с этим возник вопрос, если ты не против, можно я буду в своей игре использовать тайлы снега - в той же стилистике?
Уж очень понравилось. =)

whilesocold
18 Октября 2011
— 23:55
#

@whilesocold, на использование графического стиля я не могу давать ограничений. Главное чтобы в вашей игре не использовалась графика непосредственно из моей игры.

Ant.Karlov
20 Октября 2011
— 11:44
#

Само собой разумеется :)
У игры совсем другой жанр и персонажи.

whilesocold
20 Октября 2011
— 12:56
#

А если добавить на сцену пустой спрайт, а в него запихнуть еще bitmap — это будет кушать намного больше ресурсов, чем если просто bitmap добавить на сцену?

?ли вот в этой самом tower defence, о котором урок. Класс мира, да и вышележащие классы, расширяют Sprite, а не Bitmap - будут ли они сильно поедать производительность в таком виде, и нужно ли их все переделывать так, чтобы они расширяли Bitmap, или же достаточно растеризовать только их содержимое?

приветкакдела
20 Октября 2011
— 21:02
#

@приветкакдела, вообще, можно так делать, и ресурсов больше кушаться не должно. Но растеризовать есть смысл только тогда, когда у тебя вектор очень сложный, с градиентами и фильтрами. А если вектор простенький, то он будет меньше весить и быстрее обрабатываться. К тому же, если ты просто будешь использовать битмапы на сцене вместо спрайтов, большого прироста производительности это не даст, лучше сразу делать растровый рендер. ? отделять графику от кода. Т.е. объекты не должны расширять спрайты или мувиклипы, они должны хранить в себе свойства и ссылку на графическое представление. В главном методе update() приложения обновляются свойства, а в методе draw() все объекты рисуются в нужном положении в главный холст игры (Bitmap) - это единственный отображаемый объект в игре. Правда, не получается обычно сделать растровое меню простым способом... Я вот пока что делаю меню из спрайтов, а вся игра рисуется на единственном холсте. Тут уж сам решай, как тебе удобнее.

Proletarium
21 Октября 2011
— 19:39
#

Антон, уроки клёвые, всё хорошо, но есть один момент. Сейчас я копаюсь в старых уроках и очень часто натыкаюсь на слово ПР?ДЕЛ. В начале я думал, что это опечатка. Потом, что совпадение. Но когда я стал замечать это слово то в одном уроке, то в другом, меня уже стало коробить. ? вот, когда я сегодня скачал исходники и опять наткнутлся это слово, я начал закипать. Я не граммар наци или что-то в этом роде. Просто устал уже ментально спотыкаться об это слово. В связи с этим я был бы ОЧЕНЬ признателен, если бы вы, многоуважаемый мной Антон, были любезны употреблять это слово правильно. Если конечно под приделом не имелось ввиду это: http://ru.wikipedia.org/wiki/%D0%9F%D1%80%D0%B8%D0%B4%D0%B5%D0%BB

С наилучшими пожеланиями, Василий.

Василий
25 Октября 2011
— 16:39
#

Василий выпей йаду

orbit
25 Октября 2011
— 18:43
#

P.S. если не нравятся грам. ошибки автора - не читай, тебя ведь на аркане не ташют

orbit
25 Октября 2011
— 18:45
#

"если не нравятся грам. ошибки автора - не читай". Уроки у автора очень хорошие, много чего полезного нашел для себя. За это Антону низкий поклон. Поэтому не читать не смогу. Буду мучиться, плакать, но читать буду.

Василий
25 Октября 2011
— 20:34
#

Приношу свои извинения. В более свежих уроках слово "предел" употреблено правильно! Просто елей на душу!

Василий
26 Октября 2011
— 00:24
#

Шах и Мат. В два хода.

http://s006.radikal.ru/i213/1111/7e/d7e68f014bfe.png

Черешня
7 Ноября 2011
— 02:46
#

Антон, когда будет следующий урок ?

PleaseTwo
9 Ноября 2011
— 12:59
#

Когда вообще новіе записи в блоге будут?

Андрей
9 Ноября 2011
— 15:38
#

Вот.

Черешня
9 Ноября 2011
— 22:12
#

@PleaseTwo, новый урок будет на следующей недели :)

Ant.Karlov
11 Ноября 2011
— 13:07
#

@Андрей, новая запись будет сегодня вечером :)

Ant.Karlov
11 Ноября 2011
— 13:08
#

Подсказка для тех, кто захочет выводить текст в верхнем левом углу:
в Properties для текста выбираем кнопку Embeded, дальше в “character ranges” проставить “Uppercase”,”Lowercase”,”Numerals”, і “Punctuation”. В противном случаи придется долго чесать репу, пытаясь разобраться ”почему оно не работает”.

2 Ant.Karlov
Спасибо за труды! С нетерпением жду продолжения.

FirstFlashGame
24 Ноября 2011
— 19:39
#

Здравствуй, Антон.

Похоже, что я попал туда, куда давно хотел — место где я смогу найти энтузиастов для совместной работы над инди-играми.

Думаю найдутся такие же люди как я — со своими проектами, идеями или просто желанием поучаствовать. Я был бы очень признателен, если бы провели перепись таких разработчиков, что помогло бы нам собрать пару команд =)

DG
25 Ноября 2011
— 19:56
#

@DG, я рад что тебе здесь понравилось, спасибо!

Вообще я считаю что большинство Flash разработчиков весьма приветливы и уютно не только здесь :) Очень часто разработчики представляют свои проекты на форуме flashgamedev.ru и на блогах flashgameblogs.ru — где в основном и находят себе сообщников по цеху, общение по актуальным темам и просто друзей! :)

Ant.Karlov
26 Ноября 2011
— 12:38
#

Вопрос по теме урока: как реализовать внеочередной вызов волны врагов, скажем, по нажатию пользователем специальной кнопки “Next Wave”?

FirstFlashGame
24 Февраля 2012
— 13:30
#

@FirstFlashGame, сейчас код игры реализован таким образом, что в одно время не может быть запущенно более одной вражеской волны, то если следующая вражеская волна может быть активирована только когда закончится текущая.

Но добавить возможность вызывать следующую волну одновременно с текущей волной — не сложно. Для этого в место переменной _currentWave следует сделать массив, например _activeWaves и при вызове следующей волны добавлять в этот массив новую волну. Далее в методе updateWaves процессить все волны находящиеся в этом массиве и проверку на завершенность волн следует изменить таким образом чтобы новая волна автоматически запускалась только в том случае, если все активные волны завершились. По мимо этого вы можете создать новый метод nextWaveImmediately() который бы вызывался нажатием кнопки:

function nextWaveImmediately():void
{
_waveIndex = (_waveIndex + 1 >= _waves.length) ? 0 : _waveIndex + 1;
_activeWaves.push((_waves[_waveIndex] as EnemyWave).startWave());
}
}

Ant.Karlov
24 Февраля 2012
— 16:43
#

Спасибо Вам за подсказку!
Реализовал через вектор с возможностью немедленного старта волны без ожидания (вызываю дополнительно nextEnemy)
Но возник вопрос – почему ожидание реализовано через startDelay вместо endDelay? Почему ожидание реализовано перед появлением первого юнита а не после выхода последнего?
У меня происходит следующая ситуация – когда выходит последний юнит первой волны, то стартует вторая волна и ждет указанный startDelay. А вот статистика обманывает пользователя, потому что показывает, что сейчас уже началась вторая волна, хотя юниты на игровом поле с первой волны, и реально юниты второй волны выйдут только через 30 секунд…
Дальше нажимаю nextWave и стартует третья волна немедленно, хотя по идее, должна наконец начаться вторая.

FirstFlashGame
24 Февраля 2012
— 20:07
#

Здравствуйте! Упёрся в создание баланса. Не знаю как правильно выбирать характеристики врагов и башен, волн... Каковы подходы по созданию баланса в игре?

1) Вариант первый: все делаем на глаз.
Выбираются с потолка х-ки башен.
Выпускается волна с определённым количеством врагов и смотрим какие пушки как быстро могут их уничтожить? Дальше правим характеристики врагов и их количество в волне?

2) Ведётся какая-то таблица в excel с расчётом баланса: стоимость башен, апгрейда, ресурсы за убитых врагов... какие-то формулы для расчета...

С какой стороны подойти к этому вопросу? ? сколько приблизительно времени тратится на создание баланса игры?

FirstFlashGame
8 Сентября 2012
— 16:05
#

@FirstFlashGame, оба варианта имеют свои плюсы и недостатки.

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

2. Вариант с таблицой не такой интересный и более нудный, но зато в будущем позволит вносить быстрые правки без необходимости переигрывать уровни по 100500 раз. Впрочем нудность варианта заключается только в придумывании и настройке такой таблицы, далее же все окажется намного проще и интереснее :)

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

В хороших играх на баланс и шлифовку уходит до 70% времени, в средних до 50%. В общем баланс — это важная штука, особенно в стратегиях.

Ant.Karlov
8 Сентября 2012
— 16:31
#

Для меня оказалось сюрпризом что балансу игры уделяется так много времени. Раньше об этом не задумывался. Спасибо Вам за объяснение!

FirstFlashGame
8 Сентября 2012
— 17:10
#

Блин после первой части кода (на первых словах можно тестировать игру) получил ошибку:

TypeError: Error #1009: Cannot access a property or method of a null object reference.
at com.towerdefence::EnemyWave/addEnemy()
at com.towerdefence::Universe()
at com.towerdefence::Universe$/getInstance()
at com.towerdefence::Game()
at com.towerdefence::App/init()
at com.towerdefence::App()

Как исправить, в чем не доработка?
Обычно это происходило если я не создал объект, но я уже и исходники сверил...

Новенький
1 Августа 2014
— 23:13
#