#define RECORDING_DEMO namespace Marro.PacManUdon { using UdonSharp; using System; using UnityEngine; using VRC.SDKBase; using VRC.Udon; using VRC.SDK3.Components; using VRC.Udon.Common.Interfaces; enum PacManGhostType { Caught, Scared, ScaredWhite, Blinky, Pinky, Inky, Clyde, Special, } public enum PacManGhostState { Normal, CaughtScore, Returning, Entering, Home, Exiting } public enum PacManGhostStartState { Outside, TargetingIdlePosition1, TargetingIdlePosition2 } public class Ghost : GridMover { [SerializeField] private PacManGhostType ghostType; [SerializeField] private PacManGhostStartState startState; private GhostManager ghostManager; private Animator animator; private new Renderer renderer; private PacMan pacMan; private Ghost blinky; private ScoreBonusDisplay scoreBonusDisplay; private Vector3 startPosition; private Quaternion startRotation; private Vector3 startScale; private Vector2 homePosition; private Vector2 idlePosition1; private Vector2 idlePosition2; private Vector2 cornerPosition; private bool kinematic; private bool horizontalOnly; private int housePelletCounterLimit; private bool faceInStartingDirectionUntilUnfrozen; private bool specialLook; private bool followingPredefinedPath; private Vector2[] predefinedPath; private int predefinedPathIndex; [UdonSynced] int rngState; [UdonSynced] private Vector2 syncedPosition; [UdonSynced] private float speed; [UdonSynced] private Vector2 direction; [UdonSynced] private Vector2 target; [UdonSynced] private bool offGrid; [UdonSynced] private bool inTunnel; [UdonSynced] private PacManGhostState ghostState; [UdonSynced] private bool isScared; [UdonSynced] private bool scattering; [UdonSynced] private PacManGhostFrozenState frozenState; [UdonSynced] private bool hideUntilUnfrozen; [UdonSynced] private int housePelletCounter; [UdonSynced] private bool housePelletCounterActive; [UdonSynced] private bool turnAroundSoon; public void Initialize(PacMan pacMan, Ghost blinky, Vector2 homePosition, Vector2 idlePosition1, Vector2 idlePosition2, Vector2 cornerPosition) { ghostManager = transform.parent.GetComponent(); animator = GetComponent(); renderer = GetComponent(); this.pacMan = pacMan; this.blinky = blinky; this.homePosition = homePosition; this.idlePosition1 = idlePosition1; this.idlePosition2 = idlePosition2; this.cornerPosition = cornerPosition; scoreBonusDisplay = transform.Find("ScoreBonusDisplay").gameObject.GetComponent(); scoreBonusDisplay.Initialize(); startPosition = transform.localPosition; startRotation = transform.localRotation; startScale = transform.localScale; frozenState = PacManGhostFrozenState.Frozen; // Debug.Log($"{gameObject} Begin localScale = {initialScale}"); } public void Reset() { // Debug.Log($"{gameObject} Reset!"); transform.localPosition = startPosition; transform.localRotation = startRotation; transform.localScale = startScale; // Debug.Log($"{gameObject} Reset localScale = {transform.localScale}"); if (startState == PacManGhostStartState.Outside) { ghostState = PacManGhostState.Exiting; OffGridTargetReached(); } else { if (startState == PacManGhostStartState.TargetingIdlePosition1) { SetOffGridTarget(idlePosition1, false); } ghostState = PacManGhostState.Entering; OffGridTargetReached(); } isScared = false; inTunnel = false; kinematic = false; followingPredefinedPath = false; turnAroundSoon = false; specialLook = false; rngState = 1; UpdateSpeed(); faceInStartingDirectionUntilUnfrozen = true; UpdateAnimator(); RequestSerialization(); // Debug.Log($"{gameObject} reset with state: {state}, target: {target}, offGrid: {offGrid}"); } void FixedUpdate() { if (ghostType == PacManGhostType.Blinky) { // ghostManager.gameStateManager.statusDisplay.SetDebugText(2, $"{turnAroundSoon}"); } if (frozenState == PacManGhostFrozenState.Frozen || (frozenState == PacManGhostFrozenState.FrozenIfNotCaught && ((ghostState != PacManGhostState.Returning && ghostState != PacManGhostState.Entering) || hideUntilUnfrozen))) { return; } Vector2 position = GetPosition(); Vector2 nextPosition = GridMoverTools.GetNextPosition(position, direction, speed); nextPosition = ProcessNextPosition(position, nextPosition); SetPosition(nextPosition); } private Vector2 ProcessNextPosition(Vector2 position, Vector2 nextPosition) { if (turnAroundSoon && ghostState == PacManGhostState.Normal && GridMoverTools.CrossesTileBorder(position, nextPosition, direction.x != 0, direction.y != 0)) { // Debug.Log($"{gameObject} turned around"); SetDirection(direction * -1); turnAroundSoon = false; } if (kinematic) { return nextPosition; } if (offGrid || ghostState == PacManGhostState.Returning) { bool XAxisAlligned = CheckAndAllignToTargetX(position, nextPosition, target); bool YAxisAlligned = CheckAndAllignToTargetY(position, nextPosition, target); if (offGrid) { if (XAxisAlligned) { nextPosition.x = target.x; } if (YAxisAlligned) { nextPosition.y = target.y; } } if (XAxisAlligned && YAxisAlligned) { if (!offGrid) { nextPosition = target; } OffGridTargetReached(); // Debug.Log($"{gameObject} reached offGridTarget {target} at position {position}, new state {state}, offGrid is now {offGrid}, new direction: {direction}"); } if ((XAxisAlligned || YAxisAlligned) && offGrid) { SetDirection(GetOffGridDirectionToTarget(nextPosition, target, direction)); // Debug.Log($"{gameObject} Alligned X Axis: {XAxisAlligned}, Y Axis: {YAxisAlligned} with position: {position}, new nextPosition: {nextPosition}, new target: {target}, now moving in direction {direction}"); // nextPosition = GridMover.GetNextPosition(position, direction, speed); } } if (!offGrid && followingPredefinedPath) { nextPosition = ProcessPredefinedPath(position, nextPosition); } else if (!offGrid && GridMoverTools.CrossesTileCenter(position, nextPosition, direction.x != 0, direction.y != 0)) { Vector2 gridPosition = GridMoverTools.PositionToGrid(position); Vector2[] availableDirections = GetAvailableDirections(gridPosition, direction * -1); if (availableDirections.Length > 1) { target = GetGridTarget(gridPosition); SetDirection(GetGridDirectionToTargetGreedy(availableDirections, gridPosition, target)); nextPosition = GridMoverTools.GetNextPosition(gridPosition, direction, speed); // Debug.Log($"GetNextPosition at gridPosition {gridPosition} with direction {direction} and speed {speed} gives nextPosition {nextPosition}"); } else if (availableDirections.Length == 1 && availableDirections[0] != direction) { SetDirection(availableDirections[0]); nextPosition = GridMoverTools.GetNextPosition(gridPosition, direction, speed); } // Debug.Log($"{gameObject} crossed tile center {gridPosition}, new target: {target}, new direction: {direction}"); } return nextPosition; } private Vector2 ProcessPredefinedPath(Vector2 position, Vector2 nextPosition) { if (GridMoverTools.CrossesTileCenter(position, nextPosition, direction.x != 0, direction.y != 0)) { // Find the next valid direction which isn't Vector2.zero int nextValidDirectionIndex = predefinedPathIndex; while (predefinedPath[nextValidDirectionIndex] == Vector2.zero) { nextValidDirectionIndex += 1; } if (!GridMoverTools.CheckCollisionInDirection(transform, nextPosition, predefinedPath[nextValidDirectionIndex])) { // If we're at a Vector2.zero, we skip applying the direction and only increment. if (nextValidDirectionIndex == predefinedPathIndex) { SetDirection(predefinedPath[nextValidDirectionIndex]); nextPosition = GridMoverTools.PositionToGrid(nextPosition) + direction.normalized * 0.01f; // Check if we've reached the end of the path, which includes making sure the path doesn't end on Vector2.zero do { nextValidDirectionIndex += 1; if (nextValidDirectionIndex >= predefinedPath.Length) { followingPredefinedPath = false; break; } } while (predefinedPath[nextValidDirectionIndex] == Vector2.zero); } // ghostManager.gameStateManager.statusDisplay.SetDebugText(1, predefinedPathIndex.ToString()); predefinedPathIndex++; } } return nextPosition; } Vector2 GetGridTarget(Vector2 gridPosition) { // if (followingPredefinedPath) // { // predefinedPathIndex++; // if (predefinedPathIndex >= predefinedPath.Length) // { // followingPredefinedPath = false; // } // return gridPosition + predefinedPath[predefinedPathIndex]; // } if (isScared) { switch (PseudoRNG() % 4) { default: return gridPosition; case 0: return gridPosition + Vector2.up; case 1: return gridPosition + Vector2.left; case 2: return gridPosition + Vector2.down; case 3: return gridPosition + Vector2.right; } } switch (ghostState) { default: return gridPosition; case PacManGhostState.Normal: if (scattering) { return cornerPosition; } switch (ghostType) { default: return gridPosition; case PacManGhostType.Blinky: return GridMoverTools.PositionToGrid(pacMan.transform.localPosition); case PacManGhostType.Pinky: return GridMoverTools.PositionToGrid(pacMan.transform.localPosition) + pacMan.GetDirection() * 4; case PacManGhostType.Inky: return 2 * GridMoverTools.PositionToGrid(pacMan.transform.localPosition) + 4 * pacMan.GetDirection() - GridMoverTools.PositionToGrid(blinky.transform.localPosition); case PacManGhostType.Clyde: if (Vector2.Distance(gridPosition, GridMoverTools.PositionToGrid(pacMan.transform.localPosition)) >= 8) { return GridMoverTools.PositionToGrid(pacMan.transform.localPosition); } else { // Debug.Log($"{gameObject} goes to cornerPosition {cornerPosition}"); return cornerPosition; } } case PacManGhostState.Returning: return homePosition; } } bool CheckAndAllignToTargetX(Vector2 currentPosition, Vector2 nextPosition, Vector2 target) { return (currentPosition.x - target.x) * (nextPosition.x - target.x) <= 0.01; } bool CheckAndAllignToTargetY(Vector2 currentPosition, Vector2 nextPosition, Vector2 target) { return (currentPosition.y - target.y) * (nextPosition.y - target.y) <= 0.01; } void OffGridTargetReached() { switch (ghostState) { case PacManGhostState.Returning: offGrid = true; SetState(PacManGhostState.Entering); SetOffGridTarget(idlePosition2, false); break; case PacManGhostState.Entering: offGrid = true; SetState(PacManGhostState.Home); ghostManager.GhostReturned(); if (!target.Equals(idlePosition1)) { // This is idlePosition1 if PacManGhostStartState == TargetingIdlePosition1 SetOffGridTarget(idlePosition2, false); } TryToExit(); break; case PacManGhostState.Home: if (target.Equals(idlePosition1)) { SetOffGridTarget(idlePosition2, false); } else { SetOffGridTarget(idlePosition1, false); } break; case PacManGhostState.Exiting: offGrid = false; SetState(PacManGhostState.Normal); SetDirection(Vector2.left); break; } } void SetOffGridTarget(Vector2 newTarget, bool startHorizontal) { if (startHorizontal) { SetDirection(GetOffGridDirectionToTarget(GetPosition(), newTarget, Vector2.right)); } else { SetDirection(GetOffGridDirectionToTarget(GetPosition(), newTarget, Vector2.down)); } // Debug.Log($"{gameObject} SetOffGridTarget with position {GetPosition()}, newTarget {newTarget}, startHorizontal {startHorizontal} resulted in direction {direction}"); target = newTarget; } public int PseudoRNG() { rngState ^= rngState << 13; rngState ^= rngState >> 17; rngState ^= rngState << 5; return rngState; } Vector2[] GetAvailableDirections(Vector2 gridPosition, Vector2 discardDirection) { Vector2[] directions; Vector2[] availableDirections; if (horizontalOnly && ghostState == PacManGhostState.Normal) { directions = new Vector2[] { new Vector2(1, 0), new Vector2(-1, 0) }; availableDirections = new Vector2[2]; } else { directions = new Vector2[] { new Vector2(0, 1), new Vector2(1, 0), new Vector2(0, -1), new Vector2(-1, 0) }; availableDirections = new Vector2[4]; } int availableDirectionsNum = 0; for (int i = 0; i < directions.Length; i++) { if (directions[i].Equals(discardDirection) || GridMoverTools.CheckCollisionInDirection(transform, gridPosition, directions[i])) { continue; } availableDirections[availableDirectionsNum] = directions[i]; availableDirectionsNum++; } Vector2[] availableDirectionsTrimmed = new Vector2[availableDirectionsNum]; // Debug.Log($"{gameObject} Number of available directions: {availableDirectionsTrimmed.Length}"); Array.Copy(availableDirections, 0, availableDirectionsTrimmed, 0, availableDirectionsNum); return availableDirectionsTrimmed; } Vector2 GetGridDirectionToTargetGreedy(Vector2[] availableDirections, Vector2 gridPosition, Vector2 targetGridPosition) { Vector2 bestDirection = Vector2.zero; float bestDistance = float.MaxValue; for (int i = 0; i < availableDirections.Length; i++) { Vector2 direction = availableDirections[i]; // Debug.Log("Evaluating direction " + direction); float distance = Vector2.Distance(gridPosition + direction, targetGridPosition); if (distance < bestDistance) { bestDistance = distance; bestDirection = direction; } } // Debug.Log("Closest next tile is in direction " + bestDirection); return bestDirection; } Vector2 GetOffGridDirectionToTarget(Vector2 position, Vector2 target, Vector2 direction) { if ((direction.x != 0 && position.x != target.x) || position.y == target.y) { // Debug.Log($"{gameObject} getOffGridDirectionToTarget with position {position}, target {target} and direction {direction} gives movement on the X axis in direction {new Vector2(target.x-position.x, 0).normalized}"); return new Vector2(target.x - position.x, 0).normalized; } else { // Debug.Log($"{gameObject} getOffGridDirectionToTarget with position {position}, target {target} and direction {direction} gives movement on the Y axis in direction {new Vector2(0, target.y-position.y).normalized}"); return new Vector2(0, target.y - position.y).normalized; } } protected override void UpdateAnimator() { if (!gameObject.activeInHierarchy) return; // Debug.Log($"{gameObject} UpdateAnimator with state: {ghostState}, isScared: {isScared}, direction: {direction}"); if (specialLook) { animator.SetFloat("GhostType", GhostTypeToAnimationValue(PacManGhostType.Special)); } else if (isScared) { float currentGhostType = animator.GetFloat("GhostType"); if (currentGhostType > 0.5f && currentGhostType < 2.5f) { return; } animator.SetFloat("GhostType", GhostTypeToAnimationValue(PacManGhostType.Scared)); } else { switch (ghostState) { default: case PacManGhostState.Normal: case PacManGhostState.Home: case PacManGhostState.Exiting: // Debug.Log($"{gameObject} Set GhostType in animator to: {GhostTypeToAnimationValue(ghostType)}"); animator.SetFloat("GhostType", GhostTypeToAnimationValue(ghostType)); break; case PacManGhostState.Returning: case PacManGhostState.Entering: animator.SetFloat("GhostType", GhostTypeToAnimationValue(PacManGhostType.Caught)); break; } } if (faceInStartingDirectionUntilUnfrozen && startState == PacManGhostStartState.TargetingIdlePosition1) { animator.SetFloat("DirX", 0); animator.SetFloat("DirY", 1); } else if (faceInStartingDirectionUntilUnfrozen && startState == PacManGhostStartState.TargetingIdlePosition2) { animator.SetFloat("DirX", 0); animator.SetFloat("DirY", -1); } else if (specialLook || !direction.Equals(Vector2.zero)) { animator.SetFloat("DirX", direction.x); animator.SetFloat("DirY", direction.y); } } private float GhostTypeToAnimationValue(PacManGhostType ghostType) { switch (ghostType) { default: Debug.LogError("Invalid ghost animation value!"); return 0; case PacManGhostType.Caught: return 0; case PacManGhostType.Scared: return 1; case PacManGhostType.ScaredWhite: return 2; case PacManGhostType.Blinky: return 3; case PacManGhostType.Pinky: return 4; case PacManGhostType.Inky: return 5; case PacManGhostType.Clyde: return 6; case PacManGhostType.Special: return 7; } } public void UpdateSpeed() { speed = ghostManager.GetTargetSpeed(this, ghostState, isScared, inTunnel); // Debug.Log($"Ghost with type {ghostType} updated speed to {speed}, ghostState: {ghostState}, isScared: {isScared}, inTunnel: {inTunnel}, elroyLevel: {ghostManager.elroyLevel}"); } public void ResetHousePelletCounter() { housePelletCounter = 0; } public void IncrementHousePelletCounter() { housePelletCounter++; TryToExit(); } void TryToExit() { if ((housePelletCounterActive && housePelletCounter >= housePelletCounterLimit) || startState == PacManGhostStartState.Outside) { Release(); } } public void Caught(int scoreBonus) { isScared = false; // direction = direction * -1; target = homePosition; scoreBonusDisplay.Display(scoreBonus); SetVisibility(false); SetState(PacManGhostState.CaughtScore); } public void ReturnHome() { SetVisibility(true); scoreBonusDisplay.Hide(); SetState(PacManGhostState.Returning); } public void BecomeScared() { if (ghostState == PacManGhostState.Returning || ghostState == PacManGhostState.Entering) { return; } if (ghostState == PacManGhostState.Normal || ghostState == PacManGhostState.Home || ghostState == PacManGhostState.Exiting) { turnAroundSoon = true; } SetScared(true); } public void CalmDown() { if (isScared) { SetScared(false); } } public void Release() { // Debug.Log($"{gameObject} was released."); if (ghostState == PacManGhostState.Home) { // Debug.Log($"{gameObject} released."); SetOffGridTarget(homePosition, true); SetState(PacManGhostState.Exiting); } } public void SetState(PacManGhostState state) { ghostState = state; UpdateAnimator(); UpdateSpeed(); if (Networking.IsOwner(gameObject)) { RequestSerialization(); } } private void SetScared(bool scared) { isScared = scared; UpdateAnimator(); UpdateSpeed(); if (isScared) { SetWhite(false); } if (Networking.IsOwner(gameObject)) { RequestSerialization(); } } public void SetScattering(bool scattering, bool reverseDirection = true) { if (reverseDirection && this.scattering != scattering) { if (ghostState == PacManGhostState.Normal || ghostState == PacManGhostState.Home || ghostState == PacManGhostState.Exiting // This is afaik not normal PacMan behaviour, but is needed to accomidate slight timing differences || ghostState == PacManGhostState.Entering && ghostManager.gameController.GameState == PacManGameState.AttractModeDemo ) { turnAroundSoon = true; } } this.scattering = scattering; UpdateAnimator(); RequestSerialization(); } public void SetFrozen(bool frozen, bool ignoreIfCaught = false, bool keepAnimating = false) { if (frozen && !ignoreIfCaught) { frozenState = PacManGhostFrozenState.Frozen; } else if (frozen && ignoreIfCaught) { frozenState = PacManGhostFrozenState.FrozenIfNotCaught; } else { frozenState = PacManGhostFrozenState.NotFrozen; } animator.speed = frozen && !keepAnimating ? 0 : 1; // This would cause issues if the returning sprite was animated, luckily it isn't :) if (frozen == false && faceInStartingDirectionUntilUnfrozen) { faceInStartingDirectionUntilUnfrozen = false; UpdateAnimator(); } } public void SetHousePelletCounterActive(bool active) { housePelletCounterActive = active; } public void SetHousePelletCounterLimit(int limit) { housePelletCounterLimit = limit; } public void SetWhite(bool white) { if (!isScared || !gameObject.activeInHierarchy) return; if (white) { animator.SetFloat("GhostType", 2); } else { animator.SetFloat("GhostType", 1); } } public void SetElroy(int elroyLevel) { // Debug.Log($"{gameObject} became Elroy level {elroyLevel}"); if (elroyLevel > 0) { SetScattering(false, reverseDirection: false); } UpdateSpeed(); } public void SetActive(bool active) { gameObject.SetActive(active); renderer.enabled = active; if (active) { UpdateAnimator(); } } public void SetKinematic(bool kinematic) { this.kinematic = kinematic; } public void SetPredefinedPath(Vector2[] predefinedPath) { this.predefinedPath = predefinedPath; followingPredefinedPath = true; predefinedPathIndex = 0; } public void SetInTunnel(bool inTunnel) { this.inTunnel = inTunnel; UpdateSpeed(); } public void SetSpecialLook(bool enabled) { specialLook = enabled; UpdateAnimator(); } void SetVisibility(bool visible) { renderer.enabled = visible; } public PacManGhostState GetGhostState() { return ghostState; } public bool IsScared => isScared; public void SetSpeed(float speed) { this.speed = speed; UpdateAnimator(); } public override void AppendSyncedData(byte[][] data, ref int offset) { base.AppendSyncedData(data, ref offset); } void OnTriggerEnter(Collider other) { if (Networking.IsOwner(gameObject) && other.gameObject.GetComponent()) { if (isScared) { // Debug.Log($"{gameObject} was cought!"); ghostManager.GhostCaughtQueue(this); } else if (ghostState == PacManGhostState.Normal) { // Debug.Log($"{gameObject} cought PacMan!"); ghostManager.CapturedPacMan(); } } else if (other.gameObject.GetComponent()) { horizontalOnly = true; } else if (other.gameObject.GetComponent()) { SetInTunnel(true); } } void OnTriggerExit(Collider other) { if (other.gameObject.GetComponent()) { horizontalOnly = false; } else if (other.gameObject.GetComponent()) { SetInTunnel(false); } } PacManGhostState State { set { SetState(value); } get => ghostState; } } }