Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Dynamic placeable objects with default properties for custom engine #365

Open
ChristianIvicevic opened this Issue · 25 comments

6 participants

@ChristianIvicevic

Hello together,

so far I have been using Tiled for my 2D game engine and everything is neat and works fine. Currently I have much hardcoded the game into the engine and I am working on encapsulating this in other parts. Therefore I have developed a custom scripting language inspired by UnrealScript (with ANTLR 4) which gets executed by the engine during runtime. Now I have a basic Entity class which looks like this

class Entity abstract placeable;

var (EntityProperties) bool IsVisible;
/* ... */

and a possible object which could get triggered multiple times (duh.)

class TriggerMultiple extends Entity;

var (TriggerProperties) string Event;
/* ... */

I am looking for an easy way to provide these classes as object types to the editor without the need of some registry manipulation of the key HKEY_CURRENT_USER\Software\mapeditor.org\Tiled\ObjectTypes. That is: you write the code, metadata in a readable format gets created and the editor will load this automatically (a portable version of the editor to be included with my engine would really decouple it from the registry). So I am looking for a way that the editor can feed off of my provided data.

As another request I would like the editor to automatically create the properties IsVisible and Event in the object properties when provided with the necessary metadata like with the objects themselves. The coolest thing would be to gather them to some grouped view based on the specifiers (EntityProperties) and (TriggerProperties) but that is not necessary, yet.

I know that this is all a bit too localized and would result in changes of the sourcecode in a custom fork and not the full version, yet I would like to get some hints what the easiest way would be to do so such that I do not have to waste much time looking through the sourcecode to find what to modify.

As a possible side-question: am I allowed to fork the editor and customize it for my engine? What do I have to provide when doing so considering copy(right | left)?

@stefanbeller

As a possible side-question: am I allowed to fork the editor and customize it for my engine? What do I have to provide when doing so considering copy(right | left)?

short: Yes you are allowed to fork and customize it.

Different parts of this repository are covered by different licenses.
libtiled, which provides the very basics like tmx map reading/writing is covered by the most permissive license in this repository: BSD 2 clause license essentially allows you to do anything except removing the original authors name, as an example see the first 26 lines of
https://github.com/bjorn/tiled/blob/master/src/libtiled/gidmapper.h#L1

The editor part "tiled" however is covered by a more restrictive license: GPLv2.
That license can be read at https://github.com/bjorn/tiled/blob/master/LICENSE.GPL or if you don't like legal texts maybe this is a good entry http://www.gnu.org/licenses/gpl-faq.html
In short: GPL software can be modified and distributed even commercially, but you are forced to hand a copy of the source code to anyone whom you sell your stuff.

@stefanbeller

As another request I would like the editor to automatically create the properties IsVisible and Event in the object properties when provided with the necessary metadata like with the objects themselves.

That sounds similar to #70

@stefanbeller

In the menu Edit->Preferences->Object Types you can import and export types to xml.

@ChristianIvicevic

So let's start with the Object Types. I know that I can import and export these types, yet I would like to automate this process otherwise I would have to do this everytime I introduce a new placeable object.

The default properties you mentioned in #70 are definitely what is still missing and I looking on how to tell the editor where to fetch the templates from. Yet this has to implemented by someone (or me? maybe...).

@bjorn
Owner

@ChristianIvicevic I don't think this has to be very complicated. Sounds like you need the following:

  • Extend ObjectType with a container to hold the attributes for each object.
  • Extend ObjectTypesReader to read this information out of the XML file and into that container.
  • In main.cpp you put a line which auto-imports the object types, for example:
ObjectTypesReader reader;
Preferences::instance()->setObjectTypes(reader.readObjectTypes("my-object-types.xml"));

(If you want you can take the file by command line argument, or you even do a input field with 'Browse..' button in the preferences to make this an official way of dealing with object types. I'd accept the latter solution in Tiled proper).

  • Finally, of course you want those default properties to show up. Since this is based on the type, which you may (or even have to) change dynamically, I'm not sure what's the best way there. Maybe you could mix in the properties from your object type into the PropertiesModel by exposing it from the PropertiesDialog so you can access it from ObjectPropertiesDialog. On applying those properties you'd probably want to avoid adding empty values for all the ones you didn't use, though.

Finally, good luck! If it sounds too much, you can also "fund" me a few hours, btw.

@ChristianIvicevic

@bjorn Thank you for your suggestions so far. I was successful implementing half of the requested features yet I think that these are quite specialized for my needs and I don't know of a good generalization yet to pull a commit. However, I am going to explain a bit what I did so far, such that anyone who wants this feature too, can have a look at it.

In main.cpp :: main() I included your suggested piece of code after loading the plugins via PluginManager::instance()->loadPlugins();. Currently it looks like this:

ObjectTypesReader objectTypesReader;
// Load the object types if available - this will most likely override
// the default one loaded from the registry (windows specific).
QFile objectTypesFile(QLatin1String("ObjectTypes.xml"));
if(objectTypesFile.exists())
    Preferences::instance()->setObjectTypes(
        objectTypesReader.readObjectTypes(QLatin1String("ObjectTypes.xml")));

This method currently presumes that a possible custom file will exist, such that all values from the registry are ignored. During compilation this file will be automatically created by my compiler based on the source files. To complement this I changed the return statement of the main method to be like

int returnCode = a.exec();
// Clean the object types
Preferences::instance()->setObjectTypes(ObjectTypes());
return returnCode;

This is mainly to be only dependent on the local xml file and have everything clean.

Let's now have a look at the default properties and those which are exposed by the source code to the editor. First of I changed the editable property of the QComboBox type in objectpropertiesdialog.ui to false such that one has to use the object types the editor exposes (and to prevent unsupported bloat).

The next step was to modify the objecttypes.h file. I have created a new struct which represents one property currently looking like this:

struct ObjectTypeProperty {
    QString name;
    enum DataType {
        DT_STRING,
        DT_BOOL,
        DT_INT,
        DT_DOUBLE
    } type;

    QString stringValue;
    bool boolValue;
    int intValue;
    double doubleValue;
};

I wanted to use a union for the support values but QStrings cannot be unioned. Furthermore I looked up that a QVariant can be used for such dynamic types. - This is my first time working with Qt so it looks messy, but I want to implement the most basic features for now. I extended the existing struct ObjectType by adding a QVector<ObjectTypeProperty> exposedProperties; line. In the same file I defined a new method in the class ObjectTypesReader with the signature QVector<ObjectTypeProperty> parseProperties(QXmlStreamReader& objectTypeNode);. This was necessary to parse the properties as I extended the xml file with new tags:

<?xml version="1.0" encoding="UTF-8"?>
<objecttypes>
 <objecttype name="Entity" color="#000000" />
 <objecttype name="TriggerOnce" color="#ffff00">
    <property type="bool" name="IsTestVarActive" default="true" />
    <property type="int" name="SomeInt" default="0" />
    <property type="double" name="Opacity" default="0.5" />
    <property type="string" name="StringifiedName" default="A name" />
 </objecttype>
 <objecttype name="TriggerMultiple" color="#ffaa00" />
 <objecttype name="Teleport" color="#00ffff" />
 <objecttype name="InfoPlayerStart" color="#005500" />
</objecttypes>

The new ObjectTypesReader::readObjectTypes method calls the other new method this way:

/* ... */
while (reader.readNextStartElement()) {
    if (reader.name() == QLatin1String("objecttype")) {
        const QXmlStreamAttributes atts = reader.attributes();

        const QString name(atts.value(QLatin1String("name")).toString());
        const QColor color(atts.value(QLatin1String("color")).toString());

        ObjectType objectType(name, color);
        objectType.exposedProperties = this->parseProperties(reader);
        objectTypes.append(objectType);
    }
}
/* ... */
QVector<ObjectTypeProperty> ObjectTypesReader::parseProperties(QXmlStreamReader& objectTypeNode) {
    QVector<ObjectTypeProperty> objectProperties;
    while(objectTypeNode.readNextStartElement()) {
        if(objectTypeNode.name() == QLatin1String("property")) {
            ObjectTypeProperty property;
            const QXmlStreamAttributes atts = objectTypeNode.attributes();

            const QString type = atts.value(QLatin1String("type")).toString();
            if(type == QLatin1String("bool")) {
                property.type = ObjectTypeProperty::DT_BOOL;
            } else if(type == QLatin1String("int")) {
                property.type = ObjectTypeProperty::DT_INT;
            } else if(type == QLatin1String("double")) {
                property.type = ObjectTypeProperty::DT_DOUBLE;
            } else if(type == QLatin1String("string")) {
                property.type = ObjectTypeProperty::DT_STRING;
            } else {
                property.type = ObjectTypeProperty::DT_STRING;
            }

            property.name = atts.value(QLatin1String("name")).toString();
            const QString defaultValue = atts.value(QLatin1String("default")).toString();
            switch(property.type) {
            case ObjectTypeProperty::DT_BOOL:
                if(defaultValue == QLatin1String("true"))
                    property.boolValue = true;
                else
                    property.boolValue = false;
                break;
            case ObjectTypeProperty::DT_INT:
                property.intValue = defaultValue.toInt();
                break;
            case ObjectTypeProperty::DT_DOUBLE:
                property.doubleValue = defaultValue.toDouble();
                break;
            case ObjectTypeProperty::DT_STRING:
                property.stringValue = defaultValue;
                break;
            }
            objectProperties.append(property);
        }
        objectTypeNode.skipCurrentElement();
    }
    return objectProperties;
}

So now I have some issues - my intention was include an OnSelectedIndexChanged event or something like that to the type QComboBox to automatically create the properties, but the two parts (name, type, position and the properties list box) are separated. Having a look at the used models I couldn't come up with a good solution to combine my loaded properties with the dialog.

I would be very glad if you can give me some hints on where to exactly look as I am quite confused by the Qt structure in your files. Ah and if I don't succeed... what about "funding" you a few hours (probably some minutes) - how much coffee do you drink usually? :smile:

@bjorn
Owner

@ChristianIvicevic Good going so far! I can imagine that last part will be a little bit involved, but I don't have time to go into further detail. Contact me by email if you would like to know how much coffee I need. I would estimate that it would still take me about two hours to solve that riddle.

@ChristianIvicevic

@bjorn Once again I was successful implementing what I needed. Anyone interested in knowing how I did this should continue reading this comment - I will describe what I have changed. And this feature is really necessary in Tiled so I have decided to optimize the approach and once I'm finished I will contribute to the project with my suggestions on how to include this.

So far I have been using my custom struct ObjectTypeProperty which holds all default values for different times. I will look on how to use QVariant to have better type safety. I started by editing PropertiesDialog.cpp the base class for all dialogs which display properties (no shit Sherlock!). The constructor has to be extended with

mModel = new PropertiesModel(this);
// This is actually new.
this->updateDefaultProperties();
mModel->setProperties(mObject->properties());

So far we need some way to check whether we can safely work with MapObjects because otherwise we would add properties to random images, layers or the map itself. For now I am doing this with a really dirty check (probably localization will break this!) by just checking the kind of the object. Everything else should be very straight-forward:

void PropertiesDialog::updateDefaultProperties() {
    // TODO: This check is horrible! Fix this asap.
    if(mKind == QLatin1String("Object")) {
        MapObject *mapObject = static_cast<MapObject*>(mObject);
        ObjectTypes types = Preferences::instance()->objectTypes();
        ObjectType currentType;
        foreach(ObjectType type, types) {
            if(type.name == mapObject->type()) {
                currentType = type;
                break;
            }
        }
        foreach(ObjectTypeProperty exposedProperty, currentType.exposedProperties) {
            if(!mapObject->properties().contains(exposedProperty.name)) {
                if(exposedProperty.type == ObjectTypeProperty::DT_BOOL)
                    mapObject->setProperty(exposedProperty.name, exposedProperty.boolValue ? QLatin1String("true") : QLatin1String("false"));
                else if(exposedProperty.type == ObjectTypeProperty::DT_STRING)
                    mapObject->setProperty(exposedProperty.name, exposedProperty.stringValue);
                else if(exposedProperty.type == ObjectTypeProperty::DT_INT)
                    mapObject->setProperty(exposedProperty.name, QString::number(exposedProperty.intValue));
                else if(exposedProperty.type == ObjectTypeProperty::DT_DOUBLE)
                    mapObject->setProperty(exposedProperty.name, QString::number(exposedProperty.doubleValue));
            }
        }
        mModel->setProperties(mapObject->properties());
    }
}

What this code does, it creates the default properties for the object and adds them directly to the object. Soon I will implement a more restrictive version for myself, such that one cannot just add new properties which are unsupported. Therefore the code has to be updated so that if you change the type from A to B

  • all properties A has but B not are to be removed,
  • all properties A has and B too are just copied over,
  • all new properties B has but A did not have are created.

The last step is to handle the event when the QComboBox holding the types is being manipulated, that is a new type is selected to do what I have described in the last paragraph. To do so I implemented a new slot ObjectPropertiesDialog::objectTypeChanged(QString currentItem) which calls the code above:

void ObjectPropertiesDialog::objectTypeChanged(QString currentItem) {
    mMapObject->setType(currentItem);
    this->updateDefaultProperties();
}
@bjorn
Owner

@ChristianIvicevic This mKind hack will indeed break with localization. To avoid this kind of check I had suggested to expose the data model from PropertiesDialog, so that ObjectPropertiesDialog can access it. Then you can also move the updateDefaultProperties to the ObjectPropertiesDialog since it's only relevant there.

As soon as you open a pull request I can try it out and review the code in more detail. :-)

@bjorn
Owner

@ChristianIvicevic Did you ever finish your approach? Even if not, would you mind sharing your work so that maybe somebody else could finish it? This feature is still one of the most requested ones for Tiled, so it would be very helpful!

@ChristianIvicevic

@bjorn Sorry for not reaching you out. I will be working again with Tiled during the next months so I might share my thoughts on how to properly implement this soonish.

@bjorn
Owner

@ChristianIvicevic Great to hear from you again and looking forward to reading your thoughts. :-)

@zachprinz

Just wanted to thank @ChristianIvicevic . Took me a few hours to get this implemented and working. Most of what he posted above still works fine. Some of it is now unnecessary because of the new properties update and instead of working with propertiesbrowser.cpp the code in the second post needs to be implemented in propertybrowser.cpp.

I also played with createobjecttool.cpp to allow me to instantiate MapObjects with a certain type, and property values specific to their tiles (via tile properties) that fill in the default values that have already been implemented. That combined with the aforementioned changes (made to allow default properties) and the new "collection of images" tileset have allowed me to have premade objects that I can drop into the map via an image tile, which is cool.

I'd post my complete solution but I'm afraid I butchered the code a bit when implementing this.

@nhnb

While I cannot help with coding this feature, I like to contribute another real world use case.

Stendhal currently uses external manually-edited .xml files to place dynamic content. Based on these files, I created an objecttypes.xml for Stendhal entities.

@dazKind

@zachprinz @ChristianIvicevic @bjorn I'm looking for this functionality.
Are there any public PRs or forks with the mentioned changes in order to make this functionality work? Given that multiple people already had this working at some point I wonder if there is some place to start aside from starting from scratch with the info from the comments above.

@bjorn
Owner

@dazKind Unfortunately @ChristianIvicevic never made it to a pull request and his Tiled fork seems to be gone. I also don't see any of his work in @zachprinz his Tiled fork. I'm not aware of any other patches adding this kind of functionality.

@dazKind

@bjorn: I thought so. For what it's worth, I started working on this in my fork: dazKind@bjorn:master...master

@bjorn
Owner

@dazKind Awesome, looks like a good start!

@dazKind

Ok, I got it to a functional state. Im adding the default properties in updateCustomProperties: dazKind@bjorn:master...master#diff-fff27e659587ef19ff643fce099f69f7R949

Im not that familiar with Qt. I noticed that the added properties are all kinda greyed out and wont be exported:
properties

Is there a flag I have to set somewhere?

Edit: I'm testing with @nhnb's objecttypes.xml

Edit2: Nvm, I see that the that object itself doesnt have the actual property set, yet. Thus they get grayed out atm. fixed

@dazKind

Ok, so the proof of concept works. What bugs me is the way the editor keeps the incomplete types between restarts. Now, I read that the objecttype name and color are stored in the registry. Is this still true?

It would make more sense to remove this and just store a path to an xml containing the types. Then you could ship with an example that gets loaded by default and have users override it depending on their projects/needs.

Maybe a better idea would be to move this information into the actual map-data altogether. Any thoughts?

@dazKind

@bjorn ping!

@bjorn
Owner

@dazKind I'm reading your updates, but it's "during the week" now which leaves me with only a little time in the evenings. I'll quickly answer some of your questions:

  • About the properties getting grayed out, I see you found the cause, but you also noted you "fixed" it. But I'm thinking, isn't it actually useful to keep them gray until their default value is overridden?

  • It is still true that object types are stored in the register on Windows. And you're totally right that this would be good to change to just a reference to a file that can be set. Moving that reference into the map file is not where I want to go, since I'd rather add support for project files to Tiled. Until that happens, I think it's maybe better to keep it in the preferences (though of course that assumes support for "projects" would come sooner rather than later).

Unfortunately I don't have time for a code review right now, but please open a pull request so that I can better comment on the changes when I do have time. The feature does not need to be finished before opening a pull request.

And thanks for taking up this issue!

@dazKind

@bjorn Sry, didnt mean to be pushy or something. Just wanted to make sure we keep up the discussion so I can proceed in a timely manner.

About the properties getting grayed out, I see you found the cause, but you also noted you "fixed" it. But I'm thinking, isn't it actually useful to keep them gray until their default value is overridden?

It depends on the assumptions I guess. If the default values are also set in the game then it makes sense to gray the properties out. But if the game has no knowlegde about the defaults you might want to explicitly export every property per object. Not sure what would be the most reasonable default behavior.

It is still true that object types are stored in the register on Windows. And you're totally right that this would be good to change to just a reference to a file that can be set. Moving that reference into the map file is not where I want to go, since I'd rather add support for project files to Tiled. Until that happens, I think it's maybe better to keep it in the preferences (though of course that assumes support for "projects" would come sooner rather than later).

Cool, so i will look into the file reference set in the preferences.

Unfortunately I don't have time for a code review right now, but please open a pull request so that I can better comment on the changes when I do have time. The feature does not need to be finished before opening a pull request.

No hurry. I'm implementing this while testing it in my workflow. Once I consider the obvious rough edges gone I'll make it a PR for official consumption and further discussion

@bjorn
Owner

But if the game has no knowlegde about the defaults you might want to explicitly export every property per object. Not sure what would be the most reasonable default behavior.

I think a perfectly sane default is to only store those properties which have actually been set (regardless of whether their value is the same as the default). If you always write out all properties you're needlessly bloating up the file for most people. Besides that, it would leave no easy way to change the defaults in a way that would directly affect all objects that did not have a certain property explicitly set up them.

@dazKind

makes sense. PR is up

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.