Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Help  Issue Saving/Loading Particle Positions in play mode using ObiRope v7.0
#1
Triste 
Hello,
I managed to save and reload particle positions in ObiRope v6.x using code under the Copy ropes in play mode thread. However, this no longer works in ObiRope v7.x. Could you help me identify what I might be missing?

Here’s my current code:


Code:
public void Load(Vector3[] particlePositions, Vector3 lastParticlePosition)
{
    int particleCount = particlePositions.Length;
    while (rope.activeParticleCount > particleCount)
    {
        rope.elements.RemoveAt(rope.elements.Count - 1);
        rope.DeactivateParticle(rope.activeParticleCount - 1);
    }

    while (rope.activeParticleCount < particleCount)
    {
        rope.elements.Add(new ObiStructuralElement
        {
            particle1 = rope.elements[^1].particle1,
            //maybe this should be so:
            //particle2 = rope.elements[^1].particle2,
            particle2 = rope.solverIndices[rope.activeParticleCount],
            restLength = rope.interParticleDistance
        });
        rope.CopyParticle(rope.activeParticleCount - 1, rope.activeParticleCount);
        rope.ActivateParticle(); // This doesn't have an actorIndex anymore.
    }


    for (int j = 0; j < rope.elements.Count; ++j)
    {
        int firstParticle = rope.elements[j].particle1;
        rope.solver.positions[firstParticle] = particlePositions[j];
        rope.solver.velocities[firstParticle] = Vector4.zero;
        rope.solver.angularVelocities[firstParticle] = Vector4.zero;
    }

    int lastParticle = rope.elements[^1].particle2;
    rope.solver.positions[lastParticle] = lastParticlePosition;
    rope.solver.velocities[lastParticle] = Vector4.zero;
    rope.solver.angularVelocities[lastParticle] = Vector4.zero;
    rope.RebuildConstraintsFromElements();
}

The save sections look like this:
Code:
int positionCount = rope.elements.Count;
Vector3[] particlePositions = new Vector3[positionCount];
for (int j = 0; j < positionCount; ++j)
{
    particlePositions[j] = rope.solver.positions[rope.elements[j].particle1];
}

Vector3 lastParticlePosition = rope.solver.positions[rope.elements[positionCount - 1].particle2];



Thanks!
Reply
#2
I tried something like this:

I instantiated a rope and saved it in a "long" form via a blueprint (to ensure a higher particle count). This way, I no longer needed to add new particles to rope.elements. Specifically, I commented out the following code block:

Code:
// while (rope.elements.Count < particleCount)
// {
//     rope.elements.Add(new ObiStructuralElement
//     {
//         particle1 = rope.elements[^1].particle1,
//         //particle2 = rope.solverIndices[rope.activeParticleCount],
//         restLength = rope.interParticleDistance
//     });
//     rope.CopyParticle(rope.activeParticleCount - 1, rope.activeParticleCount);
//     rope.ActivateParticle();
// }


By doing this, I could adjust the rope’s particle count by only removing particles, without adding new ones. However, I’m still unsure how to handle cases where I do need to add new elements.

Thanks!
Reply
#3
Hi!

(29-04-2025, 07:27 AM)ozeecode Wrote: rope.ActivateParticle(); // This doesn't have an actorIndex anymore.

It always uses activeParticleCount now. It's by far the most common case and the most efficient one as well: activate the last inactive particle, which is done by just bumping a counter.

(29-04-2025, 07:27 AM)ozeecode Wrote: Specifically, I commented out the following code block:

Code:
// while (rope.elements.Count < particleCount)
// {
//     rope.elements.Add(new ObiStructuralElement
//     {
//         particle1 = rope.elements[^1].particle1,
//         //particle2 = rope.solverIndices[rope.activeParticleCount],
//         restLength = rope.interParticleDistance
//     });
//     rope.CopyParticle(rope.activeParticleCount - 1, rope.activeParticleCount);
//     rope.ActivateParticle();
// }

By doing this, I could adjust the rope’s particle count by only removing particles, without adding new ones. However, I’m still unsure how to handle cases where I do need to add new elements.

This code appends a new element that links the first particle of the previous element with the new active particle. It should link the second particle of the previous element instead.

(29-04-2025, 07:27 AM)ozeecode Wrote: However, this no longer works in ObiRope v7.x. Could you help me identify what I might be missing?

Corrected the above two issues (ActivateParticle and reversed elements), tested it and it seems to work ok. Could you specify what you mean by "not working"? does it result in an error? does it do nothing? does it do something else that what you expected?

kind regards,
Reply
#4
(29-04-2025, 07:53 AM)josemendez Wrote: Hi!


It always uses activeParticleCount now. It's by far the most common case and the most efficient one as well: activate the last inactive particle, which is done by just bumping a counter.


This code appends a new element that links the first particle of the previous element with the new active particle. It should link the second particle of the previous element instead.


Corrected the above two issues (ActivateParticle and reversed elements), tested it and it seems to work ok. Could you specify what you mean by "not working"? does it result in an error? does it do nothing? does it do something else that what you expected?

kind regards,



Hello, thank you for your response .

First of all, I want to ask this:
When I directly instantiate the ObiRope prefab and place it into the solver, since it doesn't have any particles yet, this line throws the following error:

Code:
rope.ActivateParticle();
Code:
IndexOutOfRangeException: Reading from index 0 is out of range of '0' Capacity.
Obi.ObiNativeList`1[T].get_Item (System.Int32 index) (at Assets/Obi/Scripts/Common/DataStructures/NativeList/ObiNativeList.cs:83)
Obi.ObiActor.ActivateParticle () (at Assets/Obi/Scripts/Common/Actors/ObiActor.cs:628)
Rope.Load (UnityEngine.Vector3[] particlePositions, UnityEngine.Vector3 lastParticlePosition) (at Assets/_Game/Scripts/Rope/Rope.cs:206)

To overcome this, I first spawn the rope and then load the saved data:

Code:
Rope rope = Spawner.Instance.CreateRope(startAttachmentPoint.PinPoint);
rope.SetStartPin(startAttachmentPoint);
rope.SetEndPin(endAttachmentPoint);
await UniTask.Delay(100);
rope.Load(data.particlePositions, data.lastParticlePositions);

Is there a way to remove this delay?
Code:
await UniTask.Delay(100);




As for my main issue, if I use the Load() method I sent in my first message exactly as it is, none of the particle positions are set. They all remain in their initial positions. In other words, my rope appears as it was set in the blueprint. I suspect this happens because it's an asynchronous process, but I’m not sure.

Thank you.
Reply
#5
(29-04-2025, 08:55 AM)ozeecode Wrote: Hello, thank you for your response .

First of all, I want to ask this:
When I directly instantiate the ObiRope prefab and place it into the solver, since it doesn't have any particles yet, this line throws the following error:
ns, data.lastParticlePositions);[/code]

Is there a way to remove this delay?
Code:
await UniTask.Delay(100);

Actors aren't loaded into the solver right away, as this could happen while the simulation is underway on a different thread and cause race conditions. They're usually loaded at the start of the next simulation step.

Just wait until the actor has been loaded. Actors have an OnBlueprintLoaded event for this purpose (similarly, they have an OnBlueprintUnloaded event). There's also a isLoaded boolean that you can check later on, or on a coroutine.

(29-04-2025, 08:55 AM)ozeecode Wrote: As for my main issue, if I use the Load() method I sent in my first message exactly as it is, none of the particle positions are set. They all remain in their initial positions. In other words, my rope appears as it was set in the blueprint. I suspect this happens because it's an asynchronous process, but I’m not sure.

Correct: actor loading in Obi is asynchronous and always has been, so if your script worked in previous versions is likely out of luck.

kind regards
Reply
#6
Hello again,

I’ve managed to work around the async issues for now.

Regarding your comment when adding particles:

Quote:This code appends a new element that links the first particle of the previous element with the new active particle. It should link the second particle of the previous element instead.


Code:
while (rope.activeParticleCount < particleCount)
{
    rope.elements.Add(new ObiStructuralElement
    {
        particle1 = rope.elements[^1].particle1, //--> these are not correct!
        particle2 = rope.elements[^1].particle2, //--> these are not correct!
        restLength = rope.interParticleDistance
    });
    rope.ActivateParticle();
}
How can I determine the last active particle here? In other words, what should particle1 and particle2 be? I was not able to acquire index of the particle that was activated last.

I’ve also figured out what this causes. When used this way, all the ObiStructuralElement entries I add to the array are incorrect.

[Image: Screenshot-2025-04-29-124753.png]

As a result, the saved and loaded rope does not appear the same.

[Image: saved.png] [Image: loaded.png]

I think the difference between the two images is caused by this issue.

Thank you.
Reply
#7
(29-04-2025, 10:52 AM)ozeecode Wrote: Hello again,

I’ve managed to work around the async issues for now.

Regarding your comment when adding particles:



Code:
while (rope.activeParticleCount < particleCount)
{
    rope.elements.Add(new ObiStructuralElement
    {
        particle1 = rope.elements[^1].particle1, //--> these are not correct!
        particle2 = rope.elements[^1].particle2, //--> these are not correct!
        restLength = rope.interParticleDistance
    });
    rope.ActivateParticle();
}
How can I determine the last active particle here? In other words, what should particle1 and particle2 be?

What this code does is simply append a new element and a new particle to the rope. "()" being a particle and "-----" being an element linking 2 particles, a rope looks like this:

( A )------( B )-----( C )------( D )

And the list of elements for this rope would be:

element 1: particle1 = A, particle2 = B
element 2: particle1 = B, particle2 = C,
element 3: particle1 = C, particle2 = D

(29-04-2025, 10:52 AM)ozeecode Wrote: I was not able to acquire index of the particle that was activated last.

Just like elements, particles are stored in a list. Activating a particle simply adds an active particle to the list. So rope.activeParticleCount is the index of the particle that's going to get activated next, and rope.activeParticleCount-1 the index of the last active particle.

So when adding a new element, its first particle must be the second particle of the previous element. Its second particle must of course be the new particle we are about to activate.

Code:
rope.elements.Add(new ObiStructuralElement
    {
        particle1 = rope.elements[^1].particle2,
        particle2 = rope.solverIndices[rope.activeParticleCount],
        restLength = rope.interParticleDistance
    });

kind regards
Reply
#8
Thank you for your response. I’ve managed to correctly access the particle indices.
Currently, the particle positions appear correct, but only the particles I manually added are not visible in the scene. I also checked with the extrude renderer, and the rope seems a bit incomplete. Sonrisa Could I be missing something else?
[Image: loaded2.png]
Do I need to do anything extra to make the particles visible in the scene?
Reply
#9
Alright, I just realized I forgot to add this:

Code:
rope.CopyParticle(rope.activeParticleCount - 1, rope.activeParticleCount);
Now, the ropes load exactly as per the particle positions I saved.

Here’s the final version of the Load method:
Code:
public void Load()
    {
        int particleCount = particlePositions.Length;
        while (rope.activeParticleCount > particleCount)
        {
            rope.elements.RemoveAt(rope.elements.Count - 1);
            rope.DeactivateParticle(rope.activeParticleCount - 1);
        }

        while (rope.activeParticleCount < particleCount)
        {
            rope.elements.Add(new ObiStructuralElement
            {
                particle1 = rope.elements[^1].particle2,
                particle2 = rope.solverIndices[rope.activeParticleCount],
                restLength = rope.interParticleDistance
            });
            rope.CopyParticle(rope.activeParticleCount - 1, rope.activeParticleCount);
            rope.ActivateParticle();
        }

        for (int j = 0; j < rope.elements.Count; ++j)
        {
            int firstParticle = rope.elements[j].particle1;
            rope.solver.positions[firstParticle] = particlePositions[j];
            rope.solver.velocities[firstParticle] = Vector4.zero;
            rope.solver.angularVelocities[firstParticle] = Vector4.zero;
        }

        int lastParticle = rope.elements[^1].particle2;
        rope.solver.positions[lastParticle] = particlePositions[^1];
        rope.solver.velocities[lastParticle] = Vector4.zero;
        rope.solver.angularVelocities[lastParticle] = Vector4.zero;
        rope.RebuildConstraintsFromElements();
        AddConstraints();
    }

However, in the last line of the Load method, within the AddConstraints method, something seems to be going wrong.

I adapted the code from the Adding/Removing Constraints section to my use case, but it seems like colliderB isn’t linking to the last particle, while colliderA is working as intended.


Here’s the AddConstraints() method:
Code:
[SerializeField] private ObiCollider colliderA;
[SerializeField] private ObiCollider colliderB;
private void AddConstraints()
{
    //first particle in the rope is the first particle of the first element:
    int firstParticle = rope.elements[0].particle1;

    // last particle in the rope is the second particle of the last element:
    int lastParticle = rope.elements[^1].particle2;

    // now get their positions (expressed in solver space):
    Vector3 firstPos = rope.solver.positions[firstParticle];
    Vector3 lastPos = rope.solver.positions[lastParticle];

    colliderA.transform.position = firstPos;
    colliderB.transform.position = lastPos;

    // get a hold of the constraint type we want, in this case, pin constraints:
    var pinConstraints = rope.GetConstraintsByType(Oni.ConstraintType.Pin) as ObiConstraints<ObiPinConstraintsBatch>;

    // remove all batches from it, so we start clean:
    pinConstraints.Clear();

    // create a new pin constraints batch
    var batch = new ObiPinConstraintsBatch();

    // Add a couple constraints to it, pinning the first and last particles in the rope:
    batch.AddConstraint(firstParticle, colliderA, Vector3.zero, Quaternion.identity, 0, 0);
    batch.AddConstraint(lastParticle, colliderB, Vector3.zero, Quaternion.identity, 0, 0);


    // set the amount of active constraints in the batch to 2 (the ones we just added).
    batch.activeConstraintCount = 2;

    // append the batch to the pin constraints:
    pinConstraints.AddBatch(batch);

    // this will cause the solver to rebuild pin constraints at the beginning of the next frame:
    rope.SetConstraintsDirty(Oni.ConstraintType.Pin);
}

This script used to work normally in v6. As far as I can tell, the only change is the tearRope parameter:

batch.AddConstraint(firstParticle, colliderA, new Vector3(0, 0, 0), Quaternion.identity, 0, 0, float.PositiveInfinity);

What could be my mistake here?

https://www.youtube.com/watch?v=A-5ZFtkJ89k

Yellow sphere that is visible on the video is colliderA and the Blue one is colliderB



Thanks.
Reply
#10
I still haven’t figured out what I’m doing wrong in the Load() method.
Reply