📜 ⬆️ ⬇️

Making a simple level editor based on the Inkscape plugin.

Introduction
I think many programmers have created, or tried to create their own game. Usually the process comes to the moment when the main part of everything is written, and you need to start building levels, game scenes, etc. If to use ready solutions, "from a box" - for example Unity, then problems do not arise. But there may be problems with licensing, support for different platforms - maybe someone wants to try to do something for Linux / Mac, where it is not always possible to find the right solution. Yes, and novice igrodela interesting to use something of their own, easy to develop and build functionality, adapted for themselves. For myself, I found a solution in the form of writing my own small Inkscape plugin.
In my free time, I’m curious about digging into my own AS3 library - yes, yes, flash: D. The library exists in the form of a wrapper on the physics engine Box2D, uses a bunch of useful things - its own state-machine, small wrappers on twinners for software animation, and a system of particles. In principle, something playable, small and stylish can be done to your taste. Since I love OpenSource and convenience, I program in FlashDevelop. Naturally, the graphics editor is not there. Yes, and he would hardly have greatly helped in creating objects with their own parameters. I remembered for Inkscape, its modularity, plug-ins, and for SVG itself - XML ​​itself, easy to parse. I decided to write a plugin for Inkscape.

Problems
Having started to look for information “about,” I found very little, one whole example of “Hello World plugin”, and everything else is poorly structured, and in English. And python as a scripting language for plugins. Functional and not typed, horror. You can kind of write plug-ins for Ruby, have not tried. But after analyzing the example and looking at the finished modules installed with Inkscape, I realized that everything was not so bad. You just need to find the right methods of working with layers and shapes, determine what exactly you want to do, and program. Further, already in the application parsim ready SVG / XML - the good is that all languages ​​have excellent tools for such purposes, we submit where necessary - I have a special constructor, and it is ready.

The structure of the finished SVG
I decided to set each level as a separate layer in SVG, this is important to consider when creating a plug-in. Objects nat. worlds can be round, square or complex (a convex polygon or several polygons in several shapes) - Box2D requirements. And naturally a bunch of parameters - both physical and their own. Objects go as usual for SVG, normally displayed in the editor and have a bunch of custom tags and parameters. It is convenient to select different types of bodies - dynamic, static, highlight them with different colors. So far, only implemented support for round bodies and square.
Important : when moving and rotating in Inkscape, in the final SVG, the parameters of the bodies do not change directly. Everything goes through the matrix transformation matrix for rotation and the translate property to move the body. Naturally, when parsing such data, you need to apply a little bit of matrix mathematics.
')
Plugin structure
Inkscape plugin consists of two parts, two files - for example my_super_plugin.py and my_super_plugin.inx . The file my_super_plugin.inx exists as a set of special XML tags, something like Java beans. It sets the plugin's GUI window, data input parameters, button types, etc. The screenshot below shows my creation.



The file my_super_plugin.py sets the actual script for working with the SVG file. The script takes it to the input, makes the necessary actions and submits to the exit, Inkscape draws everything. Fast and beautiful. As I understand it, in the script, the code and Inkscape are linked through the inkex.py module. The official data pages of the editor explain the necessary data types for my_super_plugin.py and my_super_plugin.inx (links below).

INX
I post the code of my .inx file:
Code
<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> 

I think everything is clear. The pf_plugin.py, inkex.py lines pf_plugin.py, inkex.py set the dependencies for the module - in fact, that will be loaded. The <param name="obj_d_type" type="enum" _gui-text="Static/Dynamic"> has an enum property inside - a drop-down list is specified externally. When you click on OK, all parameters go to the input to the Python script, the current value on the drop-down list is also a parameter. The value of param name must match the parameters declared as included in the Python script. Oh yeah, visually in the plugin you can create tabs - I tried it, it did not work for me.

Py
Now I will show my script that does all the work of filling the file with levels with tags:
Code
 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() 

Lines of the form 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('--layer_name', action='store',
type='string', dest='layer_name', default='Game objects',
help='Layer name which objects append to')
self.OptionParser.add_option('--layer_name', action='store',
type='string', dest='layer_name', default='Game objects',
help='Layer name which objects append to')
set the incoming parameters, their type (the names of the parameters with the names from .inx are the same). Further, in the effect function, the input data for the script is pushed into variables. Then I'm looking for something like for item in d_root:
if (item.attrib.get('id') == 'pf_go_id' and item.attrib.get('level_name') == layer_name)
for item in d_root:
if (item.attrib.get('id') == 'pf_go_id' and item.attrib.get('level_name') == layer_name)
for item in d_root:
if (item.attrib.get('id') == 'pf_go_id' and item.attrib.get('level_name') == layer_name)
: in SVG, the layers have the id property, and I’m stuffing it with 'pf_go_id' , for simplicity of identifying "their layers" with levels. If the layer already exists, we will add new objects to it, if not, we create a new layer, “level”, and work with it. The following lines create a layer, and already inside the generate_object function, I create objects. I think everything is clear there.

Svg
And, actually, an example of the generated SVG file:
Code
 <?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> 

If you load into the editor, you will see several objects and two layers - the Menu and Level_1 levels. Green figures will be fixed, red moving. I have the isSensor body parameter , I did not select it with color, although I can visually add transparency. On the Menu layer, the rectangles are rotated and moved — so the matrix and translate properties appear inside the rect tags. As you know, they are responsible for the rotation (and not only), and for the movement, respectively. Already in the target application, everything is read and processed. We write classes for matrices and solve a matrix equation of the form Ax = B (: D). From there we get the real coordinates of the bodies and the angle of rotation. If it is interesting, I will tell you how to do this on AS3, since now the post has come out rather large.

Links
Searching the Internet, you can completely figure out what and where. You also have to look at the difference between the coordinate systems of the application for which we are writing the level editor and Inkscape - axes, centers of objects, angles of rotation. Key links:
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

Source: https://habr.com/ru/post/208094/


All Articles