Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Help  Rope saving and restore by Json
#1
Exclamación 
Dev environment: Obi Rope version 6.5.4 / Windows 10 / Unity 2021


Hello, sorry to bother you.  I am working on a puzzle game and need to save the data of twisted ropes into a JSON file, then read the JSON during gameplay to restore the ropes to their previous twisted state. ()



I have implemented the save and load functions and can confirm that the saved data is correct because I have tested the vectors' paths of the ropes, and they match what was saved.

However, when I apply the data to the instantiated ropes, I cannot restore the ropes to their previous state. When I use the vectors stored in JSON to restore the positions of the rope's particles, the ropes can never maintain their previous twissted state. The ropes are always spread out or lying there alone.


I found some posts in the forum:

https://obi.virtualmethodstudio.com/foru...-116.html?
The first post from 2017 mentioned frequently calling ObiRopeCursor.ChangeLength() to restore the shape of the ropes, but this method does not work for me, and I am also unable to call the PushDataToSolver method mentioned in the post. (This method only implemented in class ObiStitcher) However, I understand the reason for calling ObiRopeCursor.ChangeLength(). Calling this method can increase the activeParticleCount. The number of actor.activeParticleCount may be much smaller than the number of actor.particleCount, but the number of actor.particleCount is limited by the particleCount in ObiActorBlueprint.

https://obi.virtualmethodstudio.com/foru...4112.html?
The second post was updated in February 2024, which is very recent and provides sample code. The save and load code in the post is very similar to mine, with the only difference being that the loop code during restoration ignores the legality of solver.positions[particlesIndices[i]]. During actual operation, the number of _rope.solverIndices may be much smaller than the total amount of data stored in JSON, resulting in either loss of some data or array out-of-bounds errors.

Here is the code I used to instantiate the rope / saving and restoring the ropes.


Instantiate the rope:
 
Code:
private GameObject InstantiateRope(GameObject objectToAttach1, GameObject objectToAttach2)
    {
        var rpObject = objectPool.GetRopeFromPool();
        ObiRope rope = rpObject.GetComponent<ObiRope>();
       
        Transform transformA = objectToAttach1.transform;
        Transform transformB = objectToAttach2.transform;
        Vector3 PositionA = transformA.position;
        Vector3 PositionB = transformB.position;
        Vector3 objectScale = transformA.localScale;
       
        Vector3 offset = new Vector3(0, objectScale.y + 0.05f, 0);

        Vector3 startPositionLS = transform.InverseTransformPoint(PositionA + offset);
        Vector3 endPositionLS = transform.InverseTransformPoint(PositionB + offset);
        //Vector3 tangentLS = (endPositionLS - startPositionLS).normalized;
        Vector3 tangentLS = Vector3.zero;

       
        ObiRopeBlueprint blueprint = ScriptableObject.CreateInstance<ObiRopeBlueprint>();
        blueprint.path.Clear();
        blueprint.pooledParticles = 100;
       
        // Build the rope path:
        int filter = ObiUtils.MakeFilter(ObiUtils.CollideWithEverything, 0);
        blueprint.path.AddControlPoint(startPositionLS, -tangentLS, tangentLS, Vector3.up, 1f, 0.1f, ropeThickness, filter,
            Color.white, "start");
       
        blueprint.path.AddControlPoint(endPositionLS, -tangentLS, tangentLS, Vector3.up, 1f, 0.1f, ropeThickness, filter,
            Color.white, "end");
       
        blueprint.path.FlushEvents();

        // Generate particles/constraints:
        blueprint.GenerateImmediate();
       
        ObiParticleAttachment attachment1 = rpObject.AddComponent<ObiParticleAttachment>();
        ObiParticleAttachment attachment2 = rpObject.AddComponent<ObiParticleAttachment>();

        // Set the blueprint:
        rope.ropeBlueprint = blueprint;
       
        // Attach both ends:
        attachment1.target = transformA;
        attachment2.target = transformB;
        attachment1.particleGroup = blueprint.groups[0];
        attachment2.particleGroup = blueprint.groups.Last();

        // Parent the actor under a solver to start the simulation:
        rpObject.transform.SetParent(obiSolverObject.transform);
       
        return rpObject;
    }


Save the positions of the particles in the rope:
 
Code:
public JSONObject SaveJson()
    {
        jsonObject.AddField("ropeLength", gameObject.GetComponent<ObiRope>().restLength);
        jsonObject.AddField("particles", GetRopeParticleData());
        return jsonObject;
    }

  JSONObject GetRopeParticleData()
    {
        var rope = gameObject.GetComponent<ObiRope>();
        var solver = rope.solver;
        var particlesIndices = rope.solverIndices;
        var positions = new List<Vector3>();
       
        for (int i = 0; i < particlesIndices.Length; i++)
        {
            //if (rope.IsParticleActive(i))  // I was considering save active particles only.
            {
                positions.Add(solver.positions[particlesIndices[i]]);
            }
        }
        var positionsArray = JSONObject.emptyArray;
        for (int i = 0; i < positions.Count; i++)
        {
            positionsArray.Add((JSONNode)positions[i]);
        }

        var ropeObject = JSONObject.emptyObject;
        ropeObject.AddField("positions", positionsArray);
        //ropeObject.AddField("velocities", velocitiesArray);  // Velocities are all zero, so I didn't save.
        return ropeObject;
    }




Load the positions of the particles from JSON and set to particles in the rope:

   
Code:
public void LoadJson(JSONObject jsonObject)
    {
        ropeLength = jsonObject["ropeLength"].floatValue;
        gameObject.GetComponent<ObiRopeCursor>().ChangeLength(RopeLength);
        particleDataToRestore = jsonObject["particles"];
        rope.OnBlueprintLoaded += Actor_OnBlueprintLoaded;
    }

public void Actor_OnBlueprintLoaded(ObiActor actor, ObiActorBlueprint blueprint)
    {
        if (particleDataToRestore == null)
            return;
        var solver = actor.solver;
        var particlesIndices = actor.solverIndices;

        /*for (int i = 0; i < actor.solverIndices.Length; ++i)
            solver.invMasses[actor.solverIndices[i]] = 0;*/
       
        var positionsJsonArray = particleDataToRestore["positions"];
        if (positionsJsonArray != null && positionsJsonArray.type == JSONObject.Type.Array)
        {
            for (int i = 0; i < positionsJsonArray.list.Count; i++)
            {
                if (i < particlesIndices.Length)
                {
                    solver.positions[particlesIndices[i]] = positionsJsonArray[i].ToVector3();
                    // actor.ActivateParticle(i);
                }
            }
        }
       
        /*for (int i = 0; i < actor.solverIndices.Length; ++i)
            solver.invMasses[actor.solverIndices[i]] = 1f;*/
       
        particleDataToRestore = null;
    }


Attached Files Thumbnail(s)
               
Reply
#2
If I do not check whether "i < particlesIndices.Length", then the following code "solver.positions[particlesIndices[i]]" will cause an out-of-bounds error. The number of actor.solverIndices is often much smaller than the number of particles read from the JSON. I believe this may be because, when instantiating the rope, the initial path only sets the start and end point.

If I add "Time.timeScale = 0;" after calling  Actor_OnBlueprintLoaded(), the ropes freeze, and the shape appears consistent with how it was before saving. However, there are some branches and disarray.

If I enable particle debugging and hide the rope's model, you can see that the shape formed by the particles is correct. However, there are gaps in the areas marked with red circles, which look like some particles are missing. I suspect this is the possible reason why the ropes do not remain intertwined under the influence of physics.

Once the freeze is lifted, the rope's state immediately reverts to a scattered state, exactly the same as in the previous screenshots.

I even tried to increase the particle count array of the blueprint based on the number of particles in the JSON after the blueprint generation was completed, but it still didn't work.

I have been stuck here for a long time and have tried many methods. Have I missed something? Or is there a better and more efficient way to generate a rope with a specific shape at runtime based on the data?

Please help me, I would be extremely grateful!
Reply
#3
(22-06-2024, 06:37 PM)tapLucas Wrote: If I do not check whether "i < particlesIndices.Length", then the following code "solver.positions[particlesIndices]" will cause an out-of-bounds error. The number of actor.solverIndices is often much smaller than the number of particles read from the JSON. I believe this may be because, when instantiating the rope, the initial path only sets the start and end point.

Hi,

You're only storing particle positions and total rope length, but not storing constraint counts, parameters, elements, or anything else really. This assumes all other data that makes up the rope is the same for both the rope you store them from and the one you write them to. Is the rope you're using to deserialize your data the same you're using to serialize it?

Based on your post, my guess is that prior to deserializing your data you're instantiating a blueprint with just start/end control points. This will create a straight rope between both points, which will of course be shorter than the original one and have less particles and less constraints as a consequence.

If your goal is to save and then restore the particle positions, you must make sure the rope you're restoring them to is the exact same one you stored them from. This means using the same blueprint, and if you're using a cursor to change its length or you're tearing the rope, both ropes must undergo the same sequence of operations. Otherwise it stands to reason that it might have a different amount of particles, constraints, elements, etc and your code will break.

(22-06-2024, 06:37 PM)tapLucas Wrote: Or is there a better and more efficient way to generate a rope with a specific shape at runtime based on the data?

This is what blueprints are designed for: they define all properties of a rope just by storing a handful of control points. They also generate elements, constraints and particle data automatically upon being instantiated in a solver. They're also assets (scriptable objects) so they can be automatically serialized to disk. And if you're using asset bundles/addressables, they can also be remotely stored on a server.

Only situation where you can't rely on blueprints alone is when the rope's topology changes at runtime: when you've changed its length using a cursor, or when it has been torn/cut. In these cases you should store particle/element data instead.
Reply
#4
(25-06-2024, 08:29 AM)josemendez Wrote: What's your use case? Why are you using JSON to serialize this data?
I've reflected on it, and there are probably two main reasons for using JSON to store and restore ropes:
  1. Before I was preparing to implement the function of restoring ropes with data, I saw the above two posts in this forum. They all used the method of particle positions to restore the original shape of the rope, and from the responses in the posts, they seem to have succeeded. In addition, after using the Obi Rope plugin, my understanding is that it only requires basic particles to draw a complete rope.

  2. This reason is more important. Because I am developing a game that can be social in the future. For example, I made some complex entangled ropes, and when I want to share them with friends for decryption, I only need to serialize the rope data into JSON. Essentially, what is shared is just a string of characters. Others only need to parse the string when they open the game to restore the rope level I designed before. Because I was in a hurry to develop the function, I didn't spend too much time researching whether the blueprint could also achieve the function I mentioned above. My first intuition was that JSON is the most convenient and feasible, and the blueprint is limited by the serialization of the file (although when opening the blueprint with Notepad, you can see that the data stored inside is similar to what I need). If the data is based on the blueprint, then must it be shared through the server when sharing, because this is a file?
Reply
#5
(25-06-2024, 09:02 AM)tapLucas Wrote: Before I was preparing to implement the function of restoring ropes with data, I saw the above two posts in this forum. They all used the method of particle positions to restore the original shape of the rope, and from the responses in the posts, they seem to have succeeded.

Both the above posts use the exact same rope for both serialization and deserialization, which doesn't seem to be your case: you don't have any pre-existing rope to set particle positions to, since the original rope is gone. All you have is the data in your JSON, so you need it to contain a complete description of the rope.

Creating a different rope and then just setting the position of its particles won't work since the amount of particles and their order in the rope won't be the same, the rest distance between them won't be the same, the constraints holding them together won't be the same, etc.

To give you a real world example: imagine someone built a car, and then sent you data so you can build the exact same car without having ever seen it before. However this data only consists of the color of the car and the type of gearbox used. How can you be expected to build the same thing?

(25-06-2024, 09:02 AM)tapLucas Wrote: In addition, after using the Obi Rope plugin, my understanding is that it only requires basic particles to draw a complete rope.

Not at all: particles are just lumps of mass, all they can do by themselves is fall due to gravity. They need to somehow relate to and interact with each other in order to form any coherent shape such as a rope. This is done using constraints. In case of a rope, this involves distance and bend constraints. Quoting the asset's description:

Quote:Everything in Obi is made out of small spheres called particles. Particles can interact with each other, affect and be affected by other objects through the use of constraints.

Additionally, information about the topology of the rope needs to be stored for cases where the rope is cut/resized at runtime and particles/constraints need to be created/destroyed/reordered. This is done using elements.

Also particles have a lot more data than just positions: they have a size, they have mass, etc. Storing particle positions alone is not nearly enough to completely define a rope, or any kind of deformable object really.

(25-06-2024, 09:02 AM)tapLucas Wrote: Because I am developing a game that can be social in the future. For example, I made some complex entangled ropes, and when I want to share them with friends for decryption, I only need to serialize the rope data into JSON. Essentially, what is shared is just a string of characters.

If you want to send text data (instead of binary) over the network JSON is a good choice. However you can't just send particle positions unless all other data is the same both on the sending and the receiving end.

Assuming you want to store and send all the necessary data, and that your rope's topology doesn't change at runtime (no resizing or tearing) your best course of action is to store the blueprint's control points in JSON instead.

kind regards,
Reply
#6
I forgot to mention, I am truly grateful for your response to my question. For some time, I have been struggling to implement the restoration function. Your reply has brought a glimmer of light in the darkness, giving me hope.

Regardless of whether this issue can be resolved in the end, I express my sincere gratitude!  Gran sonrisa



(25-06-2024, 08:29 AM)josemendez Wrote: Is the rope you're using to deserialize your data the same you're using to serialize it?
According to my understanding, the ropes are different. After the ropes are moved and entwined by the players, multiple ropes become entangled and eventually stabilize. At this point, the storage function is called. When it is necessary to restore the rope, firstly, generate the simplest rope and fix the start and end points of the rope, because the two ends of the rope need to be fixed according to the Obi attachment, so that the rope can be dragged and move. Then I restore the data from JSON and set it to the particle, theoretically, these are two different ropes, so the data is also different.



(25-06-2024, 08:29 AM)josemendez Wrote: Based on your post, my guess is that prior to deserializing your data you're instantiating a blueprint with just start/end control points. This will create a straight rope between both points, which will of course be shorter than the original one and have less particles and less constraints as a consequence.

If your goal is to save and then restore the particle positions, you must make sure the rope you're restoring them to is the exact same one you stored them from. 

Regarding blueprints, from my code, you can see that I dynamically create blueprints at runtime, from my view, it just like a runtime particles container.


I still don't quite understand. If I still want to use JSON, what data must be saved? Are the ones you mentioned above enough? And if I need to restore, when should the timing be?
Reply
#7
(25-06-2024, 09:31 AM)tapLucas Wrote: generate the simplest rope and fix the start and end points of the rope, because the two ends of the rope need to be fixed according to the Obi attachment, so that the rope can be dragged and move. Then I restore the data from JSON and set it to the particle, theoretically, these are two different ropes, so the data is also different.

You're creating a straight rope between two points. However since it's a lot shorter than the original rope, it has less particles and less constraints: this can't possibly work.

You're creating a blueprint using just the start/end points, so you must be somehow storing/serializing the start and end points as well, correct?

If so, you should store all control points in the blueprint (not just the first and the last): at least their position, thickness, and tangent vectors. You'll also need to store the blueprint's resolution, as it determines the amount of particles created per distance unit.
This data determines the overall shape of the rope and the amount of particles/constraints used to create it. Then you'll be able to set the particle positions of the original rope, as you know both ropes have been created from the same blueprint and have the same data.

(25-06-2024, 09:31 AM)tapLucas Wrote: Regarding blueprints, from my code, you can see that I dynamically create blueprints at runtime, from my view, it just like a runtime particles container.

Blueprints store particle data, true. But they create this data based on a description of the object being created, in the case of ropes, a path defined by control points. If you use different control points, the amount of data and the values they store is different.

(25-06-2024, 09:31 AM)tapLucas Wrote: And if I need to restore, when should the timing be?

Anytime. First, create your blueprint from the data stored in the JSON. After the blueprint has been loaded, then set particle positions.

kind regards,
Reply
#8
(25-06-2024, 09:41 AM)josemendez Wrote: You're creating a blueprint using just the start/end points, so you must be somehow storing/serializing the start and end points as well, correct?
No, the storing of start and end points is not necessary, I was explaining what I have done in the code.



I feel there's a bit of a time lag, and I type a bit slowly. Thank you very much for your patient explanation.

I will carefully read and understand the points you mentioned. Let me give it a try.
Reply
#9
(25-06-2024, 09:55 AM)tapLucas Wrote: No, the storing of start and end points is not necessary, I was explaining what I have done in the code.

That's predefined in the scene then, right? Like, your game has a set amount of levels that contain the amount of ropes and their start/end, you're only interested in storing/sending/loading the state of each rope?

You'll need to store blueprint control points as well anyway, so the start/end points of each rope will be stored in your JSON. At this point you can probably just have one scene in your game and store the entire level setup in JSON.

(25-06-2024, 09:55 AM)tapLucas Wrote: I feel there's a bit of a time lag, and I type a bit slowly. Thank you very much for your patient explanation.

I will carefully read and understand the points you mentioned. Let me give it a try.

Sure! let me know if you need further help Sonrisa
Reply
#10
(25-06-2024, 09:58 AM)josemendez Wrote: That's predefined in the scene then, right? Like, your game has a set amount of levels that contain the amount of ropes and their start/end, you're only interested in storing/sending/loading the state of each rope?

You'll need to store blueprint control points as well anyway, so the start/end points of each rope will be stored in your JSON. At this point you can probably just have one scene in your game and store the entire level setup in JSON.


Sure! let me know if you need further help Sonrisa

Yes, because there are many ropes that need to be solved, I used an object pool to cache these ropes. But when creating ropes in advance, there is no need for specific shapes, so only the start and end points are given to temporarily fix the ropes. From a coding perspective, it is also to ensure that the blueprints created at runtime are successful, just like applying for space for positions, at least two points can represent a rope/line. Then, when facing data representing different shapes, reuse the entities of these ropes and set the positions in the blueprint. This is the final function I need.

I will try the solution you provided for me above, and if it is successful, I will come back to share the method and tell you the good news! Lengua  You've made my day, sir. Have a nice day!
Reply