You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

196 lines
6.0 KiB

2 years ago
using System;
using System.Linq;
using UnityEngine;
namespace Game {
2 years ago
public class AIPlayer : Player {
public Difficulty Difficulty { get; set; }
private float FutureSeconds => (float) Difficulty * 0.5f;
2 years ago
// True if ball y velocity points towards player
private bool BallApproaches(Ball ball) {
var ballVy = ball.Rb.velocity.y;
2 years ago
return ballVy * transform.up.y < 0;
2 years ago
}
// Not manhattan, only Y direction
private float YDistanceToBall(Ball ball) {
2 years ago
return Mathf.Abs(ball.Rb.position.y - Y);
2 years ago
}
// o: Origin, e: End
public static bool Intersect(Vector2 o1, Vector2 e1, Vector2 o2, Vector2 e2, out Vector2 point, out Vector2 rs) {
2 years ago
point = Vector2.zero;
rs = Vector2.zero;
2 years ago
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
*/
2 years ago
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);
2 years ago
return true;
}
return false;
}
2 years ago
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) {
2 years ago
float requiredDistance = Mathf.Abs(futurePosition - X) - Width / 2;
if (requiredDistance < 0)
return true;
if (Mathf.Abs(futurePosition) > Border)
return false;
return Speed * seconds > requiredDistance;
}
2 years ago
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;
2 years ago
Rect area = new Rect(validAreaOrigin, validAreaSize);
// Try to follow this line from origin -> end
2 years ago
Vector2 end = origin + velocity * secondsLeft;
// Line ends in playground
2 years ago
if (area.Contains(end))
return end.x;
2 years ago
2 years ago
float playerY = Side == Side.Bottom ? area.yMin : area.yMax;
2 years ago
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)))
2 years ago
ignore = true;
return point.x;
}
2 years ago
bool borderHit = false;
Vector2 borderHitPoint = Vector2.zero;
Vector2 borderRs = Vector2.zero;
2 years ago
// Left vertical border
if (Intersect(origin, end, new Vector2(area.xMin, area.yMin), new Vector2(area.xMin, area.yMax), out point, out rs)) {
2 years ago
borderHit = true;
borderHitPoint = point;
borderRs = rs;
2 years ago
}
// Right vertical border
if (Intersect(origin, end, new Vector2(area.xMax, area.yMin), new Vector2(area.xMax, area.yMax), out point, out rs)) {
2 years ago
borderHit = true;
borderHitPoint = point;
borderRs = rs;
2 years ago
}
// Any border -> invert x velocity and simulate again from there
2 years ago
if (borderHit) {
float secondsUsedHere = borderRs.x * secondsLeft;
secondsLeft -= secondsUsedHere;
secondsUsed += secondsUsedHere;
2 years ago
velocity = new Vector2(-velocity.x, velocity.y);
origin = borderHitPoint;
2 years ago
return Simulate(origin, velocity, radius, secondsLeft, secondsUsed, out ignore);
2 years ago
}
2 years ago
// 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;
2 years ago
}
private float GetTargetPosition() {
var approaching = GameManager.Singleton.Balls.Where(BallApproaches).ToList();
2 years ago
while (approaching.Count > 0) {
// Nearest by Y-Distance
Ball ball = approaching.Aggregate(approaching[0], (prev, current) => YDistanceToBall(current) < YDistanceToBall(prev) ? current : prev);
2 years ago
float target = Simulate(ball.Rb.position, ball.Rb.velocity, ball.Radius, FutureSeconds, 0, out bool ignore);
if (!ignore)
return Distort(target);
approaching.Remove(ball);
2 years ago
// 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);
2 years ago
transform.position = new Vector2(result, Y);
ClampInsideBorders();
2 years ago
}
private bool isApproaching;
private float lastDirection;
2 years ago
private void FixedUpdate() {
2 years ago
float dt = Time.fixedDeltaTime;
2 years ago
float target = GetTargetPosition();
2 years ago
float h = Mathf.Max(Speed * dt, Width / 2);
2 years ago
goingLeft = target < X - h;
goingRight = target > X + h;
if (goingLeft || goingRight) {
isApproaching = false;
lastDirection = goingLeft ? -1 : 1;
2 years ago
TryLinearMove(dt);
}
else {
if (!isApproaching) {
isApproaching = true;
currentSmoothV = Speed * lastDirection;
}
ApproachPosition(target);
}
2 years ago
}
}
}