Вступление
Думаю многие программисты создали, или пробовали создать свою игру. Обычно процесс доходит до момента, когда основная часть всего написана, и нужно начинать строить уровни, игровые сцены, и т.д. Если использовать готовые решения, «из коробки» — например 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',
задают входящие параметры, их тип (имена параметров с именами из .inx совпадают). Далее в функции effect данные, входные для скрипта, запихиваются в переменные. Потом я ищу что-то типа
type='string', dest='layer_name', default='Game objects',
help='Layer name which objects append to')for item in d_root:
: в SVG слои имеют свойство id, и туда я запихиваю именно
if (item.attrib.get('id') == 'pf_go_id' and item.attrib.get('level_name') == layer_name)'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
Автор: zeksa