Думаю многие программисты создали, или пробовали создать свою игру. Обычно процесс доходит до момента, когда основная часть всего написана, и нужно начинать строить уровни, игровые сцены, и т.д. Если использовать готовые решения, «из коробки» — например Unity, тогда проблем не возникает. Но могут появиться проблемы с лицензированием, поддержкой разных платформ — может кто-то хочет попробовать что-то поделать под Linux / Mac, где не всегда можно найти нужное решение. Да и начинающим игроделам интересней использовать что-то своё, лёгкое в разработке и наращивании функционала, адаптированное под себя. Для себя я нашёл решение в виде написания собственного небольшого плагина к Inkscape.
В свободное время мне интересно поковыряться в своей же библиотеке написанной на AS3 — да, да, флеш :D. Библиотека существует в виде обёртки на физический движок Box2D, использует кучу всего полезного — собственную state-machine, небольшие обёртки на твиннеры для программной анимации и систему частиц. В принципе, что-то играбельное, небольшое и на свой вкус стильное сделать можно. Так как я люблю OpenSource и удобство, то программирую во FlashDevelop. Естественно, что редактора графики там нет. Да и он вряд ли бы сильно помог в создании объектов со собственными параметрами. Вспомнил за Inkscape, его модульность, плагины, и за сам SVG — собственно XML, лёгок для парсинга. Решил писать плагин для Inkscape.
Проблемы
Начав искать информацию «по поводу», нашёл очень мало, один целый пример «Hello World plugin», а всё остальное плохо структурировано, и на английском. И python в качестве скрипт-языка для плагинов. Функциональный и не типизированный, ужас. Можно вроде писать плагины на руби, не пробовал. Но разобрав пример и посмотрев готовые модули, установленные вместе с Inkscape, понял что не всё так плохо. Нужно просто найти правильные методы работы со слоями и фигурами, определить что именно хочешь сделать, и запрограммировать. Далее уже в приложении парсим готовый SVG / XML — благо все языки имеют отличные инструменты для таких целей, подаём куда нужно — у меня специальный конструктор, и готово.
Структура готового SVG
Я решил каждый уровень задавать как отдельный слой в SVG, это важно продумать при создании плагина. Объекты физ. мира могут иметь форму круглую, квадратную и комплексную (выпуклый многоугольник или же несколько любых многоугольников через несколько шейпов) — требования Box2D. И естественно кучу параметров — как и физических, так и своих. Объекты идут как обычные для SVG, нормально отображаются в редакторе и имеют кучу кастомных тегов и параметров. Удобно для выделения разных типов тел — динамических, статических, подсвечивать их разным цветом. Пока только реализовал поддержку круглых тел и квадратных.
Важно: при операциях в Inkscape по перемещению и вращению в конечном SVG не изменяются параметры тел напрямую. Всё идёт посредством матриц трансформаций matrix для вращения и свойства translate для перемещения тела. Естественно уже при парсинге таких данных нужно применить немножко матричной математики.
Структура плагина
Плагин в Inkscape состоит из двух частей, двух файлов — например my_super_plugin.py и my_super_plugin.inx. Файл my_super_plugin.inx существует в виде набора специальных XML — тегов, что-то похожее на бины в Java. Он задаёт GUI окошечка плагина, входные параметры данных, типы кнопок, и т.д. На скриншоте ниже показано «моё детище».
Файл my_super_plugin.py задаёт собственно сам скрипт работы с SVG — файлом. Скрипт берёт его на вход, делает нужные действия и подаёт на выход, Inkscape всё отрисовывает. Быстро и красиво. Насколько я понял, в скрипте код и Inkscape связаны через модуль inkex.py. На официальных страницах документации к редактору объяснены нужные типы данных для my_super_plugin.py и my_super_plugin.inx (ссылки внизу).
INX
Выкладываю код моего .inx файла:
<inkscape-extension> <_name>PF Editor</_name> <id>org.pf.inkscape.plugins.pf_plugin</id> <dependency type="executable" location="extensions">pf_plugin.py</dependency> <dependency type="executable" location="extensions">inkex.py</dependency> <param name="layer_name" type="string" _gui_text="Layer name">Game objects</param> <param name="obj_name" type="string" _gui_text="Object name">Object1</param> <param name="obj_width" type="int" _gui-text="Width" min="10" max="12000">30</param> <param name="obj_height" type="int" _gui-text="Height" min="10" max="12000">30</param> <param name="obj_radius" type="int" _gui-text="Radius" min="10" max="12000">30</param> <param name="obj_posX" type="int" _gui-text="PosX" min="0" max="12000">30</param> <param name="obj_posY" type="int" _gui-text="PosY" min="0" max="12000">30</param> <param name="obj_density" type="float" _gui-text="Density" min="0" max="1">0.5</param> <param name="obj_friction" type="float" _gui-text="Friction" min="0" max="1">0.5</param> <param name="obj_restitution" type="float" _gui-text="Restitution" min="0" max="1">0.5</param> <param name="obj_isSensor" type="boolean" _gui-text="Sensor body">false</param> <param name="obj_isRotable" type="boolean" _gui-text="Rotable body">false</param> <param name="obj_type" type="enum" _gui-text="Object type"> <_item value="SQUARE">Square</_item> <_item value="CIRCLE">Circle</_item> </param> <param name="obj_d_type" type="enum" _gui-text="Static/Dynamic"> <_item value="STATIC">Static</_item> <_item value="DYNAMIC">Dynamic</_item> </param> <param name="obj_hasImage" type="boolean" _gui-text="Has image">false</param> <effect> <object-type>all</object-type> <effects-menu> <submenu _name="PF Plugins"/> </effects-menu> </effect> <script> <command reldir="extensions" interpreter="python">pf_plugin.py</command> </script> </inkscape-extension>
Думаю тут всё понятно. Строки <dependency type="executable" location="extensions">pf_plugin.py</dependency>, <dependency type="executable" location="extensions">inkex.py</dependency>
задают зависимости для модуля — собственно, что будет подгружаться. Тег <param name="obj_d_type" type="enum" _gui-text="Static/Dynamic">
имеет внутри свойство enum — внешне задаётся выпадающий список. При нажатии на ОК все параметры идут на вход к питоновскому скрипту, текущее значение на выпадающем списке тоже есть параметром. Значение param name должно совпадать с параметрами, объявленными как входящие в питоновском скрипте. Ах да, визуально в плагине можно создавать вкладки — попробовал, мне не подошло.
PY
Теперь я покажу свой скрипт, который делает всю работу по наполнению тегами файла с уровнями:
import sys sys.path.append('/usr/share/inkscape/extensions') import inkex class PFEditor(inkex.Effect): def __init__(self): inkex.Effect.__init__(self) self.OptionParser.add_option('--layer_name', action='store', type='string', dest='layer_name', default='Game objects', help='Layer name which objects append to') self.OptionParser.add_option('--obj_name', action='store', type='string', dest='obj_name', default='Object', help='Object name') self.OptionParser.add_option('--obj_width', action='store', type='int', dest='obj_width', default=30, help='Object width') self.OptionParser.add_option('--obj_height', action='store', type='int', dest='obj_height', default=30, help='Object height') self.OptionParser.add_option('--obj_radius', action='store', type='int', dest='obj_radius', default=30, help='Object radius') self.OptionParser.add_option('--obj_posX', action='store', type='int', dest='obj_posX', default=30, help='PosX') self.OptionParser.add_option('--obj_posY', action='store', type='int', dest='obj_posY', default=30, help='PosY') self.OptionParser.add_option('--obj_type', action='store', type='string', dest='obj_type', default='SQUARE', help='Object type') self.OptionParser.add_option('--obj_d_type', action='store', type='string', dest='obj_d_type', default='STATIC', help='Static/Dynamic') self.OptionParser.add_option('--obj_density', action='store', type='float', dest='obj_density', default=0.5, help='Density') self.OptionParser.add_option('--obj_friction', action='store', type='float', dest='obj_friction', default=0.5, help='Friction') self.OptionParser.add_option('--obj_restitution', action='store', type='float', dest='obj_restitution', default=0.5, help='Restitution') self.OptionParser.add_option('--obj_isSensor', action='store', type='inkbool', dest='obj_isSensor', default=False, help='Sensor body') self.OptionParser.add_option('--obj_isRotable', action='store', type='inkbool', dest='obj_isRotable', default=True, help='Rotable body') self.OptionParser.add_option('--obj_hasImage', action='store', type='inkbool', dest='obj_hasImage', default=False, help='Body has image') def pfbTypes(self, x): return { 'STATIC' : '#00ff00', 'DYNAMIC' : '#ff0000', 'SQUARE' : 'SQUARE', 'CIRCLE' : 'CIRCLE' }.get(x, 0) def pfbType_SVG(self, x): return { 'SQUARE' : 'rect', 'CIRCLE' : 'circle' }.get(x, 'rect') def concat_style(self, style): # @NoSelf style_str = '' for stl in style: style_str += stl + ':' + style[stl] + ';' style_str = style_str[:-1] return style_str def generate_object(self, w, h, r, x, y, density, friction, restitution, isSensor, isRotable, parent, type, d_type, name, hasImage): # @NoSelf style = { 'fill' : self.pfbTypes(d_type), 'fill-rule' :'evenodd', 'stroke' :'000000', 'stroke-width' :'0px', 'stroke-linecap' :'butt', 'stroke-linejoin' :'miter', 'stroke-opacity' :'0' } attribs = { 'type' : type, 'd_type' : d_type, 'height' : str(h), 'width' : str(w), 'density' : str(density), 'friction' : str(friction), 'restitution' : str(restitution), 'isSensor' : str(isSensor).lower(), 'isRotable' : str(isRotable).lower(), 'hasImage' : str(hasImage).lower(), 'name' : name, 'style' : self.concat_style(style), } if d_type == 'DYNAMIC': attribs['isDynamic'] = 'true' else: attribs['isDynamic'] = 'false' if type == 'SQUARE' : attribs['x'] = str(x); attribs['y'] = str(y); if type == 'CIRCLE' : attribs['cx'] = str(x); attribs['cy'] = str(y); attribs['r'] = str(r); obj = inkex.etree.SubElement(parent, inkex.addNS(self.pfbType_SVG(type), 'svg'), attribs) def effect(self) : layer_name = self.options.layer_name obj_name = self.options.obj_name obj_width = self.options.obj_width obj_height = self.options.obj_height obj_radius = self.options.obj_radius obj_posX = self.options.obj_posX obj_posY = self.options.obj_posY obj_type = self.options.obj_type obj_d_type = self.options.obj_d_type obj_density = self.options.obj_density obj_friction = self.options.obj_friction obj_restitution = self.options.obj_restitution obj_isSensor = self.options.obj_isSensor obj_isRotable = self.options.obj_isRotable obj_hasImage = self.options.obj_hasImage svg = self.document.getroot() d_root = self.document.getroot() layer = None iter = 0 for item in d_root: if (item.attrib.get('id') == 'pf_go_id' and item.attrib.get('level_name') == layer_name): layer = item iter += 1 break if(iter == 0): layer = inkex.etree.SubElement(svg, 'g') layer.set(inkex.addNS('id'), 'pf_go_id') layer.set(inkex.addNS('level_name'), layer_name) layer.set(inkex.addNS('label', 'inkscape'), layer_name) layer.set(inkex.addNS('groupmode', 'inkscape'), 'layer') self.generate_object(obj_width, obj_height, obj_radius, obj_posX, obj_posY, obj_density, obj_friction, obj_restitution, obj_isSensor, obj_isRotable, layer, obj_type, obj_d_type, obj_name, obj_hasImage) effect = PFEditor() effect.affect()
Строки вида self.OptionParser.add_option('--layer_name', action='store', type='string', dest='layer_name', default='Game objects', help='Layer name which objects append to')
задают входящие параметры, их тип (имена параметров с именами из .inx совпадают). Далее в функции effect данные, входные для скрипта, запихиваются в переменные. Потом я ищу что-то типа for item in d_root: if (item.attrib.get('id') == 'pf_go_id' and item.attrib.get('level_name') == layer_name)
: в SVG слои имеют свойство id, и туда я запихиваю именно 'pf_go_id'
, для простоты идентификации «своих слоёв» с уровнями. Если слой уже существует, мы будем добавлять новые объекты в него, если нет — создаём новый слой, «уровень», и работаем с ним. Следующие строки создают слой, и уже внутри функции generate_object я создаю объекты. Думаю, что там всё понятно.
SVG
И, собственно, пример сгенерированного SVG файла:
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <!-- Created with Inkscape (http://www.inkscape.org/) --> <svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="630" height="480" id="svg2" version="1.1" inkscape:version="0.48.4 r9939" sodipodi:docname="levels_tmp.svg"> <sodipodi:namedview id="base" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="0.98994949" inkscape:cx="60.920287" inkscape:cy="223.06442" inkscape:document-units="px" inkscape:current-layer="pf_go_id" showgrid="false" inkscape:window-width="1366" inkscape:window-height="716" inkscape:window-x="-8" inkscape:window-y="-8" inkscape:window-maximized="1" /> <defs id="defs4" /> <metadata id="metadata7"> <rdf:RDF> <cc:Work rdf:about=""> <dc:format>image/svg+xml</dc:format> <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> <dc:title /> </cc:Work> </rdf:RDF> </metadata> <g id="pf_go_id" level_name="Menu" inkscape:label="Menu" inkscape:groupmode="layer" style="display:inline"> <circle transform="translate(194.95944,151.52288)" sodipodi:ry="40" sodipodi:rx="40" sodipodi:cy="40" sodipodi:cx="40" isSensor="false" isRotable="true" height="10" cy="40" cx="40" friction="0.5" restitution="0.5" style="fill:#00ff00;fill-rule:evenodd;stroke-width:0px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0" name="Object1" density="0.5" isDynamic="false" width="100" r="40" type="CIRCLE" d_type="STATIC" hasImage="false" id="circle3294" /> <rect id="rect2997" hasImage="false" d_type="DYNAMIC" type="SQUARE" x="4.7976952" y="405.67188" width="100" isDynamic="true" density="0.5" name="Object2" style="fill:#ff0000;fill-rule:evenodd;stroke-width:0px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0" restitution="0.5" friction="0.5" height="10" isRotable="true" isSensor="false" transform="matrix(0.88912747,-0.45765964,0.45765964,0.88912747,0,0)" /> <rect isSensor="false" isRotable="true" height="10" friction="0.5" restitution="0.5" style="fill:#00ff00;fill-rule:evenodd;stroke-width:0px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0" name="Object2" density="0.5" isDynamic="false" width="300" y="391.62952" x="173.53809" type="SQUARE" d_type="STATIC" hasImage="false" id="rect3011" /> </g> <g inkscape:groupmode="layer" inkscape:label="Level_1" level_name="Level_1" id="g3027"> <circle id="circle3029" hasImage="false" d_type="DYNAMIC" type="CIRCLE" r="30" width="300" isDynamic="true" density="0.5" name="Object1" style="stroke-linejoin:miter;stroke-opacity:0;fill-rule:evenodd;stroke:000000;stroke-linecap:butt;stroke-width:0px;fill:#ff0000" restitution="0.5" friction="0.5" cx="120" cy="130" height="10" isRotable="true" isSensor="false" /> </g> </svg>
Если загрузить в редактор, увидим несколько объектов и два слоя — уровни Menu и Level_1. Зелёные фигуры будут неподвижными, красные подвижными. У меня есть параметр тел isSensor, я его не выделял цветом, хотя можно визуально добавить прозрачность. На слое Menu прямоугольники повёрнуты и передвинуты — поэтому появились свойства matrix и translate внутри тегов rect. Как известно, они отвечают за поворот (и не только), и за перемещение соответственно. Уже в целевом приложении всё считываем и обрабатываем. Пишем классы для матриц и решаем матричное уравнение вида Ax=B (:D). Оттуда достаём настоящие координаты тел и угол поворота. Если будет интересно — расскажу как такое сделать на AS3, так как сейчас пост вышел довольно большим.
Ссылки
Поискав в интернете, можно полностью самому разобраться что и куда. Ещё обязательно нужно посмотреть разницу между системами координат приложения, для которого пишем редактор уровней и Inkscape — оси, центры объектов, углы вращения. Ключевые ссылки:
wiki.inkscape.org/wiki/index.php/Script_extensions
wiki.inkscape.org/wiki/index.php/PythonEffectTutorial
wiki.inkscape.org/wiki/index.php/Generating_objects_from_extensions
wiki.inkscape.org/wiki/index.php/INX_extension_descriptor_format
docs.python.org/2/library/xml.etree.elementtree.html
wiki.inkscape.org/wiki/index.php/INX_Parameters
www.w3schools.com/svg/svg_rect.asp
developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform
ссылка на оригинал статьи http://habrahabr.ru/post/208094/
Добавить комментарий