Простой движок для флэш-игр типа “Вид сверху”

22.09.2008, автор Stormit, рубрики: ActionScript, Flash игры, Игровые баннеры

Давно я не писал новых статей. На то есть много причин, главные из которых - отпуск и новая флеш-игра (как найдётся спонсор, выставлю её на обозрение). Блог не умер и не собирался умирать, новые материалы будут публиковаться по мере появления свободного времени. Многие вещи у меня лежат в полуготовом состоянии и дорабатываются до состояния “интересно” - раньше выкладывать нет смысла.

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

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

  1. Блочное решение. Карта разбивается на кубики и у каждого стенки задаются как свойства “left”, “right”, “top” и “bottom”. Если объект вошёл в кубик, он свободно может по нему перемещаться. Перейти в соседний можно только если смежные грани не имеют стенок. Для оптимизации можно количество стенок уменьшить в 2 раза оставив только “left” и “top”. Две остальные сработают у соседних кубиков.
  2. Реализовать физический движок с нулевой гравитацией.

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

Большую часть работы выполняет старая добрая функция hitTest(). Для неё рисуется зона проходимости и персонаж может перемещаться только в её пределах. Если на карте есть горы, стены, водоёмы, дома - эти области просто не закрашиваются.

Пойдём от простого к сложному.

  1. В этом примере участвуют только 2 символа: path -залитая красным область проходимости и сам персонаж - символ man. Сами препятствия рисуются сверху и в расчетах не учавствуют. Функция hitTest() вызывается для проверки пересечения координат символа man с символом path. Чтобы персонаж не наезжал плечами на стены, рабочую зону нужно немного прихудить, оставив зазор около стен (проще всего это сделать при помощи Modify -> Shape -> Expand Fill). Внутри клипа man сделана анимация ходьбы. Далее, когда персонаж останавливается, он переводится в 5-й кадр. Просто в этом положении он похож на стоящего.
  2. Сделаем, чтобы наш герой стремился за мышкой. Слоем выше, в кадре пишем этот код:
    //Шаг за 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. Они не зависят друг от друга и объект сможет двигаться по иксу, даже если по игреку уже тупик. С учетом этого движение получилось специфическим, по восьми направлениям. Местами из-за этого заметно дёргается. Но если кому-либо нужна будет такая дискретность, забирайте.

  3. Идём дальше. Прямоугольные формы - это прошлый век. Рельеф местности обычно описывается кривыми и каждый раз неповторим. Это не проблема, но перемещение персонажа придется немного переписать. Теперь (и дальше) персонаж будет двигаться в полярной системе координат (скорость и направление движения). Код становится короче:
    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);
    		}
     
    	}
    }

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

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

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

    Теоретически объект всегда старается идти напролом к цели (”базовое направление”). Но если это невозможно, то он начинает “сканировать” территорию в обе стороны от “базового направления” и поворачивает в ту сторону где “земля” окажется первой. Но так не учитывается решение предыдущего шага (куда мы пошли: налево или направо). Чтобы его учитывать, для “базового направления” выбирается угол в диапазоне между направлением движения и целью. На слайде он обозначен жирным вектором и делит диапазон пополам (в коде он определяется коэффициентом).

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

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

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

  5. Пробуем все это на практике, код меняем на этот:
    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 параметра: шаг поворота при “сканировании”, выбор “базового направления”, и скорость доворота клипа. Они подгоняются индивидуально для каждой карты. Хотя те что выставлены сейчас, должны работать в большинстве случаев.

  6. Как это может выглядеть в жизни
  7. Я никогда не делал флэш-игр на этом движке, но игровые баннеры отлично себя зарекомендовали. Тем не менее hitTest() не самая легкая для процессора функция и мне стало интересно: возможно ли на нем сделать игру в жанре RTS? Здесь для теста с разной скоростью одновременно бегают 50 юнитов. Просьба всем отписать какой fps на ваших характеристиках.

Глянул последний раз на статью перед публикацией и пришла еще одна идея. Если зону проходимости хранить как двухцветную картинку в объекте BitmapData, а проверку на пересечение выполнять функцией getPixel(). Не будет ли так работать быстрее чем hitTest()?

Интересно на 64%

(60) Хитрых на тему «Простой движок для флэш-игр типа “Вид сверху”»

  1. kutu

    7. 10-12 fps (Intel Pentium 4 2.8Gh, 512 RAM)

  2. Ixifeus

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

  3. Ixifeus

    Насчёт седьмого пункта : 25-30 fps (Intel Core 2 Duo 2.33Gh, 2 Гб памяти(DDR2))

  4. midnighter

    Спасибо! Офигенный блог и офигенные хитрости! Так держать!!!! Будем пробовать и ковырять! С нетерпением жду следующих публикаций!!!! УРА!

  5. Iga

    Спасибо за прекрасную хитрость!

  6. GB

    Насчёт седьмого пункта : 25-30 fps (Intel Core 2 Duo 2.66Gh, 2 Гб памяти(DDR2)) :o)
    И еще в левой скале порой застревают, когдмышь напротив по центру

  7. Stormit

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

  8. mif2000

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

  9. Никита

    Для интереса заглянул(:)Что ж - неплохо весьма:)

  10. Destroyer

    Отлично! Классная анимация персонажа! (намек ;) )

  11. Destroyer

    У меня 20 fps…

  12. Destroyer

    А что делать в случае если управление ведется с клавиатуры? Какой код?

  13. Stormit

    Смотря как он ходит по задумке (WSAD или Угол-Скорость).
    В общем случае, проверять будущее положение на попадание в зону и корректировать если надо. Код тогда выполняется не на enterFrame, а по нажатию на клавишу (или все же на enterFrame, но только когда клавиша зажата).

  14. Xpb7

    11 fps - Pentium IV 2.40GHz, 1GB RAM

  15. Світовида

    http://noregret.org/tutor/n/ - алгоритми визначення перетину

    маленька бага: треба перевіряти, чи чувак не “топчеться на місці”:
    http://img111.imageshack.us/img111/9522/flashyq9.gif
    - мишка в точці, де чувак справа, чуваки зліва з тої “западини” не виходять, зациклюються

  16. stingri

    Оказывается все не так просто как я себе представляла. Но все равно большое спасибо.

  17. Stormit

    Не сложнее чем выглядит на презентации :)

  18. Slk

    IntelCore2Duo 1.8Ghz
    256mb video size
    2Gb RAM
    •32fps

  19. bruklin

    Дэн! Тормознутость самого флэш-плеера в принципе(а не свф-ок) - немного отвернула меня от этой платформы в свое время, как помнишь ))) Конечно это на руку производителям железа, но свои 12 фпс мой 2.16 одноядерник считает.
    А идея сделать маску карты и пробивать гетПикселом - радует мою душу бывшего спектрумиста. Имхо работать должно быстрее. правда уровень будет выглядеть странно с этими контурами )) но можно ее не показывать, а спрятать в 25-м кадре… в нем же и расчеты делать.. в обсчем есть над чем подумать.

  20. Stormit

    to bruklin
    Рад тебя здесь видеть.
    Я обязательно попробую вариант с растровой картой как будет время. А видно её не будет - _visible = false.
    А вообще стоит тебе к этой платформе повернуться лицом :) Сейчас во флэше многое можно делать растром, и возможности для этого есть. Реально делать хорошую производительность для больших уровней.

  21. ded pb|xto

    Bruklin привет! Бросай железки давай игрухи делать! :)

  22. KLYMBA

    Спасибо за совет. Офигенный сайт!

  23. KLYMBA

    А где можно скачать саму среду разработки???

  24. 4udo

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

  25. PeTa4eK

    В последнем комменте в ызадали вопрос про ртс :) ртс можно сделать. Прошу ознакомится http://fundux.ru/project260 но игра довольно старая, я ее делал когда был еще не спец в программировании, программировал на As2, сейчас уже как год или 2 работаю на As3 и думаю на нем стратегия будет удачней. Вот сейчас уже планирую делать стратегию на AS3. А так сайт отличный :) тоже люблю хитрить :)

  26. 4udo

    Спс огромное, что не оставили без внимания надеюсь поможет!!!но уже поздно делаю тетрис))))пожелайте удачи))

  27. KUSAKA

    А зачем на 5-ю часть поворачивать:
    //поворячиваем клип к направлени движения на 5-ю часть
    man._rotation += dAngle * .2;

  28. Stormit

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

  29. KUSAKA

    Хотел приспособить вашу идею под игрушку типа tower defense, но как-то все криво получается: http://narod.ru/disk/2902132000/risovka2.swf.html
    Большой процент крипов застревает в препятствиях…

  30. Stormit

    Меня больше радует что не тормозит :)
    Мне кажется что это не мой код (что-то в нем изменилось, похоже что “базовое направление” некорректно выбирается). Эта часть кода ниже нормально работает?
    var dAngle = dAngleRadian(direction, angle);
    workAngle = angle + dAngle*.8;

  31. KUSAKA

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

  32. Stormit

    Попробуй пикселами. http://xitri.com/2008/09/29/simple-engine-flash-game-top-view-part2.html
    В объекте BitmapData нужно закрашивать область препятствия черным. Если его потом продать - обратно белым.

  33. Костя

    А разве не так?:if (!path.hitTest(tgtX, tgtY, true))

  34. Stormit

    По идее да, только в нескольких местах поменять нужно.

  35. gaka

    Stormit, подскажи, а как сделать что бы карта была больше? и как прикрутить это к DLE? я просто в пхп не селён.

  36. Stormit

    Так и я не силен :)
    А размер карты определяется размером символа path.

  37. Димас

    Что странное. Вроде делаею все как показано, но ни hittest ни getpixel не работают.

  38. Иван

    6-13 fps celeron 2.4, 736 ram, notebook

  39. ezheka

    3.2ггц х2 пень, 3 гб рам - 33-34 fps
    А за идеи спасибо

  40. zzarzz

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

  41. Stormit

    Ленивое поколение растёт :)

  42. zzarzz

    ну вы поможете мне народ если чё вот моя почта tuzik_st@hotmail.com …..очень прошу

  43. shustric

    поддерживаю zzarzz, если не трудно опишити плиз.

  44. Ryab_i4

    14-33 fps (Pentium D 920 2.8GHz, 2GB Ram, GeForce 8600GT, чипсет i945PL)
    14 - это когда они огибают препядствия в разных направлениях
    33 - стоят на месте

  45. Иван

    28-38 fps (MacBook, Intel Core 2 Duo 2.16 GHz, 2 Gb Ram)

  46. SADoff

    Я попробовал применить этот пример для своей игрухи.
    Этот алгоритм применим для игр, которые помещаются на экране, а вот для больших карт не катит.
    Там надо будет добавить в эти строки if (path.hitTest(man._x, tgtY, true)) {
    относительные координаты карты.
    Когда доделаю - ссылку пришлю

  47. mimix

    Классный блог! Огромное спасибо!

    Все статьи без исключения очень интересны и познавательны…

    AMD Turion 64×2 Mobile (TL-60) 2GHz, 2GB RAM, GF 8400G 256 MB в среднем 23fps,
    максимально смог загрузить до 13fps (когда все в поиске направления постоянно), когда все остановились - 33 fps

  48. Михаил

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

  49. SADoff

    А вот и игруха.
    Как обещал http://www.flashgamelicense.com/view_game.php?game_id=6682

  50. Михаил

    офигенная штука… если это ты сделал, то ты крут

  51. Михаил

    sadoff подскажи… а как сделать так чтобы враги агрились на управляемых персонажей и начинали стрельбу и т.д. я задолбался делать этот искусственный интелект - у меня не получается, он ведет себя как то совсем не так (тоже делаю игру с видом сверху)

  52. 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);
    }

    }
    }

  53. Михаил

    спасибо за скрипт. написано понятно и удобно, но как это не парадоксально этот скрипт тоже не работает

  54. Nrjwolf

    Чувак я просто в диком восторге,всё что мне на ум не придёт всё тут нахожу,ты просто супер!!!!Прекланяюсь перед тобой!!!)))))))))

  55. kabanov

    Суперский код! Вот бы такой же для AS3…

  56. Jazzcat

    Огромное спасибо за уроки! Жутко полезно!!!

  57. Бебешка

    А сделайте пожалуйста исходник!

  58. greegreeman

    Спасибо за алгоритм. У меня возник вопрос. SADoff его уже озвучил собственно, но у меня мало опыта, чтобы доделать самому. он написал: “Этот алгоритм применим для игр, которые помещаются на экране, а вот для больших карт не катит.
    Там надо будет добавить в эти строки if (path.hitTest(man._x, tgtY, true)) {
    относительные координаты карты.” У меня как раз такая ситуация. Карта больше стэйджа и я ее(карту) драгаю. Если не драгать, то все окей. Как только драгну - фиг мой чувак что обходит. Помоги, плиз, разобраться, буду оч благодарен

  59. 47

    Привет всем.
    Возникла такая проблемка, копирую код, использую объекты с такими же именами и всё действительно работает, за что большое спасибо автору. Но вот когда те же объекты кидаю в другой мувик назовем его Scen, не забывая о скрипте, hitTest() начинает работать неправильно, а точнее он находит объект path ниже и левее, т.е. там где он даже не нарисован. Видимо это связано со смещение объекта Scen относительно верхнего кадра. Как исправить и есть вопрос =)

  60. Unter

    AMD Athlon64, 1Гб(166мГц) 19-33 фпс

    Спасибо за сайт, очень познавательно!

Оставить комментарий