Еще один способ создания изометрического мира
Запись от HardCoder размещена 20.01.2012 в 21:04
Обновил(-а) HardCoder 21.01.2012 в 11:48 (Переделал классы и их названия)
Обновил(-а) HardCoder 21.01.2012 в 11:48 (Переделал классы и их названия)
Здравствуйте, уважаемые коллеги! Хочу представить вам мой способ создания изометрического игрового мира. Прежде чем начать, хочу попросить Вас воздержаться от фраз типа: "Зачем изобретать велосипед?". Думаю, те - кому интересно создавать что-то свое собственное - меня поймут. Весь алгоритм преобразования координат и сортировки созданы мной лично, поэтому цель этой статьи - услышать об ошибках для дальнейшего их устранения.
И так, начать хочу с примера. В этом примере нужно с помощью мыши перетаскивать серые кубики по полю. Они представляют собой персонажей будущей игры. А вот и сам swf: http://megaswf.com/serve/1955305
1. Обзор
Чем отличается мой подход от остальных я и сам толком не знаю, так как никогда не пользовался и даже не интересовался другими изометрическими библиотеками.
1.1 Недостатки
Если Вы, заметили - все поле состоит из клеток. В данном способе клетки могут быть лишь квадратной формы (если смотреть сверху), но не прямоугольной. Отсюда главный недостаток - невозможность создания обьектов прямоугольной формы - одни лишь "квадратики". Но, как Вы, заметили - это не мешает создавать прямоугольные здания из набора мелких обьектов квадратной формы.
Второй недостаток - обьекты не могут летать в воздухе. Вернее - могут, но сортироваться они будут в этом случае неправильно. Короче говоря: это не 3D - все вычисления происходят лишь двухмерной системе координат.
1.2 Преимущество
Очень удобный и простой, для понимания инструмент для создания игр, основанных на лабиринтах.
2. Основные понятия
2.1 Клетка
Клетка - это экранный обьект, представляющий собой отдельную квадратную область плоскости, на которой стоят или перемещаются разные обьекты. Клетка не может быть обьемной, она всегда плоская. Клетка может быть изображением травы, песка, асфальта и пр. Но она не может иметь изображение дерева, кустов, гор, так как эти обьекты имеют определенную высоту.
2.1.1 Требования:
а) клетки должны добавляться в самый нижний спрайт, который представляет собой фон изометрического мира;
б) клетки не сортируются;
в) все 4 стороны клетки должны быть равны (ромб, но не параллелограмм);
г) все клетки должны быть одинакового размера;
д) угол между сторонами клетки и осью Х должен быть равен 26.6° (см. дальше);
е) точка регистрации клетки должна быть в самой верхней точке по середине (см. рисунок 1).
2.2 Сортируемый обьект
Сортируемый обьект - это экранный обьект, представляющий собой трехмерную фигуру. Это может быть здание, солдат, дерево, гора и др.
2.2.1 Требования:
а) сортируемые обьекты должны быть добавлены и удалены только с помощью методов addItem() и removeItem соответственно, класса IsoSorter (см. ниже);
б) контейнер сортируемых обьектов должен всегда находится над спрайтом клеток (то есть - выше фона изометрического мира);
в) в контейнере с обьектами не должно находится сторонних обьектов, которые не должны сортироваться, или по ошибке были добавлены стандартными методами addChild(), addChildAt();
г) желательно, чтобы точка регистрации сортируемых обьектов соответствовала примеру на рисунке1.
2.3 Контейнер сортируемых обьектов
Контейнер сортируемых обьектов - это экранный обьект, в котором находятся герои, домики, деревья и т.д. Таких контейнеров может быть несколько в игре. Они могут содержать в себе другие контейнеры. Тогда эти дочерние контейнеры будут сортироваться как отдельные "сортируемые обьекты.".
Класс IsoSorter хранит в себе список всех контейнеров (см. п.5).
3. Изометрия
3.1 Система координат
Как я заметил выше, все клетки добавляются в самый нижний спрайт - фон. Согласно приведенному в п.4 алгоритму, все клетки добавляются на сцену так что одна половина находится в положительной области спрайта - фона по оси Х, а другая - в отрицательной. Для кого-то это может быть неудобно, но меня совсем не смущает. Самая первая клетка (самая дальняя от зрителя) всегда находится в 0 точке спрайта - фона. Изометрическая карта имеет две оси: x и z. Пример системы координат данного подхода приведен на рисунке 2.
3.2 Создание клетки
Выше я упоминал что угол между сторонами клетки и осью Х должен быть равен 26.6°. Обычно, говоря "изометрия" - имеют в виду 30°. В моем случае по другому. Откуда такая цифра 26.6°? Все дело в том, что очень удобно создавать клетки под таким углом. Мне этот способ создания клеток понравился и я решил написать алгоритм преобразования координат именно под этим углом.
Для создания клетки необходимо нарисовать квадрат, повернуть его на 45° и высоту того что получилось уменьшить вдвое (см. рисунок 3).
3.3 Spacing
Spacing - это пространство между точками регистрации двух клеток, которые граничат между собой. Грубо говоря - это расстояние между соседними клетками (рисунок 4).
4. Класс IsoTransformer
4.1 Метод setSpacing()
Ниже приведен класс, преобразующий координаты. У него есть статическое свойство Spacing. Перед началом использования этого класса сначала нужно определить это свойство с помощью статического метода setSpacing(). Если расстояние между клетками (spacing) заранее известно, можно передать это значение в метод setSpacing() в иде числа (Number). Если же расстояние неизвестно, то есть мы его не измеряли, необходимо передать в метод тестовый экземпляр клетки. В этом случае программа сама высчитает свойство Spacing, исходя из геометрических размеров переданной клетки.
//Временная клетка. Вместо мувиклипа - класс клетки (зависит от фантазии) var tempCell:MovieClip = new MovieClip(); //устанавливаем пространство между клетками IsoTransformer.setSpacing(tempCell);
Статический метод screenToIso() вычисляет изометрические координаты. Он принимает в себя два параметра: x и y. Это координаты точки НА КОНТЕЙНЕРЕ. Иногда очень легко запутаться, если передавать глобальные координаты в этот метод. Тогда он возвратит совсем не те значения которые нужно. Поэтому - в метод screenToIso() нужно передавать только координаты точки, которая относится к контейнеру, для которого нужно найти изометрические координаты. Если необходимо вычислить isoX и isoZ по точке, например, из главной сцены - то нужно воспользоваться стандартным методом globalToLocal() и преобразовать эту точку в точку нужного нам контейнера. Возвращает этот метод обьект Point со свойствами: x - координаты точки по оси isoX; y - координаты точки по оси isoZ.
4.3 Метод isoToScreen()
Статический метод isoToScreen() преобразовывает изометрические координаты контейнера в экранные координаты того же контейнера (не родительского, или какого-нибудь другого). Принимает в себя 2 параметра: isoX и isoZ. Возвращает обьект Point с экранными координатами.
4.4 Построение карты
Как я упоминал выше: спрайт клеток должен быть ниже контейнера сортируемых обьектов. Для этого нужно создать два спрайта, определить свойство Spacing класса IsoTransformer и добавить клетки, приблизительно по следующему примеру:
//спрайт клеток (фон) var background:Sprite = new Sprite(); this.addChild(background); //Временная клетка. Вместо мувиклипа - класс клетки (зависит от фантазии) var tempCell:MovieClip = new MovieClip(); //устанавливаем пространство между клетками с помощью временной клетки IsoTransformer.setSpacing(tempCell); //количество клеток по оси isoX var rows:uint = 10; //количество клеток по оси isoZ var columns:uint = 10; for (var x:uint = 0; x < rows; x++){ for (var z:uint = 0; z < columns; z++){ //создается клетка и добавляется в спрайт клеток var cell:MovieClip = new MovieClip(); background.addChild(cell); //вычисляются экранные координаты клетки в спрайте клеток var tempPoint:Point = IsoTransformer.isoToScreen(x * IsoTransform.Spacing, z * IsoTransform.Spacing); сell.x = tempPoint.x; сell.y = tempPoint.y; } }
package { import flash.display.DisplayObject; import flash.geom.Point; /** * Преобразует экранные координаты контейнера в изометрические и наоботрот * @author HardCoder */ public class IsoTransformer { private static const ALPHA:Number = 0.4636476090008062; private static var _spacing:Number; /** * Преобразует экранные координаты контейнера в изометрические * @param x - экранная x * @param y - экранная y * @return Point(isox, isoZ) */ public static function screenToIso(x:Number, y:Number):Point { var isoX:Number = (x * Math.tan(ALPHA) + ((y - x * Math.tan(ALPHA)) / 2)) / Math.sin(ALPHA); var isoZ:Number = ((y - x * Math.tan(ALPHA)) / 2) / Math.sin(ALPHA); return new Point(isoX, isoZ); } /** * Преобразует изометрические координаты в экранные координаты контейнера * @param isoX - изометрическая x * @param isoZ - изометрическая z * @return Point(x, y) */ public static function isoToScreen(isoX:Number, isoZ:Number):Point { var x:Number = isoX * (Math.cos(ALPHA)) - isoZ * (Math.cos(ALPHA)); var y:Number = isoX * (Math.sin(ALPHA)) + isoZ * (Math.sin(ALPHA)); return new Point(x, y); } /** * Вычисляет и возвращает пространство между точками регистрации плиток * @param obj - значение (Number) или клетка (DisplayObject) * @return Number */ public static function setSpacing(obj:*):Number { if (!_spacing){ if (obj is DisplayObject){ _spacing = Math.floor(Math.sqrt(5 * Math.pow(obj.width, 2) / 16)); } else { _spacing = obj; } return _spacing; } return _spacing; } /** * Возвращает пространство между точками регистрации плиток */ public static function get Spacing():Number { return _spacing; } } }
Класс имеет лишь статические методы. Он хранит в себе список контейнеров (их в приложении может быть несколько) и методы работы с сортируемыми обьектами.
5.1 Список контейнеров сортируемых обьектов
Список контейнеров сортируемых обьектов - статический. Поэтому когда какой нибудь контейнер удален со сцены - необходимо его также удалить из этого списка, иначе он будет висеть в памяти до закрытия приложения.
В каждом контейнере из списка хранятся сортируемые обьекты (деревья, человечки, домики, или другие контейнеры с человечками, деревьями и домиками). Дальше каждый сортируемый обьект буду называть "человечек".
5.2 Метод addContainerToList()
Этот метод добавляет новый контейнер в список, но не добавляет его в дисплейлист (это надо делать "вручную"). Принимает в себя параметр DisplayObjectContainer - спрайт в котором находятся наши человечки.
5.3 Метод removeContainerFromList()
Удаляет контейнер из списка, но не удаляет его из дисплейлиста (это надо делать "вручную"). Принимает в себя параметр DisplayObjectContainer - спрайт в котором находятся наши человечки, который нужно убрать из списка, чтобы он не висел в памяти.
5.4 Добавление и удаление сортируемых обьектов в контейнер
Для того чтобы добавить сортируемый обьект (человечка) в контейнер - нельзя использовать стандартный метод addChild(). Для этого нужно вызвать метод addItem() класса IsoSorter и передать в него: контейнер (спрайт, в котором находится человечек); второй параметр - сам человечек. Метод сам добавит человечка в его контейнер и поместит на нужную глубину в контейнере.
Для удаления человечка нужно вызывать метод removeItem() класса IsoSorter и передать в него: контейнер (спрайт, в котором находится человечек); второй параметр - сам человечек. И человечек будет удален из своего контейнера.
Для того, чтобы полностью очистить контейнер от человечков - можно вызвать метод removeAllItems(). В этот метод передать контейнер (спрайт с человечками). Если после удаления всех человечков нужно удалить и сам контейнер из памяти, надо передать второй параметр - deleteContainerFromList:Boolean как true.
И так, можно немного дополнить код, приведенный выше:
//спрайт клеток (фон) var background:Sprite = new Sprite(); this.addChild(background); //контейнер человечков var objectsContainer:Sprite = new Sprite(); this.addChild(objectsContainer); //Добавим контейнер человечков в список контейнеров класса IsoSorter IsoSorter.addContainerToList(objectsContainer); //Временная клетка. Вместо мувиклипа - класс клетки (зависит от фантазии) var tempCell:MovieClip = new MovieClip(); //устанавливаем пространство между клетками с помощью временной клетки IsoTransformer.setSpacing(tempCell); //количество клеток по оси isoX var rows:uint = 10; //количество клеток по оси isoZ var columns:uint = 10; for (var x:uint = 0; x < rows; x++){ for (var z:uint = 0; z < columns; z++){ //создается клетка и добавляется в спрайт клеток var cell:MovieClip = new MovieClip(); background.addChild(cell); //вычисляются экранные координаты клетки в спрайте клеток var tempPoint:Point = IsoTransformer.isoToScreen(x * IsoTransform.Spacing, z * IsoTransform.Spacing); сell.x = tempPoint.x; сell.y = tempPoint.y; //код, приведенный ниже - используется, когда над клеткой должен быть человечек //Вместо мувиклипа - класс человечкп (зависит от фантазии) var obj:MovieClip = new MovieClip(); //помещаем обьект в контейнер человечков, созданный ранее и позиционируем его по середине клетки IsoSorter.addItem(objectsContainer, obj, cell.x, cell.y + cell.height / 2); } }
Сортировка обьектов осуществляется с помощью метода sortItems() класса IsoSorter. Этот метод принимает в себя контейнер, в котором будет происходить сортировка и массив человечко, которые были перемещены (короче - те которые нужно отсортировать).
В примере, который я привел в самом начале - метод sortItems() вызывается каждый раз, когда двигается мышь. Поскольку все обьекты стоят на месте - нет надобности всех их сортировать. Поэтому на сортировку отправляется лишь перетаскиваемый обьект, что экономит производительность:
Вот несколько примеров:
1) В контейнере перемещается лишь один обьект и он нам известен:
2) Например, в контейнере 200 обьектов, но из них перемещаются только 5 и они нам известны:
3) а) в контейнере перемещаются все обьекты, и всех их нужно сортировать; б) мы понятия не имеем какие обьекты в контейнере перемещаются, поэтому нужно отсортировать их всех. Для этих случаев (а и б) - в метод sortItems() не нужно передать лишь контейнер, в котором надо произвести сортировку. Это будет означать, что сортировать нужно все подряд в этом контейнере.
Код класса IsoSorter
package { import flash.display.DisplayObjectContainer; import flash.display.DisplayObject; import flash.geom.Point; /** * Добавляет, удаляет и сортирует экранные обьекты * @author HardCoder */ public final class IsoSorter { //Список всех контейнеров //Этот список статический, поэтому, если контейнер удален со сцены //- его нужно удалить также из списка с помощью removeContainerFromList(), //иначе ссылка на него будет храниться все время private static var _containers:Array = new Array(); /** * Добавляет сортируемый обьект в список отображения контейнера * @param container - контейнер, в который добавляем обьект * @param obj - сортируемый обьект * @param x - х-координата обьекта в контейнере * @param y - у-координата обьекта в контейнере */ public static function addItem(container:DisplayObjectContainer, obj:DisplayObject, x:Number = 0, y:Number = 0):void { var sortedContainer:SortedContainer = checkForContainer(container); if (!sortedContainer){ throw new Error("Переданный контейнер не существует в списке!"); return; } var item:SortedItem = new SortedItem(); item.object = obj; item.object.x = x; item.object.y = y; sortedContainer.items.push(item); sortAndReplace(sortedContainer, item); } /** * Удаляет сортируемый обьект из списка отображения контейнера * @param container - контейнер, из которого удаляется обьект * @param obj - сортируемый обьект, подлежащий удалению */ public static function removeItem(container:DisplayObjectContainer, obj:DisplayObject):void { var sortedContainer:SortedContainer = checkForContainer(container); if (!sortedContainer){ throw new Error("Переданный контейнер не существует в списке!"); return; } var index:uint = container.getChildIndex(obj); sortedContainer.items.splice(index, 1); container.removeChild(obj); } /** * Удаляет все сортируемые обьекты из списка отображения контейнера * @param container - контейнер, из которого удаляются все обьекты * @param deleteContainerFromList - указывает, удалять ли контейнер из списка (не * удаляет контейнер со сцены) */ public static function removeAllItems(container:DisplayObjectContainer, deleteContainerFromList:Boolean = false):void { var sortedContainer:SortedContainer = checkForContainer(container); if (!sortedContainer){ throw new Error("Переданный контейнер не существует в списке!"); return; } for (var i:uint = 0; i < sortedContainer.items.length; i++){ sortedContainer.object.removeChild(sortedContainer.items[i].object); } if (deleteContainerFromList){ _containers.splice(_containers.indexOf(sortedContainer), 1); return; } sortedContainer.items.splice(0); } /** * Добавляет новый контейнер в список, но добавляет его на сцену. * @param container - контейнер, который нужно добавить в список */ public static function addContainerToList(container:DisplayObjectContainer):void { var sortedContainer:SortedContainer = checkForContainer(container); if (sortedContainer){ return; } sortedContainer = new SortedContainer(); sortedContainer.object = container; sortedContainer.items = new Array(); _containers.push(sortedContainer); } /** * Удаляет контейнер из списка, но не удаляет его со сцены. * @param container - контейнер, который нужно удалить из списка */ public static function removeContainerFromList(container:DisplayObjectContainer):void { var sortedContainer:SortedContainer = checkForContainer(container); if (!sortedContainer){ return; } _containers.splice(_containers.indexOf(sortedContainer), 1); } /** * Сортирует обьекты. Необходимо вызывать каждый раз когда положение * какого-либо обьекта изменилось * @param container - контейнер, в котором нужно сортировать обьекты * @param objectsToSort - массив обьектов контейнера, которые необходимо отсортировать. * Если null - сортируем все обьекты в контейнере */ public static function sortItems(container:DisplayObjectContainer, objectsToSort:Array = null):void { var sortedContainer:SortedContainer = checkForContainer(container); if (!sortedContainer){ throw new Error("Переданный контейнер не существует в списке!"); return; } var length:uint; objectsToSort ? length = objectsToSort.length : length = sortedContainer.items.length; for (var i:uint = 0; i < length; i++){ var item:SortedItem; if (objectsToSort){ item = sortedContainer.items[container.getChildIndex(objectsToSort[i])]; } else { item = sortedContainer.items[i]; } sortAndReplace(sortedContainer, item); } } //непосредственно занимается сортировкой private static function sortAndReplace(container:SortedContainer, item:SortedItem):void { //сохраняем старую глубину обьекта item.prevDepth = item.depth; //вычисляем позицию обьекта в изометрических координатах и новую глубину var point:Point = IsoTransformer.screenToIso(item.object.x, item.object.y); item.depth = point.x + point.y; //если старая и новая глубины отличаются - обьект переместился, значит сортируем if (item.prevDepth != item.depth){ container.items.sortOn("depth", Array.NUMERIC); container.object.addChildAt(item.object, container.items.indexOf(item)); } } //Ищет контейнер в списке и возвращает его private static function checkForContainer(container:DisplayObjectContainer):SortedContainer { for (var i:uint = 0; i < _containers.length; i++){ if (_containers[i].object == container){ return _containers[i]; } } return null; } } } import flash.display.DisplayObject; import flash.display.DisplayObjectContainer; //Вспомогательный класс, представляющий контейнер с набором свойств internal class SortedContainer { internal var items:Array; internal var object:DisplayObjectContainer; } //Вспомогательный класс, представляющий сортируемый обьект с набором свойств internal class SortedItem { internal var prevDepth:Number; public var depth:Number; internal var object:DisplayObject; }
Рисунки:
Всего комментариев 13
Комментарии
21.01.2012 03:22 | |
Обновил(-а) Котяра 21.01.2012 в 03:40
|
21.01.2012 11:13 | |
Картинки не отображаются.
|
21.01.2012 18:40 | |
Цитата:
Как ваш движок работает с арками?
Котяра, спасибо за замечание. Я постарался сделать как вы советовали (см. переделанную статью выше). Но, теперь, прочитав - понял что прикрутил еще больше костылей. Полностью абстрагировать класс IsoSorter не получится. Обьясню, почему. Сортировка происходит следующим образом. Каждый обьект в любом контейнере имеет свои координаты, неважно где этот контейнер находится. Исходя из этих координат - высчитывается некое число. Значит для каждого обьекта в контейнере это число будет уникальным. Чем больше число - тем на высшем уровне глубины находится обьект в контейнере. У каждого обьекта это число должно где-то хранится, а у каждого контейнера должен быть массив с этими числами. Если эти данные нигде не хранить, то каждый раз при вызове метода sortItems() нужно это число высчитывать наново для каждого обьекта в контейнере, что не есть хорошо. Например, в примере выше - метод sortItems() вызывается каждый раз, когда двигается мышка. К сожалению, у нас динамический только класс MovieClip, но не DisplayObject и DisplayObjectContainer. Поэтому это число мы не можем прикрутить к обьекту динамически как свойство. А массив мы не можем прикрутить к контейнеру. Поэтому есть два варианта: 1. Создавать экземпляры класса IsoSorter каждый экземпляр, которого будет хранить данные лишь об одном контейнере. Так было в первой редакции статьи. 2. Создать все методы класса IsoSorter статическими. Но тогда в этом классе нужно хранить список контейнеров с данными об обьектах. Так как сейчас описано в статье. Даже не знаю какой способ лучше..? |
21.01.2012 19:02 | |
Спасибо, etc. Это я загнался. Уже исправил. Ссылки обновил.
|
21.01.2012 21:59 | |
Ну, такой вариант был бы очень прост в реализации, но немного запутаннее в понимании. Как я говорил - я могу спокойно оперировать приведенными алгоритмами в любой точке кода. Но если другой разработчик захотел бы использоваться им, то я хотел подать материал как можно понятнее - без IsoMap() и без IsoItem(), так чтобы: увидел - и сразу написал свое, без расшифровки разных классов.
Кстати, на счет псевдо 3D - ты меня вдохновил . Сейчас думаю, как прикрутить к сортируемому обьекту третью координату. Здесь планирую уже использовать 2-3 класса для сортировки. Если что-то получится, может накатаю еще одну статейку (ну, и если полностью не закритикуют еще в этом блоге ) Цитата:
И еще смущает метод просчета глубины.
С таким подходом у нас будет пачка клеток с одинаковой глубиной. и между собой они сортируются случайным образом. Вот как например дерево, которое в изометрии занимает одну клетку, но визуально торчит в стороны. И вот эти ветки которые торчат они могут быть один раз перед соседним объектом а в другой раз за ним. |
|
Обновил(-а) HardCoder 22.01.2012 в 01:58
|
21.01.2012 22:05 | |
Цитата:
без IsoMap() и без IsoItem(), так чтобы: увидел - и сразу написал свое, без расшифровки разных классов.
Так что как минимум изометрические координаты у каждого итема наружу нужно вытягивать. Иначе это получается изометрия ради изометрии. Другими словами попросту бесполезная штука. |
|
Обновил(-а) Dukobpa3 21.01.2012 в 22:12
|
21.01.2012 22:22 | |
Такие блоги повышают мое желание работать. Спасибо за арку.
|
Последние записи от HardCoder