From 0dd5d9bf3cdd3baa403d82b785c4195804c5d424 Mon Sep 17 00:00:00 2001 From: Benjamin Kraft Date: Fri, 7 Apr 2023 19:39:34 +0200 Subject: [PATCH] AI done --- Assets/Prefabs/Ball.prefab | 3 +- Assets/Scenes/Game.unity | 18 +-- Assets/Scripts/BorderSize.cs | 16 +-- Assets/Scripts/Game/Ball.cs | 22 +++- Assets/Scripts/Game/GameManager.cs | 61 ++++++++-- Assets/Scripts/Game/Player.cs | 177 ++++++++++++++++++++++++----- Pong.sln.DotSettings | 3 +- packages.config | 4 + 8 files changed, 248 insertions(+), 56 deletions(-) create mode 100644 packages.config diff --git a/Assets/Prefabs/Ball.prefab b/Assets/Prefabs/Ball.prefab index ef193d8..046df46 100644 --- a/Assets/Prefabs/Ball.prefab +++ b/Assets/Prefabs/Ball.prefab @@ -102,6 +102,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 267dfd77302043669cc125190ddfb575, type: 3} m_Name: m_EditorClassIdentifier: + rb: {fileID: 0} --- !u!114 &8059693774009316043 MonoBehaviour: m_ObjectHideFlags: 0 @@ -179,7 +180,7 @@ Rigidbody2D: m_Material: {fileID: 0} m_Interpolate: 0 m_SleepingMode: 1 - m_CollisionDetection: 0 + m_CollisionDetection: 1 m_Constraints: 0 --- !u!114 &8029731608843643848 MonoBehaviour: diff --git a/Assets/Scenes/Game.unity b/Assets/Scenes/Game.unity index 18f4a8d..3a0af7c 100644 --- a/Assets/Scenes/Game.unity +++ b/Assets/Scenes/Game.unity @@ -169,10 +169,10 @@ EdgeCollider2D: m_UsedByEffector: 0 m_UsedByComposite: 0 m_Offset: {x: 0, y: 0} - m_EdgeRadius: 0 + m_EdgeRadius: 0.33 m_Points: - - {x: -10, y: -15} - {x: -10, y: 15} + - {x: -10, y: -15} m_AdjacentStartPoint: {x: 0, y: 0} m_AdjacentEndPoint: {x: 0, y: 0} m_UseAdjacentStartPoint: 0 @@ -320,7 +320,7 @@ SpriteRenderer: m_SortingLayer: 0 m_SortingOrder: -1 m_Sprite: {fileID: 7482667652216324306, guid: 311925a002f4447b3a28927169b83ea6, type: 3} - m_Color: {r: 0.22628158, g: 0.28601155, b: 0.4245283, a: 1} + m_Color: {r: 0.20705765, g: 0.2779592, b: 0.4433962, a: 1} m_FlipX: 0 m_FlipY: 0 m_DrawMode: 0 @@ -339,7 +339,7 @@ Transform: m_GameObject: {fileID: 338338487} m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} - m_LocalScale: {x: 20, y: 30, z: 1} + m_LocalScale: {x: 20, y: 30, z: 0} m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} @@ -446,8 +446,8 @@ EdgeCollider2D: m_Offset: {x: 0, y: 0} m_EdgeRadius: 0 m_Points: - - {x: -10, y: 15} - - {x: 10, y: 15} + - {x: -10, y: -15} + - {x: 10, y: -15} m_AdjacentStartPoint: {x: 0, y: 0} m_AdjacentEndPoint: {x: 0, y: 0} m_UseAdjacentStartPoint: 0 @@ -500,8 +500,8 @@ EdgeCollider2D: m_Offset: {x: 0, y: 0} m_EdgeRadius: 0 m_Points: - - {x: 10, y: -15} - {x: 10, y: 15} + - {x: 10, y: -15} m_AdjacentStartPoint: {x: 0, y: 0} m_AdjacentEndPoint: {x: 0, y: 0} m_UseAdjacentStartPoint: 0 @@ -711,8 +711,8 @@ EdgeCollider2D: m_Offset: {x: 0, y: 0} m_EdgeRadius: 0 m_Points: - - {x: -10, y: -15} - - {x: 10, y: -15} + - {x: -10, y: 15} + - {x: 10, y: 15} m_AdjacentStartPoint: {x: 0, y: 0} m_AdjacentEndPoint: {x: 0, y: 0} m_UseAdjacentStartPoint: 0 diff --git a/Assets/Scripts/BorderSize.cs b/Assets/Scripts/BorderSize.cs index 8f573c5..bfe5b72 100644 --- a/Assets/Scripts/BorderSize.cs +++ b/Assets/Scripts/BorderSize.cs @@ -10,7 +10,9 @@ public class BorderSize : MonoBehaviour { public Transform playGround; public static BorderSize Singleton; - + + public Vector2 Size => new(x2 - x1, y2 - y1); + private void OnEnable() { Singleton = this; top = transform.Find("Top").GetComponent(); @@ -20,16 +22,16 @@ public class BorderSize : MonoBehaviour { } private void Update() { - Vector2 v1 = new Vector2(x1, y1); - Vector2 v2 = new Vector2(x2, y1); - Vector2 v3 = new Vector2(x1, y2); - Vector2 v4 = new Vector2(x2, y2); + Vector2 v1 = new Vector2(x1, y2); + Vector2 v2 = new Vector2(x2, y2); + Vector2 v3 = new Vector2(x1, y1); + Vector2 v4 = new Vector2(x2, y1); top.points = new[] {v1, v2}; bottom.points = new[] {v3, v4}; left.points = new[] {v1, v3}; right.points = new[] {v2, v4}; - playGround.localPosition = new Vector3((x2 + x1) / 2, (y2 + y1) / 2, 0); - playGround.localScale = new Vector3(x2 - x1, y2 - y1, 1); + playGround.localPosition = new Vector3((x1 + x2) / 2, (y1 + y2) / 2, 0); + playGround.localScale = Size; } } diff --git a/Assets/Scripts/Game/Ball.cs b/Assets/Scripts/Game/Ball.cs index 91ba51d..d7131a9 100644 --- a/Assets/Scripts/Game/Ball.cs +++ b/Assets/Scripts/Game/Ball.cs @@ -1,8 +1,28 @@ +using System; using Unity.Netcode; using UnityEngine; namespace Game { public class Ball : NetworkBehaviour { - + public Rigidbody2D rb; + + public float Radius => transform.localScale.x / 4; + + public bool IsAlive { + get { + float y = transform.position.y; + float y1 = BorderSize.Singleton.y1; + float y2 = BorderSize.Singleton.y2; + return y > y1 && y < y2; + } + } + + private void OnEnable() { + rb = GetComponent(); + } + + private void Start() { + rb.velocity = new Vector2(0, 20); + } } } diff --git a/Assets/Scripts/Game/GameManager.cs b/Assets/Scripts/Game/GameManager.cs index 4f60d76..9ab1372 100644 --- a/Assets/Scripts/Game/GameManager.cs +++ b/Assets/Scripts/Game/GameManager.cs @@ -1,25 +1,68 @@ using System; +using System.Collections; +using System.Collections.Generic; using Unity.Netcode; using Unity.VisualScripting; using UnityEngine; +using UnityEngine.Assertions; using Object = UnityEngine.Object; namespace Game { public class GameManager : NetworkBehaviour { + public static GameManager Singleton { get; private set; } + + private void OnEnable() { + Singleton = this; + } + public Object ballPrefab; public Object playerPrefab; - + + public List Balls { get; } = new(); + + private static void Tests() { + var v1 = new Vector2(1, 3); + var v2 = new Vector2(3, 2); + var v3 = new Vector2(4, 4); + var v4 = new Vector2(1, 1); + var v5 = new Vector2(4, 1); + var v6 = new Vector2(2, 5); + var v7 = new Vector2(2, 3); + 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 _)); + + 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 _)); + } + private void Start() { //var ball = Instantiate(ballPrefab, Vector2.zero, Quaternion.identity).GetComponent(); - var ball = FindObjectOfType(typeof(Ball)); - var rb = ball.GetComponent(); - rb.velocity = new Vector2(0, 10); - - var p1 = Instantiate(playerPrefab, new Vector2(0, BorderSize.Singleton.y1), Quaternion.identity); - var p2 = Instantiate(playerPrefab, new Vector2(0, BorderSize.Singleton.y2), Quaternion.identity); - p1.AddComponent(); - p2.AddComponent(); + var ball = FindObjectOfType(typeof(Ball)).GetComponent(); + Balls.Add(ball); + + var p1Obj = Instantiate(playerPrefab); + var p2Obj = Instantiate(playerPrefab); + var p1 = p1Obj.AddComponent(); + var p2 = p2Obj.AddComponent(); + p1.Side = Player.ESide.Top; + p2.Side = Player.ESide.Bottom; + p1.Difficulty = AIPlayer.EDifficulty.VeryEasy; + p2.isThisClient = true; + + Tests(); } } diff --git a/Assets/Scripts/Game/Player.cs b/Assets/Scripts/Game/Player.cs index 23f1bc0..b8eaa2d 100644 --- a/Assets/Scripts/Game/Player.cs +++ b/Assets/Scripts/Game/Player.cs @@ -1,34 +1,33 @@ 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; + protected bool goingLeft, goingRight; // Units per second - private float baseSpeed = 10; - + protected float Speed => 10; + // Unit distance from zero - private float baseBorder = 7; + private float Border => 10; private SpeedModification speedModification; private BorderModification borderModification; - private float GetSpeed() { - return baseSpeed; - } - - private float GetBorder() { - return baseBorder; - } - private float LeftSide() { return X() - transform.localScale.x / 2; } @@ -41,45 +40,167 @@ namespace Game { 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 *= baseSpeed * h; + Vector2 trans = new Vector2((goingLeft ? -1 : 0) + (goingRight ? 1 : 0), 0); + trans *= Speed * h; transform.Translate(trans); - Debug.Log(trans.magnitude); - if (LeftSide() < -baseBorder) - transform.Translate(Vector2.right * (-baseBorder - LeftSide())); - if (RightSide() > baseBorder) - transform.Translate(Vector2.left * (RightSide() - baseBorder)); + 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 Difficulty { + public enum EDifficulty { VeryEasy, Easy, Medium, Hard, VeryHard } - private Difficulty difficulty = Difficulty.VeryEasy; + public EDifficulty Difficulty { get; set; } - private float GetTargetPosition() { + // 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 0; + 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 = 1; - GoingLeft = target < X() - h; - GoingRight = target > X() + h; + 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; + goingLeft = keyboard.aKey.isPressed; + goingRight = keyboard.dKey.isPressed; TryMove(Time.fixedDeltaTime); } diff --git a/Pong.sln.DotSettings b/Pong.sln.DotSettings index a53c474..b3ccc1f 100644 --- a/Pong.sln.DotSettings +++ b/Pong.sln.DotSettings @@ -1,4 +1,5 @@  UXML <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> \ No newline at end of file + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /> \ No newline at end of file diff --git a/packages.config b/packages.config new file mode 100644 index 0000000..30be0cd --- /dev/null +++ b/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file