Jump to content

[SOLVED] transform.Find() with full path to named transform


Recommended Posts

To elaborate :
I'm writing a plugin that will read part.cfg to get a transform for a part's sub-object.

There is part.FindModelTransform() but it relies on transform's name being unique.

The plugin i'm writing will be released to be used by part creators and will read transform name from part.cfg, so i don't have control over what gets entered.

What i want is to use part.transform.Find() that can use full path to named transform but so far i had no luck on my demo part. I tried to recursively build a path from transform to its parents by concatenating their names and it didn't work.

I did find an older post about pretty much the same problem and it did show some clear examples but it didn't work for me.

Here is example code i tried :

Transform partTransform = part.FindModelTransform("gate_shield_root");
string transformPath = partTransform.name;
while (partTransform.parent != null)
{
partTransform = partTransform.parent;
transformPath = partTransform.name + "/" + transformPath;
}
Debug.Log ("path = " + transformPath);
Debug.Log("find() = " + (transform.Find(transformPath) != null).ToString());

This printed out

HX1AerodynamicGateShell/model/0000_MyParts/HX_1_Aerodynamic_Gate_Shell/model(Clone)/base/gate_shield_root

and Find() failed on that one.

In Unity, my model hierarchy looks like this (names are as seen in hierarchy view) :

GameObject > base > gate_shield_root

The only clue how a path should look like in the above mentioned post was this (extra spaces for clarity)

model / Your_Game_Object_In_Unity / Root_Object_In_Model / Sub_Object / ... / Sub_Object_WIth_Needed_Transform

 

Edited by fatcargo
Link to comment
Share on other sites

23 minutes ago, fatcargo said:

This printed out

HX1AerodynamicGateShell/model/0000_MyParts/HX_1_Aerodynamic_Gate_Shell/model(Clone)/base/gate_shield_root

and Find() failed on that one.

In Unity, my model hierarchy looks like this (names are as seen in hierarchy view) :

GameObject > base > gate_shield_root

The only clue how a path should look like in the above mentioned post was this (extra spaces for clarity)

model / Your_Game_Object_In_Unity / Root_Object_In_Model / Sub_Object / ... / Sub_Object_WIth_Needed_Transform

 

Are you sure that's what it printed? Does your part config use MODEL nodes? What does the hierarchy actually look like in game? That would tell us a lot. Here's a snippet you can edit if you don't already have something similar:

    public static class GameObjectExtensions
    {
        public delegate void VisitorDelegate(GameObject go, int depth);


        public static void TraverseHierarchy(this GameObject go, VisitorDelegate visitor)
        {
            if (go == null) throw new ArgumentNullException("go");
            if (visitor == null) throw new ArgumentNullException("visitor");
            TraverseHierarchy(go, visitor, 0);
        }

        private static void TraverseHierarchy(GameObject go, VisitorDelegate visitor, int depth)
        {
            visitor(go, depth);

            foreach (Transform t in go.transform)
                TraverseHierarchy(t.gameObject, visitor, depth + 1);
        }


        public static void PrintComponents(this GameObject go, ILog baseLog)
        {
            if (go == null) throw new ArgumentNullException("go");
            if (baseLog == null) throw new ArgumentNullException("baseLog");

            go.TraverseHierarchy((gameObject, depth) =>
            {
                baseLog.Debug("{0}{1} has components:", depth > 0 ? new string('-', depth) + ">" : "", gameObject.name);

                var components = gameObject.GetComponents<Component>();
                foreach (var c in components)
                {

                    baseLog.Debug("{0}: {1}", new string('.', depth + 3) + "c",
                        c == null ? "[missing script]" : c.GetType().FullName);
                }
            });
        }
    }

Edit: Also Find() would fail in your case because you've appended the name of the parent itself. If HX1AerodynamicGateShell/model/0000_MyParts/HX_1_Aerodynamic_Gate_Shell/model(Clone)/base/gate_shield_root  is what your code printed, your PartModule (which is attached to that top transform) should transform.Find("model/0000_MyParts/HX_1_Aerodynamic_Gate_Shell/model(Clone)/base/gate_shield_root")

Edited by xEvilReeperx
Link to comment
Share on other sites

Here are some results :

I was able to get a transform from a path, but in my haste i deleted that code thinking i was on track so i don't remember how i did it. A leading slash character maybe...

As for part.cfg, i did reference my model as per guidelines. I used same part.cfg while modeling my part and writing code for testing, it did not give me any trouble before. I also tried to copy/paste the above "GameObjectExtensions" code into my namespace but SharpDevelop complained that ILog could not be found (CS0246).

Here are important lines from part.cfg :
 

PART
 {
 name = HX1AerodynamicGateShell
 module = Part
 author = fatcargo
 MODEL
  {
  model = 0000_MyParts/HX_1_Aerodynamic_Gate_Shell/model
  }
//stuff like attachment nodes, weight, cost etc...
MODULE
  {
  name = ChainedAnimationsModule
  // more stuff to be used later in plugin development
  }
}

More info to come tomorrow. Thanks for taking time to reply.

Edited by fatcargo
Link to comment
Share on other sites

5 minutes ago, fatcargo said:

I also tried to copy/paste the above "GameObjectExtensions" code into my namespace but SharpDevelop complained that ILog could not be found (CS0246).

That's part of my own library; you'd need to replace them with your own log statements or Debug.Log.

When GameDatabase loads the part models, their top level transform names get replaced with the URL of the model. That's why Find() is choking for you... Unity is treating the forward slashes like transform separators. You'll have to avoid using Unity's transform.Find for parts with MODEL nodes defined. Also apparently the model transform is no longer its own GameObject in parts without MODEL nodes; the top-level model transform name just gets overwritten with "model".

This is a bit of code I tinkered with. Your input here would be as though you were referring to the hierarchy inside Unity, so the url argument for your case would be "GameObject/base/gate_shield_root". It was not tested for parts with multiple MODELs at all. There's a wrinkle there: because the game overwrites the original top level node name, you won't be able to distinguish between multiple MODELs if only searching for the top node. That might or might not be a problem. If it was serious enough, you could parse the original Mu to read it back again if you had to

        private static Transform FindModelTransformEx(Part part, string url)
        {
            if (!part.partInfo.partConfig.HasNode("MODEL"))
            {
                return part.transform.Find(string.Join("/", new[] {"model"}.Union(url.Split('/').Skip(1)).ToArray()));
            }

            foreach (var modelConfig in part.partInfo.partConfig.GetNodes("MODEL"))
            {
                var transformNames = new Queue<string>();
                var modelUrl = modelConfig.GetValue("model");
                var pathPortions = url.Split('/');
 
                if (string.IsNullOrEmpty(modelUrl)) continue;

                transformNames.Enqueue("model");

                pathPortions[0] = modelUrl + "(Clone)";

                foreach (var portion in pathPortions)
                    transformNames.Enqueue(portion);
               
                var result = FindTransform(part.transform, transformNames);

                if (result != null) return result;
            }

            return null;
        }


        private static Transform FindTransform(Transform search, Queue<string> transformNames)
        {
            if (!transformNames.Any())
                return null;

            var target = transformNames.Dequeue();

            foreach (Transform child in search)
                if (child.name == target)
                {
                    if (transformNames.Any())
                        return FindTransform(child, transformNames);
                    return child;
                }

            return null;
        }

 

Link to comment
Share on other sites

Sorry for late response, i'm busy doing other things, so not much time left for modding KSP.

Thanks for the source, it gave me few ideas about custom TransformFind(). Use of Queue class was an eye-opener (i'm new to C# myself), and i'll try to clone/mimick the above code and form my own function. For now, idea is to use Stack class (its reverse of Queue and will do nicely) to try and create a kind of recursive function without calling it from inside and avoid using system/program stack. A kind of recursion without recursive function :) This is turning out to be also a programming class as well, not just plain API calls bunched together. Anyway, once i build a recursive loop (finally an adequate expression !) it will build an array of strings, each constructed of branch elements that will represent a branching path. Now all i have to do is match one of those paths to one provided in part.cfg. Since all paths should be unique, there ever is only one match possible (unless Unity allows for sibling-components with same name in hierarchy, which is a bad practice anyway). Building such an array of paths and not trying to match inside loop itself may be bruteforce, but it is simple.

Link to comment
Share on other sites

ATTENTION : I tried several times to correct a mistake with GetChild() in example sources, it should read GetChild(0). I tried to reload same link in different browser (thus avoiding cached version) and problem persisted.

I have found it. My assumption that Transform.Find() didn't work in KSP was only partially correct. It does not work if one tries to use it straight from part.transform.Find(), instead it needs to go two "levels" down in hierarchy where GameObject resides, namely :

Transform rootTransform = part.transform.GetChild().GetChild();
Transform targetTransform = rootTransform.Find("base/gate_1_root/gate_1/gate_1_collider_1"));

To elaborate on the above : the hierarchy visible in Unity3D editor begins after a second-order child transform.

In my designed part, hierachy looks like

GameObject > base > gate_1_root > gate_1 > gate_1_collider_1

Here is a breakdown :

Debug.Log ("part transform : " + part.transform);

// prints "HX1AerodynamicGateShell (UnityEngine.Transform)"

// this is same as name field in PART{} node in part.cfg

Debug.Log ("part transform child" + part.transform.GetChild());

//prints "model (UnityEngine.Transform)"

//hmm looks like variable "model" field in MODEL{} node in part.cfg

Debug.Log ("part transform child of child" + part.transform.GetChild(). GetChild());

//prints "0000_MyParts/HX_1_Aerodynamic_Gate_Shell/model(Clone) (UnityEngine.Transform)"

//this one seems to reflect a folder path under GameData directory with a cloned model thrown at end

So, all that is required to use Transform.Find() (note the upper case) is to go two levels down and then you can input the usual "parent/child/grandchild" format string.

NOTE : i did manage to make a rather complicated (and now obsolete) function to augment the "lacking" Find(), and the above should work for anyone.

And to @xEvilReeperx in the above i referenced branch with GetChild(0), maybe if using multiple MODEL {} nodes there will be another branch with GetChild(1) ? If anyone has a need for this, it can be easily tested. If i ever need this, i'll try to see if it works.

 

 

Edited by fatcargo
Link to comment
Share on other sites

3 hours ago, fatcargo said:

I have found it. My assumption that Transform.Find() didn't work in KSP was only partially correct. It does not work if one tries to use it straight from part.transform.Find(), instead it needs to go two "levels" down in hierarchy where GameObject resides, namely :@xEvilReeperx

I guess I didn't do a good job explaining. The problem is that parts defined using MODEL{} nodes have the GameDatabase model cloned. This is an issue for Transform.Find because the GameDatabase model top-level transform gets renamed to its url inside GameData, so "GameObject > base > gate_1_root > gate_1 > gate_1_collider_1" is renamed to "YourPluginFolder/muname > base > gate_1_root > gate_1 > gate_1_collider_1". The overwritten name now contains slashes which Transform.Find will incorrectly interpret. The two problems I have with your solution is that it will fail for parts that aren't defined using MODEL{} and the only way to specify the uppermost node of the model would for your code to check for a blank path (because that's what part.transform.GetChild(0).GetChild(n) will return)

Edit: It might make it clearer just to show you the hierarchy dumps. Here's what my test part looks like in Unity:
6f7349fb28.png

Here's what it looks like loaded from the regular mesh = model.mu style inside a part ConfigNode:
 

Spoiler

checkpart has components:
...c: UnityEngine.Transform
...c: Part
[snip]
->model has components: // formerly known as "base"
....c: UnityEngine.Transform
....c: UnityEngine.BoxCollider
....c: UnityEngine.MeshFilter
....c: UnityEngine.MeshRenderer
....c: HighlightingSystem.Highlighter
-->Stalk has components:
.....c: UnityEngine.Transform
.....c: UnityEngine.BoxCollider
.....c: UnityEngine.MeshFilter
.....c: UnityEngine.MeshRenderer
--->Hinge has components:
......c: UnityEngine.Transform
---->Capsule has components:
.......c: UnityEngine.Transform
.......c: UnityEngine.MeshFilter
.......c: UnityEngine.MeshRenderer

Here's the same part, loaded via MODEL{}

Spoiler

checkpart has components:
...c: UnityEngine.Transform
...c: Part
...c: ModuleCommand
...c: PartResource
...c: ModuleReactionWheel
...c: ModuleScienceExperiment
...c: ModuleScienceContainer
...c: PartResource
...c: FlagDecal
...c: ModuleConductionMultiplier
...c: ModuleTripLogger
->model has components:
....c: UnityEngine.Transform
....c: HighlightingSystem.Highlighter
-->/telescope(Clone) has components: // URL of model, in this case it's telescope.mu in the root of GameData. The slashes confuse Transform.Find. Formerly known as "base"
.....c: UnityEngine.Transform
.....c: UnityEngine.BoxCollider
.....c: UnityEngine.MeshFilter
.....c: UnityEngine.MeshRenderer
--->Stalk has components:
......c: UnityEngine.Transform
......c: UnityEngine.BoxCollider
......c: UnityEngine.MeshFilter
......c: UnityEngine.MeshRenderer
---->Hinge has components:
.......c: UnityEngine.Transform
----->Capsule has components:
........c: UnityEngine.Transform
........c: UnityEngine.MeshFilter
........c: UnityEngine.MeshRenderer

I do like the idea of ignoring the URL transform like you have though, it lets you clean up the earlier method a bit to this:

private static Transform FindModelTransformEx(Part part, string transformPath)
{
    if (string.IsNullOrEmpty(transformPath))
        throw new ArgumentException("must specify a url", "transformPath");

    var pathParts = transformPath.Trim('/').Split('/');
    var modelTransform = part.transform.Find("model");

    if (pathParts.Length == 1)
    {
        if (modelTransform.childCount == 1)
            return modelTransform.GetChild(0); // no way to tell for certain this transform name matches without parsing the mu again

        throw new ArgumentException(
            "The part has multiple models and there isn't enough information to figure out which one " +
            transformPath + " specifies");
    }

    var targetPath = string.Join("/", pathParts.Skip(1).ToArray());

    if (!part.partInfo.partConfig.HasNode("MODEL"))
        return pathParts.Length == 1
            ? modelTransform
            : modelTransform.Find(targetPath);

    foreach (Transform model in modelTransform)
    {
        var found = model.transform.Find(targetPath);

        if (found) return found;
    }

    return null;
}

 

Edited by xEvilReeperx
Link to comment
Share on other sites

Not wanting to criticize, but KSP wiki part cfg Model Parameters section states that "mesh" should be avoided in favor of MODEL{}. I guess "mesh" field is still valid because its not too hard for devs to maintain in new versions of KSP. I hope that if i choose to use the simpler Find() method in my plugin and force part creators to use single MODEL{}, nobody will hold it against me :)

PS: I was surprised how many parts still have "mesh" in their cfgs.

PPS : If there is a thread that collects useful code snippets, examples etc, FindModelTransformEx() should be included.

Edited by fatcargo
Link to comment
Share on other sites

This thread is quite old. Please consider starting a new thread rather than reviving this one.

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...