At long last.. an object merger! What is this wondrous thing and why?
Merging Unity assets used to be impossible when scenes were binary-only. With text assets, things have gotten much better, but shit still fucks up sometimes. So, we introduce a tool to allow a user to compare two GameObjects and everything about them, in the same interface you use to edit the original objects. Hooray!
Why exactly can’t we merge via text? There’s a lot of extraneous information which is useful to Unity but really doesn’t matter to us, so that gets confusing, and it seems that Unity does not maintain consistent ordering when saving scenes and prefabs (the real kicker). This means that when you go back to merge the scene, unless the merger can identify areas that have been moved, you end up with duplicated and/or mangled objects in the scene. There is a Scene Merge tool on AssetStore that supposedly sorts the data internally so that you can do text-based merges. I haven’t used it so I can’t say whether it works well, but it could be used along with, or instead of, this extension.
But no more confusion! Now we try to keep Git out of the equation and merge everything within Unity, where this ordering and deserialization will have happened just as Unity will do it again and again. As long as your scene is intact in one form or fashion, you can get your changes back. Now you can actually make use of those changes and merge ‘em in.
Anyway enough about the tool, here’s how to use it! Let’s start with a very basic case. We have two versions of some object and want to see if/how they are different. Our job is to make them the saaaaame.
How To Merge Objects
- Open the ObjectMerge Window (Window -> Object Merge -> Object Merge)
- Drag your objects from the scene to "mine" and "theirs" at the top of the window
That’s it! Now you’ll see the interface in all its glory. If your objects are simple, there’s not much to inspect. If you have a more complicated object, you might get something like this:
Note: I seem to be unable to upload images to the wiki, so for now the screenshot is hosted externally.
So what’s going on here?? I’ll break it down:
- Options: Deep Copy will set references to objects you just copied in the object it was copied to. Disable this if you don't want this behavior or if copying a large, complex object crashes Unity (sorry about that). Log will enable logging on certain operations.
- Filters: The drop-downs on the right will list every component type available in your project. Use them like you use the layer mask GUI on lights and cameras to include/exclude component types from the comparison. Those components will still show up red if there are differences, but they won't be checked in their parent object. All three lists are checked simultaneously, and have to be broken up because Unity's mask GUI can only handle 31 items at a time.
- Root object slots: Drop your root objects here. Once you fill both, the merge interface will magically appear! Use the clear button if you want to get rid of the interface (for some reason). The object picker breaks because we’re using a GUISin for the window. The PrefabInstance you see below the object field tells you the prefab state of the object (this one happens to have no prefab).
- Foldouts: GameObjects, Components, and some properties will be listed with a little arrow next to them. As with everywhere else in the Unity interface, this arrow expands the contents below. Note that all foldouts here are tied to both sides at once. This is intentional in order to keep the layout sane and make it so that blank space means the object is actually missing.
- Show/Hide for components: To differentiate between children and components, components are shown/hidden by a button instead of the foldout. I’ll be trying to make more visual changes to segregate the GUI areas and avoid clutter.
- Mid buttons: These have a lot of different states. Generally speaking, the one on the left pertains to an object on the left, and the right to the right. There’s one situation where a button exists but doesn't work - this is temporary. If you try to copy an object to an empty space which is also empty in the parent, you’ll get a warning telling you to copy the parent over. Moving on...
- (I didn't select the first example because the diagram was getting too cluttered) This set of buttons applies to a GameObject. Soon I’ll be differentiating the rows visually, but either way, it is important to understand that these arrows do specific things. When copying between two existing GameObjects, this copies all components and properties in the direction of the arrow. If you click the right button, the left side will override the right, and vice versa.
- These arrows copy data between components. All properties are copied.
- These arrows copy data between properties. Only the value at this row is copied.
- Sometimes, when one side is missing, you’ll get an X on the side that has the object. This will delete that object (or component), thus making that row the same by eliminating it.
- Sometimes you won’t see buttons. This is because the opposite object is missing entirely (not just missing that component). You will also get this for any properties of a missing component. You need to copy the component over to get those properties--you can’t copy them into nothing.
- Empty space: This shows up only when a component or GameObject is missing from that side’s object. You can copy the missing thing wholesale, or delete the opposite side. Either way, that red stuff’s not going away until you do one or the other.
- Mismatched row: If an object or component doesn't have a spouse (matching object on the other side), you will get an X button on the side where it exists. This button will destroy the object or component, thus making that row "equal" by getting rid of it. You will not get copy buttons when showing components of a mismatched object. You must first copy the object over to copy the components.
- You can pull up any scene or prefab from any state by using TortoiseGit’s Repo Browser. Open the log, find the commit you want, right click it and choose Repo Browser. Then find the file you want, and Save as... to the assets folder. Then merge! Good Luck!
- The conflict check (red/green background) compares all components, fields, and child objects, but the part of the code that checks if children are “the same” only compares names. The difference here is that in cases where an object on the left is not matched on the right, only names are considered. There is no “history” to tell us whether two copies of a object used to be the same thing, so this is the best we can come up with so far. Bear in mind that re-naming sub-objects will cause them to be treated as “unmatched” in this way.
Scene merging is pretty simple, too. It also has a very similar workflow! Simply do the following:
- Open the SceneMerge Window (Window -> Object Merge ->Scene Merge)
- Drag in your scenes
- Press the Merge button
This will open the scenes, "mine" first, and put their contents into objects called "mine" and "theirs." You will be in the “mine” scene, should you choose to save at any point afterward. Once the objects are grouped, the ObjectMerge window will open automagically with those two objects loaded up. Once you’re done merging, simply click Unpack Mine or Unpack Theirs to choose one or the other object. You should be back where you started, with a merged scene :)
Generally speaking, you shouldn’t have to touch this window. Everything here has to do with loading the custom skin for the colored backgrounds. The text fields set name/path values that are used to load the skin and reference the custom styles that define the background colors. There are 8 texture fields below these to override the default colors.
If you are red/green colorblind, just go ahead and drop some alternate colors into these textures. You can either create a new set of 8 images, or edit the images themselves directly. If anyone comes up with some good alternate color schemes, I’d be happy to include them and a drop-down to switch between pre-made schemes.
This is where it gets a bit complicated. This was my first experience putting a custom merge driver into gitconfig, so if I’m doing it wrong, I won’t be surprised. From what I can tell, there are two files you have to modify. Add the following lines to the following files:
*.unity merge=unity *.prefab merge=unity
- <Local .git folder>/config
[merge "unity"] name = Unity merge driver = wscript.exe "D:/Documents/GimbalCop/GimbalCop/merge-unity.js" %O %A %B //E:jscript
If you want to have this happen with TortoiseGit, go to TortoiseGit->Settings->Diff Viewer->Merge Tool->Advanced... and add two rules:
Extension or mime-type: .unity External Program: wscript.exe "D:\Documents\GimbalCop\GimbalCop\merge-unity.js" %merged %theirs %mine %base //E:jscript
Extension or mime-type: .prefab External Program: wscript.exe "D:\Documents\GimbalCop\GimbalCop\merge-unity.js" %merged %theirs %mine %base //E:jscript
If you don’t care about command line, I’m not sure the first step is needed.
I obviously have to figure out a better way to point to that script path. Either way, that path will be the path to a JScript script that is run by the Windows Scripting Host (should be on all versions post-XP) This script will check if Unity is running and depending on the state do one of two things. It will either (If Unity isn’t running) boot Unity and trigger the scene merge, or it will drop a file called merges.txt into the Assets folder. If you have the SceneMerge window open, it will listen for that file drop and trigger a merge as soon as it updates.
This seems to happen only on resolving conflicts. I’m not exactly sure how to make it do this every time a scene or prefab merge happens. Also, it obviously only works on Windows. It won’t be too big a deal to get it working on Mac/Linux... Oh right, no Unity Editor on Linux ;P
Right now, the script doesn’t clean up after itself since I don’t want it to be destructive while I work on it. So you’ll have to delete the .REMOTE, .BASE, etc. files generated by Git in this process.
Not sure if there’s an automated way of setting up this process. In lieu of tortoisegit, setting up a mergetool has always been kind of the bane of my git existence. It’s pretty obscure and nobody seems to want to help you do it =/
Other VCS Integration: Integrating this tool into other VCS solutions should be very straightforward. I’m not sure exactly how other systems specify mergetool overrides, but regardless you should be able to point them at a path which will open the bridge script. We could either create a specific bridge for each VCS, or merge them all into a single script which would auto-detect which software was calling it. Contact me if you want help setting up your VCS with the ObjectMerger. Or, if you’ve figured it out already, I’d greatly appreciate it if you shared your bridge script and some instructions so that other users of your VCS don’t have to re-invent the wheel.
Anyways, that should be all. Happy Merging!
The following tests will indicate whether any code changes have created issues in the tool. For all merge tests, I’m going to assume mine -> theirs for the sake of semantics. They should obviously work the same way in both directions
- Merge root object
- Should destroy “theirs” object and duplicate “mine.” Check that there are no errors
- Merge sub-object with no spouse
- Should duplicate “mine” and make it a child of the object in “theirs” that corresponds to the first object’s parent. If “theirs” has no such parent, a LogWarning will fire explaining why the merge can’t be done. Additionally, if any GameObjects or Components in the first object’s tree are referenced by any object within “mine,” the corresponding references should be set in “theirs”. If log is enabled it should print out all of the references that were set.
- Merge sub-object with spouse
- Should copy all components, properties, and children into the spouse. If log is enabled, any references which are set will be logged.
- Remove sub-object with no spouse
- Should destroy the sub-object and set all references to it to null. If logging is enabled, refs set to null are logged.
- Merge component with no spouse
- Should create a new component on the other object and copy all property values. Also sets any references to the new component. If log is enabled, all set references will be logged.
- Merge component with spouse
- Should copy all property values over. No references are set, nothing is logged.
- Remove component with no spouse
- Should destroy the component and set any references to it to null. If logging is enabled, refs set to null are logged
- Merge property
- Should set the value of the “theirs” property to that of “mine”. If the property references a component or GameObject, it attempts to copy the referenced object to the corresponding parent on “theirs.” This will also find and set references like a regular copy. If logging is enabled, ref settings and copy are logged.
- Refresh button
- Should refresh the object and all of its children. This will collapse children, and should set background colors right.
- Merge Scenes
- You shouldn’t be able to click Merge or the Unpack buttons at first. Once you drop in two scenes (you can actually use the same scene twice if you’re just testing), Merge should become enabled. Once you hit merge, the ObjectMerge window will open, and you’ll be able to click the Unpack buttons. As soon as you delete mine or theirs (or open another scene), the corresponding button should ghost out. Clicking Unpack should unpack one or the other object and delete both container objects.
- Git integration
- This is a little tough to test. Git should try to open Unity if it isn’t running whenever a scene or prefab is merged, and/or when you try to resolve a conflicted merge. In some cases, the git integration doesn’t work because it doesn’t create copies of the scene to be opened in Unity
If Unity is already open when the merge happens, and you have the SceneMerge window up, it will listen for a certain file that the bridge script will create and open the scenes or prefabs automatically.
- Actions will not update an object’s grandparents’ conflict state. You will have to manually hit refresh. This is not done automatically to avoid collapsing the children.
- Clicking a foldout label won’t toggle it. This is Unity’s fault.
- There are some issues with the pro skin. As it stands, I can’t save the controls images (buttons, toggles). To get it all working, do the following:
- Enable dark skin
- Open Window -> ObjectMerge -> CopyEditorSkin, and drag in the ObjectMergeDark skin from the ObjectMerge folder
- Click the button
- Enable light skin
- Enable dark skin
- You should now see the buttons and toggles
- It seems like you should be able to set it up to live in an alternate folder with the ObjectMergeConfig screen, but those paths will all get re-set when you close Unity. To actually change this, modify the script itself. I’ll make this a little more convenient as soon as it’s needed.
- Git integration is windows-only for now. Translating the JScript should be trivial, but if someone needs help, I’d be happy to set up an OS X bridge.
- The license is a little sketchy. The project isn’t strict open-source but I want to allow people to modify the code and share their modifications. There must be a license for this out there...
- Sometimes the alternating-darkness for the background doesn’t quite alternate right.... not sure what’s up with that.
- In light (free) skin, if you open the object browser from this window, the browser window is broken and you get a bunch of skin-related warnings. The solution to this is to unload the custom skin when spawning other editor windows, but I’m not sure how to do this for the object field.
- VCS integration
- I’d love to have an OS X bridge and bridges/guides for integrating into other Version Control Systems.
- Better filtering
- The current UI is a little awkward, and you can only filter by component type. I’d also like to filter out property names and/or object names
- Merging lists of objects
- Currently, for the purposes of merging two parallel lists of children, only object names are compared. For example, we have one object with Child 1, Child 2, and Child 3, and another with Child 2, Child 3, and Child 4. As it stands, we’ll see a list with 4 items. Since there is no child named Child 1 it will be alone on the left, and likewise Child 4 will be alone on the right. It might be appropriate to merge the list differently, and we’d be able to make a better decision with information about the object’s history. Anyway this is an area for improvement, but I have no idea what would be better.
- "Smarter" merging/merge options
- Currently the “merge” buttons in the middle will simply copy the object over wholesale. As a first step in the direction of “smart” merging, the tool will try to maintain references to the object and its children and components, but there could be other merge strategies that could avoid overwriting other objects, or something like that.
- There’s an issue with the dark skin which I’d like to solve. Likewise, I’d like to make changing the root folder a bit more convenient. Also, the GUI is rather simple as-is but is still pretty busy. I’m constantly thinking about ways to improve it.
- Refresh behavior
- When changes are made, I avoid refreshing from the root for two reasons: firstly, for very complicated objects, refresh can take a few seconds. Secondly, the current refresh function will re-create the holder objects that store whether a row is collapsed or open. If I refreshed the whole tree on every action, you would have to keep re-opening the tree to where you were. Likewise, whenever changes are made in the scene, you have to manually refresh the row in the merge window. Eventually, I’d like to be able to do away with the refresh button altogether, and auto-refresh whenever is needed.
- I hope to add some screencasts and better screenshots of tool in action.
- Keeping objects in the window on script compile
- This only really affects developers, but whenever unity compiles scripts, it resets the references to the two root objects. It’d be nice if they stuck around between compiles
- Code cleanup
- The way that GameObject attributes are handled is stupid. It should be less stupid
- Along with Refresh(), the various functions around Copy could be re-organized and made more clear.
- FindRefs might work better and faster if it just searches from the root instead of the current object
- Might have to turn FindAndSetRefs into a coroutine for really large objects
- Refresh and FindAndSetRefs are O(n^2). I might be able to make this faster