TowerDefence #8. Первая башня

Сегодня мы наконец-то добавим в нашу игру долгожданные башенки и научим их стрелять и убивать врагов. Но для этого прийдется много поработать.

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

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

Для реализации обоих состояний башен нам вновь понадобится класс Amath.as. Сегодня мы будем испоьзовать только один новый метод:

Amath.distance(x1:Number, y1:Number, x2:Number, y2:Number):Number;

— Рассчитывает расстояние между двумя точками. В качестве параметров передаются координаты первой точки: x1, y1 и координаты второй точки: x2, y2. Этот метод нам пригодится, чтобы определять попадает враг в радиус атаки башни или нет, а так же поможет определить столкновение пуль с врагами.

Но прежде, чем приступить к работе над кодом, подготовим временную графику для нашей первой башенки.

Графика

Пока работа над игрой у нас идет в режиме прототипа, башенку я так же, как и врагов, изображу простыми примитивами, но, в отличии от врагов, она будет строиться немного по иному принципу. Башня будет состоять из двух частей: статичный фундамент и вращающаяся пушка. Создадим в *.fla файле два новых клипа: GunTowerBody_mc и GunTowerHead_mc. В первом клипе нарисуем квадратное основание башни и сделаем его темным, а во втором нарисуем круг и небольшой прямоугольник, который будет символизировать ствол пушки. Ствол пушки должен быть повернут в клипе ровно направо, чтобы в игре он поворачивался корректно. Не забудьте добавить экспорт клипов в AS.

При экспортировании всех клипов в код я рекомендую снимать галочку «Export in 1 frame».

Класс башенки

Перейдем к нашему проекту и создадим новый пакет (папку) com.towerdefence.towers, в котором у нас будут находиться классы всех башен и другие вспомогательные классы необходимые для их работы. Далее создадим базовый класс башен для всех видов башен, назовем его TowerBase.as, базовый класс башни должен быть унаследован от класса Sprite. Добавьте в класс две статические публичные константы, они у нас будут отвечать за текущее состояние каждой башни:

public static const STATE_IDLE:uint = 1;
public static const STATE_ATTACK:uint = 2;

После этого создадим все необходимые переменные:

// Ссылка на игровой мир
protected var _universe:Universe = Universe.getInstance();
 
// Графическое тело башни
protected var _body:MovieClip;
// Графическая пушка башни
protected var _head:MovieClip;
// Позиция башни в тайлах
protected var _tilePos:Avector = new Avector();
// Текущее состояние башни
protected var _state:uint = STATE_IDLE;
// ?нтервал для задержки между проверками на приближение врагов
protected var _idleDelay:int = 0; 
// Радиус атаки башни
protected var _attackRadius:Number = 100;
// ?нтервал между выстрелами
protected var _attackInterval:Number = 8;
// Наносимый урон одним выстрелом
protected var _attackDamage:Number = 0.2;
// Скорость пули
protected var _bulletSpeed:Number = 100;
// Указатель на атакуемого юнита
protected var _enemyTarget:EnemyBase;

Теперь перейдем непосредственно к написанию самих методов башни. Первым делом напишем метод free(), который должен её удалять:

public function free():void
{
  // Удаляем тело
  if (_body && contains(_body))
  {
    removeChild(_body);
  }
            
  // Удаляем башню
  if (_head && contains(_head))
  {
    removeChild(_head);
  }
            
  // Удаляем башню из игры
  _universe.removeTower(this);
}

Здесь все достаточно просто и я думаю, что дополнительных объяснений не потребуется. Метод removeTower() в классе Universe.as мы допишем немного позже. Теперь пишем метод init():

public function init(tileX:int, tileY:int):void
{
  if (_body != null && _head != null)
  {
    addChild(_body);
    addChild(_head);
  }
            
  // Запоминаем положение в тайлах
  _tilePos.set(tileX, tileY);
            
  // Устанавливаем графический образ башни
  x = Universe.toPix(tileX);
  y = Universe.toPix(tileY);
            
  // Добавляем башню в игру
  _universe.addTower(this);
}

Этот метод будет перекрываться и дополняться в потомках этого класса, поэтому тут лишь базовые команды. Метод addTower() класса Universe.as мы так же допишем чуть позже. А сейчас перейдем к методу update():

public function update(delta:Number):void
{
  // пока пусто         
}

Метод update() в родительском классе будет пустым, его мы будем активно использовать в потомках класса. ? на этом с классом TowerBase.as пока закончим. Но прежде, чем переходить к следующему этапу, не забудьте импортировать в базовый класс башни другие необходимые классы:

import flash.display.Sprite;
import flash.display.MovieClip;
import com.framework.math.*;
import com.towerdefence.Universe;
import com.towerdefence.enemies.*;

Теперь перейдем непосредственно к разработке первого вида башни. Создаем новый класс в пакете com.towerdefence.towers, назовем его GunTower.as и унаследуем его от TowerBase.as. В конструкторе нового класса мы зададим индивидуальные параметры текущего вида башни:

public function GunTower()
{
  // ?ндивидуальные параметры текущего вида башни
  _attackRadius = 60;
  _attackInterval = 10;
  _attackDamage = 0.1;
  _bulletSpeed = 300;
}

Далее создаем метод init() класса GunTower.as:

override public function init(tileX:int, tileY:int):void
{
  // Графический образ
  _body = new GunTowerBody_mc();
  _head = new GunTowerHead_mc();
            
  super.init(tileX, tileY);
  debugDraw();
}

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

Теперь напишем метод update():

override public function update(delta:Number):void
{
    _head.rotation += 5;
}

Тут мы пока просто вращаем ствол башни — это нужно для того, чтобы протестировать работу башни в игровом мире до того, как мы приступим к работе над её алгоритмом. А теперь напишем метод debugDraw():

private function debugDraw():void
{
  graphics.lineStyle(1, 0x9fc635, 0.1);
  graphics.beginFill(0x9fc635, 0.02);
  graphics.drawCircle(0, 0, _attackRadius);
  graphics.endFill();
}

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

Universe.as

Чтобы наши башенки создавались и работали в игровом мире, добавим пару новых методов, которые мы уже как-бы используем в классах башни: addTower() и removeTower(). Обработка башен ничем не будет отличаться от обработки врагов в игровом мире, поэтому сразу создаем новый массив в игровом мире:

private var _listOfTowers:Array = [];

? пишем методы addTower() и removeTower(). Поскольку эти методы практически ничем не отличаются от методов addEnemy() и removeEnemy(), вы можете их скопировать и чуть-чуть исправить:

public function addTower(tower:TowerBase):void
{
  // Добавляем башню в список
  _listOfTowers.push(tower);
            
  // Добавляем башню на карту
  addChild(tower);
}
        
public function removeTower(tower:TowerBase):void
{
  // Удаляем башню с карты
  if (contains(tower))
  {
    removeChild(tower);
  }
            
  // Удаляем башню из списка
  for (var i:int = 0; i < _listOfTowers.length; i++)
  {
    if (_listOfTowers[i] == tower)
    {
      _listOfTowers[i] = null;
      _listOfTowers.splice(i, 1);
      break;
    }
  }
}

Тут я хочу сразу заметить, что я лично не очень люблю, когда методы с одинаковым кодом дублируют друг друга. На мой взгляд, это не красиво и не удобно, поэтому к этому моменту мы еще вернемся чуть позже. А пока перейдем к методу enterFrameHandler() класса Universe.as и добавим обработку башенок аналогично тому, как это сделано с врагами:

private function enterFrameHandler(event:Event):void
{
  // Рассчет delta времени
  _deltaTime = (getTimer() - _lastTick) / 1000;
            
  // Обработка врагов, находящихся в игровом мире
  var n:int = _listOfEnemy.length;
  for (var i:int = 0; i < n; i++)
  {
    _listOfEnemy[i].update(_deltaTime);
  }
            
  // Обработка башен, находящися в игровом мире
  n = _listOfTowers.length
  for (var i:int = 0; i < n; i++)
  {
    _listOfTowers[i].update(_deltaTime);
  }
            
  _lastTick = getTimer();
}

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

var tower:GunTower = new GunTower();
tower.init(2, 1);
tower = new GunTower();
tower.init(2, 3);

После этого добавьте в класс Universe.as импорт пакета com.towerdefence.towers.* и протестируйте игру. Ура! Вот и первая башня! Если все сделано правильно, то башенки должны появиться в левом верхнем углу и будут вращать своими стволами. Теперь можно приступать к работе над алгоритмом башни, но прежде мы решим вопрос копипастного кода для повторяющихся методов.

Контроллер объектов

Добавляя башню в игру у нас получилось так, что мы создали несколько методов, которые полностью дублируют функционал друг-друга. Это методы addEnemy(), removeEnemy(), addTower(), removeTower(). «Ну и подумаешь» — скажете вы — «все же работает, ничего страшного!». Но я так не считаю, так как в таком случае нам еще возможно не раз прийдется их продублировать. Ведь у нас еще будут пули, которые будут добавляться и обрабатываться в игровом мире похожим способом. А если мы еще что-то решим добавлять и обрабатывать в игровом мире? Например, эффекты! То тут уже будет сплошной копипаст, который в будущем не только ухудшит читаемость кода, но и сделает его не универсальным в том случае, если надо будет что-то переделать. Поэтому решение этой проблемы не следует откладывать в долгий ящик.

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

Все игровые объекты у нас будут объединены одним интерфейсом IGameObject.as, который будет требовать от игровых объектов иметь обязательные для них методы: free() и update(). Давайте создадим новый пакет com.towerdefence.interfaces — в нем у нас будут находиться все игровые интерфейсы. ? создадим первый интерфейс IGameObject.as:

package com.towerdefence.interfaces
{
  public interface IGameObject
  {
    function free():void;
    
    function update(delta:Number):void;
  }
}

Тут все очень просто, все объекты, которые имплементируют этот интерфейс, должны обязательно иметь реализацию описанных в интерфейсе методов. В нашем случае это метод free() и метод update(). Теперь добавим имплементацию этого интерфейса в наши игровые объекты EnemyBase.as и TowerBase.as. Открываем их и меняем описание классов следующим образом, для класса EnemyBase.as:

public class EnemyBase extends Sprite implements IGameObject

? для класса TowerBase.as:

public class TowerBase extends Sprite implements IGameObject

Поскольку в этих классах уже реализованы методы free() и update(), больше ничего делать не нужно. Все потомки классов TowerBase.as и EnemyBase.as наследуют интерфейсы своих родителей, поэтому в них дополнительно ничего изменять не нужно. Сейчас вы можете протестировать игру, и если при тестировании у вас не возникает никаких ошибок и все работает так же, как и до создания интерфейса, значит все сделано правильно. Теперь перейдем непосредственно к работе над менеджером объектов.

Перед тестированием игры не забудьте добавить импорт интерфейса в классы TowerBase.as и EnemyBase.as.

Менеджер объектов у нас будет очень простым, он будет добавлять, удалять и процессить игровые объекты, поэтому это не столько менеджер, сколько больше контроллер (на мой взгляд). Впрочем, вы можете называть его как хотите, а я назову его ObjectController.as :)

ObjectController.as

Контролеров в будущем, как и интерфейсов, у нас может быть несколько, поэтому для их хранения лучше всего создать отдельный пакет com.towerdefence.controllers. ? создадим в нем наш контроллер объектов ObjectController.as. Код контроллера достаточно простой, поэтому я привожу его целиком без дополнительных комментариев:

package com.towerdefence.controllers
{   
  import com.towerdefence.interfaces.IGameObject;
  import com.towerdefence.Universe;
    
  public class ObjectController extends Object
  {
    public var objects:Array = [];
    private var _universe:Universe = Universe.getInstance();
    
    // Добавляет объект в контроллер
    public function add(obj:IGameObject):void
    {
       objects[objects.length] = obj;
       _universe.addChild(obj);
    }
    
    // Удаляет объект из контроллера
    public function remove(obj:IGameObject):void
    {
      for (var i:int = 0; i < _objects.length; i++)
      {
        if (objects[i] == obj)
        {
          _universe.removeChild(obj);
          objects[i] = null;
          objects.splice(i, 1);
          break;
        }
      }
    }
    
    // Удаляет все объекты из мира
    public function clear():void
    {
      while (objects.length > 0)
      {
        objects[0].free();
      }
    }
    
    // Процесс всех объектов в контроллере
    public function update(delta:Number):void
    {
      var n:int = objects.length - 1;
      for (var i:int = n; i >= 0; i--)
      {
        objects[i].update(delta);
      }
    }
  }
}

Обратите внимание, что в методах добавления и удаления в качестве параметров передаются только объекты типа IGameObject — то есть это значит, что в этот контроллер мы можем помещать любые классы, имплементирующие интерфейс IGameObject.as. Если какой-либо класс имплементирует интерфейс IGameObject, то мы можем быть уверенны в том, что у этого класса существует реализация описанных в интерфейсе методов. Таким образом нам удалось объединить все наши игровые объекты по одному принципу и написать универсальный для всех объектов контроллер. Только обратите внимание, что контроллер не осуществляет добавления визуального образа объектов в игрой мир, теперь каждый объект должен позаботиться об этом самостоятельно.

?спользование контроллера

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

public var towers:ObjectController;
public var enemies:ObjectController;
public var bullets:ObjectController;

? в конструкторе класса Universe.as создадим все эти экземпляры:

towers = new ObjectController();
enemies = new ObjectController();
bullets = new ObjectController();

Потом удалим все старые методы addEnemy(), removeEnemy(), addTower() и removeTower(). Таким образом переменные _listOfEnemy и _listOfTowers тоже больше не нужны. Теперь перейдем к методу enterFrameHandler() и в место всех циклов напишем три строчки:

private function enterFrameHandler(event:Event):void
{
  // Рассчет delta времени
  _deltaTime = (getTimer() - _lastTick) / 1000;
            
  enemies.update(_deltaTime);
  towers.update(_deltaTime);
  bullets.update(_deltaTime);
            
  _lastTick = getTimer();
}

Сейчас еще раз вернемся к базовым классам врагов и башен, и в их методах free() и init() заменим старые строчки:

_universe.removeTower(this); 
_universe.removeEnemy(this);
_universe.addTower(this);
_universe.addEnemy(this);

На строчки:

_universe.towers.remove(this);
_universe.enemies.remove(this);
_universe.towers.add(this);
_universe.enemies.add(this);

Так же не забудьте сразу после добавления/удаления объекта в контроллер добавлять/удалять визуальный образ объекта в игровой мир дополнительной строчкой:

_universe.addChild(this); // Добавить
_universe.removeChild(this); // Удалить

Например:

// Удаляем башню из игры
_universe.removeChild(this);
_universe.towers.remove(this);

// Добавляем башню в игру
_universe.towers.add(this);
_universe.addChild(this);

Вот вроде пока и все по поводу контроллера объектов. Многим может показаться, что проделанные изменения не стоят того, но в любом случае код стал логичнее, понятнее и универсальнее :)

Состояния башенок

Возвращаемся к работе над башенками, открываем класс GunTower.as и сразу переходим к методу update(). Надеюсь вы еще помните, что поведение всех башенок мы разделили на два состояния: состояние наблюдение и состояние атаки — работу над башнями мы продолжим с состояния наблюдения. Переписываем метод update() следующим образом:

override public function update(delta:Number):void
{
  switch (_state)
  {
    // Состояние наблюдение за врагами
    case TowerBase.STATE_IDLE :
      if (_idleDelay >= 5)
      {
        // Указатель на список всех врагов
        var enemies:Array = _universe.enemies.objects;
        // Количество врагов в списке
        var n:int = enemies.length;
        // Текущий враг из списка
        var enemy:EnemyBase;

        // Перебираем всех врагов в списке
        for (var i:int = 0; i < n; i++)
        {
          enemy = enemies[i]; // Текущий враг

          // Если дистанция от врага до башни меньше 
          // или равна радиусу атаки башни, значит атакуем врага!
          if (Amath.distance(enemy.x, enemy.y, this.x, this.y) <= _attackRadius)
          {
            // Устанавливаем атакуемую цель
            _enemyTarget = enemy;
            // Переключаемся в состояние атаки
            _state = TowerBase.STATE_ATTACK;
            trace("Атака!");
            break;
          }
        }
      }
      _idleDelay++;
    break;
                
    // Состояние атаки!
    case TowerBase.STATE_ATTACK :
      // пока пусто
    break;
  }
}

Тут, как всегда, все просто :) Единственное, в целях оптимизации в режиме наблюдения башня проверяет приближение врагов с неким интервалом. В данном случае интервал между проверками в 5 игровых кадров. Визуально эту задержку не заметно, но зато количество расчетов на каждый кадр становится намного меньше, особенно актуально это будет, когда на игровом поле будет много башен. Сейчас вы можете протестировать игру. Если все сделано правильно, то при попадании врага в радиус действия башни в окне output будет выводиться «Атака!». Далее перейдем к программированию второго состояния башни — атака. Дописываем следующий код в метод update() класса GunTower.as:

case TowerBase.STATE_ATTACK :
  if (_enemyTarget != null)
  {
    // Поворачиваем башню в сторону врага
    _head.rotation = Amath.getAngleDeg(this.x, this.y, _enemyTarget.x, _enemyTarget.y);
                        
    // todo стрелять во врага
    // todo проверять, не погиб ли враг

    // Враг убежал
    if (Amath.distance(_enemyTarget.x, _enemyTarget.y, this.x, this.y) > _attackRadius)
    {
      _enemyTarget = null;
      _state = TowerBase.STATE_IDLE;
    }
  }
  else
  {
    _state = TowerBase.STATE_IDLE;
  }
break;

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

Давайте в место строчки:

// todo стрелять во врага

Напишем следующий код:

_shootDelay--;
if (_shootDelay <= 0)
{
  shoot();
  _shootDelay = _attackInterval;
}

Здесь у нас используется новая переменная _shootDelay типа int, которую ранее мы еще не добавляли. Добавьте её сейчас в приватные переменные класса GunTower.as. Принцип действия таков, что мы присваиваем в эту переменную интервал атаки башни и потом каждый кадр убавляем её на единицу, когда время вышло — производим выстрел. Такой вот простой таймер. А вот выстрел производится методом shoot(). Пока у нас нет пуль и стрелять нам нечем, поэтому создайте пустой метод shoot() в классе GunTower.as и перейдем к пулям.

Пули

С пулями у нас будет все точно так же, как с башнями и врагами. Для всех пуль будет базовый класс пули BulletBase.as и потомки от этого класса будут отличаться лишь своими уникальными характеристиками. Поскольку пули у нас будут принадлежать только башням, то мы не будем создавать отдельного для них пакета и разместим все классы пуль в пакете com.towerdefence.towers.*;

Создаем класс BulletBase.as и унаследуем его от Sprite, и не забудьте имплементировать интерфейс IGameObject. У пули пока будет одна публичная и несколько приватных переменных:

// Урон пули
public var damage:Number = 0.1;
// Ссылка на мир
protected var _universe:Universe = Universe.getInstance();
// Визуальная часть
protected var _sprite:Sprite;
// Скорость пули
protected var _speed:Avector = new Avector;

Конструктор пули пока оставим пустым и перейдем сразу к созданию метода free(). Он у нас будет похожим на метод free() башен и врагов:

public function free():void
{
  if (_sprite && contains(_sprite))
  {
    removeChild(_sprite);
  }
  
  if (_universe.contains(this)
  { 
    _universe.removeChild(this);
  }

  _universe.bullets.remove(this);
}

Теперь создадим метод init() для пули:

public function init(ax:int, ay:int, speed:Number, angle:Number):void
{
  // Добавляем спрайт пули
  if (_sprite)
  {
    addChild(_sprite);
  }
  
  // Устанавливаем пулю
  this.x = ax;
  this.y = ay;
  
  // Устаналиваем скорость пули
  _speed.asSpeed(speed, Amath.toRadians(angle));

  // Добавляем пулю в игровой мир
  _universe.bullets.add(this);
  _universe.addChild(this);
}

Далее напишем метод update() для пули:

public function update(delta:Number):void
{
  this.x += _speed.x * delta;
  this.y += _speed.y * delta;
  // todo проверка столкновения с врагом
}

В этом методе мы пока лишь только двигаем пулю с заданной скоростью. К нему мы еще вернемся позже, когда протестируем текущий этап работы. Прежде, чем приступить к работе над классом пули для башни GunTower.as, давайте нарисуем её графический образ во Flash IDE. Я нарисовал маленький кругляшок размером 3x3 пикселя и назвал клип GunBullet_mc.

Теперь создадим класс GunBullet.as в пакете com.towerdefence.towers. Унаследуйте его от BulletBase.as. В этом классе у нас пока будет только один метод:

override public function init(ax:int, ay:int, speed:Number, angle:Number):void
{
  _sprite = new GunBullet_mc();
  super.init(ax, ay, speed, angle);
}

Здесь мы создаем уникальный визуальный образ для текущей пули и вызываем метод init() родительского класса BulletBase.as. На первый взгляд может показаться, что создание отдельного класса для каждого типа пули не самое рациональное решение. Но могу вас с уверенностью заверить, что это только на текущем этапе могут возникнуть такие ощущения. Когда для каждой башни пули будут отличаться не только визуальным видом, но и оказываемым действием на вражеских юнитов (взрывы, ракеты, электрические разряды), а так же графическими эффектами, то все будет опять же просто и понятно.

Боезапас заготовлен, пора зарядить башни! :) Возвращаемся в класс GunTower.as и пишем в методе shoot() следующий код:

private function shoot():void
{
  var bullet:GunBullet = new GunBullet();
  bullet.damage = _attackDamage; // Передаем урон башни в пулю
  bullet.init(this.x, this.y, _bulletSpeed, _head.rotation); // ?нициализируем пулю
}

? тестируем игру. Теперь башня не только наводится на цель, но и ведет массивный огонь по цели. Здорово! Правда пули пока пролетают насквозь врагов, сейчас мы это поправим. Но перед этим я хотел бы обратить ваше внимание на не большую проблему.

«Целкость» башен

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

  1. Сделать врагов «толстыми и сочными», чтобы пули попадали с наибольшей вероятностью даже при большей скорости движения врагов.
  2. Сделать «не честную» атаку, то есть, чтобы пуля попадала во врага, даже если враг убежал от нее. Например: когда пуля достигает точки, где враг был последний раз, она наносит урон тому врагу, в которого был произведен выстрел, но для этого при создании пули нужно передавать ей указатель на врага, в которого она как бы полетела.
  3. Сделать самонаводящиеся пули, то есть пули должны так же иметь ссылку на атакуемую цель и корректировать свое движение относительно меняющихся координат врага.
  4. ? самый простой вариант — это вообще не делать пуль: создаем эффект выстрела из пушки и сразу отнимаем урон у противника.

Но мы ведь легких путей не ищем! Поэтому о читерских вариантах 2 и 4 забываем :) Будем делать все по честному, балансируя на грани скорости и сочности врагов, а так же сделаем башню ракетницу с самонаводящимися ракетами. Вот где будет весело! Но это все в следующих уроках, а пока вернемся к классу BulletBase.as и сделаем проверку на столкновение с врагами.

Столкновение пуль с врагами

Перепишем метод update() класса BulletBase.as следующим образом:

public function update(delta:Number):void
{
  this.x += _speed.x * delta;
  this.y += _speed.y * delta;
            
  // Список врагов
  var enemies:Array = _universe.enemies.objects;
  // Количество врагов
  var n:int = enemies.length;
  // Текущий враг
  var enemy:EnemyBase;
  // Дистанция между текущим врагом и пулей
  var dist:Number;
            
  for (var i:int = 0; i < n; i++)
  {
    enemy = enemies[i];
    dist = Amath.distance(this.x, this.y, enemy.x, enemy.y);
    
    // Если дистанция между врагом и пулей меньше или равна
    // сумме радиусов юнита и пули, значит они сталкиваются
    if (dist <= this.width * .5 + enemy.width * .5)
    {
      enemy.addDamage(damage); // Наносим урон врагу
      free(); // Удаляем пулю
      break;
    }
  }
}

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

Поскольку ранее у нас не был реализован метод addDamage() у врагов, давайте создадим его сейчас. Откройте класс EnemyBase.as и создайте пустой метод addDamage(damage:Number):void, а так же новую публичную переменную isDead:Boolean = false. После чего откройте класс EnemySoldier.as и перекройте в нем метод родительского класса:

override public function addDamage(damage:Number):void
{
  _health -= damage;
            
  // Враг погиб
  if (_health <= 0)
  {
    isDead = true;
  }

  // Временный эффект атаки
  _sprite.gotoAndPlay(2);
  _isAttaked = true;
}

В своем спрайте солдатика я сделал еще пару кадров, где меняю цвет солдатика в момент попадания пули — это временное решение, чтобы визуально видеть, что враг действительно получил урон. ? когда наносится урон для спрайта врага включается проигрывание анимации, а так же устанавливается флаг _isAttaked в true. Далее в методе update() класса EnemySoldier.as я добавил несколько строк кода, которые выключают анимацию атаки:

if (_isAttaked)
{
  if (_sprite.currentFrame == 1)
  {
    _sprite.stop();
    _isAttaked = false;
  }
}

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

Если вы внимательно смотрели код, то должны были заметить, что урон пулям я везде ставил как дробные числа — я привык работать с дробными, поэтому 1 для меня = 100% здоровья врага, а урон в 0.1 — это 10% урона. Но единица — это не ограничение здоровья у врагов, значение может быть более, чем 1, например 2 или 3 = 200%-300% здоровья. С прошлых версий наш EnemySoldier.as имеет здоровье по умолчанию 100, то есть фактически сейчас он бессмертный для башен, поэтому исправьте это значение на 1, а башне GunTower.as поставьте _attaсkDamage = 0.4.

Далее сделаем еще небольшую доделку в методе update() класса GunTower.as, перепишем состояние атаки следующим образом:

case TowerBase.STATE_ATTACK :
  if (_enemyTarget != null)
  {
    // Поворачиваем башню в сторону врага
    _head.rotation = Amath.getAngleDeg(this.x, this.y, _enemyTarget.x, _enemyTarget.y);
                        
    // Враг убит
    if (_enemyTarget.isDead)
    {
      _enemyTarget.free();
      _enemyTarget = null;
      _state = TowerBase.STATE_IDLE;
    }
    // Враг убежал
    else if (Amath.distance(_enemyTarget.x, _enemyTarget.y, this.x, this.y) > _attackRadius)
    {
      _enemyTarget = null;
      _state = TowerBase.STATE_IDLE;
    }           
    // Атакуем
    else
    {
      _shootDelay--;
      if (_shootDelay <= 0)
      {
        shoot();
        _shootDelay = _attackInterval;
      }
    }
  }
  else
  {
    _state = TowerBase.STATE_IDLE;
  }
break;

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

Почти все готово, осталось только сделать установку башен мышкой. Для этого удаляем код создания башен из конструктора класса Universe.as и создаем новый публичный метод buildTower():

public function buildTower():void
{
  if (getCellState(cellPosX, cellPosY) != STATE_CELL_BUSY)
  {
    var tower:GunTower = new GunTower();
    tower.init(cellPosX, cellPosY);
    setCellState(cellPosX, cellPosY, STATE_CELL_BUSY);
    updateDebugGrid();
  }
}

Этот метод без комментариев, так как тут все должно быть уже понятно. Теперь переходим к классу Game.as и в методе init() добавляем новый перехватчик клика мышкой:

addEventListener(MouseEvent.CLICK, mouseClickHandler);

Далее пишем метод mouseClickHandler():

private function mouseClickHandler(event:MouseEvent):void
{
  _universe.buildTower();
}

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

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

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

  1. Сделать время жизни для пуль — то есть в базовом классе пули мы заводим новую переменную и после инициализации пули заполняем её неким значением, которое потом каждый кадр отнимаем. ? когда это значение стало меньше или равно 0 — удаляем пулю.
  2. Сделать проверку на вылет пули за пределы игрового экрана и удалять сразу, как только она оказалась за пределами экрана.

Какой из способов лучше сделать, определяйте сами.

Заключение

Сегодня мы проделали большую работу, добавили первые башни, научили их стрелять, переработали обработку игровых объектов и получили прототип с параметрами которого уже можно поиграться и который хоть как-то уже напоминает игру. Но как не крути, это еще далеко не пол пути к финалу.

В следующем уроке мы добавим для врагов отображение их здоровья полосками. Добавим простые вражеские волны, поговорим о кэшировании и повторном использовании игровых объектов, а так же добавим профайлер.

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

Результат

Устанавливайте башенки кликом мышки. А чтобы пустить врагов нажимайте и удерживайте клавишу пробел.

В Флешке результате я скрыл кнопку «Editor» потому что в веб версии игры в редакторе уровней больше делать нечего. Переход из редактора в игру и обратно я убрал :)

Содержание

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

 

 

Здовово! Спасибо!

Samana
27 Марта 2011
— 21:27
#

Как обычно очень интересно, доступно и понятно! Спасибо за статью.
По поводу наследования для разных типов пуль: имхо, такой подход хорош когда типов оружия планируется немного. Скажем пуля, медленная ракетница и лазер(т.е. принципиаьно разные)...Но если же типов однородного оружия будет много и отличия будут лишь только некоторых характеристиках(скорострельность и тп) то плодить классы нерационально будет пожалуй. Лучше при инициализации сообщать экземпляру эти наборы характеристик. + такие наборы можно будет собрать в одном конфиге и потом не прыгать по классам при отладке баланса.
Но это так, грубый субъектив :) Вобщем все здорово!

Bazz
27 Марта 2011
— 21:59
#

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

Aber
27 Марта 2011
— 22:12
#

Спасибо огромное!

Андрей
27 Марта 2011
— 22:29
#

Заметил такой же баг как и Aber.

Андрей
27 Марта 2011
— 22:34
#

@Aber и @Андрей, а какой у вас браузер?
Сразу так даже и не предположу почему это может происходить, надо потестировать.

Ant.Karlov
27 Марта 2011
— 22:40
#

Спасибо большое за очередной туториал. Так же наблюдал баг, который описал Alber. Часть "врагов" игнорируют препятствия после прокрутки страницы или смены таба. Браузер Opera
11.01

Денис
27 Марта 2011
— 22:57
#

Пользуюсь мозилой, но и в IE тот-же эффект. Кстати в предыдущем уроке то-же самое.

Aber
27 Марта 2011
— 22:59
#

Проблему повторил. Скорее всего враги сбиваются с курса из-за притормаживания флешки при переключении вкладок или прокручивания страницы. А когда мы вновь возвращаемся к игре, то из-за динамического deltaTime враги проскакивают контрольную точку для смены направления пути и убегают за приделы экрана сбившись с курса. Можно попробовать два варианта решения этой проблемы:

1. Сделать статический deltaTime.
2. Переделать условие для проверки достижения контрольной точки. Скорее всего вовсе изменить споб проверки. Например сделать аналогично тому как мы проверяем столкновение пуль с врагами — измерять расстояние до точки и при определенной величине переходить к следующей точке. А если расстояние вдруг начнет увеличиваться, то значит точку уже вовсе проскочили и тут же менять направление.

Ant.Karlov
27 Марта 2011
— 23:37
#

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

Ant.Karlov
27 Марта 2011
— 23:45
#

Проблема скорее всего в том, что при флеш уменьшает фпс при переключении вкладок (фича плеера начиная с 10.1)

http://www.adobe.com/products/flashplayer/features/

Periodic timer
Eliminate the dependency on different browser timer implementations to deliver consistent cross-platform behavior, significantly lower CPU utilization with nonvisible SWF content, and extend battery life. Nonvisible SWFs and SWFs on hidden tabs are throttled down to 2 frames per second.

Flasher
27 Марта 2011
— 23:59
#

? еще один способ решения проблемы пропуска контрольной точки, и пожалуй самый правильный и оптимальный:

3. Пересчитывать векторную скорость врага относительно текущей контрольной точки во время движения. Сейчас скорость задается один раз при выборе следующей точки в методе setNextTarget(). А если скорость пересчитывать хотя бы один раз в 10-15 кадров в методе update() у врага, то если вдруг юнит собьется с курса — он обязательно к нему вернется :)

Впрочем я свои предположения еще не тестировал в проекте, поэтому пробуйте :)

Ant.Karlov
27 Марта 2011
— 23:59
#

@Flasher, да, точно! Это в нашем понимании тоже является «притормаживанием» игры :)

Ant.Karlov
28 Марта 2011
— 00:01
#

Спасибо Антон все супер :)

?ван
28 Марта 2011
— 09:17
#

Класный урок, но есть 2 глюка.

1. поставил башню в левую верхнюю серую клетку и пустил врагов. Стреляет до сих пор по горизонтали вправо.

2.Кучка врагов забилась в тупик с координатами x:15, y:6 и стояли там не двигаясь пока не поставил пушку.

Komizart
28 Марта 2011
— 10:28
#

@Komizart, это не совсем глюки.

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

2. Тупик с координатами x:15, :y:6 — это одна из финишных точек для врагов, поэтому прибегая туда юниты там и оставались.

Ant.Karlov
28 Марта 2011
— 10:37
#

Понятно, что нужны ещё проверки. Так в любом проекте, главное основа есть. А там ещё и летающих юнитов можно сделать.

Кстати, когда зажимаю пробел, на 10-15й секунде часть юнитов просто летит сквозь стены за экран

Komizart
28 Марта 2011
— 11:55
#

В состояниях башен забыл про апгрейд. Да и не только, но апгрейд самое явное. Формула рассчета времени выстрела бажная. Остальное я так же делал. Даже совпадают некоторые наименования. Космос то один. Хотя думаю мой коммент низкого качества, как и моя игра.

RaymondGames
29 Марта 2011
— 09:06
#

Давно слежу за вашими уроками, можно сказать учусь по ним. Созрел вопрос. Пытался переделать TD под Alternativa3D. Так как я пока ноль в этом вопросе - ничего не получилось. Вопрос следующий: как правильно подключить камеру, чтобы смотреть на плоскость под углом?
Приношу свои извинения, понимаю что вопрос по альтернативе больше, но ничего не нашел.

Alter
29 Марта 2011
— 11:16
#

@Ant.Karlov
В идеале нужно отлавливать потерю фокуса и ставить игру на паузу, а так же резать дельту по некоторому максимуму, который точно не сможет навредить расчётам.

@Alter
Камера в A3D такой же объект, как и все остальные. У неё есть rotationX,Y,Z. А вообще у Альтернативы есть форум, если ты вдруг не заметил.

elmortem
30 Марта 2011
— 02:08
#

@RaymondGames, про апгрейд я даже еще не думал. Позже еще обязательно к этому вернемся.

«Формула расчета времени выстрела бажная» — это ты имеешь в виду, что не стоит использовать кадры для расчета интервала между выстрелами? Конечно тут возможно наиболее точным решением будет использовать getTimer() для расчета интервала, но для простоты примера и так пока пойдет :)

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

Ant.Karlov
30 Марта 2011
— 10:10
#

@Alter, к сожалению в Alternativa3D я не силен совсем. Но думаю, что вам следует посмотреть примеры движка и обязательно посетить официальный форум, как предложил elmortem. Уверен, там вы найдете ответы на все ваши вопросы по Alternativa3D.

Ant.Karlov
30 Марта 2011
— 10:13
#

@elmortem, да, я тоже думал об этом, но потом вспомнил что где-то читал о том, что некоторые игроки в TD играют в фоновом режиме. Например, наставят башенок, запустят волну и пока там происходит рубилово они чатятся или что-нибудь читают в другой вкладке браузера :) В общем надо посмотреть как реализован этот момент в других популярных играх в жанре TD.

Ant.Karlov
30 Марта 2011
— 10:19
#

Как бы тебе сказать чтобы не обидеть. Я очень много времени провел за полировкой, мне игра нравится на 100% даже не смотря на то что пришлось существенно уменьшить сложность и прочие аспекты. ?гра поменялась чуть ли не полностью уже на последних этапах (? ЭТО НЕ ДВА ДНЯ РАБОТЫ). То что мне не удается сделать так чтобы вам было не напряжно - это конечно косяк. Но я реально не знаю как это сделать, хоть ты мне сто советов дай. Поэтому игре уже ниче не поможет. Еще лучше именно я (!) её уже сделать не смогу. Ну и естественно депрес нехилый от понимания того что пол года потрачено в пустую. Ну а в целом не было таких мыслей делать текучий тавер дефенс где ставишь башню и уходишь пить кофе. Была мысль сделать напряженные бои. Короче если спонсор найдется на игру узнаем насколько кому понравится такой стиль.

RaymondGames
30 Марта 2011
— 12:45
#

@RaymondGames, эх я вот тоже очень много времени провел за полировкой Mining Truck 2, и мне игра нравится такой какая она получилась. Но момент со сложностью игры я так же как и ты упустил. Сделал слишком много хардкорных элементов и сложный кран в конце первых уровней. За счет чего игра раздражала более половины пользователей которые не готовы были немного разобраться в управлении чтобы начать получать удовольствие. В моем случае проблему конечно можно было бы решить за пару дней работы вырезав кран вообще, но мне было очень жалко его убирать :) В итоге только после релиза стало понятно, что во флеше не так уж и много хардкорных игроков. ? особенно грустно допускать такие глупые ошибки с балансом именно в сложных играх которые заняли большой срок разработки.

В твоей игре не я один заметил аналогичную ошибку и все подробно тебе об этом написали. Но ты слишком близко все принял к сердцу. ? тут я тебе могу с уверенностью сказать, что полгода работы над игрой не потрачены в пустую. Но если ты не изменишь баланс и не доведешь игру до блеска, то тогда они станут пустыми, да :(

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

В текущем своем проекте я стараюсь делать все достаточно казуально. Но нефига не получается :) Поэтому в этот раз я уделю плейтестам намного больше внимания и буду досконально разбирать фидбэк ;)

Ant.Karlov
30 Марта 2011
— 17:01
#

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

ZloyMedved
30 Марта 2011
— 17:52
#

Кроме этого, я хотел сказать.
Все, кроме этого урока :)

ZloyMedved
30 Марта 2011
— 17:53
#

@Ant.Karlov
Не знаю, как это возможно с новым плеером, который снижает fps при потере фокуса. Как вариант сделать fps статичным 1 / 30, тогда не будет проблем при снижении fps, просто игра замедлится.

@ZloyMedved
Чтобы ворочать большими кусками уровней не обойтись без различных ухищрений, которые помогут быстро рисовать и обсчитывать уровень. В первую очередь это различные BSP и схожие алгоритмы организации объектов на уровне. Так же лучше не использовать векторные изображения или кешировать их в Bitmap (вручную).
Для динамической подгрузки кусков уровня нужно решить ряд проблем:
1. Как будут взаимодействовать объекты на незагруженных частях.
2. Будут ли объекты перемещаться между частями уровня.
3. Как будут сохраняться удаляемые части уровня.
4. Как хранить части уровня (ландшафт, объекты, персонажи).

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

elmortem
31 Марта 2011
— 11:07
#

Антон так я уменьшил сложность, приняв во внимание ваши комменты. Причем во всей игре. Довольно сложно проиграть сейчас, возможно утащат мага - но на то он и тавер дефенс чтобы совершенствоваться со временем, либо не совершенствоваться и проходить просто напролом (так тоже можно). Я не знаю смотрел ты или нет последний вариант, как я понял нет. Там где нету кнопок плей/пауза/1х/2х. Причем сложность уменьшилась но не стала такой что совсем ничего делать не надо. Хардкорной игру делать не было умысла, наоборот на последних этапах это хардкорство резалось усиленно что сильно расстраивало тестеров которые уже привыкли что им бросают вызов. Вобщем я дошёл до той точки где уменьшение сложности уже не приносит фана играющим - отсюда и вывод что дальше резать бессмысленно. Не уверен повторюсь что вы смотрели последнюю версию.

RaymondGames
31 Марта 2011
— 14:48
#

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

Не знаю, играл ли ты в Zombie vs. Plants. Но если не играл, то обязательно поиграй. Вначале все так казуальненько и весело, а ближе к концу игры зубодробительное месево :) Хороший пример правильного баланса.

Ant.Karlov
31 Марта 2011
— 15:40
#

RaymondGames, надо прислушаться к тому, что вам говорят, толк обязательно будет. А сейчас у вас какое-то неконструктивное поведение. Я это наблюдал на себе и на других много раз: первая реакция на критику это отторжение и попытка доказать, что критика несправедлива. Но включите здравый смысл, если вам говорят одно и тоже несколько человек, то есть смысл взглянуть на проблему трезво, собраться с последними силами и исправить недоработки. Да иногда замыленный взгляд очень сложно преодолет, но нужно стараться. ? еще сейчас у вас поведение такое, будто напрашиваетесь на жалость, утешение и слова одобрения, при том, что проект не хочется переделывать. Это мало того, что не очень красиво со стороны выглядит, но и никак не повлияет на реальную монетизацию вашей игры. Надо набраться мужества и доделывать свой проект, никто кроме вас его не доделает.

Андрей
31 Марта 2011
— 16:15
#

@elmortem
Ну, к примеру, есть три bitmap-куска.
Как можно их подгружать без задержек и зависаний, друг за другом? Обязательно использование хитрых алгоритмов?

http://www.thegamehomepage.com/play/flight/
к примеру вот. сзади хорошо видны дома, которые периодически повторяются.
неужели для этого нужно организовывать BSP?

ZloyMedved
31 Марта 2011
— 17:24
#

@ZloyMedved, рассмотрим пример на двух битмапах: загружаем сразу два битмапа в игровой процесс и двигаем их по мере необходимости. Например, если наш герой движется слева на право, то когда первый кусок уходит налево за приделы экрана, мы переставляем его направо и продолжаем процесс. Когда второй кусок уходит налево, следом за ним уже двигается первый, и когда второй пкидает приделы экрана, мы его так же как и первый переставляем направо. ? так по кругу :) То есть принцип цепочки, а битмапов может быть несколько. В любом случае создавать их нужно сразу все, а использовать в игре по мере необходимости.

Ant.Karlov
31 Марта 2011
— 20:03
#

Антон, у меня незначительный вопросик, просто стало интересно разобраться в одном моменте. В вашем коде и в других примерах заметил в комментариях, такие конструкции @param, @return, @author, @version. Причем они распознаются редактором кода и подсвечиваются в ФлэшДевелепоре. Не могу нагуглить про это ничего, как они называются, для чего они нужны кроме дополнительного выделения ключевых комментариев?

Андрей
1 Апреля 2011
— 12:21
#

Андрей формат этого блога не позволяет мне что-либо негативное тебе ответить, но я тебе намекну - давай ты не будешь говорить о том чего не знаешь.

Антон да это всё понятно. Не понятно как сделать легко - понимаешь? В т.ч. давал одному тестеру последний вариант - он знаешь как отреагировал?)) Говорит - ну так не интересно - легко же! ? где правда?)) А схема от простого к сложному это база, я по ней и делал. Просто люди настолько разные что реально не понятно кому легко это легко, кому сложно, а кому уже скучно. Ну и учти что это второй проект всего лишь, наращивал геймплей и фичи без осознания того как их последовательно представить. Мейби следующая игра будет показуальнее. Ты сам то собираешься тавер дефенс делать или нет?

RaymondGames
1 Апреля 2011
— 15:03
#

ZloyMedved любые алгоритмы разбиения пространства нужны только в том случае если у тебя куча объектов. Для игры типа flight это в принципе не нужно - там чистый рандом (звездочки и пр. бонусы) плюс пара повторяющихся задников. Всяческие деревья могут пригодиться для комплексных уровней, например для платформеров/адвенчур. В т.ч. там же может пригодиться навык хранения уровней в какой либо форме. Тем не менее не наступай на мои грабли (и не только мои). Если ты начинающий - делай простые игры. Как показала практика, основные грабли лежат на пути к адаптации твоего творчества к потребностям масс, а не в коде/графике. Соответственно чем проще тебе будет вносить изменения на последних этапах, тем лучше.

RaymondGames
1 Апреля 2011
— 15:12
#

Вот что значить не выспаться и читать на работе! Сначала подумал что Todo - это имя башни, которая говорит
// todo стрелять во врага
// todo проверять, не погиб ли враг

Статьи классные, молодец!

0_o
1 Апреля 2011
— 15:47
#

RaymondGames, а что захотелось мне ответить как-то жестко? Так не надо стесняться, я не обижусь, вы лишь себя продемонстрируете. Вы в публичном месте развели нытье "хотя думаю мой коммент низкого качества, как и моя игра". Мне это показалось некрасивым, я оставил комментарий, потому что здесь комментируют друг друга и статью. Если вы не хотите чтобы вас комментировали, то можете переписываться с Антоном по емейлу, разве это не очевидно? Можете и тут, я не настаиваю, но просто тогда нет смысла обижаться на комментарии.

Андрей
1 Апреля 2011
— 16:52
#

благодарю за урок. уже что-то вырисовывается =)
с нетерпением жду продолжения.

Demoralizator
1 Апреля 2011
— 18:11
#

Спасибо.
Антон, ваши уроки можно "читать" по коду - все в комментах... тут не осилил с прочтением, но исходники проглядел.

Теперь я ваши уроки буду читать по исходникам :)

з.ы. в App забыли отписаться от слушателя события ADDED_TO_STAGE.

NoICE
5 Апреля 2011
— 19:07
#

@Андрей, конструкции типа @param, @return и т.п. называются тегами и являются частью стандарта оформления комментариев, особенно в классах фреймворках или утилитных классах, которые могут распространятся и использоваться в разных проектах. В большей степени эти тэги носят справочный характер и используются для быстрого создания документации специальными программами.

Вот здесь можно немного почитать об этом... А для гугла используйте запрос as3 documentation generator.

А это ссылка на pdf от Adobe со стандартами и примерами оформления комментариев с использованием тегов.

Ant.Karlov
6 Апреля 2011
— 11:08
#

@NoICE, в статьях много интересной теории о том, почему это сделано именно так, а не иначе. Да и исходники каждый раз перечитывать чтобы найти пару новых изменений — это тоже не быстрый процесс ;)

Ant.Karlov
6 Апреля 2011
— 11:12
#

тоже подумываю написать мануал.
собственно твой цикл статей и подвигнул меня на создание собственной реализации - http://www.flashgamelicense.com/view_game.php?from=dev&game_id=17445.

У меня более классический вариант (w3 alike), так что реализация немного другая, но местами тоже совпадает.
Как я понял анимация и игровая логика должна быть завязана на фреймах, иначе рассинхронизации и за тормозов не избежать. это добавляет определенных проблем со сглаживание анимации..но решаемо.

Насчет пуль. Я сделал все варианты. Т.е. есть пули которые самонаводятся (корректируют свою траекторию). Есть которые не наводятся. Есть которые бьют моментально, не достигая цели. Это мне кажется логичным. К примеру магическая молния всегда наводится и попадает, но наносит урон одному врагу. Ядро катапульты наоборот - летит медленно, не всегда попадает, но наносит урон нескольким врагам, не обязательно тем в которых стреляла.

squirrelfm
6 Апреля 2011
— 16:54
#

@squirrelfm, огосебе, какая серьезная игра получилась. Почти WarCraft! :) А графика собственной разработки?

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

По позже еще вернусь и по внимательнее посмотрю на игру.

Ant.Karlov
6 Апреля 2011
— 17:38
#

правая кнопка поддерживается в игре. см. wmode opaque. просто длоя этого нужнозаембедить обьект с параметром wmode=opaque и allowScriptExecution=sameDomain.

FLG этого делать не желает.

squirrelfm
6 Апреля 2011
— 17:54
#

Про графику - в кредитсах написанно откуда)

squirrelfm
6 Апреля 2011
— 18:00
#

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

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

Сам я ActionScript учил очень давно и уже ничего не помню и возможно в чём-то ошибаюсь, тогда поправьте пожалуйста...

Заранее спасибо!

PS. блог офигенный! Когда нашёл его, прочёл за ночь все записи, а тут появились новые, но пока не успел прочесть)))

DEM
12 Апреля 2011
— 06:07
#

@DEM, проверка расстояния конечно затратная, но не настолько чтобы можно было об этом сильно переживать, есть вещи намного затратнее. В нашем примере расстояние от врагов до башен рассчитывается с небольшим интервалом, что снижает количество расчетов.

Ваш пример с дополнительным параметром для врагов конечно интересен, но упущен самый интересный момент: как вы предлагаете определять попал ли враг в радиус действия башни? :)

Спасибо за отзыв!

Ant.Karlov
12 Апреля 2011
— 10:30
#

@DEM:
Кто и когда сказал, что посчитать квадратный корень от суммы квадратов 2х - 3х чисел - это затратно?

Если так рассуждать, то затратными станут и операции сложения, "затратнее" некуда :)))

BuxomBerry
12 Апреля 2011
— 23:07
#

Здравствуйте Антон!

Я добавил в игру меню выбора башен. открывается по нажатию левой кнопки мыши, при отпускании кнопки ставит выбранную башню в ячейку на которую нажимал.
Хотел спросить Вас, лучше ли это чем статическая панель где-нибудь справа, как во многих других TD?

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

Alter
15 Апреля 2011
— 13:57
#

@Alter, про меню выбора башен мне сложно что-то однозначно сказать не видя самого меню. Подобные панели инструментов как описали вы уже давно существуют в играх и некоторые из них удобные, а другие нет — в конечном итоге все зависит от того как они реализованы. Если нестандартной панелью установки башен удобно пользоваться — то это хорошо, а если нет, то плохо. А с точки зрения оформления все зависит уже от самой игры ;)

Следующий урок будет на днях, я надеюсь.

Ant.Karlov
16 Апреля 2011
— 10:54
#

Антон, заметил такой нюанс, который никак не могу понять сам. У вас в классе контроллера объектов ObjectController метод update перебирает элементы массива с конца в обратном порядке. Я в своем классе перебирал их сначала и иногда, при определенных обстоятельствах возникали ошибки обращения к пустому элементу.

?справил как было у вас на перебор с конца - все прошло, ошибок нет. В чем причина так и не смог понять, решил время на это пока не тратить, попролбовать узнать у автора.

Андрей
10 Мая 2011
— 03:04
#

@Андрей, если при переборе элементов массива от нуля будет удален какой-то из элементов массива, то размер массива изменится и получается выход за приделы массива, что влечет ошибку.

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

В нашем случае удаление объектов из игрового мира, а следственно и из перебираемого массива происходит при игровой смерти объекта в методе update();

Ant.Karlov
10 Мая 2011
— 11:52
#

Спасибо, теперь ясно. Все элементарно, вот что значит нет опыта, как я сам забыл что в update() еще и удаление этого объекта происходит)

Андрей
10 Мая 2011
— 12:15
#

Заметил что вы везде пишете придел. Это баг, или фича?

Seagull
19 Июня 2011
— 17:01
#

@Seagull, у каждого есть свои слабые стороны ;)

Ant.Karlov
19 Июня 2011
— 18:37
#

Спасибо за урок, осилил таки его :) пока проблемы есть , домашнее задание буду делать завтра, голова уже не соображает сегодня :)

? почемуто заметил у себя баг, когда 2 пушки стреляют в 1 клетку то вылетает ошибка: ArgumentError: Error #2025: Предоставленный DisplayObject должен быть дочерним элементом вызывающего объекта.

Ну в общем низкий Вам поклон, за то что помогаее нам "зеленым".

Marik
1 Июля 2011
— 22:16
#

Доброе утро всем :) снова я встал сутра и исправил свои ошибки допущенные в этом уроке, теперь все хорошо, все работает.

Еще раз спасибо, приступлю ка я пожалуй к домашнему заданию и следующему уроку :)

Marik
2 Июля 2011
— 09:17
#

Зачем в функцию clear() контроллера передавать параметр obj? Наверное это ошибка? )

Dan4ez
12 Августа 2011
— 23:26
#

2 Marik
Этот эксепшн вызывает попытка обращения к уже уничтоженному врагу.

Описание бага: Если две и больше башней обстреливают одного врага и он погибает, то первая башня вызовет метод free(), а следующие башни продолжат обращаться к уничтоженному объекту по указателю_enemyTarget и так же попробуют его удалить.

FirstFlashGame
22 Ноября 2011
— 19:14
#

Ant.Karlov
Вся проблема была в том, что в уроке нет упоминания о проверке существования объекта перед его удалением:

В уроке написано так:
public function free():void
{
if (_sprite && contains(_sprite))
{
removeChild(_sprite);
}

_universe.removeChild(this);
_universe.bullets.remove(this);
}

В исходниках на самом деле так:
public function free():void
{
if (_sprite && contains(_sprite))
{
removeChild(_sprite);
}

if (_universe.contains(this)) Вот где собака зарыта (С)
{
_universe.removeChild(this);
}
_universe.bullets.remove(this);
}


Вывод:
проверка if (_universe.contains(this)) не позволяет удалить уже удалённый объект.

[b]Ant.Karlov[.b]
маленькая рассинхронизация между уроками и сорсами. Класс BulletBase.as, функция free().

FirstFlashGame
23 Ноября 2011
— 00:50
#

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

В любом случае спасибо за указание недостатка!Поправлю пост.

Ant.Karlov
23 Ноября 2011
— 11:04
#

Ant.Karlov
Но с другой стороны у читателей есть отличная возможность разобраться в том что ломается и как это можно починить

Это действительно так. Очень интересно наблюдать за развитием архитектуры, рассмотрением вариантов решения проблем, ну и самое главное – наблюдать за выбором оптимального решения согласно внятным критериям.

Спасибо за уроки!

FirstFlashGame
23 Ноября 2011
— 12:58
#

Огромное спасибо за Ваши статьи:мне как начинаещему разработчику они оказались очень кстати!

Stanislav
13 Января 2012
— 15:17
#

Антон, не знаю, читаете ли вы эту тему, доволноь времени многопрошло, но если вдруг заглянете непонятно
class ObjectController
строка _universe.addChild(obj); Компилятор ругается на неявное приведения типов . ? как я посмотрел в исходниках вы эти строки удалили, Т.к. добавляем и удаляем обьекты уже в Universe. Но может можно сделать в контроллере это все ,типа as Sprite или еще как-то? Просто я новиче ,непонятно еще

Роман
19 Марта 2012
— 20:19
#

to Роман
На самом деле ObjectController вообще не должен ничего знать об Universe. В уроке на сайте действительно используется Universe в середине ObjectController. В исходниках уже немного почищено, хотя ссылка на universe все равно осталась, но это не так важно так как в уроках остался отладочный код.

?так, задача контролера – хранить объекты, удалять/добавлять новые, и самое главное – вызывать update для своего хозяйства.
_universe.addChild(obj); -- лишняя строка в середине контролера. ?спользовалась для отладки, чтобы видеть как объекты действительно добавляются в контролер.
addChild – метод который добавляет объект на экран (в список отображения). Этот объект должен быть порожден от DisplayObject или его потомков (Sprite, MovieClip…)
Компилятор правильно ругается, так как obj это интерфейс IGameObject, а не класс -потомок DispalyObject. Если делать правильно, то необходимо привести напрямую к DisplayObject . Например:
var dispObj:DisplayObject = obj as DisplayObject;
if (null == dispObj) throw new Error("Попытка отобразить что-то левое...");
_universe.addChild(dispObj);
Но это лишь отладочный код !

FirstFlashGame
20 Марта 2012
— 15:28
#

Для TowerDeneces, по моему ?МХО, довольно важна кнопка ” >>” (ускорения времени). ?спользуется для того, чтобы игрок не заснул, если волны врагов для него становятся однородными. Скажем, построив очень мощную защиту из башен игрок может пожелать прокрутить несколько следующих волн побыстрее, так как враги легко уничтожаются. Функция полезная, но вот с реализацией куча проблем.
Понятно, что режим перезарядки, режим поиска, полет пули… все это должно напрямую зависеть от delta. Никаких константных инкрементов/декрементов на единичку или что-то подобное, только привязка к delta. Но вот закавыка, всегда существуют граничные условия, при которых башня или стреляет немного чаще, или находит цель немного быстрее, или враги слишком быстро срезают края… В результате при ускорении на 3x видно что волны врагов немного по другому уничтожаются. Еще большая скорость дает еще более грубые погрешности в игровом процессе. Как с этим бороться? ?ли вообще отказаться от ускорения, как это сделали в kingdom rush? Насколько действительно важна нопка “>>” для игры?

FirstFlashGame
28 Мая 2012
— 13:12
#

?так, относительно ускорения в игре.
Есть два похода, которые Вы описали в 3-ем уроке для расчета интервала между игровыми кадрами.
1) ?спользование плавающей delta через вызовы getTimer()
2) Задание константной частоты 1/stage.frameRate
Первый подход гарантирует плавность движения врагов, но не гарантирует корректной работы ускорения времени. Связано это с тем, что при слишком сильных колебаниях delta возможны серьезные отклонения. Скажем враг проскочил место удара пули, или наоборот, находился в зоне досягаемости пушки слишком долго.
Второй подход гарантирует одинаковое поведение при скачках FPS, но на движение врагов смотреть тяжело без слез – сплошные рывки.
В игровых расчетах для ускорения времени можно использовать три подхода:
1) Штучно поднимать framerate
2) Увеличить delta соразмерно требуемой скорости игры: delta*=getGameSpeed();
3) Внутри enterFrameHandler вызывать несколько раз подряд главную ф-ию update
Первый подход откровенный хак. Второй подход сложен в реализации и полон интересных грабель по всему коду. Третий имеет недостаток в плане производительности, так как все вычисления должны повторяться несколько раз, но с точки зрения минимизации багов он наиболее предпочтителен.
Вывод: использование статичного вычисления delta + многократный вызов ф-ии update для эмуляции увеличения скорости игры.
Минусы: увеличение нагрузки на CPU, а также отсутствие плавности в движения врагов.

FirstFlashGame
31 Мая 2012
— 12:04
#

Почитал комментарии, один вопрос по поводу контроллера решился сам собой, но вот тема про падение fps... может конечно эта тема уже устарела, но все же..

Если не ошибаюсь еще Колин Мук в своей книге предлагал вообще не использовать Enter_frame для обновления программы, а запустить для этого таймер... таймеры не должны терять скорость, вне зависимости от fps, или потери фокуса...

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

?ван Cheshir
10 Июня 2013
— 17:25
#

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

Если каждый спрайт не будет подписываться на enterFrame, а, например, завести один или 2-3 обработчика enterFrame в которых будут обновляться игровые объекты и игровая логика, то с производительностью не возникнет никаких проблем. Так же с несколькими enterFrame не составит труда реализовать и паузу для приложения при потери фокуса.

Ant.Karlov
11 Июня 2013
— 07:59
#

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

Так например:
Потом удалим все старые методы addEnemy(), removeEnemy(), addTower() и removeTower(). Таким образом переменные _listOfEnemy и _listOfTowers тоже больше не нужны.

Почему бы не сказать открыто что можно удалить все старые методы addEnemy(), removeEnemy(), addTower() и removeTower(). Таким образом переменные _listOfEnemy и _listOfTowers тоже больше не нужны. А значит можно удалить все методы что их используют в файлах .................

Нет сами сидите и копайте ваш код... и много уже таких случаев...

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

Надеюсь быть услышанным.

новичек
30 Июля 2014
— 17:53
#

@новичок, молодец что нашел в чем дело и исправил, но как там в классике звучало:

Шарик, поздравляю тебя, ты балбес (с) Каникулы в простоквашино

ну или просто взгляни на "проблему" с другой стороны не виданной тобой доселе :)

во многих фундаментальных трудах например Дональда Кнута о с++, которые помогли многим новичкам стать настоящими профессионалами с++, во всех примерах книги делались ошибки, что бы ученики не превращались в тупорылых копипастеров, а наоборот - исправление этих ошибок давало ученику понять усвоил он материал или нет.

P.s. хотя думаю вряд ли Антон делал что либо умышленно

Elmigo
30 Июля 2014
— 18:14
#

@Elmigo, Абсолютно с тобой согласен, но все же довольно трудно перекапать тонну кода и примерно понять где находится ошибка...

Но все же довольно полезно их делать в новых класах, новых частях кода и т.п., но если переписать 1/4 игрового кода и потом получить 15 ошибок типа:
AS3 #2007: Parameter child must be non-null.

? не пойми как же ее читать или что то типа:
TypeError: Error #1009: Cannot access a property or method of a null object reference.
at com.towerdefence.towers::TowerBase/init()
at com.towerdefence.towers::GunTower/init()
at com.towerdefence::Universe()
at com.towerdefence::Universe$/getInstance()
at com.towerdefence::Game()
at com.towerdefence::App/init()
at com.towerdefence::App()

как то не понятно где искать проблему...

Да и рас уже спустя год тут ктото есть, поделитесь опытом как читать подобные ошибки? Хотя бы как по данной ошибке понять в каком файле искать проблему, т.к. пробовал ставить точки остановки, но они как то не корректно отрабатывают... когда были подобные ошибки (там косяк был т.к. я пытаюсь не копипастить код а переписывать, перестраивать по своему, подписывать коментами тоже по своему, и в итоге более понять что где для чего) я не понимал где искать проблему, а когда ставил в Universe.as точку останова на импорте классов башен и на строчку раньше, все равно получал туже самую ошибку, как будто бы точек остановки не было...

PS. Не подскажите редактор как у автора только под Windows, просто судя по исходникам и первоначальным описаниям у автора стоит Mac. А так например Flash Professional CC 2014 отказались от панельки проект в связи с чем не очень удобно держать все классы во вкладках...

новичек
31 Июля 2014
— 08:34
#

@новичек, исходниками уже 3 года, за это время столько воды утекло, у меня вон к примеру AntHill компилялся для 10ой версии плеера, обновил и опа - какие-то непонятные ошибки, благо был уже опыт с компилянием примеров, стал поднимать версию в настройка, на 11.2 и скомпилялось.

знаю что хочется иметь в руках рабочий пример, но лучше бы вам взять только концепцию из статьи, а остальное вытворить собственноручно и рекомендую делать это на AntHill http://anthill.ant-karlov.ru/wiki/whatsnew
т.к. от многих административных моментов избавит

Elmigo
1 Августа 2014
— 22:01
#

Привет!) Ребята кто подскажет как в AS3 добавлять пулю четко на кончик ствола??? Ствол следит за мышью!!! Метод от Xitri var p = {x:tgt._x, y:tgt._y};
tgt._parent.localToGlobal(p);
globalToLocal(p);
не получилась, отправную точку(tgt._x, tgt._y) внутри ствола он не считывает, а выдает ошибку((
Подскажите пожалуйста не разумному программисту=)

Владимир
26 Сентября 2014
— 21:41
#

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

Нулевое положение угла находится ровно справа, значит точка которая будет поворачиваться должна быть справа на 50px (в том случае если ствол равен длине 50px).

Задаете точку для поворота следующим образом:

bulletPoint.x = weapon.x + weapon.width;
bulletPoint.y = weapon.y;

Далее идем по ссылке и находим там метод rotatePointDeg() (строка 346) и используем его следующим образом:

bulletPos = AntMath.rotatePointDeg(bulletPoint, weapon.position, weapon.angle);

Таким образом получаем в bulletPos — координаты где должна будет появится ваша пуля.

Хочу обратить ваше внимание что rotatePointDeg() использует класс точки AntPoint и если вы не используете Anthill. Просто возьмите этот метод в свой проект и замените все AntPoint на Point.

Ant.Karlov
27 Сентября 2014
— 19:12
#

@Ant.Karlov,спасибо большое и за Anthill-Framework тоже большое спасибо, очень удобная вещь!)

Владимир
29 Сентября 2014
— 08:23
#

if (_isAttaked)
{
  if (_sprite.currentFrame == 1)
  {
    _sprite.stop();
    _isAttaked = false;
  }
}

- как тут текущий кадр может быть 1ым при _isAttaked=true, если мы всегда сами готу`ем его на 2ой при
Временный эффект атаки
12 _sprite.gotoAndPlay(2);
13 _isAttaked = true;

...чувствую опять на мой наспех дурацкий вопрос никто не ответит пока сам не допру :)

igor
12 Марта 2015
— 20:44
#

О_о
А почему не сделать, чтобы Башни стреляли на упреждение, то есть целились на упреждение?

Скорость Враги у нас меняют не так часто, расстояние до них известно. Посчитать точку прицеливания несложно

А если Башни будут мазать из-за того, что Враг поменял скорость, то это нормально воспринимается

Дан
22 Июля 2015
— 15:54
#