Keep local version of files
This commit is contained in:
+1098
-1098
File diff suppressed because it is too large
Load Diff
@@ -1,2 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
fileFormatVersion: 2
|
||||
guid: c886581a7bac8f34ba8f7995eb48db02
|
||||
+203
-203
@@ -1,203 +1,203 @@
|
||||
using System.Collections;
|
||||
using Mirror;
|
||||
using UnityEngine;
|
||||
|
||||
public class Ball : NetworkBehaviour
|
||||
{
|
||||
[HideInInspector]public Rigidbody2D rb;
|
||||
Vector3 startPosition;
|
||||
public float wallDetectionLength = 0.5f;
|
||||
public float wallStuckFixForce = 1f;
|
||||
public float maxWallStuckVelocity = 5f;
|
||||
public LayerMask wallLayerMask;
|
||||
public float distToGoal;
|
||||
public float nearGoalThreshold = 2.5f;
|
||||
void Awake()
|
||||
{
|
||||
rb = GetComponent<Rigidbody2D>();
|
||||
col = GetComponent<Collider2D>();
|
||||
startPosition = transform.position;
|
||||
}
|
||||
|
||||
Collider2D col;
|
||||
Coroutine coroutineReset;
|
||||
bool holdingCollisionLock;
|
||||
|
||||
public void SetCollisionsEnabled(bool enabled)
|
||||
{
|
||||
if (col != null)
|
||||
col.enabled = enabled;
|
||||
if (rb != null && !enabled)
|
||||
{
|
||||
rb.linearVelocity = Vector2.zero;
|
||||
rb.angularVelocity = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
public void Reset(float duration){
|
||||
if (coroutineReset != null)
|
||||
{
|
||||
StopCoroutine(coroutineReset);
|
||||
RestoreAfterReset();
|
||||
}
|
||||
coroutineReset = StartCoroutine(CoroutineReset(duration));
|
||||
}
|
||||
|
||||
IEnumerator CoroutineReset(float duration){
|
||||
if (GameManager.instance != null)
|
||||
{
|
||||
GameManager.instance.PushResetCollisionLock();
|
||||
holdingCollisionLock = true;
|
||||
}
|
||||
|
||||
rb.linearVelocity = Vector2.zero;
|
||||
rb.angularVelocity = 0f;
|
||||
|
||||
float t=0;
|
||||
Vector3 pos1 = transform.position;
|
||||
Quaternion rot1 = transform.rotation;
|
||||
while (t < duration){
|
||||
t+=Time.deltaTime;
|
||||
float u = Mathf.Clamp01(t / duration);
|
||||
transform.position = Vector3.Lerp(pos1, startPosition, u);
|
||||
transform.rotation = Quaternion.Lerp(rot1, Quaternion.identity, u);
|
||||
yield return null;
|
||||
}
|
||||
transform.position = startPosition;
|
||||
transform.rotation = Quaternion.identity;
|
||||
|
||||
RestoreAfterReset();
|
||||
}
|
||||
|
||||
void RestoreAfterReset()
|
||||
{
|
||||
if (rb != null)
|
||||
{
|
||||
rb.linearVelocity = Vector2.zero;
|
||||
rb.angularVelocity = 0f;
|
||||
}
|
||||
if (holdingCollisionLock && GameManager.instance != null)
|
||||
{
|
||||
GameManager.instance.PopResetCollisionLock();
|
||||
holdingCollisionLock = false;
|
||||
}
|
||||
coroutineReset = null;
|
||||
}
|
||||
|
||||
public bool touchingB,touchingL,touchingR,touchingT = false;
|
||||
void Update()
|
||||
{
|
||||
CheckNearGoal();
|
||||
|
||||
if(!isServer){return;}
|
||||
WallDetection();
|
||||
}
|
||||
|
||||
|
||||
void CheckNearGoal(){
|
||||
Goal[] goals = FindObjectsByType<Goal>(FindObjectsSortMode.None);
|
||||
float closestDist = float.MaxValue;
|
||||
foreach(Goal goal in goals){
|
||||
float dist = Vector3.Distance(transform.position, goal.transform.position);
|
||||
if(dist < closestDist){
|
||||
closestDist = dist;
|
||||
}
|
||||
}
|
||||
distToGoal = closestDist;
|
||||
|
||||
AudioManager.instance.SetIsBallNearGoal(distToGoal < nearGoalThreshold);
|
||||
}
|
||||
|
||||
void OnCollisionEnter2D(Collision2D collision)
|
||||
{
|
||||
float otherSpeed = collision.relativeVelocity.magnitude;
|
||||
float velocityMult = 1- Mathf.Clamp01(rb.linearVelocity.magnitude / maxWallStuckVelocity);
|
||||
|
||||
// Debug.Log($"{collision.collider.name} hit ball @ {otherSpeed}, ball speed: {rb.linearVelocity.magnitude}");
|
||||
int mult=0;
|
||||
if(touchingB){
|
||||
mult++;
|
||||
}
|
||||
if(touchingT){
|
||||
mult++;
|
||||
}
|
||||
if(touchingL){
|
||||
mult++;
|
||||
}
|
||||
if(touchingR){
|
||||
mult++;
|
||||
}
|
||||
velocityMult *= mult;
|
||||
if (touchingR) {
|
||||
rb.AddForce(Vector2.left * otherSpeed *wallStuckFixForce * velocityMult );
|
||||
}
|
||||
if (touchingL) {
|
||||
rb.AddForce(Vector2.right * otherSpeed * wallStuckFixForce * velocityMult );
|
||||
}
|
||||
if (touchingT) {
|
||||
rb.AddForce(Vector2.down * otherSpeed * wallStuckFixForce * velocityMult );
|
||||
}
|
||||
if (touchingB) {
|
||||
rb.AddForce(Vector2.up *otherSpeed * wallStuckFixForce * velocityMult );
|
||||
}
|
||||
|
||||
|
||||
float maxVolSpeed =5f;
|
||||
float vol = Mathf.Clamp01(otherSpeed / maxVolSpeed);
|
||||
|
||||
if(collision.collider.CompareTag("wall")){
|
||||
AudioManager.instance.PlayWallHit(vol);
|
||||
if (isServer)
|
||||
ReplayRecorder.RecordHit("ball_wall", vol, this);
|
||||
RpcWallHitAudio(vol);
|
||||
}
|
||||
else if(collision.collider.CompareTag("Puck")){
|
||||
AudioManager.instance.PlayBallHit(vol);
|
||||
if (isServer)
|
||||
ReplayRecorder.RecordHit("ball_puck", vol, this);
|
||||
RpcBallHitAudio(vol);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
[ClientRpc]
|
||||
void RpcWallHitAudio(float vol){
|
||||
AudioManager.instance.PlayWallHit(vol);
|
||||
}
|
||||
|
||||
[ClientRpc]
|
||||
void RpcBallHitAudio(float vol){
|
||||
AudioManager.instance.PlayBallHit(vol);
|
||||
}
|
||||
|
||||
void WallDetection(){
|
||||
// Linecast for each direction
|
||||
Vector3 pos = transform.position;
|
||||
|
||||
RaycastHit2D hitL = Physics2D.Linecast(pos, pos + Vector3.left * wallDetectionLength, wallLayerMask);
|
||||
touchingL = hitL.collider != null;
|
||||
|
||||
RaycastHit2D hitR = Physics2D.Linecast(pos, pos + Vector3.right * wallDetectionLength, wallLayerMask);
|
||||
touchingR = hitR.collider != null;
|
||||
|
||||
RaycastHit2D hitT = Physics2D.Linecast(pos, pos + Vector3.up * wallDetectionLength, wallLayerMask);
|
||||
touchingT = hitT.collider != null;
|
||||
|
||||
RaycastHit2D hitB = Physics2D.Linecast(pos, pos + Vector3.down * wallDetectionLength, wallLayerMask);
|
||||
touchingB = hitB.collider != null;
|
||||
}
|
||||
void OnDrawGizmos()
|
||||
{
|
||||
Gizmos.color = touchingL ? Color.red : Color.green;
|
||||
Gizmos.DrawLine(transform.position, transform.position + (Vector3.left * wallDetectionLength));
|
||||
|
||||
Gizmos.color = touchingR ? Color.red : Color.green;
|
||||
Gizmos.DrawLine(transform.position, transform.position + (Vector3.right * wallDetectionLength));
|
||||
|
||||
Gizmos.color = touchingT ? Color.red : Color.green;
|
||||
Gizmos.DrawLine(transform.position, transform.position + (Vector3.up * wallDetectionLength));
|
||||
|
||||
Gizmos.color = touchingB ? Color.red : Color.green;
|
||||
Gizmos.DrawLine(transform.position, transform.position + (Vector3.down * wallDetectionLength));
|
||||
}
|
||||
}
|
||||
using System.Collections;
|
||||
using Mirror;
|
||||
using UnityEngine;
|
||||
|
||||
public class Ball : NetworkBehaviour
|
||||
{
|
||||
[HideInInspector]public Rigidbody2D rb;
|
||||
Vector3 startPosition;
|
||||
public float wallDetectionLength = 0.5f;
|
||||
public float wallStuckFixForce = 1f;
|
||||
public float maxWallStuckVelocity = 5f;
|
||||
public LayerMask wallLayerMask;
|
||||
public float distToGoal;
|
||||
public float nearGoalThreshold = 2.5f;
|
||||
void Awake()
|
||||
{
|
||||
rb = GetComponent<Rigidbody2D>();
|
||||
col = GetComponent<Collider2D>();
|
||||
startPosition = transform.position;
|
||||
}
|
||||
|
||||
Collider2D col;
|
||||
Coroutine coroutineReset;
|
||||
bool holdingCollisionLock;
|
||||
|
||||
public void SetCollisionsEnabled(bool enabled)
|
||||
{
|
||||
if (col != null)
|
||||
col.enabled = enabled;
|
||||
if (rb != null && !enabled)
|
||||
{
|
||||
rb.linearVelocity = Vector2.zero;
|
||||
rb.angularVelocity = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
public void Reset(float duration){
|
||||
if (coroutineReset != null)
|
||||
{
|
||||
StopCoroutine(coroutineReset);
|
||||
RestoreAfterReset();
|
||||
}
|
||||
coroutineReset = StartCoroutine(CoroutineReset(duration));
|
||||
}
|
||||
|
||||
IEnumerator CoroutineReset(float duration){
|
||||
if (GameManager.instance != null)
|
||||
{
|
||||
GameManager.instance.PushResetCollisionLock();
|
||||
holdingCollisionLock = true;
|
||||
}
|
||||
|
||||
rb.linearVelocity = Vector2.zero;
|
||||
rb.angularVelocity = 0f;
|
||||
|
||||
float t=0;
|
||||
Vector3 pos1 = transform.position;
|
||||
Quaternion rot1 = transform.rotation;
|
||||
while (t < duration){
|
||||
t+=Time.deltaTime;
|
||||
float u = Mathf.Clamp01(t / duration);
|
||||
transform.position = Vector3.Lerp(pos1, startPosition, u);
|
||||
transform.rotation = Quaternion.Lerp(rot1, Quaternion.identity, u);
|
||||
yield return null;
|
||||
}
|
||||
transform.position = startPosition;
|
||||
transform.rotation = Quaternion.identity;
|
||||
|
||||
RestoreAfterReset();
|
||||
}
|
||||
|
||||
void RestoreAfterReset()
|
||||
{
|
||||
if (rb != null)
|
||||
{
|
||||
rb.linearVelocity = Vector2.zero;
|
||||
rb.angularVelocity = 0f;
|
||||
}
|
||||
if (holdingCollisionLock && GameManager.instance != null)
|
||||
{
|
||||
GameManager.instance.PopResetCollisionLock();
|
||||
holdingCollisionLock = false;
|
||||
}
|
||||
coroutineReset = null;
|
||||
}
|
||||
|
||||
public bool touchingB,touchingL,touchingR,touchingT = false;
|
||||
void Update()
|
||||
{
|
||||
CheckNearGoal();
|
||||
|
||||
if(!isServer){return;}
|
||||
WallDetection();
|
||||
}
|
||||
|
||||
|
||||
void CheckNearGoal(){
|
||||
Goal[] goals = FindObjectsByType<Goal>(FindObjectsSortMode.None);
|
||||
float closestDist = float.MaxValue;
|
||||
foreach(Goal goal in goals){
|
||||
float dist = Vector3.Distance(transform.position, goal.transform.position);
|
||||
if(dist < closestDist){
|
||||
closestDist = dist;
|
||||
}
|
||||
}
|
||||
distToGoal = closestDist;
|
||||
|
||||
AudioManager.instance.SetIsBallNearGoal(distToGoal < nearGoalThreshold);
|
||||
}
|
||||
|
||||
void OnCollisionEnter2D(Collision2D collision)
|
||||
{
|
||||
float otherSpeed = collision.relativeVelocity.magnitude;
|
||||
float velocityMult = 1- Mathf.Clamp01(rb.linearVelocity.magnitude / maxWallStuckVelocity);
|
||||
|
||||
// Debug.Log($"{collision.collider.name} hit ball @ {otherSpeed}, ball speed: {rb.linearVelocity.magnitude}");
|
||||
int mult=0;
|
||||
if(touchingB){
|
||||
mult++;
|
||||
}
|
||||
if(touchingT){
|
||||
mult++;
|
||||
}
|
||||
if(touchingL){
|
||||
mult++;
|
||||
}
|
||||
if(touchingR){
|
||||
mult++;
|
||||
}
|
||||
velocityMult *= mult;
|
||||
if (touchingR) {
|
||||
rb.AddForce(Vector2.left * otherSpeed *wallStuckFixForce * velocityMult );
|
||||
}
|
||||
if (touchingL) {
|
||||
rb.AddForce(Vector2.right * otherSpeed * wallStuckFixForce * velocityMult );
|
||||
}
|
||||
if (touchingT) {
|
||||
rb.AddForce(Vector2.down * otherSpeed * wallStuckFixForce * velocityMult );
|
||||
}
|
||||
if (touchingB) {
|
||||
rb.AddForce(Vector2.up *otherSpeed * wallStuckFixForce * velocityMult );
|
||||
}
|
||||
|
||||
|
||||
float maxVolSpeed =5f;
|
||||
float vol = Mathf.Clamp01(otherSpeed / maxVolSpeed);
|
||||
|
||||
if(collision.collider.CompareTag("wall")){
|
||||
AudioManager.instance.PlayWallHit(vol);
|
||||
if (isServer)
|
||||
ReplayRecorder.RecordHit("ball_wall", vol, this);
|
||||
RpcWallHitAudio(vol);
|
||||
}
|
||||
else if(collision.collider.CompareTag("Puck")){
|
||||
AudioManager.instance.PlayBallHit(vol);
|
||||
if (isServer)
|
||||
ReplayRecorder.RecordHit("ball_puck", vol, this);
|
||||
RpcBallHitAudio(vol);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
[ClientRpc]
|
||||
void RpcWallHitAudio(float vol){
|
||||
AudioManager.instance.PlayWallHit(vol);
|
||||
}
|
||||
|
||||
[ClientRpc]
|
||||
void RpcBallHitAudio(float vol){
|
||||
AudioManager.instance.PlayBallHit(vol);
|
||||
}
|
||||
|
||||
void WallDetection(){
|
||||
// Linecast for each direction
|
||||
Vector3 pos = transform.position;
|
||||
|
||||
RaycastHit2D hitL = Physics2D.Linecast(pos, pos + Vector3.left * wallDetectionLength, wallLayerMask);
|
||||
touchingL = hitL.collider != null;
|
||||
|
||||
RaycastHit2D hitR = Physics2D.Linecast(pos, pos + Vector3.right * wallDetectionLength, wallLayerMask);
|
||||
touchingR = hitR.collider != null;
|
||||
|
||||
RaycastHit2D hitT = Physics2D.Linecast(pos, pos + Vector3.up * wallDetectionLength, wallLayerMask);
|
||||
touchingT = hitT.collider != null;
|
||||
|
||||
RaycastHit2D hitB = Physics2D.Linecast(pos, pos + Vector3.down * wallDetectionLength, wallLayerMask);
|
||||
touchingB = hitB.collider != null;
|
||||
}
|
||||
void OnDrawGizmos()
|
||||
{
|
||||
Gizmos.color = touchingL ? Color.red : Color.green;
|
||||
Gizmos.DrawLine(transform.position, transform.position + (Vector3.left * wallDetectionLength));
|
||||
|
||||
Gizmos.color = touchingR ? Color.red : Color.green;
|
||||
Gizmos.DrawLine(transform.position, transform.position + (Vector3.right * wallDetectionLength));
|
||||
|
||||
Gizmos.color = touchingT ? Color.red : Color.green;
|
||||
Gizmos.DrawLine(transform.position, transform.position + (Vector3.up * wallDetectionLength));
|
||||
|
||||
Gizmos.color = touchingB ? Color.red : Color.green;
|
||||
Gizmos.DrawLine(transform.position, transform.position + (Vector3.down * wallDetectionLength));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
fileFormatVersion: 2
|
||||
guid: f6fe03c800a746a4486ffa0add89e74c
|
||||
@@ -1,58 +1,58 @@
|
||||
using UnityEngine;
|
||||
using DG.Tweening;
|
||||
[RequireComponent(typeof(Camera))]
|
||||
public class CameraEffects : MonoBehaviour
|
||||
{
|
||||
public static CameraEffects instance;
|
||||
Camera cam;
|
||||
|
||||
[SerializeField]private float stretchFactor = 0;
|
||||
[SerializeField]private bool isBeingStretched = false;
|
||||
[SerializeField]private float minFov = 5;
|
||||
[SerializeField]private float maxFov = 10;
|
||||
[SerializeField]private float fovSpeed = 10;
|
||||
[SerializeField] private float bounceDuration = 0.35f;
|
||||
[SerializeField] private float bounceElasticity = 0.7f;
|
||||
[SerializeField] private float bounceOvershoot = 0.2f;
|
||||
void Awake()
|
||||
{
|
||||
instance = this;
|
||||
cam = Camera.main;
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if(isBeingStretched){
|
||||
float targetFov = minFov + stretchFactor * (maxFov - minFov);
|
||||
cam.orthographicSize = Mathf.Lerp(cam.orthographicSize, targetFov, Time.deltaTime * fovSpeed);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetStretchFactor(float stretchFactor){
|
||||
this.stretchFactor = stretchFactor;
|
||||
isBeingStretched = true;
|
||||
}
|
||||
|
||||
public void SetUpsideDown(bool isUpsideDown){
|
||||
if(isUpsideDown){
|
||||
transform.rotation = Quaternion.Euler(0, 0, 180);
|
||||
transform.position = new Vector3(transform.position.x, Mathf.Abs(transform.position.y), transform.position.z);
|
||||
}else{
|
||||
transform.rotation = Quaternion.Euler(0, 0, 0);
|
||||
transform.position = new Vector3(transform.position.x, -Mathf.Abs(transform.position.y), transform.position.z);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void ReleaseStretchFactor(){
|
||||
stretchFactor = 0;
|
||||
isBeingStretched = false;
|
||||
cam.DOOrthoSize(minFov, bounceDuration)
|
||||
.SetEase(Ease.OutElastic, bounceElasticity, bounceOvershoot)
|
||||
.OnComplete(() => {
|
||||
stretchFactor = 0;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
using UnityEngine;
|
||||
using DG.Tweening;
|
||||
[RequireComponent(typeof(Camera))]
|
||||
public class CameraEffects : MonoBehaviour
|
||||
{
|
||||
public static CameraEffects instance;
|
||||
Camera cam;
|
||||
|
||||
[SerializeField]private float stretchFactor = 0;
|
||||
[SerializeField]private bool isBeingStretched = false;
|
||||
[SerializeField]private float minFov = 5;
|
||||
[SerializeField]private float maxFov = 10;
|
||||
[SerializeField]private float fovSpeed = 10;
|
||||
[SerializeField] private float bounceDuration = 0.35f;
|
||||
[SerializeField] private float bounceElasticity = 0.7f;
|
||||
[SerializeField] private float bounceOvershoot = 0.2f;
|
||||
void Awake()
|
||||
{
|
||||
instance = this;
|
||||
cam = Camera.main;
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if(isBeingStretched){
|
||||
float targetFov = minFov + stretchFactor * (maxFov - minFov);
|
||||
cam.orthographicSize = Mathf.Lerp(cam.orthographicSize, targetFov, Time.deltaTime * fovSpeed);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetStretchFactor(float stretchFactor){
|
||||
this.stretchFactor = stretchFactor;
|
||||
isBeingStretched = true;
|
||||
}
|
||||
|
||||
public void SetUpsideDown(bool isUpsideDown){
|
||||
if(isUpsideDown){
|
||||
transform.rotation = Quaternion.Euler(0, 0, 180);
|
||||
transform.position = new Vector3(transform.position.x, Mathf.Abs(transform.position.y), transform.position.z);
|
||||
}else{
|
||||
transform.rotation = Quaternion.Euler(0, 0, 0);
|
||||
transform.position = new Vector3(transform.position.x, -Mathf.Abs(transform.position.y), transform.position.z);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void ReleaseStretchFactor(){
|
||||
stretchFactor = 0;
|
||||
isBeingStretched = false;
|
||||
cam.DOOrthoSize(minFov, bounceDuration)
|
||||
.SetEase(Ease.OutElastic, bounceElasticity, bounceOvershoot)
|
||||
.OnComplete(() => {
|
||||
stretchFactor = 0;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
fileFormatVersion: 2
|
||||
guid: 0c056bc55f8e80b408b5d45af7e7ac02
|
||||
@@ -1,57 +1,57 @@
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using UnityEngine.UI;
|
||||
|
||||
public class DevOptionItem : MonoBehaviour
|
||||
{
|
||||
public TMP_Text txtTitle;
|
||||
public TMP_InputField inputMin, inputMax, inputValue;
|
||||
public Slider slider;
|
||||
|
||||
public UnityAction<float> OnChanged;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
inputMin.onEndEdit.AddListener(OnEndEditMin);
|
||||
inputMax.onEndEdit.AddListener(OnEndEditMax);
|
||||
inputValue.onEndEdit.AddListener(OnEndEditValue);
|
||||
slider.onValueChanged.AddListener(OnValueChanged);
|
||||
}
|
||||
|
||||
public void Setup(string title, float min, float max, float value){
|
||||
txtTitle.text = title;
|
||||
inputMin.text = min.ToString();
|
||||
inputMax.text = max.ToString();
|
||||
inputValue.text = value.ToString();
|
||||
slider.minValue = min;
|
||||
slider.maxValue = max;
|
||||
slider.value = value;
|
||||
}
|
||||
|
||||
void OnEndEditMin(string value){
|
||||
float.TryParse(value, out float min);
|
||||
slider.minValue = min;
|
||||
}
|
||||
|
||||
void OnEndEditMax(string value){
|
||||
float.TryParse(value, out float max);
|
||||
slider.maxValue = max;
|
||||
}
|
||||
|
||||
void OnEndEditValue(string value){
|
||||
float parsedValue;
|
||||
if (float.TryParse(value, out parsedValue)) {
|
||||
slider.value = parsedValue;
|
||||
}
|
||||
}
|
||||
|
||||
void OnValueChanged(float value){
|
||||
inputValue.text = value.ToString();
|
||||
if (OnChanged != null) {
|
||||
OnChanged.Invoke(value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using UnityEngine.UI;
|
||||
|
||||
public class DevOptionItem : MonoBehaviour
|
||||
{
|
||||
public TMP_Text txtTitle;
|
||||
public TMP_InputField inputMin, inputMax, inputValue;
|
||||
public Slider slider;
|
||||
|
||||
public UnityAction<float> OnChanged;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
inputMin.onEndEdit.AddListener(OnEndEditMin);
|
||||
inputMax.onEndEdit.AddListener(OnEndEditMax);
|
||||
inputValue.onEndEdit.AddListener(OnEndEditValue);
|
||||
slider.onValueChanged.AddListener(OnValueChanged);
|
||||
}
|
||||
|
||||
public void Setup(string title, float min, float max, float value){
|
||||
txtTitle.text = title;
|
||||
inputMin.text = min.ToString();
|
||||
inputMax.text = max.ToString();
|
||||
inputValue.text = value.ToString();
|
||||
slider.minValue = min;
|
||||
slider.maxValue = max;
|
||||
slider.value = value;
|
||||
}
|
||||
|
||||
void OnEndEditMin(string value){
|
||||
float.TryParse(value, out float min);
|
||||
slider.minValue = min;
|
||||
}
|
||||
|
||||
void OnEndEditMax(string value){
|
||||
float.TryParse(value, out float max);
|
||||
slider.maxValue = max;
|
||||
}
|
||||
|
||||
void OnEndEditValue(string value){
|
||||
float parsedValue;
|
||||
if (float.TryParse(value, out parsedValue)) {
|
||||
slider.value = parsedValue;
|
||||
}
|
||||
}
|
||||
|
||||
void OnValueChanged(float value){
|
||||
inputValue.text = value.ToString();
|
||||
if (OnChanged != null) {
|
||||
OnChanged.Invoke(value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
fileFormatVersion: 2
|
||||
guid: fc8c3535b2249f247abdb4f3964eca7e
|
||||
@@ -1,93 +1,93 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
public class DevSettingsPanel : MonoBehaviour
|
||||
{
|
||||
public static Action OnChanged;
|
||||
public static DevSettingsPanel instance;
|
||||
|
||||
public Transform contentParent;
|
||||
public DevOptionItem devOptionItemPrefab;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
instance = this;
|
||||
}
|
||||
|
||||
|
||||
void Start()
|
||||
{
|
||||
StartCoroutine(CoroutinePopulate());
|
||||
}
|
||||
|
||||
IEnumerator CoroutinePopulate()
|
||||
{
|
||||
yield return new WaitForSeconds(1f); //Wait for the game manager to be initialized
|
||||
Populate();
|
||||
}
|
||||
|
||||
void Populate()
|
||||
{
|
||||
DevOptionItem puckForce = Instantiate(devOptionItemPrefab, contentParent);
|
||||
puckForce.Setup("Puck Force", 40, 120, GameManager.instance.puckForce);
|
||||
puckForce.OnChanged += OnPuckForceChanged;
|
||||
|
||||
DevOptionItem puckDragClampMax = Instantiate(devOptionItemPrefab, contentParent);
|
||||
puckDragClampMax.Setup("Puck Launch Max Length", 1, 10, GameManager.instance.puckDragClampMax);
|
||||
puckDragClampMax.OnChanged += OnPuckDragClampMaxChanged;
|
||||
|
||||
DevOptionItem puckDragClampMin = Instantiate(devOptionItemPrefab, contentParent);
|
||||
puckDragClampMin.Setup("Puck Launch Min Length", 0, 2, GameManager.instance.puckDragMinClamp);
|
||||
puckDragClampMin.OnChanged += OnPuckDragClampMinChanged;
|
||||
|
||||
DevOptionItem ballMoveTime = Instantiate(devOptionItemPrefab, contentParent);
|
||||
ballMoveTime.Setup("Ball Drag", 0f, 10, GameManager.instance.ballMoveTime);
|
||||
ballMoveTime.OnChanged += OnBallMoveTimeChanged;
|
||||
|
||||
DevOptionItem puckMass = Instantiate(devOptionItemPrefab, contentParent);
|
||||
puckMass.Setup("Puck Mass", 1f, 50, GameManager.instance.puckMass);
|
||||
puckMass.OnChanged += OnPuckMassChanged;
|
||||
|
||||
DevOptionItem puckDrag = Instantiate(devOptionItemPrefab, contentParent);
|
||||
puckDrag.Setup("Puck Drag", 0f, 10, GameManager.instance.puckDrag);
|
||||
puckDrag.OnChanged += OnPuckDragChanged;
|
||||
}
|
||||
|
||||
|
||||
void OnPuckForceChanged(float value)
|
||||
{
|
||||
GameManager.instance.puckForce = value;
|
||||
OnChanged?.Invoke();
|
||||
}
|
||||
|
||||
void OnPuckDragClampMaxChanged(float value)
|
||||
{
|
||||
GameManager.instance.puckDragClampMax = value;
|
||||
OnChanged?.Invoke();
|
||||
}
|
||||
|
||||
void OnPuckDragClampMinChanged(float value)
|
||||
{
|
||||
GameManager.instance.puckDragMinClamp = value;
|
||||
OnChanged?.Invoke();
|
||||
}
|
||||
|
||||
void OnBallMoveTimeChanged(float value)
|
||||
{
|
||||
GameManager.instance.ballMoveTime = value;
|
||||
OnChanged?.Invoke();
|
||||
}
|
||||
|
||||
void OnPuckMassChanged(float value)
|
||||
{
|
||||
GameManager.instance.puckMass = value;
|
||||
OnChanged?.Invoke();
|
||||
}
|
||||
|
||||
void OnPuckDragChanged(float value)
|
||||
{
|
||||
GameManager.instance.puckDrag = value;
|
||||
OnChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
public class DevSettingsPanel : MonoBehaviour
|
||||
{
|
||||
public static Action OnChanged;
|
||||
public static DevSettingsPanel instance;
|
||||
|
||||
public Transform contentParent;
|
||||
public DevOptionItem devOptionItemPrefab;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
instance = this;
|
||||
}
|
||||
|
||||
|
||||
void Start()
|
||||
{
|
||||
StartCoroutine(CoroutinePopulate());
|
||||
}
|
||||
|
||||
IEnumerator CoroutinePopulate()
|
||||
{
|
||||
yield return new WaitForSeconds(1f); //Wait for the game manager to be initialized
|
||||
Populate();
|
||||
}
|
||||
|
||||
void Populate()
|
||||
{
|
||||
DevOptionItem puckForce = Instantiate(devOptionItemPrefab, contentParent);
|
||||
puckForce.Setup("Puck Force", 40, 120, GameManager.instance.puckForce);
|
||||
puckForce.OnChanged += OnPuckForceChanged;
|
||||
|
||||
DevOptionItem puckDragClampMax = Instantiate(devOptionItemPrefab, contentParent);
|
||||
puckDragClampMax.Setup("Puck Launch Max Length", 1, 10, GameManager.instance.puckDragClampMax);
|
||||
puckDragClampMax.OnChanged += OnPuckDragClampMaxChanged;
|
||||
|
||||
DevOptionItem puckDragClampMin = Instantiate(devOptionItemPrefab, contentParent);
|
||||
puckDragClampMin.Setup("Puck Launch Min Length", 0, 2, GameManager.instance.puckDragMinClamp);
|
||||
puckDragClampMin.OnChanged += OnPuckDragClampMinChanged;
|
||||
|
||||
DevOptionItem ballMoveTime = Instantiate(devOptionItemPrefab, contentParent);
|
||||
ballMoveTime.Setup("Ball Drag", 0f, 10, GameManager.instance.ballMoveTime);
|
||||
ballMoveTime.OnChanged += OnBallMoveTimeChanged;
|
||||
|
||||
DevOptionItem puckMass = Instantiate(devOptionItemPrefab, contentParent);
|
||||
puckMass.Setup("Puck Mass", 1f, 50, GameManager.instance.puckMass);
|
||||
puckMass.OnChanged += OnPuckMassChanged;
|
||||
|
||||
DevOptionItem puckDrag = Instantiate(devOptionItemPrefab, contentParent);
|
||||
puckDrag.Setup("Puck Drag", 0f, 10, GameManager.instance.puckDrag);
|
||||
puckDrag.OnChanged += OnPuckDragChanged;
|
||||
}
|
||||
|
||||
|
||||
void OnPuckForceChanged(float value)
|
||||
{
|
||||
GameManager.instance.puckForce = value;
|
||||
OnChanged?.Invoke();
|
||||
}
|
||||
|
||||
void OnPuckDragClampMaxChanged(float value)
|
||||
{
|
||||
GameManager.instance.puckDragClampMax = value;
|
||||
OnChanged?.Invoke();
|
||||
}
|
||||
|
||||
void OnPuckDragClampMinChanged(float value)
|
||||
{
|
||||
GameManager.instance.puckDragMinClamp = value;
|
||||
OnChanged?.Invoke();
|
||||
}
|
||||
|
||||
void OnBallMoveTimeChanged(float value)
|
||||
{
|
||||
GameManager.instance.ballMoveTime = value;
|
||||
OnChanged?.Invoke();
|
||||
}
|
||||
|
||||
void OnPuckMassChanged(float value)
|
||||
{
|
||||
GameManager.instance.puckMass = value;
|
||||
OnChanged?.Invoke();
|
||||
}
|
||||
|
||||
void OnPuckDragChanged(float value)
|
||||
{
|
||||
GameManager.instance.puckDrag = value;
|
||||
OnChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
fileFormatVersion: 2
|
||||
guid: 42df6f977bdfdf54092d0442ed988ef0
|
||||
+380
-380
@@ -1,380 +1,380 @@
|
||||
|
||||
using System.Collections;
|
||||
using Mirror;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.Serialization;
|
||||
using UnityEngine.UI;
|
||||
using DG.Tweening;
|
||||
public class GameCanvas : MonoBehaviour
|
||||
{
|
||||
public static GameCanvas instance;
|
||||
const int OpponentWaitTimeoutSeconds = 15;
|
||||
static readonly WaitForSeconds WaitOneSecond = new WaitForSeconds(1f);
|
||||
int opponentWaitSecondsElapsed;
|
||||
bool hasShownOpponentTimeout;
|
||||
Coroutine opponentWaitRoutine;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
instance = this;
|
||||
|
||||
ShowWaitingForOpponentPanel();
|
||||
HideEmotesPanel();
|
||||
HideAllPostGamePanels();
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
StopOpponentWaitRoutine();
|
||||
}
|
||||
|
||||
public EventTrigger gameInputPanel;
|
||||
|
||||
public Image turnTimerSliderTransform;
|
||||
public Color redColor = Color.red;
|
||||
public Color blueColor = Color.blue;
|
||||
public TMP_Text blueTurnTimer;
|
||||
[FormerlySerializedAs("txtTurnTimer")] public TMP_Text redTurnTimer;
|
||||
public TMP_Text redName,blueName;
|
||||
public TMP_Text txtRcPrize;
|
||||
public TMP_Text txtWhosTurn;
|
||||
public Image blueTurnTimerRound;
|
||||
[FormerlySerializedAs("turnTimerRound")] public Image redTurnTimerRound;
|
||||
|
||||
public GameObject waitingForOpponentPanel;
|
||||
public GameObject timeoutForOpponentPanel;
|
||||
public Button btnLeaveGame;
|
||||
public Button btnLeavePrematureGame;
|
||||
|
||||
[Header("Emotes")]
|
||||
public Button btnEmote;
|
||||
public Transform emotesPanel;
|
||||
public Transform textEmotesParent;
|
||||
public Transform emojiEmotesParent;
|
||||
public GameObject textEmotePrefab;
|
||||
public Transform EmoteSpawnRed, EmoteSpawnBlue;
|
||||
public Vector3 textEmoteMovement = new Vector3(0,100,0);
|
||||
public Vector3 emojiEmoteMovement = new Vector3(0,100,0);
|
||||
public float textEmoteDuration = 5f;
|
||||
public float emojiEmoteDuration = 5f;
|
||||
bool isShowingEmotes=> emotesPanel.gameObject.activeSelf;
|
||||
|
||||
public RectTransform kickWarning;
|
||||
public GameObject[] postGamePanels;
|
||||
public TMP_Text txtMatchNumber;
|
||||
const float PostGamePanelDuration = 3f;
|
||||
|
||||
|
||||
void Start(){
|
||||
if (btnLeaveGame != null)
|
||||
btnLeaveGame.onClick.AddListener(OnLeaveGamePressed);
|
||||
|
||||
if (btnLeavePrematureGame == null && timeoutForOpponentPanel != null)
|
||||
btnLeavePrematureGame = timeoutForOpponentPanel.GetComponentInChildren<Button>(true);
|
||||
|
||||
if (btnLeavePrematureGame != null)
|
||||
btnLeavePrematureGame.onClick.AddListener(ConfirmLeaveMatch);
|
||||
|
||||
if(btnEmote != null)
|
||||
btnEmote.onClick.AddListener(OnEmotePressed);
|
||||
|
||||
if(textEmotesParent != null){
|
||||
for(int i=0; i<textEmotesParent.childCount; i++){
|
||||
string txtName = textEmotesParent.GetChild(i).GetComponentInChildren<TMP_Text>().text;
|
||||
textEmotesParent.GetChild(i).GetComponent<Button>().onClick.AddListener(() => OnEmoteTextPressed(txtName));
|
||||
}
|
||||
}
|
||||
|
||||
if(emojiEmotesParent != null){
|
||||
for(int i=0; i<emojiEmotesParent.childCount; i++){
|
||||
int index = i;
|
||||
emojiEmotesParent.GetChild(i).GetComponent<Button>().onClick.AddListener(() => OnEmojiPressed(index));
|
||||
|
||||
}
|
||||
}
|
||||
try{
|
||||
redName.text = GameManager.RedPlayer.Name;
|
||||
blueName.text = GameManager.BluePlayer.Name;
|
||||
}catch{
|
||||
Logger.Log("Failed to fetch red and blue names, isServer?");
|
||||
}
|
||||
|
||||
if (txtRcPrize != null)
|
||||
txtRcPrize.text = CoinHelper.FormatString(GameManager.MatchRcPrizeCoins) + " RC";
|
||||
|
||||
if (txtMatchNumber != null && GameManager.DedicatedMatchId > 0)
|
||||
txtMatchNumber.text = "M" + GameManager.DedicatedMatchId;
|
||||
}
|
||||
|
||||
void OnEmoteTextPressed(string txtName){
|
||||
Debug.Log("OnEmoteTextPressed: " + txtName);
|
||||
HideEmotesPanel();
|
||||
NetPlayer.localPlayer.SendTextEmote(txtName);
|
||||
}
|
||||
|
||||
void OnEmojiPressed(int id){
|
||||
Debug.Log("OnEmojiPressed: " + id);
|
||||
HideEmotesPanel();
|
||||
NetPlayer.localPlayer.SendEmojiEmote(id);
|
||||
}
|
||||
|
||||
public void ShowTextEmote(string txtName, Team team){
|
||||
Debug.Log("Showing text emote: " + txtName +" from " + team);
|
||||
GameObject textEmote = Instantiate(textEmotePrefab, team == Team.Red ? EmoteSpawnRed : EmoteSpawnBlue);
|
||||
Destroy(textEmote.GetComponent<Button>());
|
||||
textEmote.transform.localPosition = Vector3.zero;
|
||||
textEmote.GetComponentInChildren<TMP_Text>().text = txtName;
|
||||
textEmote.transform.localScale = new Vector3(0, 0, 0);
|
||||
StartCoroutine(TextEmoteRoutine(textEmote));
|
||||
}
|
||||
|
||||
IEnumerator TextEmoteRoutine(GameObject textEmote){
|
||||
textEmote.transform.DOScale(1, 0.5f).SetEase(Ease.OutBack);
|
||||
textEmote.transform.DOMove(textEmote.transform.position + textEmoteMovement, textEmoteDuration).SetEase(Ease.OutBack);
|
||||
yield return new WaitForSeconds(textEmoteDuration);
|
||||
textEmote.transform.DOScale(0, 0.5f).SetEase(Ease.InBack);
|
||||
yield return new WaitForSeconds(0.5f);
|
||||
Destroy(textEmote);
|
||||
}
|
||||
|
||||
public void ShowEmojiEmote(int id, Team team){
|
||||
Debug.Log("Showing emoji emote: " + id);
|
||||
GameObject emojiEmote = Instantiate(emojiEmotesParent.GetChild(id).gameObject, team == Team.Red ? EmoteSpawnRed : EmoteSpawnBlue);
|
||||
Destroy(emojiEmote.GetComponent<Button>());
|
||||
emojiEmote.transform.localPosition = Vector3.zero;
|
||||
emojiEmote.transform.localScale = new Vector3(0, 0, 0);
|
||||
StartCoroutine(EmojiEmoteRoutine(emojiEmote));
|
||||
}
|
||||
|
||||
IEnumerator EmojiEmoteRoutine(GameObject emojiEmote){
|
||||
emojiEmote.transform.DOScale(1, 0.5f).SetEase(Ease.OutBack);
|
||||
emojiEmote.transform.DOMove(emojiEmote.transform.position + emojiEmoteMovement, emojiEmoteDuration).SetEase(Ease.OutBack);
|
||||
yield return new WaitForSeconds(emojiEmoteDuration);
|
||||
emojiEmote.transform.DOScale(0, 0.5f).SetEase(Ease.InBack);
|
||||
yield return new WaitForSeconds(0.5f);
|
||||
Destroy(emojiEmote);
|
||||
}
|
||||
|
||||
void OnEmotePressed()
|
||||
{
|
||||
if(isShowingEmotes){
|
||||
HideEmotesPanel();
|
||||
}else{
|
||||
ShowEmotesPanel();
|
||||
}
|
||||
}
|
||||
|
||||
public void ShowEmotesPanel(){
|
||||
emotesPanel.gameObject.SetActive(true);
|
||||
emotesPanel.localScale = new Vector3(0, 0, 0);
|
||||
emotesPanel.DOScale(1, 0.2f).SetEase(Ease.OutBack);
|
||||
}
|
||||
|
||||
public void HideEmotesPanel(){
|
||||
emotesPanel.DOScale(0, 0.25f).SetEase(Ease.InBack).OnComplete(() => {
|
||||
emotesPanel.gameObject.SetActive(false);
|
||||
});
|
||||
}
|
||||
|
||||
void OnLeaveGamePressed()
|
||||
{
|
||||
if (MessageBoxDialog.IsAvailable)
|
||||
{
|
||||
MessageBoxDialog.Show(
|
||||
"Forfeit match?",
|
||||
"Leaving will result in a loss, making opponent the winner. Are you sure?",
|
||||
confirmed =>
|
||||
{
|
||||
if (confirmed)
|
||||
ConfirmLeaveMatch();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
ConfirmLeaveMatch();
|
||||
}
|
||||
|
||||
void ConfirmLeaveMatch()
|
||||
{
|
||||
if (CupidLobby.instance != null)
|
||||
CupidLobby.instance.Cancel();
|
||||
LoginManager.ClearMatchYourTeam();
|
||||
|
||||
if (GameManager.instance != null)
|
||||
{
|
||||
GameManager.instance.Leave();
|
||||
return;
|
||||
}
|
||||
|
||||
NetManager.StopNetworkingForSceneChange();
|
||||
LevelLoadManager.LoadLevel("MainMenu");
|
||||
}
|
||||
|
||||
public void ShowWaitingForOpponentPanel(){
|
||||
waitingForOpponentPanel.SetActive(true);
|
||||
ResetOpponentTimeoutState();
|
||||
StopOpponentWaitRoutine();
|
||||
opponentWaitRoutine = StartCoroutine(OpponentWaitingTimeoutRoutine());
|
||||
}
|
||||
public void HideWaitingForOpponentPanel(){
|
||||
StopOpponentWaitRoutine();
|
||||
waitingForOpponentPanel.SetActive(false);
|
||||
timeoutForOpponentPanel.SetActive(false);
|
||||
ResetOpponentTimeoutState();
|
||||
}
|
||||
|
||||
void StopOpponentWaitRoutine()
|
||||
{
|
||||
if (opponentWaitRoutine != null)
|
||||
{
|
||||
StopCoroutine(opponentWaitRoutine);
|
||||
opponentWaitRoutine = null;
|
||||
}
|
||||
}
|
||||
|
||||
void Update(){
|
||||
UpdateTurnTimer();
|
||||
}
|
||||
Team prevTeam = Team.Red;
|
||||
|
||||
bool init=false;
|
||||
void UpdateTurnTimer(){
|
||||
if(NetPlayer.localPlayer == null){return;}
|
||||
if(!init){
|
||||
init=true;
|
||||
SwitchTeams();
|
||||
}
|
||||
|
||||
float turnTimerFillAmount = 1 - (GameManager.instance.turnTimerCounter / GameManager.instance.turnTimer);
|
||||
if(prevTeam != GameManager.instance.SelectedTeam){
|
||||
SwitchTeams();
|
||||
prevTeam = GameManager.instance.SelectedTeam;
|
||||
}
|
||||
|
||||
turnTimerSliderTransform.transform.localScale = new Vector3(turnTimerFillAmount, 1, 1);
|
||||
string timerString = (GameManager.instance.turnTimer - GameManager.instance.turnTimerCounter).ToString("F0");
|
||||
if (GameManager.instance.SelectedTeam == Team.Red)
|
||||
{
|
||||
if (redTurnTimer != null) redTurnTimer.text = ":"+timerString;
|
||||
if (blueTurnTimer != null) blueTurnTimer.text = string.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (blueTurnTimer != null) blueTurnTimer.text = ":"+timerString;
|
||||
if (redTurnTimer != null) redTurnTimer.text = string.Empty;
|
||||
}
|
||||
if (GameManager.instance.SelectedTeam == Team.Red)
|
||||
{
|
||||
if (redTurnTimerRound != null) redTurnTimerRound.fillAmount = turnTimerFillAmount;
|
||||
if (blueTurnTimerRound != null) blueTurnTimerRound.fillAmount = 0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (blueTurnTimerRound != null) blueTurnTimerRound.fillAmount = turnTimerFillAmount;
|
||||
if (redTurnTimerRound != null) redTurnTimerRound.fillAmount = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
void SwitchTeams(){
|
||||
bool isMyTurn = GameManager.instance.SelectedTeam == NetPlayer.localPlayer.myTeam;
|
||||
|
||||
Color newColor = GameManager.instance.SelectedTeam == Team.Red ? redColor : blueColor;
|
||||
turnTimerSliderTransform.DOColor(newColor, 0.5f);
|
||||
|
||||
if (txtWhosTurn != null)
|
||||
{
|
||||
txtWhosTurn.DOFade(0, 0.25f).OnComplete(() => {
|
||||
txtWhosTurn.text = isMyTurn ? "Your Turn" : "Opponent's Turn";
|
||||
txtWhosTurn.DOFade(1, 0.25f);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator OpponentWaitingTimeoutRoutine()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
yield return WaitOneSecond;
|
||||
|
||||
if (waitingForOpponentPanel == null || timeoutForOpponentPanel == null)
|
||||
yield break;
|
||||
|
||||
if (!waitingForOpponentPanel.activeSelf)
|
||||
{
|
||||
timeoutForOpponentPanel.SetActive(false);
|
||||
ResetOpponentTimeoutState();
|
||||
yield break;
|
||||
}
|
||||
|
||||
bool isSoloPlayer = FindObjectsByType<NetPlayer>(FindObjectsSortMode.None).Length <= 1;
|
||||
if (!isSoloPlayer)
|
||||
{
|
||||
timeoutForOpponentPanel.SetActive(false);
|
||||
ResetOpponentTimeoutState();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hasShownOpponentTimeout)
|
||||
continue;
|
||||
|
||||
opponentWaitSecondsElapsed++;
|
||||
if (opponentWaitSecondsElapsed >= OpponentWaitTimeoutSeconds)
|
||||
{
|
||||
timeoutForOpponentPanel.transform.localScale = new Vector3(1, 0, 1);
|
||||
timeoutForOpponentPanel.transform.DOScaleY(1,0.1f).SetEase(Ease.InOutBounce);
|
||||
timeoutForOpponentPanel.SetActive(true);
|
||||
hasShownOpponentTimeout = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ResetOpponentTimeoutState()
|
||||
{
|
||||
opponentWaitSecondsElapsed = 0;
|
||||
hasShownOpponentTimeout = false;
|
||||
timeoutForOpponentPanel.SetActive(false);
|
||||
}
|
||||
|
||||
|
||||
public void ShowKickWarning(){
|
||||
kickWarning.transform.localScale = new Vector3(0, 0, 0);
|
||||
kickWarning.transform.DOScale(1, 0.2f).SetEase(Ease.OutBack);
|
||||
kickWarning.gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
public void HideKickWarning(){
|
||||
kickWarning.transform.DOScale(0, 0.2f).SetEase(Ease.InBack).OnComplete(() => {
|
||||
kickWarning.gameObject.SetActive(false);
|
||||
});
|
||||
}
|
||||
|
||||
public IEnumerator ShowRandomPostGamePanelThenHide()
|
||||
{
|
||||
HideAllPostGamePanels();
|
||||
if (postGamePanels == null || postGamePanels.Length == 0)
|
||||
yield break;
|
||||
|
||||
GameObject chosen = postGamePanels[Random.Range(0, postGamePanels.Length)];
|
||||
if (chosen == null)
|
||||
yield break;
|
||||
|
||||
chosen.SetActive(true);
|
||||
yield return new WaitForSeconds(PostGamePanelDuration);
|
||||
chosen.SetActive(false);
|
||||
}
|
||||
|
||||
void HideAllPostGamePanels()
|
||||
{
|
||||
if (postGamePanels == null)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < postGamePanels.Length; i++)
|
||||
{
|
||||
if (postGamePanels[i] != null)
|
||||
postGamePanels[i].SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
using System.Collections;
|
||||
using Mirror;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.Serialization;
|
||||
using UnityEngine.UI;
|
||||
using DG.Tweening;
|
||||
public class GameCanvas : MonoBehaviour
|
||||
{
|
||||
public static GameCanvas instance;
|
||||
const int OpponentWaitTimeoutSeconds = 15;
|
||||
static readonly WaitForSeconds WaitOneSecond = new WaitForSeconds(1f);
|
||||
int opponentWaitSecondsElapsed;
|
||||
bool hasShownOpponentTimeout;
|
||||
Coroutine opponentWaitRoutine;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
instance = this;
|
||||
|
||||
ShowWaitingForOpponentPanel();
|
||||
HideEmotesPanel();
|
||||
HideAllPostGamePanels();
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
StopOpponentWaitRoutine();
|
||||
}
|
||||
|
||||
public EventTrigger gameInputPanel;
|
||||
|
||||
public Image turnTimerSliderTransform;
|
||||
public Color redColor = Color.red;
|
||||
public Color blueColor = Color.blue;
|
||||
public TMP_Text blueTurnTimer;
|
||||
[FormerlySerializedAs("txtTurnTimer")] public TMP_Text redTurnTimer;
|
||||
public TMP_Text redName,blueName;
|
||||
public TMP_Text txtRcPrize;
|
||||
public TMP_Text txtWhosTurn;
|
||||
public Image blueTurnTimerRound;
|
||||
[FormerlySerializedAs("turnTimerRound")] public Image redTurnTimerRound;
|
||||
|
||||
public GameObject waitingForOpponentPanel;
|
||||
public GameObject timeoutForOpponentPanel;
|
||||
public Button btnLeaveGame;
|
||||
public Button btnLeavePrematureGame;
|
||||
|
||||
[Header("Emotes")]
|
||||
public Button btnEmote;
|
||||
public Transform emotesPanel;
|
||||
public Transform textEmotesParent;
|
||||
public Transform emojiEmotesParent;
|
||||
public GameObject textEmotePrefab;
|
||||
public Transform EmoteSpawnRed, EmoteSpawnBlue;
|
||||
public Vector3 textEmoteMovement = new Vector3(0,100,0);
|
||||
public Vector3 emojiEmoteMovement = new Vector3(0,100,0);
|
||||
public float textEmoteDuration = 5f;
|
||||
public float emojiEmoteDuration = 5f;
|
||||
bool isShowingEmotes=> emotesPanel.gameObject.activeSelf;
|
||||
|
||||
public RectTransform kickWarning;
|
||||
public GameObject[] postGamePanels;
|
||||
public TMP_Text txtMatchNumber;
|
||||
const float PostGamePanelDuration = 3f;
|
||||
|
||||
|
||||
void Start(){
|
||||
if (btnLeaveGame != null)
|
||||
btnLeaveGame.onClick.AddListener(OnLeaveGamePressed);
|
||||
|
||||
if (btnLeavePrematureGame == null && timeoutForOpponentPanel != null)
|
||||
btnLeavePrematureGame = timeoutForOpponentPanel.GetComponentInChildren<Button>(true);
|
||||
|
||||
if (btnLeavePrematureGame != null)
|
||||
btnLeavePrematureGame.onClick.AddListener(ConfirmLeaveMatch);
|
||||
|
||||
if(btnEmote != null)
|
||||
btnEmote.onClick.AddListener(OnEmotePressed);
|
||||
|
||||
if(textEmotesParent != null){
|
||||
for(int i=0; i<textEmotesParent.childCount; i++){
|
||||
string txtName = textEmotesParent.GetChild(i).GetComponentInChildren<TMP_Text>().text;
|
||||
textEmotesParent.GetChild(i).GetComponent<Button>().onClick.AddListener(() => OnEmoteTextPressed(txtName));
|
||||
}
|
||||
}
|
||||
|
||||
if(emojiEmotesParent != null){
|
||||
for(int i=0; i<emojiEmotesParent.childCount; i++){
|
||||
int index = i;
|
||||
emojiEmotesParent.GetChild(i).GetComponent<Button>().onClick.AddListener(() => OnEmojiPressed(index));
|
||||
|
||||
}
|
||||
}
|
||||
try{
|
||||
redName.text = GameManager.RedPlayer.Name;
|
||||
blueName.text = GameManager.BluePlayer.Name;
|
||||
}catch{
|
||||
Logger.Log("Failed to fetch red and blue names, isServer?");
|
||||
}
|
||||
|
||||
if (txtRcPrize != null)
|
||||
txtRcPrize.text = CoinHelper.FormatString(GameManager.MatchRcPrizeCoins) + " RC";
|
||||
|
||||
if (txtMatchNumber != null && GameManager.DedicatedMatchId > 0)
|
||||
txtMatchNumber.text = "M" + GameManager.DedicatedMatchId;
|
||||
}
|
||||
|
||||
void OnEmoteTextPressed(string txtName){
|
||||
Debug.Log("OnEmoteTextPressed: " + txtName);
|
||||
HideEmotesPanel();
|
||||
NetPlayer.localPlayer.SendTextEmote(txtName);
|
||||
}
|
||||
|
||||
void OnEmojiPressed(int id){
|
||||
Debug.Log("OnEmojiPressed: " + id);
|
||||
HideEmotesPanel();
|
||||
NetPlayer.localPlayer.SendEmojiEmote(id);
|
||||
}
|
||||
|
||||
public void ShowTextEmote(string txtName, Team team){
|
||||
Debug.Log("Showing text emote: " + txtName +" from " + team);
|
||||
GameObject textEmote = Instantiate(textEmotePrefab, team == Team.Red ? EmoteSpawnRed : EmoteSpawnBlue);
|
||||
Destroy(textEmote.GetComponent<Button>());
|
||||
textEmote.transform.localPosition = Vector3.zero;
|
||||
textEmote.GetComponentInChildren<TMP_Text>().text = txtName;
|
||||
textEmote.transform.localScale = new Vector3(0, 0, 0);
|
||||
StartCoroutine(TextEmoteRoutine(textEmote));
|
||||
}
|
||||
|
||||
IEnumerator TextEmoteRoutine(GameObject textEmote){
|
||||
textEmote.transform.DOScale(1, 0.5f).SetEase(Ease.OutBack);
|
||||
textEmote.transform.DOMove(textEmote.transform.position + textEmoteMovement, textEmoteDuration).SetEase(Ease.OutBack);
|
||||
yield return new WaitForSeconds(textEmoteDuration);
|
||||
textEmote.transform.DOScale(0, 0.5f).SetEase(Ease.InBack);
|
||||
yield return new WaitForSeconds(0.5f);
|
||||
Destroy(textEmote);
|
||||
}
|
||||
|
||||
public void ShowEmojiEmote(int id, Team team){
|
||||
Debug.Log("Showing emoji emote: " + id);
|
||||
GameObject emojiEmote = Instantiate(emojiEmotesParent.GetChild(id).gameObject, team == Team.Red ? EmoteSpawnRed : EmoteSpawnBlue);
|
||||
Destroy(emojiEmote.GetComponent<Button>());
|
||||
emojiEmote.transform.localPosition = Vector3.zero;
|
||||
emojiEmote.transform.localScale = new Vector3(0, 0, 0);
|
||||
StartCoroutine(EmojiEmoteRoutine(emojiEmote));
|
||||
}
|
||||
|
||||
IEnumerator EmojiEmoteRoutine(GameObject emojiEmote){
|
||||
emojiEmote.transform.DOScale(1, 0.5f).SetEase(Ease.OutBack);
|
||||
emojiEmote.transform.DOMove(emojiEmote.transform.position + emojiEmoteMovement, emojiEmoteDuration).SetEase(Ease.OutBack);
|
||||
yield return new WaitForSeconds(emojiEmoteDuration);
|
||||
emojiEmote.transform.DOScale(0, 0.5f).SetEase(Ease.InBack);
|
||||
yield return new WaitForSeconds(0.5f);
|
||||
Destroy(emojiEmote);
|
||||
}
|
||||
|
||||
void OnEmotePressed()
|
||||
{
|
||||
if(isShowingEmotes){
|
||||
HideEmotesPanel();
|
||||
}else{
|
||||
ShowEmotesPanel();
|
||||
}
|
||||
}
|
||||
|
||||
public void ShowEmotesPanel(){
|
||||
emotesPanel.gameObject.SetActive(true);
|
||||
emotesPanel.localScale = new Vector3(0, 0, 0);
|
||||
emotesPanel.DOScale(1, 0.2f).SetEase(Ease.OutBack);
|
||||
}
|
||||
|
||||
public void HideEmotesPanel(){
|
||||
emotesPanel.DOScale(0, 0.25f).SetEase(Ease.InBack).OnComplete(() => {
|
||||
emotesPanel.gameObject.SetActive(false);
|
||||
});
|
||||
}
|
||||
|
||||
void OnLeaveGamePressed()
|
||||
{
|
||||
if (MessageBoxDialog.IsAvailable)
|
||||
{
|
||||
MessageBoxDialog.Show(
|
||||
"Forfeit match?",
|
||||
"Leaving will result in a loss, making opponent the winner. Are you sure?",
|
||||
confirmed =>
|
||||
{
|
||||
if (confirmed)
|
||||
ConfirmLeaveMatch();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
ConfirmLeaveMatch();
|
||||
}
|
||||
|
||||
void ConfirmLeaveMatch()
|
||||
{
|
||||
if (CupidLobby.instance != null)
|
||||
CupidLobby.instance.Cancel();
|
||||
LoginManager.ClearMatchYourTeam();
|
||||
|
||||
if (GameManager.instance != null)
|
||||
{
|
||||
GameManager.instance.Leave();
|
||||
return;
|
||||
}
|
||||
|
||||
NetManager.StopNetworkingForSceneChange();
|
||||
LevelLoadManager.LoadLevel("MainMenu");
|
||||
}
|
||||
|
||||
public void ShowWaitingForOpponentPanel(){
|
||||
waitingForOpponentPanel.SetActive(true);
|
||||
ResetOpponentTimeoutState();
|
||||
StopOpponentWaitRoutine();
|
||||
opponentWaitRoutine = StartCoroutine(OpponentWaitingTimeoutRoutine());
|
||||
}
|
||||
public void HideWaitingForOpponentPanel(){
|
||||
StopOpponentWaitRoutine();
|
||||
waitingForOpponentPanel.SetActive(false);
|
||||
timeoutForOpponentPanel.SetActive(false);
|
||||
ResetOpponentTimeoutState();
|
||||
}
|
||||
|
||||
void StopOpponentWaitRoutine()
|
||||
{
|
||||
if (opponentWaitRoutine != null)
|
||||
{
|
||||
StopCoroutine(opponentWaitRoutine);
|
||||
opponentWaitRoutine = null;
|
||||
}
|
||||
}
|
||||
|
||||
void Update(){
|
||||
UpdateTurnTimer();
|
||||
}
|
||||
Team prevTeam = Team.Red;
|
||||
|
||||
bool init=false;
|
||||
void UpdateTurnTimer(){
|
||||
if(NetPlayer.localPlayer == null){return;}
|
||||
if(!init){
|
||||
init=true;
|
||||
SwitchTeams();
|
||||
}
|
||||
|
||||
float turnTimerFillAmount = 1 - (GameManager.instance.turnTimerCounter / GameManager.instance.turnTimer);
|
||||
if(prevTeam != GameManager.instance.SelectedTeam){
|
||||
SwitchTeams();
|
||||
prevTeam = GameManager.instance.SelectedTeam;
|
||||
}
|
||||
|
||||
turnTimerSliderTransform.transform.localScale = new Vector3(turnTimerFillAmount, 1, 1);
|
||||
string timerString = (GameManager.instance.turnTimer - GameManager.instance.turnTimerCounter).ToString("F0");
|
||||
if (GameManager.instance.SelectedTeam == Team.Red)
|
||||
{
|
||||
if (redTurnTimer != null) redTurnTimer.text = ":"+timerString;
|
||||
if (blueTurnTimer != null) blueTurnTimer.text = string.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (blueTurnTimer != null) blueTurnTimer.text = ":"+timerString;
|
||||
if (redTurnTimer != null) redTurnTimer.text = string.Empty;
|
||||
}
|
||||
if (GameManager.instance.SelectedTeam == Team.Red)
|
||||
{
|
||||
if (redTurnTimerRound != null) redTurnTimerRound.fillAmount = turnTimerFillAmount;
|
||||
if (blueTurnTimerRound != null) blueTurnTimerRound.fillAmount = 0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (blueTurnTimerRound != null) blueTurnTimerRound.fillAmount = turnTimerFillAmount;
|
||||
if (redTurnTimerRound != null) redTurnTimerRound.fillAmount = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
void SwitchTeams(){
|
||||
bool isMyTurn = GameManager.instance.SelectedTeam == NetPlayer.localPlayer.myTeam;
|
||||
|
||||
Color newColor = GameManager.instance.SelectedTeam == Team.Red ? redColor : blueColor;
|
||||
turnTimerSliderTransform.DOColor(newColor, 0.5f);
|
||||
|
||||
if (txtWhosTurn != null)
|
||||
{
|
||||
txtWhosTurn.DOFade(0, 0.25f).OnComplete(() => {
|
||||
txtWhosTurn.text = isMyTurn ? "Your Turn" : "Opponent's Turn";
|
||||
txtWhosTurn.DOFade(1, 0.25f);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator OpponentWaitingTimeoutRoutine()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
yield return WaitOneSecond;
|
||||
|
||||
if (waitingForOpponentPanel == null || timeoutForOpponentPanel == null)
|
||||
yield break;
|
||||
|
||||
if (!waitingForOpponentPanel.activeSelf)
|
||||
{
|
||||
timeoutForOpponentPanel.SetActive(false);
|
||||
ResetOpponentTimeoutState();
|
||||
yield break;
|
||||
}
|
||||
|
||||
bool isSoloPlayer = FindObjectsByType<NetPlayer>(FindObjectsSortMode.None).Length <= 1;
|
||||
if (!isSoloPlayer)
|
||||
{
|
||||
timeoutForOpponentPanel.SetActive(false);
|
||||
ResetOpponentTimeoutState();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hasShownOpponentTimeout)
|
||||
continue;
|
||||
|
||||
opponentWaitSecondsElapsed++;
|
||||
if (opponentWaitSecondsElapsed >= OpponentWaitTimeoutSeconds)
|
||||
{
|
||||
timeoutForOpponentPanel.transform.localScale = new Vector3(1, 0, 1);
|
||||
timeoutForOpponentPanel.transform.DOScaleY(1,0.1f).SetEase(Ease.InOutBounce);
|
||||
timeoutForOpponentPanel.SetActive(true);
|
||||
hasShownOpponentTimeout = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ResetOpponentTimeoutState()
|
||||
{
|
||||
opponentWaitSecondsElapsed = 0;
|
||||
hasShownOpponentTimeout = false;
|
||||
timeoutForOpponentPanel.SetActive(false);
|
||||
}
|
||||
|
||||
|
||||
public void ShowKickWarning(){
|
||||
kickWarning.transform.localScale = new Vector3(0, 0, 0);
|
||||
kickWarning.transform.DOScale(1, 0.2f).SetEase(Ease.OutBack);
|
||||
kickWarning.gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
public void HideKickWarning(){
|
||||
kickWarning.transform.DOScale(0, 0.2f).SetEase(Ease.InBack).OnComplete(() => {
|
||||
kickWarning.gameObject.SetActive(false);
|
||||
});
|
||||
}
|
||||
|
||||
public IEnumerator ShowRandomPostGamePanelThenHide()
|
||||
{
|
||||
HideAllPostGamePanels();
|
||||
if (postGamePanels == null || postGamePanels.Length == 0)
|
||||
yield break;
|
||||
|
||||
GameObject chosen = postGamePanels[Random.Range(0, postGamePanels.Length)];
|
||||
if (chosen == null)
|
||||
yield break;
|
||||
|
||||
chosen.SetActive(true);
|
||||
yield return new WaitForSeconds(PostGamePanelDuration);
|
||||
chosen.SetActive(false);
|
||||
}
|
||||
|
||||
void HideAllPostGamePanels()
|
||||
{
|
||||
if (postGamePanels == null)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < postGamePanels.Length; i++)
|
||||
{
|
||||
if (postGamePanels[i] != null)
|
||||
postGamePanels[i].SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
fileFormatVersion: 2
|
||||
guid: 09dd2727c9588024781c4d5160f19c4d
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
|
||||
public static class GameEvents{
|
||||
public static Action<Puck> OnSelectedPuckChanged;
|
||||
public static Action<Team> OnTeamChanged;
|
||||
using System;
|
||||
|
||||
public static class GameEvents{
|
||||
public static Action<Puck> OnSelectedPuckChanged;
|
||||
public static Action<Team> OnTeamChanged;
|
||||
}
|
||||
@@ -1,2 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
fileFormatVersion: 2
|
||||
guid: 0e7ff01514aafb34d805be0d1662de29
|
||||
+1836
-1836
File diff suppressed because it is too large
Load Diff
@@ -1,2 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
fileFormatVersion: 2
|
||||
guid: 12befc4868932ca479d93e14120ffd0e
|
||||
+770
-757
File diff suppressed because it is too large
Load Diff
@@ -1,2 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
fileFormatVersion: 2
|
||||
guid: 34fb5f6c8224c94438e92e0f8f5c8471
|
||||
@@ -1,372 +1,372 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using Mirror;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Networking;
|
||||
using UnityEngine.UI;
|
||||
using DG.Tweening;
|
||||
|
||||
public class GameTutorialManager : MonoBehaviour
|
||||
{
|
||||
public const string CpuOpponentName = "CPU";
|
||||
public const string DefaultGameSceneName = "Game";
|
||||
public const string CpuL10PrefsKey = "practice_cpu_l10";
|
||||
const int L10Window = 10;
|
||||
|
||||
public static bool tutorialModeTrigger = false;
|
||||
public bool isTutorial = false;
|
||||
public static bool tutorialModeEnabled = false;
|
||||
|
||||
public AiBotController aiController;
|
||||
|
||||
[Header("UI")]
|
||||
public CanvasGroup intro_shoot;
|
||||
public CanvasGroup intro_goal;
|
||||
public float introFadeDuration = 0.3f;
|
||||
|
||||
Button _introShootButton;
|
||||
Button _introGoalButton;
|
||||
bool _shootIntroDismissed;
|
||||
bool _goalIntroDismissed;
|
||||
bool _introVisible;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
#if UNITY_SERVER
|
||||
tutorialModeEnabled = false;
|
||||
tutorialModeTrigger = false;
|
||||
HideIntroImmediate(intro_shoot);
|
||||
HideIntroImmediate(intro_goal);
|
||||
enabled = false;
|
||||
return;
|
||||
#endif
|
||||
|
||||
tutorialModeEnabled = false;
|
||||
if (isTutorial)
|
||||
tutorialModeTrigger = true;
|
||||
|
||||
if (tutorialModeTrigger)
|
||||
tutorialModeEnabled = true;
|
||||
|
||||
tutorialModeTrigger = false;
|
||||
|
||||
if (!tutorialModeEnabled)
|
||||
{
|
||||
HideIntroImmediate(intro_shoot);
|
||||
HideIntroImmediate(intro_goal);
|
||||
}
|
||||
}
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
GameManager.OnTeamChanged += OnTeamChanged;
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
GameManager.OnTeamChanged -= OnTeamChanged;
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
if (tutorialModeEnabled)
|
||||
tutorialModeEnabled = false;
|
||||
|
||||
if (intro_shoot != null)
|
||||
DOTween.Kill(intro_shoot);
|
||||
if (intro_goal != null)
|
||||
DOTween.Kill(intro_goal);
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
if (tutorialModeEnabled)
|
||||
{
|
||||
NetManager.StopNetworkingForSceneChange();
|
||||
|
||||
int port = GetAvailablePort.GetAvailableUdpPort();
|
||||
Transport transport = NetworkManager.singleton != null
|
||||
? NetworkManager.singleton.transport
|
||||
: Transport.active;
|
||||
if (transport is PortTransport portTransport)
|
||||
portTransport.Port = (ushort)port;
|
||||
Logger.Log("Practice host starting on port " + port);
|
||||
|
||||
NetworkManager.singleton.networkAddress = "localhost";
|
||||
NetworkManager.singleton.StartHost();
|
||||
|
||||
WireIntroButtons();
|
||||
HideIntroImmediate(intro_goal);
|
||||
HideIntroImmediate(intro_shoot);
|
||||
|
||||
StartCoroutine(SetupAiWhenReady());
|
||||
StartCoroutine(ShowShootIntroWhenReady());
|
||||
}
|
||||
}
|
||||
|
||||
void WireIntroButtons()
|
||||
{
|
||||
if (intro_shoot != null)
|
||||
_introShootButton = intro_shoot.GetComponentInChildren<Button>(true);
|
||||
if (intro_goal != null)
|
||||
_introGoalButton = intro_goal.GetComponentInChildren<Button>(true);
|
||||
|
||||
if (_introShootButton != null)
|
||||
_introShootButton.onClick.AddListener(OnShootIntroDismissed);
|
||||
if (_introGoalButton != null)
|
||||
_introGoalButton.onClick.AddListener(OnGoalIntroDismissed);
|
||||
}
|
||||
|
||||
IEnumerator ShowShootIntroWhenReady()
|
||||
{
|
||||
while (GameManager.instance == null || !GameManager.instance.gameStarted)
|
||||
yield return null;
|
||||
|
||||
while (NetPlayer.localPlayer == null)
|
||||
yield return null;
|
||||
|
||||
ShowIntro(intro_shoot);
|
||||
}
|
||||
|
||||
void OnTeamChanged(Team team)
|
||||
{
|
||||
if (!tutorialModeEnabled || _introVisible)
|
||||
return;
|
||||
if (team != GetPlayerTeam())
|
||||
return;
|
||||
if (!_shootIntroDismissed || _goalIntroDismissed)
|
||||
return;
|
||||
|
||||
ShowIntro(intro_goal);
|
||||
}
|
||||
|
||||
void OnShootIntroDismissed()
|
||||
{
|
||||
if (_shootIntroDismissed)
|
||||
return;
|
||||
|
||||
_shootIntroDismissed = true;
|
||||
DismissIntro(intro_shoot);
|
||||
}
|
||||
|
||||
void OnGoalIntroDismissed()
|
||||
{
|
||||
if (_goalIntroDismissed)
|
||||
return;
|
||||
|
||||
_goalIntroDismissed = true;
|
||||
DismissIntro(intro_goal);
|
||||
}
|
||||
|
||||
void ShowIntro(CanvasGroup group)
|
||||
{
|
||||
if (group == null)
|
||||
return;
|
||||
|
||||
EnsureTutorialCanvasVisible(group);
|
||||
DOTween.Kill(group);
|
||||
|
||||
group.gameObject.SetActive(true);
|
||||
group.alpha = 1f;
|
||||
group.interactable = true;
|
||||
group.blocksRaycasts = true;
|
||||
|
||||
_introVisible = true;
|
||||
if (GameManager.instance != null)
|
||||
GameManager.instance.SetTurnTimerPaused(true);
|
||||
}
|
||||
|
||||
void DismissIntro(CanvasGroup group)
|
||||
{
|
||||
if (group == null)
|
||||
return;
|
||||
|
||||
group.interactable = false;
|
||||
group.blocksRaycasts = false;
|
||||
|
||||
DOTween.Kill(group);
|
||||
group.DOFade(0f, introFadeDuration)
|
||||
.SetEase(Ease.InOutSine)
|
||||
.OnComplete(() =>
|
||||
{
|
||||
group.gameObject.SetActive(false);
|
||||
_introVisible = false;
|
||||
if (GameManager.instance != null)
|
||||
GameManager.instance.SetTurnTimerPaused(false);
|
||||
});
|
||||
}
|
||||
|
||||
static void HideIntroImmediate(CanvasGroup group)
|
||||
{
|
||||
if (group == null)
|
||||
return;
|
||||
|
||||
DOTween.Kill(group);
|
||||
group.alpha = 0f;
|
||||
group.interactable = false;
|
||||
group.blocksRaycasts = false;
|
||||
group.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
static void EnsureTutorialCanvasVisible(CanvasGroup group)
|
||||
{
|
||||
Transform root = group.transform.root;
|
||||
if (root.localScale == Vector3.zero)
|
||||
root.localScale = Vector3.one;
|
||||
}
|
||||
|
||||
static Team GetPlayerTeam()
|
||||
{
|
||||
if (NetPlayer.localPlayer != null)
|
||||
return NetPlayer.localPlayer.myTeam;
|
||||
return GameManager.MyTeam;
|
||||
}
|
||||
|
||||
/// <summary>Call from the main menu before loading the game scene.</summary>
|
||||
public static void PrepareLaunch(MatchmadePlayer myPlayer, MatchmadePlayer cpuPlayer, Team myTeam = Team.Red)
|
||||
{
|
||||
LoginManager.ClearMatchYourTeam();
|
||||
GameManager.MyTeam = myTeam;
|
||||
GameManager.MatchRcPrizeCoins = 0;
|
||||
GameManager.ConfigureDedicatedMatchReporting(0);
|
||||
|
||||
if (myTeam == Team.Red)
|
||||
{
|
||||
GameManager.RedPlayer = myPlayer;
|
||||
GameManager.BluePlayer = cpuPlayer;
|
||||
}
|
||||
else
|
||||
{
|
||||
GameManager.RedPlayer = cpuPlayer;
|
||||
GameManager.BluePlayer = myPlayer;
|
||||
}
|
||||
|
||||
LevelLoadManager.myPlayer = myPlayer;
|
||||
LevelLoadManager.opponentPlayer = cpuPlayer;
|
||||
if (LevelLoadManager.instance != null)
|
||||
LevelLoadManager.instance.SetupMatchMade(myTeam, myPlayer, cpuPlayer, "0 RC");
|
||||
|
||||
tutorialModeTrigger = true;
|
||||
}
|
||||
|
||||
public static MatchmadePlayer CreateCpuOpponent()
|
||||
{
|
||||
GetCpuL10(out int wins, out int losses);
|
||||
return new MatchmadePlayer
|
||||
{
|
||||
Name = CpuOpponentName,
|
||||
l10_wins = wins,
|
||||
l10_losses = losses
|
||||
};
|
||||
}
|
||||
|
||||
public static void GetCpuL10(out int wins, out int losses)
|
||||
{
|
||||
CountL10(PlayerPrefs.GetString(CpuL10PrefsKey, string.Empty), out wins, out losses);
|
||||
}
|
||||
|
||||
public static void ApplyCpuL10(MatchmadePlayer cpu)
|
||||
{
|
||||
if (cpu == null)
|
||||
return;
|
||||
GetCpuL10(out int wins, out int losses);
|
||||
cpu.l10_wins = wins;
|
||||
cpu.l10_losses = losses;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records one practice result from the CPU's perspective (rolling last 10).
|
||||
/// Cleared on logout because <see cref="LoginManager.Logout"/> calls PlayerPrefs.DeleteAll.
|
||||
/// </summary>
|
||||
public static void RecordCpuL10Result(bool cpuWon)
|
||||
{
|
||||
string history = PlayerPrefs.GetString(CpuL10PrefsKey, string.Empty) ?? string.Empty;
|
||||
history += cpuWon ? 'W' : 'L';
|
||||
if (history.Length > L10Window)
|
||||
history = history.Substring(history.Length - L10Window);
|
||||
PlayerPrefs.SetString(CpuL10PrefsKey, history);
|
||||
PlayerPrefs.Save();
|
||||
}
|
||||
|
||||
static void CountL10(string history, out int wins, out int losses)
|
||||
{
|
||||
wins = 0;
|
||||
losses = 0;
|
||||
if (string.IsNullOrEmpty(history))
|
||||
return;
|
||||
for (int i = 0; i < history.Length; i++)
|
||||
{
|
||||
if (history[i] == 'W')
|
||||
wins++;
|
||||
else if (history[i] == 'L')
|
||||
losses++;
|
||||
}
|
||||
}
|
||||
|
||||
public static MatchmadePlayer CreateMyPlayerFromUser(UserData user)
|
||||
{
|
||||
return new MatchmadePlayer
|
||||
{
|
||||
Name = user.username,
|
||||
UserId = user.id,
|
||||
rc = user.rc
|
||||
};
|
||||
}
|
||||
|
||||
public static IEnumerator CoFetchPlayerL10(MatchmadePlayer player)
|
||||
{
|
||||
if (player == null || player.UserId <= 0)
|
||||
yield break;
|
||||
|
||||
string baseUrl = (GameManager.DedicatedInternalApiBase ?? LoginManager.AuthBaseUrl ?? "").TrimEnd('/');
|
||||
if (string.IsNullOrEmpty(baseUrl))
|
||||
yield break;
|
||||
|
||||
string url = baseUrl + "/players/" + player.UserId + "/l10";
|
||||
using (var req = UnityWebRequest.Get(url))
|
||||
{
|
||||
yield return req.SendWebRequest();
|
||||
if (req.result != UnityWebRequest.Result.Success)
|
||||
{
|
||||
Logger.Log("Tutorial L10 fetch failed: " + req.error + " " + url);
|
||||
yield break;
|
||||
}
|
||||
|
||||
var resp = JsonUtility.FromJson<PlayerL10Response>(req.downloadHandler.text);
|
||||
if (resp != null && resp.ok)
|
||||
{
|
||||
player.l10_wins = resp.l10_wins;
|
||||
player.l10_losses = resp.l10_losses;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
class PlayerL10Response
|
||||
{
|
||||
public bool ok;
|
||||
public int player_id;
|
||||
public int l10_wins;
|
||||
public int l10_losses;
|
||||
}
|
||||
|
||||
IEnumerator SetupAiWhenReady()
|
||||
{
|
||||
float timeout = 5f;
|
||||
while (NetPlayer.localPlayer == null && timeout > 0f)
|
||||
{
|
||||
timeout -= Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
Team playerTeam = NetPlayer.localPlayer != null
|
||||
? NetPlayer.localPlayer.myTeam
|
||||
: GameManager.MyTeam;
|
||||
Team opponentTeam = playerTeam == Team.Red ? Team.Blue : Team.Red;
|
||||
|
||||
if (GameManager.instance.SelectedTeam != playerTeam)
|
||||
GameManager.instance.SwitchTeams();
|
||||
|
||||
aiController.Setup(opponentTeam);
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Collections;
|
||||
using Mirror;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Networking;
|
||||
using UnityEngine.UI;
|
||||
using DG.Tweening;
|
||||
|
||||
public class GameTutorialManager : MonoBehaviour
|
||||
{
|
||||
public const string CpuOpponentName = "CPU";
|
||||
public const string DefaultGameSceneName = "Game";
|
||||
public const string CpuL10PrefsKey = "practice_cpu_l10";
|
||||
const int L10Window = 10;
|
||||
|
||||
public static bool tutorialModeTrigger = false;
|
||||
public bool isTutorial = false;
|
||||
public static bool tutorialModeEnabled = false;
|
||||
|
||||
public AiBotController aiController;
|
||||
|
||||
[Header("UI")]
|
||||
public CanvasGroup intro_shoot;
|
||||
public CanvasGroup intro_goal;
|
||||
public float introFadeDuration = 0.3f;
|
||||
|
||||
Button _introShootButton;
|
||||
Button _introGoalButton;
|
||||
bool _shootIntroDismissed;
|
||||
bool _goalIntroDismissed;
|
||||
bool _introVisible;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
#if UNITY_SERVER
|
||||
tutorialModeEnabled = false;
|
||||
tutorialModeTrigger = false;
|
||||
HideIntroImmediate(intro_shoot);
|
||||
HideIntroImmediate(intro_goal);
|
||||
enabled = false;
|
||||
return;
|
||||
#endif
|
||||
|
||||
tutorialModeEnabled = false;
|
||||
if (isTutorial)
|
||||
tutorialModeTrigger = true;
|
||||
|
||||
if (tutorialModeTrigger)
|
||||
tutorialModeEnabled = true;
|
||||
|
||||
tutorialModeTrigger = false;
|
||||
|
||||
if (!tutorialModeEnabled)
|
||||
{
|
||||
HideIntroImmediate(intro_shoot);
|
||||
HideIntroImmediate(intro_goal);
|
||||
}
|
||||
}
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
GameManager.OnTeamChanged += OnTeamChanged;
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
GameManager.OnTeamChanged -= OnTeamChanged;
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
if (tutorialModeEnabled)
|
||||
tutorialModeEnabled = false;
|
||||
|
||||
if (intro_shoot != null)
|
||||
DOTween.Kill(intro_shoot);
|
||||
if (intro_goal != null)
|
||||
DOTween.Kill(intro_goal);
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
if (tutorialModeEnabled)
|
||||
{
|
||||
NetManager.StopNetworkingForSceneChange();
|
||||
|
||||
int port = GetAvailablePort.GetAvailableUdpPort();
|
||||
Transport transport = NetworkManager.singleton != null
|
||||
? NetworkManager.singleton.transport
|
||||
: Transport.active;
|
||||
if (transport is PortTransport portTransport)
|
||||
portTransport.Port = (ushort)port;
|
||||
Logger.Log("Practice host starting on port " + port);
|
||||
|
||||
NetworkManager.singleton.networkAddress = "localhost";
|
||||
NetworkManager.singleton.StartHost();
|
||||
|
||||
WireIntroButtons();
|
||||
HideIntroImmediate(intro_goal);
|
||||
HideIntroImmediate(intro_shoot);
|
||||
|
||||
StartCoroutine(SetupAiWhenReady());
|
||||
StartCoroutine(ShowShootIntroWhenReady());
|
||||
}
|
||||
}
|
||||
|
||||
void WireIntroButtons()
|
||||
{
|
||||
if (intro_shoot != null)
|
||||
_introShootButton = intro_shoot.GetComponentInChildren<Button>(true);
|
||||
if (intro_goal != null)
|
||||
_introGoalButton = intro_goal.GetComponentInChildren<Button>(true);
|
||||
|
||||
if (_introShootButton != null)
|
||||
_introShootButton.onClick.AddListener(OnShootIntroDismissed);
|
||||
if (_introGoalButton != null)
|
||||
_introGoalButton.onClick.AddListener(OnGoalIntroDismissed);
|
||||
}
|
||||
|
||||
IEnumerator ShowShootIntroWhenReady()
|
||||
{
|
||||
while (GameManager.instance == null || !GameManager.instance.gameStarted)
|
||||
yield return null;
|
||||
|
||||
while (NetPlayer.localPlayer == null)
|
||||
yield return null;
|
||||
|
||||
ShowIntro(intro_shoot);
|
||||
}
|
||||
|
||||
void OnTeamChanged(Team team)
|
||||
{
|
||||
if (!tutorialModeEnabled || _introVisible)
|
||||
return;
|
||||
if (team != GetPlayerTeam())
|
||||
return;
|
||||
if (!_shootIntroDismissed || _goalIntroDismissed)
|
||||
return;
|
||||
|
||||
ShowIntro(intro_goal);
|
||||
}
|
||||
|
||||
void OnShootIntroDismissed()
|
||||
{
|
||||
if (_shootIntroDismissed)
|
||||
return;
|
||||
|
||||
_shootIntroDismissed = true;
|
||||
DismissIntro(intro_shoot);
|
||||
}
|
||||
|
||||
void OnGoalIntroDismissed()
|
||||
{
|
||||
if (_goalIntroDismissed)
|
||||
return;
|
||||
|
||||
_goalIntroDismissed = true;
|
||||
DismissIntro(intro_goal);
|
||||
}
|
||||
|
||||
void ShowIntro(CanvasGroup group)
|
||||
{
|
||||
if (group == null)
|
||||
return;
|
||||
|
||||
EnsureTutorialCanvasVisible(group);
|
||||
DOTween.Kill(group);
|
||||
|
||||
group.gameObject.SetActive(true);
|
||||
group.alpha = 1f;
|
||||
group.interactable = true;
|
||||
group.blocksRaycasts = true;
|
||||
|
||||
_introVisible = true;
|
||||
if (GameManager.instance != null)
|
||||
GameManager.instance.SetTurnTimerPaused(true);
|
||||
}
|
||||
|
||||
void DismissIntro(CanvasGroup group)
|
||||
{
|
||||
if (group == null)
|
||||
return;
|
||||
|
||||
group.interactable = false;
|
||||
group.blocksRaycasts = false;
|
||||
|
||||
DOTween.Kill(group);
|
||||
group.DOFade(0f, introFadeDuration)
|
||||
.SetEase(Ease.InOutSine)
|
||||
.OnComplete(() =>
|
||||
{
|
||||
group.gameObject.SetActive(false);
|
||||
_introVisible = false;
|
||||
if (GameManager.instance != null)
|
||||
GameManager.instance.SetTurnTimerPaused(false);
|
||||
});
|
||||
}
|
||||
|
||||
static void HideIntroImmediate(CanvasGroup group)
|
||||
{
|
||||
if (group == null)
|
||||
return;
|
||||
|
||||
DOTween.Kill(group);
|
||||
group.alpha = 0f;
|
||||
group.interactable = false;
|
||||
group.blocksRaycasts = false;
|
||||
group.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
static void EnsureTutorialCanvasVisible(CanvasGroup group)
|
||||
{
|
||||
Transform root = group.transform.root;
|
||||
if (root.localScale == Vector3.zero)
|
||||
root.localScale = Vector3.one;
|
||||
}
|
||||
|
||||
static Team GetPlayerTeam()
|
||||
{
|
||||
if (NetPlayer.localPlayer != null)
|
||||
return NetPlayer.localPlayer.myTeam;
|
||||
return GameManager.MyTeam;
|
||||
}
|
||||
|
||||
/// <summary>Call from the main menu before loading the game scene.</summary>
|
||||
public static void PrepareLaunch(MatchmadePlayer myPlayer, MatchmadePlayer cpuPlayer, Team myTeam = Team.Red)
|
||||
{
|
||||
LoginManager.ClearMatchYourTeam();
|
||||
GameManager.MyTeam = myTeam;
|
||||
GameManager.MatchRcPrizeCoins = 0;
|
||||
GameManager.ConfigureDedicatedMatchReporting(0);
|
||||
|
||||
if (myTeam == Team.Red)
|
||||
{
|
||||
GameManager.RedPlayer = myPlayer;
|
||||
GameManager.BluePlayer = cpuPlayer;
|
||||
}
|
||||
else
|
||||
{
|
||||
GameManager.RedPlayer = cpuPlayer;
|
||||
GameManager.BluePlayer = myPlayer;
|
||||
}
|
||||
|
||||
LevelLoadManager.myPlayer = myPlayer;
|
||||
LevelLoadManager.opponentPlayer = cpuPlayer;
|
||||
if (LevelLoadManager.instance != null)
|
||||
LevelLoadManager.instance.SetupMatchMade(myTeam, myPlayer, cpuPlayer, "0 RC");
|
||||
|
||||
tutorialModeTrigger = true;
|
||||
}
|
||||
|
||||
public static MatchmadePlayer CreateCpuOpponent()
|
||||
{
|
||||
GetCpuL10(out int wins, out int losses);
|
||||
return new MatchmadePlayer
|
||||
{
|
||||
Name = CpuOpponentName,
|
||||
l10_wins = wins,
|
||||
l10_losses = losses
|
||||
};
|
||||
}
|
||||
|
||||
public static void GetCpuL10(out int wins, out int losses)
|
||||
{
|
||||
CountL10(PlayerPrefs.GetString(CpuL10PrefsKey, string.Empty), out wins, out losses);
|
||||
}
|
||||
|
||||
public static void ApplyCpuL10(MatchmadePlayer cpu)
|
||||
{
|
||||
if (cpu == null)
|
||||
return;
|
||||
GetCpuL10(out int wins, out int losses);
|
||||
cpu.l10_wins = wins;
|
||||
cpu.l10_losses = losses;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records one practice result from the CPU's perspective (rolling last 10).
|
||||
/// Cleared on logout because <see cref="LoginManager.Logout"/> calls PlayerPrefs.DeleteAll.
|
||||
/// </summary>
|
||||
public static void RecordCpuL10Result(bool cpuWon)
|
||||
{
|
||||
string history = PlayerPrefs.GetString(CpuL10PrefsKey, string.Empty) ?? string.Empty;
|
||||
history += cpuWon ? 'W' : 'L';
|
||||
if (history.Length > L10Window)
|
||||
history = history.Substring(history.Length - L10Window);
|
||||
PlayerPrefs.SetString(CpuL10PrefsKey, history);
|
||||
PlayerPrefs.Save();
|
||||
}
|
||||
|
||||
static void CountL10(string history, out int wins, out int losses)
|
||||
{
|
||||
wins = 0;
|
||||
losses = 0;
|
||||
if (string.IsNullOrEmpty(history))
|
||||
return;
|
||||
for (int i = 0; i < history.Length; i++)
|
||||
{
|
||||
if (history[i] == 'W')
|
||||
wins++;
|
||||
else if (history[i] == 'L')
|
||||
losses++;
|
||||
}
|
||||
}
|
||||
|
||||
public static MatchmadePlayer CreateMyPlayerFromUser(UserData user)
|
||||
{
|
||||
return new MatchmadePlayer
|
||||
{
|
||||
Name = user.username,
|
||||
UserId = user.id,
|
||||
rc = user.rc
|
||||
};
|
||||
}
|
||||
|
||||
public static IEnumerator CoFetchPlayerL10(MatchmadePlayer player)
|
||||
{
|
||||
if (player == null || player.UserId <= 0)
|
||||
yield break;
|
||||
|
||||
string baseUrl = (GameManager.DedicatedInternalApiBase ?? LoginManager.AuthBaseUrl ?? "").TrimEnd('/');
|
||||
if (string.IsNullOrEmpty(baseUrl))
|
||||
yield break;
|
||||
|
||||
string url = baseUrl + "/players/" + player.UserId + "/l10";
|
||||
using (var req = UnityWebRequest.Get(url))
|
||||
{
|
||||
yield return req.SendWebRequest();
|
||||
if (req.result != UnityWebRequest.Result.Success)
|
||||
{
|
||||
Logger.Log("Tutorial L10 fetch failed: " + req.error + " " + url);
|
||||
yield break;
|
||||
}
|
||||
|
||||
var resp = JsonUtility.FromJson<PlayerL10Response>(req.downloadHandler.text);
|
||||
if (resp != null && resp.ok)
|
||||
{
|
||||
player.l10_wins = resp.l10_wins;
|
||||
player.l10_losses = resp.l10_losses;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
class PlayerL10Response
|
||||
{
|
||||
public bool ok;
|
||||
public int player_id;
|
||||
public int l10_wins;
|
||||
public int l10_losses;
|
||||
}
|
||||
|
||||
IEnumerator SetupAiWhenReady()
|
||||
{
|
||||
float timeout = 5f;
|
||||
while (NetPlayer.localPlayer == null && timeout > 0f)
|
||||
{
|
||||
timeout -= Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
Team playerTeam = NetPlayer.localPlayer != null
|
||||
? NetPlayer.localPlayer.myTeam
|
||||
: GameManager.MyTeam;
|
||||
Team opponentTeam = playerTeam == Team.Red ? Team.Blue : Team.Red;
|
||||
|
||||
if (GameManager.instance.SelectedTeam != playerTeam)
|
||||
GameManager.instance.SwitchTeams();
|
||||
|
||||
aiController.Setup(opponentTeam);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
fileFormatVersion: 2
|
||||
guid: 304204825fc486545a7a9bd7dd232861
|
||||
+57
-57
@@ -1,57 +1,57 @@
|
||||
using Mirror;
|
||||
using UnityEngine;
|
||||
|
||||
public class Goal : NetworkBehaviour
|
||||
{
|
||||
public Team team;
|
||||
|
||||
public static Goal RedGoal,BlueGoal;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
if(team == Team.Red){
|
||||
RedGoal = this;
|
||||
}else{
|
||||
BlueGoal = this;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void OnTriggerEnter2D(Collider2D collision)
|
||||
{
|
||||
if(!isServer){return;}
|
||||
if(GameManager.instance != null && GameManager.instance.AreResetCollisionsDisabled)
|
||||
return;
|
||||
|
||||
if(collision.gameObject.CompareTag("Player")){
|
||||
if(GameManager.instance == null || !GameManager.instance.CanProcessGoal){
|
||||
return;
|
||||
}
|
||||
Debug.Log($"GOAL! {team}", gameObject);
|
||||
Ball ball = collision.gameObject.GetComponent<Ball>();
|
||||
Debug.Log(ball.name);
|
||||
GameManager.instance.OnGoal(team);
|
||||
}else if(collision.gameObject.CompareTag("Puck")){
|
||||
if(GameManager.instance == null || !GameManager.instance.CanProcessGoal)
|
||||
return;
|
||||
Debug.Log("Puck goal", gameObject);
|
||||
Puck puck = collision.gameObject.GetComponent<Puck>();
|
||||
Debug.Log(puck.name);
|
||||
puck.ScheduleReset(collision.transform.position);
|
||||
}
|
||||
}
|
||||
|
||||
void OnTriggerExit2D(Collider2D collision)
|
||||
{
|
||||
if(!isServer){return;}
|
||||
|
||||
if(collision.gameObject.CompareTag("Puck")){
|
||||
if(GameManager.instance != null && GameManager.instance.AreResetCollisionsDisabled)
|
||||
return;
|
||||
Debug.Log("Puck exit goal", gameObject);
|
||||
Puck puck = collision.gameObject.GetComponent<Puck>();
|
||||
Debug.Log(puck.name);
|
||||
puck.UnscheduleReset();
|
||||
}
|
||||
}
|
||||
}
|
||||
using Mirror;
|
||||
using UnityEngine;
|
||||
|
||||
public class Goal : NetworkBehaviour
|
||||
{
|
||||
public Team team;
|
||||
|
||||
public static Goal RedGoal,BlueGoal;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
if(team == Team.Red){
|
||||
RedGoal = this;
|
||||
}else{
|
||||
BlueGoal = this;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void OnTriggerEnter2D(Collider2D collision)
|
||||
{
|
||||
if(!isServer){return;}
|
||||
if(GameManager.instance != null && GameManager.instance.AreResetCollisionsDisabled)
|
||||
return;
|
||||
|
||||
if(collision.gameObject.CompareTag("Player")){
|
||||
if(GameManager.instance == null || !GameManager.instance.CanProcessGoal){
|
||||
return;
|
||||
}
|
||||
Debug.Log($"GOAL! {team}", gameObject);
|
||||
Ball ball = collision.gameObject.GetComponent<Ball>();
|
||||
Debug.Log(ball.name);
|
||||
GameManager.instance.OnGoal(team);
|
||||
}else if(collision.gameObject.CompareTag("Puck")){
|
||||
if(GameManager.instance == null || !GameManager.instance.CanProcessGoal)
|
||||
return;
|
||||
Debug.Log("Puck goal", gameObject);
|
||||
Puck puck = collision.gameObject.GetComponent<Puck>();
|
||||
Debug.Log(puck.name);
|
||||
puck.ScheduleReset(collision.transform.position);
|
||||
}
|
||||
}
|
||||
|
||||
void OnTriggerExit2D(Collider2D collision)
|
||||
{
|
||||
if(!isServer){return;}
|
||||
|
||||
if(collision.gameObject.CompareTag("Puck")){
|
||||
if(GameManager.instance != null && GameManager.instance.AreResetCollisionsDisabled)
|
||||
return;
|
||||
Debug.Log("Puck exit goal", gameObject);
|
||||
Puck puck = collision.gameObject.GetComponent<Puck>();
|
||||
Debug.Log(puck.name);
|
||||
puck.UnscheduleReset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
fileFormatVersion: 2
|
||||
guid: f5ca0ff31c184b04da729df1fae08a5e
|
||||
+1304
-1304
File diff suppressed because it is too large
Load Diff
@@ -1,2 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
fileFormatVersion: 2
|
||||
guid: bdd03d8cc6ae1ad4d89c39ba1d752da7
|
||||
+633
-633
File diff suppressed because it is too large
Load Diff
@@ -1,2 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
fileFormatVersion: 2
|
||||
guid: 7d216f08d4b95494ba7875cca68b440a
|
||||
+57
-57
@@ -1,57 +1,57 @@
|
||||
using System.Collections;
|
||||
using DG.Tweening;
|
||||
using Mirror;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
public class NetEmotes : NetworkBehaviour
|
||||
{
|
||||
public GameObject textEmotePrefab;
|
||||
public Transform textEmotesParent;
|
||||
|
||||
public float textEmoteDuration = 5f;
|
||||
public Vector3 textEmoteMovement = new Vector3(0,100,0);
|
||||
|
||||
public static NetEmotes instance;
|
||||
void Awake()
|
||||
{
|
||||
instance = this;
|
||||
}
|
||||
|
||||
|
||||
public void SendTextEmote(string txtName){
|
||||
if(isServer){
|
||||
RpcSendTextEmote(txtName);
|
||||
ShowTextEmote(txtName);
|
||||
}else{
|
||||
CmdSendTextEmote(txtName);
|
||||
}
|
||||
}
|
||||
|
||||
[Command]
|
||||
void CmdSendTextEmote(string txtName){
|
||||
RpcSendTextEmote(txtName);
|
||||
}
|
||||
|
||||
[ClientRpc]
|
||||
void RpcSendTextEmote(string txtName){
|
||||
ShowTextEmote(txtName);
|
||||
}
|
||||
|
||||
void ShowTextEmote(string txtName){
|
||||
Debug.Log("Showing text emote: " + txtName);
|
||||
GameObject textEmote = Instantiate(textEmotePrefab, textEmotesParent);
|
||||
textEmote.GetComponentInChildren<TMP_Text>().text = txtName;
|
||||
textEmote.transform.localScale = new Vector3(0, 0, 0);
|
||||
StartCoroutine(TextEmoteRoutine(textEmote));
|
||||
}
|
||||
|
||||
IEnumerator TextEmoteRoutine(GameObject textEmote){
|
||||
textEmote.transform.DOScale(1, 0.5f).SetEase(Ease.OutBack);
|
||||
textEmote.transform.DOMove(textEmote.transform.position + textEmoteMovement, textEmoteDuration).SetEase(Ease.OutBack);
|
||||
yield return new WaitForSeconds(textEmoteDuration);
|
||||
textEmote.transform.DOScale(0, 0.5f).SetEase(Ease.InBack);
|
||||
yield return new WaitForSeconds(0.5f);
|
||||
Destroy(textEmote);
|
||||
}
|
||||
}
|
||||
using System.Collections;
|
||||
using DG.Tweening;
|
||||
using Mirror;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
public class NetEmotes : NetworkBehaviour
|
||||
{
|
||||
public GameObject textEmotePrefab;
|
||||
public Transform textEmotesParent;
|
||||
|
||||
public float textEmoteDuration = 5f;
|
||||
public Vector3 textEmoteMovement = new Vector3(0,100,0);
|
||||
|
||||
public static NetEmotes instance;
|
||||
void Awake()
|
||||
{
|
||||
instance = this;
|
||||
}
|
||||
|
||||
|
||||
public void SendTextEmote(string txtName){
|
||||
if(isServer){
|
||||
RpcSendTextEmote(txtName);
|
||||
ShowTextEmote(txtName);
|
||||
}else{
|
||||
CmdSendTextEmote(txtName);
|
||||
}
|
||||
}
|
||||
|
||||
[Command]
|
||||
void CmdSendTextEmote(string txtName){
|
||||
RpcSendTextEmote(txtName);
|
||||
}
|
||||
|
||||
[ClientRpc]
|
||||
void RpcSendTextEmote(string txtName){
|
||||
ShowTextEmote(txtName);
|
||||
}
|
||||
|
||||
void ShowTextEmote(string txtName){
|
||||
Debug.Log("Showing text emote: " + txtName);
|
||||
GameObject textEmote = Instantiate(textEmotePrefab, textEmotesParent);
|
||||
textEmote.GetComponentInChildren<TMP_Text>().text = txtName;
|
||||
textEmote.transform.localScale = new Vector3(0, 0, 0);
|
||||
StartCoroutine(TextEmoteRoutine(textEmote));
|
||||
}
|
||||
|
||||
IEnumerator TextEmoteRoutine(GameObject textEmote){
|
||||
textEmote.transform.DOScale(1, 0.5f).SetEase(Ease.OutBack);
|
||||
textEmote.transform.DOMove(textEmote.transform.position + textEmoteMovement, textEmoteDuration).SetEase(Ease.OutBack);
|
||||
yield return new WaitForSeconds(textEmoteDuration);
|
||||
textEmote.transform.DOScale(0, 0.5f).SetEase(Ease.InBack);
|
||||
yield return new WaitForSeconds(0.5f);
|
||||
Destroy(textEmote);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
fileFormatVersion: 2
|
||||
guid: d17e1ac28f957254795443173835b577
|
||||
+346
-346
@@ -1,346 +1,346 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using Mirror;
|
||||
using UnityEngine.Networking;
|
||||
|
||||
public class NetManager : NetworkManager
|
||||
{
|
||||
const float InitialReconnectDelaySeconds = 2f;
|
||||
const float ReopenTriggerDelaySeconds = 15f;
|
||||
[SerializeField] float reconnectIntervalSeconds = 15f;
|
||||
Coroutine reconnectCoroutine;
|
||||
bool reopenRequestedThisDisconnect;
|
||||
bool reopenRequestInFlight;
|
||||
|
||||
[SerializeField] private CanvasGroup reconnectCanvasGroup;
|
||||
|
||||
static bool s_IntentionalClientDisconnect;
|
||||
|
||||
public static NetManager instance;
|
||||
public override void Awake()
|
||||
{
|
||||
base.Awake();
|
||||
instance = this;
|
||||
Transport.active.OnClientError += OnTransportClientError;
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
if (Transport.active != null)
|
||||
Transport.active.OnClientError -= OnTransportClientError;
|
||||
|
||||
if (NetworkServer.active || NetworkClient.active)
|
||||
StopNetworkingForSceneChange();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fully tears down Mirror before loading another scene. Required for tutorial host mode;
|
||||
/// <see cref="NetworkManager.StopClient"/> alone leaves the server running.
|
||||
/// </summary>
|
||||
public static void StopNetworkingForSceneChange()
|
||||
{
|
||||
MarkIntentionalClientDisconnect();
|
||||
|
||||
NetworkManager manager = singleton != null ? singleton : NetworkManager.singleton;
|
||||
if (manager != null)
|
||||
{
|
||||
if (NetworkServer.active && NetworkClient.active)
|
||||
manager.StopHost();
|
||||
else if (NetworkClient.active)
|
||||
manager.StopClient();
|
||||
else if (NetworkServer.active)
|
||||
manager.StopServer();
|
||||
return;
|
||||
}
|
||||
|
||||
if (NetworkClient.active)
|
||||
NetworkClient.Shutdown();
|
||||
if (NetworkServer.active)
|
||||
NetworkServer.Shutdown();
|
||||
}
|
||||
|
||||
static void OnTransportClientError(TransportError error, string reason)
|
||||
{
|
||||
Logger.Log($"Transport client error ({error}): {reason}");
|
||||
}
|
||||
|
||||
public override void OnStartServer()
|
||||
{
|
||||
base.OnStartServer();
|
||||
Application.logMessageReceived += ForwardMirrorServerLogToLogger;
|
||||
Logger.Log("Mirror server exception logging enabled");
|
||||
}
|
||||
|
||||
|
||||
public override void OnServerError(NetworkConnectionToClient conn, TransportError error, string reason)
|
||||
{
|
||||
int connId = conn != null ? conn.connectionId : -1;
|
||||
Logger.Log($"Mirror server transport error (connId={connId}, {error}): {reason}");
|
||||
base.OnServerError(conn, error, reason);
|
||||
}
|
||||
|
||||
public override void OnServerTransportException(NetworkConnectionToClient conn, Exception exception)
|
||||
{
|
||||
int connId = conn != null ? conn.connectionId : -1;
|
||||
Logger.Log($"Mirror server transport exception (connId={connId}): {exception}");
|
||||
base.OnServerTransportException(conn, exception);
|
||||
}
|
||||
|
||||
public override void OnServerDisconnect(NetworkConnectionToClient conn)
|
||||
{
|
||||
NetPlayer disconnectingPlayer = null;
|
||||
if (conn != null && conn.identity != null)
|
||||
{
|
||||
disconnectingPlayer = conn.identity.GetComponent<NetPlayer>();
|
||||
if (disconnectingPlayer != null && GameManager.instance != null)
|
||||
GameManager.instance.OnDedicatedMatchPlayerDisconnected(disconnectingPlayer.myTeam);
|
||||
}
|
||||
|
||||
NetPlayer.CancelPendingRematchOnDisconnect(disconnectingPlayer);
|
||||
|
||||
if (conn != null)
|
||||
Logger.Log($"Mirror server: client disconnected (connId={conn.connectionId})");
|
||||
base.OnServerDisconnect(conn);
|
||||
}
|
||||
|
||||
static void ForwardMirrorServerLogToLogger(string logString, string stackTrace, LogType type)
|
||||
{
|
||||
if (!NetworkServer.active)
|
||||
return;
|
||||
if (!ShouldForwardMirrorServerLog(logString, stackTrace, type))
|
||||
return;
|
||||
|
||||
if (string.IsNullOrEmpty(stackTrace))
|
||||
Logger.Log($"[Mirror/{type}] {logString}");
|
||||
else
|
||||
Logger.Log($"[Mirror/{type}] {logString}\n{stackTrace}");
|
||||
}
|
||||
|
||||
static bool ShouldForwardMirrorServerLog(string message, string stackTrace, LogType type)
|
||||
{
|
||||
if (type == LogType.Exception)
|
||||
return ContainsMirrorNetworkContext(message, stackTrace);
|
||||
|
||||
if (type != LogType.Error)
|
||||
return false;
|
||||
|
||||
return message.Contains("Disconnecting connection:")
|
||||
|| message.Contains("NetworkServer:")
|
||||
|| message.Contains("because handling a message")
|
||||
|| message.Contains("because reading a message")
|
||||
|| message.Contains("failed to unpack and invoke")
|
||||
|| message.Contains("was too short (messages should start with message id)");
|
||||
}
|
||||
|
||||
static bool ContainsMirrorNetworkContext(string message, string stackTrace)
|
||||
{
|
||||
return (message != null && (message.Contains("Mirror") || message.Contains("NetworkServer")))
|
||||
|| (stackTrace != null && stackTrace.Contains("Mirror"));
|
||||
}
|
||||
|
||||
public static void MarkIntentionalClientDisconnect()
|
||||
{
|
||||
s_IntentionalClientDisconnect = true;
|
||||
}
|
||||
|
||||
public override void OnApplicationQuit()
|
||||
{
|
||||
MarkIntentionalClientDisconnect();
|
||||
StopReconnectLoop();
|
||||
base.OnApplicationQuit();
|
||||
}
|
||||
|
||||
public override void OnStopServer()
|
||||
{
|
||||
Application.logMessageReceived -= ForwardMirrorServerLogToLogger;
|
||||
|
||||
ReplayRecorder.StopAndFlush();
|
||||
GameManager.ReportDedicatedMatchRoomClosed();
|
||||
base.OnStopServer();
|
||||
}
|
||||
|
||||
public override void OnClientConnect()
|
||||
{
|
||||
base.OnClientConnect();
|
||||
Logger.Log("Client connected");
|
||||
reopenRequestedThisDisconnect = false;
|
||||
reopenRequestInFlight = false;
|
||||
StopReconnectLoop();
|
||||
}
|
||||
|
||||
public override void OnClientDisconnect()
|
||||
{
|
||||
base.OnClientDisconnect();
|
||||
Logger.Log("Client disconnected");
|
||||
// Mirror raises OnDisconnectedEvent before clearing ConnectState, so
|
||||
// NetworkClient.isConnected is still true here on involuntary drops.
|
||||
// Gate on policy flags only; teardown wait happens in the reconnect loop.
|
||||
if (CanAttemptReconnect())
|
||||
{
|
||||
if (reconnectCoroutine == null)
|
||||
reconnectCoroutine = StartCoroutine(ReconnectLoopCoroutine());
|
||||
else
|
||||
SetReconnectCanvasVisible(true);
|
||||
}
|
||||
else
|
||||
SetReconnectCanvasVisible(false);
|
||||
}
|
||||
|
||||
public override void OnStopClient()
|
||||
{
|
||||
s_IntentionalClientDisconnect = false;
|
||||
base.OnStopClient();
|
||||
}
|
||||
|
||||
void StopReconnectLoop()
|
||||
{
|
||||
if (reconnectCoroutine == null)
|
||||
{
|
||||
SetReconnectCanvasVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
StopCoroutine(reconnectCoroutine);
|
||||
reconnectCoroutine = null;
|
||||
SetReconnectCanvasVisible(false);
|
||||
}
|
||||
|
||||
void SetReconnectCanvasVisible(bool visible)
|
||||
{
|
||||
if (reconnectCanvasGroup == null)
|
||||
return;
|
||||
reconnectCanvasGroup.alpha = visible ? 1f : 0f;
|
||||
reconnectCanvasGroup.interactable = visible;
|
||||
reconnectCanvasGroup.blocksRaycasts = visible;
|
||||
}
|
||||
|
||||
IEnumerator ReconnectLoopCoroutine()
|
||||
{
|
||||
SetReconnectCanvasVisible(true);
|
||||
reopenRequestedThisDisconnect = false;
|
||||
reopenRequestInFlight = false;
|
||||
|
||||
// Wait until Mirror finishes OnClientDisconnectInternal (clears Connected
|
||||
// then Shutdown). Without this, ShouldReconnect() fails immediately.
|
||||
while (NetworkClient.isConnected || NetworkClient.active)
|
||||
yield return null;
|
||||
|
||||
float reconnectStartedAt = Time.realtimeSinceStartup;
|
||||
float initialWaitEnd = Time.realtimeSinceStartup + InitialReconnectDelaySeconds;
|
||||
while (ShouldReconnect() && Time.realtimeSinceStartup < initialWaitEnd)
|
||||
yield return null;
|
||||
|
||||
while (ShouldReconnect())
|
||||
{
|
||||
if (!reopenRequestedThisDisconnect && !reopenRequestInFlight
|
||||
&& Time.realtimeSinceStartup - reconnectStartedAt >= ReopenTriggerDelaySeconds)
|
||||
{
|
||||
reopenRequestedThisDisconnect = true;
|
||||
StartCoroutine(CoRequestReopenForCurrentPort());
|
||||
}
|
||||
|
||||
if (NetworkClient.active && !NetworkClient.isConnected)
|
||||
{
|
||||
StopClient();
|
||||
yield return null;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!NetworkClient.active)
|
||||
{
|
||||
Logger.Log("Reconnect: attempting StartClient");
|
||||
StartClient();
|
||||
}
|
||||
|
||||
float waitEnd = Time.realtimeSinceStartup + reconnectIntervalSeconds;
|
||||
while (ShouldReconnect() && Time.realtimeSinceStartup < waitEnd)
|
||||
yield return null;
|
||||
}
|
||||
|
||||
reconnectCoroutine = null;
|
||||
SetReconnectCanvasVisible(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Policy checks that are safe during Mirror's disconnect callback
|
||||
/// (when <see cref="NetworkClient.isConnected"/> may still be true).
|
||||
/// </summary>
|
||||
bool CanAttemptReconnect()
|
||||
{
|
||||
if (s_IntentionalClientDisconnect)
|
||||
return false;
|
||||
if (NetworkServer.active)
|
||||
return false;
|
||||
if (GameManager.instance != null && GameManager.instance.isGameEnded)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ShouldReconnect()
|
||||
{
|
||||
if (!CanAttemptReconnect())
|
||||
return false;
|
||||
if (NetworkClient.isConnected)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
IEnumerator CoRequestReopenForCurrentPort()
|
||||
{
|
||||
reopenRequestInFlight = true;
|
||||
|
||||
int port = Cupid.RoomPort;
|
||||
if (port <= 0)
|
||||
{
|
||||
Logger.Log("Reconnect: skipping /reopen because room port is invalid");
|
||||
reopenRequestInFlight = false;
|
||||
yield break;
|
||||
}
|
||||
|
||||
string baseUri = Cupid.CupidURI;
|
||||
string password = Cupid.SettingsPassword;
|
||||
if (string.IsNullOrWhiteSpace(baseUri) || string.IsNullOrEmpty(password))
|
||||
{
|
||||
Logger.Log("Reconnect: skipping /reopen because Cupid URI/password is not available");
|
||||
reopenRequestInFlight = false;
|
||||
yield break;
|
||||
}
|
||||
|
||||
string query = "?password=" + UnityWebRequest.EscapeURL(password) + "&port=" + port;
|
||||
string[] endpointCandidates =
|
||||
{
|
||||
baseUri.TrimEnd('/') + "/internal/admin/reopen" + query,
|
||||
baseUri.TrimEnd('/') + "/reopen" + query
|
||||
};
|
||||
|
||||
bool success = false;
|
||||
for (int i = 0; i < endpointCandidates.Length; i++)
|
||||
{
|
||||
string url = endpointCandidates[i];
|
||||
using (UnityWebRequest req = UnityWebRequest.Get(url))
|
||||
{
|
||||
req.downloadHandler = new DownloadHandlerBuffer();
|
||||
yield return req.SendWebRequest();
|
||||
string body = req.downloadHandler != null ? req.downloadHandler.text : "";
|
||||
|
||||
if (req.result == UnityWebRequest.Result.Success && req.responseCode >= 200 && req.responseCode < 300)
|
||||
{
|
||||
Logger.Log("Reconnect: /reopen success on port " + port + ". Response: " + body);
|
||||
success = true;
|
||||
break;
|
||||
}
|
||||
|
||||
Logger.Log("Reconnect: /reopen failed at " + url + " status " + req.responseCode + " error: " + req.error + " body: " + body);
|
||||
}
|
||||
}
|
||||
|
||||
if (!success)
|
||||
Logger.Log("Reconnect: all /reopen endpoint attempts failed.");
|
||||
|
||||
reopenRequestInFlight = false;
|
||||
}
|
||||
|
||||
}
|
||||
using System;
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using Mirror;
|
||||
using UnityEngine.Networking;
|
||||
|
||||
public class NetManager : NetworkManager
|
||||
{
|
||||
const float InitialReconnectDelaySeconds = 2f;
|
||||
const float ReopenTriggerDelaySeconds = 15f;
|
||||
[SerializeField] float reconnectIntervalSeconds = 15f;
|
||||
Coroutine reconnectCoroutine;
|
||||
bool reopenRequestedThisDisconnect;
|
||||
bool reopenRequestInFlight;
|
||||
|
||||
[SerializeField] private CanvasGroup reconnectCanvasGroup;
|
||||
|
||||
static bool s_IntentionalClientDisconnect;
|
||||
|
||||
public static NetManager instance;
|
||||
public override void Awake()
|
||||
{
|
||||
base.Awake();
|
||||
instance = this;
|
||||
Transport.active.OnClientError += OnTransportClientError;
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
if (Transport.active != null)
|
||||
Transport.active.OnClientError -= OnTransportClientError;
|
||||
|
||||
if (NetworkServer.active || NetworkClient.active)
|
||||
StopNetworkingForSceneChange();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fully tears down Mirror before loading another scene. Required for tutorial host mode;
|
||||
/// <see cref="NetworkManager.StopClient"/> alone leaves the server running.
|
||||
/// </summary>
|
||||
public static void StopNetworkingForSceneChange()
|
||||
{
|
||||
MarkIntentionalClientDisconnect();
|
||||
|
||||
NetworkManager manager = singleton != null ? singleton : NetworkManager.singleton;
|
||||
if (manager != null)
|
||||
{
|
||||
if (NetworkServer.active && NetworkClient.active)
|
||||
manager.StopHost();
|
||||
else if (NetworkClient.active)
|
||||
manager.StopClient();
|
||||
else if (NetworkServer.active)
|
||||
manager.StopServer();
|
||||
return;
|
||||
}
|
||||
|
||||
if (NetworkClient.active)
|
||||
NetworkClient.Shutdown();
|
||||
if (NetworkServer.active)
|
||||
NetworkServer.Shutdown();
|
||||
}
|
||||
|
||||
static void OnTransportClientError(TransportError error, string reason)
|
||||
{
|
||||
Logger.Log($"Transport client error ({error}): {reason}");
|
||||
}
|
||||
|
||||
public override void OnStartServer()
|
||||
{
|
||||
base.OnStartServer();
|
||||
Application.logMessageReceived += ForwardMirrorServerLogToLogger;
|
||||
Logger.Log("Mirror server exception logging enabled");
|
||||
}
|
||||
|
||||
|
||||
public override void OnServerError(NetworkConnectionToClient conn, TransportError error, string reason)
|
||||
{
|
||||
int connId = conn != null ? conn.connectionId : -1;
|
||||
Logger.Log($"Mirror server transport error (connId={connId}, {error}): {reason}");
|
||||
base.OnServerError(conn, error, reason);
|
||||
}
|
||||
|
||||
public override void OnServerTransportException(NetworkConnectionToClient conn, Exception exception)
|
||||
{
|
||||
int connId = conn != null ? conn.connectionId : -1;
|
||||
Logger.Log($"Mirror server transport exception (connId={connId}): {exception}");
|
||||
base.OnServerTransportException(conn, exception);
|
||||
}
|
||||
|
||||
public override void OnServerDisconnect(NetworkConnectionToClient conn)
|
||||
{
|
||||
NetPlayer disconnectingPlayer = null;
|
||||
if (conn != null && conn.identity != null)
|
||||
{
|
||||
disconnectingPlayer = conn.identity.GetComponent<NetPlayer>();
|
||||
if (disconnectingPlayer != null && GameManager.instance != null)
|
||||
GameManager.instance.OnDedicatedMatchPlayerDisconnected(disconnectingPlayer.myTeam);
|
||||
}
|
||||
|
||||
NetPlayer.CancelPendingRematchOnDisconnect(disconnectingPlayer);
|
||||
|
||||
if (conn != null)
|
||||
Logger.Log($"Mirror server: client disconnected (connId={conn.connectionId})");
|
||||
base.OnServerDisconnect(conn);
|
||||
}
|
||||
|
||||
static void ForwardMirrorServerLogToLogger(string logString, string stackTrace, LogType type)
|
||||
{
|
||||
if (!NetworkServer.active)
|
||||
return;
|
||||
if (!ShouldForwardMirrorServerLog(logString, stackTrace, type))
|
||||
return;
|
||||
|
||||
if (string.IsNullOrEmpty(stackTrace))
|
||||
Logger.Log($"[Mirror/{type}] {logString}");
|
||||
else
|
||||
Logger.Log($"[Mirror/{type}] {logString}\n{stackTrace}");
|
||||
}
|
||||
|
||||
static bool ShouldForwardMirrorServerLog(string message, string stackTrace, LogType type)
|
||||
{
|
||||
if (type == LogType.Exception)
|
||||
return ContainsMirrorNetworkContext(message, stackTrace);
|
||||
|
||||
if (type != LogType.Error)
|
||||
return false;
|
||||
|
||||
return message.Contains("Disconnecting connection:")
|
||||
|| message.Contains("NetworkServer:")
|
||||
|| message.Contains("because handling a message")
|
||||
|| message.Contains("because reading a message")
|
||||
|| message.Contains("failed to unpack and invoke")
|
||||
|| message.Contains("was too short (messages should start with message id)");
|
||||
}
|
||||
|
||||
static bool ContainsMirrorNetworkContext(string message, string stackTrace)
|
||||
{
|
||||
return (message != null && (message.Contains("Mirror") || message.Contains("NetworkServer")))
|
||||
|| (stackTrace != null && stackTrace.Contains("Mirror"));
|
||||
}
|
||||
|
||||
public static void MarkIntentionalClientDisconnect()
|
||||
{
|
||||
s_IntentionalClientDisconnect = true;
|
||||
}
|
||||
|
||||
public override void OnApplicationQuit()
|
||||
{
|
||||
MarkIntentionalClientDisconnect();
|
||||
StopReconnectLoop();
|
||||
base.OnApplicationQuit();
|
||||
}
|
||||
|
||||
public override void OnStopServer()
|
||||
{
|
||||
Application.logMessageReceived -= ForwardMirrorServerLogToLogger;
|
||||
|
||||
ReplayRecorder.StopAndFlush();
|
||||
GameManager.ReportDedicatedMatchRoomClosed();
|
||||
base.OnStopServer();
|
||||
}
|
||||
|
||||
public override void OnClientConnect()
|
||||
{
|
||||
base.OnClientConnect();
|
||||
Logger.Log("Client connected");
|
||||
reopenRequestedThisDisconnect = false;
|
||||
reopenRequestInFlight = false;
|
||||
StopReconnectLoop();
|
||||
}
|
||||
|
||||
public override void OnClientDisconnect()
|
||||
{
|
||||
base.OnClientDisconnect();
|
||||
Logger.Log("Client disconnected");
|
||||
// Mirror raises OnDisconnectedEvent before clearing ConnectState, so
|
||||
// NetworkClient.isConnected is still true here on involuntary drops.
|
||||
// Gate on policy flags only; teardown wait happens in the reconnect loop.
|
||||
if (CanAttemptReconnect())
|
||||
{
|
||||
if (reconnectCoroutine == null)
|
||||
reconnectCoroutine = StartCoroutine(ReconnectLoopCoroutine());
|
||||
else
|
||||
SetReconnectCanvasVisible(true);
|
||||
}
|
||||
else
|
||||
SetReconnectCanvasVisible(false);
|
||||
}
|
||||
|
||||
public override void OnStopClient()
|
||||
{
|
||||
s_IntentionalClientDisconnect = false;
|
||||
base.OnStopClient();
|
||||
}
|
||||
|
||||
void StopReconnectLoop()
|
||||
{
|
||||
if (reconnectCoroutine == null)
|
||||
{
|
||||
SetReconnectCanvasVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
StopCoroutine(reconnectCoroutine);
|
||||
reconnectCoroutine = null;
|
||||
SetReconnectCanvasVisible(false);
|
||||
}
|
||||
|
||||
void SetReconnectCanvasVisible(bool visible)
|
||||
{
|
||||
if (reconnectCanvasGroup == null)
|
||||
return;
|
||||
reconnectCanvasGroup.alpha = visible ? 1f : 0f;
|
||||
reconnectCanvasGroup.interactable = visible;
|
||||
reconnectCanvasGroup.blocksRaycasts = visible;
|
||||
}
|
||||
|
||||
IEnumerator ReconnectLoopCoroutine()
|
||||
{
|
||||
SetReconnectCanvasVisible(true);
|
||||
reopenRequestedThisDisconnect = false;
|
||||
reopenRequestInFlight = false;
|
||||
|
||||
// Wait until Mirror finishes OnClientDisconnectInternal (clears Connected
|
||||
// then Shutdown). Without this, ShouldReconnect() fails immediately.
|
||||
while (NetworkClient.isConnected || NetworkClient.active)
|
||||
yield return null;
|
||||
|
||||
float reconnectStartedAt = Time.realtimeSinceStartup;
|
||||
float initialWaitEnd = Time.realtimeSinceStartup + InitialReconnectDelaySeconds;
|
||||
while (ShouldReconnect() && Time.realtimeSinceStartup < initialWaitEnd)
|
||||
yield return null;
|
||||
|
||||
while (ShouldReconnect())
|
||||
{
|
||||
if (!reopenRequestedThisDisconnect && !reopenRequestInFlight
|
||||
&& Time.realtimeSinceStartup - reconnectStartedAt >= ReopenTriggerDelaySeconds)
|
||||
{
|
||||
reopenRequestedThisDisconnect = true;
|
||||
StartCoroutine(CoRequestReopenForCurrentPort());
|
||||
}
|
||||
|
||||
if (NetworkClient.active && !NetworkClient.isConnected)
|
||||
{
|
||||
StopClient();
|
||||
yield return null;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!NetworkClient.active)
|
||||
{
|
||||
Logger.Log("Reconnect: attempting StartClient");
|
||||
StartClient();
|
||||
}
|
||||
|
||||
float waitEnd = Time.realtimeSinceStartup + reconnectIntervalSeconds;
|
||||
while (ShouldReconnect() && Time.realtimeSinceStartup < waitEnd)
|
||||
yield return null;
|
||||
}
|
||||
|
||||
reconnectCoroutine = null;
|
||||
SetReconnectCanvasVisible(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Policy checks that are safe during Mirror's disconnect callback
|
||||
/// (when <see cref="NetworkClient.isConnected"/> may still be true).
|
||||
/// </summary>
|
||||
bool CanAttemptReconnect()
|
||||
{
|
||||
if (s_IntentionalClientDisconnect)
|
||||
return false;
|
||||
if (NetworkServer.active)
|
||||
return false;
|
||||
if (GameManager.instance != null && GameManager.instance.isGameEnded)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ShouldReconnect()
|
||||
{
|
||||
if (!CanAttemptReconnect())
|
||||
return false;
|
||||
if (NetworkClient.isConnected)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
IEnumerator CoRequestReopenForCurrentPort()
|
||||
{
|
||||
reopenRequestInFlight = true;
|
||||
|
||||
int port = Cupid.RoomPort;
|
||||
if (port <= 0)
|
||||
{
|
||||
Logger.Log("Reconnect: skipping /reopen because room port is invalid");
|
||||
reopenRequestInFlight = false;
|
||||
yield break;
|
||||
}
|
||||
|
||||
string baseUri = Cupid.CupidURI;
|
||||
string password = Cupid.SettingsPassword;
|
||||
if (string.IsNullOrWhiteSpace(baseUri) || string.IsNullOrEmpty(password))
|
||||
{
|
||||
Logger.Log("Reconnect: skipping /reopen because Cupid URI/password is not available");
|
||||
reopenRequestInFlight = false;
|
||||
yield break;
|
||||
}
|
||||
|
||||
string query = "?password=" + UnityWebRequest.EscapeURL(password) + "&port=" + port;
|
||||
string[] endpointCandidates =
|
||||
{
|
||||
baseUri.TrimEnd('/') + "/internal/admin/reopen" + query,
|
||||
baseUri.TrimEnd('/') + "/reopen" + query
|
||||
};
|
||||
|
||||
bool success = false;
|
||||
for (int i = 0; i < endpointCandidates.Length; i++)
|
||||
{
|
||||
string url = endpointCandidates[i];
|
||||
using (UnityWebRequest req = UnityWebRequest.Get(url))
|
||||
{
|
||||
req.downloadHandler = new DownloadHandlerBuffer();
|
||||
yield return req.SendWebRequest();
|
||||
string body = req.downloadHandler != null ? req.downloadHandler.text : "";
|
||||
|
||||
if (req.result == UnityWebRequest.Result.Success && req.responseCode >= 200 && req.responseCode < 300)
|
||||
{
|
||||
Logger.Log("Reconnect: /reopen success on port " + port + ". Response: " + body);
|
||||
success = true;
|
||||
break;
|
||||
}
|
||||
|
||||
Logger.Log("Reconnect: /reopen failed at " + url + " status " + req.responseCode + " error: " + req.error + " body: " + body);
|
||||
}
|
||||
}
|
||||
|
||||
if (!success)
|
||||
Logger.Log("Reconnect: all /reopen endpoint attempts failed.");
|
||||
|
||||
reopenRequestInFlight = false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
fileFormatVersion: 2
|
||||
guid: c4e95ceb8bcd79f488de2cb8a5d0ca2d
|
||||
+676
-676
File diff suppressed because it is too large
Load Diff
@@ -1,2 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
fileFormatVersion: 2
|
||||
guid: 63314f7b4865af348a747f86b6aab5a6
|
||||
@@ -1,62 +1,62 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.AddressableAssets;
|
||||
using UnityEngine.ResourceManagement.AsyncOperations;
|
||||
using UnityEngine.ResourceManagement.ResourceProviders;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
public static class AddressableSceneLoader
|
||||
{
|
||||
public const string IntroScene = "Intro";
|
||||
public const string MainMenuScene = "MainMenu";
|
||||
public const string GameScene = "Game";
|
||||
|
||||
public static void Load(string sceneName)
|
||||
{
|
||||
AddressableSceneLoadRunner.Instance.Begin(sceneName);
|
||||
}
|
||||
|
||||
public static IEnumerator LoadRoutine(string sceneName)
|
||||
{
|
||||
AsyncOperationHandle<SceneInstance> handle = Addressables.LoadSceneAsync(sceneName, LoadSceneMode.Single);
|
||||
yield return handle;
|
||||
if (handle.Status != AsyncOperationStatus.Succeeded)
|
||||
Debug.LogError("Failed to load addressable scene '" + sceneName + "': " + handle.OperationException);
|
||||
}
|
||||
}
|
||||
|
||||
public class AddressableSceneLoadRunner : MonoBehaviour
|
||||
{
|
||||
static AddressableSceneLoadRunner instance;
|
||||
|
||||
public static AddressableSceneLoadRunner Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (instance != null)
|
||||
return instance;
|
||||
|
||||
var go = new GameObject(nameof(AddressableSceneLoadRunner));
|
||||
DontDestroyOnLoad(go);
|
||||
instance = go.AddComponent<AddressableSceneLoadRunner>();
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
void Awake()
|
||||
{
|
||||
if (instance != null && instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
}
|
||||
|
||||
public void Begin(string sceneName)
|
||||
{
|
||||
StartCoroutine(AddressableSceneLoader.LoadRoutine(sceneName));
|
||||
}
|
||||
}
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.AddressableAssets;
|
||||
using UnityEngine.ResourceManagement.AsyncOperations;
|
||||
using UnityEngine.ResourceManagement.ResourceProviders;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
public static class AddressableSceneLoader
|
||||
{
|
||||
public const string IntroScene = "Intro";
|
||||
public const string MainMenuScene = "MainMenu";
|
||||
public const string GameScene = "Game";
|
||||
|
||||
public static void Load(string sceneName)
|
||||
{
|
||||
AddressableSceneLoadRunner.Instance.Begin(sceneName);
|
||||
}
|
||||
|
||||
public static IEnumerator LoadRoutine(string sceneName)
|
||||
{
|
||||
AsyncOperationHandle<SceneInstance> handle = Addressables.LoadSceneAsync(sceneName, LoadSceneMode.Single);
|
||||
yield return handle;
|
||||
if (handle.Status != AsyncOperationStatus.Succeeded)
|
||||
Debug.LogError("Failed to load addressable scene '" + sceneName + "': " + handle.OperationException);
|
||||
}
|
||||
}
|
||||
|
||||
public class AddressableSceneLoadRunner : MonoBehaviour
|
||||
{
|
||||
static AddressableSceneLoadRunner instance;
|
||||
|
||||
public static AddressableSceneLoadRunner Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (instance != null)
|
||||
return instance;
|
||||
|
||||
var go = new GameObject(nameof(AddressableSceneLoadRunner));
|
||||
DontDestroyOnLoad(go);
|
||||
instance = go.AddComponent<AddressableSceneLoadRunner>();
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
void Awake()
|
||||
{
|
||||
if (instance != null && instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
}
|
||||
|
||||
public void Begin(string sceneName)
|
||||
{
|
||||
StartCoroutine(AddressableSceneLoader.LoadRoutine(sceneName));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,238 +1,238 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.AddressableAssets;
|
||||
using UnityEngine.AddressableAssets.ResourceLocators;
|
||||
using UnityEngine.Networking;
|
||||
using UnityEngine.ResourceManagement.AsyncOperations;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
|
||||
public class BootstrapUpdater : MonoBehaviour
|
||||
{
|
||||
public const string ContentReadyPrefsKey = "Ota.ContentReady";
|
||||
|
||||
[Header("Host")]
|
||||
[Tooltip("Apache folder that serves version.json and the Addressables catalog. Example: https://kkapi.playpoolstudios.com/ota")]
|
||||
[SerializeField] string otaBaseUrl = "https://kkapi.playpoolstudios.com/ota";
|
||||
[Tooltip("Opened when version.json has no storeUrl. Same role as the login screen Telegram link.")]
|
||||
[SerializeField] string fallbackStoreUrl = "https://t.me/+dfUhy4gmGXk0ZDRl";
|
||||
|
||||
[Header("Content")]
|
||||
[SerializeField] string introSceneAddress = AddressableSceneLoader.IntroScene;
|
||||
[SerializeField] string gameContentLabel = "game-content";
|
||||
|
||||
[Header("UI")]
|
||||
[SerializeField] TMP_Text statusText;
|
||||
[SerializeField] Slider progressSlider;
|
||||
[SerializeField] TMP_Text progressText;
|
||||
[SerializeField] GameObject retryButton;
|
||||
[SerializeField] GameObject updateButton;
|
||||
|
||||
string pendingStoreUrl = "";
|
||||
bool updateRunning;
|
||||
|
||||
void Start()
|
||||
{
|
||||
SetRetryVisible(false);
|
||||
SetUpdateVisible(false);
|
||||
SetProgress(0f, "");
|
||||
if (!updateRunning)
|
||||
StartCoroutine(RunUpdate());
|
||||
}
|
||||
|
||||
public void Retry()
|
||||
{
|
||||
if (updateRunning)
|
||||
return;
|
||||
SetRetryVisible(false);
|
||||
SetUpdateVisible(false);
|
||||
StartCoroutine(RunUpdate());
|
||||
}
|
||||
|
||||
public void OpenStore()
|
||||
{
|
||||
string url = string.IsNullOrWhiteSpace(pendingStoreUrl) ? fallbackStoreUrl : pendingStoreUrl;
|
||||
if (!string.IsNullOrWhiteSpace(url))
|
||||
Application.OpenURL(url.Trim());
|
||||
}
|
||||
|
||||
IEnumerator RunUpdate()
|
||||
{
|
||||
updateRunning = true;
|
||||
SetStatus("Checking for updates");
|
||||
|
||||
OtaVersionInfo version = null;
|
||||
bool versionFetched = false;
|
||||
yield return FetchVersion(info =>
|
||||
{
|
||||
version = info;
|
||||
versionFetched = info != null;
|
||||
});
|
||||
|
||||
if (!versionFetched)
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
Debug.LogWarning("OTA version.json was not reachable. Editor play continues into " + introSceneAddress + ".");
|
||||
#else
|
||||
if (PlayerPrefs.GetInt(ContentReadyPrefsKey, 0) != 1)
|
||||
{
|
||||
Fail("Check your connection and try again.");
|
||||
yield break;
|
||||
}
|
||||
SetStatus("Offline. Loading saved game");
|
||||
#endif
|
||||
}
|
||||
else if (OtaVersionInfo.IsClientBelowRequiredVersion(version.minPlayerVersion))
|
||||
{
|
||||
pendingStoreUrl = version.storeUrl;
|
||||
string message = string.IsNullOrWhiteSpace(version.message)
|
||||
? "A new version is required."
|
||||
: version.message.Trim();
|
||||
SetStatus(message);
|
||||
SetUpdateVisible(true);
|
||||
updateRunning = false;
|
||||
yield break;
|
||||
}
|
||||
|
||||
AsyncOperationHandle<IResourceLocator> init = Addressables.InitializeAsync(false);
|
||||
yield return init;
|
||||
bool initOk = init.IsValid() && init.Status == AsyncOperationStatus.Succeeded;
|
||||
if (init.IsValid())
|
||||
Addressables.Release(init);
|
||||
if (!initOk)
|
||||
{
|
||||
Fail("Could not start content update.");
|
||||
yield break;
|
||||
}
|
||||
|
||||
AsyncOperationHandle<System.Collections.Generic.List<string>> check = Addressables.CheckForCatalogUpdates(false);
|
||||
yield return check;
|
||||
if (check.Status == AsyncOperationStatus.Succeeded && check.Result != null && check.Result.Count > 0)
|
||||
{
|
||||
SetStatus("Updating catalog");
|
||||
AsyncOperationHandle<System.Collections.Generic.List<IResourceLocator>> catalogs = Addressables.UpdateCatalogs(check.Result, false);
|
||||
yield return catalogs;
|
||||
if (catalogs.Status != AsyncOperationStatus.Succeeded)
|
||||
{
|
||||
Addressables.Release(catalogs);
|
||||
Addressables.Release(check);
|
||||
Fail("Could not update the content catalog.");
|
||||
yield break;
|
||||
}
|
||||
Addressables.Release(catalogs);
|
||||
}
|
||||
Addressables.Release(check);
|
||||
|
||||
long downloadBytes = 0;
|
||||
AsyncOperationHandle<long> sizeHandle = Addressables.GetDownloadSizeAsync(gameContentLabel);
|
||||
yield return sizeHandle;
|
||||
if (sizeHandle.Status == AsyncOperationStatus.Succeeded)
|
||||
downloadBytes = sizeHandle.Result;
|
||||
Addressables.Release(sizeHandle);
|
||||
|
||||
if (downloadBytes > 0)
|
||||
{
|
||||
AsyncOperationHandle download = Addressables.DownloadDependenciesAsync(gameContentLabel, false);
|
||||
while (!download.IsDone)
|
||||
{
|
||||
DownloadStatus status = download.GetDownloadStatus();
|
||||
float percent = status.TotalBytes > 0
|
||||
? (float)status.DownloadedBytes / status.TotalBytes
|
||||
: download.PercentComplete;
|
||||
SetProgress(percent, FormatMegabytes(status.DownloadedBytes) + " / " + FormatMegabytes(status.TotalBytes));
|
||||
SetStatus("Downloading update " + Mathf.RoundToInt(percent * 100f) + "%");
|
||||
yield return null;
|
||||
}
|
||||
|
||||
if (download.Status != AsyncOperationStatus.Succeeded)
|
||||
{
|
||||
Addressables.Release(download);
|
||||
Fail("Download failed. Check your connection and try again.");
|
||||
yield break;
|
||||
}
|
||||
Addressables.Release(download);
|
||||
}
|
||||
|
||||
PlayerPrefs.SetInt(ContentReadyPrefsKey, 1);
|
||||
PlayerPrefs.Save();
|
||||
SetStatus("Loading");
|
||||
SetProgress(1f, "");
|
||||
yield return AddressableSceneLoader.LoadRoutine(introSceneAddress);
|
||||
updateRunning = false;
|
||||
}
|
||||
|
||||
IEnumerator FetchVersion(Action<OtaVersionInfo> onResult)
|
||||
{
|
||||
string baseUrl = (otaBaseUrl ?? "").Trim().TrimEnd('/');
|
||||
if (string.IsNullOrEmpty(baseUrl))
|
||||
{
|
||||
onResult?.Invoke(null);
|
||||
yield break;
|
||||
}
|
||||
|
||||
string url = baseUrl + "/version.json";
|
||||
using (var request = UnityWebRequest.Get(url))
|
||||
{
|
||||
request.timeout = 20;
|
||||
yield return request.SendWebRequest();
|
||||
if (request.result != UnityWebRequest.Result.Success)
|
||||
{
|
||||
onResult?.Invoke(null);
|
||||
yield break;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
onResult?.Invoke(JsonUtility.FromJson<OtaVersionInfo>(request.downloadHandler.text));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogWarning("OTA version.json parse failed: " + e.Message);
|
||||
onResult?.Invoke(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Fail(string message)
|
||||
{
|
||||
SetStatus(message);
|
||||
SetRetryVisible(true);
|
||||
updateRunning = false;
|
||||
}
|
||||
|
||||
void SetStatus(string message)
|
||||
{
|
||||
if (statusText != null)
|
||||
statusText.text = message;
|
||||
}
|
||||
|
||||
void SetProgress(float normalized, string label)
|
||||
{
|
||||
if (progressSlider != null)
|
||||
{
|
||||
progressSlider.gameObject.SetActive(true);
|
||||
progressSlider.value = Mathf.Clamp01(normalized);
|
||||
}
|
||||
if (progressText != null)
|
||||
progressText.text = label ?? "";
|
||||
}
|
||||
|
||||
void SetRetryVisible(bool visible)
|
||||
{
|
||||
if (retryButton != null)
|
||||
retryButton.SetActive(visible);
|
||||
}
|
||||
|
||||
void SetUpdateVisible(bool visible)
|
||||
{
|
||||
if (updateButton != null)
|
||||
updateButton.SetActive(visible);
|
||||
}
|
||||
|
||||
static string FormatMegabytes(long bytes)
|
||||
{
|
||||
float mb = bytes / (1024f * 1024f);
|
||||
return mb.ToString("0.0", System.Globalization.CultureInfo.InvariantCulture) + " MB";
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.AddressableAssets;
|
||||
using UnityEngine.AddressableAssets.ResourceLocators;
|
||||
using UnityEngine.Networking;
|
||||
using UnityEngine.ResourceManagement.AsyncOperations;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
|
||||
public class BootstrapUpdater : MonoBehaviour
|
||||
{
|
||||
public const string ContentReadyPrefsKey = "Ota.ContentReady";
|
||||
|
||||
[Header("Host")]
|
||||
[Tooltip("Apache folder that serves version.json and the Addressables catalog. Example: https://kkapi.playpoolstudios.com/ota")]
|
||||
[SerializeField] string otaBaseUrl = "https://kkapi.playpoolstudios.com/ota";
|
||||
[Tooltip("Opened when version.json has no storeUrl. Same role as the login screen Telegram link.")]
|
||||
[SerializeField] string fallbackStoreUrl = "https://t.me/+dfUhy4gmGXk0ZDRl";
|
||||
|
||||
[Header("Content")]
|
||||
[SerializeField] string introSceneAddress = AddressableSceneLoader.IntroScene;
|
||||
[SerializeField] string gameContentLabel = "game-content";
|
||||
|
||||
[Header("UI")]
|
||||
[SerializeField] TMP_Text statusText;
|
||||
[SerializeField] Slider progressSlider;
|
||||
[SerializeField] TMP_Text progressText;
|
||||
[SerializeField] GameObject retryButton;
|
||||
[SerializeField] GameObject updateButton;
|
||||
|
||||
string pendingStoreUrl = "";
|
||||
bool updateRunning;
|
||||
|
||||
void Start()
|
||||
{
|
||||
SetRetryVisible(false);
|
||||
SetUpdateVisible(false);
|
||||
SetProgress(0f, "");
|
||||
if (!updateRunning)
|
||||
StartCoroutine(RunUpdate());
|
||||
}
|
||||
|
||||
public void Retry()
|
||||
{
|
||||
if (updateRunning)
|
||||
return;
|
||||
SetRetryVisible(false);
|
||||
SetUpdateVisible(false);
|
||||
StartCoroutine(RunUpdate());
|
||||
}
|
||||
|
||||
public void OpenStore()
|
||||
{
|
||||
string url = string.IsNullOrWhiteSpace(pendingStoreUrl) ? fallbackStoreUrl : pendingStoreUrl;
|
||||
if (!string.IsNullOrWhiteSpace(url))
|
||||
Application.OpenURL(url.Trim());
|
||||
}
|
||||
|
||||
IEnumerator RunUpdate()
|
||||
{
|
||||
updateRunning = true;
|
||||
SetStatus("Checking for updates");
|
||||
|
||||
OtaVersionInfo version = null;
|
||||
bool versionFetched = false;
|
||||
yield return FetchVersion(info =>
|
||||
{
|
||||
version = info;
|
||||
versionFetched = info != null;
|
||||
});
|
||||
|
||||
if (!versionFetched)
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
Debug.LogWarning("OTA version.json was not reachable. Editor play continues into " + introSceneAddress + ".");
|
||||
#else
|
||||
if (PlayerPrefs.GetInt(ContentReadyPrefsKey, 0) != 1)
|
||||
{
|
||||
Fail("Check your connection and try again.");
|
||||
yield break;
|
||||
}
|
||||
SetStatus("Offline. Loading saved game");
|
||||
#endif
|
||||
}
|
||||
else if (OtaVersionInfo.IsClientBelowRequiredVersion(version.minPlayerVersion))
|
||||
{
|
||||
pendingStoreUrl = version.storeUrl;
|
||||
string message = string.IsNullOrWhiteSpace(version.message)
|
||||
? "A new version is required."
|
||||
: version.message.Trim();
|
||||
SetStatus(message);
|
||||
SetUpdateVisible(true);
|
||||
updateRunning = false;
|
||||
yield break;
|
||||
}
|
||||
|
||||
AsyncOperationHandle<IResourceLocator> init = Addressables.InitializeAsync(false);
|
||||
yield return init;
|
||||
bool initOk = init.IsValid() && init.Status == AsyncOperationStatus.Succeeded;
|
||||
if (init.IsValid())
|
||||
Addressables.Release(init);
|
||||
if (!initOk)
|
||||
{
|
||||
Fail("Could not start content update.");
|
||||
yield break;
|
||||
}
|
||||
|
||||
AsyncOperationHandle<System.Collections.Generic.List<string>> check = Addressables.CheckForCatalogUpdates(false);
|
||||
yield return check;
|
||||
if (check.Status == AsyncOperationStatus.Succeeded && check.Result != null && check.Result.Count > 0)
|
||||
{
|
||||
SetStatus("Updating catalog");
|
||||
AsyncOperationHandle<System.Collections.Generic.List<IResourceLocator>> catalogs = Addressables.UpdateCatalogs(check.Result, false);
|
||||
yield return catalogs;
|
||||
if (catalogs.Status != AsyncOperationStatus.Succeeded)
|
||||
{
|
||||
Addressables.Release(catalogs);
|
||||
Addressables.Release(check);
|
||||
Fail("Could not update the content catalog.");
|
||||
yield break;
|
||||
}
|
||||
Addressables.Release(catalogs);
|
||||
}
|
||||
Addressables.Release(check);
|
||||
|
||||
long downloadBytes = 0;
|
||||
AsyncOperationHandle<long> sizeHandle = Addressables.GetDownloadSizeAsync(gameContentLabel);
|
||||
yield return sizeHandle;
|
||||
if (sizeHandle.Status == AsyncOperationStatus.Succeeded)
|
||||
downloadBytes = sizeHandle.Result;
|
||||
Addressables.Release(sizeHandle);
|
||||
|
||||
if (downloadBytes > 0)
|
||||
{
|
||||
AsyncOperationHandle download = Addressables.DownloadDependenciesAsync(gameContentLabel, false);
|
||||
while (!download.IsDone)
|
||||
{
|
||||
DownloadStatus status = download.GetDownloadStatus();
|
||||
float percent = status.TotalBytes > 0
|
||||
? (float)status.DownloadedBytes / status.TotalBytes
|
||||
: download.PercentComplete;
|
||||
SetProgress(percent, FormatMegabytes(status.DownloadedBytes) + " / " + FormatMegabytes(status.TotalBytes));
|
||||
SetStatus("Downloading update " + Mathf.RoundToInt(percent * 100f) + "%");
|
||||
yield return null;
|
||||
}
|
||||
|
||||
if (download.Status != AsyncOperationStatus.Succeeded)
|
||||
{
|
||||
Addressables.Release(download);
|
||||
Fail("Download failed. Check your connection and try again.");
|
||||
yield break;
|
||||
}
|
||||
Addressables.Release(download);
|
||||
}
|
||||
|
||||
PlayerPrefs.SetInt(ContentReadyPrefsKey, 1);
|
||||
PlayerPrefs.Save();
|
||||
SetStatus("Loading");
|
||||
SetProgress(1f, "");
|
||||
yield return AddressableSceneLoader.LoadRoutine(introSceneAddress);
|
||||
updateRunning = false;
|
||||
}
|
||||
|
||||
IEnumerator FetchVersion(Action<OtaVersionInfo> onResult)
|
||||
{
|
||||
string baseUrl = (otaBaseUrl ?? "").Trim().TrimEnd('/');
|
||||
if (string.IsNullOrEmpty(baseUrl))
|
||||
{
|
||||
onResult?.Invoke(null);
|
||||
yield break;
|
||||
}
|
||||
|
||||
string url = baseUrl + "/version.json";
|
||||
using (var request = UnityWebRequest.Get(url))
|
||||
{
|
||||
request.timeout = 20;
|
||||
yield return request.SendWebRequest();
|
||||
if (request.result != UnityWebRequest.Result.Success)
|
||||
{
|
||||
onResult?.Invoke(null);
|
||||
yield break;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
onResult?.Invoke(JsonUtility.FromJson<OtaVersionInfo>(request.downloadHandler.text));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogWarning("OTA version.json parse failed: " + e.Message);
|
||||
onResult?.Invoke(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Fail(string message)
|
||||
{
|
||||
SetStatus(message);
|
||||
SetRetryVisible(true);
|
||||
updateRunning = false;
|
||||
}
|
||||
|
||||
void SetStatus(string message)
|
||||
{
|
||||
if (statusText != null)
|
||||
statusText.text = message;
|
||||
}
|
||||
|
||||
void SetProgress(float normalized, string label)
|
||||
{
|
||||
if (progressSlider != null)
|
||||
{
|
||||
progressSlider.gameObject.SetActive(true);
|
||||
progressSlider.value = Mathf.Clamp01(normalized);
|
||||
}
|
||||
if (progressText != null)
|
||||
progressText.text = label ?? "";
|
||||
}
|
||||
|
||||
void SetRetryVisible(bool visible)
|
||||
{
|
||||
if (retryButton != null)
|
||||
retryButton.SetActive(visible);
|
||||
}
|
||||
|
||||
void SetUpdateVisible(bool visible)
|
||||
{
|
||||
if (updateButton != null)
|
||||
updateButton.SetActive(visible);
|
||||
}
|
||||
|
||||
static string FormatMegabytes(long bytes)
|
||||
{
|
||||
float mb = bytes / (1024f * 1024f);
|
||||
return mb.ToString("0.0", System.Globalization.CultureInfo.InvariantCulture) + " MB";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,155 +1,155 @@
|
||||
#if UNITY_EDITOR
|
||||
using System.IO;
|
||||
using UnityEditor;
|
||||
using UnityEditor.AddressableAssets;
|
||||
using UnityEditor.AddressableAssets.Build;
|
||||
using UnityEditor.AddressableAssets.Settings;
|
||||
using UnityEditor.AddressableAssets.Settings.GroupSchemas;
|
||||
using UnityEngine;
|
||||
|
||||
public static class OtaAddressablesSetup
|
||||
{
|
||||
const string RemoteLoadUrl = "https://kkapi.playpoolstudios.com/ota/[BuildTarget]";
|
||||
const string GameContentLabel = "game-content";
|
||||
const string BootstrapScenePath = "Assets/Scenes/Bootstrap.unity";
|
||||
|
||||
static readonly string[] RemoteScenePaths =
|
||||
{
|
||||
"Assets/Scenes/Intro.unity",
|
||||
"Assets/Scenes/MainMenu.unity",
|
||||
"Assets/Scenes/Game.unity"
|
||||
};
|
||||
|
||||
[MenuItem("Tools/OTA/Configure Addressables")]
|
||||
public static void Configure()
|
||||
{
|
||||
AddressableAssetSettings settings = AddressableAssetSettingsDefaultObject.GetSettings(true);
|
||||
settings.BuildRemoteCatalog = true;
|
||||
settings.DisableCatalogUpdateOnStartup = true;
|
||||
settings.OverridePlayerVersion = "";
|
||||
|
||||
string profileId = settings.activeProfileId;
|
||||
settings.profileSettings.SetValue(profileId, AddressableAssetSettings.kRemoteBuildPath, "ServerData/[BuildTarget]");
|
||||
settings.profileSettings.SetValue(profileId, AddressableAssetSettings.kRemoteLoadPath, RemoteLoadUrl);
|
||||
settings.RemoteCatalogBuildPath.SetVariableByName(settings, AddressableAssetSettings.kRemoteBuildPath);
|
||||
settings.RemoteCatalogLoadPath.SetVariableByName(settings, AddressableAssetSettings.kRemoteLoadPath);
|
||||
|
||||
for (int i = 0; i < settings.DataBuilders.Count; i++)
|
||||
{
|
||||
if (settings.GetDataBuilder(i) != null && settings.GetDataBuilder(i).Name.Contains("Asset Database"))
|
||||
{
|
||||
settings.ActivePlayModeDataBuilderIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
AddressableAssetGroup local = GetOrCreateGroup(settings, "Local_Bootstrap");
|
||||
AddressableAssetGroup remote = GetOrCreateGroup(settings, "Remote_Game");
|
||||
ConfigureGroup(settings, local, true);
|
||||
ConfigureGroup(settings, remote, false);
|
||||
|
||||
MarkScene(settings, local, BootstrapScenePath, "Bootstrap", false);
|
||||
for (int i = 0; i < RemoteScenePaths.Length; i++)
|
||||
{
|
||||
string path = RemoteScenePaths[i];
|
||||
string address = Path.GetFileNameWithoutExtension(path);
|
||||
MarkScene(settings, remote, path, address, true);
|
||||
}
|
||||
|
||||
settings.SetDirty(AddressableAssetSettings.ModificationEvent.BatchModification, null, true, true);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
var scenes = new EditorBuildSettingsScene[1];
|
||||
scenes[0] = new EditorBuildSettingsScene(BootstrapScenePath, true);
|
||||
EditorBuildSettings.scenes = scenes;
|
||||
Debug.Log("OTA Addressables configured. Remote load path: " + RemoteLoadUrl);
|
||||
}
|
||||
|
||||
[MenuItem("Tools/OTA/Build Content")]
|
||||
public static void BuildContent()
|
||||
{
|
||||
Configure();
|
||||
AddressableAssetSettings.BuildPlayerContent(out AddressablesPlayerBuildResult result);
|
||||
if (!string.IsNullOrEmpty(result.Error))
|
||||
{
|
||||
Debug.LogError("OTA content build failed: " + result.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
string statePath = ContentUpdateScript.GetContentStateDataPath(false);
|
||||
Debug.Log("OTA content build finished. Keep this file for later patches: " + statePath);
|
||||
}
|
||||
|
||||
[MenuItem("Tools/OTA/Build Content Update")]
|
||||
public static void BuildContentUpdate()
|
||||
{
|
||||
AddressableAssetSettings settings = AddressableAssetSettingsDefaultObject.Settings;
|
||||
if (settings == null)
|
||||
{
|
||||
Debug.LogError("Addressables settings are missing. Run Tools/OTA/Configure Addressables first.");
|
||||
return;
|
||||
}
|
||||
|
||||
string statePath = ContentUpdateScript.GetContentStateDataPath(false);
|
||||
if (!File.Exists(statePath))
|
||||
{
|
||||
Debug.LogError("Missing content state at " + statePath + ". Run Tools/OTA/Build Content from the player build you shipped.");
|
||||
return;
|
||||
}
|
||||
|
||||
ContentUpdateScript.BuildContentUpdate(settings, statePath);
|
||||
Debug.Log("OTA content update built. Upload ServerData and keep " + statePath);
|
||||
}
|
||||
|
||||
static AddressableAssetGroup GetOrCreateGroup(AddressableAssetSettings settings, string name)
|
||||
{
|
||||
AddressableAssetGroup group = settings.FindGroup(name);
|
||||
if (group != null)
|
||||
return group;
|
||||
|
||||
return settings.CreateGroup(name, false, false, false, null, typeof(BundledAssetGroupSchema), typeof(ContentUpdateGroupSchema));
|
||||
}
|
||||
|
||||
static void ConfigureGroup(AddressableAssetSettings settings, AddressableAssetGroup group, bool local)
|
||||
{
|
||||
BundledAssetGroupSchema bundle = group.GetSchema<BundledAssetGroupSchema>();
|
||||
if (bundle == null)
|
||||
bundle = group.AddSchema<BundledAssetGroupSchema>();
|
||||
|
||||
if (local)
|
||||
{
|
||||
bundle.BuildPath.SetVariableByName(settings, AddressableAssetSettings.kLocalBuildPath);
|
||||
bundle.LoadPath.SetVariableByName(settings, AddressableAssetSettings.kLocalLoadPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
bundle.BuildPath.SetVariableByName(settings, AddressableAssetSettings.kRemoteBuildPath);
|
||||
bundle.LoadPath.SetVariableByName(settings, AddressableAssetSettings.kRemoteLoadPath);
|
||||
}
|
||||
|
||||
bundle.BundleMode = BundledAssetGroupSchema.BundlePackingMode.PackTogether;
|
||||
bundle.UseAssetBundleCrc = true;
|
||||
bundle.UseAssetBundleCache = true;
|
||||
|
||||
ContentUpdateGroupSchema update = group.GetSchema<ContentUpdateGroupSchema>();
|
||||
if (update == null)
|
||||
update = group.AddSchema<ContentUpdateGroupSchema>();
|
||||
update.StaticContent = local;
|
||||
}
|
||||
|
||||
static void MarkScene(AddressableAssetSettings settings, AddressableAssetGroup group, string assetPath, string address, bool gameContent)
|
||||
{
|
||||
string guid = AssetDatabase.AssetPathToGUID(assetPath);
|
||||
if (string.IsNullOrEmpty(guid))
|
||||
{
|
||||
Debug.LogWarning("OTA scene missing, skipped: " + assetPath);
|
||||
return;
|
||||
}
|
||||
|
||||
AddressableAssetEntry entry = settings.CreateOrMoveEntry(guid, group, false, false);
|
||||
entry.address = address;
|
||||
if (gameContent)
|
||||
entry.SetLabel(GameContentLabel, true, true, true);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#if UNITY_EDITOR
|
||||
using System.IO;
|
||||
using UnityEditor;
|
||||
using UnityEditor.AddressableAssets;
|
||||
using UnityEditor.AddressableAssets.Build;
|
||||
using UnityEditor.AddressableAssets.Settings;
|
||||
using UnityEditor.AddressableAssets.Settings.GroupSchemas;
|
||||
using UnityEngine;
|
||||
|
||||
public static class OtaAddressablesSetup
|
||||
{
|
||||
const string RemoteLoadUrl = "https://kkapi.playpoolstudios.com/ota/[BuildTarget]";
|
||||
const string GameContentLabel = "game-content";
|
||||
const string BootstrapScenePath = "Assets/Scenes/Bootstrap.unity";
|
||||
|
||||
static readonly string[] RemoteScenePaths =
|
||||
{
|
||||
"Assets/Scenes/Intro.unity",
|
||||
"Assets/Scenes/MainMenu.unity",
|
||||
"Assets/Scenes/Game.unity"
|
||||
};
|
||||
|
||||
[MenuItem("Tools/OTA/Configure Addressables")]
|
||||
public static void Configure()
|
||||
{
|
||||
AddressableAssetSettings settings = AddressableAssetSettingsDefaultObject.GetSettings(true);
|
||||
settings.BuildRemoteCatalog = true;
|
||||
settings.DisableCatalogUpdateOnStartup = true;
|
||||
settings.OverridePlayerVersion = "";
|
||||
|
||||
string profileId = settings.activeProfileId;
|
||||
settings.profileSettings.SetValue(profileId, AddressableAssetSettings.kRemoteBuildPath, "ServerData/[BuildTarget]");
|
||||
settings.profileSettings.SetValue(profileId, AddressableAssetSettings.kRemoteLoadPath, RemoteLoadUrl);
|
||||
settings.RemoteCatalogBuildPath.SetVariableByName(settings, AddressableAssetSettings.kRemoteBuildPath);
|
||||
settings.RemoteCatalogLoadPath.SetVariableByName(settings, AddressableAssetSettings.kRemoteLoadPath);
|
||||
|
||||
for (int i = 0; i < settings.DataBuilders.Count; i++)
|
||||
{
|
||||
if (settings.GetDataBuilder(i) != null && settings.GetDataBuilder(i).Name.Contains("Asset Database"))
|
||||
{
|
||||
settings.ActivePlayModeDataBuilderIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
AddressableAssetGroup local = GetOrCreateGroup(settings, "Local_Bootstrap");
|
||||
AddressableAssetGroup remote = GetOrCreateGroup(settings, "Remote_Game");
|
||||
ConfigureGroup(settings, local, true);
|
||||
ConfigureGroup(settings, remote, false);
|
||||
|
||||
MarkScene(settings, local, BootstrapScenePath, "Bootstrap", false);
|
||||
for (int i = 0; i < RemoteScenePaths.Length; i++)
|
||||
{
|
||||
string path = RemoteScenePaths[i];
|
||||
string address = Path.GetFileNameWithoutExtension(path);
|
||||
MarkScene(settings, remote, path, address, true);
|
||||
}
|
||||
|
||||
settings.SetDirty(AddressableAssetSettings.ModificationEvent.BatchModification, null, true, true);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
var scenes = new EditorBuildSettingsScene[1];
|
||||
scenes[0] = new EditorBuildSettingsScene(BootstrapScenePath, true);
|
||||
EditorBuildSettings.scenes = scenes;
|
||||
Debug.Log("OTA Addressables configured. Remote load path: " + RemoteLoadUrl);
|
||||
}
|
||||
|
||||
[MenuItem("Tools/OTA/Build Content")]
|
||||
public static void BuildContent()
|
||||
{
|
||||
Configure();
|
||||
AddressableAssetSettings.BuildPlayerContent(out AddressablesPlayerBuildResult result);
|
||||
if (!string.IsNullOrEmpty(result.Error))
|
||||
{
|
||||
Debug.LogError("OTA content build failed: " + result.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
string statePath = ContentUpdateScript.GetContentStateDataPath(false);
|
||||
Debug.Log("OTA content build finished. Keep this file for later patches: " + statePath);
|
||||
}
|
||||
|
||||
[MenuItem("Tools/OTA/Build Content Update")]
|
||||
public static void BuildContentUpdate()
|
||||
{
|
||||
AddressableAssetSettings settings = AddressableAssetSettingsDefaultObject.Settings;
|
||||
if (settings == null)
|
||||
{
|
||||
Debug.LogError("Addressables settings are missing. Run Tools/OTA/Configure Addressables first.");
|
||||
return;
|
||||
}
|
||||
|
||||
string statePath = ContentUpdateScript.GetContentStateDataPath(false);
|
||||
if (!File.Exists(statePath))
|
||||
{
|
||||
Debug.LogError("Missing content state at " + statePath + ". Run Tools/OTA/Build Content from the player build you shipped.");
|
||||
return;
|
||||
}
|
||||
|
||||
ContentUpdateScript.BuildContentUpdate(settings, statePath);
|
||||
Debug.Log("OTA content update built. Upload ServerData and keep " + statePath);
|
||||
}
|
||||
|
||||
static AddressableAssetGroup GetOrCreateGroup(AddressableAssetSettings settings, string name)
|
||||
{
|
||||
AddressableAssetGroup group = settings.FindGroup(name);
|
||||
if (group != null)
|
||||
return group;
|
||||
|
||||
return settings.CreateGroup(name, false, false, false, null, typeof(BundledAssetGroupSchema), typeof(ContentUpdateGroupSchema));
|
||||
}
|
||||
|
||||
static void ConfigureGroup(AddressableAssetSettings settings, AddressableAssetGroup group, bool local)
|
||||
{
|
||||
BundledAssetGroupSchema bundle = group.GetSchema<BundledAssetGroupSchema>();
|
||||
if (bundle == null)
|
||||
bundle = group.AddSchema<BundledAssetGroupSchema>();
|
||||
|
||||
if (local)
|
||||
{
|
||||
bundle.BuildPath.SetVariableByName(settings, AddressableAssetSettings.kLocalBuildPath);
|
||||
bundle.LoadPath.SetVariableByName(settings, AddressableAssetSettings.kLocalLoadPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
bundle.BuildPath.SetVariableByName(settings, AddressableAssetSettings.kRemoteBuildPath);
|
||||
bundle.LoadPath.SetVariableByName(settings, AddressableAssetSettings.kRemoteLoadPath);
|
||||
}
|
||||
|
||||
bundle.BundleMode = BundledAssetGroupSchema.BundlePackingMode.PackTogether;
|
||||
bundle.UseAssetBundleCrc = true;
|
||||
bundle.UseAssetBundleCache = true;
|
||||
|
||||
ContentUpdateGroupSchema update = group.GetSchema<ContentUpdateGroupSchema>();
|
||||
if (update == null)
|
||||
update = group.AddSchema<ContentUpdateGroupSchema>();
|
||||
update.StaticContent = local;
|
||||
}
|
||||
|
||||
static void MarkScene(AddressableAssetSettings settings, AddressableAssetGroup group, string assetPath, string address, bool gameContent)
|
||||
{
|
||||
string guid = AssetDatabase.AssetPathToGUID(assetPath);
|
||||
if (string.IsNullOrEmpty(guid))
|
||||
{
|
||||
Debug.LogWarning("OTA scene missing, skipped: " + assetPath);
|
||||
return;
|
||||
}
|
||||
|
||||
AddressableAssetEntry entry = settings.CreateOrMoveEntry(guid, group, false, false);
|
||||
entry.address = address;
|
||||
if (gameContent)
|
||||
entry.SetLabel(GameContentLabel, true, true, true);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using UnityEngine;
|
||||
|
||||
[Serializable]
|
||||
public class OtaVersionInfo
|
||||
{
|
||||
public string minPlayerVersion = "";
|
||||
public string contentVersion = "";
|
||||
public string message = "";
|
||||
public string storeUrl = "";
|
||||
|
||||
public static bool IsClientBelowRequiredVersion(string requiredVersion)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(requiredVersion))
|
||||
return false;
|
||||
if (!TryParseVersionFloat(requiredVersion, out float required))
|
||||
return false;
|
||||
if (!TryParseVersionFloat(Application.version, out float client))
|
||||
return false;
|
||||
return client < required;
|
||||
}
|
||||
|
||||
public static bool TryParseVersionFloat(string version, out float value)
|
||||
{
|
||||
value = 0f;
|
||||
if (string.IsNullOrWhiteSpace(version))
|
||||
return false;
|
||||
return float.TryParse(version.Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out value);
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using UnityEngine;
|
||||
|
||||
[Serializable]
|
||||
public class OtaVersionInfo
|
||||
{
|
||||
public string minPlayerVersion = "";
|
||||
public string contentVersion = "";
|
||||
public string message = "";
|
||||
public string storeUrl = "";
|
||||
|
||||
public static bool IsClientBelowRequiredVersion(string requiredVersion)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(requiredVersion))
|
||||
return false;
|
||||
if (!TryParseVersionFloat(requiredVersion, out float required))
|
||||
return false;
|
||||
if (!TryParseVersionFloat(Application.version, out float client))
|
||||
return false;
|
||||
return client < required;
|
||||
}
|
||||
|
||||
public static bool TryParseVersionFloat(string version, out float value)
|
||||
{
|
||||
value = 0f;
|
||||
if (string.IsNullOrWhiteSpace(version))
|
||||
return false;
|
||||
return float.TryParse(version.Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,56 +1,56 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Networking;
|
||||
|
||||
public class ProfileInfoFetcher : MonoBehaviour
|
||||
{
|
||||
/// <summary>GET /players/:playerId/profile. Callback (success, errorOrNull, profileOrNull).</summary>
|
||||
public void GetPlayerProfile(int playerId, Action<bool, string, PlayerProfileApiResponse> onComplete)
|
||||
{
|
||||
StartCoroutine(FetchPlayerProfileCoroutine(playerId, onComplete));
|
||||
}
|
||||
|
||||
IEnumerator FetchPlayerProfileCoroutine(int playerId, Action<bool, string, PlayerProfileApiResponse> onComplete)
|
||||
{
|
||||
string url = LoginManager.AuthBaseUrl + "/players/" + playerId + "/profile";
|
||||
using (var request = UnityWebRequest.Get(url))
|
||||
{
|
||||
request.downloadHandler = new DownloadHandlerBuffer();
|
||||
if (!string.IsNullOrEmpty(LoginManager.AuthToken))
|
||||
request.SetRequestHeader("Authorization", "Bearer " + LoginManager.AuthToken);
|
||||
|
||||
yield return request.SendWebRequest();
|
||||
|
||||
string text = request.downloadHandler != null ? request.downloadHandler.text : "";
|
||||
|
||||
if (request.result != UnityWebRequest.Result.Success &&
|
||||
request.result != UnityWebRequest.Result.ProtocolError)
|
||||
{
|
||||
onComplete?.Invoke(false, string.IsNullOrEmpty(request.error) ? "Network error" : request.error, null);
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (request.responseCode < 200 || request.responseCode >= 300)
|
||||
{
|
||||
onComplete?.Invoke(false, string.IsNullOrEmpty(text) ? "Request failed (" + request.responseCode + ")" : text, null);
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
onComplete?.Invoke(false, "Empty response from server", null);
|
||||
yield break;
|
||||
}
|
||||
|
||||
PlayerProfileApiResponse resp = JsonUtility.FromJson<PlayerProfileApiResponse>(text);
|
||||
if (resp != null && resp.ok)
|
||||
onComplete?.Invoke(true, null, resp);
|
||||
else
|
||||
{
|
||||
string err = resp != null && !string.IsNullOrEmpty(resp.error) ? resp.error : "Request failed";
|
||||
onComplete?.Invoke(false, err, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Networking;
|
||||
|
||||
public class ProfileInfoFetcher : MonoBehaviour
|
||||
{
|
||||
/// <summary>GET /players/:playerId/profile. Callback (success, errorOrNull, profileOrNull).</summary>
|
||||
public void GetPlayerProfile(int playerId, Action<bool, string, PlayerProfileApiResponse> onComplete)
|
||||
{
|
||||
StartCoroutine(FetchPlayerProfileCoroutine(playerId, onComplete));
|
||||
}
|
||||
|
||||
IEnumerator FetchPlayerProfileCoroutine(int playerId, Action<bool, string, PlayerProfileApiResponse> onComplete)
|
||||
{
|
||||
string url = LoginManager.AuthBaseUrl + "/players/" + playerId + "/profile";
|
||||
using (var request = UnityWebRequest.Get(url))
|
||||
{
|
||||
request.downloadHandler = new DownloadHandlerBuffer();
|
||||
if (!string.IsNullOrEmpty(LoginManager.AuthToken))
|
||||
request.SetRequestHeader("Authorization", "Bearer " + LoginManager.AuthToken);
|
||||
|
||||
yield return request.SendWebRequest();
|
||||
|
||||
string text = request.downloadHandler != null ? request.downloadHandler.text : "";
|
||||
|
||||
if (request.result != UnityWebRequest.Result.Success &&
|
||||
request.result != UnityWebRequest.Result.ProtocolError)
|
||||
{
|
||||
onComplete?.Invoke(false, string.IsNullOrEmpty(request.error) ? "Network error" : request.error, null);
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (request.responseCode < 200 || request.responseCode >= 300)
|
||||
{
|
||||
onComplete?.Invoke(false, string.IsNullOrEmpty(text) ? "Request failed (" + request.responseCode + ")" : text, null);
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
onComplete?.Invoke(false, "Empty response from server", null);
|
||||
yield break;
|
||||
}
|
||||
|
||||
PlayerProfileApiResponse resp = JsonUtility.FromJson<PlayerProfileApiResponse>(text);
|
||||
if (resp != null && resp.ok)
|
||||
onComplete?.Invoke(true, null, resp);
|
||||
else
|
||||
{
|
||||
string err = resp != null && !string.IsNullOrEmpty(resp.error) ? resp.error : "Request failed";
|
||||
onComplete?.Invoke(false, err, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+275
-275
@@ -1,276 +1,276 @@
|
||||
using System.Collections;
|
||||
using Mirror;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
|
||||
public class Puck : NetworkBehaviour
|
||||
{
|
||||
[HideInInspector]public Rigidbody2D rb;
|
||||
Collider2D col;
|
||||
[SyncVar]
|
||||
public bool isSelected;
|
||||
public LineRenderer lineRenderer;
|
||||
[SyncVar]
|
||||
public Team team;
|
||||
public GameObject redTeamMarker;
|
||||
public GameObject blueTeamMarker;
|
||||
public Transform selectedMarker;
|
||||
public float markerRotateSpeed;
|
||||
public float markerBounceIntensity;
|
||||
public float markerBounceSpeed;
|
||||
Vector3 markerStartScale;
|
||||
Vector3 startPosition;
|
||||
float bounceSinA;
|
||||
void OnValidate()
|
||||
{
|
||||
UpdateTeam();
|
||||
}
|
||||
|
||||
void Awake()
|
||||
{
|
||||
UpdateTeam();
|
||||
markerStartScale = selectedMarker.localScale;
|
||||
rb=GetComponent<Rigidbody2D>();
|
||||
col = GetComponent<Collider2D>();
|
||||
startPosition = transform.position;
|
||||
}
|
||||
|
||||
|
||||
void UpdateTeam(){
|
||||
redTeamMarker.SetActive(team == Team.Red);
|
||||
blueTeamMarker.SetActive(team == Team.Blue);
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
GameEvents.OnSelectedPuckChanged += OnSelectedPuckChanged;
|
||||
GameManager.OnTeamChanged += OnTeamChanged;
|
||||
|
||||
GameManager.instance.RegisterPuck(this);
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
GameEvents.OnSelectedPuckChanged -= OnSelectedPuckChanged;
|
||||
GameManager.OnTeamChanged -= OnTeamChanged;
|
||||
|
||||
RestoreAfterReset();
|
||||
if (GameManager.instance != null)
|
||||
GameManager.instance.DisposePuck(this);
|
||||
}
|
||||
public Vector3 lineEnd;
|
||||
void Update()
|
||||
{
|
||||
if (isSelected && lineRenderer != null)
|
||||
{
|
||||
Vector3 worldEnd = Camera.main.ScreenToWorldPoint(new Vector3(NetPlayer.curPosition.x, NetPlayer.curPosition.y, Camera.main.nearClipPlane));
|
||||
|
||||
lineEnd = transform.InverseTransformPoint(worldEnd);
|
||||
float clamp = GameManager.instance.puckDragClampMax;
|
||||
lineEnd = Vector2.ClampMagnitude(lineEnd, clamp);
|
||||
|
||||
if (NetPlayer.localPlayer != null
|
||||
&& team == NetPlayer.localPlayer.myTeam
|
||||
&& GameManager.instance.SelectedTeam == team)
|
||||
{
|
||||
float baseStretch = lineEnd.magnitude / clamp;
|
||||
float distanceMultiplier = GameManager.instance.GetCameraStretchDistanceMultiplier(transform.position);
|
||||
CameraEffects.instance.SetStretchFactor(baseStretch * distanceMultiplier);
|
||||
}
|
||||
|
||||
// Only two points for the line
|
||||
lineRenderer.positionCount = 2;
|
||||
lineRenderer.SetPosition(0, Vector3.zero);
|
||||
lineRenderer.SetPosition(1, new Vector3(lineEnd.x, lineEnd.y, 0));
|
||||
}
|
||||
|
||||
selectedMarker.gameObject.SetActive(GetMarkerVisibility());
|
||||
if(GameManager.instance.SelectedTeam == team){
|
||||
selectedMarker.Rotate(new Vector3(0,0,1), markerRotateSpeed * Time.deltaTime);
|
||||
bounceSinA += Time.deltaTime * markerBounceSpeed;
|
||||
float bounce = Mathf.Sin(bounceSinA) * markerBounceIntensity;
|
||||
selectedMarker.localScale = markerStartScale + (Vector3.one * bounce);
|
||||
}
|
||||
}
|
||||
|
||||
bool GetMarkerVisibility(){
|
||||
if(GameManager.instance.m_isMoving){return false;}
|
||||
if(NetPlayer.localPlayer == null){return false;}
|
||||
if(NetPlayer.localPlayer.myTeam != team){return false;}
|
||||
|
||||
if(GameManager.instance.SelectedTeam == team){
|
||||
if(GameManager.NoPuckSelected){
|
||||
return true;
|
||||
}else{
|
||||
return isSelected;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void OnSelectedPuckChanged(Puck puck)
|
||||
{
|
||||
isSelected = puck == this;
|
||||
lineRenderer.enabled = isSelected;
|
||||
|
||||
if(NetPlayer.localPlayer == null){return;}
|
||||
if(team != NetPlayer.localPlayer.myTeam){
|
||||
lineRenderer.widthMultiplier = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void OnTeamChanged(Team team){
|
||||
|
||||
}
|
||||
|
||||
void OnCollisionEnter2D(Collision2D collision)
|
||||
{
|
||||
if (!collision.collider.CompareTag("Puck"))
|
||||
return;
|
||||
// Both pucks get this callback; play once per contact.
|
||||
if (gameObject.GetInstanceID() >= collision.gameObject.GetInstanceID())
|
||||
return;
|
||||
|
||||
float otherSpeed = collision.relativeVelocity.magnitude;
|
||||
Debug.Log("Puck collision speed: " + otherSpeed);
|
||||
float maxVolSpeed =5f;
|
||||
float vol = Mathf.Clamp(otherSpeed / maxVolSpeed,0.1f,1);
|
||||
if (AudioManager.instance != null){
|
||||
AudioManager.instance.PlayPuckHit(vol);
|
||||
if(isServer){
|
||||
ReplayRecorder.RecordHit("puck_puck", vol, this);
|
||||
RpcPuckHitAudio(vol);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[ClientRpc]
|
||||
void RpcPuckHitAudio(float vol){
|
||||
AudioManager.instance.PlayPuckHit(vol);
|
||||
}
|
||||
|
||||
Coroutine coroutineReset;
|
||||
bool holdingCollisionLock;
|
||||
public void Reset(Vector3? position = null, float duration = 0.5f)
|
||||
{
|
||||
UnscheduleReset();
|
||||
Vector3 pos = position ?? startPosition;
|
||||
if (coroutineReset != null)
|
||||
{
|
||||
StopCoroutine(coroutineReset);
|
||||
RestoreAfterReset();
|
||||
}
|
||||
coroutineReset = StartCoroutine(CoroutineReset(pos, duration));
|
||||
}
|
||||
|
||||
public void SetCollisionsEnabled(bool enabled)
|
||||
{
|
||||
if (col != null)
|
||||
col.enabled = enabled;
|
||||
if (rb != null && !enabled)
|
||||
{
|
||||
rb.linearVelocity = Vector2.zero;
|
||||
rb.angularVelocity = 0f;
|
||||
}
|
||||
}
|
||||
Coroutine coroutineScheduleReset;
|
||||
public void ScheduleReset(Vector3 position, float duration = 0.5f){
|
||||
UnscheduleReset();
|
||||
coroutineScheduleReset = StartCoroutine(CoroutineScheduleReset(position, duration));
|
||||
}
|
||||
|
||||
public void UnscheduleReset(){
|
||||
if(coroutineScheduleReset != null){
|
||||
StopCoroutine(coroutineScheduleReset);
|
||||
coroutineScheduleReset = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void CancelInGoalReset()
|
||||
{
|
||||
UnscheduleReset();
|
||||
if (coroutineReset != null)
|
||||
{
|
||||
StopCoroutine(coroutineReset);
|
||||
RestoreAfterReset();
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator CoroutineScheduleReset(Vector3 position, float duration){
|
||||
while(GameManager.isMoving){
|
||||
if (GameManager.instance != null && !GameManager.instance.CanProcessGoal)
|
||||
yield break;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
if (GameManager.instance != null && !GameManager.instance.CanProcessGoal)
|
||||
yield break;
|
||||
|
||||
coroutineScheduleReset = null;
|
||||
Debug.Log("Resetting puck", gameObject);
|
||||
Vector3 dir = position - transform.position;
|
||||
Vector3 centerDir = Vector3.zero - transform.position;
|
||||
|
||||
dir = dir.normalized;
|
||||
centerDir = centerDir.normalized;
|
||||
|
||||
// Clamp direction to within 30 degrees of centerDir if needed
|
||||
float maxAngle = 30f;
|
||||
float angle = Vector3.Angle(dir, centerDir);
|
||||
if (angle > maxAngle)
|
||||
{
|
||||
dir = Vector3.Slerp(centerDir, dir, maxAngle / angle).normalized;
|
||||
}
|
||||
|
||||
position += dir * 1.25f;
|
||||
Reset(position, duration);
|
||||
|
||||
}
|
||||
|
||||
IEnumerator CoroutineReset(Vector3 pos, float duration){
|
||||
if (GameManager.instance != null)
|
||||
{
|
||||
GameManager.instance.PushResetCollisionLock();
|
||||
holdingCollisionLock = true;
|
||||
}
|
||||
|
||||
rb.linearVelocity = Vector2.zero;
|
||||
rb.angularVelocity = 0f;
|
||||
|
||||
float t=0;
|
||||
Vector3 pos1 = transform.position;
|
||||
Quaternion rot1 = transform.rotation;
|
||||
while(t<duration){
|
||||
t+=Time.deltaTime;
|
||||
float u = Mathf.Clamp01(t/duration);
|
||||
transform.position = Vector3.Lerp(pos1, pos, u);
|
||||
transform.rotation = Quaternion.Lerp(rot1, Quaternion.identity, u);
|
||||
yield return null;
|
||||
}
|
||||
transform.position = pos;
|
||||
transform.rotation = Quaternion.identity;
|
||||
|
||||
RestoreAfterReset();
|
||||
}
|
||||
|
||||
void RestoreAfterReset()
|
||||
{
|
||||
if (rb != null)
|
||||
{
|
||||
rb.linearVelocity = Vector2.zero;
|
||||
rb.angularVelocity = 0f;
|
||||
}
|
||||
if (holdingCollisionLock && GameManager.instance != null)
|
||||
{
|
||||
GameManager.instance.PopResetCollisionLock();
|
||||
holdingCollisionLock = false;
|
||||
}
|
||||
coroutineReset = null;
|
||||
}
|
||||
}
|
||||
|
||||
public enum Team{
|
||||
Red,
|
||||
Blue
|
||||
using System.Collections;
|
||||
using Mirror;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
|
||||
public class Puck : NetworkBehaviour
|
||||
{
|
||||
[HideInInspector]public Rigidbody2D rb;
|
||||
Collider2D col;
|
||||
[SyncVar]
|
||||
public bool isSelected;
|
||||
public LineRenderer lineRenderer;
|
||||
[SyncVar]
|
||||
public Team team;
|
||||
public GameObject redTeamMarker;
|
||||
public GameObject blueTeamMarker;
|
||||
public Transform selectedMarker;
|
||||
public float markerRotateSpeed;
|
||||
public float markerBounceIntensity;
|
||||
public float markerBounceSpeed;
|
||||
Vector3 markerStartScale;
|
||||
Vector3 startPosition;
|
||||
float bounceSinA;
|
||||
void OnValidate()
|
||||
{
|
||||
UpdateTeam();
|
||||
}
|
||||
|
||||
void Awake()
|
||||
{
|
||||
UpdateTeam();
|
||||
markerStartScale = selectedMarker.localScale;
|
||||
rb=GetComponent<Rigidbody2D>();
|
||||
col = GetComponent<Collider2D>();
|
||||
startPosition = transform.position;
|
||||
}
|
||||
|
||||
|
||||
void UpdateTeam(){
|
||||
redTeamMarker.SetActive(team == Team.Red);
|
||||
blueTeamMarker.SetActive(team == Team.Blue);
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
GameEvents.OnSelectedPuckChanged += OnSelectedPuckChanged;
|
||||
GameManager.OnTeamChanged += OnTeamChanged;
|
||||
|
||||
GameManager.instance.RegisterPuck(this);
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
GameEvents.OnSelectedPuckChanged -= OnSelectedPuckChanged;
|
||||
GameManager.OnTeamChanged -= OnTeamChanged;
|
||||
|
||||
RestoreAfterReset();
|
||||
if (GameManager.instance != null)
|
||||
GameManager.instance.DisposePuck(this);
|
||||
}
|
||||
public Vector3 lineEnd;
|
||||
void Update()
|
||||
{
|
||||
if (isSelected && lineRenderer != null)
|
||||
{
|
||||
Vector3 worldEnd = Camera.main.ScreenToWorldPoint(new Vector3(NetPlayer.curPosition.x, NetPlayer.curPosition.y, Camera.main.nearClipPlane));
|
||||
|
||||
lineEnd = transform.InverseTransformPoint(worldEnd);
|
||||
float clamp = GameManager.instance.puckDragClampMax;
|
||||
lineEnd = Vector2.ClampMagnitude(lineEnd, clamp);
|
||||
|
||||
if (NetPlayer.localPlayer != null
|
||||
&& team == NetPlayer.localPlayer.myTeam
|
||||
&& GameManager.instance.SelectedTeam == team)
|
||||
{
|
||||
float baseStretch = lineEnd.magnitude / clamp;
|
||||
float distanceMultiplier = GameManager.instance.GetCameraStretchDistanceMultiplier(transform.position);
|
||||
CameraEffects.instance.SetStretchFactor(baseStretch * distanceMultiplier);
|
||||
}
|
||||
|
||||
// Only two points for the line
|
||||
lineRenderer.positionCount = 2;
|
||||
lineRenderer.SetPosition(0, Vector3.zero);
|
||||
lineRenderer.SetPosition(1, new Vector3(lineEnd.x, lineEnd.y, 0));
|
||||
}
|
||||
|
||||
selectedMarker.gameObject.SetActive(GetMarkerVisibility());
|
||||
if(GameManager.instance.SelectedTeam == team){
|
||||
selectedMarker.Rotate(new Vector3(0,0,1), markerRotateSpeed * Time.deltaTime);
|
||||
bounceSinA += Time.deltaTime * markerBounceSpeed;
|
||||
float bounce = Mathf.Sin(bounceSinA) * markerBounceIntensity;
|
||||
selectedMarker.localScale = markerStartScale + (Vector3.one * bounce);
|
||||
}
|
||||
}
|
||||
|
||||
bool GetMarkerVisibility(){
|
||||
if(GameManager.instance.m_isMoving){return false;}
|
||||
if(NetPlayer.localPlayer == null){return false;}
|
||||
if(NetPlayer.localPlayer.myTeam != team){return false;}
|
||||
|
||||
if(GameManager.instance.SelectedTeam == team){
|
||||
if(GameManager.NoPuckSelected){
|
||||
return true;
|
||||
}else{
|
||||
return isSelected;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void OnSelectedPuckChanged(Puck puck)
|
||||
{
|
||||
isSelected = puck == this;
|
||||
lineRenderer.enabled = isSelected;
|
||||
|
||||
if(NetPlayer.localPlayer == null){return;}
|
||||
if(team != NetPlayer.localPlayer.myTeam){
|
||||
lineRenderer.widthMultiplier = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void OnTeamChanged(Team team){
|
||||
|
||||
}
|
||||
|
||||
void OnCollisionEnter2D(Collision2D collision)
|
||||
{
|
||||
if (!collision.collider.CompareTag("Puck"))
|
||||
return;
|
||||
// Both pucks get this callback; play once per contact.
|
||||
if (gameObject.GetInstanceID() >= collision.gameObject.GetInstanceID())
|
||||
return;
|
||||
|
||||
float otherSpeed = collision.relativeVelocity.magnitude;
|
||||
Debug.Log("Puck collision speed: " + otherSpeed);
|
||||
float maxVolSpeed =5f;
|
||||
float vol = Mathf.Clamp(otherSpeed / maxVolSpeed,0.1f,1);
|
||||
if (AudioManager.instance != null){
|
||||
AudioManager.instance.PlayPuckHit(vol);
|
||||
if(isServer){
|
||||
ReplayRecorder.RecordHit("puck_puck", vol, this);
|
||||
RpcPuckHitAudio(vol);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[ClientRpc]
|
||||
void RpcPuckHitAudio(float vol){
|
||||
AudioManager.instance.PlayPuckHit(vol);
|
||||
}
|
||||
|
||||
Coroutine coroutineReset;
|
||||
bool holdingCollisionLock;
|
||||
public void Reset(Vector3? position = null, float duration = 0.5f)
|
||||
{
|
||||
UnscheduleReset();
|
||||
Vector3 pos = position ?? startPosition;
|
||||
if (coroutineReset != null)
|
||||
{
|
||||
StopCoroutine(coroutineReset);
|
||||
RestoreAfterReset();
|
||||
}
|
||||
coroutineReset = StartCoroutine(CoroutineReset(pos, duration));
|
||||
}
|
||||
|
||||
public void SetCollisionsEnabled(bool enabled)
|
||||
{
|
||||
if (col != null)
|
||||
col.enabled = enabled;
|
||||
if (rb != null && !enabled)
|
||||
{
|
||||
rb.linearVelocity = Vector2.zero;
|
||||
rb.angularVelocity = 0f;
|
||||
}
|
||||
}
|
||||
Coroutine coroutineScheduleReset;
|
||||
public void ScheduleReset(Vector3 position, float duration = 0.5f){
|
||||
UnscheduleReset();
|
||||
coroutineScheduleReset = StartCoroutine(CoroutineScheduleReset(position, duration));
|
||||
}
|
||||
|
||||
public void UnscheduleReset(){
|
||||
if(coroutineScheduleReset != null){
|
||||
StopCoroutine(coroutineScheduleReset);
|
||||
coroutineScheduleReset = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void CancelInGoalReset()
|
||||
{
|
||||
UnscheduleReset();
|
||||
if (coroutineReset != null)
|
||||
{
|
||||
StopCoroutine(coroutineReset);
|
||||
RestoreAfterReset();
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator CoroutineScheduleReset(Vector3 position, float duration){
|
||||
while(GameManager.isMoving){
|
||||
if (GameManager.instance != null && !GameManager.instance.CanProcessGoal)
|
||||
yield break;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
if (GameManager.instance != null && !GameManager.instance.CanProcessGoal)
|
||||
yield break;
|
||||
|
||||
coroutineScheduleReset = null;
|
||||
Debug.Log("Resetting puck", gameObject);
|
||||
Vector3 dir = position - transform.position;
|
||||
Vector3 centerDir = Vector3.zero - transform.position;
|
||||
|
||||
dir = dir.normalized;
|
||||
centerDir = centerDir.normalized;
|
||||
|
||||
// Clamp direction to within 30 degrees of centerDir if needed
|
||||
float maxAngle = 30f;
|
||||
float angle = Vector3.Angle(dir, centerDir);
|
||||
if (angle > maxAngle)
|
||||
{
|
||||
dir = Vector3.Slerp(centerDir, dir, maxAngle / angle).normalized;
|
||||
}
|
||||
|
||||
position += dir * 1.25f;
|
||||
Reset(position, duration);
|
||||
|
||||
}
|
||||
|
||||
IEnumerator CoroutineReset(Vector3 pos, float duration){
|
||||
if (GameManager.instance != null)
|
||||
{
|
||||
GameManager.instance.PushResetCollisionLock();
|
||||
holdingCollisionLock = true;
|
||||
}
|
||||
|
||||
rb.linearVelocity = Vector2.zero;
|
||||
rb.angularVelocity = 0f;
|
||||
|
||||
float t=0;
|
||||
Vector3 pos1 = transform.position;
|
||||
Quaternion rot1 = transform.rotation;
|
||||
while(t<duration){
|
||||
t+=Time.deltaTime;
|
||||
float u = Mathf.Clamp01(t/duration);
|
||||
transform.position = Vector3.Lerp(pos1, pos, u);
|
||||
transform.rotation = Quaternion.Lerp(rot1, Quaternion.identity, u);
|
||||
yield return null;
|
||||
}
|
||||
transform.position = pos;
|
||||
transform.rotation = Quaternion.identity;
|
||||
|
||||
RestoreAfterReset();
|
||||
}
|
||||
|
||||
void RestoreAfterReset()
|
||||
{
|
||||
if (rb != null)
|
||||
{
|
||||
rb.linearVelocity = Vector2.zero;
|
||||
rb.angularVelocity = 0f;
|
||||
}
|
||||
if (holdingCollisionLock && GameManager.instance != null)
|
||||
{
|
||||
GameManager.instance.PopResetCollisionLock();
|
||||
holdingCollisionLock = false;
|
||||
}
|
||||
coroutineReset = null;
|
||||
}
|
||||
}
|
||||
|
||||
public enum Team{
|
||||
Red,
|
||||
Blue
|
||||
}
|
||||
@@ -1,2 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
fileFormatVersion: 2
|
||||
guid: 4b7edf42d59a3c942b1f2f66b31aaa75
|
||||
@@ -1,8 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9cff4a867ddc05341bf42777b654509a
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
fileFormatVersion: 2
|
||||
guid: 9cff4a867ddc05341bf42777b654509a
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
@@ -1,291 +1,291 @@
|
||||
# Match Replay JSON Format
|
||||
|
||||
Guide for implementing the admin-panel replay viewer. Files are written by the Unity dedicated server (and practice host) to disk only.
|
||||
|
||||
## Where files live
|
||||
|
||||
Written next to the match log with the **same basename**, only the extension differs:
|
||||
|
||||
```
|
||||
{ApplicationDirectory}/Logs/{matchId}.txt ← match log
|
||||
{ApplicationDirectory}/Logs/{matchId}.json ← replay
|
||||
```
|
||||
|
||||
| Match type | `matchId` | Example pair |
|
||||
|------------|-----------|--------------|
|
||||
| Ranked / dedicated | Positive matchmaker id | `Logs/228.txt` + `Logs/228.json` |
|
||||
| Practice / tutorial | Negative unix timestamp | `Logs/-1769539200.txt` + `Logs/-1769539200.json` |
|
||||
|
||||
Admin panel: given a log at `Logs/{id}.txt`, load `Logs/{id}.json` beside it.
|
||||
|
||||
## Top-level object
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"matchId": 228,
|
||||
"isPractice": false,
|
||||
"fixedDeltaTime": 0.02,
|
||||
"duration": 184.32,
|
||||
"entities": [ /* ... */ ],
|
||||
"frames": [ /* ... */ ],
|
||||
"events": [ /* ... */ ]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `version` | int | Schema version. Currently `1`. |
|
||||
| `matchId` | int | Match id (positive = ranked, negative = practice). |
|
||||
| `isPractice` | bool | `true` for localhost practice / tutorial host. |
|
||||
| `fixedDeltaTime` | float | Physics step used while recording (typically `0.02` → ~50 Hz). |
|
||||
| `duration` | float | Length of the recording in seconds (`t` of last sample / end). |
|
||||
| `entities` | array | Ball + pucks. Order defines pose array indices in every frame. |
|
||||
| `frames` | array | Time-ordered pose samples (~every FixedUpdate). |
|
||||
| `events` | array | Time-ordered discrete events (hits, goals, resets). |
|
||||
|
||||
Time `t` everywhere is **seconds since recording start** (match start), not wall-clock UTC.
|
||||
|
||||
---
|
||||
|
||||
## Entities
|
||||
|
||||
```json
|
||||
{ "id": 0, "type": "ball", "team": "" }
|
||||
{ "id": 1, "type": "puck", "team": "Red" }
|
||||
{ "id": 2, "type": "puck", "team": "Blue" }
|
||||
```
|
||||
|
||||
| Field | Type | Values |
|
||||
|-------|------|--------|
|
||||
| `id` | int | Stable index `0 .. N-1`. Matches array index in each frame’s `x`/`y`/`vx`/`vy`. |
|
||||
| `type` | string | `"ball"` or `"puck"` |
|
||||
| `team` | string | `"Red"`, `"Blue"`, or `""` for ball |
|
||||
|
||||
Entity list is fixed for the whole file. **Index `i` in a frame’s pose arrays always refers to `entities[i]`.**
|
||||
|
||||
Typical order: ball first, then pucks sorted by team then spawn position.
|
||||
|
||||
---
|
||||
|
||||
## Frames (motion samples)
|
||||
|
||||
```json
|
||||
{
|
||||
"t": 12.40,
|
||||
"x": [0.0, 1.2, -0.5],
|
||||
"y": [0.1, -0.3, 2.0],
|
||||
"vx": [0.0, 3.1, -1.0],
|
||||
"vy": [0.2, 0.0, 0.5]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `t` | float | Sample time in seconds (monotonic, increasing). |
|
||||
| `x`, `y` | float[] | World position per entity (Unity 2D world units). |
|
||||
| `vx`, `vy` | float[] | Linear velocity per entity at that sample (world units / second). |
|
||||
|
||||
- Array length always equals `entities.length`.
|
||||
- Samples are dense (~`1 / fixedDeltaTime` Hz). Do **not** treat them as render frames.
|
||||
|
||||
### Coordinate system
|
||||
|
||||
Unity 2D world space:
|
||||
|
||||
- Origin at field center
|
||||
- `+X` right, `+Y` up (as in Unity)
|
||||
- Scale matches the in-game pitch (see game `fieldSize` / art for admin canvas mapping)
|
||||
|
||||
---
|
||||
|
||||
## Events
|
||||
|
||||
```json
|
||||
{ "t": 12.4, "type": "hit", "kind": "ball_wall", "vol": 0.6, "id": 0, "team": "", "redScore": 0, "blueScore": 0, "kickoff": false }
|
||||
{ "t": 30.1, "type": "goal", "kind": "", "vol": 0, "id": -1, "team": "Blue", "redScore": 1, "blueScore": 0, "kickoff": false }
|
||||
{ "t": 32.2, "type": "reset", "kind": "", "vol": 0, "id": -1, "team": "", "redScore": 0, "blueScore": 0, "kickoff": false }
|
||||
```
|
||||
|
||||
All event objects share the same fields (Unity `JsonUtility` shape). Unused fields are empty / zero / `-1`.
|
||||
|
||||
| Field | Used by | Description |
|
||||
|-------|---------|-------------|
|
||||
| `t` | all | Event time in seconds. |
|
||||
| `type` | all | `"hit"` \| `"goal"` \| `"reset"` |
|
||||
| `kind` | `hit` | `"puck_puck"` \| `"ball_puck"` \| `"ball_wall"` |
|
||||
| `vol` | `hit` | SFX volume `0..1` (from relative impact speed). |
|
||||
| `id` | `hit` | Entity id involved (spatial cue). `-1` if unknown. |
|
||||
| `team` | `goal` | **Conceding** side / goal owner (`"Red"` or `"Blue"`). The other team scored. |
|
||||
| `redScore` | `goal` | Score after this goal. |
|
||||
| `blueScore` | `goal` | Score after this goal. |
|
||||
| `kickoff` | `goal`, `reset` | Kickoff-goal / kickoff-style reset when `true`. |
|
||||
|
||||
### Event semantics
|
||||
|
||||
| `type` | Meaning for the viewer |
|
||||
|--------|-------------------------|
|
||||
| `hit` | Play collision SFX at `vol`. Optional: flash near entity `id`. |
|
||||
| `goal` | Goal scored against `team`. Update scoreboard to `redScore`/`blueScore`. Play goal SFX. **Discontinuity** — do not interpolate motion across this time. |
|
||||
| `reset` | Pieces are lerping / teleporting back. **Discontinuity** — snap poses; do not blend across. |
|
||||
|
||||
First-to-3 wins; a completed match usually ends shortly after a goal that makes a score `3`.
|
||||
|
||||
---
|
||||
|
||||
## Playback contract (required for smooth motion)
|
||||
|
||||
Recording is FixedUpdate (~50 Hz). Snapping happens if the viewer steps sample-by-sample without blending. Follow this:
|
||||
|
||||
### 1. Drive time with wall clock
|
||||
|
||||
```text
|
||||
playbackTime += (deltaMs / 1000) * playbackSpeed
|
||||
playbackTime = clamp(playbackTime, 0, duration)
|
||||
```
|
||||
|
||||
Use `requestAnimationFrame` (or equivalent). Do **not** advance one JSON frame per rAF tick.
|
||||
|
||||
### 2. Interpolate poses between samples
|
||||
|
||||
Binary-search `frames` for neighbors where:
|
||||
|
||||
```text
|
||||
frames[i].t <= playbackTime < frames[i+1].t
|
||||
```
|
||||
|
||||
Then for each entity index `e`:
|
||||
|
||||
**Minimum (good):** linear lerp of `x`/`y`:
|
||||
|
||||
```text
|
||||
u = (playbackTime - t0) / (t1 - t0)
|
||||
x = lerp(x0[e], x1[e], u)
|
||||
y = lerp(y0[e], y1[e], u)
|
||||
```
|
||||
|
||||
**Recommended (smooth under acceleration):** cubic Hermite using velocities:
|
||||
|
||||
```text
|
||||
dt = t1 - t0
|
||||
u = (playbackTime - t0) / dt
|
||||
u2 = u*u
|
||||
u3 = u2*u
|
||||
|
||||
h00 = 2*u3 - 3*u2 + 1
|
||||
h10 = u3 - 2*u2 + u
|
||||
h01 = -2*u3 + 3*u2
|
||||
h11 = u3 - u2
|
||||
|
||||
x = h00*x0 + h10*dt*vx0 + h01*x1 + h11*dt*vx1
|
||||
y = h00*y0 + h10*dt*vy0 + h01*y1 + h11*dt*vy1
|
||||
```
|
||||
|
||||
(Same for each entity index.)
|
||||
|
||||
### 3. Snap across discontinuities
|
||||
|
||||
Treat every `goal` and `reset` event as a cut:
|
||||
|
||||
- When `playbackTime` crosses that event’s `t`, **do not** interpolate from the previous sample across the cut.
|
||||
- Seek to the frame at or immediately after `t` and snap positions.
|
||||
|
||||
Practical approach: build a sorted list of discontinuity times from events where `type === "goal" || type === "reset"`. If `t0` and `t1` straddle a discontinuity, snap to the post-cut sample instead of lerping.
|
||||
|
||||
### 4. Fire events once when crossed
|
||||
|
||||
Keep `lastEventIndex` (or last fired `t`). When `playbackTime` advances past an event’s `t`, fire it once:
|
||||
|
||||
- `hit` → SFX by `kind` / `vol`
|
||||
- `goal` → score UI + goal audio
|
||||
- `reset` → optional reset cue (usually silent)
|
||||
|
||||
On seek / scrub: reset event cursor; either skip SFX or replay only from the new time forward.
|
||||
|
||||
---
|
||||
|
||||
## Suggested viewer pipeline
|
||||
|
||||
```text
|
||||
Load JSON
|
||||
→ map entities to sprites (ball / red pucks / blue pucks)
|
||||
→ sort events by t (already ordered, but verify)
|
||||
→ each animation frame:
|
||||
advance playbackTime
|
||||
resolve poses (Hermite + discontinuity snaps)
|
||||
draw entities
|
||||
dispatch newly crossed events
|
||||
```
|
||||
|
||||
### Seeking / scrubbing
|
||||
|
||||
1. Set `playbackTime` to the scrub value.
|
||||
2. Find the nearest frame (or interpolate as above).
|
||||
3. Recompute score from the last `goal` event with `t <= playbackTime` (else `0–0`).
|
||||
4. Reset the event-fire cursor to that time so SFX don’t spam.
|
||||
|
||||
### Scoreboard without scrubbing
|
||||
|
||||
Start at `0–0`. On each `goal` event, set scores from that event’s `redScore` / `blueScore`.
|
||||
|
||||
---
|
||||
|
||||
## Minimal TypeScript types
|
||||
|
||||
```ts
|
||||
export interface ReplayFile {
|
||||
version: number;
|
||||
matchId: number;
|
||||
isPractice: boolean;
|
||||
fixedDeltaTime: number;
|
||||
duration: number;
|
||||
entities: ReplayEntity[];
|
||||
frames: ReplayFrame[];
|
||||
events: ReplayEvent[];
|
||||
}
|
||||
|
||||
export interface ReplayEntity {
|
||||
id: number;
|
||||
type: "ball" | "puck" | string;
|
||||
team: "Red" | "Blue" | "" | string;
|
||||
}
|
||||
|
||||
export interface ReplayFrame {
|
||||
t: number;
|
||||
x: number[];
|
||||
y: number[];
|
||||
vx: number[];
|
||||
vy: number[];
|
||||
}
|
||||
|
||||
export interface ReplayEvent {
|
||||
t: number;
|
||||
type: "hit" | "goal" | "reset" | string;
|
||||
kind: "puck_puck" | "ball_puck" | "ball_wall" | "" | string;
|
||||
vol: number;
|
||||
id: number; // entity id, or -1
|
||||
team: "Red" | "Blue" | "" | string; // conceding team on goal
|
||||
redScore: number;
|
||||
blueScore: number;
|
||||
kickoff: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Checklist for a correct viewer
|
||||
|
||||
- [ ] Time driven by real elapsed time × speed, not frame index
|
||||
- [ ] Poses interpolated between samples (Hermite preferred)
|
||||
- [ ] No interpolation across `goal` / `reset`
|
||||
- [ ] Entity index `i` always matches `entities[i]`
|
||||
- [ ] Hit SFX use `kind` + `vol`
|
||||
- [ ] Goal `team` means **conceding** side
|
||||
- [ ] Practice files filtered via `isPractice` or negative `matchId`
|
||||
|
||||
## Out of scope (not in the JSON)
|
||||
|
||||
- Player names / user ids (use matchmaker / match API by `matchId` for ranked games)
|
||||
- Camera / UI layout
|
||||
- Launch aim lines / turn timer
|
||||
- Network latency or client prediction
|
||||
# Match Replay JSON Format
|
||||
|
||||
Guide for implementing the admin-panel replay viewer. Files are written by the Unity dedicated server (and practice host) to disk only.
|
||||
|
||||
## Where files live
|
||||
|
||||
Written next to the match log with the **same basename**, only the extension differs:
|
||||
|
||||
```
|
||||
{ApplicationDirectory}/Logs/{matchId}.txt ← match log
|
||||
{ApplicationDirectory}/Logs/{matchId}.json ← replay
|
||||
```
|
||||
|
||||
| Match type | `matchId` | Example pair |
|
||||
|------------|-----------|--------------|
|
||||
| Ranked / dedicated | Positive matchmaker id | `Logs/228.txt` + `Logs/228.json` |
|
||||
| Practice / tutorial | Negative unix timestamp | `Logs/-1769539200.txt` + `Logs/-1769539200.json` |
|
||||
|
||||
Admin panel: given a log at `Logs/{id}.txt`, load `Logs/{id}.json` beside it.
|
||||
|
||||
## Top-level object
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"matchId": 228,
|
||||
"isPractice": false,
|
||||
"fixedDeltaTime": 0.02,
|
||||
"duration": 184.32,
|
||||
"entities": [ /* ... */ ],
|
||||
"frames": [ /* ... */ ],
|
||||
"events": [ /* ... */ ]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `version` | int | Schema version. Currently `1`. |
|
||||
| `matchId` | int | Match id (positive = ranked, negative = practice). |
|
||||
| `isPractice` | bool | `true` for localhost practice / tutorial host. |
|
||||
| `fixedDeltaTime` | float | Physics step used while recording (typically `0.02` → ~50 Hz). |
|
||||
| `duration` | float | Length of the recording in seconds (`t` of last sample / end). |
|
||||
| `entities` | array | Ball + pucks. Order defines pose array indices in every frame. |
|
||||
| `frames` | array | Time-ordered pose samples (~every FixedUpdate). |
|
||||
| `events` | array | Time-ordered discrete events (hits, goals, resets). |
|
||||
|
||||
Time `t` everywhere is **seconds since recording start** (match start), not wall-clock UTC.
|
||||
|
||||
---
|
||||
|
||||
## Entities
|
||||
|
||||
```json
|
||||
{ "id": 0, "type": "ball", "team": "" }
|
||||
{ "id": 1, "type": "puck", "team": "Red" }
|
||||
{ "id": 2, "type": "puck", "team": "Blue" }
|
||||
```
|
||||
|
||||
| Field | Type | Values |
|
||||
|-------|------|--------|
|
||||
| `id` | int | Stable index `0 .. N-1`. Matches array index in each frame’s `x`/`y`/`vx`/`vy`. |
|
||||
| `type` | string | `"ball"` or `"puck"` |
|
||||
| `team` | string | `"Red"`, `"Blue"`, or `""` for ball |
|
||||
|
||||
Entity list is fixed for the whole file. **Index `i` in a frame’s pose arrays always refers to `entities[i]`.**
|
||||
|
||||
Typical order: ball first, then pucks sorted by team then spawn position.
|
||||
|
||||
---
|
||||
|
||||
## Frames (motion samples)
|
||||
|
||||
```json
|
||||
{
|
||||
"t": 12.40,
|
||||
"x": [0.0, 1.2, -0.5],
|
||||
"y": [0.1, -0.3, 2.0],
|
||||
"vx": [0.0, 3.1, -1.0],
|
||||
"vy": [0.2, 0.0, 0.5]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `t` | float | Sample time in seconds (monotonic, increasing). |
|
||||
| `x`, `y` | float[] | World position per entity (Unity 2D world units). |
|
||||
| `vx`, `vy` | float[] | Linear velocity per entity at that sample (world units / second). |
|
||||
|
||||
- Array length always equals `entities.length`.
|
||||
- Samples are dense (~`1 / fixedDeltaTime` Hz). Do **not** treat them as render frames.
|
||||
|
||||
### Coordinate system
|
||||
|
||||
Unity 2D world space:
|
||||
|
||||
- Origin at field center
|
||||
- `+X` right, `+Y` up (as in Unity)
|
||||
- Scale matches the in-game pitch (see game `fieldSize` / art for admin canvas mapping)
|
||||
|
||||
---
|
||||
|
||||
## Events
|
||||
|
||||
```json
|
||||
{ "t": 12.4, "type": "hit", "kind": "ball_wall", "vol": 0.6, "id": 0, "team": "", "redScore": 0, "blueScore": 0, "kickoff": false }
|
||||
{ "t": 30.1, "type": "goal", "kind": "", "vol": 0, "id": -1, "team": "Blue", "redScore": 1, "blueScore": 0, "kickoff": false }
|
||||
{ "t": 32.2, "type": "reset", "kind": "", "vol": 0, "id": -1, "team": "", "redScore": 0, "blueScore": 0, "kickoff": false }
|
||||
```
|
||||
|
||||
All event objects share the same fields (Unity `JsonUtility` shape). Unused fields are empty / zero / `-1`.
|
||||
|
||||
| Field | Used by | Description |
|
||||
|-------|---------|-------------|
|
||||
| `t` | all | Event time in seconds. |
|
||||
| `type` | all | `"hit"` \| `"goal"` \| `"reset"` |
|
||||
| `kind` | `hit` | `"puck_puck"` \| `"ball_puck"` \| `"ball_wall"` |
|
||||
| `vol` | `hit` | SFX volume `0..1` (from relative impact speed). |
|
||||
| `id` | `hit` | Entity id involved (spatial cue). `-1` if unknown. |
|
||||
| `team` | `goal` | **Conceding** side / goal owner (`"Red"` or `"Blue"`). The other team scored. |
|
||||
| `redScore` | `goal` | Score after this goal. |
|
||||
| `blueScore` | `goal` | Score after this goal. |
|
||||
| `kickoff` | `goal`, `reset` | Kickoff-goal / kickoff-style reset when `true`. |
|
||||
|
||||
### Event semantics
|
||||
|
||||
| `type` | Meaning for the viewer |
|
||||
|--------|-------------------------|
|
||||
| `hit` | Play collision SFX at `vol`. Optional: flash near entity `id`. |
|
||||
| `goal` | Goal scored against `team`. Update scoreboard to `redScore`/`blueScore`. Play goal SFX. **Discontinuity** — do not interpolate motion across this time. |
|
||||
| `reset` | Pieces are lerping / teleporting back. **Discontinuity** — snap poses; do not blend across. |
|
||||
|
||||
First-to-3 wins; a completed match usually ends shortly after a goal that makes a score `3`.
|
||||
|
||||
---
|
||||
|
||||
## Playback contract (required for smooth motion)
|
||||
|
||||
Recording is FixedUpdate (~50 Hz). Snapping happens if the viewer steps sample-by-sample without blending. Follow this:
|
||||
|
||||
### 1. Drive time with wall clock
|
||||
|
||||
```text
|
||||
playbackTime += (deltaMs / 1000) * playbackSpeed
|
||||
playbackTime = clamp(playbackTime, 0, duration)
|
||||
```
|
||||
|
||||
Use `requestAnimationFrame` (or equivalent). Do **not** advance one JSON frame per rAF tick.
|
||||
|
||||
### 2. Interpolate poses between samples
|
||||
|
||||
Binary-search `frames` for neighbors where:
|
||||
|
||||
```text
|
||||
frames[i].t <= playbackTime < frames[i+1].t
|
||||
```
|
||||
|
||||
Then for each entity index `e`:
|
||||
|
||||
**Minimum (good):** linear lerp of `x`/`y`:
|
||||
|
||||
```text
|
||||
u = (playbackTime - t0) / (t1 - t0)
|
||||
x = lerp(x0[e], x1[e], u)
|
||||
y = lerp(y0[e], y1[e], u)
|
||||
```
|
||||
|
||||
**Recommended (smooth under acceleration):** cubic Hermite using velocities:
|
||||
|
||||
```text
|
||||
dt = t1 - t0
|
||||
u = (playbackTime - t0) / dt
|
||||
u2 = u*u
|
||||
u3 = u2*u
|
||||
|
||||
h00 = 2*u3 - 3*u2 + 1
|
||||
h10 = u3 - 2*u2 + u
|
||||
h01 = -2*u3 + 3*u2
|
||||
h11 = u3 - u2
|
||||
|
||||
x = h00*x0 + h10*dt*vx0 + h01*x1 + h11*dt*vx1
|
||||
y = h00*y0 + h10*dt*vy0 + h01*y1 + h11*dt*vy1
|
||||
```
|
||||
|
||||
(Same for each entity index.)
|
||||
|
||||
### 3. Snap across discontinuities
|
||||
|
||||
Treat every `goal` and `reset` event as a cut:
|
||||
|
||||
- When `playbackTime` crosses that event’s `t`, **do not** interpolate from the previous sample across the cut.
|
||||
- Seek to the frame at or immediately after `t` and snap positions.
|
||||
|
||||
Practical approach: build a sorted list of discontinuity times from events where `type === "goal" || type === "reset"`. If `t0` and `t1` straddle a discontinuity, snap to the post-cut sample instead of lerping.
|
||||
|
||||
### 4. Fire events once when crossed
|
||||
|
||||
Keep `lastEventIndex` (or last fired `t`). When `playbackTime` advances past an event’s `t`, fire it once:
|
||||
|
||||
- `hit` → SFX by `kind` / `vol`
|
||||
- `goal` → score UI + goal audio
|
||||
- `reset` → optional reset cue (usually silent)
|
||||
|
||||
On seek / scrub: reset event cursor; either skip SFX or replay only from the new time forward.
|
||||
|
||||
---
|
||||
|
||||
## Suggested viewer pipeline
|
||||
|
||||
```text
|
||||
Load JSON
|
||||
→ map entities to sprites (ball / red pucks / blue pucks)
|
||||
→ sort events by t (already ordered, but verify)
|
||||
→ each animation frame:
|
||||
advance playbackTime
|
||||
resolve poses (Hermite + discontinuity snaps)
|
||||
draw entities
|
||||
dispatch newly crossed events
|
||||
```
|
||||
|
||||
### Seeking / scrubbing
|
||||
|
||||
1. Set `playbackTime` to the scrub value.
|
||||
2. Find the nearest frame (or interpolate as above).
|
||||
3. Recompute score from the last `goal` event with `t <= playbackTime` (else `0–0`).
|
||||
4. Reset the event-fire cursor to that time so SFX don’t spam.
|
||||
|
||||
### Scoreboard without scrubbing
|
||||
|
||||
Start at `0–0`. On each `goal` event, set scores from that event’s `redScore` / `blueScore`.
|
||||
|
||||
---
|
||||
|
||||
## Minimal TypeScript types
|
||||
|
||||
```ts
|
||||
export interface ReplayFile {
|
||||
version: number;
|
||||
matchId: number;
|
||||
isPractice: boolean;
|
||||
fixedDeltaTime: number;
|
||||
duration: number;
|
||||
entities: ReplayEntity[];
|
||||
frames: ReplayFrame[];
|
||||
events: ReplayEvent[];
|
||||
}
|
||||
|
||||
export interface ReplayEntity {
|
||||
id: number;
|
||||
type: "ball" | "puck" | string;
|
||||
team: "Red" | "Blue" | "" | string;
|
||||
}
|
||||
|
||||
export interface ReplayFrame {
|
||||
t: number;
|
||||
x: number[];
|
||||
y: number[];
|
||||
vx: number[];
|
||||
vy: number[];
|
||||
}
|
||||
|
||||
export interface ReplayEvent {
|
||||
t: number;
|
||||
type: "hit" | "goal" | "reset" | string;
|
||||
kind: "puck_puck" | "ball_puck" | "ball_wall" | "" | string;
|
||||
vol: number;
|
||||
id: number; // entity id, or -1
|
||||
team: "Red" | "Blue" | "" | string; // conceding team on goal
|
||||
redScore: number;
|
||||
blueScore: number;
|
||||
kickoff: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Checklist for a correct viewer
|
||||
|
||||
- [ ] Time driven by real elapsed time × speed, not frame index
|
||||
- [ ] Poses interpolated between samples (Hermite preferred)
|
||||
- [ ] No interpolation across `goal` / `reset`
|
||||
- [ ] Entity index `i` always matches `entities[i]`
|
||||
- [ ] Hit SFX use `kind` + `vol`
|
||||
- [ ] Goal `team` means **conceding** side
|
||||
- [ ] Practice files filtered via `isPractice` or negative `matchId`
|
||||
|
||||
## Out of scope (not in the JSON)
|
||||
|
||||
- Player names / user ids (use matchmaker / match API by `matchId` for ranked games)
|
||||
- Camera / UI layout
|
||||
- Launch aim lines / turn timer
|
||||
- Network latency or client prediction
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8cced32e9b154814b99db72a5ae58c70
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
fileFormatVersion: 2
|
||||
guid: 8cced32e9b154814b99db72a5ae58c70
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a1b2c3d4e5f60718293a4b5c6d7e8f90
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
fileFormatVersion: 2
|
||||
guid: a1b2c3d4e5f60718293a4b5c6d7e8f90
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
@@ -1,61 +1,61 @@
|
||||
using System;
|
||||
|
||||
/// <summary>
|
||||
/// Replay JSON DTOs for admin-panel playback.
|
||||
/// Playback contract:
|
||||
/// 1. Advance playbackTime with real time (scaled).
|
||||
/// 2. Binary-search frames by t; Hermite-interpolate x/y using vx/vy between neighbors.
|
||||
/// 3. On reset/goal events (discontinuities): snap to the sample at/after t — do not blend across.
|
||||
/// 4. Fire SFX on hit/goal when playbackTime crosses event t.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class ReplayFile
|
||||
{
|
||||
public int version = 1;
|
||||
public int matchId;
|
||||
/// <summary>True for localhost practice / tutorial host matches (no dedicated matchmaker id).</summary>
|
||||
public bool isPractice;
|
||||
public float fixedDeltaTime;
|
||||
public float duration;
|
||||
public ReplayEntity[] entities;
|
||||
public ReplayFrame[] frames;
|
||||
public ReplayEvent[] events;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class ReplayEntity
|
||||
{
|
||||
public int id;
|
||||
/// <summary>"ball" or "puck"</summary>
|
||||
public string type;
|
||||
/// <summary>"Red", "Blue", or empty for ball</summary>
|
||||
public string team;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class ReplayFrame
|
||||
{
|
||||
public float t;
|
||||
public float[] x;
|
||||
public float[] y;
|
||||
public float[] vx;
|
||||
public float[] vy;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class ReplayEvent
|
||||
{
|
||||
public float t;
|
||||
/// <summary>"hit", "goal", or "reset"</summary>
|
||||
public string type;
|
||||
/// <summary>For hits: "puck_puck", "ball_puck", or "ball_wall"</summary>
|
||||
public string kind;
|
||||
public float vol;
|
||||
/// <summary>Entity id for spatial cueing; -1 when N/A</summary>
|
||||
public int id = -1;
|
||||
/// <summary>For goals: conceding team ("Red"/"Blue")</summary>
|
||||
public string team;
|
||||
public int redScore;
|
||||
public int blueScore;
|
||||
public bool kickoff;
|
||||
}
|
||||
using System;
|
||||
|
||||
/// <summary>
|
||||
/// Replay JSON DTOs for admin-panel playback.
|
||||
/// Playback contract:
|
||||
/// 1. Advance playbackTime with real time (scaled).
|
||||
/// 2. Binary-search frames by t; Hermite-interpolate x/y using vx/vy between neighbors.
|
||||
/// 3. On reset/goal events (discontinuities): snap to the sample at/after t — do not blend across.
|
||||
/// 4. Fire SFX on hit/goal when playbackTime crosses event t.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class ReplayFile
|
||||
{
|
||||
public int version = 1;
|
||||
public int matchId;
|
||||
/// <summary>True for localhost practice / tutorial host matches (no dedicated matchmaker id).</summary>
|
||||
public bool isPractice;
|
||||
public float fixedDeltaTime;
|
||||
public float duration;
|
||||
public ReplayEntity[] entities;
|
||||
public ReplayFrame[] frames;
|
||||
public ReplayEvent[] events;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class ReplayEntity
|
||||
{
|
||||
public int id;
|
||||
/// <summary>"ball" or "puck"</summary>
|
||||
public string type;
|
||||
/// <summary>"Red", "Blue", or empty for ball</summary>
|
||||
public string team;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class ReplayFrame
|
||||
{
|
||||
public float t;
|
||||
public float[] x;
|
||||
public float[] y;
|
||||
public float[] vx;
|
||||
public float[] vy;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class ReplayEvent
|
||||
{
|
||||
public float t;
|
||||
/// <summary>"hit", "goal", or "reset"</summary>
|
||||
public string type;
|
||||
/// <summary>For hits: "puck_puck", "ball_puck", or "ball_wall"</summary>
|
||||
public string kind;
|
||||
public float vol;
|
||||
/// <summary>Entity id for spatial cueing; -1 when N/A</summary>
|
||||
public int id = -1;
|
||||
/// <summary>For goals: conceding team ("Red"/"Blue")</summary>
|
||||
public string team;
|
||||
public int redScore;
|
||||
public int blueScore;
|
||||
public bool kickoff;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b2c3d4e5f60718293a4b5c6d7e8f901a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
fileFormatVersion: 2
|
||||
guid: b2c3d4e5f60718293a4b5c6d7e8f901a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
@@ -1,301 +1,301 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Mirror;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Server-only match recorder. Samples poses every FixedUpdate; flushes JSON to disk on stop.
|
||||
/// Admin playback must interpolate by time (Hermite using velocity) — never snap one FixedUpdate sample per render frame.
|
||||
/// </summary>
|
||||
public class ReplayRecorder : MonoBehaviour
|
||||
{
|
||||
public static ReplayRecorder Instance { get; private set; }
|
||||
|
||||
public static bool IsRecording => Instance != null && Instance._recording && !Instance._flushed;
|
||||
|
||||
bool _recording;
|
||||
bool _flushed;
|
||||
int _matchId;
|
||||
bool _isPractice;
|
||||
float _startFixedTime;
|
||||
float _duration;
|
||||
|
||||
readonly List<ReplayEntity> _entities = new List<ReplayEntity>();
|
||||
readonly List<Rigidbody2D> _bodies = new List<Rigidbody2D>();
|
||||
readonly Dictionary<int, int> _instanceIdToEntityId = new Dictionary<int, int>();
|
||||
readonly List<ReplayFrame> _frames = new List<ReplayFrame>();
|
||||
readonly List<ReplayEvent> _events = new List<ReplayEvent>();
|
||||
|
||||
/// <returns>True if recording started (or already recording this session).</returns>
|
||||
public static bool StartRecording(int matchId, GameManager gm)
|
||||
{
|
||||
if (!NetworkServer.active || gm == null)
|
||||
return false;
|
||||
|
||||
if (Instance == null)
|
||||
{
|
||||
var go = new GameObject("ReplayRecorder");
|
||||
DontDestroyOnLoad(go);
|
||||
Instance = go.AddComponent<ReplayRecorder>();
|
||||
}
|
||||
|
||||
Instance.Begin(matchId, gm);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void StopAndFlush()
|
||||
{
|
||||
if (Instance != null)
|
||||
Instance.Flush();
|
||||
}
|
||||
|
||||
public static void RecordHit(string kind, float vol, Component source)
|
||||
{
|
||||
if (!IsRecording)
|
||||
return;
|
||||
Instance.AddHit(kind, vol, source);
|
||||
}
|
||||
|
||||
public static void RecordGoal(Team concedingTeam, int redScore, int blueScore, bool kickoff)
|
||||
{
|
||||
if (!IsRecording)
|
||||
return;
|
||||
Instance.AddEvent(new ReplayEvent
|
||||
{
|
||||
t = Instance.CurrentT(),
|
||||
type = "goal",
|
||||
kind = "",
|
||||
vol = 0f,
|
||||
id = -1,
|
||||
team = concedingTeam.ToString(),
|
||||
redScore = redScore,
|
||||
blueScore = blueScore,
|
||||
kickoff = kickoff
|
||||
});
|
||||
}
|
||||
|
||||
public static void RecordReset(bool kickoff)
|
||||
{
|
||||
if (!IsRecording)
|
||||
return;
|
||||
Instance.AddEvent(new ReplayEvent
|
||||
{
|
||||
t = Instance.CurrentT(),
|
||||
type = "reset",
|
||||
kind = "",
|
||||
vol = 0f,
|
||||
id = -1,
|
||||
team = "",
|
||||
redScore = 0,
|
||||
blueScore = 0,
|
||||
kickoff = kickoff
|
||||
});
|
||||
}
|
||||
|
||||
public static bool TryGetEntityId(Component source, out int entityId)
|
||||
{
|
||||
entityId = -1;
|
||||
if (Instance == null || source == null)
|
||||
return false;
|
||||
return Instance._instanceIdToEntityId.TryGetValue(source.gameObject.GetInstanceID(), out entityId);
|
||||
}
|
||||
|
||||
void Begin(int matchId, GameManager gm)
|
||||
{
|
||||
if (_recording && !_flushed)
|
||||
Flush();
|
||||
|
||||
// Practice / tutorial host uses DedicatedMatchId 0 — mint a unique id so files don't overwrite.
|
||||
_isPractice = matchId <= 0 || GameTutorialManager.tutorialModeEnabled;
|
||||
if (matchId <= 0)
|
||||
matchId = GeneratePracticeMatchId();
|
||||
|
||||
_matchId = matchId;
|
||||
_recording = true;
|
||||
_flushed = false;
|
||||
_startFixedTime = Time.fixedTime;
|
||||
_duration = 0f;
|
||||
_entities.Clear();
|
||||
_bodies.Clear();
|
||||
_instanceIdToEntityId.Clear();
|
||||
_frames.Clear();
|
||||
_events.Clear();
|
||||
|
||||
// Keep log + replay siblings: Logs/{matchId}.txt and Logs/{matchId}.json
|
||||
if (_isPractice)
|
||||
Logger.SetFileName(_matchId.ToString());
|
||||
|
||||
BuildEntities(gm);
|
||||
string mode = _isPractice ? "practice" : "match";
|
||||
Logger.Log($"ReplayRecorder started ({mode}) id {_matchId} with {_entities.Count} entities");
|
||||
}
|
||||
|
||||
/// <summary>Negative unix timestamp so practice ids never collide with positive matchmaker ids.</summary>
|
||||
static int GeneratePracticeMatchId()
|
||||
{
|
||||
long unix = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
if (unix > int.MaxValue)
|
||||
unix %= int.MaxValue;
|
||||
int id = -(int)unix;
|
||||
return id == 0 ? -1 : id;
|
||||
}
|
||||
|
||||
void BuildEntities(GameManager gm)
|
||||
{
|
||||
if (gm.ball != null)
|
||||
{
|
||||
RegisterEntity(gm.ball.gameObject, gm.ball, "ball", "");
|
||||
}
|
||||
|
||||
var sorted = new List<Puck>(gm.pucks);
|
||||
sorted.Sort((a, b) =>
|
||||
{
|
||||
int teamCmp = a.team.CompareTo(b.team);
|
||||
if (teamCmp != 0)
|
||||
return teamCmp;
|
||||
int xCmp = a.transform.position.x.CompareTo(b.transform.position.x);
|
||||
if (xCmp != 0)
|
||||
return xCmp;
|
||||
return a.transform.position.y.CompareTo(b.transform.position.y);
|
||||
});
|
||||
|
||||
foreach (Puck puck in sorted)
|
||||
{
|
||||
if (puck == null || puck.rb == null)
|
||||
continue;
|
||||
RegisterEntity(puck.gameObject, puck.rb, "puck", puck.team.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
void RegisterEntity(GameObject go, Rigidbody2D rb, string type, string team)
|
||||
{
|
||||
int id = _entities.Count;
|
||||
_entities.Add(new ReplayEntity { id = id, type = type, team = team ?? "" });
|
||||
_bodies.Add(rb);
|
||||
_instanceIdToEntityId[go.GetInstanceID()] = id;
|
||||
}
|
||||
|
||||
float CurrentT() => Time.fixedTime - _startFixedTime;
|
||||
|
||||
void FixedUpdate()
|
||||
{
|
||||
if (!_recording || _flushed || !NetworkServer.active)
|
||||
return;
|
||||
|
||||
int n = _bodies.Count;
|
||||
if (n == 0)
|
||||
return;
|
||||
|
||||
var frame = new ReplayFrame
|
||||
{
|
||||
t = CurrentT(),
|
||||
x = new float[n],
|
||||
y = new float[n],
|
||||
vx = new float[n],
|
||||
vy = new float[n]
|
||||
};
|
||||
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
Rigidbody2D rb = _bodies[i];
|
||||
if (rb == null)
|
||||
continue;
|
||||
Vector2 pos = rb.position;
|
||||
Vector2 vel = rb.linearVelocity;
|
||||
frame.x[i] = pos.x;
|
||||
frame.y[i] = pos.y;
|
||||
frame.vx[i] = vel.x;
|
||||
frame.vy[i] = vel.y;
|
||||
}
|
||||
|
||||
_duration = frame.t;
|
||||
_frames.Add(frame);
|
||||
}
|
||||
|
||||
void AddHit(string kind, float vol, Component source)
|
||||
{
|
||||
int entityId = -1;
|
||||
if (source != null)
|
||||
_instanceIdToEntityId.TryGetValue(source.gameObject.GetInstanceID(), out entityId);
|
||||
|
||||
AddEvent(new ReplayEvent
|
||||
{
|
||||
t = CurrentT(),
|
||||
type = "hit",
|
||||
kind = kind ?? "",
|
||||
vol = vol,
|
||||
id = entityId,
|
||||
team = "",
|
||||
redScore = 0,
|
||||
blueScore = 0,
|
||||
kickoff = false
|
||||
});
|
||||
}
|
||||
|
||||
void AddEvent(ReplayEvent ev)
|
||||
{
|
||||
_events.Add(ev);
|
||||
}
|
||||
|
||||
void Flush()
|
||||
{
|
||||
if (!_recording || _flushed)
|
||||
return;
|
||||
|
||||
_flushed = true;
|
||||
_recording = false;
|
||||
_duration = Mathf.Max(_duration, CurrentT());
|
||||
|
||||
var file = new ReplayFile
|
||||
{
|
||||
version = 1,
|
||||
matchId = _matchId,
|
||||
isPractice = _isPractice,
|
||||
fixedDeltaTime = Time.fixedDeltaTime,
|
||||
duration = _duration,
|
||||
entities = _entities.ToArray(),
|
||||
frames = _frames.ToArray(),
|
||||
events = _events.ToArray()
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
string path = ResolveReplayFilePath();
|
||||
string json = JsonUtility.ToJson(file);
|
||||
File.WriteAllText(path, json);
|
||||
Logger.Log($"ReplayRecorder wrote {path} ({_frames.Count} frames, {_events.Count} events, duration {_duration:F2}s)");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Log("ReplayRecorder flush failed: " + e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Same directory and basename as the match log, extension <c>.json</c>
|
||||
/// (e.g. <c>Logs/228.txt</c> → <c>Logs/228.json</c>).
|
||||
/// </summary>
|
||||
string ResolveReplayFilePath()
|
||||
{
|
||||
string expectedName = _matchId.ToString();
|
||||
string logPath = Logger.instance != null ? Logger.instance.LogFilePath : null;
|
||||
if (!string.IsNullOrEmpty(logPath))
|
||||
{
|
||||
string logBase = Path.GetFileNameWithoutExtension(logPath);
|
||||
if (logBase == expectedName)
|
||||
return Path.ChangeExtension(logPath, ".json");
|
||||
}
|
||||
|
||||
return Path.Combine(Logger.GetLogsDirectory(), expectedName + ".json");
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
if (Instance == this)
|
||||
{
|
||||
Flush();
|
||||
Instance = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Mirror;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Server-only match recorder. Samples poses every FixedUpdate; flushes JSON to disk on stop.
|
||||
/// Admin playback must interpolate by time (Hermite using velocity) — never snap one FixedUpdate sample per render frame.
|
||||
/// </summary>
|
||||
public class ReplayRecorder : MonoBehaviour
|
||||
{
|
||||
public static ReplayRecorder Instance { get; private set; }
|
||||
|
||||
public static bool IsRecording => Instance != null && Instance._recording && !Instance._flushed;
|
||||
|
||||
bool _recording;
|
||||
bool _flushed;
|
||||
int _matchId;
|
||||
bool _isPractice;
|
||||
float _startFixedTime;
|
||||
float _duration;
|
||||
|
||||
readonly List<ReplayEntity> _entities = new List<ReplayEntity>();
|
||||
readonly List<Rigidbody2D> _bodies = new List<Rigidbody2D>();
|
||||
readonly Dictionary<int, int> _instanceIdToEntityId = new Dictionary<int, int>();
|
||||
readonly List<ReplayFrame> _frames = new List<ReplayFrame>();
|
||||
readonly List<ReplayEvent> _events = new List<ReplayEvent>();
|
||||
|
||||
/// <returns>True if recording started (or already recording this session).</returns>
|
||||
public static bool StartRecording(int matchId, GameManager gm)
|
||||
{
|
||||
if (!NetworkServer.active || gm == null)
|
||||
return false;
|
||||
|
||||
if (Instance == null)
|
||||
{
|
||||
var go = new GameObject("ReplayRecorder");
|
||||
DontDestroyOnLoad(go);
|
||||
Instance = go.AddComponent<ReplayRecorder>();
|
||||
}
|
||||
|
||||
Instance.Begin(matchId, gm);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void StopAndFlush()
|
||||
{
|
||||
if (Instance != null)
|
||||
Instance.Flush();
|
||||
}
|
||||
|
||||
public static void RecordHit(string kind, float vol, Component source)
|
||||
{
|
||||
if (!IsRecording)
|
||||
return;
|
||||
Instance.AddHit(kind, vol, source);
|
||||
}
|
||||
|
||||
public static void RecordGoal(Team concedingTeam, int redScore, int blueScore, bool kickoff)
|
||||
{
|
||||
if (!IsRecording)
|
||||
return;
|
||||
Instance.AddEvent(new ReplayEvent
|
||||
{
|
||||
t = Instance.CurrentT(),
|
||||
type = "goal",
|
||||
kind = "",
|
||||
vol = 0f,
|
||||
id = -1,
|
||||
team = concedingTeam.ToString(),
|
||||
redScore = redScore,
|
||||
blueScore = blueScore,
|
||||
kickoff = kickoff
|
||||
});
|
||||
}
|
||||
|
||||
public static void RecordReset(bool kickoff)
|
||||
{
|
||||
if (!IsRecording)
|
||||
return;
|
||||
Instance.AddEvent(new ReplayEvent
|
||||
{
|
||||
t = Instance.CurrentT(),
|
||||
type = "reset",
|
||||
kind = "",
|
||||
vol = 0f,
|
||||
id = -1,
|
||||
team = "",
|
||||
redScore = 0,
|
||||
blueScore = 0,
|
||||
kickoff = kickoff
|
||||
});
|
||||
}
|
||||
|
||||
public static bool TryGetEntityId(Component source, out int entityId)
|
||||
{
|
||||
entityId = -1;
|
||||
if (Instance == null || source == null)
|
||||
return false;
|
||||
return Instance._instanceIdToEntityId.TryGetValue(source.gameObject.GetInstanceID(), out entityId);
|
||||
}
|
||||
|
||||
void Begin(int matchId, GameManager gm)
|
||||
{
|
||||
if (_recording && !_flushed)
|
||||
Flush();
|
||||
|
||||
// Practice / tutorial host uses DedicatedMatchId 0 — mint a unique id so files don't overwrite.
|
||||
_isPractice = matchId <= 0 || GameTutorialManager.tutorialModeEnabled;
|
||||
if (matchId <= 0)
|
||||
matchId = GeneratePracticeMatchId();
|
||||
|
||||
_matchId = matchId;
|
||||
_recording = true;
|
||||
_flushed = false;
|
||||
_startFixedTime = Time.fixedTime;
|
||||
_duration = 0f;
|
||||
_entities.Clear();
|
||||
_bodies.Clear();
|
||||
_instanceIdToEntityId.Clear();
|
||||
_frames.Clear();
|
||||
_events.Clear();
|
||||
|
||||
// Keep log + replay siblings: Logs/{matchId}.txt and Logs/{matchId}.json
|
||||
if (_isPractice)
|
||||
Logger.SetFileName(_matchId.ToString());
|
||||
|
||||
BuildEntities(gm);
|
||||
string mode = _isPractice ? "practice" : "match";
|
||||
Logger.Log($"ReplayRecorder started ({mode}) id {_matchId} with {_entities.Count} entities");
|
||||
}
|
||||
|
||||
/// <summary>Negative unix timestamp so practice ids never collide with positive matchmaker ids.</summary>
|
||||
static int GeneratePracticeMatchId()
|
||||
{
|
||||
long unix = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
if (unix > int.MaxValue)
|
||||
unix %= int.MaxValue;
|
||||
int id = -(int)unix;
|
||||
return id == 0 ? -1 : id;
|
||||
}
|
||||
|
||||
void BuildEntities(GameManager gm)
|
||||
{
|
||||
if (gm.ball != null)
|
||||
{
|
||||
RegisterEntity(gm.ball.gameObject, gm.ball, "ball", "");
|
||||
}
|
||||
|
||||
var sorted = new List<Puck>(gm.pucks);
|
||||
sorted.Sort((a, b) =>
|
||||
{
|
||||
int teamCmp = a.team.CompareTo(b.team);
|
||||
if (teamCmp != 0)
|
||||
return teamCmp;
|
||||
int xCmp = a.transform.position.x.CompareTo(b.transform.position.x);
|
||||
if (xCmp != 0)
|
||||
return xCmp;
|
||||
return a.transform.position.y.CompareTo(b.transform.position.y);
|
||||
});
|
||||
|
||||
foreach (Puck puck in sorted)
|
||||
{
|
||||
if (puck == null || puck.rb == null)
|
||||
continue;
|
||||
RegisterEntity(puck.gameObject, puck.rb, "puck", puck.team.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
void RegisterEntity(GameObject go, Rigidbody2D rb, string type, string team)
|
||||
{
|
||||
int id = _entities.Count;
|
||||
_entities.Add(new ReplayEntity { id = id, type = type, team = team ?? "" });
|
||||
_bodies.Add(rb);
|
||||
_instanceIdToEntityId[go.GetInstanceID()] = id;
|
||||
}
|
||||
|
||||
float CurrentT() => Time.fixedTime - _startFixedTime;
|
||||
|
||||
void FixedUpdate()
|
||||
{
|
||||
if (!_recording || _flushed || !NetworkServer.active)
|
||||
return;
|
||||
|
||||
int n = _bodies.Count;
|
||||
if (n == 0)
|
||||
return;
|
||||
|
||||
var frame = new ReplayFrame
|
||||
{
|
||||
t = CurrentT(),
|
||||
x = new float[n],
|
||||
y = new float[n],
|
||||
vx = new float[n],
|
||||
vy = new float[n]
|
||||
};
|
||||
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
Rigidbody2D rb = _bodies[i];
|
||||
if (rb == null)
|
||||
continue;
|
||||
Vector2 pos = rb.position;
|
||||
Vector2 vel = rb.linearVelocity;
|
||||
frame.x[i] = pos.x;
|
||||
frame.y[i] = pos.y;
|
||||
frame.vx[i] = vel.x;
|
||||
frame.vy[i] = vel.y;
|
||||
}
|
||||
|
||||
_duration = frame.t;
|
||||
_frames.Add(frame);
|
||||
}
|
||||
|
||||
void AddHit(string kind, float vol, Component source)
|
||||
{
|
||||
int entityId = -1;
|
||||
if (source != null)
|
||||
_instanceIdToEntityId.TryGetValue(source.gameObject.GetInstanceID(), out entityId);
|
||||
|
||||
AddEvent(new ReplayEvent
|
||||
{
|
||||
t = CurrentT(),
|
||||
type = "hit",
|
||||
kind = kind ?? "",
|
||||
vol = vol,
|
||||
id = entityId,
|
||||
team = "",
|
||||
redScore = 0,
|
||||
blueScore = 0,
|
||||
kickoff = false
|
||||
});
|
||||
}
|
||||
|
||||
void AddEvent(ReplayEvent ev)
|
||||
{
|
||||
_events.Add(ev);
|
||||
}
|
||||
|
||||
void Flush()
|
||||
{
|
||||
if (!_recording || _flushed)
|
||||
return;
|
||||
|
||||
_flushed = true;
|
||||
_recording = false;
|
||||
_duration = Mathf.Max(_duration, CurrentT());
|
||||
|
||||
var file = new ReplayFile
|
||||
{
|
||||
version = 1,
|
||||
matchId = _matchId,
|
||||
isPractice = _isPractice,
|
||||
fixedDeltaTime = Time.fixedDeltaTime,
|
||||
duration = _duration,
|
||||
entities = _entities.ToArray(),
|
||||
frames = _frames.ToArray(),
|
||||
events = _events.ToArray()
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
string path = ResolveReplayFilePath();
|
||||
string json = JsonUtility.ToJson(file);
|
||||
File.WriteAllText(path, json);
|
||||
Logger.Log($"ReplayRecorder wrote {path} ({_frames.Count} frames, {_events.Count} events, duration {_duration:F2}s)");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Log("ReplayRecorder flush failed: " + e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Same directory and basename as the match log, extension <c>.json</c>
|
||||
/// (e.g. <c>Logs/228.txt</c> → <c>Logs/228.json</c>).
|
||||
/// </summary>
|
||||
string ResolveReplayFilePath()
|
||||
{
|
||||
string expectedName = _matchId.ToString();
|
||||
string logPath = Logger.instance != null ? Logger.instance.LogFilePath : null;
|
||||
if (!string.IsNullOrEmpty(logPath))
|
||||
{
|
||||
string logBase = Path.GetFileNameWithoutExtension(logPath);
|
||||
if (logBase == expectedName)
|
||||
return Path.ChangeExtension(logPath, ".json");
|
||||
}
|
||||
|
||||
return Path.Combine(Logger.GetLogsDirectory(), expectedName + ".json");
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
if (Instance == this)
|
||||
{
|
||||
Flush();
|
||||
Instance = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c3d4e5f60718293a4b5c6d7e8f901a2b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
fileFormatVersion: 2
|
||||
guid: c3d4e5f60718293a4b5c6d7e8f901a2b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class SceneManagerUIHelper : MonoBehaviour
|
||||
{
|
||||
public void LoadScene(string sceneName){
|
||||
AddressableSceneLoader.Load(sceneName);
|
||||
}
|
||||
|
||||
public void Quit(){
|
||||
Application.Quit();
|
||||
}
|
||||
}
|
||||
using UnityEngine;
|
||||
|
||||
public class SceneManagerUIHelper : MonoBehaviour
|
||||
{
|
||||
public void LoadScene(string sceneName){
|
||||
AddressableSceneLoader.Load(sceneName);
|
||||
}
|
||||
|
||||
public void Quit(){
|
||||
Application.Quit();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
fileFormatVersion: 2
|
||||
guid: c919971d56a5c33418c7a76a4744b63c
|
||||
@@ -1,66 +1,66 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
public class ServerAutoClose : MonoBehaviour
|
||||
{
|
||||
#if UNITY_SERVER
|
||||
|
||||
void Start()
|
||||
{
|
||||
Logger.Log("Starting auto close watchdog");
|
||||
}
|
||||
|
||||
int lastObservedPlayerCount = -1;
|
||||
bool hasPlayerEverJoined = false;
|
||||
float noPlayersTimer = 0f;
|
||||
int lastCountdownLoggedSecond = -1;
|
||||
const float NoPlayersTimeoutSeconds = 65f;
|
||||
|
||||
void Update()
|
||||
{
|
||||
int activePlayers = 0;
|
||||
try
|
||||
{
|
||||
activePlayers = CupidConnector.GetActivePlayerConnectionCount();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Log("Error getting active player connection count: " + e.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (activePlayers != lastObservedPlayerCount)
|
||||
{
|
||||
Logger.Log("Active player connections: " + activePlayers);
|
||||
lastObservedPlayerCount = activePlayers;
|
||||
}
|
||||
|
||||
if (activePlayers > 0)
|
||||
{
|
||||
hasPlayerEverJoined = true;
|
||||
noPlayersTimer = 0f;
|
||||
lastCountdownLoggedSecond = -1;
|
||||
}
|
||||
|
||||
noPlayersTimer += Time.deltaTime;
|
||||
int secondsRemaining = Mathf.CeilToInt(NoPlayersTimeoutSeconds - noPlayersTimer);
|
||||
if (secondsRemaining > 0 && secondsRemaining % 10 == 0 && secondsRemaining != lastCountdownLoggedSecond)
|
||||
{
|
||||
lastCountdownLoggedSecond = secondsRemaining;
|
||||
Logger.Log("Server will be closed due to no players in, " + secondsRemaining);
|
||||
}
|
||||
|
||||
if (noPlayersTimer >= NoPlayersTimeoutSeconds)
|
||||
{
|
||||
if (hasPlayerEverJoined)
|
||||
Logger.Log("All players left, exiting");
|
||||
else
|
||||
Logger.Log("No players joined, exiting");
|
||||
// ReportDedicatedMatchRoomClosed settles /winner first when escrow is open.
|
||||
GameManager.ReportDedicatedMatchRoomClosed();
|
||||
Application.Quit();
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
public class ServerAutoClose : MonoBehaviour
|
||||
{
|
||||
#if UNITY_SERVER
|
||||
|
||||
void Start()
|
||||
{
|
||||
Logger.Log("Starting auto close watchdog");
|
||||
}
|
||||
|
||||
int lastObservedPlayerCount = -1;
|
||||
bool hasPlayerEverJoined = false;
|
||||
float noPlayersTimer = 0f;
|
||||
int lastCountdownLoggedSecond = -1;
|
||||
const float NoPlayersTimeoutSeconds = 65f;
|
||||
|
||||
void Update()
|
||||
{
|
||||
int activePlayers = 0;
|
||||
try
|
||||
{
|
||||
activePlayers = CupidConnector.GetActivePlayerConnectionCount();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Log("Error getting active player connection count: " + e.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (activePlayers != lastObservedPlayerCount)
|
||||
{
|
||||
Logger.Log("Active player connections: " + activePlayers);
|
||||
lastObservedPlayerCount = activePlayers;
|
||||
}
|
||||
|
||||
if (activePlayers > 0)
|
||||
{
|
||||
hasPlayerEverJoined = true;
|
||||
noPlayersTimer = 0f;
|
||||
lastCountdownLoggedSecond = -1;
|
||||
}
|
||||
|
||||
noPlayersTimer += Time.deltaTime;
|
||||
int secondsRemaining = Mathf.CeilToInt(NoPlayersTimeoutSeconds - noPlayersTimer);
|
||||
if (secondsRemaining > 0 && secondsRemaining % 10 == 0 && secondsRemaining != lastCountdownLoggedSecond)
|
||||
{
|
||||
lastCountdownLoggedSecond = secondsRemaining;
|
||||
Logger.Log("Server will be closed due to no players in, " + secondsRemaining);
|
||||
}
|
||||
|
||||
if (noPlayersTimer >= NoPlayersTimeoutSeconds)
|
||||
{
|
||||
if (hasPlayerEverJoined)
|
||||
Logger.Log("All players left, exiting");
|
||||
else
|
||||
Logger.Log("No players joined, exiting");
|
||||
// ReportDedicatedMatchRoomClosed settles /winner first when escrow is open.
|
||||
GameManager.ReportDedicatedMatchRoomClosed();
|
||||
Application.Quit();
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
fileFormatVersion: 2
|
||||
guid: c10a35ed198fa2941b2b28a03c558897
|
||||
@@ -1,25 +1,25 @@
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
|
||||
public class StatCanvas : MonoBehaviour
|
||||
{
|
||||
public static StatCanvas instance;
|
||||
public TMP_Text txtVersion;
|
||||
void Awake()
|
||||
{
|
||||
if(instance!=null){
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
instance = this;
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
txtVersion.text = "v " + Application.version;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
|
||||
public class StatCanvas : MonoBehaviour
|
||||
{
|
||||
public static StatCanvas instance;
|
||||
public TMP_Text txtVersion;
|
||||
void Awake()
|
||||
{
|
||||
if(instance!=null){
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
instance = this;
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
txtVersion.text = "v " + Application.version;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
fileFormatVersion: 2
|
||||
guid: a49b5cb62dd2545408364e077bdf3a0a
|
||||
@@ -1,25 +1,25 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class TeamSpecificEffects : MonoBehaviour
|
||||
{
|
||||
public static TeamSpecificEffects instance;
|
||||
|
||||
public Transform[] flipY;
|
||||
void Awake()
|
||||
{
|
||||
instance = this;
|
||||
}
|
||||
|
||||
public void SetTeamSpecificEffects(Team team){
|
||||
foreach(Transform t in flipY){
|
||||
float y = Mathf.Abs(t.localScale.y);
|
||||
t.localScale = new Vector3(t.localScale.x, team == Team.Blue ? -y : y, t.localScale.z);
|
||||
}
|
||||
|
||||
if(team == Team.Blue){
|
||||
CameraEffects.instance.SetUpsideDown(true);
|
||||
}else{
|
||||
CameraEffects.instance.SetUpsideDown(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
using UnityEngine;
|
||||
|
||||
public class TeamSpecificEffects : MonoBehaviour
|
||||
{
|
||||
public static TeamSpecificEffects instance;
|
||||
|
||||
public Transform[] flipY;
|
||||
void Awake()
|
||||
{
|
||||
instance = this;
|
||||
}
|
||||
|
||||
public void SetTeamSpecificEffects(Team team){
|
||||
foreach(Transform t in flipY){
|
||||
float y = Mathf.Abs(t.localScale.y);
|
||||
t.localScale = new Vector3(t.localScale.x, team == Team.Blue ? -y : y, t.localScale.z);
|
||||
}
|
||||
|
||||
if(team == Team.Blue){
|
||||
CameraEffects.instance.SetUpsideDown(true);
|
||||
}else{
|
||||
CameraEffects.instance.SetUpsideDown(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
fileFormatVersion: 2
|
||||
guid: 370d8135f2d97da4194a41598ce8c4a9
|
||||
@@ -1,148 +1,148 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
public class TutorialManagerMainMenu : MonoBehaviour
|
||||
{
|
||||
public const string TUTORIAL_PREF_KEY = "tutorial_main_menu_completed";
|
||||
public bool forceShow= false;
|
||||
public GameObject tutorialPanel;
|
||||
public int curScreen = 1;
|
||||
|
||||
[Header("Screen 1")]
|
||||
public GameObject screen1;
|
||||
public Button btnNext1;
|
||||
public Button btnSkip;
|
||||
|
||||
|
||||
[Header("Screen 2")]
|
||||
public GameObject screen2;
|
||||
public Button btnNext2;
|
||||
|
||||
[Header("Screen 3")]
|
||||
public GameObject screen3;
|
||||
public GameObject buyScreen;
|
||||
|
||||
|
||||
[Header("Screen 4")]
|
||||
public GameObject screen4;
|
||||
|
||||
[Header("Screen 5")]
|
||||
public GameObject screen5;
|
||||
|
||||
[Header("Screen 6")]
|
||||
public GameObject screen6;
|
||||
public Button btnNext6;
|
||||
public CardCarousel gameModeCarousel;
|
||||
|
||||
[Header("Screen 7")]
|
||||
public GameObject screen7;
|
||||
public Button btnFinish;
|
||||
|
||||
void Start()
|
||||
{
|
||||
btnNext1.onClick.AddListener(OnNext1);
|
||||
btnSkip.onClick.AddListener(OnSkip);
|
||||
btnNext2.onClick.AddListener(OnNext2);
|
||||
btnNext6.onClick.AddListener(OnNext6);
|
||||
|
||||
btnFinish.onClick.AddListener(OnStartTutorialMatch);
|
||||
|
||||
if(!forceShow){
|
||||
if(PlayerPrefs.HasKey(TUTORIAL_PREF_KEY)){
|
||||
tutorialPanel.SetActive(false);
|
||||
Debug.Log("Tutorial pref key is available, stopping;");
|
||||
return;
|
||||
}
|
||||
|
||||
if(LoginManager.CurrentUser.rc > 0){
|
||||
tutorialPanel.SetActive(false);
|
||||
Debug.Log("User has rc, stopping;");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
tutorialPanel.SetActive(true);
|
||||
screen1.SetActive(true);
|
||||
PlayerPrefs.SetInt(TUTORIAL_PREF_KEY, 1);
|
||||
PlayerPrefs.Save();
|
||||
}
|
||||
|
||||
void OnNext1(){
|
||||
screen1.SetActive(false);
|
||||
screen2.SetActive(true);
|
||||
curScreen = 2;
|
||||
}
|
||||
|
||||
void OnSkip(){
|
||||
tutorialPanel.SetActive(false);
|
||||
OnTutorialCompleted();
|
||||
}
|
||||
|
||||
void OnNext2(){
|
||||
screen2.SetActive(false);
|
||||
screen3.SetActive(true);
|
||||
|
||||
curScreen = 3;
|
||||
}
|
||||
|
||||
void OnNext6(){
|
||||
screen6.SetActive(false);
|
||||
screen7.SetActive(true);
|
||||
|
||||
gameModeCarousel.SetSelectedIndex(1);
|
||||
|
||||
curScreen = 7;
|
||||
}
|
||||
|
||||
void OnNext3(){
|
||||
screen3.SetActive(false);
|
||||
screen4.SetActive(true);
|
||||
curScreen = 4;
|
||||
}
|
||||
|
||||
void OnNext4(){
|
||||
MainMenuManager.instance.ShowMainMenuScreen();
|
||||
screen4.SetActive(false);
|
||||
screen5.SetActive(true);
|
||||
curScreen = 5;
|
||||
}
|
||||
|
||||
void OnNext5(){
|
||||
screen5.SetActive(false);
|
||||
screen6.SetActive(true);
|
||||
curScreen = 6;
|
||||
}
|
||||
|
||||
|
||||
public void OnTutorialCompleted(){
|
||||
PlayerPrefs.SetInt(TUTORIAL_PREF_KEY, 1);
|
||||
PlayerPrefs.Save();
|
||||
tutorialPanel.SetActive(false);
|
||||
}
|
||||
|
||||
void OnStartTutorialMatch()
|
||||
{
|
||||
OnTutorialCompleted();
|
||||
if (MainMenuManager.instance != null)
|
||||
MainMenuManager.instance.OnPlayTutorial();
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if(curScreen < 2){return;}
|
||||
|
||||
if(curScreen == 3 && buyScreen.activeSelf){
|
||||
OnNext3();
|
||||
}
|
||||
if(curScreen == 4 && LoginManager.CurrentUser.rc > 0){
|
||||
OnNext4();
|
||||
}
|
||||
if(curScreen == 5 && MainMenuManager.instance.gameModesScreen.activeSelf){
|
||||
OnNext5();
|
||||
}
|
||||
|
||||
if(curScreen == 6 && CupidLobby.instance.isMatchmaking){
|
||||
OnTutorialCompleted();
|
||||
}
|
||||
}
|
||||
}
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
public class TutorialManagerMainMenu : MonoBehaviour
|
||||
{
|
||||
public const string TUTORIAL_PREF_KEY = "tutorial_main_menu_completed";
|
||||
public bool forceShow= false;
|
||||
public GameObject tutorialPanel;
|
||||
public int curScreen = 1;
|
||||
|
||||
[Header("Screen 1")]
|
||||
public GameObject screen1;
|
||||
public Button btnNext1;
|
||||
public Button btnSkip;
|
||||
|
||||
|
||||
[Header("Screen 2")]
|
||||
public GameObject screen2;
|
||||
public Button btnNext2;
|
||||
|
||||
[Header("Screen 3")]
|
||||
public GameObject screen3;
|
||||
public GameObject buyScreen;
|
||||
|
||||
|
||||
[Header("Screen 4")]
|
||||
public GameObject screen4;
|
||||
|
||||
[Header("Screen 5")]
|
||||
public GameObject screen5;
|
||||
|
||||
[Header("Screen 6")]
|
||||
public GameObject screen6;
|
||||
public Button btnNext6;
|
||||
public CardCarousel gameModeCarousel;
|
||||
|
||||
[Header("Screen 7")]
|
||||
public GameObject screen7;
|
||||
public Button btnFinish;
|
||||
|
||||
void Start()
|
||||
{
|
||||
btnNext1.onClick.AddListener(OnNext1);
|
||||
btnSkip.onClick.AddListener(OnSkip);
|
||||
btnNext2.onClick.AddListener(OnNext2);
|
||||
btnNext6.onClick.AddListener(OnNext6);
|
||||
|
||||
btnFinish.onClick.AddListener(OnStartTutorialMatch);
|
||||
|
||||
if(!forceShow){
|
||||
if(PlayerPrefs.HasKey(TUTORIAL_PREF_KEY)){
|
||||
tutorialPanel.SetActive(false);
|
||||
Debug.Log("Tutorial pref key is available, stopping;");
|
||||
return;
|
||||
}
|
||||
|
||||
if(LoginManager.CurrentUser.rc > 0){
|
||||
tutorialPanel.SetActive(false);
|
||||
Debug.Log("User has rc, stopping;");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
tutorialPanel.SetActive(true);
|
||||
screen1.SetActive(true);
|
||||
PlayerPrefs.SetInt(TUTORIAL_PREF_KEY, 1);
|
||||
PlayerPrefs.Save();
|
||||
}
|
||||
|
||||
void OnNext1(){
|
||||
screen1.SetActive(false);
|
||||
screen2.SetActive(true);
|
||||
curScreen = 2;
|
||||
}
|
||||
|
||||
void OnSkip(){
|
||||
tutorialPanel.SetActive(false);
|
||||
OnTutorialCompleted();
|
||||
}
|
||||
|
||||
void OnNext2(){
|
||||
screen2.SetActive(false);
|
||||
screen3.SetActive(true);
|
||||
|
||||
curScreen = 3;
|
||||
}
|
||||
|
||||
void OnNext6(){
|
||||
screen6.SetActive(false);
|
||||
screen7.SetActive(true);
|
||||
|
||||
gameModeCarousel.SetSelectedIndex(1);
|
||||
|
||||
curScreen = 7;
|
||||
}
|
||||
|
||||
void OnNext3(){
|
||||
screen3.SetActive(false);
|
||||
screen4.SetActive(true);
|
||||
curScreen = 4;
|
||||
}
|
||||
|
||||
void OnNext4(){
|
||||
MainMenuManager.instance.ShowMainMenuScreen();
|
||||
screen4.SetActive(false);
|
||||
screen5.SetActive(true);
|
||||
curScreen = 5;
|
||||
}
|
||||
|
||||
void OnNext5(){
|
||||
screen5.SetActive(false);
|
||||
screen6.SetActive(true);
|
||||
curScreen = 6;
|
||||
}
|
||||
|
||||
|
||||
public void OnTutorialCompleted(){
|
||||
PlayerPrefs.SetInt(TUTORIAL_PREF_KEY, 1);
|
||||
PlayerPrefs.Save();
|
||||
tutorialPanel.SetActive(false);
|
||||
}
|
||||
|
||||
void OnStartTutorialMatch()
|
||||
{
|
||||
OnTutorialCompleted();
|
||||
if (MainMenuManager.instance != null)
|
||||
MainMenuManager.instance.OnPlayTutorial();
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if(curScreen < 2){return;}
|
||||
|
||||
if(curScreen == 3 && buyScreen.activeSelf){
|
||||
OnNext3();
|
||||
}
|
||||
if(curScreen == 4 && LoginManager.CurrentUser.rc > 0){
|
||||
OnNext4();
|
||||
}
|
||||
if(curScreen == 5 && MainMenuManager.instance.gameModesScreen.activeSelf){
|
||||
OnNext5();
|
||||
}
|
||||
|
||||
if(curScreen == 6 && CupidLobby.instance.isMatchmaking){
|
||||
OnTutorialCompleted();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
fileFormatVersion: 2
|
||||
guid: a813b293eefc93349a0238a65e962944
|
||||
@@ -1,8 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 37e196db53cb2f948969d90e648a355a
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
fileFormatVersion: 2
|
||||
guid: 37e196db53cb2f948969d90e648a355a
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 07c0cc0c5a337f24986822f356e2f9f2
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
fileFormatVersion: 2
|
||||
guid: 07c0cc0c5a337f24986822f356e2f9f2
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
@@ -1,39 +1,39 @@
|
||||
using UnityEngine;
|
||||
|
||||
[CreateAssetMenu(fileName = "AudioDirectory", menuName = "Scriptable Objects/AudioDirectory")]
|
||||
public class AudioDirectory : ScriptableObject
|
||||
{
|
||||
[Header("Scenes")]
|
||||
public string introSceneName = "Intro";
|
||||
public string menuSceneName = "MainMenu";
|
||||
public string gameSceneName = "Game";
|
||||
|
||||
|
||||
[Header("Music")]
|
||||
public AudioClip introMusic;
|
||||
public AudioClip menuMusic;
|
||||
public AudioClip gameMusic;
|
||||
|
||||
[Header("SFX")]
|
||||
public AudioClip[] coinsSfx;
|
||||
[Header("Ball")]
|
||||
public AudioClip[] ballHits;
|
||||
public AudioClip[] puckHits;
|
||||
public AudioClip[] wallHits;
|
||||
public AudioClip[] ballHitNet;
|
||||
|
||||
[Header("Game")]
|
||||
public AudioClip[] timerBeep;
|
||||
public AudioClip[] matchMakingBeep;
|
||||
public AudioClip[] matchFound;
|
||||
|
||||
[Header("Referee")]
|
||||
public AudioClip[] refereeGoal;
|
||||
public AudioClip[] refereeWhistle;
|
||||
|
||||
[Header("Crowd")]
|
||||
public AudioClip[] crowdAmbientNoise;
|
||||
public AudioClip[] crowdAmbientNoiseNearGoal;
|
||||
public AudioClip[] crowdCheering;
|
||||
public AudioClip[] crowdNoiseMatchStart;
|
||||
}
|
||||
using UnityEngine;
|
||||
|
||||
[CreateAssetMenu(fileName = "AudioDirectory", menuName = "Scriptable Objects/AudioDirectory")]
|
||||
public class AudioDirectory : ScriptableObject
|
||||
{
|
||||
[Header("Scenes")]
|
||||
public string introSceneName = "Intro";
|
||||
public string menuSceneName = "MainMenu";
|
||||
public string gameSceneName = "Game";
|
||||
|
||||
|
||||
[Header("Music")]
|
||||
public AudioClip introMusic;
|
||||
public AudioClip menuMusic;
|
||||
public AudioClip gameMusic;
|
||||
|
||||
[Header("SFX")]
|
||||
public AudioClip[] coinsSfx;
|
||||
[Header("Ball")]
|
||||
public AudioClip[] ballHits;
|
||||
public AudioClip[] puckHits;
|
||||
public AudioClip[] wallHits;
|
||||
public AudioClip[] ballHitNet;
|
||||
|
||||
[Header("Game")]
|
||||
public AudioClip[] timerBeep;
|
||||
public AudioClip[] matchMakingBeep;
|
||||
public AudioClip[] matchFound;
|
||||
|
||||
[Header("Referee")]
|
||||
public AudioClip[] refereeGoal;
|
||||
public AudioClip[] refereeWhistle;
|
||||
|
||||
[Header("Crowd")]
|
||||
public AudioClip[] crowdAmbientNoise;
|
||||
public AudioClip[] crowdAmbientNoiseNearGoal;
|
||||
public AudioClip[] crowdCheering;
|
||||
public AudioClip[] crowdNoiseMatchStart;
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
fileFormatVersion: 2
|
||||
guid: 776cfba92ccdba54d8129b1338fbcc6c
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,2 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
fileFormatVersion: 2
|
||||
guid: 6aae5977db663834f86b46979365fa92
|
||||
@@ -1,21 +1,21 @@
|
||||
public static class CoinHelper
|
||||
{
|
||||
public static float Format(int coins)
|
||||
{
|
||||
int whole = coins / 4;
|
||||
int fraction = coins % 4;
|
||||
return float.Parse(whole + "." + fraction);
|
||||
}
|
||||
|
||||
public static int RcToCoins(float coins){
|
||||
string s = coins.ToString("N1");
|
||||
int whole = int.Parse(s.Split('.')[0]);
|
||||
int fraction = int.Parse(s.Split('.')[1]);
|
||||
return (whole * 4) + fraction;
|
||||
}
|
||||
|
||||
public static string FormatString(int coins)
|
||||
{
|
||||
return Format(coins).ToString("N1");
|
||||
}
|
||||
}
|
||||
public static class CoinHelper
|
||||
{
|
||||
public static float Format(int coins)
|
||||
{
|
||||
int whole = coins / 4;
|
||||
int fraction = coins % 4;
|
||||
return float.Parse(whole + "." + fraction);
|
||||
}
|
||||
|
||||
public static int RcToCoins(float coins){
|
||||
string s = coins.ToString("N1");
|
||||
int whole = int.Parse(s.Split('.')[0]);
|
||||
int fraction = int.Parse(s.Split('.')[1]);
|
||||
return (whole * 4) + fraction;
|
||||
}
|
||||
|
||||
public static string FormatString(int coins)
|
||||
{
|
||||
return Format(coins).ToString("N1");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
fileFormatVersion: 2
|
||||
guid: 5bf3c59f7c1a7e640a05edc5f9828abd
|
||||
@@ -1,19 +1,19 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
public static class GetAvailablePort
|
||||
{
|
||||
|
||||
public static int GetAvailableUdpPort()
|
||||
{
|
||||
using var socket = new Socket(
|
||||
AddressFamily.InterNetwork,
|
||||
SocketType.Dgram,
|
||||
ProtocolType.Udp
|
||||
);
|
||||
|
||||
socket.Bind(new IPEndPoint(IPAddress.Any, 0));
|
||||
|
||||
return ((IPEndPoint)socket.LocalEndPoint).Port;
|
||||
}
|
||||
}
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
public static class GetAvailablePort
|
||||
{
|
||||
|
||||
public static int GetAvailableUdpPort()
|
||||
{
|
||||
using var socket = new Socket(
|
||||
AddressFamily.InterNetwork,
|
||||
SocketType.Dgram,
|
||||
ProtocolType.Udp
|
||||
);
|
||||
|
||||
socket.Bind(new IPEndPoint(IPAddress.Any, 0));
|
||||
|
||||
return ((IPEndPoint)socket.LocalEndPoint).Port;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,229 +1,229 @@
|
||||
using System;
|
||||
using DG.Tweening;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEngine.UI;
|
||||
|
||||
public class MessageBoxDialog : MonoBehaviour
|
||||
{
|
||||
public const float ANIMATION_DURATION = 0.25f;
|
||||
/// <summary>Above game/menu/loader canvases; below reconnect (500).</summary>
|
||||
const int OverlaySortingOrder = 400;
|
||||
|
||||
public static MessageBoxDialog instance;
|
||||
public static bool IsAvailable => instance != null;
|
||||
|
||||
public RectTransform dimmer;
|
||||
public RectTransform basicMessageBox;
|
||||
|
||||
[Header("MessageBox")]
|
||||
public RectTransform messageBox;
|
||||
public TMP_Text txtTitle, txtMessage;
|
||||
public Button btnOk, btnYes, btnNo;
|
||||
|
||||
Image dimmerImg;
|
||||
TMP_Text basicMessageText;
|
||||
Action<bool> onYesOrNo;
|
||||
Sequence showHideSequence;
|
||||
Canvas overlayCanvas;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
if (instance != null && instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
|
||||
overlayCanvas = GetComponent<Canvas>();
|
||||
if (overlayCanvas != null)
|
||||
overlayCanvas.sortingOrder = OverlaySortingOrder;
|
||||
|
||||
if (dimmer != null)
|
||||
dimmerImg = dimmer.GetComponent<Image>();
|
||||
if (basicMessageBox != null)
|
||||
basicMessageText = basicMessageBox.GetComponentInChildren<TMP_Text>();
|
||||
}
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
if (instance != this)
|
||||
return;
|
||||
SceneManager.sceneLoaded += OnSceneLoaded;
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
if (instance != this)
|
||||
return;
|
||||
SceneManager.sceneLoaded -= OnSceneLoaded;
|
||||
}
|
||||
|
||||
void OnSceneLoaded(Scene scene, LoadSceneMode mode)
|
||||
{
|
||||
transform.SetAsLastSibling();
|
||||
}
|
||||
|
||||
public static void ShowBasic(string title)
|
||||
{
|
||||
if (instance == null)
|
||||
{
|
||||
Debug.LogWarning("MessageBoxDialog missing: " + title);
|
||||
return;
|
||||
}
|
||||
|
||||
instance.ShowMessageBox(title);
|
||||
}
|
||||
|
||||
public static void Show(string title, string message, Action<bool> onResult = null)
|
||||
{
|
||||
if (instance == null)
|
||||
{
|
||||
Debug.LogWarning("MessageBoxDialog missing: " + title);
|
||||
return;
|
||||
}
|
||||
|
||||
instance.ShowMessageBox(title, message, onResult);
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
KillTweens();
|
||||
if (instance == this)
|
||||
instance = null;
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
Button basicButton = basicMessageBox != null ? basicMessageBox.GetComponent<Button>() : null;
|
||||
if (basicButton != null)
|
||||
basicButton.onClick.AddListener(HideMessageBox);
|
||||
|
||||
if (btnOk != null)
|
||||
btnOk.onClick.AddListener(HideMessageBox);
|
||||
if (btnYes != null)
|
||||
btnYes.onClick.AddListener(() => OnQuestionDialog(true));
|
||||
if (btnNo != null)
|
||||
btnNo.onClick.AddListener(() => OnQuestionDialog(false));
|
||||
}
|
||||
|
||||
void OnQuestionDialog(bool val)
|
||||
{
|
||||
Action<bool> callback = onYesOrNo;
|
||||
onYesOrNo = null;
|
||||
HideMessageBox();
|
||||
callback?.Invoke(val);
|
||||
}
|
||||
|
||||
public void ShowMessageBox(string title)
|
||||
{
|
||||
PrepareShow();
|
||||
HideBoxImmediate(messageBox);
|
||||
|
||||
if (basicMessageText != null)
|
||||
basicMessageText.text = title;
|
||||
|
||||
AnimateIn(basicMessageBox);
|
||||
}
|
||||
|
||||
public void ShowMessageBox(string title, string message, Action<bool> onResult = null)
|
||||
{
|
||||
PrepareShow();
|
||||
HideBoxImmediate(basicMessageBox);
|
||||
|
||||
txtTitle.text = title;
|
||||
txtMessage.text = message;
|
||||
|
||||
bool isQuestion = onResult != null;
|
||||
btnNo.gameObject.SetActive(isQuestion);
|
||||
btnYes.gameObject.SetActive(isQuestion);
|
||||
btnOk.gameObject.SetActive(!isQuestion);
|
||||
|
||||
onYesOrNo = onResult;
|
||||
AnimateIn(messageBox);
|
||||
}
|
||||
|
||||
public void HideMessageBox()
|
||||
{
|
||||
bool basicActive = basicMessageBox != null && basicMessageBox.gameObject.activeSelf;
|
||||
bool fullActive = messageBox != null && messageBox.gameObject.activeSelf;
|
||||
if (!basicActive && !fullActive)
|
||||
return;
|
||||
|
||||
if (basicActive && fullActive)
|
||||
HideBoxImmediate(basicMessageBox);
|
||||
|
||||
AnimateOut(basicActive && !fullActive ? basicMessageBox : messageBox);
|
||||
}
|
||||
|
||||
void PrepareShow()
|
||||
{
|
||||
KillTweens();
|
||||
onYesOrNo = null;
|
||||
transform.SetAsLastSibling();
|
||||
|
||||
dimmer.gameObject.SetActive(true);
|
||||
if (dimmerImg != null)
|
||||
{
|
||||
Color c = dimmerImg.color;
|
||||
dimmerImg.color = new Color(c.r, c.g, c.b, 0f);
|
||||
}
|
||||
}
|
||||
|
||||
void HideBoxImmediate(RectTransform box)
|
||||
{
|
||||
if (box == null)
|
||||
return;
|
||||
|
||||
box.DOKill();
|
||||
box.localScale = Vector3.zero;
|
||||
box.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
void AnimateIn(RectTransform box)
|
||||
{
|
||||
box.localScale = Vector3.zero;
|
||||
box.gameObject.SetActive(true);
|
||||
|
||||
showHideSequence = DOTween.Sequence().SetUpdate(true);
|
||||
if (dimmerImg != null)
|
||||
showHideSequence.Join(dimmerImg.DOFade(0.8f, ANIMATION_DURATION));
|
||||
showHideSequence.Join(box.DOScale(1f, ANIMATION_DURATION).SetEase(Ease.OutBack));
|
||||
}
|
||||
|
||||
void AnimateOut(RectTransform box)
|
||||
{
|
||||
KillTweens();
|
||||
|
||||
RectTransform targetBox = box;
|
||||
showHideSequence = DOTween.Sequence().SetUpdate(true);
|
||||
if (dimmerImg != null)
|
||||
showHideSequence.Join(dimmerImg.DOFade(0f, ANIMATION_DURATION));
|
||||
showHideSequence.Join(targetBox.DOScale(0f, ANIMATION_DURATION).SetEase(Ease.InBack));
|
||||
showHideSequence.OnComplete(() =>
|
||||
{
|
||||
if (targetBox != null)
|
||||
targetBox.gameObject.SetActive(false);
|
||||
if (dimmer != null)
|
||||
dimmer.gameObject.SetActive(false);
|
||||
});
|
||||
}
|
||||
|
||||
void KillTweens()
|
||||
{
|
||||
if (showHideSequence != null && showHideSequence.IsActive())
|
||||
showHideSequence.Kill();
|
||||
showHideSequence = null;
|
||||
|
||||
if (dimmerImg != null)
|
||||
dimmerImg.DOKill();
|
||||
if (basicMessageBox != null)
|
||||
basicMessageBox.DOKill();
|
||||
if (messageBox != null)
|
||||
messageBox.DOKill();
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using DG.Tweening;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEngine.UI;
|
||||
|
||||
public class MessageBoxDialog : MonoBehaviour
|
||||
{
|
||||
public const float ANIMATION_DURATION = 0.25f;
|
||||
/// <summary>Above game/menu/loader canvases; below reconnect (500).</summary>
|
||||
const int OverlaySortingOrder = 400;
|
||||
|
||||
public static MessageBoxDialog instance;
|
||||
public static bool IsAvailable => instance != null;
|
||||
|
||||
public RectTransform dimmer;
|
||||
public RectTransform basicMessageBox;
|
||||
|
||||
[Header("MessageBox")]
|
||||
public RectTransform messageBox;
|
||||
public TMP_Text txtTitle, txtMessage;
|
||||
public Button btnOk, btnYes, btnNo;
|
||||
|
||||
Image dimmerImg;
|
||||
TMP_Text basicMessageText;
|
||||
Action<bool> onYesOrNo;
|
||||
Sequence showHideSequence;
|
||||
Canvas overlayCanvas;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
if (instance != null && instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
|
||||
overlayCanvas = GetComponent<Canvas>();
|
||||
if (overlayCanvas != null)
|
||||
overlayCanvas.sortingOrder = OverlaySortingOrder;
|
||||
|
||||
if (dimmer != null)
|
||||
dimmerImg = dimmer.GetComponent<Image>();
|
||||
if (basicMessageBox != null)
|
||||
basicMessageText = basicMessageBox.GetComponentInChildren<TMP_Text>();
|
||||
}
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
if (instance != this)
|
||||
return;
|
||||
SceneManager.sceneLoaded += OnSceneLoaded;
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
if (instance != this)
|
||||
return;
|
||||
SceneManager.sceneLoaded -= OnSceneLoaded;
|
||||
}
|
||||
|
||||
void OnSceneLoaded(Scene scene, LoadSceneMode mode)
|
||||
{
|
||||
transform.SetAsLastSibling();
|
||||
}
|
||||
|
||||
public static void ShowBasic(string title)
|
||||
{
|
||||
if (instance == null)
|
||||
{
|
||||
Debug.LogWarning("MessageBoxDialog missing: " + title);
|
||||
return;
|
||||
}
|
||||
|
||||
instance.ShowMessageBox(title);
|
||||
}
|
||||
|
||||
public static void Show(string title, string message, Action<bool> onResult = null)
|
||||
{
|
||||
if (instance == null)
|
||||
{
|
||||
Debug.LogWarning("MessageBoxDialog missing: " + title);
|
||||
return;
|
||||
}
|
||||
|
||||
instance.ShowMessageBox(title, message, onResult);
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
KillTweens();
|
||||
if (instance == this)
|
||||
instance = null;
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
Button basicButton = basicMessageBox != null ? basicMessageBox.GetComponent<Button>() : null;
|
||||
if (basicButton != null)
|
||||
basicButton.onClick.AddListener(HideMessageBox);
|
||||
|
||||
if (btnOk != null)
|
||||
btnOk.onClick.AddListener(HideMessageBox);
|
||||
if (btnYes != null)
|
||||
btnYes.onClick.AddListener(() => OnQuestionDialog(true));
|
||||
if (btnNo != null)
|
||||
btnNo.onClick.AddListener(() => OnQuestionDialog(false));
|
||||
}
|
||||
|
||||
void OnQuestionDialog(bool val)
|
||||
{
|
||||
Action<bool> callback = onYesOrNo;
|
||||
onYesOrNo = null;
|
||||
HideMessageBox();
|
||||
callback?.Invoke(val);
|
||||
}
|
||||
|
||||
public void ShowMessageBox(string title)
|
||||
{
|
||||
PrepareShow();
|
||||
HideBoxImmediate(messageBox);
|
||||
|
||||
if (basicMessageText != null)
|
||||
basicMessageText.text = title;
|
||||
|
||||
AnimateIn(basicMessageBox);
|
||||
}
|
||||
|
||||
public void ShowMessageBox(string title, string message, Action<bool> onResult = null)
|
||||
{
|
||||
PrepareShow();
|
||||
HideBoxImmediate(basicMessageBox);
|
||||
|
||||
txtTitle.text = title;
|
||||
txtMessage.text = message;
|
||||
|
||||
bool isQuestion = onResult != null;
|
||||
btnNo.gameObject.SetActive(isQuestion);
|
||||
btnYes.gameObject.SetActive(isQuestion);
|
||||
btnOk.gameObject.SetActive(!isQuestion);
|
||||
|
||||
onYesOrNo = onResult;
|
||||
AnimateIn(messageBox);
|
||||
}
|
||||
|
||||
public void HideMessageBox()
|
||||
{
|
||||
bool basicActive = basicMessageBox != null && basicMessageBox.gameObject.activeSelf;
|
||||
bool fullActive = messageBox != null && messageBox.gameObject.activeSelf;
|
||||
if (!basicActive && !fullActive)
|
||||
return;
|
||||
|
||||
if (basicActive && fullActive)
|
||||
HideBoxImmediate(basicMessageBox);
|
||||
|
||||
AnimateOut(basicActive && !fullActive ? basicMessageBox : messageBox);
|
||||
}
|
||||
|
||||
void PrepareShow()
|
||||
{
|
||||
KillTweens();
|
||||
onYesOrNo = null;
|
||||
transform.SetAsLastSibling();
|
||||
|
||||
dimmer.gameObject.SetActive(true);
|
||||
if (dimmerImg != null)
|
||||
{
|
||||
Color c = dimmerImg.color;
|
||||
dimmerImg.color = new Color(c.r, c.g, c.b, 0f);
|
||||
}
|
||||
}
|
||||
|
||||
void HideBoxImmediate(RectTransform box)
|
||||
{
|
||||
if (box == null)
|
||||
return;
|
||||
|
||||
box.DOKill();
|
||||
box.localScale = Vector3.zero;
|
||||
box.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
void AnimateIn(RectTransform box)
|
||||
{
|
||||
box.localScale = Vector3.zero;
|
||||
box.gameObject.SetActive(true);
|
||||
|
||||
showHideSequence = DOTween.Sequence().SetUpdate(true);
|
||||
if (dimmerImg != null)
|
||||
showHideSequence.Join(dimmerImg.DOFade(0.8f, ANIMATION_DURATION));
|
||||
showHideSequence.Join(box.DOScale(1f, ANIMATION_DURATION).SetEase(Ease.OutBack));
|
||||
}
|
||||
|
||||
void AnimateOut(RectTransform box)
|
||||
{
|
||||
KillTweens();
|
||||
|
||||
RectTransform targetBox = box;
|
||||
showHideSequence = DOTween.Sequence().SetUpdate(true);
|
||||
if (dimmerImg != null)
|
||||
showHideSequence.Join(dimmerImg.DOFade(0f, ANIMATION_DURATION));
|
||||
showHideSequence.Join(targetBox.DOScale(0f, ANIMATION_DURATION).SetEase(Ease.InBack));
|
||||
showHideSequence.OnComplete(() =>
|
||||
{
|
||||
if (targetBox != null)
|
||||
targetBox.gameObject.SetActive(false);
|
||||
if (dimmer != null)
|
||||
dimmer.gameObject.SetActive(false);
|
||||
});
|
||||
}
|
||||
|
||||
void KillTweens()
|
||||
{
|
||||
if (showHideSequence != null && showHideSequence.IsActive())
|
||||
showHideSequence.Kill();
|
||||
showHideSequence = null;
|
||||
|
||||
if (dimmerImg != null)
|
||||
dimmerImg.DOKill();
|
||||
if (basicMessageBox != null)
|
||||
basicMessageBox.DOKill();
|
||||
if (messageBox != null)
|
||||
messageBox.DOKill();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class OpenLink : MonoBehaviour
|
||||
{
|
||||
public string url = "https://www.google.com";
|
||||
|
||||
public void Open(){
|
||||
Application.OpenURL(url);
|
||||
}
|
||||
}
|
||||
using UnityEngine;
|
||||
|
||||
public class OpenLink : MonoBehaviour
|
||||
{
|
||||
public string url = "https://www.google.com";
|
||||
|
||||
public void Open(){
|
||||
Application.OpenURL(url);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class RotateAround : MonoBehaviour
|
||||
{
|
||||
public Vector3 axis = Vector3.up;
|
||||
public float speed = 10f;
|
||||
public Vector3 center = Vector3.zero;
|
||||
void Update()
|
||||
{
|
||||
transform.RotateAround(transform.position + center, axis, speed * Time.deltaTime);
|
||||
}
|
||||
}
|
||||
using UnityEngine;
|
||||
|
||||
public class RotateAround : MonoBehaviour
|
||||
{
|
||||
public Vector3 axis = Vector3.up;
|
||||
public float speed = 10f;
|
||||
public Vector3 center = Vector3.zero;
|
||||
void Update()
|
||||
{
|
||||
transform.RotateAround(transform.position + center, axis, speed * Time.deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
fileFormatVersion: 2
|
||||
guid: f67c475e52308cd42a56f95fd38c86c4
|
||||
@@ -1,8 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0b498645b8402b742a93101fc04df79f
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
fileFormatVersion: 2
|
||||
guid: 0b498645b8402b742a93101fc04df79f
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
@@ -1,236 +1,236 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using DG.Tweening;
|
||||
using UnityEngine.SceneManagement;
|
||||
using System.Collections;
|
||||
using TMPro;
|
||||
using Mirror;
|
||||
public class LevelLoadManager : MonoBehaviour
|
||||
{
|
||||
public static LevelLoadManager instance;
|
||||
|
||||
public Image contentImage;
|
||||
public float transitionDuration = 0.5f;
|
||||
CanvasGroup canvasGroup;
|
||||
|
||||
public bool isShowing => canvasGroup.alpha == 1;
|
||||
|
||||
[Header("Matchmade UI")]
|
||||
public GameObject matchmadeUI;
|
||||
public GameObject p1_red,p2_red,p1_blue,p2_blue;
|
||||
public Image p1_icon, p2_icon;
|
||||
public TMP_Text p1_name, p2_name;
|
||||
public TMP_Text p1_l10, p2_l10;
|
||||
public Sprite redIcon, blueIcon;
|
||||
public TMP_Text txtMiddle;
|
||||
public TMP_Text txtRcPrize;
|
||||
[Header("Game Over UI")]
|
||||
public GameObject p1_winner;
|
||||
public GameObject p2_winner;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
if(instance != null){
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
canvasGroup = GetComponent<CanvasGroup>();
|
||||
instance = this;
|
||||
|
||||
DontDestroyOnLoad(gameObject);
|
||||
Hide();
|
||||
|
||||
matchmadeUI.SetActive(false);
|
||||
}
|
||||
|
||||
|
||||
public static void LoadLevel(string levelName){
|
||||
instance.loadLevel(levelName);
|
||||
}
|
||||
|
||||
void loadLevel(string levelName){
|
||||
StartCoroutine(CoroutineLoadLevel(levelName));
|
||||
}
|
||||
|
||||
/// <summary>When true, Main Menu bootstrap (matchmaker settings) owns the overlay; suppress duplicate work in <see cref="MainMenuManager"/>.</summary>
|
||||
public static bool BlocksMainMenuSettingsBootstrap { get; private set; }
|
||||
|
||||
IEnumerator CoroutineLoadLevel(string levelName){
|
||||
instance.Show();
|
||||
yield return new WaitForSeconds(transitionDuration);
|
||||
if(matchmadeUI.activeSelf){
|
||||
yield return new WaitForSeconds(3f);
|
||||
}
|
||||
|
||||
bool mainMenuDestination = levelName == MainMenuManager.MainMenuSceneName;
|
||||
if (mainMenuDestination)
|
||||
BlocksMainMenuSettingsBootstrap = true;
|
||||
|
||||
yield return AddressableSceneLoader.LoadRoutine(levelName);
|
||||
|
||||
if (mainMenuDestination)
|
||||
{
|
||||
try
|
||||
{
|
||||
MainMenuManager menu = UnityEngine.Object.FindObjectOfType<MainMenuManager>();
|
||||
if (menu != null)
|
||||
yield return menu.FinishLoadingScreenAndApplySettings();
|
||||
}
|
||||
finally
|
||||
{
|
||||
BlocksMainMenuSettingsBootstrap = false;
|
||||
}
|
||||
}
|
||||
|
||||
instance.Hide();
|
||||
}
|
||||
|
||||
|
||||
public void Show(){
|
||||
if(isShowing){return;} //already is showing
|
||||
canvasGroup.blocksRaycasts=true;
|
||||
canvasGroup.interactable=true;
|
||||
contentImage.fillOrigin = 0;
|
||||
canvasGroup.DOFade(1, transitionDuration).SetEase(Ease.OutSine);
|
||||
contentImage.DOFillAmount(1, transitionDuration).SetEase(Ease.OutSine);
|
||||
}
|
||||
|
||||
public void Hide(){
|
||||
canvasGroup.blocksRaycasts=false;
|
||||
canvasGroup.interactable=false;
|
||||
contentImage.fillOrigin = 1;
|
||||
canvasGroup.DOFade(0, transitionDuration).SetEase(Ease.InSine);
|
||||
contentImage.DOFillAmount(0, transitionDuration).SetEase(Ease.InSine).OnComplete(() => {
|
||||
if(gameOver){
|
||||
gameOver = false;
|
||||
matchmadeUI.SetActive(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drops the last match from the loading screen. This object survives scene loads, so logout must call this
|
||||
/// or the next login shows the previous matchup while the overlay is up.
|
||||
/// </summary>
|
||||
public void ResetSession()
|
||||
{
|
||||
StopAllCoroutines();
|
||||
BlocksMainMenuSettingsBootstrap = false;
|
||||
gameOver = false;
|
||||
myPlayer = null;
|
||||
opponentPlayer = null;
|
||||
|
||||
canvasGroup.DOKill();
|
||||
contentImage.DOKill();
|
||||
canvasGroup.alpha = 0f;
|
||||
canvasGroup.blocksRaycasts = false;
|
||||
canvasGroup.interactable = false;
|
||||
contentImage.fillAmount = 0f;
|
||||
|
||||
matchmadeUI.SetActive(false);
|
||||
p1_winner.SetActive(false);
|
||||
p2_winner.SetActive(false);
|
||||
txtMiddle.text = "VS";
|
||||
p1_name.text = "";
|
||||
p2_name.text = "";
|
||||
p1_l10.text = "";
|
||||
p2_l10.text = "";
|
||||
txtRcPrize.text = "";
|
||||
}
|
||||
|
||||
Team myTeam = Team.Red;
|
||||
|
||||
public void SetupRematch(int rc_prize, int port, int gameId){
|
||||
txtRcPrize.text = CoinHelper.FormatString(rc_prize) + " RC";
|
||||
|
||||
}
|
||||
|
||||
public void SetupMatchMade(Team myTeam_, string opponentName, string myName, string myL10, string opponentL10, string rcPrize){
|
||||
myTeam = myTeam_;
|
||||
matchmadeUI.SetActive(true);
|
||||
p1_winner.SetActive(false);
|
||||
p2_winner.SetActive(false);
|
||||
|
||||
txtMiddle.text = "VS";
|
||||
if(myTeam == Team.Red){
|
||||
p1_red.SetActive(true);
|
||||
p1_blue.SetActive(false);
|
||||
p2_red.SetActive(false);
|
||||
p2_blue.SetActive(true);
|
||||
|
||||
p1_icon.sprite = redIcon;
|
||||
p2_icon.sprite = blueIcon;
|
||||
}else{
|
||||
p1_red.SetActive(false);
|
||||
p1_blue.SetActive(true);
|
||||
p2_red.SetActive(true);
|
||||
p2_blue.SetActive(false);
|
||||
|
||||
p1_icon.sprite = blueIcon;
|
||||
p2_icon.sprite = redIcon;
|
||||
}
|
||||
|
||||
p1_name.text = opponentName;
|
||||
p2_name.text = myName;
|
||||
p1_l10.text = myL10;
|
||||
p2_l10.text = opponentL10;
|
||||
|
||||
txtRcPrize.text = rcPrize;
|
||||
}
|
||||
|
||||
public static MatchmadePlayer myPlayer;
|
||||
public static MatchmadePlayer opponentPlayer;
|
||||
|
||||
public void SetupMatchMade(Team myTeam_, MatchmadePlayer myPlayer_, MatchmadePlayer opponentPlayer_, string rcPrize){
|
||||
myTeam = myTeam_;
|
||||
myPlayer = myPlayer_;
|
||||
opponentPlayer = opponentPlayer_;
|
||||
string opponentName = opponentPlayer_.Name;
|
||||
string myName = myPlayer_.Name;
|
||||
|
||||
string myL10 = $"L10 {myPlayer.l10_wins} -{myPlayer.l10_losses}";
|
||||
string opponentL10 = $"L10 {opponentPlayer.l10_wins} -{opponentPlayer.l10_losses}";
|
||||
|
||||
SetupMatchMade(myTeam, myName, opponentName, myL10, opponentL10, rcPrize);
|
||||
}
|
||||
bool gameOver = false;
|
||||
public void SetupGameOver(Team winningTeam, int redScore, int blueScore){
|
||||
gameOver = true;
|
||||
|
||||
txtMiddle.text = $"{(myTeam == Team.Red ? redScore : blueScore)} FINAL {(myTeam == Team.Red ? blueScore : redScore)}";
|
||||
|
||||
if(myTeam == winningTeam){
|
||||
p1_winner.SetActive(true);
|
||||
p2_winner.SetActive(false);
|
||||
}else{
|
||||
p1_winner.SetActive(false);
|
||||
p2_winner.SetActive(true);
|
||||
}
|
||||
|
||||
p1_l10.text = $"L10 {myPlayer.l10_wins}-{myPlayer.l10_losses}";
|
||||
p2_l10.text = $"L10 {opponentPlayer.l10_wins}-{opponentPlayer.l10_losses}";
|
||||
|
||||
}
|
||||
|
||||
|
||||
public void Leave(string levelName = "MainMenu"){
|
||||
NetManager.StopNetworkingForSceneChange();
|
||||
|
||||
StartCoroutine(CoroutineWaitTillDisconnection(levelName));
|
||||
}
|
||||
|
||||
IEnumerator CoroutineWaitTillDisconnection(string levelName){
|
||||
Show(); //Load the UI until the level is loaded
|
||||
const float timeoutSeconds = 5f;
|
||||
float elapsed = 0f;
|
||||
while ((NetworkClient.isConnected || NetworkClient.active || NetworkServer.active) && elapsed < timeoutSeconds)
|
||||
{
|
||||
elapsed += Time.unscaledDeltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
yield return new WaitForSeconds(0.25f);
|
||||
|
||||
LoadLevel(levelName);
|
||||
}
|
||||
}
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using DG.Tweening;
|
||||
using UnityEngine.SceneManagement;
|
||||
using System.Collections;
|
||||
using TMPro;
|
||||
using Mirror;
|
||||
public class LevelLoadManager : MonoBehaviour
|
||||
{
|
||||
public static LevelLoadManager instance;
|
||||
|
||||
public Image contentImage;
|
||||
public float transitionDuration = 0.5f;
|
||||
CanvasGroup canvasGroup;
|
||||
|
||||
public bool isShowing => canvasGroup.alpha == 1;
|
||||
|
||||
[Header("Matchmade UI")]
|
||||
public GameObject matchmadeUI;
|
||||
public GameObject p1_red,p2_red,p1_blue,p2_blue;
|
||||
public Image p1_icon, p2_icon;
|
||||
public TMP_Text p1_name, p2_name;
|
||||
public TMP_Text p1_l10, p2_l10;
|
||||
public Sprite redIcon, blueIcon;
|
||||
public TMP_Text txtMiddle;
|
||||
public TMP_Text txtRcPrize;
|
||||
[Header("Game Over UI")]
|
||||
public GameObject p1_winner;
|
||||
public GameObject p2_winner;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
if(instance != null){
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
canvasGroup = GetComponent<CanvasGroup>();
|
||||
instance = this;
|
||||
|
||||
DontDestroyOnLoad(gameObject);
|
||||
Hide();
|
||||
|
||||
matchmadeUI.SetActive(false);
|
||||
}
|
||||
|
||||
|
||||
public static void LoadLevel(string levelName){
|
||||
instance.loadLevel(levelName);
|
||||
}
|
||||
|
||||
void loadLevel(string levelName){
|
||||
StartCoroutine(CoroutineLoadLevel(levelName));
|
||||
}
|
||||
|
||||
/// <summary>When true, Main Menu bootstrap (matchmaker settings) owns the overlay; suppress duplicate work in <see cref="MainMenuManager"/>.</summary>
|
||||
public static bool BlocksMainMenuSettingsBootstrap { get; private set; }
|
||||
|
||||
IEnumerator CoroutineLoadLevel(string levelName){
|
||||
instance.Show();
|
||||
yield return new WaitForSeconds(transitionDuration);
|
||||
if(matchmadeUI.activeSelf){
|
||||
yield return new WaitForSeconds(3f);
|
||||
}
|
||||
|
||||
bool mainMenuDestination = levelName == MainMenuManager.MainMenuSceneName;
|
||||
if (mainMenuDestination)
|
||||
BlocksMainMenuSettingsBootstrap = true;
|
||||
|
||||
yield return AddressableSceneLoader.LoadRoutine(levelName);
|
||||
|
||||
if (mainMenuDestination)
|
||||
{
|
||||
try
|
||||
{
|
||||
MainMenuManager menu = UnityEngine.Object.FindObjectOfType<MainMenuManager>();
|
||||
if (menu != null)
|
||||
yield return menu.FinishLoadingScreenAndApplySettings();
|
||||
}
|
||||
finally
|
||||
{
|
||||
BlocksMainMenuSettingsBootstrap = false;
|
||||
}
|
||||
}
|
||||
|
||||
instance.Hide();
|
||||
}
|
||||
|
||||
|
||||
public void Show(){
|
||||
if(isShowing){return;} //already is showing
|
||||
canvasGroup.blocksRaycasts=true;
|
||||
canvasGroup.interactable=true;
|
||||
contentImage.fillOrigin = 0;
|
||||
canvasGroup.DOFade(1, transitionDuration).SetEase(Ease.OutSine);
|
||||
contentImage.DOFillAmount(1, transitionDuration).SetEase(Ease.OutSine);
|
||||
}
|
||||
|
||||
public void Hide(){
|
||||
canvasGroup.blocksRaycasts=false;
|
||||
canvasGroup.interactable=false;
|
||||
contentImage.fillOrigin = 1;
|
||||
canvasGroup.DOFade(0, transitionDuration).SetEase(Ease.InSine);
|
||||
contentImage.DOFillAmount(0, transitionDuration).SetEase(Ease.InSine).OnComplete(() => {
|
||||
if(gameOver){
|
||||
gameOver = false;
|
||||
matchmadeUI.SetActive(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drops the last match from the loading screen. This object survives scene loads, so logout must call this
|
||||
/// or the next login shows the previous matchup while the overlay is up.
|
||||
/// </summary>
|
||||
public void ResetSession()
|
||||
{
|
||||
StopAllCoroutines();
|
||||
BlocksMainMenuSettingsBootstrap = false;
|
||||
gameOver = false;
|
||||
myPlayer = null;
|
||||
opponentPlayer = null;
|
||||
|
||||
canvasGroup.DOKill();
|
||||
contentImage.DOKill();
|
||||
canvasGroup.alpha = 0f;
|
||||
canvasGroup.blocksRaycasts = false;
|
||||
canvasGroup.interactable = false;
|
||||
contentImage.fillAmount = 0f;
|
||||
|
||||
matchmadeUI.SetActive(false);
|
||||
p1_winner.SetActive(false);
|
||||
p2_winner.SetActive(false);
|
||||
txtMiddle.text = "VS";
|
||||
p1_name.text = "";
|
||||
p2_name.text = "";
|
||||
p1_l10.text = "";
|
||||
p2_l10.text = "";
|
||||
txtRcPrize.text = "";
|
||||
}
|
||||
|
||||
Team myTeam = Team.Red;
|
||||
|
||||
public void SetupRematch(int rc_prize, int port, int gameId){
|
||||
txtRcPrize.text = CoinHelper.FormatString(rc_prize) + " RC";
|
||||
|
||||
}
|
||||
|
||||
public void SetupMatchMade(Team myTeam_, string opponentName, string myName, string myL10, string opponentL10, string rcPrize){
|
||||
myTeam = myTeam_;
|
||||
matchmadeUI.SetActive(true);
|
||||
p1_winner.SetActive(false);
|
||||
p2_winner.SetActive(false);
|
||||
|
||||
txtMiddle.text = "VS";
|
||||
if(myTeam == Team.Red){
|
||||
p1_red.SetActive(true);
|
||||
p1_blue.SetActive(false);
|
||||
p2_red.SetActive(false);
|
||||
p2_blue.SetActive(true);
|
||||
|
||||
p1_icon.sprite = redIcon;
|
||||
p2_icon.sprite = blueIcon;
|
||||
}else{
|
||||
p1_red.SetActive(false);
|
||||
p1_blue.SetActive(true);
|
||||
p2_red.SetActive(true);
|
||||
p2_blue.SetActive(false);
|
||||
|
||||
p1_icon.sprite = blueIcon;
|
||||
p2_icon.sprite = redIcon;
|
||||
}
|
||||
|
||||
p1_name.text = opponentName;
|
||||
p2_name.text = myName;
|
||||
p1_l10.text = myL10;
|
||||
p2_l10.text = opponentL10;
|
||||
|
||||
txtRcPrize.text = rcPrize;
|
||||
}
|
||||
|
||||
public static MatchmadePlayer myPlayer;
|
||||
public static MatchmadePlayer opponentPlayer;
|
||||
|
||||
public void SetupMatchMade(Team myTeam_, MatchmadePlayer myPlayer_, MatchmadePlayer opponentPlayer_, string rcPrize){
|
||||
myTeam = myTeam_;
|
||||
myPlayer = myPlayer_;
|
||||
opponentPlayer = opponentPlayer_;
|
||||
string opponentName = opponentPlayer_.Name;
|
||||
string myName = myPlayer_.Name;
|
||||
|
||||
string myL10 = $"L10 {myPlayer.l10_wins} -{myPlayer.l10_losses}";
|
||||
string opponentL10 = $"L10 {opponentPlayer.l10_wins} -{opponentPlayer.l10_losses}";
|
||||
|
||||
SetupMatchMade(myTeam, myName, opponentName, myL10, opponentL10, rcPrize);
|
||||
}
|
||||
bool gameOver = false;
|
||||
public void SetupGameOver(Team winningTeam, int redScore, int blueScore){
|
||||
gameOver = true;
|
||||
|
||||
txtMiddle.text = $"{(myTeam == Team.Red ? redScore : blueScore)} FINAL {(myTeam == Team.Red ? blueScore : redScore)}";
|
||||
|
||||
if(myTeam == winningTeam){
|
||||
p1_winner.SetActive(true);
|
||||
p2_winner.SetActive(false);
|
||||
}else{
|
||||
p1_winner.SetActive(false);
|
||||
p2_winner.SetActive(true);
|
||||
}
|
||||
|
||||
p1_l10.text = $"L10 {myPlayer.l10_wins}-{myPlayer.l10_losses}";
|
||||
p2_l10.text = $"L10 {opponentPlayer.l10_wins}-{opponentPlayer.l10_losses}";
|
||||
|
||||
}
|
||||
|
||||
|
||||
public void Leave(string levelName = "MainMenu"){
|
||||
NetManager.StopNetworkingForSceneChange();
|
||||
|
||||
StartCoroutine(CoroutineWaitTillDisconnection(levelName));
|
||||
}
|
||||
|
||||
IEnumerator CoroutineWaitTillDisconnection(string levelName){
|
||||
Show(); //Load the UI until the level is loaded
|
||||
const float timeoutSeconds = 5f;
|
||||
float elapsed = 0f;
|
||||
while ((NetworkClient.isConnected || NetworkClient.active || NetworkServer.active) && elapsed < timeoutSeconds)
|
||||
{
|
||||
elapsed += Time.unscaledDeltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
yield return new WaitForSeconds(0.25f);
|
||||
|
||||
LoadLevel(levelName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
fileFormatVersion: 2
|
||||
guid: be7470bea6c16b9439ddf2a399e94cfc
|
||||
@@ -1,30 +1,30 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class SetActiveOnEditor : MonoBehaviour
|
||||
{
|
||||
public Component[] components;
|
||||
public GameObject[] gameObjects;
|
||||
public bool activeOnEditor = true;
|
||||
void Start()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
foreach(var component in components)
|
||||
{
|
||||
component.gameObject.SetActive(activeOnEditor);
|
||||
}
|
||||
foreach(var gameObject in gameObjects)
|
||||
{
|
||||
gameObject.SetActive(activeOnEditor);
|
||||
}
|
||||
#else
|
||||
foreach(var component in components)
|
||||
{
|
||||
component.gameObject.SetActive(!activeOnEditor);
|
||||
}
|
||||
foreach(var gameObject in gameObjects)
|
||||
{
|
||||
gameObject.SetActive(!activeOnEditor);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
using UnityEngine;
|
||||
|
||||
public class SetActiveOnEditor : MonoBehaviour
|
||||
{
|
||||
public Component[] components;
|
||||
public GameObject[] gameObjects;
|
||||
public bool activeOnEditor = true;
|
||||
void Start()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
foreach(var component in components)
|
||||
{
|
||||
component.gameObject.SetActive(activeOnEditor);
|
||||
}
|
||||
foreach(var gameObject in gameObjects)
|
||||
{
|
||||
gameObject.SetActive(activeOnEditor);
|
||||
}
|
||||
#else
|
||||
foreach(var component in components)
|
||||
{
|
||||
component.gameObject.SetActive(!activeOnEditor);
|
||||
}
|
||||
foreach(var gameObject in gameObjects)
|
||||
{
|
||||
gameObject.SetActive(!activeOnEditor);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
fileFormatVersion: 2
|
||||
guid: 333a4e464b877d44abda3fce877fa3be
|
||||
@@ -1,8 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fd2602c01ff00f047aab3e93a95bb230
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
fileFormatVersion: 2
|
||||
guid: fd2602c01ff00f047aab3e93a95bb230
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/// <summary>Typical <c>pack</c> ids for <see cref="LoginManager.PurchaseRcPack"/>; prefer <see cref="LoginManager.GetRcPacks"/> for the live list.</summary>
|
||||
public static class RcPack
|
||||
{
|
||||
public const string Usd5 = "usd_5";
|
||||
public const string Usd20 = "usd_20";
|
||||
public const string Usd50 = "usd_50";
|
||||
|
||||
public static bool IsValid(string packId)
|
||||
{
|
||||
return packId == Usd5 || packId == Usd20 || packId == Usd50;
|
||||
}
|
||||
}
|
||||
/// <summary>Typical <c>pack</c> ids for <see cref="LoginManager.PurchaseRcPack"/>; prefer <see cref="LoginManager.GetRcPacks"/> for the live list.</summary>
|
||||
public static class RcPack
|
||||
{
|
||||
public const string Usd5 = "usd_5";
|
||||
public const string Usd20 = "usd_20";
|
||||
public const string Usd50 = "usd_50";
|
||||
|
||||
public static bool IsValid(string packId)
|
||||
{
|
||||
return packId == Usd5 || packId == Usd20 || packId == Usd50;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
fileFormatVersion: 2
|
||||
guid: 241f412d7db597748af9c1138e640e0e
|
||||
@@ -1,14 +1,14 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
[Serializable]
|
||||
public class UserData
|
||||
{
|
||||
public int id;
|
||||
public string username;
|
||||
public string email;
|
||||
public int cc;
|
||||
public int rc;
|
||||
public string created_at;
|
||||
public string last_logged_at;
|
||||
}
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
[Serializable]
|
||||
public class UserData
|
||||
{
|
||||
public int id;
|
||||
public string username;
|
||||
public string email;
|
||||
public int cc;
|
||||
public int rc;
|
||||
public string created_at;
|
||||
public string last_logged_at;
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
fileFormatVersion: 2
|
||||
guid: 7fa72c32d6a0d1e4abc0b724b1988d8c
|
||||
@@ -1,8 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 031adef9a13d67c48b0969916c4e2160
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
fileFormatVersion: 2
|
||||
guid: 031adef9a13d67c48b0969916c4e2160
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
[Serializable]
|
||||
public class AuthApiResponse
|
||||
{
|
||||
public bool ok;
|
||||
public string error;
|
||||
public string code;
|
||||
public string token;
|
||||
public UserData user;
|
||||
}
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
[Serializable]
|
||||
public class AuthApiResponse
|
||||
{
|
||||
public bool ok;
|
||||
public string error;
|
||||
public string code;
|
||||
public string token;
|
||||
public UserData user;
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
fileFormatVersion: 2
|
||||
guid: 91e26300ebc95df47949bbd3744bf975
|
||||
@@ -1,10 +1,10 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
[Serializable]
|
||||
public class AuthUserApiResponse
|
||||
{
|
||||
public bool ok;
|
||||
public string error;
|
||||
public UserData user;
|
||||
}
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
[Serializable]
|
||||
public class AuthUserApiResponse
|
||||
{
|
||||
public bool ok;
|
||||
public string error;
|
||||
public UserData user;
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
fileFormatVersion: 2
|
||||
guid: d82de2425d5fb6448bc038942fb1aa88
|
||||
@@ -1,254 +1,254 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
[Serializable]
|
||||
public class MatchmadePlayer
|
||||
{
|
||||
public string Name;
|
||||
public long LastSeen;
|
||||
public int l10_wins;
|
||||
public int l10_losses;
|
||||
public int UserId;
|
||||
/// <summary>Rocket Credits from server (e.g. GET /players/:id/rc). Cached client-side.</summary>
|
||||
public float rc;
|
||||
}
|
||||
|
||||
/// <summary>Per-team block from matchmaker GET / when a side is in queue or match is ready.</summary>
|
||||
[Serializable]
|
||||
public class MatchmadeTeamPayload
|
||||
{
|
||||
public MatchmadePlayer[] Players;
|
||||
public string GameName;
|
||||
public int Port;
|
||||
public long InitTime;
|
||||
public bool instance_started;
|
||||
public int match_id;
|
||||
public int entry_fee;
|
||||
public int rc_prize;
|
||||
public string your_team;
|
||||
}
|
||||
|
||||
/// <summary>Envelope from matchmaker fill / rematch success (and in-progress queue state).</summary>
|
||||
[Serializable]
|
||||
public class MatchmadeResponse
|
||||
{
|
||||
public bool ok;
|
||||
public int entry_fee;
|
||||
public int rc_prize;
|
||||
public MatchmadeTeamPayload for_red;
|
||||
public MatchmadeTeamPayload for_blue;
|
||||
/// <summary>Optional root field: which nested block is for this client when both <see cref="for_red"/> and <see cref="for_blue"/> list the same players.</summary>
|
||||
public string your_team;
|
||||
|
||||
public static bool TeamPayloadHasPort(MatchmadeTeamPayload p) =>
|
||||
p != null && p.Port > 0;
|
||||
|
||||
/// <summary>True when both sides are present on the same dedicated instance (2v2-style duplicate rosters).</summary>
|
||||
public static bool IsMatchReadyDuplicateRosters(MatchmadeResponse e)
|
||||
{
|
||||
if (e == null || !e.ok)
|
||||
return false;
|
||||
if (!TeamPayloadHasPort(e.for_red) || !TeamPayloadHasPort(e.for_blue))
|
||||
return false;
|
||||
if (e.for_red.Players == null || e.for_blue.Players == null)
|
||||
return false;
|
||||
if (e.for_red.match_id != e.for_blue.match_id || e.for_red.Port != e.for_blue.Port)
|
||||
return false;
|
||||
return e.for_red.Players.Length >= 2 && e.for_blue.Players.Length >= 2;
|
||||
}
|
||||
|
||||
/// <summary>True when each side lists at least one player and together they form a full 1v1 (disjoint user sets, same instance).</summary>
|
||||
public static bool IsMatchReadySplitRosters(MatchmadeResponse e)
|
||||
{
|
||||
if (e == null || !e.ok)
|
||||
return false;
|
||||
if (!TeamPayloadHasPort(e.for_red) || !TeamPayloadHasPort(e.for_blue))
|
||||
return false;
|
||||
if (e.for_red.Players == null || e.for_blue.Players == null)
|
||||
return false;
|
||||
if (e.for_red.match_id != e.for_blue.match_id || e.for_red.Port != e.for_blue.Port)
|
||||
return false;
|
||||
if (e.for_red.Players.Length < 1 || e.for_blue.Players.Length < 1)
|
||||
return false;
|
||||
|
||||
int a = e.for_red.Players[0].UserId;
|
||||
int b = e.for_blue.Players[0].UserId;
|
||||
if (a == b)
|
||||
return false;
|
||||
if (e.for_red.Players.Length == 1 && e.for_blue.Players.Length == 1)
|
||||
return true;
|
||||
// Wider payloads: require no user id appearing on both sides
|
||||
foreach (var pr in e.for_red.Players)
|
||||
{
|
||||
foreach (var pb in e.for_blue.Players)
|
||||
{
|
||||
if (pr.UserId == pb.UserId)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool IsMatchReady(MatchmadeResponse e) =>
|
||||
IsMatchReadyDuplicateRosters(e) || IsMatchReadySplitRosters(e);
|
||||
|
||||
static bool PayloadContainsUserId(MatchmadeTeamPayload p, int userId)
|
||||
{
|
||||
if (p?.Players == null || userId == 0)
|
||||
return false;
|
||||
for (int i = 0; i < p.Players.Length; i++)
|
||||
{
|
||||
if (p.Players[i].UserId == userId)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static int IndexOfUserId(MatchmadePlayer[] players, int userId)
|
||||
{
|
||||
if (players == null || userId == 0)
|
||||
return -1;
|
||||
for (int i = 0; i < players.Length; i++)
|
||||
{
|
||||
if (players[i].UserId == userId)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// <summary>Duplicate roster payloads: same users in the same order on red and blue blocks.</summary>
|
||||
static bool SamePlayerOrder(MatchmadePlayer[] a, MatchmadePlayer[] b)
|
||||
{
|
||||
if (a == null || b == null || a.Length != b.Length)
|
||||
return false;
|
||||
for (int i = 0; i < a.Length; i++)
|
||||
{
|
||||
if (a[i].UserId != b[i].UserId)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When <see cref="for_red"/> and <see cref="for_blue"/> list the same roster in the same order (1v1),
|
||||
/// matchmaker convention: index 0 = red team, index 1 = blue team.
|
||||
/// </summary>
|
||||
static MatchmadeTeamPayload SelectDuplicateRosterBySlot(MatchmadeResponse env, int userId)
|
||||
{
|
||||
MatchmadePlayer[] r = env.for_red?.Players;
|
||||
MatchmadePlayer[] b = env.for_blue?.Players;
|
||||
if (r == null || b == null || r.Length != 2 || b.Length != 2)
|
||||
return null;
|
||||
if (!SamePlayerOrder(r, b))
|
||||
return null;
|
||||
int idx = IndexOfUserId(r, userId);
|
||||
if (idx == 0)
|
||||
return env.for_red;
|
||||
if (idx == 1)
|
||||
return env.for_blue;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>Pick the nested block for this client (port, your_team, players for UI).</summary>
|
||||
public static MatchmadeTeamPayload SelectTeamPayload(MatchmadeResponse env, int userId, Team? teamDisambiguation = null)
|
||||
{
|
||||
if (env == null)
|
||||
return null;
|
||||
|
||||
if (!string.IsNullOrEmpty(env.your_team))
|
||||
{
|
||||
string y = env.your_team.Trim().ToLowerInvariant();
|
||||
if (y == "red")
|
||||
return env.for_red;
|
||||
if (y == "blue")
|
||||
return env.for_blue;
|
||||
}
|
||||
|
||||
bool onRed = PayloadContainsUserId(env.for_red, userId);
|
||||
bool onBlue = PayloadContainsUserId(env.for_blue, userId);
|
||||
if (onRed && !onBlue)
|
||||
return env.for_red;
|
||||
if (onBlue && !onRed)
|
||||
return env.for_blue;
|
||||
|
||||
if (onRed && onBlue)
|
||||
{
|
||||
MatchmadeTeamPayload bySlot = SelectDuplicateRosterBySlot(env, userId);
|
||||
if (bySlot != null)
|
||||
return bySlot;
|
||||
|
||||
if (teamDisambiguation.HasValue)
|
||||
{
|
||||
if (teamDisambiguation.Value == Team.Red)
|
||||
return env.for_red;
|
||||
if (teamDisambiguation.Value == Team.Blue)
|
||||
return env.for_blue;
|
||||
}
|
||||
|
||||
Debug.LogWarning("MatchmadeResponse: both sides list this user but roster is not 1v1 duplicate order; set root your_team on the server or pass team disambiguation.");
|
||||
}
|
||||
|
||||
return env.for_red;
|
||||
}
|
||||
|
||||
/// <summary>Resolve self and opponent from the selected branch (duplicate full roster) or from both sides (split 1v1 rosters).</summary>
|
||||
public static bool TryResolveMyAndOpponent(MatchmadeResponse env, MatchmadeTeamPayload branch, int userId, out MatchmadePlayer myPlayer, out MatchmadePlayer opponentPlayer)
|
||||
{
|
||||
myPlayer = null;
|
||||
opponentPlayer = null;
|
||||
if (env == null || branch == null || branch.Players == null || userId == 0)
|
||||
return false;
|
||||
|
||||
MatchmadeTeamPayload other = ReferenceEquals(branch, env.for_blue) ? env.for_red : env.for_blue;
|
||||
|
||||
if (branch.Players.Length >= 2)
|
||||
{
|
||||
for (int i = 0; i < branch.Players.Length; i++)
|
||||
{
|
||||
if (branch.Players[i].UserId == userId)
|
||||
{
|
||||
myPlayer = branch.Players[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (myPlayer == null)
|
||||
return false;
|
||||
for (int i = 0; i < branch.Players.Length; i++)
|
||||
{
|
||||
if (branch.Players[i].UserId != myPlayer.UserId)
|
||||
{
|
||||
opponentPlayer = branch.Players[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
return opponentPlayer != null;
|
||||
}
|
||||
|
||||
for (int i = 0; i < branch.Players.Length; i++)
|
||||
{
|
||||
if (branch.Players[i].UserId == userId)
|
||||
{
|
||||
myPlayer = branch.Players[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (myPlayer == null && branch.Players.Length == 1 && branch.Players[0].UserId == userId)
|
||||
myPlayer = branch.Players[0];
|
||||
|
||||
if (other?.Players == null)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < other.Players.Length; i++)
|
||||
{
|
||||
if (other.Players[i].UserId != userId)
|
||||
{
|
||||
opponentPlayer = other.Players[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (opponentPlayer == null && other.Players.Length == 1)
|
||||
opponentPlayer = other.Players[0];
|
||||
|
||||
return myPlayer != null && opponentPlayer != null && myPlayer.UserId != opponentPlayer.UserId;
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
[Serializable]
|
||||
public class MatchmadePlayer
|
||||
{
|
||||
public string Name;
|
||||
public long LastSeen;
|
||||
public int l10_wins;
|
||||
public int l10_losses;
|
||||
public int UserId;
|
||||
/// <summary>Rocket Credits from server (e.g. GET /players/:id/rc). Cached client-side.</summary>
|
||||
public float rc;
|
||||
}
|
||||
|
||||
/// <summary>Per-team block from matchmaker GET / when a side is in queue or match is ready.</summary>
|
||||
[Serializable]
|
||||
public class MatchmadeTeamPayload
|
||||
{
|
||||
public MatchmadePlayer[] Players;
|
||||
public string GameName;
|
||||
public int Port;
|
||||
public long InitTime;
|
||||
public bool instance_started;
|
||||
public int match_id;
|
||||
public int entry_fee;
|
||||
public int rc_prize;
|
||||
public string your_team;
|
||||
}
|
||||
|
||||
/// <summary>Envelope from matchmaker fill / rematch success (and in-progress queue state).</summary>
|
||||
[Serializable]
|
||||
public class MatchmadeResponse
|
||||
{
|
||||
public bool ok;
|
||||
public int entry_fee;
|
||||
public int rc_prize;
|
||||
public MatchmadeTeamPayload for_red;
|
||||
public MatchmadeTeamPayload for_blue;
|
||||
/// <summary>Optional root field: which nested block is for this client when both <see cref="for_red"/> and <see cref="for_blue"/> list the same players.</summary>
|
||||
public string your_team;
|
||||
|
||||
public static bool TeamPayloadHasPort(MatchmadeTeamPayload p) =>
|
||||
p != null && p.Port > 0;
|
||||
|
||||
/// <summary>True when both sides are present on the same dedicated instance (2v2-style duplicate rosters).</summary>
|
||||
public static bool IsMatchReadyDuplicateRosters(MatchmadeResponse e)
|
||||
{
|
||||
if (e == null || !e.ok)
|
||||
return false;
|
||||
if (!TeamPayloadHasPort(e.for_red) || !TeamPayloadHasPort(e.for_blue))
|
||||
return false;
|
||||
if (e.for_red.Players == null || e.for_blue.Players == null)
|
||||
return false;
|
||||
if (e.for_red.match_id != e.for_blue.match_id || e.for_red.Port != e.for_blue.Port)
|
||||
return false;
|
||||
return e.for_red.Players.Length >= 2 && e.for_blue.Players.Length >= 2;
|
||||
}
|
||||
|
||||
/// <summary>True when each side lists at least one player and together they form a full 1v1 (disjoint user sets, same instance).</summary>
|
||||
public static bool IsMatchReadySplitRosters(MatchmadeResponse e)
|
||||
{
|
||||
if (e == null || !e.ok)
|
||||
return false;
|
||||
if (!TeamPayloadHasPort(e.for_red) || !TeamPayloadHasPort(e.for_blue))
|
||||
return false;
|
||||
if (e.for_red.Players == null || e.for_blue.Players == null)
|
||||
return false;
|
||||
if (e.for_red.match_id != e.for_blue.match_id || e.for_red.Port != e.for_blue.Port)
|
||||
return false;
|
||||
if (e.for_red.Players.Length < 1 || e.for_blue.Players.Length < 1)
|
||||
return false;
|
||||
|
||||
int a = e.for_red.Players[0].UserId;
|
||||
int b = e.for_blue.Players[0].UserId;
|
||||
if (a == b)
|
||||
return false;
|
||||
if (e.for_red.Players.Length == 1 && e.for_blue.Players.Length == 1)
|
||||
return true;
|
||||
// Wider payloads: require no user id appearing on both sides
|
||||
foreach (var pr in e.for_red.Players)
|
||||
{
|
||||
foreach (var pb in e.for_blue.Players)
|
||||
{
|
||||
if (pr.UserId == pb.UserId)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool IsMatchReady(MatchmadeResponse e) =>
|
||||
IsMatchReadyDuplicateRosters(e) || IsMatchReadySplitRosters(e);
|
||||
|
||||
static bool PayloadContainsUserId(MatchmadeTeamPayload p, int userId)
|
||||
{
|
||||
if (p?.Players == null || userId == 0)
|
||||
return false;
|
||||
for (int i = 0; i < p.Players.Length; i++)
|
||||
{
|
||||
if (p.Players[i].UserId == userId)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static int IndexOfUserId(MatchmadePlayer[] players, int userId)
|
||||
{
|
||||
if (players == null || userId == 0)
|
||||
return -1;
|
||||
for (int i = 0; i < players.Length; i++)
|
||||
{
|
||||
if (players[i].UserId == userId)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// <summary>Duplicate roster payloads: same users in the same order on red and blue blocks.</summary>
|
||||
static bool SamePlayerOrder(MatchmadePlayer[] a, MatchmadePlayer[] b)
|
||||
{
|
||||
if (a == null || b == null || a.Length != b.Length)
|
||||
return false;
|
||||
for (int i = 0; i < a.Length; i++)
|
||||
{
|
||||
if (a[i].UserId != b[i].UserId)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When <see cref="for_red"/> and <see cref="for_blue"/> list the same roster in the same order (1v1),
|
||||
/// matchmaker convention: index 0 = red team, index 1 = blue team.
|
||||
/// </summary>
|
||||
static MatchmadeTeamPayload SelectDuplicateRosterBySlot(MatchmadeResponse env, int userId)
|
||||
{
|
||||
MatchmadePlayer[] r = env.for_red?.Players;
|
||||
MatchmadePlayer[] b = env.for_blue?.Players;
|
||||
if (r == null || b == null || r.Length != 2 || b.Length != 2)
|
||||
return null;
|
||||
if (!SamePlayerOrder(r, b))
|
||||
return null;
|
||||
int idx = IndexOfUserId(r, userId);
|
||||
if (idx == 0)
|
||||
return env.for_red;
|
||||
if (idx == 1)
|
||||
return env.for_blue;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>Pick the nested block for this client (port, your_team, players for UI).</summary>
|
||||
public static MatchmadeTeamPayload SelectTeamPayload(MatchmadeResponse env, int userId, Team? teamDisambiguation = null)
|
||||
{
|
||||
if (env == null)
|
||||
return null;
|
||||
|
||||
if (!string.IsNullOrEmpty(env.your_team))
|
||||
{
|
||||
string y = env.your_team.Trim().ToLowerInvariant();
|
||||
if (y == "red")
|
||||
return env.for_red;
|
||||
if (y == "blue")
|
||||
return env.for_blue;
|
||||
}
|
||||
|
||||
bool onRed = PayloadContainsUserId(env.for_red, userId);
|
||||
bool onBlue = PayloadContainsUserId(env.for_blue, userId);
|
||||
if (onRed && !onBlue)
|
||||
return env.for_red;
|
||||
if (onBlue && !onRed)
|
||||
return env.for_blue;
|
||||
|
||||
if (onRed && onBlue)
|
||||
{
|
||||
MatchmadeTeamPayload bySlot = SelectDuplicateRosterBySlot(env, userId);
|
||||
if (bySlot != null)
|
||||
return bySlot;
|
||||
|
||||
if (teamDisambiguation.HasValue)
|
||||
{
|
||||
if (teamDisambiguation.Value == Team.Red)
|
||||
return env.for_red;
|
||||
if (teamDisambiguation.Value == Team.Blue)
|
||||
return env.for_blue;
|
||||
}
|
||||
|
||||
Debug.LogWarning("MatchmadeResponse: both sides list this user but roster is not 1v1 duplicate order; set root your_team on the server or pass team disambiguation.");
|
||||
}
|
||||
|
||||
return env.for_red;
|
||||
}
|
||||
|
||||
/// <summary>Resolve self and opponent from the selected branch (duplicate full roster) or from both sides (split 1v1 rosters).</summary>
|
||||
public static bool TryResolveMyAndOpponent(MatchmadeResponse env, MatchmadeTeamPayload branch, int userId, out MatchmadePlayer myPlayer, out MatchmadePlayer opponentPlayer)
|
||||
{
|
||||
myPlayer = null;
|
||||
opponentPlayer = null;
|
||||
if (env == null || branch == null || branch.Players == null || userId == 0)
|
||||
return false;
|
||||
|
||||
MatchmadeTeamPayload other = ReferenceEquals(branch, env.for_blue) ? env.for_red : env.for_blue;
|
||||
|
||||
if (branch.Players.Length >= 2)
|
||||
{
|
||||
for (int i = 0; i < branch.Players.Length; i++)
|
||||
{
|
||||
if (branch.Players[i].UserId == userId)
|
||||
{
|
||||
myPlayer = branch.Players[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (myPlayer == null)
|
||||
return false;
|
||||
for (int i = 0; i < branch.Players.Length; i++)
|
||||
{
|
||||
if (branch.Players[i].UserId != myPlayer.UserId)
|
||||
{
|
||||
opponentPlayer = branch.Players[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
return opponentPlayer != null;
|
||||
}
|
||||
|
||||
for (int i = 0; i < branch.Players.Length; i++)
|
||||
{
|
||||
if (branch.Players[i].UserId == userId)
|
||||
{
|
||||
myPlayer = branch.Players[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (myPlayer == null && branch.Players.Length == 1 && branch.Players[0].UserId == userId)
|
||||
myPlayer = branch.Players[0];
|
||||
|
||||
if (other?.Players == null)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < other.Players.Length; i++)
|
||||
{
|
||||
if (other.Players[i].UserId != userId)
|
||||
{
|
||||
opponentPlayer = other.Players[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (opponentPlayer == null && other.Players.Length == 1)
|
||||
opponentPlayer = other.Players[0];
|
||||
|
||||
return myPlayer != null && opponentPlayer != null && myPlayer.UserId != opponentPlayer.UserId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
fileFormatVersion: 2
|
||||
guid: 9f3314f8ed9de9c4babeefda34b3026c
|
||||
@@ -1,10 +1,10 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
[Serializable]
|
||||
public class PingReportApiResponse
|
||||
{
|
||||
public bool ok;
|
||||
public string error;
|
||||
public PingReportData report;
|
||||
}
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
[Serializable]
|
||||
public class PingReportApiResponse
|
||||
{
|
||||
public bool ok;
|
||||
public string error;
|
||||
public PingReportData report;
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
fileFormatVersion: 2
|
||||
guid: 76e4e69960816204d8a01d0e534e0bf4
|
||||
@@ -1,13 +1,13 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
[Serializable]
|
||||
public class PingReportData
|
||||
{
|
||||
public int id;
|
||||
public int user_id;
|
||||
public int match_id;
|
||||
public int ping;
|
||||
public string ip_address;
|
||||
public string created_at;
|
||||
}
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
[Serializable]
|
||||
public class PingReportData
|
||||
{
|
||||
public int id;
|
||||
public int user_id;
|
||||
public int match_id;
|
||||
public int ping;
|
||||
public string ip_address;
|
||||
public string created_at;
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
fileFormatVersion: 2
|
||||
guid: b3f6089d3b18cb34482d7de0cd8bca51
|
||||
@@ -1,14 +1,14 @@
|
||||
using System;
|
||||
|
||||
[Serializable]
|
||||
public class PlayerProfileApiResponse
|
||||
{
|
||||
public bool ok;
|
||||
public string error;
|
||||
public int player_id;
|
||||
public string username;
|
||||
public string email;
|
||||
public int days_since_registration;
|
||||
public int matches_played;
|
||||
public float win_ratio;
|
||||
}
|
||||
using System;
|
||||
|
||||
[Serializable]
|
||||
public class PlayerProfileApiResponse
|
||||
{
|
||||
public bool ok;
|
||||
public string error;
|
||||
public int player_id;
|
||||
public string username;
|
||||
public string email;
|
||||
public int days_since_registration;
|
||||
public int matches_played;
|
||||
public float win_ratio;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
[Serializable]
|
||||
public class PurchaseRcApiResponse
|
||||
{
|
||||
public bool ok;
|
||||
public string error;
|
||||
}
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
[Serializable]
|
||||
public class PurchaseRcApiResponse
|
||||
{
|
||||
public bool ok;
|
||||
public string error;
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
fileFormatVersion: 2
|
||||
guid: 4aef938f509352a43920aa7bc5eb593d
|
||||
@@ -1,8 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3dd42b9e0f78cdb4eb43c211cc4097c3
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
fileFormatVersion: 2
|
||||
guid: 3dd42b9e0f78cdb4eb43c211cc4097c3
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class CanvasGroupUtils : MonoBehaviour
|
||||
{
|
||||
public UnityEngine.UI.Graphic[] graphics;
|
||||
public bool useOverrideColor = false;
|
||||
public Color overrideColor = Color.white;
|
||||
|
||||
|
||||
void OnValidate()
|
||||
{
|
||||
RefreshColor();
|
||||
}
|
||||
|
||||
|
||||
void Update()
|
||||
{
|
||||
RefreshColor();
|
||||
}
|
||||
|
||||
|
||||
void RefreshColor(){
|
||||
if(useOverrideColor)
|
||||
{
|
||||
foreach(var graphic in graphics)
|
||||
{
|
||||
graphic.color = new Color(overrideColor.r, overrideColor.g, overrideColor.b, overrideColor.a);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
using UnityEngine;
|
||||
|
||||
public class CanvasGroupUtils : MonoBehaviour
|
||||
{
|
||||
public UnityEngine.UI.Graphic[] graphics;
|
||||
public bool useOverrideColor = false;
|
||||
public Color overrideColor = Color.white;
|
||||
|
||||
|
||||
void OnValidate()
|
||||
{
|
||||
RefreshColor();
|
||||
}
|
||||
|
||||
|
||||
void Update()
|
||||
{
|
||||
RefreshColor();
|
||||
}
|
||||
|
||||
|
||||
void RefreshColor(){
|
||||
if(useOverrideColor)
|
||||
{
|
||||
foreach(var graphic in graphics)
|
||||
{
|
||||
graphic.color = new Color(overrideColor.r, overrideColor.g, overrideColor.b, overrideColor.a);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
fileFormatVersion: 2
|
||||
guid: c6e9b80351cd150478194c9959e87012
|
||||
@@ -1,456 +1,456 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DG.Tweening;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
|
||||
[RequireComponent(typeof(EventTrigger))]
|
||||
public class CardCarousel : MonoBehaviour
|
||||
{
|
||||
[SerializeField] RectTransform[] items;
|
||||
[SerializeField] float spacing = 40f;
|
||||
[SerializeField] float swipeThreshold = 60f;
|
||||
[SerializeField] float snapDuration = 0.25f;
|
||||
[SerializeField] Ease snapEase = Ease.OutCubic;
|
||||
[Tooltip("Horizontal half-width of the center tap zone, in local units of this RectTransform.")]
|
||||
[SerializeField] float centerTapHalfWidth = 260f;
|
||||
[SerializeField] bool looped = true;
|
||||
[SerializeField] int selectedIndex;
|
||||
[Header("Focus Animation")]
|
||||
[SerializeField] float activeScale = 1.05f;
|
||||
[SerializeField] float inactiveScale = 0.88f;
|
||||
[SerializeField] float activePullDown = 35f;
|
||||
|
||||
public UnityEvent<int> onSelected = new UnityEvent<int>();
|
||||
|
||||
EventTrigger eventTrigger;
|
||||
RectTransform rectTransform;
|
||||
RectTransform dragSpace;
|
||||
RectTransform duplicateClone;
|
||||
readonly Dictionary<RectTransform, Vector3> baseScales = new();
|
||||
|
||||
float slotStep;
|
||||
float focusX;
|
||||
float focusY;
|
||||
float focusPosition;
|
||||
float dragStartPosition;
|
||||
float pointerDownLocalX;
|
||||
bool isDragging;
|
||||
bool isAnimating;
|
||||
Tween snapTween;
|
||||
|
||||
public int SelectedIndex => selectedIndex;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
rectTransform = (RectTransform)transform;
|
||||
eventTrigger = GetComponent<EventTrigger>();
|
||||
RegisterTrigger(EventTriggerType.PointerDown, OnPointerDown);
|
||||
RegisterTrigger(EventTriggerType.Drag, OnDrag);
|
||||
RegisterTrigger(EventTriggerType.PointerUp, OnPointerUp);
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
if (items == null || items.Length < 2)
|
||||
{
|
||||
Debug.LogWarning($"{nameof(CardCarousel)} on {name} needs at least 2 items.", this);
|
||||
return;
|
||||
}
|
||||
|
||||
selectedIndex = Mathf.Clamp(selectedIndex, 0, items.Length - 1);
|
||||
dragSpace = items[0].parent as RectTransform;
|
||||
slotStep = items[0].rect.width + spacing;
|
||||
focusX = items[0].anchoredPosition.x;
|
||||
focusY = items[0].anchoredPosition.y;
|
||||
|
||||
duplicateClone = CreateClone(items[0], "_carouselClone_Dup");
|
||||
|
||||
foreach (var item in items)
|
||||
CacheBaseScale(item);
|
||||
|
||||
focusPosition = selectedIndex;
|
||||
ApplyLayout();
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
snapTween?.Kill();
|
||||
}
|
||||
|
||||
void RegisterTrigger(EventTriggerType type, Action<BaseEventData> handler)
|
||||
{
|
||||
var entry = new EventTrigger.Entry { eventID = type };
|
||||
entry.callback.AddListener(handler.Invoke);
|
||||
eventTrigger.triggers.Add(entry);
|
||||
}
|
||||
|
||||
RectTransform CreateClone(RectTransform source, string suffix)
|
||||
{
|
||||
var clone = Instantiate(source, source.parent);
|
||||
clone.name = source.name + suffix;
|
||||
DisableInteractions(clone);
|
||||
return clone;
|
||||
}
|
||||
|
||||
static void DisableInteractions(RectTransform target)
|
||||
{
|
||||
foreach (var graphic in target.GetComponentsInChildren<Graphic>(true))
|
||||
graphic.raycastTarget = false;
|
||||
|
||||
foreach (var button in target.GetComponentsInChildren<Button>(true))
|
||||
button.interactable = false;
|
||||
}
|
||||
|
||||
void OnPointerDown(BaseEventData eventData)
|
||||
{
|
||||
if (!TryGetPointer(eventData, out var pointer) || isAnimating || items == null || items.Length < 2)
|
||||
return;
|
||||
|
||||
snapTween?.Kill();
|
||||
isAnimating = false;
|
||||
isDragging = false;
|
||||
dragStartPosition = focusPosition;
|
||||
pointerDownLocalX = GetLocalDragX(pointer);
|
||||
}
|
||||
|
||||
void OnDrag(BaseEventData eventData)
|
||||
{
|
||||
if (!TryGetPointer(eventData, out var pointer) || items == null || items.Length < 2)
|
||||
return;
|
||||
|
||||
isDragging = true;
|
||||
float delta = GetLocalDragX(pointer) - pointerDownLocalX;
|
||||
focusPosition = dragStartPosition - delta / slotStep;
|
||||
if (!looped)
|
||||
focusPosition = Mathf.Clamp(focusPosition, 0f, items.Length - 1);
|
||||
|
||||
ApplyLayout();
|
||||
}
|
||||
|
||||
void OnPointerUp(BaseEventData eventData)
|
||||
{
|
||||
if (!TryGetPointer(eventData, out var pointer) || isAnimating || items == null || items.Length < 2)
|
||||
return;
|
||||
|
||||
float pointerDelta = GetLocalDragX(pointer) - pointerDownLocalX;
|
||||
float dragDelta = focusPosition - dragStartPosition;
|
||||
bool passedSwipeThreshold = Mathf.Abs(pointerDelta) >= swipeThreshold
|
||||
|| Mathf.Abs(dragDelta * slotStep) >= swipeThreshold;
|
||||
|
||||
if (!passedSwipeThreshold)
|
||||
{
|
||||
if (TryGetTapZone(pointer, out int tapDirection))
|
||||
{
|
||||
if (tapDirection == 0)
|
||||
SelectCurrent();
|
||||
else if (CanMoveBy(tapDirection))
|
||||
AnimateToDirection(tapDirection);
|
||||
else
|
||||
SnapTo(selectedIndex, null);
|
||||
}
|
||||
else
|
||||
SnapTo(selectedIndex, null);
|
||||
|
||||
isDragging = false;
|
||||
return;
|
||||
}
|
||||
|
||||
isDragging = false;
|
||||
|
||||
int direction = dragDelta > 0f ? 1 : -1;
|
||||
if (Mathf.Abs(focusPosition - selectedIndex) > 0.5f)
|
||||
{
|
||||
int nearest = Mathf.RoundToInt(focusPosition);
|
||||
if (!looped)
|
||||
nearest = Mathf.Clamp(nearest, 0, items.Length - 1);
|
||||
if (nearest != selectedIndex && CanMoveBy(nearest > selectedIndex ? 1 : -1))
|
||||
{
|
||||
AnimateTo(nearest);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!CanMoveBy(direction))
|
||||
{
|
||||
SnapTo(selectedIndex, null);
|
||||
return;
|
||||
}
|
||||
|
||||
AnimateToDirection(direction);
|
||||
}
|
||||
|
||||
float GetLocalDragX(PointerEventData pointer)
|
||||
{
|
||||
if (dragSpace == null)
|
||||
return pointer.position.x;
|
||||
|
||||
RectTransformUtility.ScreenPointToLocalPointInRectangle(
|
||||
dragSpace, pointer.position, pointer.pressEventCamera, out var local);
|
||||
return local.x;
|
||||
}
|
||||
|
||||
bool TryGetTapZone(PointerEventData pointer, out int direction)
|
||||
{
|
||||
direction = 0;
|
||||
if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(
|
||||
rectTransform, pointer.position, pointer.pressEventCamera, out var local))
|
||||
return false;
|
||||
|
||||
if (Mathf.Abs(local.x) <= centerTapHalfWidth)
|
||||
return true;
|
||||
|
||||
direction = local.x < 0f ? -1 : 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
void SelectCurrent()
|
||||
{
|
||||
if (Mathf.Approximately(focusPosition, selectedIndex))
|
||||
onSelected.Invoke(selectedIndex);
|
||||
else
|
||||
SnapTo(selectedIndex, () => onSelected.Invoke(selectedIndex));
|
||||
}
|
||||
|
||||
void AnimateToDirection(int direction)
|
||||
{
|
||||
int newIndex = GetAdjacentIndex(direction, items.Length);
|
||||
if (newIndex < 0)
|
||||
return;
|
||||
|
||||
AnimateTo(newIndex);
|
||||
}
|
||||
|
||||
void AnimateTo(int newIndex)
|
||||
{
|
||||
isAnimating = true;
|
||||
|
||||
snapTween?.Kill();
|
||||
snapTween = DOTween.To(
|
||||
() => focusPosition,
|
||||
value =>
|
||||
{
|
||||
focusPosition = value;
|
||||
ApplyLayout();
|
||||
},
|
||||
newIndex,
|
||||
snapDuration)
|
||||
.SetEase(snapEase)
|
||||
.OnComplete(() => CompleteSlide(newIndex));
|
||||
}
|
||||
|
||||
void CompleteSlide(int newIndex)
|
||||
{
|
||||
selectedIndex = newIndex;
|
||||
focusPosition = newIndex;
|
||||
isAnimating = false;
|
||||
ApplyLayout();
|
||||
}
|
||||
|
||||
void SnapTo(float targetPosition, Action onComplete)
|
||||
{
|
||||
isAnimating = true;
|
||||
snapTween?.Kill();
|
||||
snapTween = DOTween.To(
|
||||
() => focusPosition,
|
||||
value =>
|
||||
{
|
||||
focusPosition = value;
|
||||
ApplyLayout();
|
||||
},
|
||||
targetPosition,
|
||||
snapDuration)
|
||||
.SetEase(snapEase)
|
||||
.OnComplete(() =>
|
||||
{
|
||||
isAnimating = false;
|
||||
onComplete?.Invoke();
|
||||
});
|
||||
}
|
||||
|
||||
float GetItemOffset(int itemIndex)
|
||||
{
|
||||
return (itemIndex - focusPosition) * slotStep;
|
||||
}
|
||||
|
||||
bool IsItemVisible(float offset)
|
||||
{
|
||||
return Mathf.Abs(offset) <= slotStep * 1.5f;
|
||||
}
|
||||
|
||||
void ApplyLayout()
|
||||
{
|
||||
int count = items.Length;
|
||||
bool useDuplicate = looped && count == 2;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
float offset = GetItemOffset(i);
|
||||
if (IsItemVisible(offset))
|
||||
{
|
||||
SetItemTransform(items[i], offset);
|
||||
items[i].gameObject.SetActive(true);
|
||||
}
|
||||
else
|
||||
items[i].gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
if (useDuplicate && Mathf.Approximately(focusPosition, Mathf.Round(focusPosition)))
|
||||
{
|
||||
int restIndex = Mathf.RoundToInt(focusPosition);
|
||||
int otherIndex = 1 - restIndex;
|
||||
float otherOffset = GetItemOffset(otherIndex);
|
||||
float duplicateOffset = -otherOffset;
|
||||
|
||||
if (IsItemVisible(duplicateOffset))
|
||||
{
|
||||
SyncDuplicateClone(items[otherIndex]);
|
||||
SetItemTransform(duplicateClone, duplicateOffset);
|
||||
duplicateClone.gameObject.SetActive(true);
|
||||
}
|
||||
else if (duplicateClone != null)
|
||||
duplicateClone.gameObject.SetActive(false);
|
||||
}
|
||||
else if (duplicateClone != null)
|
||||
{
|
||||
duplicateClone.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
int topIndex = 0;
|
||||
float bestFocus = float.MaxValue;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
if (!items[i].gameObject.activeSelf)
|
||||
continue;
|
||||
|
||||
float focus = GetFocusT(GetItemOffset(i));
|
||||
if (focus < bestFocus)
|
||||
{
|
||||
bestFocus = focus;
|
||||
topIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
items[topIndex].SetAsLastSibling();
|
||||
}
|
||||
|
||||
void SyncDuplicateClone(RectTransform source)
|
||||
{
|
||||
string expectedName = source.name + "_carouselClone_Dup";
|
||||
if (duplicateClone == null || duplicateClone.name != expectedName)
|
||||
{
|
||||
var parent = source.parent;
|
||||
int sibling = duplicateClone != null ? duplicateClone.GetSiblingIndex() : source.GetSiblingIndex();
|
||||
if (duplicateClone != null)
|
||||
{
|
||||
baseScales.Remove(duplicateClone);
|
||||
Destroy(duplicateClone.gameObject);
|
||||
}
|
||||
|
||||
duplicateClone = CreateClone(source, "_carouselClone_Dup");
|
||||
duplicateClone.SetParent(parent, false);
|
||||
duplicateClone.SetSiblingIndex(sibling);
|
||||
}
|
||||
|
||||
CacheBaseScale(duplicateClone, source);
|
||||
}
|
||||
|
||||
void CacheBaseScale(RectTransform item, RectTransform source = null)
|
||||
{
|
||||
if (baseScales.ContainsKey(item))
|
||||
return;
|
||||
|
||||
baseScales[item] = source != null ? source.localScale : item.localScale;
|
||||
}
|
||||
|
||||
void SetItemTransform(RectTransform item, float xOffset)
|
||||
{
|
||||
CacheBaseScale(item);
|
||||
|
||||
float focusT = GetFocusT(xOffset);
|
||||
float scale = Mathf.Lerp(activeScale, inactiveScale, focusT);
|
||||
item.localScale = baseScales[item] * scale;
|
||||
|
||||
var pos = item.anchoredPosition;
|
||||
pos.x = focusX + xOffset;
|
||||
pos.y = focusY - Mathf.Lerp(activePullDown, 0f, focusT);
|
||||
item.anchoredPosition = pos;
|
||||
}
|
||||
|
||||
float GetFocusT(float xOffset)
|
||||
{
|
||||
if (slotStep <= 0f)
|
||||
return 0f;
|
||||
|
||||
float t = Mathf.Clamp01(Mathf.Abs(xOffset) / slotStep);
|
||||
return t * t * (3f - 2f * t);
|
||||
}
|
||||
|
||||
static int WrapIndex(int index, int count)
|
||||
{
|
||||
if (count <= 0)
|
||||
return 0;
|
||||
return ((index % count) + count) % count;
|
||||
}
|
||||
|
||||
int GetAdjacentIndex(int direction, int count)
|
||||
{
|
||||
int index = selectedIndex + direction;
|
||||
if (looped)
|
||||
return WrapIndex(index, count);
|
||||
return index >= 0 && index < count ? index : -1;
|
||||
}
|
||||
|
||||
bool CanMoveBy(int direction)
|
||||
{
|
||||
if (items == null || items.Length == 0)
|
||||
return false;
|
||||
return GetAdjacentIndex(direction, items.Length) >= 0;
|
||||
}
|
||||
|
||||
int NormalizeIndex(int index)
|
||||
{
|
||||
if (items == null || items.Length == 0)
|
||||
return 0;
|
||||
return looped ? WrapIndex(index, items.Length) : Mathf.Clamp(index, 0, items.Length - 1);
|
||||
}
|
||||
|
||||
static bool TryGetPointer(BaseEventData eventData, out PointerEventData pointer)
|
||||
{
|
||||
pointer = eventData as PointerEventData;
|
||||
return pointer != null;
|
||||
}
|
||||
|
||||
public void SetSelectedIndex(int index, bool animate = false)
|
||||
{
|
||||
if (items == null || items.Length == 0)
|
||||
return;
|
||||
|
||||
int target = NormalizeIndex(index);
|
||||
if (target == selectedIndex)
|
||||
return;
|
||||
|
||||
if (!animate || !isActiveAndEnabled)
|
||||
{
|
||||
snapTween?.Kill();
|
||||
isAnimating = false;
|
||||
selectedIndex = target;
|
||||
focusPosition = target;
|
||||
ApplyLayout();
|
||||
return;
|
||||
}
|
||||
|
||||
int direction = target > selectedIndex ? 1 : -1;
|
||||
if (!CanMoveBy(direction) || Mathf.Abs(target - selectedIndex) != 1)
|
||||
{
|
||||
selectedIndex = target;
|
||||
focusPosition = target;
|
||||
ApplyLayout();
|
||||
return;
|
||||
}
|
||||
|
||||
AnimateToDirection(direction);
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DG.Tweening;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
|
||||
[RequireComponent(typeof(EventTrigger))]
|
||||
public class CardCarousel : MonoBehaviour
|
||||
{
|
||||
[SerializeField] RectTransform[] items;
|
||||
[SerializeField] float spacing = 40f;
|
||||
[SerializeField] float swipeThreshold = 60f;
|
||||
[SerializeField] float snapDuration = 0.25f;
|
||||
[SerializeField] Ease snapEase = Ease.OutCubic;
|
||||
[Tooltip("Horizontal half-width of the center tap zone, in local units of this RectTransform.")]
|
||||
[SerializeField] float centerTapHalfWidth = 260f;
|
||||
[SerializeField] bool looped = true;
|
||||
[SerializeField] int selectedIndex;
|
||||
[Header("Focus Animation")]
|
||||
[SerializeField] float activeScale = 1.05f;
|
||||
[SerializeField] float inactiveScale = 0.88f;
|
||||
[SerializeField] float activePullDown = 35f;
|
||||
|
||||
public UnityEvent<int> onSelected = new UnityEvent<int>();
|
||||
|
||||
EventTrigger eventTrigger;
|
||||
RectTransform rectTransform;
|
||||
RectTransform dragSpace;
|
||||
RectTransform duplicateClone;
|
||||
readonly Dictionary<RectTransform, Vector3> baseScales = new();
|
||||
|
||||
float slotStep;
|
||||
float focusX;
|
||||
float focusY;
|
||||
float focusPosition;
|
||||
float dragStartPosition;
|
||||
float pointerDownLocalX;
|
||||
bool isDragging;
|
||||
bool isAnimating;
|
||||
Tween snapTween;
|
||||
|
||||
public int SelectedIndex => selectedIndex;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
rectTransform = (RectTransform)transform;
|
||||
eventTrigger = GetComponent<EventTrigger>();
|
||||
RegisterTrigger(EventTriggerType.PointerDown, OnPointerDown);
|
||||
RegisterTrigger(EventTriggerType.Drag, OnDrag);
|
||||
RegisterTrigger(EventTriggerType.PointerUp, OnPointerUp);
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
if (items == null || items.Length < 2)
|
||||
{
|
||||
Debug.LogWarning($"{nameof(CardCarousel)} on {name} needs at least 2 items.", this);
|
||||
return;
|
||||
}
|
||||
|
||||
selectedIndex = Mathf.Clamp(selectedIndex, 0, items.Length - 1);
|
||||
dragSpace = items[0].parent as RectTransform;
|
||||
slotStep = items[0].rect.width + spacing;
|
||||
focusX = items[0].anchoredPosition.x;
|
||||
focusY = items[0].anchoredPosition.y;
|
||||
|
||||
duplicateClone = CreateClone(items[0], "_carouselClone_Dup");
|
||||
|
||||
foreach (var item in items)
|
||||
CacheBaseScale(item);
|
||||
|
||||
focusPosition = selectedIndex;
|
||||
ApplyLayout();
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
snapTween?.Kill();
|
||||
}
|
||||
|
||||
void RegisterTrigger(EventTriggerType type, Action<BaseEventData> handler)
|
||||
{
|
||||
var entry = new EventTrigger.Entry { eventID = type };
|
||||
entry.callback.AddListener(handler.Invoke);
|
||||
eventTrigger.triggers.Add(entry);
|
||||
}
|
||||
|
||||
RectTransform CreateClone(RectTransform source, string suffix)
|
||||
{
|
||||
var clone = Instantiate(source, source.parent);
|
||||
clone.name = source.name + suffix;
|
||||
DisableInteractions(clone);
|
||||
return clone;
|
||||
}
|
||||
|
||||
static void DisableInteractions(RectTransform target)
|
||||
{
|
||||
foreach (var graphic in target.GetComponentsInChildren<Graphic>(true))
|
||||
graphic.raycastTarget = false;
|
||||
|
||||
foreach (var button in target.GetComponentsInChildren<Button>(true))
|
||||
button.interactable = false;
|
||||
}
|
||||
|
||||
void OnPointerDown(BaseEventData eventData)
|
||||
{
|
||||
if (!TryGetPointer(eventData, out var pointer) || isAnimating || items == null || items.Length < 2)
|
||||
return;
|
||||
|
||||
snapTween?.Kill();
|
||||
isAnimating = false;
|
||||
isDragging = false;
|
||||
dragStartPosition = focusPosition;
|
||||
pointerDownLocalX = GetLocalDragX(pointer);
|
||||
}
|
||||
|
||||
void OnDrag(BaseEventData eventData)
|
||||
{
|
||||
if (!TryGetPointer(eventData, out var pointer) || items == null || items.Length < 2)
|
||||
return;
|
||||
|
||||
isDragging = true;
|
||||
float delta = GetLocalDragX(pointer) - pointerDownLocalX;
|
||||
focusPosition = dragStartPosition - delta / slotStep;
|
||||
if (!looped)
|
||||
focusPosition = Mathf.Clamp(focusPosition, 0f, items.Length - 1);
|
||||
|
||||
ApplyLayout();
|
||||
}
|
||||
|
||||
void OnPointerUp(BaseEventData eventData)
|
||||
{
|
||||
if (!TryGetPointer(eventData, out var pointer) || isAnimating || items == null || items.Length < 2)
|
||||
return;
|
||||
|
||||
float pointerDelta = GetLocalDragX(pointer) - pointerDownLocalX;
|
||||
float dragDelta = focusPosition - dragStartPosition;
|
||||
bool passedSwipeThreshold = Mathf.Abs(pointerDelta) >= swipeThreshold
|
||||
|| Mathf.Abs(dragDelta * slotStep) >= swipeThreshold;
|
||||
|
||||
if (!passedSwipeThreshold)
|
||||
{
|
||||
if (TryGetTapZone(pointer, out int tapDirection))
|
||||
{
|
||||
if (tapDirection == 0)
|
||||
SelectCurrent();
|
||||
else if (CanMoveBy(tapDirection))
|
||||
AnimateToDirection(tapDirection);
|
||||
else
|
||||
SnapTo(selectedIndex, null);
|
||||
}
|
||||
else
|
||||
SnapTo(selectedIndex, null);
|
||||
|
||||
isDragging = false;
|
||||
return;
|
||||
}
|
||||
|
||||
isDragging = false;
|
||||
|
||||
int direction = dragDelta > 0f ? 1 : -1;
|
||||
if (Mathf.Abs(focusPosition - selectedIndex) > 0.5f)
|
||||
{
|
||||
int nearest = Mathf.RoundToInt(focusPosition);
|
||||
if (!looped)
|
||||
nearest = Mathf.Clamp(nearest, 0, items.Length - 1);
|
||||
if (nearest != selectedIndex && CanMoveBy(nearest > selectedIndex ? 1 : -1))
|
||||
{
|
||||
AnimateTo(nearest);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!CanMoveBy(direction))
|
||||
{
|
||||
SnapTo(selectedIndex, null);
|
||||
return;
|
||||
}
|
||||
|
||||
AnimateToDirection(direction);
|
||||
}
|
||||
|
||||
float GetLocalDragX(PointerEventData pointer)
|
||||
{
|
||||
if (dragSpace == null)
|
||||
return pointer.position.x;
|
||||
|
||||
RectTransformUtility.ScreenPointToLocalPointInRectangle(
|
||||
dragSpace, pointer.position, pointer.pressEventCamera, out var local);
|
||||
return local.x;
|
||||
}
|
||||
|
||||
bool TryGetTapZone(PointerEventData pointer, out int direction)
|
||||
{
|
||||
direction = 0;
|
||||
if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(
|
||||
rectTransform, pointer.position, pointer.pressEventCamera, out var local))
|
||||
return false;
|
||||
|
||||
if (Mathf.Abs(local.x) <= centerTapHalfWidth)
|
||||
return true;
|
||||
|
||||
direction = local.x < 0f ? -1 : 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
void SelectCurrent()
|
||||
{
|
||||
if (Mathf.Approximately(focusPosition, selectedIndex))
|
||||
onSelected.Invoke(selectedIndex);
|
||||
else
|
||||
SnapTo(selectedIndex, () => onSelected.Invoke(selectedIndex));
|
||||
}
|
||||
|
||||
void AnimateToDirection(int direction)
|
||||
{
|
||||
int newIndex = GetAdjacentIndex(direction, items.Length);
|
||||
if (newIndex < 0)
|
||||
return;
|
||||
|
||||
AnimateTo(newIndex);
|
||||
}
|
||||
|
||||
void AnimateTo(int newIndex)
|
||||
{
|
||||
isAnimating = true;
|
||||
|
||||
snapTween?.Kill();
|
||||
snapTween = DOTween.To(
|
||||
() => focusPosition,
|
||||
value =>
|
||||
{
|
||||
focusPosition = value;
|
||||
ApplyLayout();
|
||||
},
|
||||
newIndex,
|
||||
snapDuration)
|
||||
.SetEase(snapEase)
|
||||
.OnComplete(() => CompleteSlide(newIndex));
|
||||
}
|
||||
|
||||
void CompleteSlide(int newIndex)
|
||||
{
|
||||
selectedIndex = newIndex;
|
||||
focusPosition = newIndex;
|
||||
isAnimating = false;
|
||||
ApplyLayout();
|
||||
}
|
||||
|
||||
void SnapTo(float targetPosition, Action onComplete)
|
||||
{
|
||||
isAnimating = true;
|
||||
snapTween?.Kill();
|
||||
snapTween = DOTween.To(
|
||||
() => focusPosition,
|
||||
value =>
|
||||
{
|
||||
focusPosition = value;
|
||||
ApplyLayout();
|
||||
},
|
||||
targetPosition,
|
||||
snapDuration)
|
||||
.SetEase(snapEase)
|
||||
.OnComplete(() =>
|
||||
{
|
||||
isAnimating = false;
|
||||
onComplete?.Invoke();
|
||||
});
|
||||
}
|
||||
|
||||
float GetItemOffset(int itemIndex)
|
||||
{
|
||||
return (itemIndex - focusPosition) * slotStep;
|
||||
}
|
||||
|
||||
bool IsItemVisible(float offset)
|
||||
{
|
||||
return Mathf.Abs(offset) <= slotStep * 1.5f;
|
||||
}
|
||||
|
||||
void ApplyLayout()
|
||||
{
|
||||
int count = items.Length;
|
||||
bool useDuplicate = looped && count == 2;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
float offset = GetItemOffset(i);
|
||||
if (IsItemVisible(offset))
|
||||
{
|
||||
SetItemTransform(items[i], offset);
|
||||
items[i].gameObject.SetActive(true);
|
||||
}
|
||||
else
|
||||
items[i].gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
if (useDuplicate && Mathf.Approximately(focusPosition, Mathf.Round(focusPosition)))
|
||||
{
|
||||
int restIndex = Mathf.RoundToInt(focusPosition);
|
||||
int otherIndex = 1 - restIndex;
|
||||
float otherOffset = GetItemOffset(otherIndex);
|
||||
float duplicateOffset = -otherOffset;
|
||||
|
||||
if (IsItemVisible(duplicateOffset))
|
||||
{
|
||||
SyncDuplicateClone(items[otherIndex]);
|
||||
SetItemTransform(duplicateClone, duplicateOffset);
|
||||
duplicateClone.gameObject.SetActive(true);
|
||||
}
|
||||
else if (duplicateClone != null)
|
||||
duplicateClone.gameObject.SetActive(false);
|
||||
}
|
||||
else if (duplicateClone != null)
|
||||
{
|
||||
duplicateClone.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
int topIndex = 0;
|
||||
float bestFocus = float.MaxValue;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
if (!items[i].gameObject.activeSelf)
|
||||
continue;
|
||||
|
||||
float focus = GetFocusT(GetItemOffset(i));
|
||||
if (focus < bestFocus)
|
||||
{
|
||||
bestFocus = focus;
|
||||
topIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
items[topIndex].SetAsLastSibling();
|
||||
}
|
||||
|
||||
void SyncDuplicateClone(RectTransform source)
|
||||
{
|
||||
string expectedName = source.name + "_carouselClone_Dup";
|
||||
if (duplicateClone == null || duplicateClone.name != expectedName)
|
||||
{
|
||||
var parent = source.parent;
|
||||
int sibling = duplicateClone != null ? duplicateClone.GetSiblingIndex() : source.GetSiblingIndex();
|
||||
if (duplicateClone != null)
|
||||
{
|
||||
baseScales.Remove(duplicateClone);
|
||||
Destroy(duplicateClone.gameObject);
|
||||
}
|
||||
|
||||
duplicateClone = CreateClone(source, "_carouselClone_Dup");
|
||||
duplicateClone.SetParent(parent, false);
|
||||
duplicateClone.SetSiblingIndex(sibling);
|
||||
}
|
||||
|
||||
CacheBaseScale(duplicateClone, source);
|
||||
}
|
||||
|
||||
void CacheBaseScale(RectTransform item, RectTransform source = null)
|
||||
{
|
||||
if (baseScales.ContainsKey(item))
|
||||
return;
|
||||
|
||||
baseScales[item] = source != null ? source.localScale : item.localScale;
|
||||
}
|
||||
|
||||
void SetItemTransform(RectTransform item, float xOffset)
|
||||
{
|
||||
CacheBaseScale(item);
|
||||
|
||||
float focusT = GetFocusT(xOffset);
|
||||
float scale = Mathf.Lerp(activeScale, inactiveScale, focusT);
|
||||
item.localScale = baseScales[item] * scale;
|
||||
|
||||
var pos = item.anchoredPosition;
|
||||
pos.x = focusX + xOffset;
|
||||
pos.y = focusY - Mathf.Lerp(activePullDown, 0f, focusT);
|
||||
item.anchoredPosition = pos;
|
||||
}
|
||||
|
||||
float GetFocusT(float xOffset)
|
||||
{
|
||||
if (slotStep <= 0f)
|
||||
return 0f;
|
||||
|
||||
float t = Mathf.Clamp01(Mathf.Abs(xOffset) / slotStep);
|
||||
return t * t * (3f - 2f * t);
|
||||
}
|
||||
|
||||
static int WrapIndex(int index, int count)
|
||||
{
|
||||
if (count <= 0)
|
||||
return 0;
|
||||
return ((index % count) + count) % count;
|
||||
}
|
||||
|
||||
int GetAdjacentIndex(int direction, int count)
|
||||
{
|
||||
int index = selectedIndex + direction;
|
||||
if (looped)
|
||||
return WrapIndex(index, count);
|
||||
return index >= 0 && index < count ? index : -1;
|
||||
}
|
||||
|
||||
bool CanMoveBy(int direction)
|
||||
{
|
||||
if (items == null || items.Length == 0)
|
||||
return false;
|
||||
return GetAdjacentIndex(direction, items.Length) >= 0;
|
||||
}
|
||||
|
||||
int NormalizeIndex(int index)
|
||||
{
|
||||
if (items == null || items.Length == 0)
|
||||
return 0;
|
||||
return looped ? WrapIndex(index, items.Length) : Mathf.Clamp(index, 0, items.Length - 1);
|
||||
}
|
||||
|
||||
static bool TryGetPointer(BaseEventData eventData, out PointerEventData pointer)
|
||||
{
|
||||
pointer = eventData as PointerEventData;
|
||||
return pointer != null;
|
||||
}
|
||||
|
||||
public void SetSelectedIndex(int index, bool animate = false)
|
||||
{
|
||||
if (items == null || items.Length == 0)
|
||||
return;
|
||||
|
||||
int target = NormalizeIndex(index);
|
||||
if (target == selectedIndex)
|
||||
return;
|
||||
|
||||
if (!animate || !isActiveAndEnabled)
|
||||
{
|
||||
snapTween?.Kill();
|
||||
isAnimating = false;
|
||||
selectedIndex = target;
|
||||
focusPosition = target;
|
||||
ApplyLayout();
|
||||
return;
|
||||
}
|
||||
|
||||
int direction = target > selectedIndex ? 1 : -1;
|
||||
if (!CanMoveBy(direction) || Mathf.Abs(target - selectedIndex) != 1)
|
||||
{
|
||||
selectedIndex = target;
|
||||
focusPosition = target;
|
||||
ApplyLayout();
|
||||
return;
|
||||
}
|
||||
|
||||
AnimateToDirection(direction);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
fileFormatVersion: 2
|
||||
guid: a4951ec6b41c5d14cb8f5a8803dfa0c2
|
||||
@@ -1,20 +1,20 @@
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
[RequireComponent(typeof(TMP_Text))]
|
||||
public class CopyTextTMP : MonoBehaviour
|
||||
{
|
||||
public TMP_Text origin;
|
||||
|
||||
TMP_Text _text;
|
||||
void Awake()
|
||||
{
|
||||
_text = GetComponent<TMP_Text>();
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
_text.text = origin.text;
|
||||
}
|
||||
|
||||
}
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
[RequireComponent(typeof(TMP_Text))]
|
||||
public class CopyTextTMP : MonoBehaviour
|
||||
{
|
||||
public TMP_Text origin;
|
||||
|
||||
TMP_Text _text;
|
||||
void Awake()
|
||||
{
|
||||
_text = GetComponent<TMP_Text>();
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
_text.text = origin.text;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user