From 51a6d1fceaec1671088e3ce93cb7afcb1a92d87c Mon Sep 17 00:00:00 2001 From: Benjamin Kraft Date: Sat, 8 Apr 2023 21:49:27 +0200 Subject: [PATCH] AI ignores impossible balls --- Assets/Scripts/Game/AIPlayer.cs | 105 +++++++++++++++++++---------- Assets/Scripts/Game/Ball.cs | 2 +- Assets/Scripts/Game/GameManager.cs | 31 +++++---- Assets/Scripts/Game/Player.cs | 23 +++---- 4 files changed, 98 insertions(+), 63 deletions(-) diff --git a/Assets/Scripts/Game/AIPlayer.cs b/Assets/Scripts/Game/AIPlayer.cs index e00cdf5..96ce0fa 100644 --- a/Assets/Scripts/Game/AIPlayer.cs +++ b/Assets/Scripts/Game/AIPlayer.cs @@ -8,13 +8,14 @@ namespace Game { /* * Possible optimizations: - * - Ignore impossible balls * - Try to hit ball with edge */ 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) { @@ -32,21 +33,22 @@ namespace Game { } // o: Origin, e: End - public static bool Intersect(Vector2 o1, Vector2 e1, Vector2 o2, Vector2 e2, out Vector2 point) { + 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; /* - * 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 - */ + * 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) { @@ -61,75 +63,105 @@ namespace Game { 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 Vector2 Simulate(Vector2 origin, Vector2 velocity, float radius, float secondsLeft) { + private float IdlePosition => Difficulty >= Difficulty.Medium ? 0 : X(); + + private bool IsPositionReachableInTime(float futurePosition, float seconds) { + float requiredDistance = Mathf.Abs(futurePosition - X()) - Width / 2; + if (requiredDistance < 0) + return true; + return Speed * seconds > requiredDistance; + } + + private float Simulate(Vector2 origin, Vector2 velocity, float radius, float secondsLeft, float secondsUsed, out bool impossible) { + impossible = false; Vector2 validAreaOrigin = new Vector2(BorderSize.Singleton.x1, BorderSize.Singleton.y1) + new Vector2(radius, radius); Vector2 validAreaSize = BorderSize.Singleton.Size - new Vector2(radius, radius) * 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; + return end.x; float playerY = Side == ESide.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)) - return point; + if (Intersect(origin, end, playerLeft, playerRight, out Vector2 point, out Vector2 rs)) { + secondsUsed += secondsLeft * rs.x; + if (!IsPositionReachableInTime(point.x, secondsUsed)) + impossible = 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)) { + 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)) { + 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 all seconds left + // Any border -> invert x velocity and simulate again from there if (borderHit) { - secondsLeft -= (borderHitPoint - origin).magnitude / Speed; + float secondsUsedHere = borderRs.x * secondsLeft; + secondsLeft -= secondsUsedHere; + secondsUsed += secondsUsedHere; velocity = new Vector2(-velocity.x, velocity.y); origin = borderHitPoint; - return Simulate(origin, velocity, radius, secondsLeft); + return Simulate(origin, velocity, radius, secondsLeft, secondsUsed, out impossible); } // No intersection -> Ball outside of field -> dont simulate further - return origin; + return origin.x; } - private float FutureSeconds => (float) Difficulty * 0.5f; - private float GetTargetPosition() { var balls = GameManager.Singleton.Balls.Where(b => b.IsAlive); var approaching = balls.Where(BallApproaches).ToList(); - - if (approaching.Count == 0) - return Difficulty >= Difficulty.Medium ? 0 : X(); - - // Nearest by Y-Distance - Ball ball = approaching.Aggregate(approaching[0], (prev, current) => YDistanceToBall(current) < YDistanceToBall(prev) ? current : prev); - - Vector2 origin = ball.Rb.position; - Vector2 velocity = ball.Rb.velocity; - float radius = ball.Radius; - Vector2 simulatedPosition = Simulate(origin, velocity, radius, FutureSeconds); + while (approaching.Count > 0) { + + // Nearest by Y-Distance + Ball ball = approaching.Aggregate(approaching[0], (prev, current) => YDistanceToBall(current) < YDistanceToBall(prev) ? current : prev); - return simulatedPosition.x; + Vector2 origin = ball.Rb.position; + Vector2 velocity = ball.Rb.velocity; + float radius = ball.Radius; + + float target = Simulate(origin, velocity, radius, FutureSeconds, 0, out bool impossible); + if (!impossible) + return target; + + approaching.Remove(ball); + // This ball was impossible to catch, try next one + } + + return IdlePosition; + } + + private void ApproachPosition(float pos) { + // Move smoothly, velocity capped by Speed } private void FixedUpdate() { @@ -137,7 +169,10 @@ namespace Game { const float h = 0.5f; goingLeft = target < X() - h; goingRight = target > X() + h; - TryMove(Time.fixedDeltaTime); + if (goingLeft || goingRight) + TryLinearMove(Time.fixedDeltaTime); + else + ApproachPosition(target); } } } \ No newline at end of file diff --git a/Assets/Scripts/Game/Ball.cs b/Assets/Scripts/Game/Ball.cs index 98d7d2b..850e4b6 100644 --- a/Assets/Scripts/Game/Ball.cs +++ b/Assets/Scripts/Game/Ball.cs @@ -27,7 +27,7 @@ namespace Game { } private void Start() { - Rb.velocity = new Vector2(0, 20); + Rb.velocity = new Vector2(0, 25); } } } diff --git a/Assets/Scripts/Game/GameManager.cs b/Assets/Scripts/Game/GameManager.cs index 092047a..0cff2b9 100644 --- a/Assets/Scripts/Game/GameManager.cs +++ b/Assets/Scripts/Game/GameManager.cs @@ -32,25 +32,28 @@ namespace Game { var v8 = new Vector2(-1, 4); var v9 = new Vector2(-2, 1); var v10 = new Vector2(3, -1); - Assert.IsTrue(AIPlayer.Intersect(v8, v4, v1, v9, out _)); - Assert.IsTrue(AIPlayer.Intersect(v1, v2, v4, v7, out _)); - Assert.IsTrue(AIPlayer.Intersect(v10, v6, v9, v2, out _)); - Assert.IsTrue(AIPlayer.Intersect(v9, v5, v8, v10, out _)); - Assert.IsFalse(AIPlayer.Intersect(v8, v4, v6, v5, out _)); - Assert.IsFalse(AIPlayer.Intersect(v3, v5, v6, v8, out _)); - Assert.IsFalse(AIPlayer.Intersect(v10, v4, v8, v9, out _)); - Assert.IsFalse(AIPlayer.Intersect(v1, v7, v3, v2, out _)); + Vector2 p, rs; + Assert.IsTrue(AIPlayer.Intersect(v8, v4, v1, v9, out p, out rs)); + Assert.IsTrue(AIPlayer.Intersect(v1, v2, v4, v7, out p, out rs)); + Assert.IsTrue(AIPlayer.Intersect(v10, v6, v9, v2, out p, out rs)); + Assert.IsTrue(AIPlayer.Intersect(v9, v5, v8, v10, out p, out rs)); + Assert.IsFalse(AIPlayer.Intersect(v8, v4, v6, v5, out p, out rs)); + Assert.IsFalse(AIPlayer.Intersect(v3, v5, v6, v8, out p, out rs)); + Assert.IsFalse(AIPlayer.Intersect(v10, v4, v8, v9, out p, out rs)); + Assert.IsFalse(AIPlayer.Intersect(v1, v7, v3, v2, out p, out rs)); - var v11 = new Vector2(0, 4.8f); - var v12 = new Vector2(0, 14.8f); - var v13 = new Vector2(-9.75f, 14.75f); - var v14 = new Vector2(9.75f, 14.75f); - Assert.IsTrue(AIPlayer.Intersect(v11, v12, v13, v14, out _)); + var v11 = new Vector2(0, 0); + var v12 = new Vector2(0, 5); + var v13 = new Vector2(-2, 2); + var v14 = new Vector2(2, 2); + Assert.IsTrue(AIPlayer.Intersect(v11, v12, v13, v14, out p, out rs)); + Assert.AreApproximatelyEqual(rs.x, 0.4f); + Assert.AreApproximatelyEqual(rs.y, 0.5f); } private void Start() { Settings.Type = Type.Hybrid; - Settings.AIDifficulty = Difficulty.Easy; + Settings.AIDifficulty = Difficulty.VeryHard; var ball = Instantiate(ballPrefab).GetComponent(); Balls.Add(ball); diff --git a/Assets/Scripts/Game/Player.cs b/Assets/Scripts/Game/Player.cs index b14d645..fd9b1f5 100644 --- a/Assets/Scripts/Game/Player.cs +++ b/Assets/Scripts/Game/Player.cs @@ -23,13 +23,10 @@ namespace Game { private SpeedModification speedModification; private BorderModification borderModification; - private float LeftSide() { - return X() - transform.localScale.x / 2; - } - - private float RightSide() { - return X() + transform.localScale.x / 2; - } + protected float Width => transform.localScale.x; + + private float LeftSide => X() - Width / 2; + private float RightSide => X() + Width / 2; protected float X() { return transform.position.x; @@ -39,14 +36,14 @@ namespace Game { return transform.position.y; } - protected void TryMove(float h) { + protected void TryLinearMove(float h) { Vector2 trans = new Vector2((goingLeft ? -1 : 0) + (goingRight ? 1 : 0), 0); trans *= Speed * h; transform.Translate(trans); - if (LeftSide() < -Border) - transform.Translate(Vector2.right * (-Border - LeftSide())); - if (RightSide() > Border) - transform.Translate(Vector2.left * (RightSide() - Border)); + if (LeftSide < -Border) + transform.Translate(Vector2.right * (-Border - LeftSide)); + if (RightSide > Border) + transform.Translate(Vector2.left * (RightSide - Border)); } private void Start() { @@ -70,7 +67,7 @@ namespace Game { goingLeft = keyboard.aKey.isPressed; goingRight = keyboard.dKey.isPressed; - TryMove(Time.fixedDeltaTime); + TryLinearMove(Time.fixedDeltaTime); } } }