TowerDefence #7. Движение врагов

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

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

  1. Рассчитываем угол между текущим положением и следующей целью;
  2. Рассчитываем векторную скорость согласно полученному углу;
  3. Двигаемся с расчитанной векторной скоростью до тех пор, пока не достигнем цели;
  4. Когда цель достигнута, переходим к пункту 1.

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

Математико

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

Amath.as — это класс с набором статических методов, использующихся для единоразовых расчетов. Перед использованием методов экземпляр класса создавать не нужно. Доступные методы:

Amath.random(lower:Number, upper:Number):Number;

— Возвращает случайное число в заданном диапазоне. Lower — минимальное число, upper —максимальное число.

Amath.getAngle(x1:Number, y1:Number, x2:Number, y2:Number, norm:Boolean = true):Number;

— Рассчитывает угол в радианах между двумя точками. x1, y1 — координаты первой точки, x2, y2 —координаты второй точки, norm — необязательный, отвечающий за корректировку результата. Этот метод нам пригодится для определения угла между текущей и следующей точкой. Так же у этого метода есть аналог, который производит такие же расчеты, но результат возвращает в градусах: Amath.getAngleDeg().

Amath.toDegrees(radians:Number):Number;

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

Amath.toRadians(degrees:Number):Number;

— Переводит градусы в радианы.

Amath.equal(a:Number, b:Number, diff:Number = 0.0001):Number;

— Сравнивает два значения с допустимой погрешностью. Если значения с учетом погрешности равны, то вернет true, не равны — false.

Это пока все методы, которые нам понадобятся из класса Amath.as, а далее по мере необходимости я буду добавлять и рассказывать о новых методах.

Теперь второй не менее интересный и важный класс Avector.as — это класс аналог стандартного Point, но с рассширенными возможностями. Перед использованием необходимо создавать экземпляр класса и он не имеет статических методов. Avector, как и Point, имеет два параметра x, y типа Number и несколько полезных методов для удобной работы с ними:

set(ax:Number, ay:Number):void;

— Устанавливает новое значение вектора.

copy(v:Avector):void;

— Копирует значения переданного вектора в текущий.

add(v:Avector):void;

— Складывает значения переданного вектора с текущим.

asSpeed(speed:Number, angle:Number):void;

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

equal(v:Avector, diff:Number = 0.0001):Boolean;

— Сравнивает текущий вектор с указанным вектором с заданной погрешностью. Для сравнения векторов используется метод Amath.equal().

Утилитные математические классы я поместил в пакет com.framework.math.* чтобы их как и движок поиска пути можно было легко переносить и использовать в любых других проектах.

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

EnemyBase.as

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

Открываем EnemyBase.as для редактирования и удаляем переменные _speedX и _speedY — в место двух переменных оставим одну, которая будет отвечать за базовую скорость каждого из врагов _defSpeed.

protected var _defSpeed:Number = 100;

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

protected var _speed:Avector = new Avector();

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

import com.framework.math.*;

Теперь перейдем в метод init() класса EnemyBase.as и заменим строчку:

_wayTarget = _way[_wayIndex];

на

setNextTarget();

Сейчас напишем метод setNextTarget():

protected function setNextTarget():void
{
  if (_wayIndex == _way.length)
  {
    // Весь маршрут пройден
    _isWay = false;
  }
  else
  {
    // Новая цель
    _wayTarget = _way[_wayIndex];
            
    // Расчет угла между текущими координатами 
    // и следующей точкой
    var angle:Number = Amath.getAngle(x, y, 
      Universe.toPix(_wayTarget.x), 
      Universe.toPix(_wayTarget.y));
            
    // Установка новой скорости
    _speed.asSpeed(_defSpeed, angle);

    // Разворот спрайта
    _sprite.rotation = Amath.toDegrees(angle);
  }
}

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

Кстати, я надеюсь вы заметили появление в коде нового метода toPix() — не знаю как вам, а для меня на деле оказалось проблематичным писать большущие формулы для перевода координат из тайлов в пиксели и обратно, например: pix = tile * Universe.MAP_CELL_SIZE + Universe.MAP_CELL_HALF. Во всяком случае я минут 5 потратил на поиски проблемы неправильного поведения юнита, когда пытался текущие координаты юнита в пикселях сравнивать со следующей ячейкой в тайлах :) Поэтому очень рекомендую реализовать и вам пару методов помощников в классе Universe.as:

public static function toTile(value:Number):int
{
  return int(value / MAP_CELL_SIZE);
}
        
public static function toPix(value:int):Number
{
  return MAP_CELL_HALF + value * MAP_CELL_SIZE;
}

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

EnemySoldier.as

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

Открываем EnemySoldier.as для редактирования и сразу переходим к методу update(), и меняем его следующим образом:

override public function update(delta:Number):void
{
  if (_isWay)
  {
    // Двигаем юнита
    x += _speed.x * delta;
    y += _speed.y * delta;
                
    // Обновляем текущие координаты в клеточках
    _position.x = Universe.toTile(x);
    _position.y = Universe.toTile(y);
                
    // Переходим к новому шагу, если 
    // текущая цель достигнута
    if (_position.x == _wayTarget.x && 
        _position.y == _wayTarget.y)
    {
      _wayIndex++;
      setNextTarget();
    }
}

Метод update(), конечно, по убавил в объеме и все это в основном благодаря тому, что все основные рассчеты мы перенесли в метод setNextTarget(). Тут я хочу обратить ваше внимание на то, что основные рассчеты выполняются всего один раз при переходе от одной ключевой точки пути к другой, и их результаты сохраняются в переменных _speed и _sprite.rotation для дальнейшего использования, что в итоге уже есть небольшая оптимизация :) Правда в дальнейшем, возможно нам придется выполнять перерасчет намного чаще, чем один раз при смене направления.

Теперь перейдите к методу init() класса EnemySoldier.as и удалите строчки с переменными _speedX и _speedY — они нам больше не нужны. Так же не забудьте импортировать в этот класс наши математические классы и протестируйте игру.

Ура! Юниты теперь бегут плавно, но что за дела, они срезают углы —непорядок. А все дело в том, что переход к следующей ключевой точке сейчас осуществляется у нас тогда, когда юнит только-только оказывается в целевой клетке, таким образом он немного раньше времени перенастраивается на новую цель, и выглядит это как срезание углов. Чтобы исправить этот недостаток, нужно сделать проверку на достижение юнитом не целевой клеточки, а конкретно целевой точки! Сделать это не сложно и метод equal() класса Avector.as поможет нам с этим, но для этого придется сделать некоторые изменения.

Если в своем проекте вы используете клип врага Solder_mc из моего примера, или свой, но развернут он у вас так же, как у меня (смотрит вниз), то вам следует повернуть клип на 45 градусов против часовой стрелки так, чтобы он смотрел ровно на право. ? переименуйте его тоже в Soldier_mc :)

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

override public function update(delta:Number):void
{
  if (_isWay)
  {
    // Двигаем юнита
    x += _speed.x * delta;
    y += _speed.y * delta;
                
    // Текущее положение
    var cp:Avector = new Avector(x, y);
                
    // Финишная точка
    var tp:Avector = new Avector(Universe.toPix(_wayTarget.x),
      Universe.toPix(_wayTarget.y));
                
    // Переходим к новому шагу, если 
    // текущая цель достигнута
    // Внимание! Чем больше скорость 
    // движения врага, тем больше 
    // должна быть погрешность
    if (cp.equal(tp, _defSpeed / 10))
    {
      // Обновляем текущие координаты в клеточках
      _position.x = Universe.toTile(x);
      _position.y = Universe.toTile(y);
                    
      _wayIndex++;
      setNextTarget();
    }
  }
}

Рассмотрим изменения: как видно из кода, теперь текущие координаты юнита мы сохраняем в вектор cp (сокращенно от currentPoint), а так же конвертируем координаты следующей клетки в пиксели и сохраняем их в вектор tp (сокращенно от targetPoint), после чего сравниваем два вектора с помощью метода equal(). Тут важно помнить, что, чем больше скорость движения врага, тем больше следует устанавливать погрешность, так как при большой скорости и маленькой погрешности враг может просто проскочить целевую точку, и, сбившись с курса, убежать далеко за пределы карты. Чтобы случайно не забыть об этом моменте в будущем, когда мы вдруг решимся скорректировать скорости врагов, я решил просто делить базовую скорость на некое число, чтобы получать пропорциональную погрешность для разных скоростей. Так же теперь мы обновляем текущее положение юнита в тайлах только тогда, когда он достиг целевой точки, что мне кажется наиболее логичным :)

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

Теперь можно протестировать игру! Сейчас юнит должен бежать правильно, держась строго между препятствий, и впридачу держать мордочку по курсу. Кстати, в одном из прошлых уроков я давал домашнее задание, в задачи которого входило реализовать установку юнита в определенный тайл. С тех пор, конечно, я не раз уже обновил исходники с уроками, где в методе инициализации init() появились параметры posX и posY, отвечающие за текущее положение врага в тайлах, но кода, устанавливающего юнита согласно этим параметрам в пиксели, я не писал, поэтому если вы этого еще не сделали, то добавьте в классе EnemyBase.as в метод init() пару строк:

// Устанавливаем положение врага в пикселях
x = Universe.toPix(tileX);
y = Universe.toPix(tileY);

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

public function init(tileX:int, tileY:int, targetTileX:int, targetTileY:int):void;

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

Редактируем редактор

Для задания стартовой и финишной точки нам необходимо немного модернизировать наш редактор уровней и даже более того, мы наконец-то сделаем его максимально похожим на полноценный редактор уровней, добавив панель инструментов и возможность протестировать текущий уровень в игре. Первым делом мы создадим все необходимые кнопочки для панели инструментов: Busy_btn, BuildOnly_btn, StartPoint_btn и FinishPoint_btn — это должны быть квадратненькие кнопочки с размером на ваше усмотрение, но чем меньше они будут, тем лучше ;) 

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

Когда клипы кнопочек готовы, можно приступать к их реализации в игре. Открываем Editor.as и в методе init() пишем следущий код:

// Создаем кнопку "занята"
_btnBusy = new Busy_btn();
_btnBusy.x = App.SCREEN_WIDTH - _btnBusy.width / 2 - 52;
_btnBusy.y = _btnBusy.height / 2 + 10;
_btnBusy.addEventListener(MouseEvent.CLICK, busyHandler);
addChild(_btnBusy);
            
// Создаем кнопку "только для строительства"
_btnBuildOnly = new BuildOnly_btn();
_btnBuildOnly.x = App.SCREEN_WIDTH - _btnBuildOnly.width / 2 - 10;
_btnBuildOnly.y = _btnBuildOnly.height / 2 + 10;
_btnBuildOnly.addEventListener(MouseEvent.CLICK, buildOnlyHandler);
addChild(_btnBuildOnly);
            
// Создаем кнопку "стартовая точка"
_btnStart = new StartPoint_btn();
_btnStart.x = App.SCREEN_WIDTH - _btnStart.width / 2 - 52;
_btnStart.y = _btnStart.height / 2 + 52;
_btnStart.addEventListener(MouseEvent.CLICK, startPointHandler);
addChild(_btnStart);
            
// Создаем кнопку "финишная точка"
_btnFinish = new FinishPoint_btn();
_btnFinish.x = App.SCREEN_WIDTH - _btnFinish.width / 2 - 10;
_btnFinish.y = _btnFinish.height / 2 + 52;
_btnFinish.addEventListener(MouseEvent.CLICK, finishPointHandler);
addChild(_btnFinish);

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

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

Brush.as

Давайте создадим новый пакет (папку) com.towerdefence.editor — здесь у нас будут храниться все необходимые классы для работы редактора уровней. ? сразу добавим в него новый класс Brush.as. В этом классе у нас будет пока несколько публичных переменных:

// Вкл/выкл рисование
public var drawMode:Boolean = false;
// Вид ячейки в кисточке
public var kind:int = Universe.STATE_CELL_BUSY;
public var tileX:int = -1;
public var tileY:int = -1;

Конструктор класса оставим пустым и собственно на этом с этим классом пока все.

Universe.as

Теперь следует научить наш игровой мир изменять карту проходимости на основе информации из кисточки. Для этого создадим новый метод applyBrush(brush:Brush) в классе Universe.as:

public function applyBrush(brush:Brush):void
{
  // Рисуем только если изменилось положение кисти
  if (brush.tileX != cellPosX || brush.tileY != cellPosY)
  {
    // Если ячейка занята, то освобождаем её
    var cellState:int = getCellState(cellPosX, cellPosY);
    if (cellState != STATE_CELL_FREE &&
        cellState == brush.kind)
    {
      setCellState(cellPosX, cellPosY, STATE_CELL_FREE);
    }
    else
    {
      setCellState(cellPosX, cellPosY, brush.kind);
    }
        
    brush.tileX = cellPosX;
    brush.tileY = cellPosY;
    updateDebugGrid();
  }
}

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

Теперь вернемся обратно к классу редактора Editor.as и первым делом добавим переменную для нашей кисточки:

private var _brush:Brush = new Brush();

? напишем следующий код для всех наших обработчиков кнопок:

private function busyHandler(event:MouseEvent):void
{
  _brush.kind = Universe.STATE_CELL_BUSY;
}
        
private function buildOnlyHandler(event:MouseEvent):void
{
  _brush.kind = Universe.STATE_CELL_BUILD_ONLY;
}
        
private function startPointHandler(event:MouseEvent):void
{
  _brush.kind = Universe.STATE_CELL_START;
}
        
private function finishPointHandler(event:MouseEvent):void
{
  _brush.kind = Universe.STATE_CELL_FINISH;
}

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

public static const STATE_CELL_START:int = 4;
public static const STATE_CELL_FINISH:int = 5;

Я надеюсь вы помните, что номер, присваиваемый каждой константе состояния ячейки, у нас соотвествует номеру кадра в клипе DebugCell_mc, поэтому после этого обязательно добавьте еще 4 и 5 кадры, чтобы новые ячейки у вас отображались. Стартовую клеточку я покрасил в зелененький цвет, а финишную в красный. Еще не забудьте импортировать классы Universe.as и Brush.as в Editor.as:

import com.towerdefence.editor.*;
import com.towerdefence.Universe;

После всего этого добавим в Editor.as в метод init() пару новых обработчиков мышки:

_universe.addEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler);
_universe.addEventListener(MouseEvent.MOUSE_UP, mouseUpHandler);

? напишем следующий код для методов mouseDownHandler() и mouseUpHandler():

private function mouseDownHandler(event:MouseEvent):void
{
  _brush.drawMode = true;
  _universe.applyBrush(_brush);
}

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

private function mouseUpHandler(event:MouseEvent):void
{
  _brush.drawMode = false;
  _brush.tileX = -1;
  _brush.tileY = -1;
}

Когда кнопка мышки отжата, то мы выключаем режим рисования и сбрасываем последнее положение кисточки — это обязательно нужно сделать, чтобы при последущем рисовании все корректно обрабатывалось. А теперь в уже существующий обработчик мышки mouseEventHandler() в классе Editor.as мы добавим еще чуть-чуть кода:

private function mouseEventHandler(event:MouseEvent):void
{
  _universe.updateMousePos(event.stageX, event.stageY);
  if (_brush.drawMode)
  {
    _universe.applyBrush(_brush);
  }
}

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

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

Старт и финиш

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

private var _startPoints:Array = [];
private var _finishPoints:Array = [];

Далее в классе Universe.as напишем такой вот метод:

public function preparePoints():void
{
  // Очищаем списки
  _startPoints.length = 0;
  _finishPoints.length = 0;
            
  // Перебираем карту проходимости и создаем новые точки
  for (var ty:int = 0; ty < _mapMask.length; ty++)
  {
    for (var tx:int = 0; tx < _mapMask[0].length; tx++)
    {
      switch (_mapMask[ty][tx])
      {
        // Стартовая точка
        case STATE_CELL_START :
          _startPoints.push(new Avector(tx, ty));
          if (!_isEditor)
          {
            _mapMask[ty][tx] = STATE_CELL_FREE;
          }
        break;

        // Финишная точка   
        case STATE_CELL_FINISH :
          _finishPoints.push(new Avector(tx, ty));
          if (!_isEditor)
          {
            _mapMask[ty][tx] = STATE_CELL_FREE;
          }
        break;
      }
    }
  }
}

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

Теперь добавьте вызов этого метода в сеттер Universe.mapMask():

public function set mapMask(value:Array):void
{
  _mapMask = value;
  preparePoints();
  updateDebugGrid();
}

Осталось только научить вражеских юнитов использовать эти списки. ?справим метод newEnemy() следующим образом:

public function newEnemy():void
{
  // Создание тестового врага
  var soldier:EnemySoldier = new EnemySoldier();

  // Выбираем случайную стартовую точку и финишную
  var startPos:Avector = _startPoints[Amath.random(1, _startPoints.length) - 1];
  var finishPos:Avector = _finishPoints[Amath.random(1, _finishPoints.length) - 1];

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

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

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

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

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

  1. Класс Game.as создавать всегда независимо от того, какая кнопка нажата в самом начале. А класс Editor.as создавать только когда нажата кнопка «Editor».
  2. В редактор добавить кнопку «Test», а в игру кнопку «Editor». Но кнопка редактор в игре должна появляться только тогда, когда вы попали в игру из редактора.
  3. При тестировании уровня из класса Editor.as не удалять класс Editor.as, а только скрывать и показывать класс Game.as. Так же при тестировании уровня важно не передавать указатель на маску из редактора, а именно копировать маску в новый массив и передавать указатель на него в игровой класс Universe.mapMask = copyOfMask;
  4. Чтобы потом было проще поддерживать такой способ тестирования уровней в последующих версиях игры, я рекомендую вам передавать параметры из редактора в игру не напрямую, а через специальный класс TestLevel.as унаследованный от LevelBase.as.

Обновлено: Можете не пытаться делать домашнее задание согласно описанным здесь действиям. Когда я их писал я совершенно забыл, что наш игровой мир является синглтоном и если одновременно создать классы Game.as и Editor.as то для них будет существовать общий игровой мир, поэтому подход к тестированию уровня из редактора должен быть совсем иным от описанного здесь.

Заключение

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

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

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

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

Добавлено (15.03.2011):

Результат

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

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

Да, не зря я сомневался по поводу домашнего задания и вообще в реализации тестирования уровня в игре прямо из редактора уровней. Чтобы быстро добавить эту возможность мне пришлось забить несколько «костылей» в существующую структуру игры. Сама задача конечно легко реализуема, но нужна небольшая реконструкция игры, что я конечно в рамках урока делать не собираюсь. Более того в будущих уроках формат уровней у нас еще усложнится и поэтому быстрое тестирование уровней мы реализовывать не будем. Далее исходники с не самым удачным решением домашнего задания для этого урока.

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

Содержание

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

 

 

? когда ты успеваеш все :) Спасибо за пост - почитаем!

oleg
15 Марта 2011
— 12:04
#

Антон спасибо!!!!
Z а тебе?

?ван
15 Марта 2011
— 13:04
#

Большое спасибо!

Семен
15 Марта 2011
— 13:26
#

Огромное спасибо тебе, и респект за труды.

Анонимус
15 Марта 2011
— 13:56
#

не работает нихрена

Конь
15 Марта 2011
— 14:12
#

чот не робит(

Legioner
15 Марта 2011
— 14:45
#

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

elmortem
15 Марта 2011
— 14:50
#

@Конь и Legioner, что у вас не работает?

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

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

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

Благодарю за урок!
Вопрос:
public function EnemySoldier()
{
super(); - вызывает конструктор EnemyBase??
}

Vacsa
15 Марта 2011
— 17:53
#

@Vacsa, да! :)

Ant.Karlov
15 Марта 2011
— 17:55
#

Внимание! Запись обновлена и добавлено небольшое дополнение, а так же исходники с решенным домашним заданием.

Ant.Karlov
15 Марта 2011
— 17:57
#

Спасибо, Антон. Чувствую, на этой неделе ждет меня как минимум одна бессонная ночь. :)

Кстати, а юниты по карте еще не ездят? ?ли я просто не разобрался, как их запустить?

jarofed
15 Марта 2011
— 19:52
#

@ДЖАРОД@ чо-то клацал и какие-то шарики катались по лабиринту... ^_^
@КАРЛОВ vs ВАКСА@ - супер() ))))))))))))
@САМ СЕБЕ@ НАследование Антонов:
Антон Антону не Робокоп ))))

tananuka
15 Марта 2011
— 20:04
#

ура! я уж думал что-то случилось =)
спасибо.

Demoralizator
15 Марта 2011
— 21:52
#

Когда мы в едиторе и жмем тест приходится много раз нажимать пробел чтоб солдат начал свое движение

?ван
16 Марта 2011
— 09:27
#

@tananuka Ты походу покурил что-то прежде чем писать тут...смотри много не балуйся ;)

Vacsa
16 Марта 2011
— 13:04
#

Вот не пойму зачем делать собственный метод update для класса EnemySoldier. Сомневаюсь, что у какого-нибудь EnemyKnight он будет другим. Не лучше ли сделать общий update в EnemyBase?

Nikius
16 Марта 2011
— 16:35
#

Отличный урок, редактор это круто!
Тоже поражаюсь как ты всё успеваешь)

J0x
11 Апреля 2011
— 06:55
#

Вотесли честно непонятно как они у вас все-таки бегают в разные позиции тайтла. Можете как-то подробнее объяснить и просче?

pavezlo
11 Апреля 2011
— 23:18
#

@pavezlo, чтобы юниты бежали в разные позиции тайла, достаточно к центру позиции тайла добавить случайное число по X и Y. Например:
targetPoint.x = Amath.random(0, 10);
targetPoint.y = Amath.random(0, 10);

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

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

Marik
28 Июня 2011
— 11:47
#

Правда говорят "Утро вечера мудренее" :)

Выспался и на свежую голову разобрался :)

Теперь все работает, спасибо за урок, успехов Вам Антон во всех делах :)

А я пошел ковыряться в 8 уроке )

Marik
1 Июля 2011
— 12:40
#

"так как при большой скорости и маленькой погрешности враг может просто проскочить целевую точку, и, сбившись с курса, убежать далеко за пределы карты"
Есть еще одна проблема: если минимизировать окно с бразузером, а потом восстановить его, то получим разбежавшихся в разные стороны юнитов. Баг можно воссоздать и другими способами. Это следствие влияния delta на окончательный результат вычислений.

P.S. Действительно, СПАС?БО за эти уроки! Очень приятно двигаться вперед, не придумывая велосипед и не вставая много раз на ненужные грабли.

FirstFlashGame
18 Ноября 2011
— 19:06
#

@FirstFlashGame, чтобы не получить проблемы которую вы описали при «подвисшем» Flash Player следует сделать фиксированный delta time, либо добавить минимальное и максимальное значения которые delta time не должно превышать.

Ant.Karlov
19 Ноября 2011
— 10:57
#

// Внимание! Чем больше скорость
// движения врага, тем больше
// должна быть погрешность

if (cp.equal(tp, _defSpeed / 10))

а я вот так сделал)
_dSpeed = _defSpeed/ (_defSpeed* .15);
.....
if (cp.equal(tp, _dSpeed)){

протестил и при разных скоростях работает норм)

P.s. Огромное спасибо за Ваши уроки! очень понятно и интересно!! прямо оторваться не могу!!)

Захар
29 Августа 2012
— 14:57
#

Сделал такой поиск пути для игрушки, где есть герой и им можно ходить: значит для мобов должен обновляться путь при изменении позиции героя. Сделал так:
if (_lastDist < Matah.distanceTo(x, y, _coordTarget.x, _coordTarget.y)) {
newWay();
}
Т.е. если дистанция до игрока больше, чем в предыдущем update, то ищем новый путь. Сделал, чтобы мобы двигались за курсором. ? если на экране всего мобов 30, и я перемещаю курсор - то происходят нереальные висы. А в игре ведь игрок постоянно будет перемещаться. Как можно это оптимизировать?

Геныч Defake
14 Июля 2013
— 00:34
#

@Геныч Defake, поиск пути — это всегда достаточно затратная задача. Чтобы оптизимировать этот процесс, вам как минимум следует выполнять поиск пути с большим интервалом. То есть если игрок сместился на 2-5 пикселей — это совсем не повод чтобы пересчитывать путь для всех юнитов, так как в этом случае путь врядли сильно изменится. Делайте большие задержки между итерациями поиска пути.

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

Ant.Karlov
18 Июля 2013
— 05:52
#

А зачем мы считаем тригонометрию при определении скорости?!

почему не
1. посчитать разницу векторов Цель - Положение,
2. и нормализовать ее до желаемой скорости

Линейные операции

(только прийдется посчитать угол для rotation)

Дан
20 Июля 2015
— 11:23
#