namespace Marro.PacManUdon { using System; using UdonSharp; using UnityEngine; using VRC.SDKBase; using VRC.Udon; using VRC.SDK3.Components; public class PacMan : GridMover { private GameManager gameController; private PlayerInput input; private float defaultSpeed; private float powerPelletSpeed; private float speed; private Vector3 startPosition; private Quaternion startRotation; private Vector3 startScale; private Animator animator; new Renderer renderer; private VRCObjectPool pelletPool; private bool hideUntilUnfrozen; private bool dead; private bool kinematic; private bool followingPredefinedPath; private Vector2[] predefinedPath; private int predefinedPathIndex; private Vector2 syncedPosition; private Vector2 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 AnimatorDirectionUp = 0.75f; private const float AnimatorDirectionDown = 1f; private const float AnimatorDirectionRightBig = 1.25f; #endregion public void Initialize(PlayerInput input, VRCObjectPool pelletPool, GameManager gameController) { this.gameController = gameController; this.input = input; this.pelletPool = pelletPool; animator = GetComponent(); renderer = GetComponent(); frozen = false; hideUntilUnfrozen = false; startPosition = transform.localPosition; startRotation = transform.localRotation; startScale = transform.localScale; } public void Reset() { // Debug.Log($"{gameObject} Reset!"); transform.localPosition = startPosition; transform.localRotation = startRotation; transform.localScale = startScale; direction = Vector2.left; targetDirection = Vector2.left; speed = defaultSpeed; kinematic = false; followingPredefinedPath = false; SetDead(false); animator.SetTrigger("Reset"); } public override void FixedUpdate() { // 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 / Time.deltaTime; if (freezePart >= 1) { freezeSeconds -= Time.deltaTime; animator.speed = 0; return; } speed *= 1 - freezePart; animator.speed = 1 - freezePart; freezeSeconds = 0; } else { animator.speed = 1; } Vector2 position = GetPosition(); Vector2 nextPosition = GridMoverTools.GetNextPosition(position, direction, speed); // 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); } } SetPosition(nextPosition); } private Vector2 ProcessNextPosition(Vector2 position, Vector2 nextPosition) { if (GridMoverTools.CrossesTileCenter(position, nextPosition, direction.x != 0, false) // If pacman is moving horizontally, check if he may cross the center of a tile in that axis && (targetDirection.x == 0 || GridMoverTools.CheckCollisionInDirection(transform, nextPosition, new Vector2(direction.x, 0)))) { // If the target direction is in the other axis or if we're about to run into a wall nextPosition.x = GridMoverTools.PositionToGrid(nextPosition).x; // Snap pacman to the center of his current tile in this axis SetDirection(new Vector2(0, direction.y)); // Debug.Log($"{gameObject} crossed X tile center from {currentPosition}, nextPosition is now {nextPosition} and direction is now {direction}"); } if (GridMoverTools.CrossesTileCenter(position, nextPosition, false, direction.y != 0) // See comments above but now vertical && (targetDirection.y == 0 || GridMoverTools.CheckCollisionInDirection(transform, nextPosition, new Vector2(0, direction.y)))) { nextPosition.y = GridMoverTools.PositionToGrid(nextPosition).y; SetDirection(new Vector2(direction.x, 0)); // Debug.Log($"{gameObject} crossed Y tile center from {currentPosition} with targetDirection {targetDirection}, nextPosition is now {nextPosition} and direction is now {direction}"); } if (Networking.IsOwner(gameObject)) { Vector2 inputDirection = input.GetDirection(); if (!inputDirection.Equals(Vector2.zero) && !inputDirection.Equals(targetDirection) // Ignore neutral input or input in our current direction && (inputDirection.x == 0 || (Math.Round(nextPosition.y, 5) - 0.5) % 1 != 0) && (inputDirection.y == 0 || (Math.Round(nextPosition.x, 5) - 0.5) % 1 != 0) // Target grid position near the edge of a tile may not be correct, ignore inputs near the border && !GridMoverTools.CheckCollisionInDirection(transform, nextPosition, inputDirection)) { // Check if the requested direction does not have a wall if (inputDirection.x != 0) { // Move in the requested direction, as well as perpundicular to it to get to the center of the tunnel SetDirection(inputDirection + new Vector2(0, GridMoverTools.PositionToGrid(nextPosition).y - nextPosition.y).normalized); } else { SetDirection(inputDirection + new Vector2(GridMoverTools.PositionToGrid(nextPosition).x - nextPosition.x, 0).normalized); } SetTargetDirection(inputDirection); // This is the direction most logic should assume pacman is moving, the actual direction may be different due to cornering } } 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]); SetTargetDirection(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); } // gameStateManager.statusDisplay.SetDebugText(1, predefinedPathIndex.ToString()); predefinedPathIndex++; } } return nextPosition; } 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(Vector2.zero)) { animator.SetFloat(AnimatorKeyDirection, AnimatorDirectionNone); animator.speed = 0; } else { animator.speed = 1; if (targetDirection.Equals(Vector2.right)) { animator.SetFloat(AnimatorKeyDirection, AnimatorDirectionRight); } else if (targetDirection.Equals(Vector2.left)) { animator.SetFloat(AnimatorKeyDirection, AnimatorDirectionLeft); } else if (targetDirection.Equals(Vector2.up)) { animator.SetFloat(AnimatorKeyDirection, AnimatorDirectionUp); } else if (targetDirection.Equals(Vector2.down)) { animator.SetFloat(AnimatorKeyDirection, AnimatorDirectionDown); } } } 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(Vector2[] 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(Vector2 targetDirection) { this.targetDirection = targetDirection; UpdateAnimator(); } void OnTriggerEnter(Collider other) { Pellet pellet = other.gameObject.GetComponent(); if (pellet) { if (Networking.IsOwner(gameObject)) { pelletPool.Return(pellet.gameObject); } else { pellet.pelletRenderer.enabled = false; pellet.gameObject.SetActive(false); } if (pellet.isPowerPellet) { gameController.GotPowerPellet(); freezeSeconds = 0.05f; } else { gameController.GotPellet(); freezeSeconds = 0.0166666666666667f; } return; } else if (Networking.IsOwner(gameObject) && other.gameObject.GetComponent()) { gameController.GotFruit(); } } } }