diff --git a/Assets/Prefabs/Ball.prefab b/Assets/Prefabs/Ball.prefab index 046df46..49f9c0a 100644 --- a/Assets/Prefabs/Ball.prefab +++ b/Assets/Prefabs/Ball.prefab @@ -160,7 +160,7 @@ CircleCollider2D: m_UsedByComposite: 0 m_Offset: {x: 0, y: 0} serializedVersion: 2 - m_Radius: 0.25 + m_Radius: 0.5 --- !u!50 &7286884547159090166 Rigidbody2D: serializedVersion: 4 @@ -173,7 +173,7 @@ Rigidbody2D: m_Simulated: 1 m_UseFullKinematicContacts: 0 m_UseAutoMass: 1 - m_Mass: 0.19634955 + m_Mass: 0.7853982 m_LinearDrag: 0 m_AngularDrag: 0.05 m_GravityScale: 0 diff --git a/Assets/Prefabs/Player.prefab b/Assets/Prefabs/Player.prefab index 213cdcb..9946bcd 100644 --- a/Assets/Prefabs/Player.prefab +++ b/Assets/Prefabs/Player.prefab @@ -13,6 +13,7 @@ GameObject: - component: {fileID: 5402279313309450412} - component: {fileID: 1666507220592599477} - component: {fileID: 1287955657} + - component: {fileID: 6551590900950860653} m_Layer: 0 m_Name: Player m_TagString: Untagged @@ -29,7 +30,7 @@ Transform: m_GameObject: {fileID: 5402279313309450415} m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} - m_LocalScale: {x: 4, y: 1, z: 1} + m_LocalScale: {x: 4, y: 4, z: 4} m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} @@ -76,7 +77,7 @@ SpriteRenderer: m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 - m_Sprite: {fileID: -2413806693520163455, guid: a86470a33a6bf42c4b3595704624658b, type: 3} + m_Sprite: {fileID: 21300000, guid: 47e353a78c92b9838963e533e37462e5, type: 3} m_Color: {r: 1, g: 0.54467595, b: 0, a: 1} m_FlipX: 0 m_FlipY: 0 @@ -120,7 +121,7 @@ PolygonCollider2D: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 5402279313309450415} - m_Enabled: 1 + m_Enabled: 0 m_Density: 1 m_Material: {fileID: 6200000, guid: 2d231bbc8208f52c797c91aa2030f60f, type: 2} m_IsTrigger: 0 @@ -191,3 +192,19 @@ MonoBehaviour: AlwaysReplicateAsRoot: 0 DontDestroyWithOwner: 0 AutoObjectParentSync: 1 +--- !u!58 &6551590900950860653 +CircleCollider2D: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5402279313309450415} + m_Enabled: 1 + m_Density: 1 + m_Material: {fileID: 6200000, guid: 2d231bbc8208f52c797c91aa2030f60f, type: 2} + m_IsTrigger: 0 + m_UsedByEffector: 0 + m_UsedByComposite: 0 + m_Offset: {x: 0, y: 0} + serializedVersion: 2 + m_Radius: 0.5 diff --git a/Assets/Scenes/Game.unity b/Assets/Scenes/Game.unity index 3a0af7c..da7078a 100644 --- a/Assets/Scenes/Game.unity +++ b/Assets/Scenes/Game.unity @@ -169,7 +169,7 @@ EdgeCollider2D: m_UsedByEffector: 0 m_UsedByComposite: 0 m_Offset: {x: 0, y: 0} - m_EdgeRadius: 0.33 + m_EdgeRadius: 0 m_Points: - {x: -10, y: 15} - {x: -10, y: -15} diff --git a/Assets/Scripts/Game/AIPlayer.cs b/Assets/Scripts/Game/AIPlayer.cs new file mode 100644 index 0000000..63bbe1a --- /dev/null +++ b/Assets/Scripts/Game/AIPlayer.cs @@ -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); + } + } +} \ No newline at end of file diff --git a/Assets/Scripts/Game/AIPlayer.cs.meta b/Assets/Scripts/Game/AIPlayer.cs.meta new file mode 100644 index 0000000..6847868 --- /dev/null +++ b/Assets/Scripts/Game/AIPlayer.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: cfc310c17fca45e5b01c2afa0c290825 +timeCreated: 1680897588 \ No newline at end of file diff --git a/Assets/Scripts/Game/Ball.cs b/Assets/Scripts/Game/Ball.cs index d7131a9..98d7d2b 100644 --- a/Assets/Scripts/Game/Ball.cs +++ b/Assets/Scripts/Game/Ball.cs @@ -4,9 +4,13 @@ using UnityEngine; namespace Game { public class Ball : NetworkBehaviour { - public Rigidbody2D rb; + public Rigidbody2D Rb { get; private set; } - public float Radius => transform.localScale.x / 4; + private CircleCollider2D Collider { get; set; } + public float Radius { + get => transform.localScale.x * Collider.radius; + set => transform.localScale = new Vector3(1, 1, 1) * value * 2; + } public bool IsAlive { get { @@ -18,11 +22,12 @@ namespace Game { } private void OnEnable() { - rb = GetComponent(); + Rb = GetComponent(); + Collider = GetComponent(); } private void Start() { - rb.velocity = new Vector2(0, 20); + Rb.velocity = new Vector2(0, 20); } } } diff --git a/Assets/Scripts/Game/GameManager.cs b/Assets/Scripts/Game/GameManager.cs index 9ab1372..fb70e18 100644 --- a/Assets/Scripts/Game/GameManager.cs +++ b/Assets/Scripts/Game/GameManager.cs @@ -52,15 +52,17 @@ namespace Game { //var ball = Instantiate(ballPrefab, Vector2.zero, Quaternion.identity).GetComponent(); var ball = FindObjectOfType(typeof(Ball)).GetComponent(); Balls.Add(ball); + ball.Radius = 0.5f; var p1Obj = Instantiate(playerPrefab); var p2Obj = Instantiate(playerPrefab); var p1 = p1Obj.AddComponent(); - var p2 = p2Obj.AddComponent(); + var p2 = p2Obj.AddComponent(); p1.Side = Player.ESide.Top; p2.Side = Player.ESide.Bottom; - p1.Difficulty = AIPlayer.EDifficulty.VeryEasy; - p2.isThisClient = true; + p1.Difficulty = AIPlayer.EDifficulty.VeryHard; + p2.Difficulty = AIPlayer.EDifficulty.VeryHard; + // p2.isThisClient = true; Tests(); } diff --git a/Assets/Scripts/Game/Player.cs b/Assets/Scripts/Game/Player.cs index 3cca02e..b14d645 100644 --- a/Assets/Scripts/Game/Player.cs +++ b/Assets/Scripts/Game/Player.cs @@ -1,12 +1,7 @@ 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 { @@ -63,143 +58,7 @@ namespace Game { transform.position = new Vector2(0, y); } } - - 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; - 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; diff --git a/Assets/Sprites/Circle.png.meta b/Assets/Sprites/Circle.png.meta index 3f46520..81e1e21 100644 --- a/Assets/Sprites/Circle.png.meta +++ b/Assets/Sprites/Circle.png.meta @@ -50,7 +50,7 @@ TextureImporter: spriteMeshType: 1 alignment: 0 spritePivot: {x: 0.5, y: 0.5} - spritePixelsToUnits: 512 + spritePixelsToUnits: 256 spriteBorder: {x: 0, y: 0, z: 0, w: 0} spriteGenerateFallbackPhysicsShape: 1 alphaUsage: 1