- Moved video screen into its own separate movie tent. - Adjusted stable post-processing volume. - Chickens are now at full volume. - Added button to toggle chickens off and on.
1063 lines
38 KiB
C#
1063 lines
38 KiB
C#
using UdonSharp;
|
|
using UnityEngine;
|
|
using VRC.Udon.Common.Enums;
|
|
|
|
// ReSharper disable InconsistentNaming
|
|
|
|
// ReSharper disable CheckNamespace
|
|
|
|
namespace VRCBilliardsCE.Packages.com.vrcbilliards.vrcbce.Runtime.Scripts
|
|
{
|
|
public partial class PoolStateManager : DebuggableUdon
|
|
{
|
|
#if UNITY_ANDROID
|
|
public const float MAX_SIMULATION_TIME_PER_FRAME = 0.05f; // max time to process per frame on quest (~4)
|
|
#else
|
|
public float MAX_SIMULATION_TIME_PER_FRAME = 0.1f; // max time to process per frame on pc (~8)
|
|
#endif
|
|
public float TIME_PER_STEP = 0.0125f; // time step in seconds per iteration
|
|
private float BALL_DIAMETER_SQUARED_MINUS_EPSILON;
|
|
private float ballRadius;
|
|
private float ONE_OVER_BALL_RADIUS;
|
|
public float EARTH_GRAVITY = 9.80665f; // Earth's gravitational acceleration
|
|
private float BALL_DIAMETER_SQUARED;
|
|
private float BALL_RADIUS_SQUARED;
|
|
public float MASS_OF_BALL = 0.165f; // Weight of ball in kg
|
|
private float POCKET_INNER_RADIUS_SQUARED;
|
|
public Vector3 CONTACT_POINT = new Vector3(0.0f, -0.03f, 0.0f);
|
|
|
|
// Cue input tracking
|
|
private const float SIN_A = 0.28078832987f;
|
|
private const float COS_A = 0.95976971915f;
|
|
private const float F = 1.72909790282f;
|
|
|
|
private float accumulatedTime;
|
|
|
|
private float tableWidthMinusHeight;
|
|
private Vector3 vA;
|
|
private Vector3 vB;
|
|
private Vector3 vC;
|
|
private Vector3 vD;
|
|
private Vector3 vX;
|
|
private Vector3 vY;
|
|
private Vector3 vZ;
|
|
private Vector3 vW;
|
|
private Vector3 pK;
|
|
private Vector3 pL;
|
|
private Vector3 pN;
|
|
private Vector3 pO;
|
|
private Vector3 pP;
|
|
private Vector3 pQ;
|
|
private Vector3 pR;
|
|
private Vector3 pT;
|
|
private Vector3 pS;
|
|
private Vector3 vAvD;
|
|
private Vector3 vAvDNormal;
|
|
private Vector3 vBvY;
|
|
private Vector3 vBvYNormal;
|
|
private Vector3 vCvZNormal;
|
|
private Vector3 vAvBNormal = new Vector3(0.0f, 0.0f, -1.0f);
|
|
private Vector3 vCvWNormal = new Vector3(-1.0f, 0.0f, 0.0f);
|
|
private Vector3 signPos = new Vector3(0.0f, 1.0f, 0.0f);
|
|
|
|
private bool enableVerboseLogging = false;
|
|
private bool isCueOutOfBounds;
|
|
private bool IsCueInPlay => !isCueOutOfBounds && !ballsArePocketed[0];
|
|
|
|
private void CalculateTableCollisionConstants()
|
|
{
|
|
BALL_DIAMETER_SQUARED_MINUS_EPSILON = Mathf.Pow(ballDiameter, 2) - 0.0002f;
|
|
ballRadius = ballDiameter / 2;
|
|
ONE_OVER_BALL_RADIUS = 1 / (ballDiameter / 2);
|
|
BALL_DIAMETER_SQUARED = Mathf.Pow(ballDiameter, 2);
|
|
BALL_RADIUS_SQUARED = Mathf.Pow(ballDiameter / 2, 2);
|
|
|
|
POCKET_INNER_RADIUS_SQUARED = Mathf.Pow(pocketInnerRadius, 2);
|
|
|
|
tableWidthMinusHeight = tableWidth - tableHeight;
|
|
// Major source vertices
|
|
vA.x = pocketOuterRadius * 0.92f;
|
|
vA.z = tableHeight;
|
|
|
|
vB.x = tableWidth - pocketOuterRadius;
|
|
vB.z = tableHeight;
|
|
|
|
vC.x = tableWidth;
|
|
vC.z = tableHeight - pocketOuterRadius;
|
|
|
|
vD.x = vA.x - 0.016f;
|
|
vD.z = vA.z + 0.060f;
|
|
|
|
// Aux points
|
|
vX = vD + Vector3.forward;
|
|
vW = vC;
|
|
vW.z = 0.0f;
|
|
|
|
vY = vB;
|
|
vY.x += 1.0f;
|
|
vY.z += 1.0f;
|
|
|
|
vZ = vC;
|
|
vZ.x += 1.0f;
|
|
vZ.z += 1.0f;
|
|
|
|
// Normals
|
|
vAvD = vD - vA;
|
|
vAvD = vAvD.normalized;
|
|
vAvDNormal.x = -vAvD.z;
|
|
vAvDNormal.z = vAvD.x;
|
|
|
|
vBvY = vB - vY;
|
|
vBvY = vBvY.normalized;
|
|
vBvYNormal.x = -vBvY.z;
|
|
vBvYNormal.z = vBvY.x;
|
|
|
|
vCvZNormal = -vBvYNormal;
|
|
|
|
// Minkowski difference
|
|
pN = vA;
|
|
pN.z -= ballRadius;
|
|
|
|
pL = vD + vAvDNormal * ballRadius;
|
|
|
|
pK = vD;
|
|
pK.x -= ballRadius;
|
|
|
|
pO = vB;
|
|
pO.z -= ballRadius;
|
|
pP = vB + vBvYNormal * ballRadius;
|
|
pQ = vC + vCvZNormal * ballRadius;
|
|
|
|
pR = vC;
|
|
pR.x -= ballRadius;
|
|
|
|
pT = vX;
|
|
pT.x -= ballRadius;
|
|
|
|
pS = vW;
|
|
pS.x -= ballRadius;
|
|
|
|
pS = vW;
|
|
pS.x -= ballRadius;
|
|
}
|
|
|
|
// Update loop-scoped handler for cue-locked functionality (warming up and hitting the ball).
|
|
// Non-pure. Returns a Vector3 as it can modify the exact position the cue tip is at.
|
|
private Vector3 AimAndHitCueBall(Vector3 copyOfLocalSpacePositionOfCueTip, Vector3 cueballPosition)
|
|
{
|
|
float sweepTimeBall = Vector3.Dot(cueballPosition - localSpacePositionOfCueTipLastFrame,
|
|
cueLocalForwardDirection);
|
|
|
|
// Check for potential skips due to low frame rate
|
|
if (sweepTimeBall > 0.0f && sweepTimeBall <
|
|
(localSpacePositionOfCueTipLastFrame - copyOfLocalSpacePositionOfCueTip).magnitude)
|
|
{
|
|
copyOfLocalSpacePositionOfCueTip =
|
|
localSpacePositionOfCueTipLastFrame + cueLocalForwardDirection * sweepTimeBall;
|
|
}
|
|
|
|
// Hit condition is when cuetip is gone inside ball
|
|
if ((copyOfLocalSpacePositionOfCueTip - cueballPosition).sqrMagnitude < BALL_RADIUS_SQUARED)
|
|
{
|
|
Vector3 force = copyOfLocalSpacePositionOfCueTip - localSpacePositionOfCueTipLastFrame;
|
|
|
|
HitBallWithCue(cueTip.transform.forward, Mathf.Min(force.magnitude / Time.fixedDeltaTime, 999.0f));
|
|
}
|
|
|
|
return copyOfLocalSpacePositionOfCueTip;
|
|
}
|
|
|
|
private const float CUE_MASS = 0.5f; // kg
|
|
private const float m_e = 0.02f;
|
|
|
|
private void HitBallWithCue(Vector3 shotDirection, float velocity)
|
|
{
|
|
//shotDirection = tableSurface.rotation * shotDirection;
|
|
|
|
Vector3 q = tableSurface.InverseTransformDirection(shotDirection); // direction of cue in surface space
|
|
Vector3 o = ballTransforms[0].localPosition; // location of ball in surface
|
|
|
|
Vector3 up = tableSurface.up;
|
|
|
|
Vector3 j = -Vector3.ProjectOnPlane(q, up); // project cue direction onto table surface, gives us j
|
|
Vector3 i = Vector3.Cross(j, up);
|
|
|
|
Plane jkPlane = new Plane(i, o);
|
|
|
|
Debug.DrawLine(o, o + i, Color.red, 15f);
|
|
|
|
Vector3 Q = raySphereOutput; // point of impact in surface space
|
|
# if UNITY_EDITOR
|
|
var worldCueHitPoint = tableSurface.position + Q;
|
|
|
|
Debug.DrawLine(worldCueHitPoint, worldCueHitPoint + shotDirection, Color.magenta, 15f);
|
|
# endif
|
|
var a = jkPlane.GetDistanceToPoint(Q);
|
|
var b = Q.y - o.y;
|
|
var c = Mathf.Sqrt(Mathf.Max(0, Mathf.Pow(ballRadius, 2) - Mathf.Pow(a, 2) - Mathf.Pow(b, 2)));
|
|
|
|
var adj = Mathf.Sqrt(Mathf.Pow(q.x, 2) + Mathf.Pow(q.z, 2));
|
|
var opp = q.y;
|
|
var theta = -Mathf.Atan(opp / adj);
|
|
|
|
var cosTheta = Mathf.Cos(theta);
|
|
var sinTheta = Mathf.Sin(theta);
|
|
|
|
var f = 2 * MASS_OF_BALL * velocity / (1 + MASS_OF_BALL / CUE_MASS + 5 / (2 * ballRadius) *
|
|
(Mathf.Pow(a, 2) + Mathf.Pow(b, 2) * Mathf.Pow(cosTheta, 2) + Mathf.Pow(c, 2) * Mathf.Pow(sinTheta, 2) -
|
|
2 * b * c * cosTheta * sinTheta));
|
|
|
|
var I = 2f / 5f * MASS_OF_BALL * Mathf.Pow(ballRadius, 2);
|
|
Vector3 v = new Vector3(0, -f / MASS_OF_BALL * cosTheta, -f / MASS_OF_BALL * sinTheta);
|
|
Vector3 w = 1 / I * new Vector3(-c * f * sinTheta + b * f * cosTheta, a * f * sinTheta, -a * f * cosTheta);
|
|
|
|
// the paper is inconsistent here. either w.x is inverted (i.e. the i axis points right instead of left) or b is inverted (which means F is wrong too)
|
|
// for my sanity I'm going to assume the former
|
|
w.x = -w.x;
|
|
|
|
// https://billiards.colostate.edu/physics_articles/Alciatore_pool_physics_article.pdf
|
|
var alpha = -Mathf.Atan(
|
|
(5f / 2f * a / ballRadius * Mathf.Sqrt(Mathf.Max(0, 1f - Mathf.Pow(a / ballRadius, 2)))) /
|
|
(1 + MASS_OF_BALL / m_e + 5f / 2f * (1f - Mathf.Pow(a / ballRadius, 2)))
|
|
) * 180 / Mathf.PI;
|
|
|
|
// rewrite to the axis we expect
|
|
v = new Vector3(-v.x, v.z, -v.y);
|
|
w = new Vector3(w.x, -w.z, w.y);
|
|
|
|
if (v.y > 0)
|
|
{
|
|
// no scooping
|
|
v.y = 0;
|
|
}
|
|
else if (v.y < 0)
|
|
{
|
|
// the ball must not be under the cue after one time step
|
|
var k_MIN_HORIZONTAL_VEL = (ballRadius - c) / TIME_PER_STEP;
|
|
|
|
if (v.z < k_MIN_HORIZONTAL_VEL)
|
|
{
|
|
// not enough strength to be a jump shot
|
|
v.y = 0;
|
|
}
|
|
else
|
|
{
|
|
// dampen y velocity because the table will eat a lot of energy (we're driving the ball straight into it)
|
|
v.y = -v.y * 0.35f;
|
|
}
|
|
}
|
|
|
|
// translate
|
|
Quaternion r = Quaternion.FromToRotation(Vector3.back, j);
|
|
v = r * v;
|
|
w = r * w;
|
|
|
|
// apply squirt
|
|
v = Quaternion.AngleAxis(alpha, tableSurface.up) * v;
|
|
|
|
// done
|
|
currentBallVelocities[0] = v;
|
|
currentAngularVelocities[0] = w;
|
|
|
|
HandleCueBallHit();
|
|
}
|
|
|
|
// TODO: This is a single-use function we can refactor. Note that its use is to equate a bool,
|
|
// so it's more acceptable to hold on to.
|
|
private bool IsIntersectingWithSphere(Vector3 start, Vector3 dir, Vector3 sphere)
|
|
{
|
|
Vector3 nrm = dir.normalized;
|
|
Vector3 h = sphere - start;
|
|
float lf = Vector3.Dot(nrm, h);
|
|
float s = BALL_RADIUS_SQUARED - Vector3.Dot(h, h) + (lf * lf);
|
|
|
|
if (s < 0.0f)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
s = Mathf.Sqrt(s);
|
|
|
|
if (lf < s)
|
|
{
|
|
if (lf + s >= 0)
|
|
{
|
|
s = -s;
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
raySphereOutput = start + (nrm * (lf - s));
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Is cue touching another ball?
|
|
/// </summary>
|
|
private bool IsCueContacting()
|
|
{
|
|
switch(gameMode) {
|
|
case GameMode.EightBall:
|
|
// Check all
|
|
for (int i = 1; i < NUMBER_OF_SIMULATED_BALLS; i++)
|
|
{
|
|
if (ballsArePocketed[i])
|
|
continue;
|
|
|
|
if ((currentBallPositions[0] - currentBallPositions[i]).sqrMagnitude < BALL_DIAMETER_SQUARED)
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
case GameMode.NineBall:
|
|
// Only check to 9 ball
|
|
for (int i = 1; i <= 9; i++)
|
|
{
|
|
if (ballsArePocketed[i])
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if ((currentBallPositions[0] - currentBallPositions[i]).sqrMagnitude < BALL_DIAMETER_SQUARED)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
case GameMode.KoreanCarom:
|
|
case GameMode.JapaneseCarom:
|
|
if ((currentBallPositions[0] - currentBallPositions[9]).sqrMagnitude < BALL_DIAMETER_SQUARED)
|
|
return true;
|
|
|
|
if ((currentBallPositions[0] - currentBallPositions[2]).sqrMagnitude < BALL_DIAMETER_SQUARED)
|
|
return true;
|
|
|
|
if ((currentBallPositions[0] - currentBallPositions[3]).sqrMagnitude < BALL_DIAMETER_SQUARED)
|
|
return true;
|
|
|
|
return false;
|
|
case GameMode.ThreeCushionCarom:
|
|
if ((currentBallPositions[0] - currentBallPositions[9]).sqrMagnitude < BALL_DIAMETER_SQUARED)
|
|
return true;
|
|
|
|
if ((currentBallPositions[0] - currentBallPositions[2]).sqrMagnitude < BALL_DIAMETER_SQUARED)
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private void AdvancePhysicsStep()
|
|
{
|
|
var ballsMoving = false;
|
|
|
|
// Cue angular velocity
|
|
var moved = new bool[NUMBER_OF_SIMULATED_BALLS];
|
|
|
|
if (IsCueInPlay)
|
|
{
|
|
# if UNITY_EDITOR
|
|
var ballPos = tableSurface.position + currentBallPositions[0];
|
|
# endif
|
|
|
|
if (currentBallPositions[0].y < 0)
|
|
{
|
|
currentBallPositions[0].y = -currentBallPositions[0].y * 0.35f; // bounce with restitution
|
|
currentBallVelocities[0].y = -currentBallVelocities[0].y * 0.35f;
|
|
|
|
// Nullify small bounces
|
|
if (currentBallVelocities[0].y < 0.0025f)
|
|
{
|
|
currentBallPositions[0].y = 0;
|
|
currentBallVelocities[0].y = 0;
|
|
}
|
|
}
|
|
|
|
// Apply movement
|
|
var deltaPos = CalculateCueBallDeltaPosition();
|
|
currentBallPositions[0] += deltaPos;
|
|
moved[0] = deltaPos != Vector3.zero;
|
|
|
|
ballsMoving |= StepOneBall(0, moved);
|
|
|
|
# if UNITY_EDITOR
|
|
Debug.DrawLine(ballPos, tableSurface.position + currentBallPositions[0], Color.cyan, 15);
|
|
# endif
|
|
}
|
|
|
|
// Run main simulation / inter-ball collision
|
|
|
|
for (var i = 1; i < ballsArePocketed.Length; i++)
|
|
{
|
|
if (ballsArePocketed[i])
|
|
continue;
|
|
|
|
currentBallVelocities[i].y = 0;
|
|
currentBallPositions[i].y = 0;
|
|
|
|
var deltaPos = currentBallVelocities[i] * TIME_PER_STEP;
|
|
currentBallPositions[i] += deltaPos;
|
|
moved[i] = deltaPos != Vector3.zero;
|
|
|
|
ballsMoving |= StepOneBall(i, moved);
|
|
}
|
|
|
|
// Check if simulation has settled
|
|
if (!ballsMoving && turnIsRunning && !_preventEndOfTurn)
|
|
{
|
|
turnIsRunning = false;
|
|
|
|
// Make sure we only run this from the client who initiated the move
|
|
if (isSimulatedByUs)
|
|
HandleEndOfTurn();
|
|
|
|
// Check if there was a network update on hold
|
|
if (!isUpdateLocked)
|
|
return;
|
|
|
|
isUpdateLocked = false;
|
|
|
|
ReadNetworkData();
|
|
|
|
return;
|
|
}
|
|
|
|
if (
|
|
IsCueInPlay &&
|
|
currentBallPositions[0].y > ballRadius &&
|
|
(Mathf.Abs(currentBallPositions[0].x) > tableWidth + 0.0001f ||
|
|
Mathf.Abs(currentBallPositions[0].z) > tableHeight + 0.0001f)
|
|
)
|
|
{
|
|
HandleCueBallOffTable();
|
|
}
|
|
|
|
switch (gameMode) {
|
|
case GameMode.EightBall:
|
|
case GameMode.NineBall:
|
|
if (moved[0] && IsCueInPlay)
|
|
CalculatePoolBallPhysics(0);
|
|
|
|
// Run edge collision
|
|
for (var i = 1; i < ballsArePocketed.Length; i++)
|
|
{
|
|
if (moved[i] && !ballsArePocketed[i])
|
|
CalculatePoolBallPhysics(i);
|
|
}
|
|
|
|
if (moved[0] && IsCueInPlay)
|
|
CheckIfBallsArePocketed(0);
|
|
|
|
// Run triggers
|
|
for (var i = 1; i < ballsArePocketed.Length; i++)
|
|
{
|
|
if (moved[i] && !ballsArePocketed[i])
|
|
CheckIfBallsArePocketed(i);
|
|
}
|
|
|
|
break;
|
|
case GameMode.KoreanCarom:
|
|
case GameMode.JapaneseCarom:
|
|
if (moved[0] && IsCueInPlay)
|
|
CalculateCaromBallPhysics(0);
|
|
if (moved[9])
|
|
CalculateCaromBallPhysics(9);
|
|
if (moved[2])
|
|
CalculateCaromBallPhysics(2);
|
|
if (moved[3])
|
|
CalculateCaromBallPhysics(3);
|
|
|
|
break;
|
|
case GameMode.ThreeCushionCarom:
|
|
if (moved[0] && IsCueInPlay)
|
|
CalculateCaromBallPhysics(0);
|
|
if (moved[9])
|
|
CalculateCaromBallPhysics(9);
|
|
if (moved[2])
|
|
CalculateCaromBallPhysics(2);
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
private Vector3 CalculateCueBallDeltaPosition()
|
|
{
|
|
if (!IsCueInPlay)
|
|
return Vector3.zero;
|
|
|
|
// Get what will be the next position
|
|
var originalDelta = currentBallVelocities[0] * TIME_PER_STEP;
|
|
|
|
var norm = currentBallVelocities[0].normalized;
|
|
|
|
// Closest found values
|
|
var minlf = float.MaxValue;
|
|
var minid = 0;
|
|
float mins = 0;
|
|
|
|
// Loop balls look for collisions
|
|
for (var i = 1; i < ballsArePocketed.Length; i++)
|
|
{
|
|
if (ballsArePocketed[i])
|
|
continue;
|
|
|
|
var h = currentBallPositions[i] - currentBallPositions[0];
|
|
var lf = Vector3.Dot(norm, h);
|
|
if (lf < 0f)
|
|
continue;
|
|
|
|
var s = BALL_DIAMETER_SQUARED_MINUS_EPSILON - Vector3.Dot(h, h) + lf * lf;
|
|
|
|
if (s < 0.0f)
|
|
continue;
|
|
|
|
if (!(lf < minlf))
|
|
continue;
|
|
|
|
minlf = lf;
|
|
minid = i;
|
|
mins = s;
|
|
}
|
|
|
|
if (minid <= 0)
|
|
return originalDelta;
|
|
|
|
var nmag = minlf - Mathf.Sqrt(mins);
|
|
|
|
// Assign new position if got appropriate magnitude
|
|
if (nmag * nmag < originalDelta.sqrMagnitude)
|
|
return norm * nmag;
|
|
|
|
return originalDelta;
|
|
}
|
|
|
|
private bool StepOneBall(int id, bool[] moved)
|
|
{
|
|
GameObject ball = ballTransforms[id].gameObject;
|
|
|
|
bool isBallMoving = false;
|
|
|
|
// no point updating velocity if ball isn't moving
|
|
if (currentBallVelocities[id] != Vector3.zero || currentAngularVelocities[id] != Vector3.zero)
|
|
{
|
|
isBallMoving = UpdateVelocity(id, ball);
|
|
}
|
|
|
|
moved[id] |= isBallMoving;
|
|
|
|
// check for collisions. a non-moving ball might be collided by a moving one
|
|
for (var i = id + 1; i < ballsArePocketed.Length; i++)
|
|
{
|
|
if (ballsArePocketed[i])
|
|
{
|
|
continue;
|
|
}
|
|
|
|
Vector3 delta = currentBallPositions[i] - currentBallPositions[id];
|
|
var dist = delta.sqrMagnitude;
|
|
|
|
if (!(dist < BALL_DIAMETER_SQUARED))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
dist = Mathf.Sqrt(dist);
|
|
Vector3 normal = delta / dist;
|
|
|
|
// static resolution
|
|
Vector3 res = (ballDiameter - dist) * normal;
|
|
currentBallPositions[i] += res;
|
|
currentBallPositions[id] -= res;
|
|
moved[i] = true;
|
|
moved[id] = true;
|
|
|
|
Vector3 velocityDelta = currentBallVelocities[id] - currentBallVelocities[i];
|
|
|
|
float dot = Vector3.Dot(velocityDelta, normal);
|
|
|
|
// Dynamic resolution (Cr is assumed to be (1)+1.0)
|
|
|
|
Vector3 reflection = normal * dot;
|
|
currentBallVelocities[id] -= reflection;
|
|
currentBallVelocities[i] += reflection;
|
|
|
|
HandleBallCollision(id, i, reflection);
|
|
}
|
|
|
|
return isBallMoving;
|
|
}
|
|
|
|
private bool UpdateVelocity(int id, GameObject ball)
|
|
{
|
|
bool ballMoving = false;
|
|
|
|
// Since v1.5.0
|
|
Vector3 V = currentBallVelocities[id];
|
|
Vector3 VwithoutY = new Vector3(V.x, 0, V.z);
|
|
Vector3 W = currentAngularVelocities[id];
|
|
Vector3 cv;
|
|
|
|
// Equations derived from: http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.89.4627&rep=rep1&type=pdf
|
|
//
|
|
// R: Contact location with ball and floor aka: (0,-r,0)
|
|
// µₛ: Slipping friction coefficient
|
|
// µᵣ: Rolling friction coefficient
|
|
// i: Up vector aka: (0,1,0)
|
|
// g: Planet Earth's gravitation acceleration ( 9.80665 )
|
|
//
|
|
// Relative contact velocity (marlow):
|
|
// c = v + R✕ω
|
|
//
|
|
// Ball is classified as 'rolling' or 'slipping'. Rolling is when the relative velocity is none and the ball is
|
|
// said to be in pure rolling motion
|
|
//
|
|
// When ball is classified as rolling:
|
|
// Δv = -µᵣ∙g∙Δt∙(v/|v|)
|
|
//
|
|
// Angular momentum can therefore be derived as:
|
|
// ωₓ = -vᵤ/R
|
|
// ωᵧ = 0
|
|
// ωᵤ = vₓ/R
|
|
//
|
|
// In the slipping state:
|
|
// Δω = ((-5∙µₛ∙g)/(2/R))∙Δt∙i✕(c/|c|)
|
|
// Δv = -µₛ∙g∙Δt(c/|c|)
|
|
|
|
if (currentBallPositions[id].y < 0.001)
|
|
{
|
|
// Relative contact velocity of ball and table
|
|
cv = VwithoutY + Vector3.Cross(CONTACT_POINT, W);
|
|
float cvMagnitude = cv.magnitude;
|
|
|
|
// Rolling is achieved when cv's length is approaching 0
|
|
// The epsilon is quite high here because of the fairly large timestep we are working with
|
|
if (cvMagnitude <= 0.1f)
|
|
{
|
|
//V += -k_F_ROLL * k_GRAVITY * k_FIXED_TIME_STEP * V.normalized;
|
|
// (baked):
|
|
V += -0.00122583125f * VwithoutY.normalized;
|
|
|
|
// Calculate rolling angular velocity
|
|
W.x = -V.z * ONE_OVER_BALL_RADIUS;
|
|
|
|
if (0.3f > Mathf.Abs(W.y))
|
|
{
|
|
W.y = 0.0f;
|
|
}
|
|
else
|
|
{
|
|
W.y -= Mathf.Sign(W.y) * 0.3f;
|
|
}
|
|
|
|
W.z = V.x * ONE_OVER_BALL_RADIUS;
|
|
|
|
// Stopping scenario
|
|
if (V.sqrMagnitude < 0.0001f && W.magnitude < 0.04f)
|
|
{
|
|
W = Vector3.zero;
|
|
V = Vector3.zero;
|
|
}
|
|
else
|
|
{
|
|
ballMoving = true;
|
|
}
|
|
}
|
|
else // Slipping
|
|
{
|
|
Vector3 nv = cv / cvMagnitude;
|
|
|
|
// Angular slipping friction
|
|
//W += ((-5.0f * k_F_SLIDE * k_GRAVITY)/(2.0f * 0.03f)) * k_FIXED_TIME_STEP * Vector3.Cross( Vector3.up, nv );
|
|
// (baked):
|
|
W += -2.04305208f * Vector3.Cross(Vector3.up, nv);
|
|
|
|
//V += -k_F_SLIDE * k_GRAVITY * k_FIXED_TIME_STEP * nv;
|
|
// (baked):
|
|
V += -0.024516625f * nv;
|
|
|
|
ballMoving = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ballMoving = true;
|
|
}
|
|
|
|
if (currentBallPositions[id].y > 0) // small epsilon to apply gravity
|
|
V.y -= EARTH_GRAVITY * TIME_PER_STEP;
|
|
else
|
|
V.y = 0;
|
|
|
|
currentAngularVelocities[id] = W;
|
|
currentBallVelocities[id] = V;
|
|
|
|
ball.transform.Rotate(this.transform.TransformDirection(W.normalized),
|
|
W.magnitude * TIME_PER_STEP * -Mathf.Rad2Deg, Space.World);
|
|
|
|
return ballMoving;
|
|
}
|
|
|
|
private void CalculatePoolBallPhysics(int id)
|
|
{
|
|
Vector3 nToVNormalized;
|
|
Vector3 a_to_v;
|
|
float dot;
|
|
|
|
var ballUnderTest = currentBallPositions[id];
|
|
|
|
signPos.x = Mathf.Sign(ballUnderTest.x);
|
|
signPos.z = Mathf.Sign(ballUnderTest.z);
|
|
|
|
ballUnderTest = Vector3.Scale(ballUnderTest, signPos);
|
|
|
|
if (ballUnderTest.x > vA.x) // Major Regions
|
|
{
|
|
if (ballUnderTest.x > ballUnderTest.z + tableWidthMinusHeight) // Minor B
|
|
{
|
|
if (ballUnderTest.z < tableHeight - pocketOuterRadius)
|
|
{
|
|
// Region H
|
|
if (ballUnderTest.x > tableWidth - ballRadius)
|
|
{
|
|
// Static resolution
|
|
ballUnderTest.x = tableWidth - ballRadius;
|
|
|
|
// Dynamic
|
|
ApplyCushionBounce(id, Vector3.Scale(vCvWNormal, signPos));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
a_to_v = ballUnderTest - vC;
|
|
|
|
if (Vector3.Dot(a_to_v, vBvY) > 0.0f)
|
|
{
|
|
// Region I ( VORONI )
|
|
if (a_to_v.magnitude < ballRadius)
|
|
{
|
|
// Static resolution
|
|
nToVNormalized = a_to_v.normalized;
|
|
ballUnderTest = vC + nToVNormalized * ballRadius;
|
|
|
|
// Dynamic
|
|
ApplyCushionBounce(id, Vector3.Scale(nToVNormalized, signPos));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Region J
|
|
a_to_v = ballUnderTest - pQ;
|
|
|
|
if (Vector3.Dot(vCvZNormal, a_to_v) < 0.0f)
|
|
{
|
|
// Static resolution
|
|
dot = Vector3.Dot(a_to_v, vBvY);
|
|
ballUnderTest = pQ + dot * vBvY;
|
|
|
|
// Dynamic
|
|
ApplyCushionBounce(id, Vector3.Scale(vCvZNormal, signPos));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else // Minor A
|
|
{
|
|
if (ballUnderTest.x < vB.x)
|
|
{
|
|
// Region A
|
|
if (ballUnderTest.z > pN.z)
|
|
{
|
|
// Velocity based A->C delegation ( scuffed CCD )
|
|
a_to_v = ballUnderTest - vA;
|
|
var _V = Vector3.Scale(currentBallVelocities[id], signPos);
|
|
var V = new Vector3(-_V.z, 0.0f, _V.x);
|
|
|
|
if (ballUnderTest.z > vA.z)
|
|
{
|
|
if (Vector3.Dot(V, a_to_v) > 0.0f)
|
|
{
|
|
// Region C ( Delegated )
|
|
a_to_v = ballUnderTest - pL;
|
|
|
|
// Static resolution
|
|
dot = Vector3.Dot(a_to_v, vAvD);
|
|
ballUnderTest = pL + dot * vAvD;
|
|
|
|
// Dynamic
|
|
ApplyCushionBounce(id, Vector3.Scale(vAvDNormal, signPos));
|
|
}
|
|
else
|
|
{
|
|
// Static resolution
|
|
ballUnderTest.z = pN.z;
|
|
|
|
// Dynamic
|
|
ApplyCushionBounce(id, Vector3.Scale(vAvBNormal, signPos));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Static resolution
|
|
ballUnderTest.z = pN.z;
|
|
|
|
// Dynamic
|
|
ApplyCushionBounce(id, Vector3.Scale(vAvBNormal, signPos));
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
a_to_v = ballUnderTest - vB;
|
|
|
|
if (Vector3.Dot(a_to_v, vBvY) > 0.0f)
|
|
{
|
|
// Region F ( VERONI )
|
|
if (a_to_v.magnitude < ballRadius)
|
|
{
|
|
// Static resolution
|
|
nToVNormalized = a_to_v.normalized;
|
|
ballUnderTest = vB + nToVNormalized * ballRadius;
|
|
|
|
// Dynamic
|
|
ApplyCushionBounce(id, Vector3.Scale(nToVNormalized, signPos));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Region G
|
|
a_to_v = ballUnderTest - pP;
|
|
|
|
if (Vector3.Dot(vBvYNormal, a_to_v) < 0.0f)
|
|
{
|
|
// Static resolution
|
|
dot = Vector3.Dot(a_to_v, vBvY);
|
|
ballUnderTest = pP + dot * vBvY;
|
|
|
|
// Dynamic
|
|
ApplyCushionBounce(id, Vector3.Scale(vBvYNormal, signPos));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
a_to_v = ballUnderTest - vA;
|
|
|
|
if (Vector3.Dot(a_to_v, vAvD) > 0.0f)
|
|
{
|
|
a_to_v = ballUnderTest - vD;
|
|
|
|
if (Vector3.Dot(a_to_v, vAvD) > 0.0f)
|
|
{
|
|
if (ballUnderTest.z > pK.z)
|
|
{
|
|
// Region E
|
|
if (ballUnderTest.x > pK.x)
|
|
{
|
|
// Static resolution
|
|
ballUnderTest.x = pK.x;
|
|
|
|
// Dynamic
|
|
ApplyCushionBounce(id, Vector3.Scale(vCvWNormal, signPos));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Region D ( VORONI )
|
|
if (a_to_v.magnitude < ballRadius)
|
|
{
|
|
// Static resolution
|
|
nToVNormalized = a_to_v.normalized;
|
|
ballUnderTest = vD + nToVNormalized * ballRadius;
|
|
|
|
// Dynamic
|
|
ApplyCushionBounce(id, Vector3.Scale(nToVNormalized, signPos));
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Region C
|
|
a_to_v = ballUnderTest - pL;
|
|
|
|
if (Vector3.Dot(vAvDNormal, a_to_v) < 0.0f)
|
|
{
|
|
// Static resolution
|
|
dot = Vector3.Dot(a_to_v, vAvD);
|
|
ballUnderTest = pL + dot * vAvD;
|
|
|
|
// Dynamic
|
|
ApplyCushionBounce(id, Vector3.Scale(vAvDNormal, signPos));
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Region B ( VORONI )
|
|
if (a_to_v.magnitude < ballRadius)
|
|
{
|
|
// Static resolution
|
|
nToVNormalized = a_to_v.normalized;
|
|
ballUnderTest = vA + nToVNormalized * ballRadius;
|
|
|
|
// Dynamic
|
|
ApplyCushionBounce(id, Vector3.Scale(nToVNormalized, signPos));
|
|
}
|
|
}
|
|
}
|
|
|
|
currentBallPositions[id] = Vector3.Scale(ballUnderTest, signPos);
|
|
}
|
|
|
|
private void ApplyCushionBounce(int id, Vector3 n)
|
|
{
|
|
// Don't bounce if the cushions obviously could not collide.
|
|
if (currentBallPositions[id].y > ballRadius)
|
|
return;
|
|
|
|
// Reject bounce if velocity is going the same way as normal
|
|
// this state means we tunneled, but it happens only on the corner
|
|
// vertexes
|
|
Vector3 currentBallVelocity = currentBallVelocities[id];
|
|
if (Vector3.Dot(currentBallVelocity, n) > 0.0f)
|
|
return;
|
|
|
|
// Rotate V, W to be in the reference frame of cushion
|
|
Quaternion rq = Quaternion.AngleAxis(Mathf.Atan2(-n.z, -n.x) * Mathf.Rad2Deg, Vector3.up);
|
|
Quaternion rb = Quaternion.Inverse(rq);
|
|
Vector3 V = rq * currentBallVelocity;
|
|
Vector3 W = rq * currentAngularVelocities[id];
|
|
|
|
Vector3 V1 = new Vector3(-V.x * F - 0.00240675711f * W.z, 0f,
|
|
0.71428571428f * V.z + 0.00857142857f * (W.x * SIN_A - W.y * COS_A) - V.z);
|
|
|
|
var s_x = V.x * SIN_A + W.z;
|
|
var s_z = -V.z - W.y * COS_A + W.x * SIN_A;
|
|
|
|
var k = s_z * 0.71428571428f;
|
|
var c = V.x * COS_A;
|
|
Vector3 W1 = new Vector3(k * SIN_A, k * COS_A, 15.625f * (-s_x * 0.04571428571f + c * 0.0546021744f));
|
|
|
|
// Unrotate result
|
|
currentBallVelocities[id] += rb * V1;
|
|
currentAngularVelocities[id] += rb * W1;
|
|
|
|
if (id == 0)
|
|
cushionsHitThisTurn++;
|
|
}
|
|
|
|
private void CheckIfBallsArePocketed(int id)
|
|
{
|
|
Vector3 A = currentBallPositions[id];
|
|
Vector3 absA = new Vector3(Mathf.Abs(A.x), A.y, Mathf.Abs(A.z));
|
|
|
|
if (
|
|
(absA - cornerPocket).sqrMagnitude < POCKET_INNER_RADIUS_SQUARED ||
|
|
(absA - middlePocket).sqrMagnitude < POCKET_INNER_RADIUS_SQUARED ||
|
|
absA.z > middlePocket.z ||
|
|
absA.z > -absA.x + cornerPocket.x + cornerPocket.z
|
|
)
|
|
{
|
|
TriggerPocketBall(id);
|
|
|
|
currentBallVelocities[id] = Vector3.zero;
|
|
currentAngularVelocities[id] = Vector3.zero;
|
|
}
|
|
}
|
|
|
|
private bool _preventEndOfTurn;
|
|
|
|
private void HandleCueBallOffTable()
|
|
{
|
|
if (logger)
|
|
logger._Log(name, "HandleCueBallOffTable");
|
|
|
|
// Only run this once.
|
|
if (!IsCueInPlay)
|
|
return;
|
|
|
|
isCueOutOfBounds = true;
|
|
|
|
if (IsCarom)
|
|
fourBallCueLeftTable = true;
|
|
else
|
|
ballsArePocketed[0] = true;
|
|
|
|
HandleFoulEffects();
|
|
|
|
// VFX ( make ball move )
|
|
var body = ballRigidbodies[0];
|
|
body.isKinematic = false;
|
|
body.velocity = transform.TransformVector(new Vector3(
|
|
currentBallVelocities[0].x,
|
|
currentBallVelocities[0].y,
|
|
currentBallVelocities[0].z
|
|
));
|
|
|
|
if (!cueBallController)
|
|
return;
|
|
|
|
cueBallController._EnableDonking(this, 3);
|
|
_preventEndOfTurn = true;
|
|
}
|
|
|
|
public void _AllowEndOfTurn()
|
|
{
|
|
_preventEndOfTurn = false;
|
|
}
|
|
|
|
private void TriggerPocketBall(int id)
|
|
{
|
|
var total = 0;
|
|
|
|
// Get total for X positioning
|
|
foreach (var pocketed in ballsArePocketed)
|
|
{
|
|
if (pocketed)
|
|
total++;
|
|
}
|
|
|
|
// place ball on the rack
|
|
currentBallPositions[id] = Vector3.zero + (float) total * ballDiameter * Vector3.zero;
|
|
|
|
// This is where we actually save the pocketed/non-pocketed state of balls.
|
|
ballsArePocketed[id] = true;
|
|
|
|
bool success = false;
|
|
|
|
// isOpen is only ever false in blackball - so this covers any ball sunk in 9 ball, which is correct (in
|
|
// our simplified 9-ball, the only way to trigger a foul is to foul the cue ball or not hit the lowest-count
|
|
// ball first when shooting.
|
|
if (isOpen && id > 1)
|
|
success = true;
|
|
// it is blue's turn
|
|
else if ((isTeam2Turn && isTeam2Blue || !isTeam2Turn && !isTeam2Blue) && id > 1 && id < 9)
|
|
success = true;
|
|
// it is orange's turn
|
|
else if (id >= 9)
|
|
success = true;
|
|
|
|
HandleBallSunk(success);
|
|
|
|
if (id == 0)
|
|
isCueOutOfBounds = true;
|
|
|
|
// VFX ( make ball move )
|
|
var body = ballRigidbodies[id];
|
|
body.isKinematic = false;
|
|
body.velocity = transform.TransformVector(new Vector3(
|
|
currentBallVelocities[id].x,
|
|
currentBallVelocities[id].y,
|
|
currentBallVelocities[id].z
|
|
));
|
|
}
|
|
}
|
|
}
|