using UdonSharp; using UnityEngine; using VRC.SDKBase; using VRC.SDK3.Data; namespace Marro.PacManUdon { public partial class GameManager { // 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; bool waitingForTimeSequenceFinalize; bool jumpingToTimeSequence; PacManTimeSequence currentTimeSequence; [UdonSynced] float timeSequenceSecondsPassed; int timeSequenceProgress; float[] timeSequenceKeyframeTimes; private void StartTimeSequence(PacManTimeSequence timeSequence) { Debug.Log($"StartTimeSequence: {timeSequence}"); if (currentlyInTimeSequence) { TimeSequenceEndCurrent(); } TimeSequencePrepareForStart(timeSequence); currentlyInTimeSequence = true; currentTimeSequence = timeSequence; timeSequenceProgress = 0; timeSequenceSecondsPassed = 0; timeSequenceKeyframeTimes = GetTimeSequenceKeyframeTimes(timeSequence); TimeSequenceProgressToTime(timeSequenceSecondsPassed); } private void TimeSequenceEndCurrent() { jumpingToTimeSequence = true; TimeSequenceProgressToTime(100000f); TryFinalizeTimeSequence(); jumpingToTimeSequence = false; } private void TimeSequenceUpdate(float deltaSeconds) { if (!currentlyInTimeSequence) { return; } 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(currentTimeSequence, timeSequenceProgress); timeSequenceProgress += 1; if (timeSequenceProgress >= timeSequenceKeyframeTimes.Length) { currentlyInTimeSequence = false; TimeSequencePrepareForFinish(currentTimeSequence); break; } } } private void TimeSequencePrepareForFinish(PacManTimeSequence timeSequence) { if (Networking.IsOwner(gameObject)) { TimeSequenceExecuteFinalize(timeSequence); if (!jumpingToTimeSequence) { TimeSequenceExecuteFinished(timeSequence); } } else { waitingForTimeSequenceFinalize = true; } } private void TryFinalizeTimeSequence() { if (!waitingForTimeSequenceFinalize) { return; } TimeSequenceExecuteFinalize(currentTimeSequence); waitingForTimeSequenceFinalize = false; } private void TimeSequenceSyncWithRemote(bool currentlyInTimeSequence, PacManTimeSequence currentTimeSequence, float timeSequenceProgress) { // If the remote is in a time sequence but we're not, or we're in a different time sequence, jump to the remote's time sequence. if (currentlyInTimeSequence && (!this.currentlyInTimeSequence || currentTimeSequence != this.currentTimeSequence)) { StartTimeSequence(currentTimeSequence); } // If we're (now) in a time sequence, jump our progress to match the one on the remote if (this.currentlyInTimeSequence) { TimeSequenceProgressToTime(timeSequenceProgress); } // If the remote has finished it's time sequence and we have one waiting to be finalized, we can do so now if (!currentlyInTimeSequence) { TryFinalizeTimeSequence(); } } #region Events public void JumpToTimeSequenceAttractScreenIntroduction() { StartTimeSequence(PacManTimeSequence.AttractScreenIntroduction); } public void JumpToTimeSequenceAttractScreenDemo() { StartTimeSequence(PacManTimeSequence.AttractScreenDemo); } public void JumpToTimeSequenceAttractScreenWaitToRestart() { StartTimeSequence(PacManTimeSequence.AttractScreenWaitToRestart); } public void JumpToTimeSequenceBoardClear() { StartTimeSequence(PacManTimeSequence.BoardClear); } public void JumpToTimeSequenceGameOver() { StartTimeSequence(PacManTimeSequence.GameOver); } public void JumpToTimeSequenceGhostCaught() { StartTimeSequence(PacManTimeSequence.GhostCaught); } public void JumpToTimeSequenceIntermission1() { StartTimeSequence(PacManTimeSequence.Intermission1); } public void JumpToTimeSequenceIntermission2() { StartTimeSequence(PacManTimeSequence.Intermission2); } public void JumpToTimeSequenceIntermission3() { StartTimeSequence(PacManTimeSequence.Intermission3); } public void JumpToTimeSequencePacManCaught() { StartTimeSequence(PacManTimeSequence.PacManCaught); } public void JumpToTimeSequenceRestartLevel() { StartTimeSequence(PacManTimeSequence.RestartLevel); } public void JumpToTimeSequenceStartNewGame() { StartTimeSequence(PacManTimeSequence.StartNewGame); } public void JumpToTimeSequenceStartNewLevel() { StartTimeSequence(PacManTimeSequence.StartNewLevel); } public void JumpToTimeSequenceWaitForStart() { StartTimeSequence(PacManTimeSequence.WaitForStart); } public void JumpToTimeSequenceWaitForStartTimeout() { StartTimeSequence(PacManTimeSequence.WaitForStartTimeout); } #endregion #region Jump tables private void TimeSequencePrepareForStart(PacManTimeSequence timeSequence) { switch (timeSequence) { default: Debug.LogError($"{gameObject} No time sequence start known for sequence {currentTimeSequence}"); break; case PacManTimeSequence.AttractScreenIntroduction: case PacManTimeSequence.AttractScreenDemo: case PacManTimeSequence.StartNewGame: case PacManTimeSequence.WaitForStart: case PacManTimeSequence.StartNewLevel: case PacManTimeSequence.Intermission1: case PacManTimeSequence.Intermission2: case PacManTimeSequence.Intermission3: case PacManTimeSequence.AttractScreenWaitToRestart: case PacManTimeSequence.WaitForStartTimeout: case PacManTimeSequence.GhostCaught: case PacManTimeSequence.GameOver: case PacManTimeSequence.PacManCaught: case PacManTimeSequence.BoardClear: case PacManTimeSequence.RestartLevel: // These don't have start logic break; } } private void TimeSequenceExecuteStep(PacManTimeSequence timeSequence, int sequenceProgress) { // Debug.Log($"{gameObject} Triggered time sequence step for sequence {currentTimeSequence} with progress {sequenceProgress}"); switch (timeSequence) { 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.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; case PacManTimeSequence.AttractScreenWaitToRestart: case PacManTimeSequence.WaitForStartTimeout: // These don't have steps break; } } private void TimeSequenceExecuteFinalize(PacManTimeSequence timeSequence) { // Debug.Log($"{gameObject} Triggered time sequence step for sequence {currentTimeSequence} with progress {sequenceProgress}"); switch (timeSequence) { default: Debug.LogError($"{gameObject} No time sequence finalize known for sequence {currentTimeSequence}"); break; case PacManTimeSequence.AttractScreenIntroduction: TimeSequenceFinalizeAttractScreenIntroduction(); break; case PacManTimeSequence.WaitForStart: TimeSequenceFinalizeWaitForStart(); break; case PacManTimeSequence.StartNewGame: TimeSequenceFinalizeStartNewGame(); break; case PacManTimeSequence.StartNewLevel: TimeSequenceFinalizeStartNewLevel(); break; case PacManTimeSequence.GhostCaught: TimeSequenceFinalizeGhostCaught(); break; case PacManTimeSequence.RestartLevel: TimeSequenceFinalizeRestartLevel(); break; case PacManTimeSequence.Intermission2: TimeSequenceFinalizeIntermission2(); break; case PacManTimeSequence.Intermission3: TimeSequenceFinalizeIntermission3(); break; case PacManTimeSequence.AttractScreenDemo: case PacManTimeSequence.WaitForStartTimeout: case PacManTimeSequence.AttractScreenWaitToRestart: case PacManTimeSequence.GameOver: case PacManTimeSequence.Intermission1: case PacManTimeSequence.PacManCaught: case PacManTimeSequence.BoardClear: // These don't have a finalize break; } } private void TimeSequenceExecuteFinished(PacManTimeSequence timeSequence) { // Debug.Log($"{gameObject} Triggered time sequence step for sequence {currentTimeSequence} with progress {sequenceProgress}"); switch (timeSequence) { default: Debug.LogError($"{gameObject} No time sequence finish known for sequence {currentTimeSequence}"); break; case PacManTimeSequence.AttractScreenIntroduction: TimeSequenceFinishedAttractScreenIntroduction(); break; case PacManTimeSequence.AttractScreenDemo: TimeSequenceFinishedAttractScreenDemo(); break; case PacManTimeSequence.AttractScreenWaitToRestart: TimeSequenceFinishedAttractScreenWaitToRestart(); break; case PacManTimeSequence.WaitForStartTimeout: TimeSequenceFinishedWaitForStartTimeout(); break; case PacManTimeSequence.BoardClear: TimeSequenceFinishedBoardClear(); break; case PacManTimeSequence.PacManCaught: TimeSequenceFinishedPacManCaught(); break; case PacManTimeSequence.GameOver: TimeSequenceFinishedGameOver(); break; case PacManTimeSequence.Intermission1: TimeSequenceFinishedIntermission1(); break; case PacManTimeSequence.Intermission2: TimeSequenceFinishedIntermission2(); break; case PacManTimeSequence.Intermission3: TimeSequenceFinishedIntermission3(); break; case PacManTimeSequence.RestartLevel: case PacManTimeSequence.StartNewLevel: case PacManTimeSequence.GhostCaught: case PacManTimeSequence.WaitForStart: case PacManTimeSequence.StartNewGame: // These don't have a finished 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.AttractScreenWaitToRestart: return DeltaToAbsolute(new float[] { 0, 2f }); 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 }); 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; } #endregion public int TimeSequenceProgress { get => timeSequenceProgress; } public float TimeSequenceSecondsPassed { get => timeSequenceSecondsPassed; set => TimeSequenceProgressToTime(value); } } }