AnnulusGames/LitMotion

LMotion.Shake keeps working while Time.scale == 0f. How can I make it follow the time scale properly?

qwe321qwe321qwe321 opened this issue · 5 comments

As a workaround, I can make it stop via changing the scheduler to FixedUpdate.

But I still want it to make it still update every frame rather than FixedUpdate but also follow the timeScale, which makes more sense as other motions work properly with the time scale in Update.

ok now I read the doc and the code.

public readonly struct FloatShakeMotionAdapter : IMotionAdapter<float, ShakeOptions>
    {
        public float Evaluate(ref float startValue, ref float endValue, ref ShakeOptions options, in MotionEvaluationContext context)
        {
            VibrationHelper.EvaluateStrength(endValue, options.Frequency, options.DampingRatio, context.Progress, out var s);
            float multipliar;
            if (options.RandomState.state == 0)
            {
                multipliar = SharedRandom.Random.NextFloat(-1f, 1f);
            }
            else
            {
                multipliar = options.RandomState.NextFloat(-1f, 1f);
            }
            return startValue + s * multipliar;
        }
    }

The Shake implementation is basically same with Punch, but with the random position.
However, this could be a problem that it is dependant on frames/updates and ignores the Progress value, which means it will never be deterministic or contorllable within a time scale.
I believe it is not an intentional decision, especially since I noticed there is a withRadnomSeed option attempting to control the randomness, but this Shake implementation does not seem feasible.

I think the random number could be sampled from a hash function with Progress value and the seed (like how shaders do randomness) rather than iterating from the RNG.
@AnnulusGames What do you think?

I made a simple approach with perlin noise function. I think this was pretty close to what I expected.

Result

2024-03-20.08-51-42.mp4

Test.cs

        void Update()
        {
            if (Input.GetKeyDown(KeyCode.Space))
            {
                handles?.Complete();
                handles = new();
                LMotion.Create(-5f, 5f, duration)
                    .BindToPositionX(target.transform)
                    .AddTo(handles);
                LMotion.Shake.Create(0f, 1f, duration)
                    .WithFrequency(2)
                    .BindToPositionY(target.transform)
                    .AddTo(handles);
            }
        }

ShakeMotionAdapters.cs

    public readonly struct FloatShakeMotionAdapter : IMotionAdapter<float, ShakeOptions>
    {
        public float Evaluate(ref float startValue, ref float endValue, ref ShakeOptions options, in MotionEvaluationContext context)
        {
            VibrationHelper.EvaluateStrength(endValue, options.Frequency, options.DampingRatio, context.Progress, out var s);
            // Since there is no "Start" function to initialize the initial random number for this motion,
            // we have to generate random seed here if this is the first time to evaluate the motion.
            // And we should reset this random seed when the motion is finished to have a new random number next time.
            // The another possible approach is having a field as an identifier of the motion, and use it as the initial random seed.
            if (options.RandomNumber == 0f)
            {
                if (options.RandomState.state == 0)
                {
                    options.RandomNumber = SharedRandom.Random.NextFloat(-1f, 1f);
                }
                else
                {
                    options.RandomNumber = options.RandomState.NextFloat(-1f, 1f);
                }
            }
            
            float multipliar = Noise(context.Progress, options.RandomNumber);
            return startValue + s * multipliar;
        }
        
        public static float Noise(float x, float y)
        {
            const float NoiseFrequency = 100f; // just randomly picked number.
            return Mathf.PerlinNoise(x * NoiseFrequency, y) * 2.0f - 1.0f;
        }
    }

I used PerlinNoise for the continuity, so it kept the consistency over time and worked perfectly with the time scale.

The other problem is that now it does not need a RNG during the motion, but a constant initial number generated at the start of the motion to have random results in different instances.
I have not found any suitable place to insert the that kind of "initialization" of the motion.

Or it could be just an identifier of the motion in context, anyways we only need that to generate the noise properly for different instances.

Yes, the random numbers currently used for LitMotion, not just Shake, are dependent on frame updates and need to be changed to calculate a definitive value from Progress and Seed.

However, the change to PerlinNoise may be a bit more difficult. The current behavior of Shake and PerlinNoise is quite different, and the sensory impression that the animation gives is very different. Perhaps a different method should be used for this.

Then, how about setting the sampling interval (or calculating from fps) and getting the value from the closest frame or complementing the value from 2 frames?

However, the change to PerlinNoise may be a bit more difficult. The current behavior of Shake and PerlinNoise is quite different, and the sensory impression that the animation gives is very different. Perhaps a different method should be used for this.

I think the difference can be measured and reproduced by the sampling interval NoiseFrequency.
The value is higher, the frequency of the randomness is closer to the current behaviour of Shake in a short time span.

        public static float Noise(float x, float y)
        {
            const float NoiseFrequency = 100f; // just randomly picked number.
            return Mathf.PerlinNoise(x * NoiseFrequency, y) * 2.0f - 1.0f;
        }

And adding something like floor() function can make it discrete and the update frequency can still be over time instead of frames.

But I agreed that it wouldn't be the same behaviour if Shake was intended to be frame-dependent.
It makes sense to have a different method for this as well.