Keep local version of files

This commit is contained in:
2026-10-08 00:49:10 +05:30
parent 7fa0621ff8
commit 053665c690
7343 changed files with 5470578 additions and 5470528 deletions
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,2 +1,2 @@
fileFormatVersion: 2
fileFormatVersion: 2
guid: c886581a7bac8f34ba8f7995eb48db02
+203 -203
View File
@@ -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 -1
View File
@@ -1,2 +1,2 @@
fileFormatVersion: 2
fileFormatVersion: 2
guid: f6fe03c800a746a4486ffa0add89e74c
+58 -58
View File
@@ -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 -1
View File
@@ -1,2 +1,2 @@
fileFormatVersion: 2
fileFormatVersion: 2
guid: 0c056bc55f8e80b408b5d45af7e7ac02
+57 -57
View File
@@ -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 -1
View File
@@ -1,2 +1,2 @@
fileFormatVersion: 2
fileFormatVersion: 2
guid: fc8c3535b2249f247abdb4f3964eca7e
+93 -93
View File
@@ -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 -1
View File
@@ -1,2 +1,2 @@
fileFormatVersion: 2
fileFormatVersion: 2
guid: 42df6f977bdfdf54092d0442ed988ef0
+380 -380
View File
@@ -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 -1
View File
@@ -1,2 +1,2 @@
fileFormatVersion: 2
fileFormatVersion: 2
guid: 09dd2727c9588024781c4d5160f19c4d
+5 -5
View File
@@ -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 -1
View File
@@ -1,2 +1,2 @@
fileFormatVersion: 2
fileFormatVersion: 2
guid: 0e7ff01514aafb34d805be0d1662de29
+1836 -1836
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,2 +1,2 @@
fileFormatVersion: 2
fileFormatVersion: 2
guid: 12befc4868932ca479d93e14120ffd0e
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,2 +1,2 @@
fileFormatVersion: 2
fileFormatVersion: 2
guid: 34fb5f6c8224c94438e92e0f8f5c8471
+372 -372
View File
@@ -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 -1
View File
@@ -1,2 +1,2 @@
fileFormatVersion: 2
fileFormatVersion: 2
guid: 304204825fc486545a7a9bd7dd232861
+57 -57
View File
@@ -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 -1
View File
@@ -1,2 +1,2 @@
fileFormatVersion: 2
fileFormatVersion: 2
guid: f5ca0ff31c184b04da729df1fae08a5e
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,2 +1,2 @@
fileFormatVersion: 2
fileFormatVersion: 2
guid: bdd03d8cc6ae1ad4d89c39ba1d752da7
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,2 +1,2 @@
fileFormatVersion: 2
fileFormatVersion: 2
guid: 7d216f08d4b95494ba7875cca68b440a
+57 -57
View File
@@ -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 -1
View File
@@ -1,2 +1,2 @@
fileFormatVersion: 2
fileFormatVersion: 2
guid: d17e1ac28f957254795443173835b577
+346 -346
View File
@@ -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 -1
View File
@@ -1,2 +1,2 @@
fileFormatVersion: 2
fileFormatVersion: 2
guid: c4e95ceb8bcd79f488de2cb8a5d0ca2d
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,2 +1,2 @@
fileFormatVersion: 2
fileFormatVersion: 2
guid: 63314f7b4865af348a747f86b6aab5a6
+62 -62
View File
@@ -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));
}
}
+238 -238
View File
@@ -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";
}
}
+155 -155
View File
@@ -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
+31 -31
View File
@@ -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);
}
}
+56 -56
View File
@@ -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
View File
@@ -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 -1
View File
@@ -1,2 +1,2 @@
fileFormatVersion: 2
fileFormatVersion: 2
guid: 4b7edf42d59a3c942b1f2f66b31aaa75
+8 -8
View File
@@ -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:
+291 -291
View File
@@ -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
+7 -7
View File
@@ -1,7 +1,7 @@
fileFormatVersion: 2
guid: 8cced32e9b154814b99db72a5ae58c70
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
fileFormatVersion: 2
guid: 8cced32e9b154814b99db72a5ae58c70
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+8 -8
View File
@@ -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:
+61 -61
View File
@@ -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;
}
+11 -11
View File
@@ -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:
+301 -301
View File
@@ -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;
}
}
}
+11 -11
View File
@@ -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:
+12 -12
View File
@@ -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 -1
View File
@@ -1,2 +1,2 @@
fileFormatVersion: 2
fileFormatVersion: 2
guid: c919971d56a5c33418c7a76a4744b63c
+66 -66
View File
@@ -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 -1
View File
@@ -1,2 +1,2 @@
fileFormatVersion: 2
fileFormatVersion: 2
guid: c10a35ed198fa2941b2b28a03c558897
+25 -25
View File
@@ -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 -1
View File
@@ -1,2 +1,2 @@
fileFormatVersion: 2
fileFormatVersion: 2
guid: a49b5cb62dd2545408364e077bdf3a0a
+25 -25
View File
@@ -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 -1
View File
@@ -1,2 +1,2 @@
fileFormatVersion: 2
fileFormatVersion: 2
guid: 370d8135f2d97da4194a41598ce8c4a9
+148 -148
View File
@@ -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
+8 -8
View File
@@ -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:
+8 -8
View File
@@ -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:
+39 -39
View File
@@ -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
+21 -21
View File
@@ -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 -1
View File
@@ -1,2 +1,2 @@
fileFormatVersion: 2
fileFormatVersion: 2
guid: 5bf3c59f7c1a7e640a05edc5f9828abd
+19 -19
View File
@@ -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;
}
}
+229 -229
View File
@@ -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();
}
}
+10 -10
View File
@@ -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);
}
}
+12 -12
View File
@@ -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 -1
View File
@@ -1,2 +1,2 @@
fileFormatVersion: 2
fileFormatVersion: 2
guid: f67c475e52308cd42a56f95fd38c86c4
+8 -8
View File
@@ -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:
+236 -236
View File
@@ -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
+30 -30
View File
@@ -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
+8 -8
View File
@@ -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:
+12 -12
View File
@@ -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 -1
View File
@@ -1,2 +1,2 @@
fileFormatVersion: 2
fileFormatVersion: 2
guid: 241f412d7db597748af9c1138e640e0e
+14 -14
View File
@@ -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 -1
View File
@@ -1,2 +1,2 @@
fileFormatVersion: 2
fileFormatVersion: 2
guid: 7fa72c32d6a0d1e4abc0b724b1988d8c
+8 -8
View File
@@ -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
+8 -8
View File
@@ -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:
+31 -31
View File
@@ -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
+456 -456
View File
@@ -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 -1
View File
@@ -1,2 +1,2 @@
fileFormatVersion: 2
fileFormatVersion: 2
guid: a4951ec6b41c5d14cb8f5a8803dfa0c2
+20 -20
View File
@@ -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