using System.Linq; using UnityEngine; using UnityEngine.UIElements; namespace Game { public class AIPlayer : Player { private const float SmoothTime = 0.1f; private float currentSmoothV; private bool isApproaching; private float lastDirection; private bool lastGoingLeft, lastGoingRight; private Label leftButton, rightButton; public Difficulty Difficulty { get; set; } private float FutureSeconds => GameManager.BalanceValues.aiFutureSecondsBase + (float) Difficulty * GameManager.BalanceValues.aiFutureSecondsPerDifficulty; private float IdlePosition => Difficulty >= GameManager.BalanceValues.aiSmartIdleMinDifficulty ? 0 : X; private float DistortAmount => 1 - 1 / ((float) Difficulty + 1); protected new void Start() { base.Start(); leftButton = GameUI.Singleton.GetGoButton(Side, "left"); rightButton = GameUI.Singleton.GetGoButton(Side, "right"); } private void FixedUpdate() { var dt = Time.fixedDeltaTime; var target = GetTargetPosition(); var h = Mathf.Max(Speed * dt, Width / 2); goingLeftLinear = target < X - h; goingRightLinear = target > X + h; if (goingLeftLinear || goingRightLinear) { isApproaching = false; lastDirection = goingLeftLinear ? -1 : 1; TryLinearMove(dt); } else { if (!isApproaching) { isApproaching = true; currentSmoothV = Speed * lastDirection; } ApproachPosition(target); } bool goingLeft = isApproaching ? currentSmoothV < 0 : goingLeftLinear; bool goingRight = isApproaching ? currentSmoothV > 0 : goingRightLinear; switch (lastGoingLeft) { case false when goingLeft: break; case true when !goingLeft: break; } switch (lastGoingRight) { case false when goingRight: break; case true when !goingRight: break; } lastGoingLeft = goingLeft; lastGoingRight = goingRight; } // True if ball y velocity points towards player private bool BallApproaches(Ball ball) { var ballVy = ball.Rb.velocity.y; return ballVy * transform.up.y < 0; } // Not euclid, but only y distance private float YDistanceToBall(Ball ball) { return Mathf.Abs(ball.Rb.position.y - Y); } // o: Origin, e: End public static bool Intersect(Vector2 o1, Vector2 e1, Vector2 o2, Vector2 e2, out Vector2 point, out Vector2 rs) { point = Vector2.zero; rs = Vector2.zero; var m1 = e1 - o1; var m2 = e2 - o2; /* * * o1 + r * m1 = 02 + s * m2 * * o1x + r * m1x = o2x + s * m2x * o1y + r * m1y = o2y + s * m2y * * Solve for r and s: * Formulas below achieved by reordering * Order is irrelevant, but if m1x == 0 then first approach results in division by zero -> * Invert order then, and if division by zero is still a problem, both lines are parallel anyway */ float r, s; if (m1.x != 0) { s = (o1.y + o2.x * m1.y / m1.x - o1.x * m1.y / m1.x - o2.y) / (m2.y - m2.x * m1.y / m1.x); r = (o2.x + s * m2.x - o1.x) / m1.x; } else if (m2.x != 0) { r = (o2.y + o1.x * m2.y / m2.x - o2.x * m2.y / m2.x - o1.y) / (m1.y - m1.x * m2.y / m2.x); s = (o1.x + r * m1.x - o2.x) / m2.x; } else { return false; } if (s is > 0 and < 1 && r is > 0 and < 1) { point = o1 + r * m1; rs = new Vector2(r, s); return true; } return false; } // TODO Also must include fact that players have a height, maybe check the incoming angle // angle: 0 -> perpendicular, from -90 to 90 private bool IsPositionReachableInTime(float futurePosition, float seconds, float radius, float angle) { var requiredDistance = Mathf.Abs(futurePosition - X) - Width / 2 - radius; if (requiredDistance < 0) return true; if (Mathf.Abs(futurePosition) > Border) return false; return Speed * seconds > requiredDistance; } private float Simulate(Vector2 origin, Vector2 velocity, float radius, float secondsLeft, float secondsUsed, out bool ignore) { ignore = false; var r = new Vector2(radius, radius); var validAreaOrigin = -Dimensions.Singleton.PlaySizeBoards / 2 + r; var validAreaSize = Dimensions.Singleton.PlaySizeBoards - r * 2; var area = new Rect(validAreaOrigin, validAreaSize); // Try to follow this line from origin -> end var end = origin + velocity * secondsLeft; // Line ends in playground if (area.Contains(end)) return end.x; var playerY = Side == Side.Bottom ? area.yMin : area.yMax; var playerLeft = new Vector2(area.xMin, playerY); var playerRight = new Vector2(area.xMax, playerY); // Horizontal line (player line) -> stop simulating if (Intersect(origin, end, playerLeft, playerRight, out var point, out var rs)) { secondsUsed += secondsLeft * rs.x; if (!IsPositionReachableInTime(point.x, secondsUsed, radius, Vector2.Angle(velocity, transform.up))) ignore = true; return point.x; } var borderHit = false; var borderHitPoint = Vector2.zero; var borderRs = Vector2.zero; // Left vertical border if (Intersect(origin, end, new Vector2(area.xMin, area.yMin), new Vector2(area.xMin, area.yMax), out point, out rs)) { borderHit = true; borderHitPoint = point; borderRs = rs; } // Right vertical border if (Intersect(origin, end, new Vector2(area.xMax, area.yMin), new Vector2(area.xMax, area.yMax), out point, out rs)) { borderHit = true; borderHitPoint = point; borderRs = rs; } // Any border -> invert x velocity and simulate again from there if (borderHit) { var secondsUsedHere = borderRs.x * secondsLeft; secondsLeft -= secondsUsedHere; secondsUsed += secondsUsedHere; velocity = new Vector2(-velocity.x, velocity.y); origin = borderHitPoint; return Simulate(origin, velocity, radius, secondsLeft, secondsUsed, out ignore); } // No intersection -> Ball outside of field -> ignore ignore = true; return 0; } private float Distort(float target) { var max = Width / 3; var distortionOffset = DistortAmount * max; distortionOffset *= target > X ? -1 : 1; return target + distortionOffset; } private float GetTargetPosition() { var approaching = GameManager.Singleton.Balls.Where(BallApproaches).ToList(); while (approaching.Count > 0) { // Nearest by Y-Distance var ball = approaching.Aggregate(approaching[0], (prev, current) => YDistanceToBall(current) < YDistanceToBall(prev) ? current : prev); var target = Simulate(ball.Rb.position, ball.Rb.velocity, ball.Radius, FutureSeconds, 0, out var ignore); if (!ignore) return Distort(target); approaching.Remove(ball); // This ball was marked "ignore" by simulation for unknown reasons, try next one } return IdlePosition; } private void ApproachPosition(float pos) { var result = Mathf.SmoothDamp(X, pos, ref currentSmoothV, SmoothTime, Speed); transform.position = new Vector2(result, Y); ClampInsideBorders(); } } }