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.
144 lines
4.1 KiB
144 lines
4.1 KiB
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 Difficulty 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);
|
|
}
|
|
}
|
|
} |