FakeFishGames / Barotrauma

A 2D online multiplayer game taking place in a submarine travelling through the icy depths of Jupiter's moon Europa.
http://www.barotraumagame.com/
1.77k stars 409 forks source link

Steering breaks on really fast ships (buffed with near 20 captains with talent) #11715

Closed shangjiaxuan closed 1 year ago

shangjiaxuan commented 1 year ago

Disclaimers

What happened?

With lots of captain's bonus to sub engine (like 16+ captains in single player) and high helms skill, the final maximum force can be massive. This makes the current logic that depends on update time and normal speed of subs to "just work" fail. Sub will move back and forth in autopilot "maintain position" mode and cannot steer. Manual movement in current mode will accelerate the ship to 220+ immediately, hit boulder and wreck (if near boulder, the back and forth movement's 220 speed will also wreck sub).

Implemented the conversion with signal components, and the sub stops perfectly in manual steering by putting the vector to center. But since terminal output is clamped to 100, and the game have no real multiplexer components (only memory as latches), going over 100 current speed will still result in sub moving back and forth (if statements ok, but no else available).

The idea is:

Vx = Vx_current
Vx_last = delay(t) Vx_current
Ax = (Vx-Vx_last)/t
M approx= ((delay(t) Fout)/Ax
A_needed = (Vx_out-Vx_current)/t
Fout = M * A_needed (clamped in engine reciever)

M here is just an estimate, and since update clicks can make it negative, it is taken as abs(x), but a clamp(1, infty, x) should be better (no component). An arbitrary positive number should also work. t is taken to be 0.1s, as pseudo clk input.

Looking at code, it seems the steering component looks for a target velocity, but the output linked to engine sets force, which is acceleration. This works for slow acceleration, since the direction is always correct and updates are frequent compared to actual velocity change (using something similar to a smoothstep function), but not for things with a HUGE force multipler.

Reproduction steps

  1. Start sub editor and load a sub with good power and engine.
  2. Spawn 16 captains and unlock talents.
  3. Set a high helm skill level for user player.
  4. Move around in autopilot mode and observe oscillation.

Bug prevalence

Happens regularly

Version

v1.0.8.0

-

No response

Which operating system did you encounter this bug on?

Windows

Relevant error messages and crash reports

No error message, since game didn't crash, just wrecking and hitting wall frames.
Regalis11 commented 1 year ago

Thank you for the report!

I'm not entirely sure I understand the issue completely, but to me it sounds like it's working as intended. The velocity_x_out is not supposed to be the same as the force - an output of 100 simply means "full speed ahead", and the actual force/acceleration depends on the engines.

However, being able to achieve enormous acceleration by stacking the captains' talents is an issue in itself that we need to fix.

shangjiaxuan commented 1 year ago

Checking the code and I found that I was wrong in assuming the speed calculated is optimal. Patching Steering.GetSteeringVelocity to be like this:

List<Engine> engines = controlledSub.GetItems(false).Select(p => p.GetComponent<Engine>()).Where(p => p != null).ToList();

float max_force_x = 0;
engines.ForEach(p => max_force_x += GetMaxForce(p));
float max_accl_x = max_force_x/ controlledSub.PhysicsBody.Mass;

Vector2 accl_y_range = buoyancyaccl_range;

float time_expected = slowdownAmount * 0.3f + 2.0f;

float resolved_accl_x = GetCurrentAcceleration(-max_accl_x, max_accl_x, currentVelocity.X, distance.X, time_expected);

float resolved_accl_y = GetCurrentAcceleration(accl_y_range.X, accl_y_range.Y, currentVelocity.Y, distance.Y, time_expected);

float max_y = accl_y_range.Y;

float normalized_accl_x = resolved_accl_x / max_accl_x;
float normalized_accl_y = resolved_accl_y / max_y;

return new Vector2(normalized_accl_x * 100f, normalized_accl_y * 100f);

Where GetCurrentAcceleration is:

private static float GetCurrentAcceleration(float accl_max_neg, float accl_max_pos, float currentV, float displacement, float no_accl_time)
{
    // moving towards destination
    if (currentV * displacement >= 0)
    {
        float two_a_s;
        if (displacement > 0) {
            two_a_s = -2 * accl_max_neg * displacement;
        }
        else {
            two_a_s = -2 * accl_max_pos * displacement;
        }
        // maximum acceleration cannot go to destination and stop
        if (currentV * currentV > two_a_s)
        {
            if (displacement > 0)
            {
                return accl_max_neg;
            }
            else
            {
                return accl_max_pos;
            }
        }
        // do not need max acceleration to stop.
        else {
            if (MathF.Abs(displacement) <= MathF.Abs(0.5f * currentV * no_accl_time))
            {
                return -0.5f * currentV * currentV / displacement;
            }
            else if (displacement >= 0 && displacement + 0.5 * accl_max_neg * no_accl_time * no_accl_time + currentV * no_accl_time <= 0 && currentV + accl_max_neg * no_accl_time > 0) {
                return -0.5f * currentV * currentV / displacement;
            }
            else if (displacement <= 0 && displacement + 0.5 * accl_max_pos * no_accl_time * no_accl_time + currentV * no_accl_time >= 0 && currentV + accl_max_pos * no_accl_time < 0) {
                return -0.5f * currentV * currentV / displacement;
            }
            // accelerate for now
            else
            {
                if (displacement > 0)
                {
                    return accl_max_pos;
                }
                else
                {
                    return accl_max_neg;
                }
            }
        }
    }
    else
    {
        if (MathF.Abs(displacement) < MathF.Abs(0.5f * currentV * no_accl_time))
        {
            return -2 * (displacement + currentV * no_accl_time) / (no_accl_time * no_accl_time);
        }
        else if (displacement <= 0 && (displacement + 0.5f * accl_max_neg * no_accl_time * no_accl_time + currentV * no_accl_time) >= 0)
        {
            return  - 2 * (displacement + currentV * no_accl_time) / (no_accl_time * no_accl_time);
        }
        else if (displacement >= 0 && (displacement + 0.5f * accl_max_pos * no_accl_time * no_accl_time + currentV * no_accl_time) <= 0)
        {
            return -2 * (displacement + currentV * no_accl_time) / (no_accl_time * no_accl_time);
        }

        if (currentV > 0)
        {
            return accl_max_neg;
        }
        else {
            return accl_max_pos;
        }
    }
}

GetMaxForce (adapted from engine's force calculation):

float GetMaxForce(Engine instance)
{
    float voltageFactor = instance.MinVoltage <= 0.0f ? 1.0f : Math.Min(instance.Voltage, 2.0f);
    float currForce = 100 * voltageFactor;
    float condition = instance.Item.MaxCondition <= 0.0f ? 0.0f : instance.Item.Condition / instance.Item.MaxCondition;
    float forceMultiplier = 0.1f;
    if (instance.User != null)
    {
        forceMultiplier *= MathHelper.Lerp(0.5f, 2.0f, (float)Math.Sqrt(instance.User.GetSkillLevel("helm") / 100));
    }
    currForce *= instance.Item.StatManager.GetAdjustedValue(ItemTalentStats.EngineMaxSpeed, instance.MaxForce) * forceMultiplier;
    if (instance.Item.GetComponent<Repairable>() is { IsTinkering: true } repairable)
    {
        currForce *= 1f + repairable.TinkeringStrength * 1.5f;
    }

    currForce = instance.Item.StatManager.GetAdjustedValue(ItemTalentStats.EngineSpeed, currForce);

    //less effective when in a bad condition
    currForce *= MathHelper.Lerp(0.5f, 2.0f, condition);
    return currForce;
}

Bouyancy, as adapted from SubmarineBody.CalculateBoyancy

public static readonly Vector2 buoyancyfactor_range =  new Vector2(-0.5f, SubmarineBody.NeutralBallastPercentage * 2.0f);

public static readonly Vector2 buoyancyaccl_range = buoyancyfactor_range * 10.0f;

This calculation did not take drag into account, but it seems not needed to be usable.

Manual steering still doesn't stop very easily with the velocity_{i}_out now defined as acceleration percentage.

But still, this way can make the ship don't use too much acceleration when really near target.

Regalis11 commented 1 year ago

Thank you for taking the time to write such thorough examples! However, I don't think this is how the steering logic should work. Going forwards at 100% simply means the sub goes full speed ahead, -20% means it reverses at 20% power and so on. Making it this sort of "smart" system that can automatically adjust the submarine's acceleration to a certain level, taking the forces of the engines, the mass of the sub and the current velocity into account would in my opinion make navigating the submarine less challenging (and less interesting).

shangjiaxuan commented 1 year ago

Implemented a C# patch dependent on Lua mod to change steering logic as proposed. https://steamcommunity.com/sharedfiles/filedetails/?id=2956996454

3e849f2e5c commented 1 year ago

Fixed in https://github.com/Regalis11/Barotrauma-development/commit/b793e51e5c0ee28e0a86f63ba11c1ecb45d14167

Regalis11 commented 1 year ago

Tested, seems like the fix is working correctly. I noticed one related issue though: the description on "Helmsman" is incorrect, it gives a bonus of 10%, not 20% like the description says.

Regalis11 commented 1 year ago

Fixed in https://github.com/Regalis11/Barotrauma-development/commit/dd6dfcd