Простой движок для флэш-игр типа “Вид сверху”
22.09.2008, автор Stormit, рубрики: ActionScript, Flash игры, Игровые баннерыДавно я не писал новых статей. На то есть много причин, главные из которых - отпуск и новая флеш-игра (как найдётся спонсор, выставлю её на обозрение). Блог не умер и не собирался умирать, новые материалы будут публиковаться по мере появления свободного времени. Многие вещи у меня лежат в полуготовом состоянии и дорабатываются до состояния “интересно” - раньше выкладывать нет смысла.
Ближе к теме. Есть множество flash-игр, в которых пользователь видит уровень как-бы с высоты птичьего полета - сверху. Одна из основных задач, с которыми сталкивается разработчик в таких играх - программно организовать проходимые зоны и препятствия.
Возьмём для примера простой случай - лабиринт. Персонаж может перемещаться по коридорам, но не должен проходить сквозь стены. Эту задачу можно решить по-разному, но чаще всего я встречал такие решения:
- Блочное решение. Карта разбивается на кубики и у каждого стенки задаются как свойства “left”, “right”, “top” и “bottom”. Если объект вошёл в кубик, он свободно может по нему перемещаться. Перейти в соседний можно только если смежные грани не имеют стенок. Для оптимизации можно количество стенок уменьшить в 2 раза оставив только “left” и “top”. Две остальные сработают у соседних кубиков.
- Реализовать физический движок с нулевой гравитацией.
Все они имеют плюсы и минусы. И мой движок тоже. Предлагаю его вашему вниманию исключительно из-за дизайнерских плюсов, а именно - карта рисуется в виде картинки и не держится в уме как набор координат. Общая демка приведена ниже.
Большую часть работы выполняет старая добрая функция hitTest(). Для неё рисуется зона проходимости и персонаж может перемещаться только в её пределах. Если на карте есть горы, стены, водоёмы, дома - эти области просто не закрашиваются.
Пойдём от простого к сложному.
- В этом примере участвуют только 2 символа: path -залитая красным область проходимости и сам персонаж - символ man. Сами препятствия рисуются сверху и в расчетах не учавствуют. Функция hitTest() вызывается для проверки пересечения координат символа man с символом path. Чтобы персонаж не наезжал плечами на стены, рабочую зону нужно немного прихудить, оставив зазор около стен (проще всего это сделать при помощи Modify -> Shape -> Expand Fill). Внутри клипа man сделана анимация ходьбы. Далее, когда персонаж останавливается, он переводится в 5-й кадр. Просто в этом положении он похож на стоящего.
- Сделаем, чтобы наш герой стремился за мышкой. Слоем выше, в кадре пишем этот код:
//Шаг за 1 кадр step = 2; onEnterFrame = function(){ var dx = _root._xmouse - man._x; var dy = _root._ymouse - man._y; if (Math.abs(dx) > step) { var tgtX = man._x + ((dx > 0)? step : -step); } if (Math.abs(dy) > step) { var tgtY = man._y + ((dy > 0)? step : -step); } //Запоминаем положение oldX = man._x; oldY = man._y; //Смещаемся если новые координаты попадают в зону if (path.hitTest(man._x, tgtY, true)) { man._y = tgtY; } if(path.hitTest(tgtX, man._y, true)) { man._x = tgtX; } timeDx = man._x - oldX; timeDy = man._y - oldY; if(timeDx==0 && timeDy==0) { //Если не было смещения - в кадр где стоим man.gotoAndStop(5); }else { man.play(); //Поворачиваемся по ходу движения rot = Math.atan2(timeDy, timeDx)*180/Math.PI; man._rotation = rot; } }
Предполагаемое (будущее) положение хранится в переменных tgtX и tgtY. Для них выполняется проверка на попадание в допустимую зону. Если все ок, то дальше применяем их к персонажу, иначе останавливаемся. Это простой и основной принцип данного движка. Функция hitTest() позволяет проверить пересечение координаты с поверхностью любой формы (с учётом отверстий).
Движение персонажа разбито на две составляющие: по _x и _y. Они не зависят друг от друга и объект сможет двигаться по иксу, даже если по игреку уже тупик. С учетом этого движение получилось специфическим, по восьми направлениям. Местами из-за этого заметно дёргается. Но если кому-либо нужна будет такая дискретность, забирайте.
- Идём дальше. Прямоугольные формы - это прошлый век. Рельеф местности обычно описывается кривыми и каждый раз неповторим. Это не проблема, но перемещение персонажа придется немного переписать. Теперь (и дальше) персонаж будет двигаться в полярной системе координат (скорость и направление движения). Код становится короче:
step = 2; onEnterFrame = function(){ var dx = _xmouse - man._x; var dy = _ymouse - man._y; var angle = Math.atan2(dy, dx); var dist = Math.sqrt(dx*dx + dy*dy); if(dist > step) { tgtX = man._x + step * Math.cos(angle); tgtY = man._y + step * Math.sin(angle); man._rotation = angle*180/Math.PI; if (path.hitTest(tgtX, tgtY, true)) { man._x = tgtX; man._y = tgtY; man.play(); } else { man.gotoAndStop(5); } } }
Вроде ничего, уже можно куда-нибуть это применить. Но вот бы объект смог еще и обходить препятствия. Чтобы сам и любые формы…
- Это реально и для меня это была самая интересная часть в разработке.Этот шаг нужно описать подробнее, в нем вся соль поиска пути. Объект не знает заранее правильный маршрут к цели (как не знает его и помещенный в незнакомые условия человек), он просто идет по направлению к нему, а если встречает на своем пути препятствие, то пытается его обойти (и если специально его не драконить, то часто выбирает короткий путь). Все что закрашено красным - это допустимая зона (масштаб для наглядности увеличен).
Для расчетов нам нужны, предыдущее и текущее положение объекта (позволяет определить направление движения) и направление движения к цели по прямой. Вот и все.
Теоретически объект всегда старается идти напролом к цели (”базовое направление”). Но если это невозможно, то он начинает “сканировать” территорию в обе стороны от “базового направления” и поворачивает в ту сторону где “земля” окажется первой. Но так не учитывается решение предыдущего шага (куда мы пошли: налево или направо). Чтобы его учитывать, для “базового направления” выбирается угол в диапазоне между направлением движения и целью. На слайде он обозначен жирным вектором и делит диапазон пополам (в коде он определяется коэффициентом).
“Сканирование” - это не что иное как просчет вероятных будущих положений на предмет попадания в зону. Объект поворачивается в сторону от “базового направления” и проверяет можно ли сюда идти. Если нет, проверяет поворачиваясь в другую сторону. Если опять нельзя, угол поворота увеличивается. И так до нахождения нужной точки. Увеличивая шаг поворота, можно разгрузить процессор в ущерб точности. Далее в коде шаг в 10 градусов хорошо себя зарекомендовал. Если флэшка начинает тормозить - это первый претендент на коррекцию.
В конце-концов находится точка попадающая в зону (на слайде обозначена дополнительным крестиком) и дальнейший расчет уже проводится с ней. Дальше новый цикл, но теперь направление движения немного изменилось (вектор немного повернулся учитывая препятствие). Так объект обходит препятствие по контуру и выходит к цели.
Когда шаг поворота достаточно большой, а скорость персонажа маленькая, направление движения, в силу погрешности, будет неприятно дергаться и к нему нельзя привязаться напрямую чтобы поворачивать сам клип объекта. Поэтому я немного сглаживаю этот процесс - доворачиваю клип только на чать угла. Поворот получается вполне приемлемым.
- Пробуем все это на практике, код меняем на этот:
step = 2; onEnterFrame = function(){ var dx = _xmouse - man._x; var dy = _ymouse - man._y; //Направление (угол) к цели var angle = Math.atan2(dy, dx); var dist = Math.sqrt(dx*dx + dy*dy); if(dist > step) { //пробуем пройти напролом tgtX = man._x + step * Math.cos(angle); tgtY = man._y + step * Math.sin(angle); if (!path.hitTest(tgtX, tgtY, true)){ //напролом не получилось, вычисляем базовое направление var dAngle = dAngleRadian(direction, angle); workAngle = angle + dAngle*.8; //Шаг поворота - 10 градусов for(var i = 0; i < 360; i += 10) { for(var j = -1; j <= 1; j += 2) { var a = workAngle + radian(i) * j; var tempX = man._x + step * Math.cos(a); var tempY = man._y + step * Math.sin(a); if (path.hitTest(tempX, tempY, true)) { //точка выхода найдена. Запоминаем ее и прерываем цикл tgtX = tempX; tgtY = tempY; break; } } } } var timeDx = tgtX - man._x; var timeDy = tgtY - man._y; //направление движения direction = Math.atan2(timeDy, timeDx); var dAngle = dAngleDegree(direction*180/Math.PI, man._rotation); //поворячиваем клип к направлени движения на 5-ю часть man._rotation += dAngle * .2; man._x = tgtX; man._y = tgtY; man.play(); } else { //стоим man.gotoAndStop(5); } } function dAngleRadian(a1, a2) { var da = a1 - a2; if (da > Math.PI) { da = -Math.PI*2 + da; } else if (da < -Math.PI) { da = Math.PI*2 + da; } return da; } function dAngleDegree(a1, a2) { var da = a1 - a2; if (da > 180) { da = -360 + da; } else if (da < -180) { da = 360 + da; } return da; } function degree(a) { return a / Math.PI * 180; } function radian(a) { return a / 180 * Math.PI; }
Часть кода я вынес в функции, работающие с углами в градусах и радианах.
Бывает, что объект обходит препятствие, а потом меняет направление и идет в противоположную сторону. Это происходит, когда контур препятствия имеет неудобную для расчетов форму (сенсорные точки вправо и влево доходят до зоны почти одновременно, 50/50 какая окажется первой). Это поправимо, - нужно вектор “базового направления” повернуть ближе к направлению движения (у меня сейчас коэффициент 0.8). Персонаж от этого становится более упрямым и упорнее следует выбранному направлению (зато хуже реагирует на изменение цели).
Итого имеем для маневра 3 параметра: шаг поворота при “сканировании”, выбор “базового направления”, и скорость доворота клипа. Они подгоняются индивидуально для каждой карты. Хотя те что выставлены сейчас, должны работать в большинстве случаев.
- Как это может выглядеть в жизни
- Я никогда не делал флэш-игр на этом движке, но игровые баннеры отлично себя зарекомендовали. Тем не менее hitTest() не самая легкая для процессора функция и мне стало интересно: возможно ли на нем сделать игру в жанре RTS? Здесь для теста с разной скоростью одновременно бегают 50 юнитов. Просьба всем отписать какой fps на ваших характеристиках.
Глянул последний раз на статью перед публикацией и пришла еще одна идея. Если зону проходимости хранить как двухцветную картинку в объекте BitmapData, а проверку на пересечение выполнять функцией getPixel(). Не будет ли так работать быстрее чем hitTest()?
Интересно на 64%




7. 10-12 fps (Intel Pentium 4 2.8Gh, 512 RAM)
Приятно увидеть новую хитрость после месячного перерыва )) Как всегда великолепно,особенно огибание препятствий. Так держать!
Насчёт седьмого пункта : 25-30 fps (Intel Core 2 Duo 2.33Gh, 2 Гб памяти(DDR2))
Спасибо! Офигенный блог и офигенные хитрости! Так держать!!!! Будем пробовать и ковырять! С нетерпением жду следующих публикаций!!!! УРА!
Спасибо за прекрасную хитрость!
Насчёт седьмого пункта : 25-30 fps (Intel Core 2 Duo 2.66Gh, 2 Гб памяти(DDR2)) :o)
И еще в левой скале порой застревают, когдмышь напротив по центру
Спасибо за ответы.
Пока только для быстрых машин катит.
А застряёт из-за того что карта как раз неудобно нарисована. Нужно контур подвигать или избегать таких глубоких впадин.
Надо проверку коллизий делать не с координатами человечка, а выбрать точку на фиксированом растоянии перед ним. И ее проверять коллизию этой точки. Тогда человечек будет на расстоянии обходить препятствия.
Для интереса заглянул(:)Что ж - неплохо весьма:)
Отлично! Классная анимация персонажа! (намек
)
У меня 20 fps…
А что делать в случае если управление ведется с клавиатуры? Какой код?
Смотря как он ходит по задумке (WSAD или Угол-Скорость).
В общем случае, проверять будущее положение на попадание в зону и корректировать если надо. Код тогда выполняется не на enterFrame, а по нажатию на клавишу (или все же на enterFrame, но только когда клавиша зажата).
11 fps - Pentium IV 2.40GHz, 1GB RAM
http://noregret.org/tutor/n/ - алгоритми визначення перетину
маленька бага: треба перевіряти, чи чувак не “топчеться на місці”:
http://img111.imageshack.us/img111/9522/flashyq9.gif
- мишка в точці, де чувак справа, чуваки зліва з тої “западини” не виходять, зациклюються
Оказывается все не так просто как я себе представляла. Но все равно большое спасибо.
Не сложнее чем выглядит на презентации
IntelCore2Duo 1.8Ghz
256mb video size
2Gb RAM
•32fps
Дэн! Тормознутость самого флэш-плеера в принципе(а не свф-ок) - немного отвернула меня от этой платформы в свое время, как помнишь ))) Конечно это на руку производителям железа, но свои 12 фпс мой 2.16 одноядерник считает.
А идея сделать маску карты и пробивать гетПикселом - радует мою душу бывшего спектрумиста. Имхо работать должно быстрее. правда уровень будет выглядеть странно с этими контурами )) но можно ее не показывать, а спрятать в 25-м кадре… в нем же и расчеты делать.. в обсчем есть над чем подумать.
to bruklin
Сейчас во флэше многое можно делать растром, и возможности для этого есть. Реально делать хорошую производительность для больших уровней.
Рад тебя здесь видеть.
Я обязательно попробую вариант с растровой картой как будет время. А видно её не будет - _visible = false.
А вообще стоит тебе к этой платформе повернуться лицом
Bruklin привет! Бросай железки давай игрухи делать!
Спасибо за совет. Офигенный сайт!
А где можно скачать саму среду разработки???
Огромное спс!! задали курсач, во флеш полный (вообще что это за зверь увидела пару недель назад).
Наткнулась на ваш блог, написано легко и просто!!!!! красава!!!!!
Не подскажите где для чайников вроде мя найти инфу по написание флеш-игр (чисто, что бы получить заслуженный удовл. и забыть весь флеш аки страшный сон)))))))). Нужна инфа без каких-либо заворотов, но понятная
В последнем комменте в ызадали вопрос про ртс
ртс можно сделать. Прошу ознакомится http://fundux.ru/project260 но игра довольно старая, я ее делал когда был еще не спец в программировании, программировал на As2, сейчас уже как год или 2 работаю на As3 и думаю на нем стратегия будет удачней. Вот сейчас уже планирую делать стратегию на AS3. А так сайт отличный
тоже люблю хитрить 
Спс огромное, что не оставили без внимания надеюсь поможет!!!но уже поздно делаю тетрис))))пожелайте удачи))
А зачем на 5-ю часть поворачивать:
//поворячиваем клип к направлени движения на 5-ю часть
man._rotation += dAngle * .2;
to KUSAKA
Там же написано:
Когда шаг поворота достаточно большой, а скорость персонажа маленькая, направление движения, в силу погрешности, будет неприятно дёргаться и к нему нельзя привязаться напрямую чтобы поворачивать сам клип объекта. Поэтому я немного сглаживаю этот процесс - доворачиваю клип только на часть угла. Поворот получается вполне приемлемым.
Хотел приспособить вашу идею под игрушку типа tower defense, но как-то все криво получается: http://narod.ru/disk/2902132000/risovka2.swf.html
Большой процент крипов застревает в препятствиях…
Меня больше радует что не тормозит
Мне кажется что это не мой код (что-то в нем изменилось, похоже что “базовое направление” некорректно выбирается). Эта часть кода ниже нормально работает?
var dAngle = dAngleRadian(direction, angle);
workAngle = angle + dAngle*.8;
Вроде нормально. Я поменял немного логику, крипы двигаются не внутри объекта а снаружи. Может из-за этого что-то нарушилось… Может посоветуете какой-нибудь еще способ? =)
Попробуй пикселами. http://xitri.com/2008/09/29/simple-engine-flash-game-top-view-part2.html
В объекте BitmapData нужно закрашивать область препятствия черным. Если его потом продать - обратно белым.
А разве не так?:if (!path.hitTest(tgtX, tgtY, true))
По идее да, только в нескольких местах поменять нужно.
Stormit, подскажи, а как сделать что бы карта была больше? и как прикрутить это к DLE? я просто в пхп не селён.
Так и я не силен
А размер карты определяется размером символа path.
Что странное. Вроде делаею все как показано, но ни hittest ни getpixel не работают.
6-13 fps celeron 2.4, 736 ram, notebook
3.2ггц х2 пень, 3 гб рам - 33-34 fps
А за идеи спасибо
спос огромное….но не мог бы ты сделать видео урок по этой теме….понимаеш я ацский но упорный ньюби…мне просто очень хочется сделать такое своими руками, а я ничего в теме непонял. Мне просто непонятно откуда чё появилось и куда надо нажать…….заранее благодарен
Ленивое поколение растёт
ну вы поможете мне народ если чё вот моя почта tuzik_st@hotmail.com …..очень прошу
поддерживаю zzarzz, если не трудно опишити плиз.
14-33 fps (Pentium D 920 2.8GHz, 2GB Ram, GeForce 8600GT, чипсет i945PL)
14 - это когда они огибают препядствия в разных направлениях
33 - стоят на месте
28-38 fps (MacBook, Intel Core 2 Duo 2.16 GHz, 2 Gb Ram)
Я попробовал применить этот пример для своей игрухи.
Этот алгоритм применим для игр, которые помещаются на экране, а вот для больших карт не катит.
Там надо будет добавить в эти строки if (path.hitTest(man._x, tgtY, true)) {
относительные координаты карты.
Когда доделаю - ссылку пришлю
Классный блог! Огромное спасибо!
Все статьи без исключения очень интересны и познавательны…
AMD Turion 64×2 Mobile (TL-60) 2GHz, 2GB RAM, GF 8400G 256 MB в среднем 23fps,
максимально смог загрузить до 13fps (когда все в поиске направления постоянно), когда все остановились - 33 fps
Интересно написано, но меня вот что стало интересовать: как можно сделать так чтобы персонаж шел не за мышкой а за другим персонажем и как сделать так чтобы он просто поворачивался в сторону определенного объекта???
А вот и игруха.
Как обещал http://www.flashgamelicense.com/view_game.php?game_id=6682
офигенная штука… если это ты сделал, то ты крут
sadoff подскажи… а как сделать так чтобы враги агрились на управляемых персонажей и начинали стрельбу и т.д. я задолбался делать этот искусственный интелект - у меня не получается, он ведет себя как то совсем не так (тоже делаю игру с видом сверху)
Да так же ,как и на любой другой объект!
onEnterFrame = function(){
var dx = кудацелится._x - злобныйиумныйвраг._x;
var dy = кудацелится._y - злобныйиумныйвраг._y;
var angle = Math.atan2(dy, dx);
var dist = Math.sqrt(dx*dx + dy*dy);
if(dist > step) {
tgtX = злобныйиумныйвраг._x + step * Math.cos(angle);
tgtY = злобныйиумныйвраг._y + step * Math.sin(angle);
злобныйиумныйвраг._rotation = angle*180/Math.PI;
if (path.hitTest(tgtX, tgtY, true)) {
злобныйиумныйвраг._x = tgtX;
злобныйиумныйвраг._y = tgtY;
злобныйиумныйвраг.play();
} else {
злобныйиумныйвраг.gotoAndStop(5);
}
}
}
спасибо за скрипт. написано понятно и удобно, но как это не парадоксально этот скрипт тоже не работает
Чувак я просто в диком восторге,всё что мне на ум не придёт всё тут нахожу,ты просто супер!!!!Прекланяюсь перед тобой!!!)))))))))
Суперский код! Вот бы такой же для AS3…
Огромное спасибо за уроки! Жутко полезно!!!
А сделайте пожалуйста исходник!
Спасибо за алгоритм. У меня возник вопрос. SADoff его уже озвучил собственно, но у меня мало опыта, чтобы доделать самому. он написал: “Этот алгоритм применим для игр, которые помещаются на экране, а вот для больших карт не катит.
Там надо будет добавить в эти строки if (path.hitTest(man._x, tgtY, true)) {
относительные координаты карты.” У меня как раз такая ситуация. Карта больше стэйджа и я ее(карту) драгаю. Если не драгать, то все окей. Как только драгну - фиг мой чувак что обходит. Помоги, плиз, разобраться, буду оч благодарен
Привет всем.
Возникла такая проблемка, копирую код, использую объекты с такими же именами и всё действительно работает, за что большое спасибо автору. Но вот когда те же объекты кидаю в другой мувик назовем его Scen, не забывая о скрипте, hitTest() начинает работать неправильно, а точнее он находит объект path ниже и левее, т.е. там где он даже не нарисован. Видимо это связано со смещение объекта Scen относительно верхнего кадра. Как исправить и есть вопрос =)
AMD Athlon64, 1Гб(166мГц) 19-33 фпс
Спасибо за сайт, очень познавательно!