#define RECORDING_DEMO namespace Marro.PacManUdon { using System; using UdonSharp; using UnityEngine; using VRC.SDKBase; using VRC.Udon; using VRC.SDK3.Components; using VRC.Udon.Common.Interfaces; using VRC.SDK3.Data; public partial class GameManager : UdonSharpBehaviour { [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 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; [UdonSynced, FieldChangeCallback(nameof(GameState))] 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]); 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 StartGameButtonPressed() { Debug.Log($"{gameObject} Start Game Button was pressed!"); TakeOwnership(); StartTimeSequence(PacManTimeSequence.StartNewGame); } public void SkipLevelButtonPressed() { if (Networking.IsOwner(gameObject)) { Debug.Log($"{gameObject} Skip level button pressed!"); StartTimeSequence(PacManTimeSequence.BoardClear); TimeSequenceSkipToNextStep(); } } public void StartDemoButtonPressed() { if (Networking.IsOwner(gameObject)) { Debug.Log($"{gameObject} Start demo button pressed!"); StartTimeSequence(PacManTimeSequence.Intermission1); } } 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!"); if (Networking.IsOwner(gameObject)) { 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); } 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() { SetMazeActive(false); 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 SetMazeActive(bool active) { maze.gameObject.SetActive(active); } 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 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; } #region TIME SEQUENCE BEHAVIOUR // A note about the quality of the code here: // I intended to write this using proper classes, right until I realized Udon does not support instantiating classes. // While I'm not a big fan of the partial class solution that I ended up doing (static classes would still be neater, or perhaps separate UdonSharpBehaviour instances), // I'm not redoing this unless I get instantiatable classes before I wrap up this project. bool currentlyInTimeSequence; PacManTimeSequence currentTimeSequence; bool hasTimeSequenceQueued; private DataList timeSequenceQueue; [UdonSynced] float timeSequenceSecondsPassed; int timeSequenceProgress; float[] timeSequenceKeyframeTimes; private void StartTimeSequence(PacManTimeSequence timeSequence) { if (timeSequenceQueue == null) { timeSequenceQueue = new DataList(); } if (currentlyInTimeSequence == true) { int timeSequenceInt = (int)timeSequence; // Doing the conversion in the line below crashes the script. I love working in Udon timeSequenceQueue.Add(timeSequenceInt); hasTimeSequenceQueued = true; return; } Debug.Log($"StartTimeSequence: {timeSequence}"); currentlyInTimeSequence = true; currentTimeSequence = timeSequence; timeSequenceProgress = 0; timeSequenceSecondsPassed = 0; timeSequenceKeyframeTimes = GetTimeSequenceKeyframeTimes(timeSequence); TimeSequenceProgressToTime(timeSequenceSecondsPassed); } private void InsertTimeSequence(PacManTimeSequence timeSequence) { StartTimeSequence(timeSequence); } private void TimeSequenceUpdate(float deltaSeconds) { if (!currentlyInTimeSequence && hasTimeSequenceQueued) { timeSequenceQueue.TryGetValue(0, out DataToken nextTimeSequence); StartTimeSequence((PacManTimeSequence)nextTimeSequence.Int); timeSequenceQueue.RemoveAt(0); hasTimeSequenceQueued = timeSequenceQueue.Count > 0 ? true : false; return; } if (currentlyInTimeSequence) { if (hasTimeSequenceQueued) { while (currentlyInTimeSequence) { TimeSequenceSkipToNextStep(); } } else { TimeSequenceProgressToTime(timeSequenceSecondsPassed + deltaSeconds); } } } private void TimeSequenceSkipToNextStep() { // Debug.Log($"{gameObject} TimeSequenceSkipToNextStep"); if (timeSequenceProgress < timeSequenceKeyframeTimes.Length) { TimeSequenceProgressToTime(timeSequenceKeyframeTimes[timeSequenceProgress]); } else { Debug.LogWarning($"{gameObject} Tried skipping to next time sequence step when already on last step!"); currentlyInTimeSequence = false; } } private void TimeSequenceProgressToTime(float seconds) { timeSequenceSecondsPassed = seconds; while (timeSequenceSecondsPassed >= timeSequenceKeyframeTimes[timeSequenceProgress]) { TimeSequenceExecuteStep(timeSequenceProgress); timeSequenceProgress += 1; if (timeSequenceProgress >= timeSequenceKeyframeTimes.Length) { currentlyInTimeSequence = false; break; } } } private void TimeSequenceExecuteStep(int sequenceProgress) { // Debug.Log($"{gameObject} Triggered time sequence step for sequence {currentTimeSequence} with progress {sequenceProgress}"); switch (currentTimeSequence) { default: Debug.LogError($"{gameObject} No time sequence keyframes known for sequence {currentTimeSequence}"); break; case PacManTimeSequence.AttractScreenIntroduction: TimeSequenceStepAttractScreenIntroduction(sequenceProgress); break; case PacManTimeSequence.AttractScreenDemo: TimeSequenceStepAttractScreenDemo(sequenceProgress); break; case PacManTimeSequence.WaitForStart: TimeSequenceStepWaitForStart(sequenceProgress); break; case PacManTimeSequence.WaitForStartTimeout: TimeSequenceStepWaitForStartTimeout(sequenceProgress); break; case PacManTimeSequence.StartNewGame: TimeSequenceStepStartNewGame(sequenceProgress); break; case PacManTimeSequence.BoardClear: TimeSequenceStepBoardClear(sequenceProgress); break; case PacManTimeSequence.StartNewLevel: TimeSequenceStepStartNewLevel(sequenceProgress); break; case PacManTimeSequence.GhostCaught: TimeSequenceStepGhostCaught(sequenceProgress); break; case PacManTimeSequence.PacManCaught: TimeSequenceStepPacManCaught(sequenceProgress); break; case PacManTimeSequence.RestartLevel: TimeSequenceStepRestartLevel(sequenceProgress); break; case PacManTimeSequence.GameOver: TimeSequenceStepGameOver(sequenceProgress); break; case PacManTimeSequence.Intermission1: TimeSequenceStepIntermission1(sequenceProgress); break; case PacManTimeSequence.Intermission2: TimeSequenceStepIntermission2(sequenceProgress); break; case PacManTimeSequence.Intermission3: TimeSequenceStepIntermission3(sequenceProgress); break; } } private float[] GetTimeSequenceKeyframeTimes(PacManTimeSequence timeSequence) { switch (timeSequence) { default: Debug.LogError($"{gameObject} No time sequence keyframe times known for sequence {timeSequence}"); return new float[0]; case PacManTimeSequence.AttractScreenIntroduction: return DeltaToAbsolute(new float[] { 0, 0.032f, 1f, 1f, .5f, .5f, 1f, .5f, .5f, 1f, .5f, .5f, 1f, .5f, 1f, 1f, 1f, 5f, 0.2f, 2f, 0.91667f, 2f, 0.91667f, 2f, 0.91667f, 2f, 0.91667f }); case PacManTimeSequence.AttractScreenDemo: return DeltaToAbsolute(new float[] { 0, 0.016f, 0.05f, 0.16f, 0.33f, 1.85f, 54f }); case PacManTimeSequence.WaitForStart: return DeltaToAbsolute(new float[] { 0, 0.016f }); case PacManTimeSequence.WaitForStartTimeout: return DeltaToAbsolute(new float[] { 0, 5f }); case PacManTimeSequence.StartNewGame: return DeltaToAbsolute(new float[] { 0, 0.016f, 2.2f, 0.032f, 0.032f, 1.92f, 0.032f }); case PacManTimeSequence.BoardClear: return DeltaToAbsolute(new float[] { 0, 2f, 0.016f, 1.6f - 0.016f, 0.016f, 0.032f, 0.3f }); case PacManTimeSequence.StartNewLevel: return DeltaToAbsolute(new float[] { 0, 0.064f, 0.032f, 1.85f, 0.016f }); case PacManTimeSequence.GhostCaught: return DeltaToAbsolute(new float[] { 0, 0.91667f }); case PacManTimeSequence.PacManCaught: return DeltaToAbsolute(new float[] { 0, 1, 0.35f, 2.40f, 2f }); case PacManTimeSequence.RestartLevel: return DeltaToAbsolute(new float[] { 0, 0.016f, 0.064f, 0.032f, 1.85f, 0.016f }); case PacManTimeSequence.GameOver: return DeltaToAbsolute(new float[] { 0, 1.95f }); case PacManTimeSequence.Intermission1: return DeltaToAbsolute(new float[] { 0, 0.316f, 0.3f, 3.96f, 2.25f, 3.93f }); case PacManTimeSequence.Intermission2: return DeltaToAbsolute(new float[] { 0, 0.25f, 0.083f, 0.3f, 1.43f, 2.5f, 1.816f, 1.25f, 0.017f, 1f, 1.966f, 0.033f }); case PacManTimeSequence.Intermission3: return DeltaToAbsolute(new float[] { 0, 0.316f, 0.7f, 3.35f, 0.83f, 3.67f }); } } private static float[] DeltaToAbsolute(float[] delta) { if (delta.Length < 1) { return new float[0]; } float[] absolute = new float[delta.Length]; absolute[0] = delta[0]; for (int i = 1; i < delta.Length; i++) { absolute[i] = delta[i] + absolute[i - 1]; } return absolute; } public int TimeSequenceProgress { get => timeSequenceProgress; } public float TimeSequenceSecondsPassed { get => timeSequenceSecondsPassed; set => TimeSequenceProgressToTime(value); } #endregion } }