TheStable/Assets/MyroP/Arcade/Melon/Code/MelonGameLoop.cs
Jamie Greunbaum 542e04afed - Replaced one skee-ball machine with two arcade machines.
- Added Black Horse And The Cherry Tree by KT Tunstall to the horse playlist.
- Added Buckin' My Horse by Sir Mix-A-Lot to the horse playlist.
2026-02-11 02:58:41 -05:00

439 lines
9.8 KiB
C#

using TMPro;
using UdonSharp;
using UnityEngine;
using UnityEngine.UI;
using VRC.SDK3.Data;
using VRC.SDKBase;
using VRC.Udon;
namespace myro.arcade
{
[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]
public class MelonGameLoop : UdonSharpBehaviour
{
public Transform LeftWall;
public Transform RightWall;
public Transform Root;
public Joystick JoystickInstance;
public Transform DropTransform;
public Transform DeathZone;
public GameObject FruitPrefab;
public GameObject GameOverMessage;
public MelonGameSettings MelonGameSettingsInstance;
public TextMeshProUGUI Score;
public MeshRenderer NextRankImage;
public AudioSource Music;
public AudioSource AudioPlayer;
public AudioSource ComboPlayer;
public AudioClip Drop;
public AudioClip Collision;
public AudioClip Combo;
private Fruit _currentFruit;
private DataList _instantiatedFruits;
private const float SYNCING_RATE = 0.2f;
private int _combo;
[UdonSynced]
private bool _joystickGrabbed;
[UdonSynced]
private GameState _gameState;
[UdonSynced]
private int _score;
[UdonSynced]
private short _nextRank;
[UdonSynced]
private Vector3[] _positionFruits;
[UdonSynced]
private Quaternion[] _rotationFruits;
[UdonSynced]
private short[] _rankFruits;
void Start()
{
_gameState = GameState.FINISH;
GameOver();
}
private short UpdateNextFruitViewAndReturnCurrentRank()
{
short newRank = _nextRank;
_nextRank = (short)Random.Range(0, 5);
UpdateNextRankImage();
return newRank;
}
private Vector3 GetCursorPosition()
{
float ratioX = JoystickInstance.GetPositionXRatio();
float radiusFruit = 0;
if (_currentFruit)
radiusFruit = _currentFruit.transform.localScale.x / 2.0f + 0.01f; //We add a little threshold at the end
Vector3 left = new Vector3(LeftWall.localPosition.x + radiusFruit, LeftWall.localPosition.y, LeftWall.localPosition.z);
Vector3 right = new Vector3(RightWall.localPosition.x - radiusFruit, RightWall.localPosition.y, RightWall.localPosition.z);
return Vector3.Lerp(left, right, ratioX);
}
public GameState GetGameState()
{
return _gameState;
}
public void NewFruit()
{
short rank = UpdateNextFruitViewAndReturnCurrentRank();
_currentFruit = InstantiateNewFruitAt(rank, false);
_currentFruit.transform.localPosition = GetCursorPosition();
_currentFruit.enabled = false;
}
public void SetTextureOffset(Material mat, int rank)
{
mat.SetTextureOffset("_MainTex", new Vector2((rank % 4) / 4.0f, (rank / 4) / 4.0f));
}
private void StartGame()
{
if (_instantiatedFruits != null)
{
for (int i = 0; i < _instantiatedFruits.Count; i++)
{
Fruit fruit = (Fruit)_instantiatedFruits[i].Reference;
if (fruit)
Destroy(fruit.gameObject);
}
}
_instantiatedFruits = new DataList();
_score = 0;
_gameState = GameState.PLAY;
NewFruit();
SyncingLoop();
UpdateUIState();
Music.Play();
}
public void AddToScore(int points)
{
_score += points;
UpdateScore();
}
#region Events and syncing
/// <summary>
/// This looping method should only be called by the owner, it syncs the data
/// </summary>
public void SyncingLoop()
{
if (_gameState == GameState.FINISH || _instantiatedFruits == null || !Networking.IsOwner(gameObject))
return;
int length = _instantiatedFruits.Count;
_positionFruits = new Vector3[length];
_rotationFruits = new Quaternion[length];
_rankFruits = new short[length];
for (int i = 0; i < length; i++)
{
Fruit fruit = (Fruit)_instantiatedFruits[i].Reference;
if (!fruit)
return;
_positionFruits[i] = fruit.transform.localPosition;
_rotationFruits[i] = fruit.transform.localRotation;
_rankFruits[i] = fruit.GetRank();
}
RequestSerialization();
SendCustomEventDelayedSeconds(nameof(SyncingLoop), SYNCING_RATE);
}
/// <summary>
/// This event is called from the Joystick script
/// </summary>
public void PlayerPickedUpJoystick()
{
if (!Networking.IsOwner(gameObject))
{
Networking.SetOwner(Networking.LocalPlayer, gameObject);
StartGame();
}
_joystickGrabbed = true;
RequestSerialization();
/*WaitMessage.SetActive(!Networking.IsOwner(gameObject) && _gameState == GameState.PLAY);
if (_gameState == GameState.FINISH)
{
Networking.SetOwner(Networking.LocalPlayer, gameObject);
}*/
}
/// <summary>
/// This event is called from the Joystick script
/// </summary>
public void PlayerDroppedJoystick()
{
_joystickGrabbed = false;
RequestSerialization();
}
bool _isPlayerInArea;
public void OnPlayerEnteredArea()
{
_isPlayerInArea = true;
}
public void OnPlayerExitedArea()
{
_isPlayerInArea = false;
}
public override void OnOwnershipTransferred(VRCPlayerApi player)
{
if (!player.isLocal)
{
_currentFruit = null;
}
}
public void OnPress()
{
if (!Networking.IsOwner(gameObject))
return;
if (_gameState == GameState.PLAY)
{
if (_currentFruit != null)
{
_currentFruit.enabled = true;
_currentFruit.DropFruit();
_currentFruit = null;
_combo = 0;
SendCustomNetworkEvent(VRC.Udon.Common.Interfaces.NetworkEventTarget.All, nameof(PlayDropAudio));
}
}
else
{
StartGame();
}
}
public void OnResetPressed()
{
if (_joystickGrabbed)
return;
if (!Networking.IsOwner(gameObject))
Networking.SetOwner(Networking.LocalPlayer, gameObject);
StartGame();
}
// Here, OnDeserialization should only be called for the remote player
public override void OnDeserialization()
{
if (!_isPlayerInArea)
return;
if (_gameState == GameState.PLAY && !Music.isPlaying)
Music.Play();
else if (_gameState == GameState.FINISH && Music.isPlaying)
Music.Stop();
UpdateUI();
if (_instantiatedFruits == null)
{
_instantiatedFruits = new DataList();
}
int numberOfSyncedFruits = _positionFruits.Length;
//first for-loop to check if all references are valid, this is to prevent possible errors
for (int i = _instantiatedFruits.Count - 1; i >= 0; i--)
{
Fruit fruit = (Fruit)_instantiatedFruits[i].Reference;
if (!Utilities.IsValid(fruit))
{
_instantiatedFruits.RemoveAt(i);
}
}
if (_instantiatedFruits.Count < numberOfSyncedFruits)
{
//Here we need to instantiate more fruits
for (int i = 0; i < numberOfSyncedFruits - _instantiatedFruits.Count; i++)
{
InstantiateNewFruitAt(0, false); //the settings will be set later
}
}
else if (_instantiatedFruits.Count > numberOfSyncedFruits)
{
//Here we need to destroy a few fruits
for (int i = 0; i < _instantiatedFruits.Count - numberOfSyncedFruits; i++)
{
Fruit fruit = (Fruit)_instantiatedFruits[0].Reference;
Destroy(fruit.gameObject);
_instantiatedFruits.RemoveAt(0);
}
}
for (int i = 0; i < numberOfSyncedFruits; i++)
{
if (_instantiatedFruits[i].Error == DataError.None)
{
//Now we update each fruit
Fruit fruit = (Fruit)_instantiatedFruits[i].Reference;
fruit.transform.localPosition = _positionFruits[i];
fruit.transform.localRotation = _rotationFruits[i];
fruit.SetRank(_rankFruits[i]);
fruit.enabled = false; //the scripts does not need to be enabled for the remote player
}
}
}
#endregion
#region UI
private void UpdateNextRankImage()
{
SetTextureOffset(NextRankImage.material, _nextRank);
float scale = (_nextRank + 1) + 1.0f;
NextRankImage.transform.localScale = new Vector3(scale, scale, scale);
}
private void UpdateScore()
{
Score.text = _score.ToString();
}
private void UpdateUIState()
{
GameOverMessage.SetActive(_gameState == GameState.FINISH);
}
private void UpdateUI()
{
UpdateScore();
UpdateUIState();
UpdateNextRankImage();
}
#endregion
public void GameOver()
{
Music.Stop();
if (_currentFruit != null)
{
Destroy(_currentFruit.gameObject);
_currentFruit = null;
}
_gameState = GameState.FINISH;
UpdateUI();
//pausing all physics
if (_instantiatedFruits != null)
{
for (int i = 0; i < _instantiatedFruits.Count; i++)
{
Fruit fruit = (Fruit)_instantiatedFruits[i].Reference;
if (fruit)
{
fruit.RigidbodyInstance.collisionDetectionMode = CollisionDetectionMode.Discrete; //to avoid warnings...
fruit.RigidbodyInstance.isKinematic = true;
fruit.enabled = false;
}
}
}
if (_score > 32 && MelonGameSettingsInstance.SharedScoreboardPrefab)
{
MelonGameSettingsInstance.SharedScoreboardPrefab.Insert(Networking.LocalPlayer, _score);
}
RequestSerialization();
}
public Fruit InstantiateNewFruitAt(short rank, bool isFused)
{
Fruit newFruit = Instantiate(FruitPrefab).GetComponent<Fruit>();
newFruit.Construct(Root, this, rank, transform.lossyScale.x, DeathZone.localPosition.y, isFused);
_instantiatedFruits.Add(newFruit);
AddToScore(1 << rank);
return newFruit;
}
internal void DestroyFruit(Fruit anotherFruit)
{
_instantiatedFruits.Remove(anotherFruit);
Destroy(anotherFruit.gameObject);
}
private void Update()
{
if (_currentFruit != null)
{
_currentFruit.transform.localPosition = GetCursorPosition();
}
}
public void ReadyForNextFruit()
{
NewFruit();
}
#region Audio
public void IncrementComboAndPlayAudio()
{
_combo++;
ComboPlayer.pitch = 1 + _combo / 6.0f;
SendCustomNetworkEvent(VRC.Udon.Common.Interfaces.NetworkEventTarget.All, nameof(PlayComboAudio));
}
public void PlayComboAudio()
{
if (!_isPlayerInArea)
return;
ComboPlayer.PlayOneShot(Combo);
}
public void PlayCollisionAudio()
{
if (!_isPlayerInArea)
return;
AudioPlayer.PlayOneShot(Collision);
}
public void PlayDropAudio()
{
if (!_isPlayerInArea)
return;
AudioPlayer.PlayOneShot(Drop);
}
#endregion
}
}