Closed shangjiaxuan closed 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.
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.
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).
Implemented a C# patch dependent on Lua mod to change steering logic as proposed. https://steamcommunity.com/sharedfiles/filedetails/?id=2956996454
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.
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 noelse
available).The idea is:
M
here is just an estimate, and since update clicks can make it negative, it is taken asabs(x)
, but aclamp(1, infty, x)
should be better (no component). An arbitrary positive number should also work.t
is taken to be0.1s
, as pseudoclk
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
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