AI ignores impossible balls

main
Benjamin Kraft 1 year ago
parent b9c07db9d0
commit 51a6d1fcea
  1. 105
      Assets/Scripts/Game/AIPlayer.cs
  2. 2
      Assets/Scripts/Game/Ball.cs
  3. 31
      Assets/Scripts/Game/GameManager.cs
  4. 23
      Assets/Scripts/Game/Player.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);
}
}
}

@ -27,7 +27,7 @@ namespace Game {
}
private void Start() {
Rb.velocity = new Vector2(0, 20);
Rb.velocity = new Vector2(0, 25);
}
}
}

@ -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<Ball>();
Balls.Add(ball);

@ -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);
}
}
}

Loading…
Cancel
Save