TowerDefence #4. Готовимся к поиску пути
Очень мне хотелось в данном уроке уже рассказать о том, как мы будем искать путь, но пока еще не все для этого готово. Нужно сделать некоторые исправления и добавить новый функционал в нашу игру.
Чтобы урок про поиск пути получился интересным и полным за счет возможности протестировать работу алгоритма сразу же — нам нужно для этого сделать еще некоторые приготовления.
Обработка мышки
Думаю вам врядли захочется создавать карту проходимости для тестирования поиска пути, задавая координаты занятых клеточек в коде, как мы это делаем сейчас. Поэтому первым приготовлением к поиску пути мы сделаем возможность редактировать карту проходимости, используя мышку. Саму карту проходимости редактировать несложно, ведь мы позаботились о необходимых для этого методах. Поэтому основной нашей задачей будет сделать именно перехват и обработку курсора мышки.
Об этом много рассказывать не нужно, поэтому сразу открываем класс Game.as и в конструкторе класса пишем следующий код:
addEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler);
В заголовке класса не забываем импортировать класс:
import flash.events.MouseEvent;
Теперь пишем обработчик движения мышки:
private function mouseMoveHandler(event:MouseEvent):void { _universe.updateMousePos(event.stageX, event.stageY); }
Как видно из кода, используя метод updateMousePos() класса Universe.as, мы передаем координаты мыши в игровой мир. Теперь нам необходимо написать метод updateMousePos(), чтобы обрабатывать координаты мышки. Переходим к редактированию класса Universe.as и добавляем новые публичные переменные:
// Текущее положение курсора мыши в пикселях public var mousePosX:int = 0; public var mousePosY:int = 0; // Текущая ячейка, над которой находится курсор мыши public var cellPosX:int = 0; public var cellPosY:int = 0;
Эти переменные у нас будут публичными для того, чтобы любые другие игровые классы могли использовать их. Далее создаем метод updateMousePos():
public function updateMousePos(mouseX:int, mouseY:int):void { mousePosX = mouseX; mousePosY = mouseY; // Координаты тайла, над которым находится курсор мыши cellPosX = int(mouseX / MAP_CELL_SIZE); cellPosY = int(mouseY / MAP_CELL_SIZE); // Подсветка текущего тайла _currentCell.x = MAP_CELL_HALF + cellPosX * MAP_CELL_SIZE; _currentCell.y = MAP_CELL_HALF + cellPosY * MAP_CELL_SIZE; }
Тут, как можно видеть из кода, я сразу добавил подсветку текущей клетки и еще одну новую константу, которая хранит в себе половину игровой ячейки в пикселях. Для чего это нужно, станет понятно позже, а пока объявим её в заголовках класса Universe.as:
public static const MAP_CELL_HALF:int = 16;
В качестве подсветки текущего тайла выступает обычный MovieClip из библиотеки клипов в TowerDefence.fla. Добавьте в папку _DEBUG новый клип с именем CurrentCell_mc и нарисуйте в нем полу-прозрачный квадратик или рамочку размером 32x32 пикселя, и выровняйте его по центру. Возвращаемся в Universe.as и добавляем новую приватную переменную, которая будет содержать этот квадратик:
private var _currentCell:Sprite;
В конструкторе мира напишем его инициализацию и обязательно после строк создания игровой маски и отладочной сетки, чтобы подсветка оказалась поверх сетки:
_currentCell = new CurrentCell_mc(); addChild(_currentCell);
Тестируем! Если все сделано правильно, то вы должны увидеть как квадратик-подсветка перемещается за курсором, и подсвечивает ту ячейку, над которой находится курсор мыши.
Если у вас что-то не работает и при этом не выводится никаких ошибок, то ищем причину, следуя шагам:
1. В методе mouseEventHandler() класса Game.as добавляем следующий код:
trace(event.stageX, event.stageY);
Тестируем приложение и смотрим, что выводится в окне output. Если в окне бегут цифры (координаты мыши), значит здесь все ок. ?наче перечитываем еще раз все внимательно или сверяемся с исходником.
2. Если вы вообще не видите спрайта _currentCell или он застыл в правом верхнем углу экрана, то добавляем trace всех координат в методе updateMousePos() и проверяем, что выводится в окне output. В особенности обратите внимание на координаты клипа _currentCell.x и _currentCell.y. Далее ищим причину ошибки самостоятельно или сверяемся с исходником.
Вопрос на засыпку: Как вы думаете зачем мы перехватываем координаты мыши непосредственно в классе Game.as, а не в игровом мире?
Доработка игровой карты
Теперь нам всегда известны текущие координаты мышки и координаты ячейки, над которой находится мышка. ? мы даже сделали подсветку текущей клетки, но это еще не все — нам нужно немного доработать игровую карту.
При создании игровой карты мы выровняли квадратики по левому верхнему углу внутри клипа, то есть центром каждой ячейки является левый верхний угол. Таким образом зная, например координаты ячейки x-3, y-2 мы можем узнать координаты этой ячейки в пикселях так:
cell.x = 3 * MAP_CELL_SIZE; cell.y = 2 * MAP_CELL_SIZE;
Сейчас у нас реализовано так.
Сейчас недостаток только один, так мы узнаем координаты левого верхнего угла ячейки. А по-хорошему, чтобы потом было удобно работать с картой и юнитами, нам нужно сделать так, чтобы мы получали реальные координаты центра ячейки в пикселях.
А нужно сделать так.
Узнать правильные коордианты ячейки очень просто, к расчетам нужно добавить лишь половину ширины и высоты ячейки, которую мы уже успели вынести в отдельную константу:
cell.x = MAP_CELL_HALF + 3 * MAP_CELL_SIZE; cell.y = MAP_CELL_HALF + 2 * MAP_CELL_SIZE;
Ленивым заводить лишнюю константу для хранение размера половины ячейки я очень не рекомендую делать так: cell.x = MAP_CELL_SIZE * .5 + 3 * MAP_CELL_SIZE; Это не единственное место, где нам понадобится размер половины ячейки, и в итоге у вас может получится много лишних вычислений.
Теперь, чтобы карта выводилась согласно новым требованиям, нам нужно переделать метод makeDebugGrid(), добавив в место обнуления координат каждой ячеки присвоение значения MAP_CELL_HALF. Более того, я переименовал этот метод в updateDebugGrid(), так как каждый раз, удаляя или изменяя состояние карты проходимости, нам прийдется его повторно вызывать. ? поскольку старая его реализация была очень не оптимальна для постоянного его вызова, я его немного оптимизировал следующим образом:
private function updateDebugGrid():void { // Графический образ ячейки var cellSprite:MovieClip = new DebugCell_mc(); // Растровый холст для рисования сетки var bmpData:BitmapData = new BitmapData(MAP_CELL_SIZE * MAP_WIDTH_MAX, MAP_CELL_SIZE * MAP_HEIGHT_MAX, true, 0x00000000); // Начальное положение текущей ячейки по высоте/ширине var matrix:Matrix = new Matrix(); matrix.tx = MAP_CELL_HALF; matrix.ty = MAP_CELL_HALF; // Двигаемся по высоте карты for (var ay:int = 0; ay < MAP_HEIGHT_MAX; ay++) { // Двигаемся по ширине карты for (var ax:int = 0; ax < MAP_WIDTH_MAX; ax++) { // Переключаем состояние ячейки cellSprite.gotoAndStop(getCellState(ax, ay)); // Рисуем текущую ячейку bmpData.draw(cellSprite, matrix); // Меняем положение текущей ячейки по ширине matrix.tx += MAP_CELL_SIZE; } // Меняем положение текущей ячейки по высоте matrix.ty += MAP_CELL_SIZE; // Обнуляем ширину matrix.tx = MAP_CELL_HALF; } // Передаем растровый холст в картинку _debugGrid.bitmapData = bmpData; cellSprite = null; }
Принципиальное его отличие от прошлой реализации только в том, что мы избавились от временного контейнера grid и сейчас не создается на каждую ячейку отдельный MovieClip. В новой реализации создается растровый холст и, используя один единственный MovieClip, мы словно штампом отпечатываем на холсте все клеточки по очереди, предварительно переключая состояние этого штампа (клипа) в зависимости от состояния клетки. В качестве координат смещения клипа используется класс Matrix, в котором сразу указываются координаты где рисовать клип. Сам же клип в структуру отображения не добавляется и его координаты не обновляются. Когда весь холст заштампован ячейками, мы присваевам его нашей растровой картинке Bitmap. Обратите внимание, что создание и добавление картинки _debugGrid было вынесено за пределы этого метода, так как она больше не пересоздается и нет необходимости каждый раз её удалять и добавлять снова. Мы меняем только графические данные в bitmapData. Так что не забудьте добавить инициализацию нашей отладочной сетки в конструктор Universe.as:
_debugGrid = new Bitmap(); addChild(_debugGrid);
Далее, чтобы наши графические ячейки вставали правильно, нужно еще отредактировать клип DebugCell_mc во Flash IDE. Открываем клип для редактирования и на всех ключевых кадрах выравниваем квадратики по центру холста.
Тестируем! Теперь все должно быть так как нам нужно.
Редактирование карты проходимости
Теперь самый простой и самый вкусный этап на сегодня — добавляем возможность редактирования карты проходимости. Открываем Universe.as и добавляем в заголовок класса импортирование flash.events.MouseEvent, как мы это делали для класса Game.as и в конструкторе класса Universe.as добавляем слушателя:
addEventListener(MouseEvent.MOUSE_UP, mouseUpHandler);
Далее добавляем метод mouseUpHandler() — это обработчик отжатия кнопки мыши:
private function mouseUpHandler(event:MouseEvent):void { if (getCellState(cellPosX, cellPosY) == STATE_CELL_BUSY) setCellState(cellPosX, cellPosY, STATE_CELL_FREE); else setCellState(cellPosX, cellPosY, STATE_CELL_BUSY); updateDebugGrid(); }
Тут я думаю все должно быть просто и понятно без дополнительных комментариев. ?спользуя методы getCellState() и setCellState(), а так же текущие координаты клетки, мы освобождаем занятую клетку при клике и наоборот, пустую клеточку занимаем, а в конце обновляем отладочную сетку. Тестируем!
Если все сделано правильно, то УРА! Мы можем наконец-то лицезреть самый простейший редактор карты проходимости, и это проклюнулась только самая макушечка большущего айсберга.
Теперь почти все готово для написания алгоритма поиска пути на карте, за маленьким исключением. Наш первый враг тоже должен соотвествовать внесенным изменениям в игровую карту, его центр должен соотвествовать центру спрайта и ещё он должен устанавливаться в определенную клеточку на карте, и уметь находить координаты текущей своей клетки при движении.
Домашнее задание
Это будет первое важное домашнее задание, так как его результаты нам понадобятся в следующей части урока. Так же оно поможет вам хорошо усвоить то, что мы разобрали в предыдущих уроках, включая сегодняшний. Очень рекомендую вам разобраться с ним самостоятельно. Домашнее задание состоит из нескольких задач:
- Добавить локальные переменные для хранения координат текущей ячейки врага в классе EnemyBase.as: _cellPosX, _cellPosY;
- Рассчитывать текущую ячейку врага при его движении в методе update() в классе EnemyBase.as. ? не забудьте добавить вызов родительского метода в EnemySoldier.update() — нужно это, чтобы для всех врагов был один рассчет координат. Подсказка: решение для рассчета текущей ячейки ищите в методе Universe.updateMousePos();
- Добавить в EnemyBase.as публичный метод setToCell(cellX:int, cellY:int), в котором необходимо производить рассчет нового положения врага, исходя из координат ячейки и помещать врага в данную ячейку на карте. Решение для рассчета текущего положения врага согласно указанной ячейки ищите в методе Universe.updateMousePos();
Проверить домашние задание вы сможете самостоятельно сверившись с исходниками из следующего урока.
Заключение
Самое большое сегодняшнее достижение — это добавленная возможность редактирования карты проходимости прямо во время игры. Эта фича нам очень пригодится для тестирования алгоритма поиска пути. Так же мы немного доработали игровую карту и оптимизировали отрисовку отладочной сетки. Теперь когда будет доработан класс врага согласно домашнему заданию, мы уже в следующем уроке сможем приступить непосредственно к написанию алгоритма поиска пути.
Ссылка на исходники — CS4, *.zip, 102кб.
Содержание
- Вступление
- Структура игры
- Карта проходимости
- Первый враг
- Готовимся к поиску пути
- Поиск пути
- Редактор уровней
- Движение врагов
- Первая башня
- Кэширование объектов
- Полоса жизни
- Вражеские волны
- Загрузка вражеских волн
- Продолжение следует...
@genm, для отрисовки отладочной сетки не особо принципиально как её рисовать. В первом варианте я вообще показал самый жуткий пример... ну чтобы потом не делали так :) copyPixels быстрее, да.
1. Таймер не советую использовать, потому что Олег Антипов проводил по этому поводу эксперимент и результаты, честно говоря, не внушают доверия.
2. Что имеется в виду под «информацией о прохождениях»? Какие игровые уровни игрок прошел, а какие нет? Если да, то для этого используется SharedObject.
Привет, отличная статья!
Можешь сказать, что за шрифт вы используете здесь:
http://www.ant-karlov.ru/formidable/uploads/2004-td4-pic1.png ?
Спасибо!!! Очень интересно!
я прям не успеваю)) спасибо огромное за статью! бум ковырять =^_^=
Отлично. Как всегда.
Антон, интересно, почему мы используем именно MOUSE_UP, а не MOUSE_CLICK? Это принципиально и дает какие-то преимущества?
@rock, это моноширный шрифт Monaco из серии стандартных шрифтов Mac OS. На просторах ?нтернет я встречал его версию для Windows но с наскоку найти не удалось.
@Iktash, в данном случае не принципиально. Можно использовать и MOUSE_CLICK.
2Ant.Karlov
Гм, спасибо.
Жаль только, что под Win они выглядят совершенно не так, как на той картинке.
http://www.peeep.us/bbfa12e2
@rock, поставь размер шрифта 9px (или 10px) и отключи ClearType (сглаживание) для шрифтов и тогда должно будет выглядеть также.
ааа)))) робокопов в спам)))))
под планшетом валяюсь!
это выстрел в моё сердце!
PS: простите за мой латентный любовный оффтоп))))
Было бы здорово пару уроков по Box2d. Как подключать и тд. А то непонятно.
Не думаю, что в этом Товер Дефенсе будет Бокс2Д))
Я не про Товер Дефенс а вообще.
?нтересная статья! Спасибо!
А поиск пути будет на основе волны?
Я готов к поиску пути :) Поехали :)
Я встрял в самом начале, в классе Game(). У меня конструктор обрабатывается, но до метода mouseMoveHandler даже не добирается. Как-будто не выполняется слушатель (всё испортировал и ошибок нет при компиляции). ? ещё, в классе создал:
private var _universe:Universe;
а то ошибку он выдаёт. Я не знаю, может _universe ты хочешь доставать из App.as ,раз не говоришь что нужно создавать переменную новую. Но она там приватная, да даже если делать публичную, то всё равно ошибку выдает. В общем я встрял в самом начале. Сильно не пинайте меня(
Может кто тыкнет пальцем что я делаю не так.
Очень интересно.
P.S.Новый индикаторы симпатичные ;)
Всё, скачал исходник, вопрос отпал. Эх, не соображаю еще до конца))
Антон, а какой ответ на "вопрос на засыпку"? (:
@Ant.Karlov, подскажи где можно подробнее почитать о методах растеризации. Я не особо понял как мы оптимизировали нашу игровую сетку ни в первый раз (Урок #2), ни сейчас. То есть конечно разжевать для себя сам процесс можно благодаря комментариям. Но в голове не укладывается как до этого можно додуматься самому. Если понадобится когда-нибудь в своем проэкте.
Ты сам придумал оба эти способа или все же это общепринято так делать?:)
Raketa вместо варианта создать кучу квадратиков - MovieClip и разместить их сеткой, Антон сделал один холст, на котором нарисовал сетку из этих квадратиков и в итоге на экране только один объект, это холст, а до этого было много MovieClip`ов. Ну это я так понял, сам бы так не делал, сделал бы кучу мувиков, мне почему то кажется что на обработку непосредственно графических эффектов, убивается куда больше производительности, а у нас там просто квадратики)
Ant.Karlov, решил начать делать ТД, как бы банально это не звучало, попробую применить твои приемы) За одно проверить эффективность оптимизации.
Отличный блог. Спасибо что вы делитесь своими знаниями и приёмами по созданию игр.
Я новичок в этом, не считая нескольких головоломок на Делфи и бэйсике.
Есть одно замечание по поводу статей. Мне кажется, надо было сначала объяснить подробнее что-такое TowerDefence или изобразить схематично что в итоге должно получится. А то я это понял только после 4 урока. ? пришлось заново осмысливать первые три.
ABER, не хочу обижать но если вы не знаете что такое TowerDefence, то тут никакие уроки не помогут :)
?ван, я очень признателен Антону за столь подробное описание создания игры.
Просто высказываю свои предложения, в них нет ничего оскорбительного.
В том то и дело, я не знаю что такое TowerDefence, поэтому с удовольствием читаю уроки.
@ABER Просто аудитория уроков активные игроки-начинающие разработчики,поэтому TD понятие самособой разумеющиеся. Рекомендую просто поиграть в пару таких игр, чтобы все уяснить.
Привет, не знаю видел этот ужас или нет но вот:
http://www.4v4.com/free-online-car-games/664/santa-truck.html
Зачетная игрушка!)))
Еще одна игра в коллекцию клонов))
Блин я такую сделать хотел :(
Hello,
1 of my crew members did buy the source and did redesign it.
We payed a reasonable amount for the source. So it isnt stolen.
With kind regards,
Vasco Rouw
Это я по теме игры, им жалобу написал, это ответ. Ждем Антона, что он скажет :).
А смысл жалобы писать графика собственная. А то что машинка везет как у Антона то тут на мои взгляд особого плагиата нету.
Графика на фоне как у Антона, и уровни один в один. Если они код не купили, то это воровство.
Словно бы никто обфускацию и не придумывал... О контрольных суммах тоже как бы никто не подозревает... Почему бы не защитить свои разработки от подобных нападок?
Все равно сломают
@WeslomPo
После правильной обфускации потребуется муторный и долгий процесс рефакторинга, да и то исходный вид кода все равно не будет получен.
А с контрольными суммами подменять графику тоже весьма затруднительно, особенно если понаставить эти проверки всюду где только можно.
Тут уже встает вопрос о выгодности этого мероприятия по взломщику.
BuxomBerry, а можно поподробнгее об контрольной суме во флеше? Можно статеику на вашем блоге накидать...
http://flashgameblogs.ru/blog/actionscript/291.html
Как защитить вашу игру от ребрендинга
Перевод поста с блога ФГЛ
Ок спс
Что то Антон опять дето пропал :( Наверное продолжение ремонта...
@?ван: Действительно. Обещал перед НГ писать чаще и пропал. Надеюсь, что с ним все в порядке
@WeslomPo, исходники игры несколько раз были проданы разным разработчиком каждые из которых использовали их в своих целях.
?гра с новогодним сеттингом действительно заключается только в рескине графики, а уровни и еще кое что, осталось почти без изменений. Это конечно огорчает, но с другой стороны в договоре оговорено только что покупатель не имеет право использовать оригинальную графику в своей разработке. Так что, тут вроде как все по честному даже :) В общем пусть это будет на их совести.
Спасибо за проявленный интерес к ситуации! :)
ждем продолжения)
Уже все готовы к поиску пути :)
@?ван значит завтра будем его искать! ;)
@Ant.Karlov, на мой взгляд было-бы удобно, если была бы уроки были вынесены в отдельное оглавление и можно было бы их по порядку все просмотреть... А то искать их в недрах лога не очень уднобно (хоть и не смертельно, конечно-же)
@Кирилл, согласен, оглавление к урокам я обязательно сделаю. Пока просто руки не доходят. А сейчас можно пользоваться тэгами для сортировки записей по ним.
Тоже очень интересно узнать ответ на "вопрос на засыпку" ;)
Если я заменю (для большей гибкости) MAP_CELL_HALF:int = 16; на MAP_CELL_HALF:int = MAP_CELL_SIZE/2; - это не будет иметь никаких неприятных последствий? )
Упс, пардон ) только сейчас заметил комментарий по этому поводу )
.
Правда в моем случае я этот параметр (координаты центра ячейки) не буду использовать, поскольку все объекты у меня планируются размером примерно с ячейку, мелких нет.
Здрасте. Возникла проблема с уроком.
_debugGrid.bitmapData = bmpData;---вот этоткусок пишет что bmpData=null. Пробовал передать в bmpData.draw()-простой мувиклип, все ровно пишет нулл.
У вас опечатка с EnemySoldier(В предыдущих главах вы почему-то везде писали EnemySolder(Включая константу).
Я сначала тоже подумал, что вы хотели написать Soldier, но потом решил всё же следовать уроку, чтобы не запутаться, и сделал, как у вас, Solder. А вы теперь пишете Soldier(правильно).
Западло =(
опечатка в 7 строке вместо "var" - "yar"...
Добрый день!
Сразу хочу поблагодарить Вас за столь доходчивые уроки, а главное понятный код!
Хочу предложить маленькую модификацию обновления карты при нажатии на кнопку мыши
private function updateDebugCell():void
{
//текущая ячейка
var pCurrentCell:MovieClip = new debug_cell();
//Перевод получившейся сетки в растр, для оптимизации
var pMatrix:Matrix = new Matrix();
// Начальное положение текущей ячейки по высоте/ширине
pMatrix.tx = Universe.MAP_CELL_SIZE*this.nCellPosX + Universe.MAP_HALF_CELL_SIZE;
pMatrix.ty = Universe.MAP_CELL_SIZE * this.nCellPosY + Universe.MAP_HALF_CELL_SIZE;
//переключение состояния тайла
pCurrentCell.gotoAndStop(this.getCellState(this.nCellPosX, this.nCellPosY));
//отображение текущей ячейки
bmpData.draw(pCurrentCell, pMatrix)
//Перевод растрового холста в картинку
this.bmpDebugGrid.bitmapData = bmpData;
pCurrentCell = null;
}
Вызывать его точно так как и updateDebugGrid из mouseUpHandler
Если такое решение не верно, то хотелось бы услышать почему.
PS Если такое реализовано дальше, прошу меня простить за поспешность! ))
Ответ ан вопрос на засыпку, кажется, потому что будем перехватывать события мыши для многих вещей, пусть лучше один класс-менеджер знает что сейчас активно и что должно реагировать на перемещения и клики, а что нет. Опять же, если понадобиться поменять контроллер - то тоже в одном месте только
Я не уверен на 100%
Вопрос. А почему мы не пользуемся свойствами mouseX, mouseY (DisplayObject)? Там где передаем координаты мыши. Вообще, зачем мы их передаем, gameplay их так и так знает.
copyPixels работает побыстрее draw , вродебы.
Можно было отрисовать в битмап сначало клетку, а затем ее штамповать...
У меня вопросики еще...
1. Многие советуют использовать таймер вместо ENTER_FRAME. ? притом события в таймере выполняются не одновременно для всех обектов. Что ты об этом думаешь.
2. У тебя в МТ2 сохраняется информация о прохождениях. Как ты это сделал?
8 Декабря 2010
— 00:49
#