#define RECORDING_DEMO namespace Marro.PacManUdon { using Assets.Scripts; using System; using UdonSharp; using UnityEngine; using VRC.SDK3.Components; using VRC.SDKBase; public partial class GameManager : SyncedObject { [Header("Static game components")] [SerializeField] private Maze[] mazes; [SerializeField] private PacMan pacMan; [SerializeField] private GhostManager ghostManager; [SerializeField] private BonusFruit bonusFruit; [SerializeField] private PelletManager pelletManager; [SerializeField] public StatusDisplay statusDisplay; // This one is public so other scripts can write to the debug display [SerializeField] private PelletManager attractScreen; [SerializeField] private GameObject intermissionScreen; [SerializeField] private GameObject pressStartButtonScreen; [SerializeField] private PlayerInput playerInput; [SerializeField] private Animator demo; [SerializeField] private SoundManager soundManager; [SerializeField] private NetworkManager networkManager; [SerializeField] private GameObject recorder; [Header("Game settings")] [SerializeField] private int startingExtraLives = 3; [SerializeField] private int scoreToExtraLife = 10000; [Tooltip("Override amount of pellets needed to clear stage, set to -1 to disable.")] [SerializeField] private int pelletCountOverride = -1; private Maze maze; private VRCObjectPool pelletPool; private Intermission2Pole intermission2Pole; private Animator mazeSpriteAnimator; private int pelletCountTotal; private int pelletCountRemaining; private GameObject[] attractScreenElements; private GameObject[] intermissionScreenElements; private PacManGameState gameState; [UdonSynced, FieldChangeCallback(nameof(Score))] private int score; [UdonSynced, FieldChangeCallback(nameof(Level))] private int level; [UdonSynced, FieldChangeCallback(nameof(HighScore))] private int highScore; [UdonSynced, FieldChangeCallback(nameof(ExtraLives))] private int extraLives; public void Start() { attractScreenElements = new GameObject[attractScreen.transform.childCount]; for (int i = 0; i < attractScreenElements.Length; i++) { attractScreenElements[i] = attractScreen.transform.GetChild(i).gameObject; } intermissionScreenElements = new GameObject[intermissionScreen.transform.childCount]; for (int i = 0; i < intermissionScreenElements.Length; i++) { intermissionScreenElements[i] = intermissionScreen.transform.GetChild(i).gameObject; } maze = mazes[0]; pelletPool = maze.pelletContainer.GetComponent(); mazeSpriteAnimator = maze.mazeSprite.GetComponent(); intermission2Pole = intermissionScreenElements[4].GetComponent(); ghostManager.Initialize(maze.ghostTargets, pacMan, this); pacMan.Initialize(playerInput, pelletPool, this); bonusFruit.Initialize(); pelletManager.Initialize(pelletPool); statusDisplay.Initialize(); playerInput.Initialize(this); soundManager.Initialize(); intermission2Pole.Initialize(this, ghostManager.Ghosts[0]); networkManager.Initialize(Networking.IsOwner(gameObject)); HideEverything(); SetScore(0); SetHighScore(0); SetLevel(0); StartAttractMode(); } public void FixedUpdate() { TimeSequenceUpdate(Time.deltaTime); } public void JoystickGrabbed() { if (gameState == PacManGameState.AttractMode || gameState == PacManGameState.AttractModeDemo) StartTimeSequence(PacManTimeSequence.WaitForStart); } public void JoystickReleased() { if (gameState == PacManGameState.WaitForStart) StartTimeSequence(PacManTimeSequence.WaitForStartTimeout); } public void ResetButtonPressed() { Debug.Log($"{gameObject} Reset button was pressed!"); Start(); } public void StartGameButtonPressed() { Debug.Log($"{gameObject} Start Game Button was pressed!"); TakeOwnership(); StartTimeSequence(PacManTimeSequence.StartNewGame); } private void StartAttractMode() { // #if RECORDING_DEMO // recorder.gameObject.SetActive(true); StartTimeSequence(PacManTimeSequence.AttractScreenIntroduction); // #else // SetGameState(PacManGameState.AttractMode); // HideEverything(); // demo.gameObject.SetActive(true); // #endif } private void InitializeNewGame() { Debug.Log($"{gameObject} Started new game!"); SetScore(0); SetExtraLives(startingExtraLives); SetLevel(1); } private void InitializeLevel() { Debug.Log($"{gameObject} New level started!"); pelletCountTotal = pelletPool.Pool.Length; pelletCountRemaining = pelletCountTotal; ghostManager.SetPelletsRemaining(pelletCountRemaining); ghostManager.NewLevel(); pelletManager.RestoreAllPellets(); if (pelletCountOverride > 0) { pelletCountRemaining = pelletCountOverride; } mazeSpriteAnimator.SetBool("Blinking", false); } private void RestartLevel() { Debug.Log($"{gameObject} (Re)started level!"); // SetInGameComponentVisibility(true); ghostManager.Reset(); pacMan.Reset(); bonusFruit.Despawn(); soundManager.Reset(); pelletManager.SetPowerPelletsBlink(false); } private void PrepareForCutscene() { HideEverything(); RestartLevel(); SetFrozen(true); } public void GotPellet(bool addScore = true) { pelletCountRemaining--; if (addScore) AddScore(10); ghostManager.PelletConsumed(); soundManager.PlayPelletSound(); soundManager.UpdatePelletCount(pelletCountRemaining); int pelletsConsumed = pelletCountTotal - pelletCountRemaining; if (pelletCountRemaining <= 0) { StartTimeSequence(PacManTimeSequence.BoardClear); } else if (pelletsConsumed == 70 || pelletsConsumed == 170) { bonusFruit.Spawn(); } } public void GotPowerPellet() { if (gameState == PacManGameState.AttractMode) { TimeSequenceSkipToNextStep(); return; } GotPellet(addScore: false); AddScore(50); ghostManager.SetPowerPellet(true); pacMan.SetPowerPellet(true); soundManager.SetGhostBlue(true); } public void EndPowerPellet() { ghostManager.SetPowerPellet(false); pacMan.SetPowerPellet(false); soundManager.SetGhostBlue(false); } public void GotFruit() { AddScore(bonusFruit.Collected()); soundManager.PlayFruitSound(); } public void GhostCaught(int scoreBonus) { if (gameState == PacManGameState.AttractMode) { TimeSequenceSkipToNextStep(); return; } AddScore(scoreBonus); StartTimeSequence(PacManTimeSequence.GhostCaught); pacMan.HideUntilUnfrozen(); } public void PacManCaught() { StartTimeSequence(PacManTimeSequence.PacManCaught); } public void NoGhostsScared() { soundManager.SetGhostBlue(false); } public void NoGhostsRetreating() { soundManager.SetGhostRetreat(false); } public void Intermission2PoleUpdate() { TimeSequenceSkipToNextStep(); } void BoardClearAnimation() { ghostManager.gameObject.SetActive(false); mazeSpriteAnimator.SetBool("Blinking", true); } private void HideEverything() { SetPelletsActive(false); SetMazeVisible(false); SetGhostsActive(false); SetPacManActive(false); SetPressStartButtonScreenVisible(false); SetIntermissionScreenVisible(false); statusDisplay.SetGameOverTextVisible(false); statusDisplay.SetExtraLivesDisplayVisible(false); statusDisplay.SetLevelDisplayVisible(false); statusDisplay.SetPlayer1TextVisible(false); statusDisplay.SetReadyTextVisible(false); demo.gameObject.SetActive(false); } void SetPelletsActive(bool active) { pelletPool.gameObject.SetActive(active); } void SetMazeVisible(bool visible) { mazeSpriteAnimator.SetBool("Hidden", !visible); } void SetGhostsActive(bool active) { ghostManager.SetActive(active); } void SetPacManActive(bool active) { pacMan.SetActive(active); } void SetPressStartButtonScreenVisible(bool visible) { pressStartButtonScreen.SetActive(visible); } void SetIntermissionScreenVisible(bool visible) { intermissionScreen.SetActive(visible); } void SetGameState(PacManGameState newGameState) { // Debug.Log($"{gameObject} State transitioning from {gameState} to {newGameState}"); gameState = newGameState; if (Networking.IsOwner(gameObject)) { RequestSerialization(); } } private void IncrementLevel() { SetLevel(level + 1); } private void SetLevel(int level) { this.level = level; pacMan.SetLevel(level); ghostManager.SetLevel(level); statusDisplay.SetLevel(level); bonusFruit.SetFruitType(PacManConstants.GetFruitTypeForLevel(level)); } void AddScore(int score) { if (gameState == PacManGameState.AttractMode || gameState == PacManGameState.AttractModeDemo) { return; } if (this.score < scoreToExtraLife && this.score + score >= scoreToExtraLife) { BonusLifeReached(); } SetScore(this.score + score); RequestSerialization(); } void SetScore(int score) { this.score = score; statusDisplay.Set1UPScore(score); if (score > highScore) { highScore = score; statusDisplay.SetHighScore(score); } } void SetHighScore(int highScore) { this.highScore = highScore; statusDisplay.SetHighScore(score); } public void DecrementLives() { if (!Networking.IsOwner(gameObject)) { return; } // Debug.Log($"{gameObject} Decremented lives from {extraLives} to {extraLives - 1}"); SetExtraLives(extraLives - 1); } void IncrementLives() { if (!Networking.IsOwner(gameObject)) { return; } // Debug.Log($"{gameObject} Incremented lives from {extraLives} to {extraLives + 1}"); SetExtraLives(extraLives + 1); } void SetExtraLives(int extraLives) { // Debug.Log($"{gameObject} Set lives from {this.extraLives} to {extraLives}"); this.extraLives = extraLives; statusDisplay.SetExtraLives(extraLives); } void BonusLifeReached() { IncrementLives(); soundManager.PlayExtraLifeSound(); } public void SetFrozen(bool frozen, bool ghostIgnoreIfCaught = false, bool ghostKeepAnimating = false) { // Debug.Log($"{gameObject} Set Frozen: {frozen}"); pacMan.SetFrozen(frozen); bonusFruit.SetFrozen(frozen); ghostManager.SetFrozen(frozen, ignoreIfCaught: ghostIgnoreIfCaught); if (!frozen) { pelletManager.SetPowerPelletsBlink(true); } } void TakeOwnership() { Networking.SetOwner(Networking.LocalPlayer, gameObject); Networking.SetOwner(Networking.LocalPlayer, pacMan.gameObject); Networking.SetOwner(Networking.LocalPlayer, pelletPool.gameObject); ghostManager.SetOwner(Networking.LocalPlayer); } public override void AppendSyncedData(byte[][] data, ref int offset) { data[offset++] = new byte[] { Int32ToByte((int)gameState) }; data[offset++] = BitConverter.GetBytes(currentlyInTimeSequence); data[offset++] = new byte[] { Int32ToByte((int)currentTimeSequence) }; data[offset++] = BitConverter.GetBytes(timeSequenceProgress); } public override bool SetSyncedData(byte[] data, ref int offset) { SetGameState((PacManGameState)data[offset++]); var currentlyInTimeSequence = BitConverter.ToBoolean(data, offset++); var currentTimeSequence = (PacManTimeSequence)data[offset++]; var timeSequenceProgress = BitConverter.ToSingle(data, offset); offset += 4; TimeSequenceSyncWithRemote(currentlyInTimeSequence, currentTimeSequence, timeSequenceProgress); return true; } public int ExtraLives { set { SetExtraLives(value); } get => extraLives; } public PacManGameState GameState { set { SetGameState(value); } get => gameState; } public bool GhostsScared { set { } get => GhostsScared; } public int Score { set { SetScore(value); } get => score; } public int HighScore { set { SetHighScore(value); } get => score; } public int Level { set { SetLevel(value); } get => level; } public static byte Int32ToByte(int value) => byte.Parse(value.ToString()); // This is the only way I could find to cast an int to byte in Udon, a regular cast crashes in runtime... } }