Files
PacManUdon/Assets/Scripts/Ghost.cs
2026-01-01 21:49:12 +01:00

828 lines
30 KiB
C#

#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;
int rngState;
private Vector2 syncedPosition;
private float speed;
private Vector2 target;
private bool offGrid;
private bool inTunnel;
private PacManGhostState ghostState;
private bool isScared;
private bool scattering;
private PacManGhostFrozenState frozenState;
private bool hideUntilUnfrozen;
private int housePelletCounter;
private bool housePelletCounterActive;
private bool turnAroundSoon;
public void Initialize(PacMan pacMan, Ghost blinky, Vector2 homePosition, Vector2 idlePosition1, Vector2 idlePosition2, Vector2 cornerPosition)
{
ghostManager = transform.parent.GetComponent<GhostManager>();
animator = GetComponent<Animator>();
renderer = GetComponent<Renderer>();
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>();
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}");
}
public override 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();
}
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
|| 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();
}
void OnTriggerEnter(Collider other)
{
if (Networking.IsOwner(gameObject) && other.gameObject.GetComponent<PacManGhostCollider>())
{
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<GhostHorizontalOnlyIndicator>())
{
horizontalOnly = true;
}
else if (other.gameObject.GetComponent<GhostTunnelIndicator>())
{
SetInTunnel(true);
}
}
void OnTriggerExit(Collider other)
{
if (other.gameObject.GetComponent<GhostHorizontalOnlyIndicator>())
{
horizontalOnly = false;
}
else if (other.gameObject.GetComponent<GhostTunnelIndicator>())
{
SetInTunnel(false);
}
}
PacManGhostState State
{
set
{
SetState(value);
}
get => ghostState;
}
}
}