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 /// /// This looping method should only be called by the owner, it syncs the data /// 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); } /// /// This event is called from the Joystick script /// 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); }*/ } /// /// This event is called from the Joystick script /// 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(); 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 } }