using System; using UnityEngine; namespace Marro.PacManUdon { 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 } [RequireComponent(typeof(Renderer))] [RequireComponent(typeof(Animator))] public class Ghost : GridMover { [SerializeField] private PacManGhostType ghostType; [SerializeField] private PacManGhostStartState startState; // External references private GhostManager ghostManager; private CollisionManager collisionManager; private Animator animator; private new Renderer renderer; private PacMan pacMan; private Ghost blinky; private ScoreBonusDisplay scoreBonusDisplay; private Vector3 startPosition; private Quaternion startRotation; private Vector2 homePosition; private Vector2 idlePosition1; private Vector2 idlePosition2; private Vector2 cornerPosition; // Pathfinding private Vector2 target; private bool horizontalOnly; private bool inTunnel; private int rngState; private bool turnAroundSoon; private float speed; // State private PacManGhostState ghostState; private bool isScared; private bool scattering; private PacManGhostFrozenState frozenState; // Home private bool offGrid; private int housePelletCounter; private bool housePelletCounterActive; private int housePelletCounterLimit; private bool faceInStartingDirectionUntilUnfrozen; // Cutscene private bool kinematic; private bool specialLook; private bool followingPredefinedPath; private Direction[] predefinedPath; private int predefinedPathIndex; public bool IsScared => isScared; public int Index { get; private set; } private readonly int animatorKeyDirection = Animator.StringToHash("Direction"); private readonly int animatorKeyGhostType = Animator.StringToHash("GhostType"); public void Initialize(CollisionManager collisionManager, PacMan pacMan, Ghost blinky, Transform startTransform, Vector2 homePosition, Vector2 idlePosition1, Vector2 idlePosition2, Vector2 cornerPosition, int index) { ghostManager = transform.parent.GetComponent(); animator = GetComponent(); renderer = GetComponent(); this.collisionManager = collisionManager; 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 = startTransform.localPosition; startRotation = startTransform.localRotation; frozenState = PacManGhostFrozenState.Frozen; Index = index; SubscribeToEvent(NetworkEventType.GhostUpdate); } public void Reset() { // Debug.Log($"{gameObject} Reset!"); transform.SetLocalPositionAndRotation(startPosition, startRotation); 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(); // Debug.Log($"{gameObject} reset with state: {state}, target: {target}, offGrid: {offGrid}"); } public override void SyncedUpdate() { if (frozenState == PacManGhostFrozenState.Frozen || (frozenState == PacManGhostFrozenState.FrozenIfNotCaught && ghostState != PacManGhostState.Returning && ghostState != PacManGhostState.Entering)) { return; } Vector2 position = GetPosition(); Vector2 nextPosition = GetNextPosition(position, directionVectors[(int)direction], speed, networkManager.SyncedDeltaTime); nextPosition = ProcessNextPosition(position, nextPosition); SetPosition(nextPosition); } private Vector2 ProcessNextPosition(Vector2 position, Vector2 nextPosition) { if (turnAroundSoon && ghostState == PacManGhostState.Normal && CrossesTileCenter(position, nextPosition, direction)) { var newDirection = GetInverseDirection(direction); SetDirectionAndTargetDirection(newDirection); turnAroundSoon = false; return nextPosition; } if (kinematic) { return nextPosition; } if (offGrid || ghostState == PacManGhostState.Returning) { PerformOffgridRelatedMovement(position, ref nextPosition); } if (!offGrid) { if (followingPredefinedPath) { ProcessPredefinedPath(position, ref nextPosition); } else if (CrossesTileCenter(position, nextPosition, direction)) { if (targetDirection != direction) { ApplyTargetDirection(position, out nextPosition); } else { TryToQueueTurn(position); } } } if (CrossesTileBorder(position, nextPosition, direction)) { var inTunnel = collisionManager.GhostMoveToTile(nextPosition, Index); if (inTunnel != this.inTunnel) { this.inTunnel = inTunnel; UpdateSpeed(); } } return nextPosition; } private Vector2 PerformOffgridRelatedMovement(Vector2 position, ref Vector2 nextPosition) { 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) { SetDirectionAndTargetDirection(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); } return nextPosition; } private void TryToQueueTurn(Vector2 position) { var upcomingGridPosition = PositionToGrid(position + directionVectors[(int)direction]); var availableDirections = collisionManager.GetAvailableDirections(upcomingGridPosition); if ((availableDirections & (int)PacManCollisionInfoType.NoTurn) != 0 ) { return; } availableDirections &= ~(int)GetInverseDirection(direction); // Not allowed to turn around if (!isScared && (availableDirections & (int)PacManCollisionInfoType.HorizontalOnly) != 0) { availableDirections &= ~0b0011; } target = GetGridTarget(upcomingGridPosition); var newDirection = GetGridDirectionToTargetGreedy(availableDirections, upcomingGridPosition, target); if (newDirection == direction) { return; } SetTargetDirection(newDirection); //Debug.Log($"{gameObject.name} Turned from direction {direction} to direction {newDirection}"); } private void ApplyTargetDirection(Vector2 position, out Vector2 nextPosition) { var gridPosition = PositionToGrid(position); nextPosition = GetNextPosition(gridPosition, directionVectors[(int)targetDirection], speed, networkManager.SyncedDeltaTime); SetDirection(targetDirection); } private void ProcessPredefinedPath(Vector2 position, ref Vector2 nextPosition) { if (CrossesTileCenter(position, nextPosition, direction)) { // Find the next valid direction which isn't Vector2.zero int nextValidDirectionIndex = predefinedPathIndex; while (predefinedPath[nextValidDirectionIndex] == Direction.Zero) { nextValidDirectionIndex += 1; } if (!collisionManager.IsWallUpcoming(nextPosition, directionVectors[(int)predefinedPath[nextValidDirectionIndex]])) { // If we're at a Vector2.zero, we skip applying the direction and only increment. if (nextValidDirectionIndex == predefinedPathIndex) { SetDirectionAndTargetDirection(predefinedPath[nextValidDirectionIndex]); nextPosition = PositionToGrid(nextPosition) + GetVector(direction) * 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] == Direction.Zero); } // ghostManager.gameStateManager.statusDisplay.SetDebugText(1, predefinedPathIndex.ToString()); predefinedPathIndex++; } } } Vector2 GetGridTarget(Vector2 gridPosition) { // if (followingPredefinedPath) // { // predefinedPathIndex++; // if (predefinedPathIndex >= predefinedPath.Length) // { // followingPredefinedPath = false; // } // return gridPosition + predefinedPath[predefinedPathIndex]; // } if (isScared) { return gridPosition + directionVectors[(int)cardinalDirections[PseudoRNG() % 4]]; } switch (ghostState) { default: return gridPosition; case PacManGhostState.Normal: if (scattering) { return cornerPosition; } switch (ghostType) { default: return gridPosition; case PacManGhostType.Blinky: return PositionToGrid(pacMan.transform.localPosition); case PacManGhostType.Pinky: return PositionToGrid(pacMan.transform.localPosition) + pacMan.GetVector(direction) * 4; case PacManGhostType.Inky: return 2 * PositionToGrid(pacMan.transform.localPosition) + 4 * pacMan.GetVector(direction) - PositionToGrid(blinky.transform.localPosition); case PacManGhostType.Clyde: if (Vector2.Distance(gridPosition, PositionToGrid(pacMan.transform.localPosition)) >= 8) { return 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); SetDirectionAndTargetDirection(Direction.Left); break; } } void SetOffGridTarget(Vector2 newTarget, bool startHorizontal) { if (startHorizontal) { SetDirectionAndTargetDirection(GetOffGridDirectionToTarget(GetPosition(), newTarget, Direction.Right)); } else { SetDirectionAndTargetDirection(GetOffGridDirectionToTarget(GetPosition(), newTarget, Direction.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 Math.Abs(rngState); } private readonly Direction[] cardinalDirections = new Direction[] { Direction.Up, Direction.Left, Direction.Down, Direction.Right }; private readonly Direction[] horizontalDirections = new Direction[] { Direction.Left, Direction.Right }; Direction GetGridDirectionToTargetGreedy(int availableDirections, Vector2 gridPosition, Vector2 targetGridPosition) { Direction bestDirection = Direction.Zero; float bestDistance = float.MaxValue; foreach (var direction in horizontalOnly ? horizontalDirections : cardinalDirections) { if (((int)direction & availableDirections) == 0) { continue; } // Debug.Log("Evaluating direction " + direction); float distance = Vector2.Distance(gridPosition + directionVectors[(int)direction], targetGridPosition); if (distance < bestDistance) { bestDistance = distance; bestDirection = direction; } } //Debug.Log($"{gameObject.name} Closest next tile is in direction {bestDirection}"); return bestDirection; } private Direction GetOffGridDirectionToTarget(Vector2 position, Vector2 target, Direction direction) { if ((IsHorizontal(direction) && 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 HorizontalToDirection(target.x - position.x); } 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 VerticalToDirection(target.y - position.y); } } protected override void UpdateAnimator() { if (!gameObject.activeInHierarchy) return; // Debug.Log($"{gameObject} UpdateAnimator with state: {ghostState}, isScared: {isScared}, direction: {direction}"); if (specialLook) { SetAnimatorGhostType((int)PacManGhostType.Special); } else if (!isScared) { switch (ghostState) { default: case PacManGhostState.Normal: case PacManGhostState.Home: case PacManGhostState.Exiting: // Debug.Log($"{gameObject} Set GhostType in animator to: {GhostTypeToAnimationValue(ghostType)}"); SetAnimatorGhostType((int)ghostType); break; case PacManGhostState.Returning: case PacManGhostState.Entering: SetAnimatorGhostType((int)PacManGhostType.Caught); break; } } if (faceInStartingDirectionUntilUnfrozen && startState == PacManGhostStartState.TargetingIdlePosition1) { SetAnimatorDirection((int)Direction.Up); } else if (faceInStartingDirectionUntilUnfrozen && startState == PacManGhostStartState.TargetingIdlePosition2) { SetAnimatorDirection((int)Direction.Down); } else if (specialLook || targetDirection != Direction.Zero) { SetAnimatorDirection((int)targetDirection); } } // A Udon bug means converting an enum to a float causes an exception unless the conversion from int to float is in a separate method private void SetAnimatorDirection(int value) => animator.SetFloat(animatorKeyDirection, value); private void SetAnimatorGhostType(int value) => animator.SetFloat(animatorKeyGhostType, value); 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(); } } internal void HitPacMan() { if (isScared) { Debug.Log($"{gameObject} was cought!"); ghostManager.GhostCaughtQueue(this); } else if (ghostState == PacManGhostState.Normal) { Debug.Log($"{gameObject} cought PacMan!"); ghostManager.CapturedPacMan(); } } 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(); } private void SetScared(bool scared) { isScared = scared; UpdateAnimator(); UpdateSpeed(); if (isScared) { SetWhite(false); } } 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 during the demo || ghostState == PacManGhostState.Entering && ghostManager.gameController.GameState == PacManGameState.AttractModeDemo ) { turnAroundSoon = true; } } this.scattering = scattering; UpdateAnimator(); } 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; SetAnimatorGhostType(white ? (int)PacManGhostType.ScaredWhite : (int)PacManGhostType.Scared); } 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(Direction[] 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 void SetSpeed(float speed) { this.speed = speed; UpdateAnimator(); } public override void CollectSyncedData(byte[] data, ref int index, NetworkEventType eventType) { if (eventType != NetworkEventType.GhostUpdate) { return; } data.Append(target, ref index); data.Append(horizontalOnly, ref index); data.Append(inTunnel, ref index); data.Append(rngState, ref index); data.Append(turnAroundSoon, ref index); data.Append(speed, ref index); data.AppendAsByte((int)ghostState, ref index); data.Append(isScared, ref index); data.Append(scattering, ref index); data.AppendAsByte((int)frozenState, ref index); data.Append(offGrid, ref index); data.AppendAsByte(housePelletCounter, ref index); data.Append(housePelletCounterActive, ref index); data.AppendAsByte(housePelletCounterLimit, ref index); data.Append(faceInStartingDirectionUntilUnfrozen, ref index); base.CollectSyncedData(data, ref index, eventType); } public override bool WriteSyncedData(byte[] data, ref int index, NetworkEventType eventType) { if (eventType != NetworkEventType.GhostUpdate) { return true; } target = data.ReadVector2(ref index); horizontalOnly = data.ReadBool(ref index); inTunnel = data.ReadBool(ref index); rngState = data.ReadInt(ref index); turnAroundSoon = data.ReadBool(ref index); speed = data.ReadFloat(ref index); ghostState = (PacManGhostState)data.ReadByte(ref index); isScared = data.ReadBool(ref index); scattering = data.ReadBool(ref index); frozenState = (PacManGhostFrozenState)data.ReadByte(ref index); offGrid = data.ReadBool(ref index); housePelletCounter = data.ReadByte(ref index); housePelletCounterActive = data.ReadBool(ref index); housePelletCounterLimit = data.ReadByte(ref index); faceInStartingDirectionUntilUnfrozen = data.ReadBool(ref index); return base.WriteSyncedData(data, ref index, eventType); } } }