parent
af8de5c61e
commit
fc04cbdb46
9 changed files with 187 additions and 156 deletions
@ -0,0 +1,145 @@ |
||||
using System; |
||||
using System.Linq; |
||||
using UnityEngine; |
||||
|
||||
namespace Game { |
||||
public class AIPlayer : Player { |
||||
|
||||
/* |
||||
* Possible optimizations: |
||||
* |
||||
* - Move to center when idle |
||||
* - Ignore impossible balls |
||||
* - Try to hit ball with edge |
||||
*/ |
||||
|
||||
public enum EDifficulty { |
||||
VeryEasy, Easy, Medium, Hard, VeryHard |
||||
} |
||||
|
||||
public EDifficulty Difficulty { get; set; } |
||||
|
||||
// True if ball y velocity points towards player |
||||
private bool BallApproaches(Ball ball) { |
||||
var ballVy = ball.Rb.velocity.y; |
||||
return Side switch { |
||||
ESide.Bottom => ballVy < 0, |
||||
ESide.Top => ballVy > 0, |
||||
_ => throw new Exception("Side not set on player!") |
||||
}; |
||||
} |
||||
|
||||
// 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) { |
||||
point = 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 |
||||
*/ |
||||
|
||||
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; |
||||
return true; |
||||
} |
||||
|
||||
return false; |
||||
} |
||||
|
||||
private Vector2 Simulate(Vector2 origin, Vector2 velocity, float radius, float secondsLeft) { |
||||
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); |
||||
|
||||
Vector2 end = origin + velocity * secondsLeft; |
||||
if (area.Contains(end)) |
||||
return end; |
||||
|
||||
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; |
||||
|
||||
bool borderHit = false; |
||||
Vector2 borderHitPoint = Vector2.zero; |
||||
|
||||
// Left vertical border |
||||
if (Intersect(origin, end, new Vector2(area.xMin, area.yMin), new Vector2(area.xMin, area.yMax), out point)) { |
||||
borderHit = true; |
||||
borderHitPoint = point; |
||||
} |
||||
|
||||
// Right vertical border |
||||
if (Intersect(origin, end, new Vector2(area.xMax, area.yMin), new Vector2(area.xMax, area.yMax), out point)) { |
||||
borderHit = true; |
||||
borderHitPoint = point; |
||||
} |
||||
|
||||
// Any border -> invert x velocity and simulate all seconds left |
||||
if (borderHit) { |
||||
secondsLeft -= (borderHitPoint - origin).magnitude / Speed; |
||||
velocity = new Vector2(-velocity.x, velocity.y); |
||||
origin = borderHitPoint; |
||||
return Simulate(origin, velocity, radius, secondsLeft); |
||||
} |
||||
|
||||
// No intersection -> Ball outside of field -> dont simulate further |
||||
return origin; |
||||
} |
||||
|
||||
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 X(); |
||||
|
||||
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); |
||||
|
||||
return simulatedPosition.x; |
||||
} |
||||
|
||||
private void FixedUpdate() { |
||||
float target = GetTargetPosition(); |
||||
const float h = 0.5f; |
||||
goingLeft = target < X() - h; |
||||
goingRight = target > X() + h; |
||||
TryMove(Time.fixedDeltaTime); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,3 @@ |
||||
fileFormatVersion: 2 |
||||
guid: cfc310c17fca45e5b01c2afa0c290825 |
||||
timeCreated: 1680897588 |
Loading…
Reference in new issue