mapeditor / tiled

Flexible level editor
https://www.mapeditor.org/
Other
11.03k stars 1.74k forks source link

Dynamic placeable objects with default properties for custom engine #365

Closed ghost closed 8 years ago

ghost commented 11 years ago

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 commented 11 years ago

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 commented 11 years ago

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 https://github.com/bjorn/tiled/issues/70

stefanbeller commented 11 years ago

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

ghost commented 11 years ago

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 commented 11 years ago

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

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, good luck! If it sounds too much, you can also "fund" me a few hours, btw.

ghost commented 11 years ago

@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 commented 11 years ago

@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.

ghost commented 11 years ago

@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

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 commented 11 years ago

@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 commented 11 years ago

@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!

ghost commented 10 years ago

@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 commented 10 years ago

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

zachprinz commented 10 years ago

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 commented 10 years ago

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 commented 9 years ago

@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 commented 9 years ago

@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 commented 9 years ago

@bjorn: I thought so. For what it's worth, I started working on this in my fork: https://github.com/dazKind/tiled/compare/bjorn:master...master

bjorn commented 9 years ago

@dazKind Awesome, looks like a good start!

dazKind commented 9 years ago

Ok, I got it to a functional state. Im adding the default properties in updateCustomProperties: https://github.com/dazKind/tiled/compare/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 commented 9 years ago

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 commented 9 years ago

@bjorn ping!

bjorn commented 9 years ago

@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:

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 commented 9 years ago

@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 commented 9 years ago

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 commented 9 years ago

makes sense. PR is up