main
Benjamin Kraft 1 year ago
parent af8de5c61e
commit fc04cbdb46
  1. 4
      Assets/Prefabs/Ball.prefab
  2. 23
      Assets/Prefabs/Player.prefab
  3. 2
      Assets/Scenes/Game.unity
  4. 145
      Assets/Scripts/Game/AIPlayer.cs
  5. 3
      Assets/Scripts/Game/AIPlayer.cs.meta
  6. 13
      Assets/Scripts/Game/Ball.cs
  7. 8
      Assets/Scripts/Game/GameManager.cs
  8. 143
      Assets/Scripts/Game/Player.cs
  9. 2
      Assets/Sprites/Circle.png.meta

@ -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

@ -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

@ -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}

@ -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);
}
}
}

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: cfc310c17fca45e5b01c2afa0c290825
timeCreated: 1680897588

@ -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<Rigidbody2D>();
Rb = GetComponent<Rigidbody2D>();
Collider = GetComponent<CircleCollider2D>();
}
private void Start() {
rb.velocity = new Vector2(0, 20);
Rb.velocity = new Vector2(0, 20);
}
}
}

@ -52,15 +52,17 @@ namespace Game {
//var ball = Instantiate(ballPrefab, Vector2.zero, Quaternion.identity).GetComponent<Ball>();
var ball = FindObjectOfType(typeof(Ball)).GetComponent<Ball>();
Balls.Add(ball);
ball.Radius = 0.5f;
var p1Obj = Instantiate(playerPrefab);
var p2Obj = Instantiate(playerPrefab);
var p1 = p1Obj.AddComponent<AIPlayer>();
var p2 = p2Obj.AddComponent<RealPlayer>();
var p2 = p2Obj.AddComponent<AIPlayer>();
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();
}

@ -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;

@ -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

Loading…
Cancel
Save