using System; using System.Linq; using UnityEngine; namespace Game { public class AIPlayer : Player { public Difficulty Difficulty { get; set; } private float FutureSeconds => (float) Difficulty * 0.5f; // 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 manhattan, only Y direction 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; Vector2 m1 = e1 - o1; Vector2 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; } private float IdlePosition => Difficulty >= Difficulty.Medium ? 0 : X; // 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 angle) { float requiredDistance = Mathf.Abs(futurePosition - X) - Width / 2; 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; Vector2 r = new Vector2(radius, radius); Vector2 validAreaOrigin = new Vector2(Dimensions.Singleton.left, Dimensions.Singleton.boardBottom) + r; Vector2 validAreaSize = new Vector2(Dimensions.Singleton.Width, Dimensions.Singleton.boardTop * 2) - r * 2; Rect area = new Rect(validAreaOrigin, validAreaSize); // Try to follow this line from origin -> end Vector2 end = origin + velocity * secondsLeft; // Line ends in playground if (area.Contains(end)) return end.x; float playerY = Side == Side.Bottom ? area.yMin : area.yMax; Vector2 playerLeft = new Vector2(area.xMin, playerY); Vector2 playerRight = new Vector2(area.xMax, playerY); // Horizontal line (player line) -> stop simulating if (Intersect(origin, end, playerLeft, playerRight, out Vector2 point, out Vector2 rs)) { secondsUsed += secondsLeft * rs.x; if (!IsPositionReachableInTime(point.x, secondsUsed, Vector2.Angle(velocity, transform.up))) ignore = true; return point.x; } bool borderHit = false; Vector2 borderHitPoint = Vector2.zero; Vector2 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) { float 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 DistortAmount => 1 - 1 / ((float) Difficulty + 1); private float Distort(float target) { float max = Width / 3; float 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 Ball ball = approaching.Aggregate(approaching[0], (prev, current) => YDistanceToBall(current) < YDistanceToBall(prev) ? current : prev); float target = Simulate(ball.Rb.position, ball.Rb.velocity, ball.Radius, FutureSeconds, 0, out bool 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 float currentSmoothV; private const float SmoothTime = 0.2f; private void ApproachPosition(float pos) { float result = Mathf.SmoothDamp(X, pos, ref currentSmoothV, SmoothTime, Speed); transform.position = new Vector2(result, Y); ClampInsideBorders(); } private bool isApproaching; private float lastDirection; private void FixedUpdate() { float dt = Time.fixedDeltaTime; float target = GetTargetPosition(); float h = Mathf.Max(Speed * dt, Width / 2); goingLeft = target < X - h; goingRight = target > X + h; if (goingLeft || goingRight) { isApproaching = false; lastDirection = goingLeft ? -1 : 1; TryLinearMove(dt); } else { if (!isApproaching) { isApproaching = true; currentSmoothV = Speed * lastDirection; } ApproachPosition(target); } } } }