using System; using System.Collections; using Unity.Netcode; using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.PlayerLoop; using Random = UnityEngine.Random; using System.Linq; using UnityEngine.Serialization; namespace Game { public class Player : NetworkBehaviour { public enum ESide {Top, Bottom} public ESide Side { get; set; } private int score; protected bool goingLeft, goingRight; // Units per second protected float Speed => 10; // Unit distance from zero private float Border => 10; 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 X() { return transform.position.x; } protected float Y() { return transform.position.y; } protected void TryMove(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)); } private void Start() { float y = Side switch { ESide.Bottom => BorderSize.Singleton.y1, ESide.Top => BorderSize.Singleton.y2, _ => throw new ArgumentOutOfRangeException() }; transform.position = new Vector2(0, y); } } public class AIPlayer : Player { 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; return Simulate(origin, velocity, radius, FutureSeconds).x; } private void FixedUpdate() { float target = GetTargetPosition(); const float h = 0.5f; goingLeft = target < X() - h; goingRight = target > X() + h; TryMove(Time.fixedDeltaTime); } } public class RealPlayer : Player { public bool isThisClient; private void FixedUpdate() { if (!isThisClient) return; var keyboard = Keyboard.current; goingLeft = keyboard.aKey.isPressed; goingRight = keyboard.dKey.isPressed; TryMove(Time.fixedDeltaTime); } } }