1099 lines
34 KiB
C#
1099 lines
34 KiB
C#
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using Mirror;
|
|
using UnityEngine;
|
|
|
|
public class AiBotController : NetworkBehaviour
|
|
{
|
|
public bool isEnabled = false;
|
|
public Team aiTeam;
|
|
public List<Puck> aiPucks;
|
|
[Tooltip("Pause after turn switches before trajectory previews begin.")]
|
|
public float aiTurnDelay = 5f;
|
|
[Tooltip("How long each puck trajectory preview is shown.")]
|
|
public float trajectoryPreviewDuration = 2f;
|
|
[Tooltip("Share of preview duration used to reveal the puck path before the ball path.")]
|
|
[Range(0.1f, 0.9f)]
|
|
public float puckCastDurationRatio = 0.4f;
|
|
[Tooltip("Max simulated seconds per trajectory.")]
|
|
public float trajectorySimMaxTime = 3f;
|
|
[Header("Trajectory visuals")]
|
|
public Color puckTrajectoryColor = new Color(1f, 0.85f, 0.2f, 0.9f);
|
|
public Color ballTrajectoryColor = new Color(0.9f, 0.95f, 1f, 0.9f);
|
|
public float trajectoryLineWidth = 0.08f;
|
|
public int trajectorySortingOrder = 30;
|
|
[Header("Shot scoring weights")]
|
|
public float ballProximityWeight = 1f;
|
|
public float ballBetweenGoalWeight = 1.5f;
|
|
public float trajectoryGoalWeight = 2f;
|
|
public float obstaclePushWeight = 0.75f;
|
|
[Tooltip("Ball within this distance of a blocking puck scores obstacle-push potential.")]
|
|
public float obstacleNearBallDistance = 2f;
|
|
[Tooltip("Any predicted path point within this distance of our own goal is penalized.")]
|
|
public float ownGoalDangerDistance = 3.5f;
|
|
|
|
Coroutine aiTurnCoroutine;
|
|
Coroutine trajectoryPreviewCoroutine;
|
|
LineRenderer puckTrajectoryLine;
|
|
LineRenderer ballTrajectoryLine;
|
|
|
|
readonly List<Vector2> puckPathBuffer = new List<Vector2>();
|
|
readonly List<Vector2> ballPathBuffer = new List<Vector2>();
|
|
|
|
const float SimDt = 1f / 60f;
|
|
const float MaxLaunchDirectionMagnitude = 4f;
|
|
const int MaxPathPoints = 96;
|
|
|
|
void Awake()
|
|
{
|
|
#if UNITY_SERVER
|
|
enabled = false;
|
|
return;
|
|
#else
|
|
EnsureTrajectoryLines();
|
|
#endif
|
|
}
|
|
|
|
void OnEnable()
|
|
{
|
|
GameManager.OnTeamChanged += OnTeamChanged;
|
|
}
|
|
|
|
void OnDisable()
|
|
{
|
|
GameManager.OnTeamChanged -= OnTeamChanged;
|
|
if (aiTurnCoroutine != null)
|
|
{
|
|
StopCoroutine(aiTurnCoroutine);
|
|
aiTurnCoroutine = null;
|
|
}
|
|
if (trajectoryPreviewCoroutine != null)
|
|
{
|
|
StopCoroutine(trajectoryPreviewCoroutine);
|
|
trajectoryPreviewCoroutine = null;
|
|
}
|
|
HideTrajectoryPreview();
|
|
}
|
|
|
|
void Update()
|
|
{
|
|
if (!isEnabled || !isServer)
|
|
return;
|
|
|
|
if (aiPucks == null || aiPucks.Count == 0)
|
|
return;
|
|
|
|
AnalyzePucks();
|
|
}
|
|
|
|
void EnsureTrajectoryLines()
|
|
{
|
|
if (puckTrajectoryLine == null)
|
|
puckTrajectoryLine = CreateTrajectoryLine("PuckTrajectory", puckTrajectoryColor);
|
|
if (ballTrajectoryLine == null)
|
|
ballTrajectoryLine = CreateTrajectoryLine("BallTrajectory", ballTrajectoryColor);
|
|
}
|
|
|
|
LineRenderer CreateTrajectoryLine(string name, Color color)
|
|
{
|
|
Transform existing = transform.Find(name);
|
|
if (existing != null)
|
|
{
|
|
LineRenderer existingLine = existing.GetComponent<LineRenderer>();
|
|
if (existingLine != null)
|
|
existingLine.sortingOrder = trajectorySortingOrder;
|
|
return existingLine;
|
|
}
|
|
|
|
Shader shader = Shader.Find("Sprites/Default");
|
|
if (shader == null)
|
|
return null;
|
|
|
|
GameObject go = new GameObject(name);
|
|
go.transform.SetParent(transform, false);
|
|
|
|
LineRenderer line = go.AddComponent<LineRenderer>();
|
|
line.useWorldSpace = true;
|
|
line.startWidth = trajectoryLineWidth;
|
|
line.endWidth = trajectoryLineWidth;
|
|
line.numCapVertices = 4;
|
|
line.sortingOrder = trajectorySortingOrder;
|
|
line.material = new Material(shader);
|
|
line.startColor = color;
|
|
line.endColor = color;
|
|
line.positionCount = 0;
|
|
line.enabled = false;
|
|
return line;
|
|
}
|
|
|
|
struct PuckShotScore
|
|
{
|
|
public Puck puck;
|
|
public float totalScore;
|
|
public float ballProximityScore;
|
|
public float ballBetweenGoalScore;
|
|
public float trajectoryGoalScore;
|
|
public float obstaclePushScore;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Scores all AI pucks and returns the best shot candidate.
|
|
/// </summary>
|
|
public Puck GetBestPuck()
|
|
{
|
|
PuckShotScore best = default;
|
|
best.totalScore = float.MinValue;
|
|
|
|
foreach (Puck puck in aiPucks)
|
|
{
|
|
if (puck == null)
|
|
continue;
|
|
|
|
PuckShotScore score = ScorePuck(puck);
|
|
if (score.totalScore > best.totalScore)
|
|
best = score;
|
|
}
|
|
|
|
if (best.puck != null)
|
|
{
|
|
Logger.Log(
|
|
$"AI best puck {best.puck.name}: total={best.totalScore:F2} " +
|
|
$"(prox={best.ballProximityScore:F2}, between={best.ballBetweenGoalScore:F2}, " +
|
|
$"traj={best.trajectoryGoalScore:F2}, push={best.obstaclePushScore:F2})");
|
|
}
|
|
|
|
return best.puck;
|
|
}
|
|
|
|
PuckShotScore ScorePuck(Puck puck)
|
|
{
|
|
GameManager gm = GameManager.instance;
|
|
PuckShotScore result = new PuckShotScore { puck = puck };
|
|
|
|
if (gm == null || gm.ball == null || puck == null)
|
|
return result;
|
|
|
|
Vector2 puckPos = puck.transform.position;
|
|
Vector2 ballPos = gm.ball.position;
|
|
Vector2 goalPos = GetAttackGoalPosition();
|
|
Vector2 defendedGoalPos = GetDefendedGoalPosition();
|
|
|
|
float maxPuckBallDist = Mathf.Max(gm.fieldSize * 2f, 1f);
|
|
float puckBallDist = Vector2.Distance(puckPos, ballPos);
|
|
result.ballProximityScore = 1f - Mathf.Clamp01(puckBallDist / maxPuckBallDist);
|
|
|
|
result.ballBetweenGoalScore = ScoreBallBetweenPuckAndGoal(puckPos, ballPos, goalPos);
|
|
result.obstaclePushScore = ScoreObstaclePushPotential(puck, puckPos, ballPos);
|
|
|
|
PredictTrajectory(puck, puckPathBuffer, ballPathBuffer);
|
|
result.trajectoryGoalScore = ScoreTrajectoryTowardGoal(ballPos, goalPos, defendedGoalPos);
|
|
|
|
result.totalScore =
|
|
result.ballProximityScore * ballProximityWeight +
|
|
result.ballBetweenGoalScore * ballBetweenGoalWeight +
|
|
result.trajectoryGoalScore * trajectoryGoalWeight +
|
|
result.obstaclePushScore * obstaclePushWeight;
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Goal this team scores into (e.g. RedGoal is where Red attacks).
|
|
/// </summary>
|
|
Vector2 GetAttackGoalPosition()
|
|
{
|
|
Goal attackGoal = aiTeam == Team.Red ? Goal.RedGoal : Goal.BlueGoal;
|
|
if (attackGoal != null)
|
|
return attackGoal.transform.position;
|
|
|
|
Goal[] goals = FindObjectsByType<Goal>(FindObjectsSortMode.None);
|
|
foreach (Goal goal in goals)
|
|
{
|
|
if (goal != null && goal.team == aiTeam)
|
|
return goal.transform.position;
|
|
}
|
|
|
|
return Vector2.zero;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Own net — the goal the opponent scores into.
|
|
/// </summary>
|
|
Vector2 GetDefendedGoalPosition()
|
|
{
|
|
Goal defendedGoal = aiTeam == Team.Red ? Goal.BlueGoal : Goal.RedGoal;
|
|
if (defendedGoal != null)
|
|
return defendedGoal.transform.position;
|
|
|
|
Goal[] goals = FindObjectsByType<Goal>(FindObjectsSortMode.None);
|
|
foreach (Goal goal in goals)
|
|
{
|
|
if (goal != null && goal.team != aiTeam)
|
|
return goal.transform.position;
|
|
}
|
|
|
|
return Vector2.zero;
|
|
}
|
|
|
|
static float ScoreBallBetweenPuckAndGoal(Vector2 puckPos, Vector2 ballPos, Vector2 goalPos)
|
|
{
|
|
Vector2 puckToGoal = goalPos - puckPos;
|
|
float puckGoalDist = puckToGoal.magnitude;
|
|
if (puckGoalDist < 0.01f)
|
|
return 0f;
|
|
|
|
Vector2 puckToBall = ballPos - puckPos;
|
|
float projection = Vector2.Dot(puckToBall, puckToGoal.normalized);
|
|
if (projection <= 0f)
|
|
return 0f;
|
|
|
|
float ballGoalDist = Vector2.Distance(ballPos, goalPos);
|
|
if (ballGoalDist >= puckGoalDist)
|
|
return 0f;
|
|
|
|
float alongLine = Mathf.Clamp01(projection / puckGoalDist);
|
|
float closerToGoal = 1f - Mathf.Clamp01(ballGoalDist / puckGoalDist);
|
|
return alongLine * closerToGoal;
|
|
}
|
|
|
|
float ScoreObstaclePushPotential(Puck puck, Vector2 puckPos, Vector2 ballPos)
|
|
{
|
|
if (HasDirectLineToBall(puck, puckPos, ballPos))
|
|
return 1f;
|
|
|
|
RaycastHit2D[] hits = Physics2D.LinecastAll(puckPos, ballPos);
|
|
float bestScore = 0f;
|
|
|
|
foreach (RaycastHit2D hit in hits)
|
|
{
|
|
if (hit.collider == null)
|
|
continue;
|
|
if (hit.collider.transform == puck.transform)
|
|
continue;
|
|
if (hit.collider.GetComponent<Ball>() != null)
|
|
continue;
|
|
|
|
float distToBall = Vector2.Distance(hit.point, ballPos);
|
|
float score = 1f - Mathf.Clamp01(distToBall / Mathf.Max(obstacleNearBallDistance, 0.01f));
|
|
if (score > bestScore)
|
|
bestScore = score;
|
|
}
|
|
|
|
return bestScore;
|
|
}
|
|
|
|
float ScoreTrajectoryTowardGoal(Vector2 ballStartPos, Vector2 goalPos, Vector2 defendedGoalPos)
|
|
{
|
|
if (goalPos.sqrMagnitude < 0.0001f)
|
|
return 0f;
|
|
|
|
float attackScore = 0f;
|
|
Vector2 travelDir = Vector2.zero;
|
|
|
|
if (ballPathBuffer.Count >= 2)
|
|
{
|
|
Vector2 end = ballPathBuffer[ballPathBuffer.Count - 1];
|
|
Vector2 prev = ballPathBuffer[ballPathBuffer.Count - 2];
|
|
travelDir = end - prev;
|
|
if (travelDir.sqrMagnitude < 0.0001f)
|
|
return 0f;
|
|
|
|
travelDir.Normalize();
|
|
Vector2 toGoal = (goalPos - end).normalized;
|
|
float directionScore = Mathf.Max(0f, Vector2.Dot(travelDir, toGoal));
|
|
|
|
float startGoalDist = Vector2.Distance(ballStartPos, goalPos);
|
|
float endGoalDist = Vector2.Distance(end, goalPos);
|
|
float progressScore = startGoalDist > 0.01f
|
|
? Mathf.Clamp01(1f - endGoalDist / startGoalDist)
|
|
: 0f;
|
|
|
|
attackScore = directionScore * 0.7f + progressScore * 0.3f;
|
|
}
|
|
else if (puckPathBuffer.Count >= 2)
|
|
{
|
|
Vector2 end = puckPathBuffer[puckPathBuffer.Count - 1];
|
|
Vector2 prev = puckPathBuffer[puckPathBuffer.Count - 2];
|
|
travelDir = end - prev;
|
|
if (travelDir.sqrMagnitude < 0.0001f)
|
|
return 0f;
|
|
|
|
travelDir.Normalize();
|
|
Vector2 toGoal = (goalPos - end).normalized;
|
|
attackScore = Mathf.Max(0f, Vector2.Dot(travelDir, toGoal)) * 0.35f;
|
|
}
|
|
|
|
if (attackScore <= 0f || defendedGoalPos.sqrMagnitude < 0.0001f)
|
|
return attackScore;
|
|
|
|
Vector2 trajectoryEnd = ballPathBuffer.Count >= 2
|
|
? ballPathBuffer[ballPathBuffer.Count - 1]
|
|
: puckPathBuffer[puckPathBuffer.Count - 1];
|
|
Vector2 toOwnGoal = (defendedGoalPos - trajectoryEnd).normalized;
|
|
float ownGoalAlignment = Mathf.Max(0f, Vector2.Dot(travelDir, toOwnGoal));
|
|
float ownGoalProximityDanger = GetTrajectoryOwnGoalProximity(defendedGoalPos);
|
|
|
|
return attackScore * (1f - ownGoalAlignment) * (1f - ownGoalProximityDanger);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns 0-1 danger based on the closest any predicted puck/ball point gets to our own goal.
|
|
/// </summary>
|
|
float GetTrajectoryOwnGoalProximity(Vector2 defendedGoalPos)
|
|
{
|
|
float radius = Mathf.Max(ownGoalDangerDistance, 0.01f);
|
|
float maxDanger = 0f;
|
|
|
|
for (int p = 0; p < puckPathBuffer.Count; p++)
|
|
maxDanger = Mathf.Max(maxDanger, GetPointOwnGoalDanger(puckPathBuffer[p], defendedGoalPos, radius));
|
|
|
|
for (int p = 0; p < ballPathBuffer.Count; p++)
|
|
maxDanger = Mathf.Max(maxDanger, GetPointOwnGoalDanger(ballPathBuffer[p], defendedGoalPos, radius));
|
|
|
|
return maxDanger;
|
|
}
|
|
|
|
static float GetPointOwnGoalDanger(Vector2 point, Vector2 defendedGoalPos, float radius)
|
|
{
|
|
float dist = Vector2.Distance(point, defendedGoalPos);
|
|
if (dist >= radius)
|
|
return 0f;
|
|
|
|
float t = 1f - dist / radius;
|
|
return t * t;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Draws per-puck debug lines using lightweight heuristics (no full trajectory sim).
|
|
/// </summary>
|
|
public Puck AnalyzePucks()
|
|
{
|
|
if (GameManager.instance == null || GameManager.instance.ball == null)
|
|
return null;
|
|
|
|
Vector2 ballPoint = GameManager.instance.ball.position;
|
|
Vector2 goalPos = GetAttackGoalPosition();
|
|
float maxPuckBallDist = Mathf.Max(GameManager.instance.fieldSize * 2f, 1f);
|
|
Puck bestPuck = null;
|
|
float bestScore = float.MinValue;
|
|
|
|
foreach (Puck puck in aiPucks)
|
|
{
|
|
if (puck == null)
|
|
continue;
|
|
|
|
Vector2 puckPoint = puck.transform.position;
|
|
bool canReachBall = HasDirectLineToBall(puck, puckPoint, ballPoint);
|
|
|
|
float proximity = 1f - Mathf.Clamp01(Vector2.Distance(puckPoint, ballPoint) / maxPuckBallDist);
|
|
float between = ScoreBallBetweenPuckAndGoal(puckPoint, ballPoint, goalPos);
|
|
float obstacle = ScoreObstaclePushPotential(puck, puckPoint, ballPoint);
|
|
float quickScore = proximity * ballProximityWeight
|
|
+ between * ballBetweenGoalWeight
|
|
+ obstacle * obstaclePushWeight;
|
|
|
|
Color lineColor = Color.Lerp(Color.red, Color.green, quickScore / Mathf.Max(GetMaxPossibleScore() - trajectoryGoalWeight, 0.01f));
|
|
if (!canReachBall && obstacle > 0.25f)
|
|
lineColor = Color.Lerp(lineColor, Color.cyan, obstacle);
|
|
|
|
Debug.DrawLine(puckPoint, ballPoint, lineColor);
|
|
|
|
if (quickScore > bestScore)
|
|
{
|
|
bestScore = quickScore;
|
|
bestPuck = puck;
|
|
}
|
|
}
|
|
|
|
return bestPuck;
|
|
}
|
|
|
|
float GetMaxPossibleScore()
|
|
{
|
|
return ballProximityWeight + ballBetweenGoalWeight + trajectoryGoalWeight + obstaclePushWeight;
|
|
}
|
|
|
|
Puck GetClosestPuckToBall()
|
|
{
|
|
if (GameManager.instance == null || GameManager.instance.ball == null)
|
|
return null;
|
|
|
|
Vector3 ballPoint = GameManager.instance.ball.position;
|
|
Puck closest = null;
|
|
float closestDistance = float.MaxValue;
|
|
|
|
foreach (Puck puck in aiPucks)
|
|
{
|
|
if (puck == null)
|
|
continue;
|
|
|
|
float dist = Vector2.Distance(puck.transform.position, ballPoint);
|
|
if (dist < closestDistance)
|
|
{
|
|
closestDistance = dist;
|
|
closest = puck;
|
|
}
|
|
}
|
|
|
|
return closest;
|
|
}
|
|
|
|
bool HasDirectLineToBall(Puck puck, Vector3 from, Vector3 to)
|
|
{
|
|
RaycastHit2D[] hits = Physics2D.LinecastAll(from, to);
|
|
foreach (RaycastHit2D hit in hits)
|
|
{
|
|
if (hit.collider == null)
|
|
continue;
|
|
if (hit.collider.transform == puck.transform)
|
|
continue;
|
|
if (hit.collider.GetComponent<Ball>() != null)
|
|
continue;
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void OnTeamChanged(Team team)
|
|
{
|
|
if (!isEnabled || !isServer)
|
|
return;
|
|
if (team != aiTeam)
|
|
return;
|
|
if (GameManager.instance == null || !GameManager.instance.gameStarted)
|
|
return;
|
|
|
|
if (aiTurnCoroutine != null)
|
|
StopCoroutine(aiTurnCoroutine);
|
|
|
|
aiTurnCoroutine = StartCoroutine(TakeAiTurn());
|
|
}
|
|
|
|
IEnumerator TakeAiTurn()
|
|
{
|
|
yield return null;
|
|
|
|
GameManager gm = GameManager.instance;
|
|
while (gm != null && gm.m_isMoving)
|
|
yield return null;
|
|
|
|
if (gm == null || !gm.gameStarted || gm.SelectedTeam != aiTeam)
|
|
yield break;
|
|
|
|
if (aiTurnDelay > 0f)
|
|
yield return new WaitForSeconds(aiTurnDelay);
|
|
|
|
if (gm == null || gm.SelectedTeam != aiTeam || gm.m_isMoving)
|
|
yield break;
|
|
|
|
foreach (Puck previewPuck in aiPucks)
|
|
{
|
|
if (previewPuck == null)
|
|
continue;
|
|
|
|
PredictTrajectory(previewPuck, puckPathBuffer, ballPathBuffer);
|
|
Vector2[] puckPoints = puckPathBuffer.ToArray();
|
|
Vector2[] ballPoints = ballPathBuffer.ToArray();
|
|
|
|
yield return AnimateTrajectoryPreview(puckPoints, ballPoints);
|
|
|
|
if (gm == null || gm.SelectedTeam != aiTeam || gm.m_isMoving)
|
|
{
|
|
HideTrajectoryPreview();
|
|
RpcHideTrajectoryPreview();
|
|
yield break;
|
|
}
|
|
|
|
HideTrajectoryPreview();
|
|
RpcHideTrajectoryPreview();
|
|
}
|
|
|
|
Puck puck = GetBestPuck();
|
|
if (puck == null)
|
|
puck = GetClosestPuckToBall();
|
|
|
|
if (puck == null)
|
|
yield break;
|
|
|
|
GameManager.SelectedPuck = puck;
|
|
Vector2 direction = ComputeLaunchDirection(puck);
|
|
gm.OnPointerUp(direction);
|
|
|
|
aiTurnCoroutine = null;
|
|
}
|
|
|
|
void PredictTrajectory(Puck shootingPuck, List<Vector2> puckPath, List<Vector2> ballPath)
|
|
{
|
|
puckPath.Clear();
|
|
ballPath.Clear();
|
|
|
|
GameManager gm = GameManager.instance;
|
|
if (gm == null || gm.ball == null || shootingPuck == null || shootingPuck.rb == null)
|
|
return;
|
|
|
|
Collider2D puckCollider = shootingPuck.GetComponent<Collider2D>();
|
|
Collider2D ballCollider = gm.ball.GetComponent<Collider2D>();
|
|
if (puckCollider == null || ballCollider == null)
|
|
return;
|
|
|
|
float puckRadius = GetColliderRadius(puckCollider, shootingPuck.transform);
|
|
float ballRadius = GetColliderRadius(ballCollider, gm.ball.transform);
|
|
float puckMass = Mathf.Max(shootingPuck.rb.mass, 0.01f);
|
|
float ballMass = Mathf.Max(gm.ball.mass, 0.01f);
|
|
float puckBounce = GetBounciness(puckCollider);
|
|
float ballBounce = GetBounciness(ballCollider);
|
|
|
|
Vector2 puckPos = shootingPuck.transform.position;
|
|
Vector2 puckVel = GetLaunchVelocity(shootingPuck);
|
|
Vector2 ballPos = gm.ball.position;
|
|
Vector2 ballVel = Vector2.zero;
|
|
bool ballReleased = false;
|
|
|
|
puckPath.Add(puckPos);
|
|
|
|
float elapsed = 0f;
|
|
int step = 0;
|
|
Vector2 ballImpactPos = ballPos;
|
|
Vector2 ballImpactVel = Vector2.zero;
|
|
while (elapsed < trajectorySimMaxTime && puckPath.Count < MaxPathPoints)
|
|
{
|
|
elapsed += SimDt;
|
|
step++;
|
|
|
|
bool puckActive = puckVel.sqrMagnitude > 0.0001f;
|
|
bool ballActive = ballReleased && ballVel.sqrMagnitude > 0.0001f;
|
|
|
|
if (!puckActive && !ballActive)
|
|
break;
|
|
|
|
bool wasBallReleased = ballReleased;
|
|
|
|
if (!ballReleased && puckActive)
|
|
{
|
|
AdvanceShootingPuck(
|
|
ref puckPos,
|
|
ref puckVel,
|
|
puckRadius,
|
|
shootingPuck.rb.linearDamping,
|
|
SimDt,
|
|
puckCollider,
|
|
puckMass,
|
|
puckBounce,
|
|
ref ballPos,
|
|
ref ballVel,
|
|
ref ballReleased,
|
|
ballRadius,
|
|
ballMass,
|
|
ballBounce);
|
|
}
|
|
else
|
|
{
|
|
if (puckActive)
|
|
{
|
|
AdvanceWallsOnly(
|
|
ref puckPos,
|
|
ref puckVel,
|
|
puckRadius,
|
|
shootingPuck.rb.linearDamping,
|
|
SimDt,
|
|
puckCollider,
|
|
puckBounce);
|
|
}
|
|
|
|
if (ballActive)
|
|
{
|
|
AdvanceWallsOnly(
|
|
ref ballPos,
|
|
ref ballVel,
|
|
ballRadius,
|
|
gm.ball.linearDamping,
|
|
SimDt,
|
|
ballCollider,
|
|
ballBounce);
|
|
}
|
|
}
|
|
|
|
if (ballReleased && !wasBallReleased)
|
|
{
|
|
ballImpactPos = ballPos;
|
|
ballImpactVel = ballVel;
|
|
}
|
|
|
|
if (step % 2 == 0)
|
|
puckPath.Add(puckPos);
|
|
}
|
|
|
|
if (ballReleased)
|
|
SimulateBallPathAfterImpact(gm, ballCollider, ballRadius, ballBounce, ballImpactPos, ballImpactVel, ballPath);
|
|
|
|
if (puckPath.Count == 1 && puckVel.sqrMagnitude > 0.0001f)
|
|
puckPath.Add(puckPos + puckVel.normalized * 0.5f);
|
|
}
|
|
|
|
void SimulateBallPathAfterImpact(
|
|
GameManager gm,
|
|
Collider2D ballCollider,
|
|
float ballRadius,
|
|
float ballBounce,
|
|
Vector2 ballPos,
|
|
Vector2 ballVel,
|
|
List<Vector2> ballPath)
|
|
{
|
|
ballPath.Clear();
|
|
AppendBallPathPoint(ballPath, ballPos);
|
|
|
|
float elapsed = 0f;
|
|
while (elapsed < trajectorySimMaxTime && ballPath.Count < MaxPathPoints && ballVel.sqrMagnitude > 0.0001f)
|
|
{
|
|
elapsed += SimDt;
|
|
AdvanceWallsOnly(
|
|
ref ballPos,
|
|
ref ballVel,
|
|
ballRadius,
|
|
gm.ball.linearDamping,
|
|
SimDt,
|
|
ballCollider,
|
|
ballBounce);
|
|
AppendBallPathPoint(ballPath, ballPos);
|
|
}
|
|
|
|
if (ballPath.Count == 1 && ballVel.sqrMagnitude > 0.0001f)
|
|
AppendBallPathPoint(ballPath, ballPos + ballVel.normalized * Mathf.Max(ballVel.magnitude * 0.35f, 0.75f));
|
|
}
|
|
|
|
static void AppendBallPathPoint(List<Vector2> ballPath, Vector2 point)
|
|
{
|
|
if (ballPath.Count > 0 && Vector2.Distance(ballPath[ballPath.Count - 1], point) < 0.05f)
|
|
return;
|
|
|
|
ballPath.Add(point);
|
|
}
|
|
|
|
void AdvanceShootingPuck(
|
|
ref Vector2 pos,
|
|
ref Vector2 vel,
|
|
float radius,
|
|
float damping,
|
|
float dt,
|
|
Collider2D selfCollider,
|
|
float selfMass,
|
|
float selfBounce,
|
|
ref Vector2 ballPos,
|
|
ref Vector2 ballVel,
|
|
ref bool ballReleased,
|
|
float ballRadius,
|
|
float ballMass,
|
|
float ballBounce)
|
|
{
|
|
ApplyDamping(ref vel, damping, dt);
|
|
if (vel.sqrMagnitude < 0.0001f)
|
|
return;
|
|
|
|
Vector2 delta = vel * dt;
|
|
float distance = delta.magnitude;
|
|
Vector2 direction = delta / distance;
|
|
|
|
RaycastHit2D[] hits = Physics2D.CircleCastAll(pos, radius * 0.98f, direction, distance);
|
|
RaycastHit2D? bestHit = null;
|
|
float bestDistance = float.MaxValue;
|
|
|
|
foreach (RaycastHit2D hit in hits)
|
|
{
|
|
if (hit.collider == null || hit.collider.isTrigger)
|
|
continue;
|
|
if (hit.collider == selfCollider)
|
|
continue;
|
|
|
|
if (hit.distance < bestDistance)
|
|
{
|
|
bestDistance = hit.distance;
|
|
bestHit = hit;
|
|
}
|
|
}
|
|
|
|
if (bestHit.HasValue)
|
|
{
|
|
RaycastHit2D hit = bestHit.Value;
|
|
pos = hit.point + hit.normal * radius;
|
|
|
|
Ball ball = hit.collider.GetComponent<Ball>();
|
|
if (ball != null && !ballReleased)
|
|
{
|
|
float restitution = (selfBounce + ballBounce) * 0.5f;
|
|
ResolveCircleCollision(
|
|
ref vel, selfMass,
|
|
ref ballVel, ballMass,
|
|
pos, ballPos,
|
|
restitution);
|
|
ballReleased = true;
|
|
SeparateCircles(ref pos, radius, ref ballPos, ballRadius);
|
|
}
|
|
else
|
|
{
|
|
float bounce = GetBounciness(hit.collider);
|
|
vel = Vector2.Reflect(vel, hit.normal) * bounce;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
pos += delta;
|
|
|
|
if (!ballReleased && Vector2.Distance(pos, ballPos) < radius + ballRadius)
|
|
{
|
|
float restitution = (selfBounce + ballBounce) * 0.5f;
|
|
ResolveCircleCollision(
|
|
ref vel, selfMass,
|
|
ref ballVel, ballMass,
|
|
pos, ballPos,
|
|
restitution);
|
|
ballReleased = true;
|
|
SeparateCircles(ref pos, radius, ref ballPos, ballRadius);
|
|
}
|
|
}
|
|
}
|
|
|
|
void AdvanceWallsOnly(
|
|
ref Vector2 pos,
|
|
ref Vector2 vel,
|
|
float radius,
|
|
float damping,
|
|
float dt,
|
|
Collider2D selfCollider,
|
|
float selfBounce)
|
|
{
|
|
ApplyDamping(ref vel, damping, dt);
|
|
if (vel.sqrMagnitude < 0.0001f)
|
|
return;
|
|
|
|
Vector2 delta = vel * dt;
|
|
float distance = delta.magnitude;
|
|
Vector2 direction = delta / distance;
|
|
|
|
RaycastHit2D[] hits = Physics2D.CircleCastAll(pos, radius * 0.98f, direction, distance);
|
|
RaycastHit2D? bestHit = null;
|
|
float bestDistance = float.MaxValue;
|
|
|
|
foreach (RaycastHit2D hit in hits)
|
|
{
|
|
if (hit.collider == null || hit.collider.isTrigger)
|
|
continue;
|
|
if (hit.collider == selfCollider)
|
|
continue;
|
|
if (hit.collider.GetComponent<Ball>() != null)
|
|
continue;
|
|
|
|
if (hit.distance < bestDistance)
|
|
{
|
|
bestDistance = hit.distance;
|
|
bestHit = hit;
|
|
}
|
|
}
|
|
|
|
if (bestHit.HasValue)
|
|
{
|
|
RaycastHit2D hit = bestHit.Value;
|
|
pos = hit.point + hit.normal * radius;
|
|
float bounce = GetBounciness(hit.collider);
|
|
vel = Vector2.Reflect(vel, hit.normal) * bounce;
|
|
}
|
|
else
|
|
{
|
|
pos += delta;
|
|
}
|
|
}
|
|
|
|
static void ApplyDamping(ref Vector2 vel, float damping, float dt)
|
|
{
|
|
if (damping <= 0f)
|
|
return;
|
|
|
|
vel *= 1f / (1f + damping * dt);
|
|
}
|
|
|
|
static void ResolveCircleCollision(
|
|
ref Vector2 velA, float massA,
|
|
ref Vector2 velB, float massB,
|
|
Vector2 posA, Vector2 posB,
|
|
float restitution)
|
|
{
|
|
Vector2 normal = (posB - posA);
|
|
if (normal.sqrMagnitude < 0.0001f)
|
|
normal = Vector2.up;
|
|
normal.Normalize();
|
|
|
|
float velAlongNormal = Vector2.Dot(velB - velA, normal);
|
|
if (velAlongNormal > 0f)
|
|
return;
|
|
|
|
float impulse = -(1f + restitution) * velAlongNormal / (1f / massA + 1f / massB);
|
|
Vector2 impulseVector = impulse * normal;
|
|
velA -= impulseVector / massA;
|
|
velB += impulseVector / massB;
|
|
}
|
|
|
|
static void SeparateCircles(ref Vector2 posA, float radiusA, ref Vector2 posB, float radiusB)
|
|
{
|
|
Vector2 delta = posB - posA;
|
|
float overlap = radiusA + radiusB - delta.magnitude;
|
|
if (overlap <= 0f)
|
|
return;
|
|
|
|
Vector2 normal = delta.sqrMagnitude > 0.0001f ? delta.normalized : Vector2.up;
|
|
posA -= normal * (overlap * 0.5f);
|
|
posB += normal * (overlap * 0.5f);
|
|
}
|
|
|
|
static float GetColliderRadius(Collider2D collider, Transform t)
|
|
{
|
|
if (collider is CircleCollider2D circle)
|
|
{
|
|
float scale = Mathf.Max(Mathf.Abs(t.lossyScale.x), Mathf.Abs(t.lossyScale.y));
|
|
return circle.radius * scale;
|
|
}
|
|
|
|
return 0.45f;
|
|
}
|
|
|
|
static float GetBounciness(Collider2D collider)
|
|
{
|
|
if (collider != null && collider.sharedMaterial != null)
|
|
return collider.sharedMaterial.bounciness;
|
|
|
|
return 0.8f;
|
|
}
|
|
|
|
Vector2 GetLaunchVelocity(Puck puck)
|
|
{
|
|
GameManager gm = GameManager.instance;
|
|
Vector2 direction = ComputeLaunchDirection(puck);
|
|
float force = gm.puckForce * gm.puckForceCurve.Evaluate(direction.magnitude);
|
|
Vector2 impulse = -direction * force;
|
|
return impulse / Mathf.Max(puck.rb.mass, 0.01f);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Catapult drag direction: opposite of the shot, matching player touch input.
|
|
/// OnPointerUp applies force as -direction.
|
|
/// </summary>
|
|
Vector2 ComputeLaunchDirection(Puck puck)
|
|
{
|
|
Vector2 puckPos = puck.transform.position;
|
|
Vector2 ballPos = GameManager.instance.ball.position;
|
|
Vector2 dragBehind = (puckPos - ballPos).normalized;
|
|
Vector2 dragWorldPoint = puckPos + dragBehind * MaxLaunchDirectionMagnitude;
|
|
Vector2 direction = puck.transform.InverseTransformPoint(dragWorldPoint);
|
|
return Vector2.ClampMagnitude(direction, MaxLaunchDirectionMagnitude);
|
|
}
|
|
|
|
IEnumerator AnimateTrajectoryPreview(Vector2[] puckPoints, Vector2[] ballPoints)
|
|
{
|
|
RpcAnimateTrajectoryPreview(puckPoints, ballPoints);
|
|
yield return new WaitForSeconds(Mathf.Max(trajectoryPreviewDuration, 0.01f));
|
|
}
|
|
|
|
IEnumerator ClientAnimateTrajectoryPreview(Vector2[] puckPoints, Vector2[] ballPoints)
|
|
{
|
|
EnsureTrajectoryLines();
|
|
HideTrajectoryPreview();
|
|
|
|
float duration = Mathf.Max(trajectoryPreviewDuration, 0.01f);
|
|
float puckPhase = Mathf.Clamp(puckCastDurationRatio, 0.1f, 0.9f);
|
|
float elapsed = 0f;
|
|
|
|
while (elapsed < duration)
|
|
{
|
|
elapsed += Time.deltaTime;
|
|
float t = Mathf.Clamp01(elapsed / duration);
|
|
|
|
if (t < puckPhase)
|
|
{
|
|
float puckT = t / puckPhase;
|
|
SetPartialLine(puckTrajectoryLine, puckPoints, puckTrajectoryColor, puckT);
|
|
SetPartialLine(ballTrajectoryLine, ballPoints, ballTrajectoryColor, 0f);
|
|
}
|
|
else
|
|
{
|
|
SetPartialLine(puckTrajectoryLine, puckPoints, puckTrajectoryColor, 1f);
|
|
float ballT = (t - puckPhase) / (1f - puckPhase);
|
|
SetPartialLine(ballTrajectoryLine, ballPoints, ballTrajectoryColor, ballT);
|
|
}
|
|
|
|
yield return null;
|
|
}
|
|
|
|
ApplyTrajectoryPreview(puckPoints, ballPoints);
|
|
trajectoryPreviewCoroutine = null;
|
|
}
|
|
|
|
static void SetPartialLine(LineRenderer line, Vector2[] points, Color color, float progress)
|
|
{
|
|
if (line == null)
|
|
return;
|
|
|
|
if (points == null || points.Length == 0)
|
|
{
|
|
line.positionCount = 0;
|
|
line.enabled = false;
|
|
return;
|
|
}
|
|
|
|
line.enabled = true;
|
|
line.startColor = color;
|
|
line.endColor = color;
|
|
|
|
if (progress <= 0f)
|
|
{
|
|
line.positionCount = 0;
|
|
line.enabled = false;
|
|
return;
|
|
}
|
|
|
|
if (points.Length == 1)
|
|
{
|
|
line.positionCount = 1;
|
|
line.SetPosition(0, new Vector3(points[0].x, points[0].y, 0f));
|
|
return;
|
|
}
|
|
|
|
if (progress >= 1f)
|
|
{
|
|
SetLinePositions(line, points, color);
|
|
return;
|
|
}
|
|
|
|
float totalLength = 0f;
|
|
for (int i = 1; i < points.Length; i++)
|
|
totalLength += Vector2.Distance(points[i - 1], points[i]);
|
|
|
|
if (totalLength < 0.0001f)
|
|
{
|
|
SetLinePositions(line, points, color);
|
|
return;
|
|
}
|
|
|
|
float targetLength = totalLength * progress;
|
|
float accumulated = 0f;
|
|
int partialCount = 1;
|
|
|
|
for (int i = 1; i < points.Length; i++)
|
|
{
|
|
float segmentLength = Vector2.Distance(points[i - 1], points[i]);
|
|
if (accumulated + segmentLength >= targetLength)
|
|
{
|
|
partialCount = i + 1;
|
|
break;
|
|
}
|
|
|
|
partialCount = i + 1;
|
|
accumulated += segmentLength;
|
|
}
|
|
|
|
line.positionCount = partialCount;
|
|
line.SetPosition(0, new Vector3(points[0].x, points[0].y, 0f));
|
|
|
|
accumulated = 0f;
|
|
for (int i = 1; i < partialCount; i++)
|
|
{
|
|
if (i < partialCount - 1)
|
|
{
|
|
line.SetPosition(i, new Vector3(points[i].x, points[i].y, 0f));
|
|
accumulated += Vector2.Distance(points[i - 1], points[i]);
|
|
continue;
|
|
}
|
|
|
|
float segmentLength = Vector2.Distance(points[i - 1], points[i]);
|
|
float remaining = targetLength - accumulated;
|
|
float segmentT = segmentLength > 0.0001f ? remaining / segmentLength : 1f;
|
|
Vector2 end = Vector2.Lerp(points[i - 1], points[i], segmentT);
|
|
line.SetPosition(i, new Vector3(end.x, end.y, 0f));
|
|
}
|
|
}
|
|
|
|
void ApplyTrajectoryPreview(Vector2[] puckPoints, Vector2[] ballPoints)
|
|
{
|
|
EnsureTrajectoryLines();
|
|
SetLinePositions(puckTrajectoryLine, puckPoints, puckTrajectoryColor);
|
|
SetLinePositions(ballTrajectoryLine, ballPoints, ballTrajectoryColor);
|
|
}
|
|
|
|
void HideTrajectoryPreview()
|
|
{
|
|
if (trajectoryPreviewCoroutine != null)
|
|
{
|
|
StopCoroutine(trajectoryPreviewCoroutine);
|
|
trajectoryPreviewCoroutine = null;
|
|
}
|
|
|
|
if (puckTrajectoryLine != null)
|
|
{
|
|
puckTrajectoryLine.positionCount = 0;
|
|
puckTrajectoryLine.enabled = false;
|
|
}
|
|
|
|
if (ballTrajectoryLine != null)
|
|
{
|
|
ballTrajectoryLine.positionCount = 0;
|
|
ballTrajectoryLine.enabled = false;
|
|
}
|
|
}
|
|
|
|
static void SetLinePositions(LineRenderer line, Vector2[] points, Color color)
|
|
{
|
|
if (line == null || points == null || points.Length == 0)
|
|
return;
|
|
|
|
line.enabled = true;
|
|
line.startColor = color;
|
|
line.endColor = color;
|
|
line.positionCount = points.Length;
|
|
for (int i = 0; i < points.Length; i++)
|
|
line.SetPosition(i, new Vector3(points[i].x, points[i].y, 0f));
|
|
}
|
|
|
|
[ClientRpc]
|
|
void RpcAnimateTrajectoryPreview(Vector2[] puckPoints, Vector2[] ballPoints)
|
|
{
|
|
if (trajectoryPreviewCoroutine != null)
|
|
StopCoroutine(trajectoryPreviewCoroutine);
|
|
|
|
trajectoryPreviewCoroutine = StartCoroutine(ClientAnimateTrajectoryPreview(puckPoints, ballPoints));
|
|
}
|
|
|
|
[ClientRpc]
|
|
void RpcShowTrajectoryPreview(Vector2[] puckPoints, Vector2[] ballPoints)
|
|
{
|
|
ApplyTrajectoryPreview(puckPoints, ballPoints);
|
|
}
|
|
|
|
[ClientRpc]
|
|
void RpcHideTrajectoryPreview()
|
|
{
|
|
HideTrajectoryPreview();
|
|
}
|
|
|
|
public void Setup(Team team)
|
|
{
|
|
#if UNITY_SERVER
|
|
return;
|
|
#else
|
|
aiTeam = team;
|
|
RefreshAiPucks();
|
|
isEnabled = true;
|
|
#endif
|
|
}
|
|
|
|
void RefreshAiPucks()
|
|
{
|
|
aiPucks = new List<Puck>();
|
|
|
|
Puck[] pucks = FindObjectsByType<Puck>(FindObjectsSortMode.None);
|
|
foreach (Puck puck in pucks)
|
|
{
|
|
if (puck.team == aiTeam)
|
|
aiPucks.Add(puck);
|
|
}
|
|
}
|
|
}
|