main
Benjamin Kraft 1 year ago
parent 1fa6f56408
commit 0dd5d9bf3c
  1. 3
      Assets/Prefabs/Ball.prefab
  2. 18
      Assets/Scenes/Game.unity
  3. 14
      Assets/Scripts/BorderSize.cs
  4. 20
      Assets/Scripts/Game/Ball.cs
  5. 59
      Assets/Scripts/Game/GameManager.cs
  6. 173
      Assets/Scripts/Game/Player.cs
  7. 3
      Pong.sln.DotSettings
  8. 4
      packages.config

@ -102,6 +102,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 267dfd77302043669cc125190ddfb575, type: 3} m_Script: {fileID: 11500000, guid: 267dfd77302043669cc125190ddfb575, type: 3}
m_Name: m_Name:
m_EditorClassIdentifier: m_EditorClassIdentifier:
rb: {fileID: 0}
--- !u!114 &8059693774009316043 --- !u!114 &8059693774009316043
MonoBehaviour: MonoBehaviour:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@ -179,7 +180,7 @@ Rigidbody2D:
m_Material: {fileID: 0} m_Material: {fileID: 0}
m_Interpolate: 0 m_Interpolate: 0
m_SleepingMode: 1 m_SleepingMode: 1
m_CollisionDetection: 0 m_CollisionDetection: 1
m_Constraints: 0 m_Constraints: 0
--- !u!114 &8029731608843643848 --- !u!114 &8029731608843643848
MonoBehaviour: MonoBehaviour:

@ -169,10 +169,10 @@ EdgeCollider2D:
m_UsedByEffector: 0 m_UsedByEffector: 0
m_UsedByComposite: 0 m_UsedByComposite: 0
m_Offset: {x: 0, y: 0} m_Offset: {x: 0, y: 0}
m_EdgeRadius: 0 m_EdgeRadius: 0.33
m_Points: 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_AdjacentStartPoint: {x: 0, y: 0}
m_AdjacentEndPoint: {x: 0, y: 0} m_AdjacentEndPoint: {x: 0, y: 0}
m_UseAdjacentStartPoint: 0 m_UseAdjacentStartPoint: 0
@ -320,7 +320,7 @@ SpriteRenderer:
m_SortingLayer: 0 m_SortingLayer: 0
m_SortingOrder: -1 m_SortingOrder: -1
m_Sprite: {fileID: 7482667652216324306, guid: 311925a002f4447b3a28927169b83ea6, type: 3} 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_FlipX: 0
m_FlipY: 0 m_FlipY: 0
m_DrawMode: 0 m_DrawMode: 0
@ -339,7 +339,7 @@ Transform:
m_GameObject: {fileID: 338338487} m_GameObject: {fileID: 338338487}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0} 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_ConstrainProportionsScale: 0
m_Children: [] m_Children: []
m_Father: {fileID: 0} m_Father: {fileID: 0}
@ -446,8 +446,8 @@ EdgeCollider2D:
m_Offset: {x: 0, y: 0} m_Offset: {x: 0, y: 0}
m_EdgeRadius: 0 m_EdgeRadius: 0
m_Points: 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_AdjacentStartPoint: {x: 0, y: 0}
m_AdjacentEndPoint: {x: 0, y: 0} m_AdjacentEndPoint: {x: 0, y: 0}
m_UseAdjacentStartPoint: 0 m_UseAdjacentStartPoint: 0
@ -500,8 +500,8 @@ EdgeCollider2D:
m_Offset: {x: 0, y: 0} m_Offset: {x: 0, y: 0}
m_EdgeRadius: 0 m_EdgeRadius: 0
m_Points: 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_AdjacentStartPoint: {x: 0, y: 0}
m_AdjacentEndPoint: {x: 0, y: 0} m_AdjacentEndPoint: {x: 0, y: 0}
m_UseAdjacentStartPoint: 0 m_UseAdjacentStartPoint: 0
@ -711,8 +711,8 @@ EdgeCollider2D:
m_Offset: {x: 0, y: 0} m_Offset: {x: 0, y: 0}
m_EdgeRadius: 0 m_EdgeRadius: 0
m_Points: 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_AdjacentStartPoint: {x: 0, y: 0}
m_AdjacentEndPoint: {x: 0, y: 0} m_AdjacentEndPoint: {x: 0, y: 0}
m_UseAdjacentStartPoint: 0 m_UseAdjacentStartPoint: 0

@ -11,6 +11,8 @@ public class BorderSize : MonoBehaviour {
public static BorderSize Singleton; public static BorderSize Singleton;
public Vector2 Size => new(x2 - x1, y2 - y1);
private void OnEnable() { private void OnEnable() {
Singleton = this; Singleton = this;
top = transform.Find("Top").GetComponent<EdgeCollider2D>(); top = transform.Find("Top").GetComponent<EdgeCollider2D>();
@ -20,16 +22,16 @@ public class BorderSize : MonoBehaviour {
} }
private void Update() { private void Update() {
Vector2 v1 = new Vector2(x1, y1); Vector2 v1 = new Vector2(x1, y2);
Vector2 v2 = new Vector2(x2, y1); Vector2 v2 = new Vector2(x2, y2);
Vector2 v3 = new Vector2(x1, y2); Vector2 v3 = new Vector2(x1, y1);
Vector2 v4 = new Vector2(x2, y2); Vector2 v4 = new Vector2(x2, y1);
top.points = new[] {v1, v2}; top.points = new[] {v1, v2};
bottom.points = new[] {v3, v4}; bottom.points = new[] {v3, v4};
left.points = new[] {v1, v3}; left.points = new[] {v1, v3};
right.points = new[] {v2, v4}; right.points = new[] {v2, v4};
playGround.localPosition = new Vector3((x2 + x1) / 2, (y2 + y1) / 2, 0); playGround.localPosition = new Vector3((x1 + x2) / 2, (y1 + y2) / 2, 0);
playGround.localScale = new Vector3(x2 - x1, y2 - y1, 1); playGround.localScale = Size;
} }
} }

@ -1,8 +1,28 @@
using System;
using Unity.Netcode; using Unity.Netcode;
using UnityEngine; using UnityEngine;
namespace Game { namespace Game {
public class Ball : NetworkBehaviour { 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<Rigidbody2D>();
}
private void Start() {
rb.velocity = new Vector2(0, 20);
}
} }
} }

@ -1,25 +1,68 @@
using System; using System;
using System.Collections;
using System.Collections.Generic;
using Unity.Netcode; using Unity.Netcode;
using Unity.VisualScripting; using Unity.VisualScripting;
using UnityEngine; using UnityEngine;
using UnityEngine.Assertions;
using Object = UnityEngine.Object; using Object = UnityEngine.Object;
namespace Game { namespace Game {
public class GameManager : NetworkBehaviour { public class GameManager : NetworkBehaviour {
public static GameManager Singleton { get; private set; }
private void OnEnable() {
Singleton = this;
}
public Object ballPrefab; public Object ballPrefab;
public Object playerPrefab; public Object playerPrefab;
public List<Ball> 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() { private void Start() {
//var ball = Instantiate(ballPrefab, Vector2.zero, Quaternion.identity).GetComponent<Ball>(); //var ball = Instantiate(ballPrefab, Vector2.zero, Quaternion.identity).GetComponent<Ball>();
var ball = FindObjectOfType(typeof(Ball)); var ball = FindObjectOfType(typeof(Ball)).GetComponent<Ball>();
var rb = ball.GetComponent<Rigidbody2D>(); Balls.Add(ball);
rb.velocity = new Vector2(0, 10);
var p1Obj = Instantiate(playerPrefab);
var p1 = Instantiate(playerPrefab, new Vector2(0, BorderSize.Singleton.y1), Quaternion.identity); var p2Obj = Instantiate(playerPrefab);
var p2 = Instantiate(playerPrefab, new Vector2(0, BorderSize.Singleton.y2), Quaternion.identity); var p1 = p1Obj.AddComponent<AIPlayer>();
p1.AddComponent<AIPlayer>(); var p2 = p2Obj.AddComponent<RealPlayer>();
p2.AddComponent<RealPlayer>(); p1.Side = Player.ESide.Top;
p2.Side = Player.ESide.Bottom;
p1.Difficulty = AIPlayer.EDifficulty.VeryEasy;
p2.isThisClient = true;
Tests();
} }
} }

@ -1,34 +1,33 @@
using System; using System;
using System.Collections;
using Unity.Netcode; using Unity.Netcode;
using UnityEngine; using UnityEngine;
using UnityEngine.InputSystem; using UnityEngine.InputSystem;
using UnityEngine.PlayerLoop; using UnityEngine.PlayerLoop;
using Random = UnityEngine.Random; using Random = UnityEngine.Random;
using System.Linq;
using UnityEngine.Serialization;
namespace Game { namespace Game {
public class Player : NetworkBehaviour { public class Player : NetworkBehaviour {
public enum ESide {Top, Bottom}
public ESide Side { get; set; }
private int score; private int score;
protected bool GoingLeft, GoingRight; protected bool goingLeft, goingRight;
// Units per second // Units per second
private float baseSpeed = 10; protected float Speed => 10;
// Unit distance from zero // Unit distance from zero
private float baseBorder = 7; private float Border => 10;
private SpeedModification speedModification; private SpeedModification speedModification;
private BorderModification borderModification; private BorderModification borderModification;
private float GetSpeed() {
return baseSpeed;
}
private float GetBorder() {
return baseBorder;
}
private float LeftSide() { private float LeftSide() {
return X() - transform.localScale.x / 2; return X() - transform.localScale.x / 2;
} }
@ -41,45 +40,167 @@ namespace Game {
return transform.position.x; return transform.position.x;
} }
protected float Y() {
return transform.position.y;
}
protected void TryMove(float h) { protected void TryMove(float h) {
Vector2 trans = new Vector2((GoingLeft ? -1 : 0) + (GoingRight ? 1 : 0), 0); Vector2 trans = new Vector2((goingLeft ? -1 : 0) + (goingRight ? 1 : 0), 0);
trans *= baseSpeed * h; trans *= Speed * h;
transform.Translate(trans); transform.Translate(trans);
Debug.Log(trans.magnitude); if (LeftSide() < -Border)
if (LeftSide() < -baseBorder) transform.Translate(Vector2.right * (-Border - LeftSide()));
transform.Translate(Vector2.right * (-baseBorder - LeftSide())); if (RightSide() > Border)
if (RightSide() > baseBorder) transform.Translate(Vector2.left * (RightSide() - Border));
transform.Translate(Vector2.left * (RightSide() - baseBorder)); }
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 class AIPlayer : Player {
public enum Difficulty { public enum EDifficulty {
VeryEasy, Easy, Medium, Hard, VeryHard VeryEasy, Easy, Medium, Hard, VeryHard
} }
private Difficulty difficulty = Difficulty.VeryEasy; 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() { private float GetTargetPosition() {
var balls = GameManager.Singleton.Balls.Where(b => b.IsAlive);
var approaching = balls.Where(BallApproaches).ToList();
if (approaching.Count == 0)
return X();
return 0; 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() { private void FixedUpdate() {
float target = GetTargetPosition(); float target = GetTargetPosition();
const float h = 1; const float h = 0.5f;
GoingLeft = target < X() - h; goingLeft = target < X() - h;
GoingRight = target > X() + h; goingRight = target > X() + h;
TryMove(Time.fixedDeltaTime); TryMove(Time.fixedDeltaTime);
} }
} }
public class RealPlayer : Player { public class RealPlayer : Player {
public bool isThisClient;
private void FixedUpdate() { private void FixedUpdate() {
if (!isThisClient)
return;
var keyboard = Keyboard.current; var keyboard = Keyboard.current;
GoingLeft = keyboard.aKey.isPressed; goingLeft = keyboard.aKey.isPressed;
GoingRight = keyboard.dKey.isPressed; goingRight = keyboard.dKey.isPressed;
TryMove(Time.fixedDeltaTime); TryMove(Time.fixedDeltaTime);
} }

@ -1,4 +1,5 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=UXML/@EntryIndexedValue">UXML</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=UXML/@EntryIndexedValue">UXML</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateInstanceFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateInstanceFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateStaticFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String></wpf:ResourceDictionary> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateStaticFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PublicFields/@EntryIndexedValue">&lt;Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String></wpf:ResourceDictionary>

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="NUnit" version="3.13.3" targetFramework="net471" />
</packages>
Loading…
Cancel
Save