using System; using UnityEngine; namespace Marro.PacManUdon { [RequireComponent(typeof(Animator))] [RequireComponent(typeof(Renderer))] public class PacMan : GridMover { private GameManager gameManager; private PlayerInput input; private PelletManager pelletManager; private float defaultSpeed; private float powerPelletSpeed; private float speed; private Vector3 startPosition; private Quaternion startRotation; private Animator animator; new Renderer renderer; private bool hideUntilUnfrozen; private bool dead; private bool kinematic; private bool followingPredefinedPath; private Direction[] predefinedPath; private int predefinedPathIndex; private Direction targetDirection; private float freezeSeconds; private bool frozen; #region Animator constants private const string AnimatorKeyDead = "Dead"; private const string AnimatorKeyDirection = "Direction"; private const float AnimatorDirectionNone = 0f; private const float AnimatorDirectionRight = 0.25f; private const float AnimatorDirectionLeft = 0.50f; private const float AnimatorDirectionDown = 0.75f; private const float AnimatorDirectionUp = 1f; private const float AnimatorDirectionRightBig = 1.25f; #endregion public void Initialize(PlayerInput input, Transform startTransform, GameManager gameManager, PelletManager pelletManager) { this.gameManager = gameManager; this.pelletManager = pelletManager; this.input = input; animator = GetComponent(); renderer = GetComponent(); frozen = false; hideUntilUnfrozen = false; startPosition = startTransform.localPosition; startRotation = startTransform.localRotation; SubscribeToEvent(NetworkEventType.PacManTurn); } public void Reset() { transform.SetLocalPositionAndRotation(startPosition, startRotation); direction = Direction.Left; targetDirection = Direction.Left; speed = defaultSpeed; kinematic = false; followingPredefinedPath = false; SetDead(false); animator.SetTrigger("Reset"); //Debug.Log($"{gameObject} Reset! Position is now {GetPosition()}."); } public override void SyncedUpdate() { // gameStateManager.statusDisplay.SetDebugText(1, this.targetDirection.ToString()); if (frozen) { return; } if (hideUntilUnfrozen) { hideUntilUnfrozen = false; SetVisibility(true); } float speed = this.speed; if (freezeSeconds > 0) { float freezePart = freezeSeconds / networkManager.SyncedDeltaTime; if (freezePart >= 1) { freezeSeconds -= networkManager.SyncedDeltaTime; animator.speed = 0; return; } speed *= 1 - freezePart; animator.speed = 1 - freezePart; freezeSeconds = 0; } else { animator.speed = 1; } Vector2 position = GetPosition(); Vector2 nextPosition = GetNextPosition(position, directionVectors[(int)direction], speed, networkManager.SyncedDeltaTime); // The position pacman will move to, assuming it doens't get changed if (!kinematic) { if (followingPredefinedPath) { nextPosition = ProcessPredefinedPath(position, nextPosition); } else { nextPosition = ProcessNextPosition(position, nextPosition); } } if (CrossesTileBorder(position, nextPosition, direction)) { CheckNewTile(nextPosition); } SetPosition(nextPosition); } private Vector2 ProcessNextPosition(Vector2 position, Vector2 nextPosition) { if (CrossesTileCenter(position, nextPosition, Direction.Left) // If pacman is moving horizontally, check if he may cross the center of a tile in that axis && (!IsHorizontal(targetDirection) || pelletManager.IsWallUpcoming(nextPosition, directionVectors[(int)HorizontalComponent(direction)]))) { // If the target direction is in the other axis or if we're about to run into a wall nextPosition.x = PositionToGrid(nextPosition).x; // Snap pacman to the center of his current tile in this axis SetDirection(VerticalComponent(direction)); Debug.Log($"{gameObject} crossed X tile center from {position}, nextPosition is now {nextPosition} and direction is now {direction}"); } if (CrossesTileCenter(position, nextPosition, Direction.Down) // See comments above but now vertical && (!IsVertical(targetDirection) || pelletManager.IsWallUpcoming(nextPosition, directionVectors[(int)VerticalComponent(direction)]))) { nextPosition.y = PositionToGrid(nextPosition).y; SetDirection(HorizontalComponent(direction)); Debug.Log($"{gameObject} crossed Y tile center from {position} with targetDirection {targetDirection}, nextPosition is now {nextPosition} and direction is now {direction}"); } var inputDirection = input.GetDirection(); if (!inputDirection.Equals(Direction.Zero) && !inputDirection.Equals(targetDirection) // Ignore neutral input or input in our current direction && (!IsHorizontal(inputDirection) || (Math.Round(nextPosition.y, 5) - 0.5) % 1 != 0) // Target grid position near the edge of a tile may not be correct, ignore inputs near the border && (!IsVertical(inputDirection) || (Math.Round(nextPosition.x, 5) - 0.5) % 1 != 0) && !pelletManager.IsWallUpcoming(nextPosition, directionVectors[(int)inputDirection])) { // Check if the requested direction does not have a wall if (IsHorizontal(inputDirection)) { // Move in the requested direction, as well as perpundicular to it to get to the center of the tunnel var directionToCenter = VerticalToDirection(PositionToGrid(nextPosition).y - nextPosition.y); SetDirection((Direction)((int)inputDirection | (int)directionToCenter)); } else { var directionToCenter = HorizontalToDirection(PositionToGrid(nextPosition).x - nextPosition.x); SetDirection((Direction)((int)inputDirection | (int)directionToCenter)); } SetTargetDirection(inputDirection); // This is the direction most logic should assume pacman is moving, the actual direction may be different due to cornering if (!followingPredefinedPath) { networkManager.SendEventSoon(NetworkEventType.PacManTurn); } } return nextPosition; } private Vector2 ProcessPredefinedPath(Vector2 position, 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 (!pelletManager.IsWallUpcoming(nextPosition, directionVectors[(int)predefinedPath[nextValidDirectionIndex]])) { // If we're at a Vector2.zero, we skip applying the direction and only increment. if (nextValidDirectionIndex == predefinedPathIndex) { SetDirection(predefinedPath[nextValidDirectionIndex]); SetTargetDirection(predefinedPath[nextValidDirectionIndex]); nextPosition = PositionToGrid(nextPosition) + directionVectors[(int)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); } // gameStateManager.statusDisplay.SetDebugText(1, predefinedPathIndex.ToString()); predefinedPathIndex++; } } return nextPosition; } private void CheckNewTile(Vector2 position) { var pellet = pelletManager.CollectPelletAt(position); if (pellet == PelletType.Pellet) { freezeSeconds = 0.0166666666666667f; } else if (pellet == PelletType.PowerPellet) { freezeSeconds = freezeSeconds = 0.05f; } } protected override void UpdateAnimator() { // Debug.Log($"{gameObject} UpdateAnimator with direction {direction}, dead {dead}, frozen {frozen}"); if (!gameObject.activeInHierarchy) return; animator.SetBool(AnimatorKeyDead, dead); if (dead) { animator.speed = 1; return; } if (frozen || direction.Equals(Direction.Zero)) { animator.SetFloat(AnimatorKeyDirection, AnimatorDirectionNone); animator.speed = 0; } else { animator.speed = 1; if (targetDirection.Equals(Direction.Right)) { animator.SetFloat(AnimatorKeyDirection, AnimatorDirectionRight); } else if (targetDirection.Equals(Direction.Left)) { animator.SetFloat(AnimatorKeyDirection, AnimatorDirectionLeft); } else if (targetDirection.Equals(Direction.Down)) { animator.SetFloat(AnimatorKeyDirection, AnimatorDirectionDown); } else if (targetDirection.Equals(Direction.Up)) { animator.SetFloat(AnimatorKeyDirection, AnimatorDirectionUp); } } } public void SetDead(bool dead) { this.dead = dead; UpdateAnimator(); } public void SetFrozen(bool frozen) { this.frozen = frozen; UpdateAnimator(); } public void HideUntilUnfrozen() { hideUntilUnfrozen = true; SetVisibility(false); } public void SetLevel(int level) { // Debug.Log($"{gameObject} SetLevel {level}"); defaultSpeed = PacManConstants.GetPacManDefaultSpeedForLevel(level); powerPelletSpeed = PacManConstants.GetPacManPowerPelletSpeedForLevel(level); } public void SetPowerPellet(bool powerPellet) { if (powerPellet) { speed = powerPelletSpeed; } else { speed = defaultSpeed; } } 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 BecomeBig() { animator.SetFloat(AnimatorKeyDirection, AnimatorDirectionRightBig); } void SetVisibility(bool visible) { renderer.enabled = visible; } public void SetTargetDirection(Direction targetDirection) { this.targetDirection = targetDirection; UpdateAnimator(); } void OnTriggerEnter(Collider other) { if (other.gameObject.GetComponent()) { gameManager.GotFruit(); } } public override void CollectSyncedData(byte[] data, ref int index, NetworkEventType eventType) { if (eventType != NetworkEventType.PacManTurn) { return; } if (kinematic || frozen || !enabled) { index += 1; base.PadSyncedData(data, ref index, eventType); return; } data.AppendAsByte((int)targetDirection, ref index); base.CollectSyncedData(data, ref index, eventType); } public override bool WriteSyncedData(byte[] data, ref int index, NetworkEventType eventType) { if (eventType != NetworkEventType.PacManTurn) { return true; } if (kinematic || frozen || !enabled) { index += 1; base.ConsumeSyncedData(data, ref index, eventType); return true; } SetTargetDirection((Direction)data.ReadByte(ref index)); return base.WriteSyncedData(data, ref index, eventType); } } }