chunk 1: core gameplay scripts scenes runtime assets
This commit is contained in:
1
Assets/Scripts/Managers/AddressableSceneLoader.cs
Normal file
1
Assets/Scripts/Managers/AddressableSceneLoader.cs
Normal file
@@ -0,0 +1 @@
|
||||
// Removed. Addressables are no longer used; script kept empty intentionally so Unity won't compile stale references.
|
||||
2
Assets/Scripts/Managers/AddressableSceneLoader.cs.meta
Normal file
2
Assets/Scripts/Managers/AddressableSceneLoader.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3aa314d1b1adf1b5a8cab2ee80e3289e
|
||||
319
Assets/Scripts/Managers/AnimationManager.cs
Normal file
319
Assets/Scripts/Managers/AnimationManager.cs
Normal file
@@ -0,0 +1,319 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Centralized animation manager class that handles playing animations
|
||||
/// and provides an abstraction layer between animation names and animation triggers.
|
||||
/// </summary>
|
||||
public class AnimationManager : MonoBehaviour
|
||||
{
|
||||
private Animator animator;
|
||||
private Dictionary<string, string> animationMappings = new Dictionary<string, string>();
|
||||
|
||||
[SerializeField] private float defaultTransitionDuration = 0.15f; // Default cross fade time
|
||||
[SerializeField] private float reactionTransitionDuration = 0.05f; // Faster transition for reactions
|
||||
|
||||
// Track the current animation state
|
||||
private string currentAnimationName;
|
||||
private float hitAlignmentOffset = 0.1f; // Small offset to align hit reactions precisely
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
animator = GetComponent<Animator>();
|
||||
InitializeAnimationMappings();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the mappings between logical animation names and actual animation triggers.
|
||||
/// This is where you can update animation names without changing the rest of the codebase.
|
||||
/// </summary>
|
||||
private void InitializeAnimationMappings()
|
||||
{
|
||||
// By default, map each animation name to itself
|
||||
foreach (var field in typeof(AnimationNames).GetFields())
|
||||
{
|
||||
if (field.IsLiteral && !field.IsInitOnly)
|
||||
{
|
||||
string animName = (string)field.GetValue(null);
|
||||
animationMappings[animName] = animName;
|
||||
}
|
||||
}
|
||||
|
||||
// MAP OLD ANIMATIONS TO NEW ANIMATIONS
|
||||
|
||||
// ACTIVE ANIMATIONS - These are the only ones that will work
|
||||
// Basic Punch Moves - Keep only specified animations
|
||||
animationMappings[AnimationNames.Jab] = AnimationNames.Jab;
|
||||
animationMappings[AnimationNames.Haymaker] = AnimationNames.Haymaker;
|
||||
animationMappings[AnimationNames.SpinningBackfist] = AnimationNames.SpinningBackfist;
|
||||
animationMappings[AnimationNames.ElbowSmash] = AnimationNames.ElbowSmash;
|
||||
animationMappings[AnimationNames.AirPunch] = AnimationNames.AirPunch;
|
||||
|
||||
// Basic Kick Moves - Keep only specified animations
|
||||
animationMappings[AnimationNames.LowKick] = AnimationNames.LowKick;
|
||||
animationMappings[AnimationNames.BasicKick] = AnimationNames.BasicKick;
|
||||
|
||||
// Add SumoSlap from vicious moves to active list
|
||||
animationMappings[AnimationNames.SumoSlap] = AnimationNames.SumoSlap;
|
||||
|
||||
// Weapon Animations - active when weapon equipped
|
||||
animationMappings[AnimationNames.BatOne] = AnimationNames.BatOne;
|
||||
animationMappings[AnimationNames.BatTwo] = AnimationNames.BatTwo;
|
||||
animationMappings[AnimationNames.HammerOne] = AnimationNames.HammerOne;
|
||||
animationMappings[AnimationNames.HammerTwo] = AnimationNames.HammerTwo;
|
||||
// Pickup animation
|
||||
animationMappings[AnimationNames.WeaponPickup] = AnimationNames.WeaponPickup;
|
||||
|
||||
// Keep reaction animations for the active moves
|
||||
animationMappings[AnimationNames.JabReaction] = AnimationNames.JabReaction;
|
||||
animationMappings[AnimationNames.HaymakerReaction] = AnimationNames.HaymakerReaction;
|
||||
animationMappings[AnimationNames.SpinningBackfistReaction] = AnimationNames.SpinningBackfistReaction;
|
||||
animationMappings[AnimationNames.ElbowSmashReaction] = AnimationNames.ElbowSmashReaction;
|
||||
animationMappings[AnimationNames.AirPunchReaction] = AnimationNames.AirPunchReaction;
|
||||
animationMappings[AnimationNames.LowKickReaction] = AnimationNames.LowKickReaction;
|
||||
animationMappings[AnimationNames.SumoSlapReaction] = AnimationNames.SumoSlapReaction;
|
||||
|
||||
// Map Input Actions to Animation Names for active animations
|
||||
animationMappings["Punch"] = AnimationNames.Jab;
|
||||
animationMappings["Strike"] = AnimationNames.Haymaker;
|
||||
animationMappings["Kick"] = AnimationNames.LowKick;
|
||||
|
||||
// Direct mappings for active animations
|
||||
animationMappings["Jab"] = AnimationNames.Jab;
|
||||
animationMappings["Haymaker"] = AnimationNames.Haymaker;
|
||||
animationMappings["SpinningBackfist"] = AnimationNames.SpinningBackfist;
|
||||
animationMappings["ElbowSmash"] = AnimationNames.ElbowSmash;
|
||||
animationMappings["AirPunch"] = AnimationNames.AirPunch;
|
||||
animationMappings["LowKick"] = AnimationNames.LowKick;
|
||||
animationMappings["BasicKick"] = AnimationNames.BasicKick;
|
||||
animationMappings["SumoSlap"] = AnimationNames.SumoSlap;
|
||||
|
||||
// COMMENTED ANIMATIONS - Kept for future implementation
|
||||
/*
|
||||
// Basic Punch Moves - Map old punches to new animations
|
||||
animationMappings[AnimationNames.Right] = AnimationNames.Haymaker;
|
||||
animationMappings[AnimationNames.LeftHook] = AnimationNames.Chop;
|
||||
animationMappings[AnimationNames.RightHook] = AnimationNames.SpinningBackfist;
|
||||
animationMappings[AnimationNames.LeftElbow] = AnimationNames.ElbowSmash;
|
||||
animationMappings[AnimationNames.RightElbow] = AnimationNames.ChargePunch;
|
||||
animationMappings[AnimationNames.RightBody] = AnimationNames.GroundPound;
|
||||
animationMappings[AnimationNames.PowerPunchLeft] = AnimationNames.Uppercut;
|
||||
animationMappings[AnimationNames.SupermanPunch] = AnimationNames.AirPunch;
|
||||
animationMappings[AnimationNames.SpecialBackfist] = AnimationNames.SpinningBackfist;
|
||||
animationMappings[AnimationNames.Chop] = AnimationNames.Chop;
|
||||
animationMappings[AnimationNames.GroundPound] = AnimationNames.GroundPound;
|
||||
animationMappings[AnimationNames.Uppercut] = AnimationNames.Uppercut;
|
||||
|
||||
// Basic Kick Moves - Map old kicks to new animations
|
||||
animationMappings[AnimationNames.FrontKickRight] = AnimationNames.DropKick;
|
||||
animationMappings[AnimationNames.BodyKickRight] = AnimationNames.AirKick;
|
||||
animationMappings[AnimationNames.KneeRight] = AnimationNames.LowKick;
|
||||
animationMappings[AnimationNames.BackSideKick] = AnimationNames.Claymore;
|
||||
animationMappings[AnimationNames.LegKickRight] = AnimationNames.LowKick;
|
||||
animationMappings[AnimationNames.LegPowerKickRight] = AnimationNames.SuperKick;
|
||||
animationMappings[AnimationNames.AirKick] = AnimationNames.AirKick;
|
||||
animationMappings[AnimationNames.Claymore] = AnimationNames.Claymore;
|
||||
animationMappings[AnimationNames.SuperKick] = AnimationNames.SuperKick;
|
||||
|
||||
// Hit Reactions - Map old hit reactions to new hit reactions
|
||||
animationMappings[AnimationNames.RightHookHitReaction] = AnimationNames.SpinningBackfistReaction;
|
||||
animationMappings[AnimationNames.BodyKickHitReaction] = AnimationNames.AirKickReaction;
|
||||
animationMappings[AnimationNames.HookHitReaction] = AnimationNames.ChopReaction;
|
||||
animationMappings[AnimationNames.JabHitReaction] = AnimationNames.JabReaction;
|
||||
animationMappings[AnimationNames.LegKickHitReaction] = AnimationNames.LowKickReaction;
|
||||
animationMappings[AnimationNames.UppercutPowerHitReaction] = AnimationNames.HaymakerReaction;
|
||||
animationMappings[AnimationNames.OverhandHitReaction] = AnimationNames.GroundPoundReaction;
|
||||
animationMappings[AnimationNames.ChopReaction] = AnimationNames.ChopReaction;
|
||||
animationMappings[AnimationNames.GroundPoundReaction] = AnimationNames.GroundPoundReaction;
|
||||
animationMappings[AnimationNames.AirKickReaction] = AnimationNames.AirKickReaction;
|
||||
|
||||
// Jump Moves
|
||||
animationMappings["Jump"] = AnimationNames.JumpBack;
|
||||
animationMappings[AnimationNames.JumpBack] = AnimationNames.JumpBack;
|
||||
animationMappings[AnimationNames.JumpLeft] = AnimationNames.JumpLeft;
|
||||
animationMappings[AnimationNames.JumpRight] = AnimationNames.JumpRight;
|
||||
|
||||
// Block Moves
|
||||
animationMappings[AnimationNames.BlockStepBack] = AnimationNames.BlockStepBack;
|
||||
animationMappings[AnimationNames.BlockBodyLeft] = AnimationNames.BlockBodyLeft;
|
||||
animationMappings[AnimationNames.BlockLeg] = AnimationNames.BlockLeg;
|
||||
animationMappings[AnimationNames.Block] = AnimationNames.Block;
|
||||
animationMappings["Block"] = AnimationNames.Block;
|
||||
|
||||
// Other movement
|
||||
animationMappings["Sprint"] = "Sprint";
|
||||
|
||||
// Special Moves and Finishers - Map all special moves from the input system
|
||||
animationMappings[AnimationNames.Clothesline] = AnimationNames.Clothesline;
|
||||
animationMappings[AnimationNames.Spear] = AnimationNames.Spear;
|
||||
animationMappings[AnimationNames.RKO] = AnimationNames.RKO;
|
||||
animationMappings[AnimationNames.Claymore] = AnimationNames.Claymore;
|
||||
animationMappings[AnimationNames.SuperDDT] = AnimationNames.SuperDDT;
|
||||
animationMappings[AnimationNames.RockBottom] = AnimationNames.RockBottom;
|
||||
animationMappings[AnimationNames.TroubleInParadise] = AnimationNames.TroubleInParadise;
|
||||
animationMappings[AnimationNames.SwantonBomb] = AnimationNames.SwantonBomb;
|
||||
animationMappings[AnimationNames.Pedigree] = AnimationNames.Pedigree;
|
||||
animationMappings[AnimationNames.GorillaPress] = AnimationNames.GorillaPress;
|
||||
animationMappings[AnimationNames.Headbutt] = AnimationNames.Headbutt;
|
||||
animationMappings[AnimationNames.KOPunch] = AnimationNames.KOPunch;
|
||||
*/
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Play an animation by name, using the mapping to find the actual animation to play.
|
||||
/// Uses cross-fade for smooth transitions between animations.
|
||||
/// </summary>
|
||||
/// <param name="animationName">The logical animation name (from AnimationNames class)</param>
|
||||
public void PlayAnimation(string animationName)
|
||||
{
|
||||
if (animator == null)
|
||||
{
|
||||
Debug.LogError("Animator component is missing");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!AnimationExists(animationName))
|
||||
{
|
||||
Debug.LogWarning($"Animation '{animationName}' does not exist in the animator");
|
||||
return;
|
||||
}
|
||||
|
||||
string mappedAnimation;
|
||||
if (animationMappings.TryGetValue(animationName, out mappedAnimation))
|
||||
{
|
||||
// Store current animation
|
||||
currentAnimationName = mappedAnimation;
|
||||
|
||||
// Determine if this is a reaction animation
|
||||
bool isReactionAnimation = IsReactionAnimation(mappedAnimation);
|
||||
|
||||
// Choose transition duration based on animation type
|
||||
float transitionDuration = isReactionAnimation ? reactionTransitionDuration : defaultTransitionDuration;
|
||||
|
||||
// Use CrossFade for smoother transitions
|
||||
animator.CrossFade(mappedAnimation, transitionDuration, 0, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback to playing the animation directly
|
||||
animator.CrossFade(animationName, defaultTransitionDuration, 0, 0);
|
||||
currentAnimationName = animationName;
|
||||
Debug.LogWarning($"No mapping found for animation: {animationName}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Play a reaction animation with proper alignment to the attack.
|
||||
/// </summary>
|
||||
/// <param name="reactionName">The reaction animation to play</param>
|
||||
public void PlayReactionAnimation(string reactionName)
|
||||
{
|
||||
if (animator == null)
|
||||
{
|
||||
Debug.LogError("Animator component is missing");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!AnimationExists(reactionName))
|
||||
{
|
||||
Debug.LogWarning($"Reaction animation '{reactionName}' does not exist in the animator");
|
||||
return;
|
||||
}
|
||||
|
||||
string mappedReaction;
|
||||
if (animationMappings.TryGetValue(reactionName, out mappedReaction))
|
||||
{
|
||||
// Use a very quick transition for reactions to make them look more impactful
|
||||
animator.CrossFade(mappedReaction, reactionTransitionDuration, 0, hitAlignmentOffset);
|
||||
currentAnimationName = mappedReaction;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback
|
||||
animator.CrossFade(reactionName, reactionTransitionDuration, 0, hitAlignmentOffset);
|
||||
currentAnimationName = reactionName;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if an animation exists in the animator controller.
|
||||
/// </summary>
|
||||
private bool AnimationExists(string animationName)
|
||||
{
|
||||
if (animator == null)
|
||||
return false;
|
||||
|
||||
// Get all animation clips from the animator controller
|
||||
RuntimeAnimatorController ac = animator.runtimeAnimatorController;
|
||||
if (ac == null)
|
||||
return false;
|
||||
|
||||
AnimationClip[] clips = ac.animationClips;
|
||||
foreach (AnimationClip clip in clips)
|
||||
{
|
||||
if (clip.name == animationName)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the animation is a reaction animation.
|
||||
/// </summary>
|
||||
private bool IsReactionAnimation(string animationName)
|
||||
{
|
||||
return animationName.Contains("Reaction") ||
|
||||
animationName.Contains("Hit") ||
|
||||
animationName.Contains("Stagger") ||
|
||||
animationName.Contains("Block");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the mapped animation name for a logical animation name.
|
||||
/// </summary>
|
||||
/// <param name="animationName">The logical animation name</param>
|
||||
/// <returns>The mapped animation name</returns>
|
||||
public string GetMappedAnimationName(string animationName)
|
||||
{
|
||||
string mappedAnimation;
|
||||
if (animationMappings.TryGetValue(animationName, out mappedAnimation))
|
||||
{
|
||||
return mappedAnimation;
|
||||
}
|
||||
return animationName; // Fallback to the original animation name
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update a specific animation mapping.
|
||||
/// </summary>
|
||||
/// <param name="logicalName">The logical animation name</param>
|
||||
/// <param name="actualAnimationName">The actual animation name to play</param>
|
||||
public void UpdateAnimationMapping(string logicalName, string actualAnimationName)
|
||||
{
|
||||
animationMappings[logicalName] = actualAnimationName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if the current animation matches any of the specified animations.
|
||||
/// </summary>
|
||||
/// <param name="animationNames">Array of animation names to check</param>
|
||||
/// <returns>True if the current animation matches any of the specified animations</returns>
|
||||
public bool IsPlayingAnimation(params string[] animationNames)
|
||||
{
|
||||
if (animator == null || animator.GetCurrentAnimatorClipInfo(0).Length == 0)
|
||||
return false;
|
||||
|
||||
string currentAnimName = animator.GetCurrentAnimatorClipInfo(0)[0].clip.name;
|
||||
|
||||
foreach (var animName in animationNames)
|
||||
{
|
||||
string mappedAnim = GetMappedAnimationName(animName);
|
||||
if (currentAnimName == mappedAnim)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Managers/AnimationManager.cs.meta
Normal file
2
Assets/Scripts/Managers/AnimationManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 27a6962d2a083a2b4a30f1bd766e29ea
|
||||
136
Assets/Scripts/Managers/AudioListenerManager.cs
Normal file
136
Assets/Scripts/Managers/AudioListenerManager.cs
Normal file
@@ -0,0 +1,136 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// AudioListenerManager - Ensures there's always an active AudioListener in the game scene.
|
||||
/// Place this on a persistent game object (like the main camera or a manager object).
|
||||
/// </summary>
|
||||
public class AudioListenerManager : MonoBehaviour
|
||||
{
|
||||
[Header("Settings")]
|
||||
[SerializeField] private bool createListenerIfMissing = true;
|
||||
|
||||
private AudioListener sceneAudioListener;
|
||||
|
||||
private static AudioListenerManager instance;
|
||||
public static AudioListenerManager Instance => instance;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
// Singleton pattern
|
||||
if (instance != null && instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
instance = this;
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
EnsureAudioListener();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// Periodically check that we have an active listener
|
||||
if (sceneAudioListener == null || !sceneAudioListener.enabled)
|
||||
{
|
||||
EnsureAudioListener();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensure there's an active AudioListener in the scene
|
||||
/// </summary>
|
||||
public void EnsureAudioListener()
|
||||
{
|
||||
// First check if any AudioListener is already active
|
||||
AudioListener[] allListeners = FindObjectsOfType<AudioListener>();
|
||||
|
||||
bool foundActive = false;
|
||||
foreach (AudioListener listener in allListeners)
|
||||
{
|
||||
if (listener.enabled && listener.gameObject.activeInHierarchy)
|
||||
{
|
||||
sceneAudioListener = listener;
|
||||
foundActive = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (foundActive)
|
||||
{
|
||||
Debug.Log($"[AudioListenerManager] Found active AudioListener on: {sceneAudioListener.gameObject.name}");
|
||||
return;
|
||||
}
|
||||
|
||||
// No active listener found - try to enable one
|
||||
if (allListeners.Length > 0)
|
||||
{
|
||||
// Enable the first available one
|
||||
allListeners[0].enabled = true;
|
||||
sceneAudioListener = allListeners[0];
|
||||
Debug.Log($"[AudioListenerManager] Enabled existing AudioListener on: {sceneAudioListener.gameObject.name}");
|
||||
return;
|
||||
}
|
||||
|
||||
// No AudioListener exists at all - create one
|
||||
if (createListenerIfMissing)
|
||||
{
|
||||
// Try to add to main camera first
|
||||
Camera mainCam = Camera.main;
|
||||
if (mainCam != null)
|
||||
{
|
||||
sceneAudioListener = mainCam.gameObject.GetComponent<AudioListener>();
|
||||
if (sceneAudioListener == null)
|
||||
{
|
||||
sceneAudioListener = mainCam.gameObject.AddComponent<AudioListener>();
|
||||
}
|
||||
sceneAudioListener.enabled = true;
|
||||
Debug.Log("[AudioListenerManager] Added AudioListener to Main Camera");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Add to this manager object
|
||||
sceneAudioListener = GetComponent<AudioListener>();
|
||||
if (sceneAudioListener == null)
|
||||
{
|
||||
sceneAudioListener = gameObject.AddComponent<AudioListener>();
|
||||
}
|
||||
sceneAudioListener.enabled = true;
|
||||
Debug.Log("[AudioListenerManager] Added AudioListener to AudioListenerManager");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("[AudioListenerManager] No AudioListener found in scene and createListenerIfMissing is disabled!");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Force refresh the audio listener (call after spawning player characters)
|
||||
/// </summary>
|
||||
public void RefreshAudioListener()
|
||||
{
|
||||
sceneAudioListener = null;
|
||||
EnsureAudioListener();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set a specific AudioListener as the active one
|
||||
/// </summary>
|
||||
public void SetActiveListener(AudioListener listener)
|
||||
{
|
||||
if (listener == null) return;
|
||||
|
||||
// Disable all other listeners
|
||||
AudioListener[] allListeners = FindObjectsOfType<AudioListener>();
|
||||
foreach (AudioListener l in allListeners)
|
||||
{
|
||||
l.enabled = (l == listener);
|
||||
}
|
||||
|
||||
sceneAudioListener = listener;
|
||||
Debug.Log($"[AudioListenerManager] Set active listener to: {listener.gameObject.name}");
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Managers/AudioListenerManager.cs.meta
Normal file
2
Assets/Scripts/Managers/AudioListenerManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c352b33c955598f64b618e19551c15ea
|
||||
449
Assets/Scripts/Managers/CameraManager.cs
Normal file
449
Assets/Scripts/Managers/CameraManager.cs
Normal file
@@ -0,0 +1,449 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using Cinemachine;
|
||||
|
||||
|
||||
public enum SinglePlayerCameraMode
|
||||
{
|
||||
Stationary,
|
||||
FollowAndOrbit
|
||||
}
|
||||
|
||||
public class CameraManager : Singleton<CameraManager>
|
||||
{
|
||||
[Header("Component References")]
|
||||
public GameObject gameplayCameraObject;
|
||||
public GameObject uiOverlayCameraObject;
|
||||
|
||||
[Header("Virtual Cameras")]
|
||||
public GameObject VCamStationaryObject;
|
||||
public GameObject VCamSinglePlayerOrbitObject;
|
||||
|
||||
[Header("Cinemachine References")]
|
||||
[SerializeField] private CinemachineFreeLook mainFreeLookCamera;
|
||||
[SerializeField] private CinemachineVirtualCamera mainVirtualCamera;
|
||||
|
||||
[Header("Current Target")]
|
||||
[SerializeField] private TeamMember currentTarget;
|
||||
[SerializeField] private bool debugLogging = true;
|
||||
|
||||
public void SetupManager()
|
||||
{
|
||||
SetCameraObjectNewState(gameplayCameraObject, true);
|
||||
SetCameraObjectNewState(uiOverlayCameraObject, false);
|
||||
|
||||
// Find camera references if not set
|
||||
FindCameraReferences();
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
FindCameraReferences();
|
||||
SubscribeToAllTeamMembers();
|
||||
}
|
||||
|
||||
private void FindCameraReferences()
|
||||
{
|
||||
if (mainFreeLookCamera == null)
|
||||
{
|
||||
GameObject camObj = GameObject.Find("CinemachineCamera");
|
||||
if (camObj != null)
|
||||
{
|
||||
mainFreeLookCamera = camObj.GetComponent<CinemachineFreeLook>();
|
||||
}
|
||||
}
|
||||
|
||||
if (mainVirtualCamera == null)
|
||||
{
|
||||
mainVirtualCamera = FindObjectOfType<CinemachineVirtualCamera>();
|
||||
}
|
||||
}
|
||||
|
||||
public void SetupSinglePlayerCamera(SinglePlayerCameraMode currentCameraMode)
|
||||
{
|
||||
switch (currentCameraMode)
|
||||
{
|
||||
case SinglePlayerCameraMode.Stationary:
|
||||
SetCameraObjectNewState(VCamStationaryObject, true);
|
||||
SetCameraObjectNewState(VCamSinglePlayerOrbitObject, false);
|
||||
break;
|
||||
|
||||
case SinglePlayerCameraMode.FollowAndOrbit:
|
||||
SetCameraObjectNewState(VCamStationaryObject, false);
|
||||
SetCameraObjectNewState(VCamSinglePlayerOrbitObject, true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void SetCameraObjectNewState(GameObject cameraObject, bool newState)
|
||||
{
|
||||
if (cameraObject != null)
|
||||
cameraObject.SetActive(newState);
|
||||
}
|
||||
|
||||
//This is called by UIBillboardBehaviour so they can orient to wherever the gameplay camera is.
|
||||
public Transform GetGameplayCameraTransform()
|
||||
{
|
||||
return gameplayCameraObject?.transform;
|
||||
}
|
||||
|
||||
public Camera GetGameplayCamera()
|
||||
{
|
||||
return gameplayCameraObject?.GetComponent<Camera>();
|
||||
}
|
||||
|
||||
// ==================== NEW: ControlType-aware Camera Targeting ====================
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to OnControlTypeChanged for all TeamMembers in the scene
|
||||
/// </summary>
|
||||
private void SubscribeToAllTeamMembers()
|
||||
{
|
||||
TeamMember[] allMembers = FindObjectsOfType<TeamMember>();
|
||||
foreach (var member in allMembers)
|
||||
{
|
||||
member.OnControlTypeChanged += OnMemberControlTypeChanged;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register a newly spawned TeamMember
|
||||
/// </summary>
|
||||
public void RegisterTeamMember(TeamMember member)
|
||||
{
|
||||
if (member != null)
|
||||
{
|
||||
member.OnControlTypeChanged += OnMemberControlTypeChanged;
|
||||
|
||||
// If this member is HumanControlled, set as target
|
||||
if (member.CurrentControlType == TeamMember.ControlType.HumanControlled)
|
||||
{
|
||||
SetTarget(member);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unregister a TeamMember (e.g., on death)
|
||||
/// </summary>
|
||||
public void UnregisterTeamMember(TeamMember member)
|
||||
{
|
||||
if (member != null)
|
||||
{
|
||||
member.OnControlTypeChanged -= OnMemberControlTypeChanged;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when any TeamMember's control type changes
|
||||
/// </summary>
|
||||
private void OnMemberControlTypeChanged(TeamMember.ControlType newType)
|
||||
{
|
||||
// Find the new human-controlled character
|
||||
if (newType == TeamMember.ControlType.HumanControlled)
|
||||
{
|
||||
FindAndSetHumanControlledTarget();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find the HumanControlled character and set as camera target
|
||||
/// </summary>
|
||||
public void FindAndSetHumanControlledTarget()
|
||||
{
|
||||
TeamMember[] allMembers = FindObjectsOfType<TeamMember>();
|
||||
foreach (var member in allMembers)
|
||||
{
|
||||
if (member.CurrentControlType == TeamMember.ControlType.HumanControlled)
|
||||
{
|
||||
SetTarget(member);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (debugLogging)
|
||||
{
|
||||
Debug.LogWarning("[CameraManager] No HumanControlled character found in scene!");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the camera target to a specific TeamMember
|
||||
/// </summary>
|
||||
public void SetTarget(TeamMember target)
|
||||
{
|
||||
if (target == null)
|
||||
{
|
||||
Debug.LogWarning("[CameraManager] Attempted to set null target");
|
||||
return;
|
||||
}
|
||||
|
||||
currentTarget = target;
|
||||
|
||||
// Use root for Follow (stable orbit center)
|
||||
Transform followTransform = target.transform;
|
||||
// Use head for LookAt (camera points at upper body)
|
||||
Transform lookAtTransform = FindCharacterHead(target.gameObject);
|
||||
|
||||
// Set FreeLook camera target
|
||||
if (mainFreeLookCamera != null)
|
||||
{
|
||||
mainFreeLookCamera.Follow = followTransform;
|
||||
mainFreeLookCamera.LookAt = lookAtTransform;
|
||||
|
||||
// CRITICAL: Tell Cinemachine to forget its cached (underground) position
|
||||
// and recalculate from scratch based on the new Follow target position
|
||||
mainFreeLookCamera.PreviousStateIsValid = false;
|
||||
|
||||
// Configure camera to automatically stay behind player
|
||||
ConfigureCameraRecentering();
|
||||
|
||||
if (debugLogging)
|
||||
{
|
||||
Debug.Log($"[CameraManager] FreeLook camera - Follow: {followTransform.name}, LookAt: {lookAtTransform.name} (state reset)");
|
||||
}
|
||||
}
|
||||
|
||||
// Set VirtualCamera target
|
||||
if (mainVirtualCamera != null)
|
||||
{
|
||||
mainVirtualCamera.Follow = followTransform;
|
||||
mainVirtualCamera.LookAt = lookAtTransform;
|
||||
}
|
||||
|
||||
// Also update CameraFollow if present
|
||||
CameraFollow cameraFollow = FindObjectOfType<CameraFollow>();
|
||||
if (cameraFollow != null)
|
||||
{
|
||||
cameraFollow.target = lookAtTransform;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configure camera to automatically recenter behind the player.
|
||||
/// Also initializes orbit heights, Y axis position, and Composer aim settings
|
||||
/// to ensure the camera starts BEHIND and ABOVE the character, never below.
|
||||
/// </summary>
|
||||
private void ConfigureCameraRecentering()
|
||||
{
|
||||
if (mainFreeLookCamera == null) return;
|
||||
|
||||
// ====== FIX #1: Start at MIDDLE rig (0.5), not bottom rig (0) ======
|
||||
// Y Axis: 0 = bottom rig, 0.5 = middle rig, 1 = top rig
|
||||
// Without this, camera defaults to bottom rig and looks UP at the character
|
||||
mainFreeLookCamera.m_YAxis.Value = 0.5f;
|
||||
|
||||
// CRITICAL: Clear default input axis names — CinemachineCameraInput handles all input.
|
||||
// Without this, Cinemachine also reads Input.GetAxis("Mouse X") which returns 0
|
||||
// and snaps the Y axis to the bottom rig (= camera underground).
|
||||
mainFreeLookCamera.m_XAxis.m_InputAxisName = "";
|
||||
mainFreeLookCamera.m_YAxis.m_InputAxisName = "";
|
||||
|
||||
// ====== FIX #2: Set proper orbit heights and radii ======
|
||||
// These define the 3 orbital rings the camera blends between.
|
||||
// Height = vertical offset from Follow target (character root)
|
||||
// Radius = horizontal distance from character
|
||||
mainFreeLookCamera.m_Orbits = new CinemachineFreeLook.Orbit[] {
|
||||
new CinemachineFreeLook.Orbit(4.5f, 5f), // TopRig: bird's eye, medium distance
|
||||
new CinemachineFreeLook.Orbit(2.5f, 6f), // MiddleRig: shoulder height, wider orbit
|
||||
new CinemachineFreeLook.Orbit(0.5f, 5f), // BottomRig: just above ground level
|
||||
};
|
||||
|
||||
// Set binding mode to follow player's heading (critical for auto-follow)
|
||||
// "SimpleFollowWithWorldUp" makes the camera orbit relative to where the player is facing
|
||||
mainFreeLookCamera.m_BindingMode = CinemachineTransposer.BindingMode.SimpleFollowWithWorldUp;
|
||||
|
||||
// Configure Heading - this is how the camera knows which direction is "behind" the player
|
||||
// Velocity-based heading follows the direction the player is moving
|
||||
mainFreeLookCamera.m_Heading.m_Definition = CinemachineOrbitalTransposer.Heading.HeadingDefinition.Velocity;
|
||||
mainFreeLookCamera.m_Heading.m_VelocityFilterStrength = 5; // Smooth out velocity changes
|
||||
|
||||
// Try to enable X Axis recentering (works in Cinemachine 2.x)
|
||||
try
|
||||
{
|
||||
mainFreeLookCamera.m_XAxis.m_Recentering.m_enabled = true;
|
||||
mainFreeLookCamera.m_XAxis.m_Recentering.m_WaitTime = 0.3f;
|
||||
mainFreeLookCamera.m_XAxis.m_Recentering.m_RecenteringTime = 0.8f;
|
||||
}
|
||||
catch (System.Exception)
|
||||
{
|
||||
Debug.Log("[CameraManager] X Axis recentering not available via m_XAxis.m_Recentering");
|
||||
}
|
||||
|
||||
// Configure Y Axis (vertical tilt) recentering
|
||||
mainFreeLookCamera.m_YAxis.m_Recentering.m_enabled = true;
|
||||
mainFreeLookCamera.m_YAxis.m_Recentering.m_WaitTime = 0.3f;
|
||||
mainFreeLookCamera.m_YAxis.m_Recentering.m_RecenteringTime = 0.8f;
|
||||
|
||||
// ====== FIX #3: Configure Composer aim on each rig ======
|
||||
// This controls WHERE on screen the character appears.
|
||||
// ScreenY = 0.4 means character is at 40% from bottom (slightly above center)
|
||||
// This prevents the camera from tilting upward to look at the character
|
||||
ConfigureRigComposers();
|
||||
|
||||
// Configure heading settings on each rig's orbital transposer
|
||||
ConfigureRigHeadings();
|
||||
|
||||
if (debugLogging)
|
||||
{
|
||||
Debug.Log("[CameraManager] Camera fully initialized:");
|
||||
Debug.Log($" Y Axis = {mainFreeLookCamera.m_YAxis.Value} (0.5 = middle rig)");
|
||||
Debug.Log($" Orbits: Top({mainFreeLookCamera.m_Orbits[0].m_Height}h, {mainFreeLookCamera.m_Orbits[0].m_Radius}r) " +
|
||||
$"Mid({mainFreeLookCamera.m_Orbits[1].m_Height}h, {mainFreeLookCamera.m_Orbits[1].m_Radius}r) " +
|
||||
$"Bot({mainFreeLookCamera.m_Orbits[2].m_Height}h, {mainFreeLookCamera.m_Orbits[2].m_Radius}r)");
|
||||
Debug.Log($" Heading: {mainFreeLookCamera.m_Heading.m_Definition}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configure the Composer aim component on each rig.
|
||||
/// This sets WHERE on screen the character is framed.
|
||||
/// Without this, ScreenY defaults to 0.27 which makes the camera look UP.
|
||||
/// Pattern from WaitingSceneManager which correctly sets these values.
|
||||
/// </summary>
|
||||
private void ConfigureRigComposers()
|
||||
{
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
var rig = mainFreeLookCamera.GetRig(i);
|
||||
if (rig == null) continue;
|
||||
|
||||
var composer = rig.GetCinemachineComponent<CinemachineComposer>();
|
||||
if (composer != null)
|
||||
{
|
||||
composer.m_ScreenX = 0.5f; // Character centered horizontally
|
||||
composer.m_ScreenY = 0.4f; // Character at 40% — standard third person
|
||||
composer.m_DeadZoneWidth = 0.1f; // Small dead zone for responsive aiming
|
||||
composer.m_DeadZoneHeight = 0.1f;
|
||||
composer.m_SoftZoneWidth = 0.8f; // Wide soft zone for smooth transitions
|
||||
composer.m_SoftZoneHeight = 0.8f;
|
||||
composer.m_LookaheadTime = 0f; // No lookahead for combat (responsive)
|
||||
composer.m_LookaheadSmoothing = 0f;
|
||||
|
||||
if (debugLogging)
|
||||
{
|
||||
string rigName = i == 0 ? "Top" : (i == 1 ? "Middle" : "Bottom");
|
||||
Debug.Log($"[CameraManager] {rigName} rig Composer: ScreenY={composer.m_ScreenY}, ScreenX={composer.m_ScreenX}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configure heading settings on each rig's orbital transposer
|
||||
/// </summary>
|
||||
private void ConfigureRigHeadings()
|
||||
{
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
var rig = mainFreeLookCamera.GetRig(i);
|
||||
if (rig != null)
|
||||
{
|
||||
var transposer = rig.GetCinemachineComponent<CinemachineOrbitalTransposer>();
|
||||
if (transposer != null)
|
||||
{
|
||||
// Enable recenter to target heading on each rig
|
||||
transposer.m_RecenterToTargetHeading.m_enabled = true;
|
||||
transposer.m_RecenterToTargetHeading.m_WaitTime = 0.3f;
|
||||
transposer.m_RecenterToTargetHeading.m_RecenteringTime = 0.8f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set target by GameObject (convenience method)
|
||||
/// </summary>
|
||||
public void SetTarget(GameObject target)
|
||||
{
|
||||
if (target == null) return;
|
||||
|
||||
TeamMember member = target.GetComponent<TeamMember>();
|
||||
if (member != null)
|
||||
{
|
||||
SetTarget(member);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback: set camera directly with correct separation
|
||||
Transform followTransform = target.transform; // Root = orbit center
|
||||
Transform lookAtTransform = FindCharacterHead(target); // Head = aim point
|
||||
if (mainFreeLookCamera != null)
|
||||
{
|
||||
mainFreeLookCamera.Follow = followTransform;
|
||||
mainFreeLookCamera.LookAt = lookAtTransform;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the currently followed character
|
||||
/// </summary>
|
||||
public TeamMember GetCurrentTarget()
|
||||
{
|
||||
return currentTarget;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find the character's head transform for camera targeting
|
||||
/// </summary>
|
||||
private Transform FindCharacterHead(GameObject character)
|
||||
{
|
||||
if (character == null) return null;
|
||||
|
||||
// Try to find common head bone names
|
||||
string[] headNames = { "mixamorig:Head", "Head", "head", "Bip001 Head", "Bone_Head", "head_bone" };
|
||||
|
||||
foreach (string headName in headNames)
|
||||
{
|
||||
Transform head = FindChildRecursive(character.transform, headName);
|
||||
if (head != null)
|
||||
{
|
||||
if (debugLogging)
|
||||
{
|
||||
Debug.Log($"[CameraManager] Found head bone '{headName}' for {character.name}");
|
||||
}
|
||||
return head;
|
||||
}
|
||||
}
|
||||
|
||||
// Try character-specific paths
|
||||
string characterName = character.name;
|
||||
try
|
||||
{
|
||||
Transform headTransform = null;
|
||||
if (characterName.Contains("Cheetah"))
|
||||
headTransform = character.transform.GetChild(9)?.GetChild(6)?.GetChild(1);
|
||||
else if (characterName.Contains("Rabbit") || characterName.Contains("Eagle") ||
|
||||
characterName.Contains("Hyena") || characterName.Contains("HM") ||
|
||||
characterName.Contains("Cat"))
|
||||
headTransform = character.transform.GetChild(7)?.GetChild(6)?.GetChild(1);
|
||||
|
||||
if (headTransform != null)
|
||||
{
|
||||
if (debugLogging)
|
||||
{
|
||||
Debug.Log($"[CameraManager] Found head via hardcoded path for {characterName}: {headTransform.name}");
|
||||
}
|
||||
return headTransform;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
// Final fallback - log warning
|
||||
Debug.LogWarning($"[CameraManager] Could not find head bone for {characterName}, using root transform. This will cause camera to show legs!");
|
||||
return character.transform;
|
||||
}
|
||||
|
||||
private Transform FindChildRecursive(Transform parent, string name)
|
||||
{
|
||||
if (parent.name == name) return parent;
|
||||
foreach (Transform child in parent)
|
||||
{
|
||||
Transform result = FindChildRecursive(child, name);
|
||||
if (result != null) return result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Managers/CameraManager.cs.meta
Normal file
11
Assets/Scripts/Managers/CameraManager.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f400f9f72c25c304c9d8a3eb81f17279
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
206
Assets/Scripts/Managers/CharacterPrefabManager.cs
Normal file
206
Assets/Scripts/Managers/CharacterPrefabManager.cs
Normal file
@@ -0,0 +1,206 @@
|
||||
using UnityEngine;
|
||||
using System.Collections.Generic;
|
||||
|
||||
/// <summary>
|
||||
/// Centralized system for managing character prefabs and names.
|
||||
/// This singleton can be used across the entire game to load character prefabs
|
||||
/// and translate between prefab names and character display names.
|
||||
/// </summary>
|
||||
public class CharacterPrefabManager : MonoBehaviour
|
||||
{
|
||||
#region Singleton
|
||||
private static CharacterPrefabManager _instance;
|
||||
public static CharacterPrefabManager Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_instance == null)
|
||||
{
|
||||
GameObject go = new GameObject("CharacterPrefabManager");
|
||||
_instance = go.AddComponent<CharacterPrefabManager>();
|
||||
DontDestroyOnLoad(go);
|
||||
_instance.Initialize();
|
||||
}
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
// Character display names (uppercase for UI display)
|
||||
public enum CharacterName
|
||||
{
|
||||
WINDHAM,
|
||||
AMIRA,
|
||||
BAHMAN,
|
||||
ZIGGY,
|
||||
AMON,
|
||||
IMANI,
|
||||
ZIGGY_NEW
|
||||
}
|
||||
|
||||
// Dictionary mapping character names to prefab resource paths
|
||||
private Dictionary<CharacterName, string> playerPrefabPaths = new Dictionary<CharacterName, string>();
|
||||
private Dictionary<CharacterName, string> aiPrefabPaths = new Dictionary<CharacterName, string>();
|
||||
|
||||
// Dictionary mapping GameObject clone names to character names
|
||||
private Dictionary<string, CharacterName> gameObjectToCharacterName = new Dictionary<string, CharacterName>();
|
||||
|
||||
private void Initialize()
|
||||
{
|
||||
// Setup player prefab paths
|
||||
playerPrefabPaths[CharacterName.WINDHAM] = "Rabbit";
|
||||
playerPrefabPaths[CharacterName.AMIRA] = "Cheetah";
|
||||
playerPrefabPaths[CharacterName.BAHMAN] = "Eagle";
|
||||
playerPrefabPaths[CharacterName.ZIGGY] = "Cat";
|
||||
playerPrefabPaths[CharacterName.AMON] = "HM";
|
||||
playerPrefabPaths[CharacterName.IMANI] = "Hyena";
|
||||
|
||||
// Setup AI prefab paths - NOW USES SAME UNIFIED PREFABS AS PLAYER
|
||||
// The modular architecture means we use ONE prefab per character
|
||||
// and configure behavior (player vs AI) at runtime via component enable/disable
|
||||
aiPrefabPaths[CharacterName.WINDHAM] = "Rabbit"; // No longer "Rabbit Enemy"
|
||||
aiPrefabPaths[CharacterName.AMIRA] = "Cheetah"; // No longer "Cheetah Enemy"
|
||||
aiPrefabPaths[CharacterName.BAHMAN] = "Eagle"; // No longer "Eagle Enemy"
|
||||
aiPrefabPaths[CharacterName.ZIGGY] = "Cat"; // No longer "Cat Enemy"
|
||||
aiPrefabPaths[CharacterName.AMON] = "HM"; // No longer "HM Enemy"
|
||||
aiPrefabPaths[CharacterName.IMANI] = "Hyena"; // No longer "Hyena Enemy"
|
||||
|
||||
// Setup GameObject name mapping
|
||||
MapCloneNamesToCharacters();
|
||||
}
|
||||
|
||||
private void MapCloneNamesToCharacters()
|
||||
{
|
||||
// Player prefabs
|
||||
gameObjectToCharacterName["Rabbit(Clone)"] = CharacterName.WINDHAM;
|
||||
// gameObjectToCharacterName["Rabbit 2(Clone)"] = CharacterName.WINDHAM;
|
||||
gameObjectToCharacterName["Cheetah(Clone)"] = CharacterName.AMIRA;
|
||||
// gameObjectToCharacterName["Cheetah 2(Clone)"] = CharacterName.AMIRA;
|
||||
gameObjectToCharacterName["Eagle(Clone)"] = CharacterName.BAHMAN;
|
||||
gameObjectToCharacterName["Cat(Clone)"] = CharacterName.ZIGGY;
|
||||
gameObjectToCharacterName["HM(Clone)"] = CharacterName.AMON;
|
||||
gameObjectToCharacterName["Hyena(Clone)"] = CharacterName.IMANI;
|
||||
|
||||
// AI prefabs
|
||||
gameObjectToCharacterName["Rabbit Enemy(Clone)"] = CharacterName.WINDHAM;
|
||||
// gameObjectToCharacterName["Rabbit Enemy 2(Clone)"] = CharacterName.WINDHAM;
|
||||
gameObjectToCharacterName["Cheetah Enemy(Clone)"] = CharacterName.AMIRA;
|
||||
// gameObjectToCharacterName["Cheetah Enemy 2(Clone)"] = CharacterName.AMIRA;
|
||||
gameObjectToCharacterName["Eagle Enemy(Clone)"] = CharacterName.BAHMAN;
|
||||
gameObjectToCharacterName["Cat Enemy(Clone)"] = CharacterName.ZIGGY;
|
||||
gameObjectToCharacterName["HM Enemy(Clone)"] = CharacterName.AMON;
|
||||
gameObjectToCharacterName["Hyena Enemy(Clone)"] = CharacterName.IMANI;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the prefab for a character based on name and mode
|
||||
/// </summary>
|
||||
/// <param name="characterName">The character to load</param>
|
||||
/// <param name="isAI">Whether to load the AI version</param>
|
||||
/// <returns>The loaded prefab GameObject</returns>
|
||||
public GameObject GetCharacterPrefab(CharacterName characterName, bool isAI = false)
|
||||
{
|
||||
string prefabPath = isAI ? aiPrefabPaths[characterName] : playerPrefabPaths[characterName];
|
||||
GameObject prefab = Resources.Load<GameObject>(prefabPath);
|
||||
|
||||
if (prefab == null)
|
||||
{
|
||||
Debug.LogError($"Failed to load character prefab: {characterName} (AI: {isAI})");
|
||||
}
|
||||
|
||||
return prefab;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the prefab for a character based on string name and mode
|
||||
/// </summary>
|
||||
/// <param name="characterName">The string name of the character</param>
|
||||
/// <param name="isAI">Whether to load the AI version</param>
|
||||
/// <returns>The loaded prefab GameObject</returns>
|
||||
public GameObject GetCharacterPrefab(string characterName, bool isAI = false)
|
||||
{
|
||||
if (TryParseCharacterName(characterName, out CharacterName parsedName))
|
||||
{
|
||||
return GetCharacterPrefab(parsedName, isAI);
|
||||
}
|
||||
|
||||
Debug.LogError($"Invalid character name: {characterName}");
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to parse a string character name (case insensitive) to a CharacterName enum
|
||||
/// </summary>
|
||||
public bool TryParseCharacterName(string nameString, out CharacterName characterName)
|
||||
{
|
||||
// Normalize input to uppercase and remove spaces
|
||||
string normalizedName = nameString.ToUpper().Replace(" ", "_");
|
||||
|
||||
|
||||
// Try to parse as enum
|
||||
if (System.Enum.TryParse(normalizedName, out characterName))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
characterName = CharacterName.WINDHAM; // Default
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the character name from a GameObject (useful for clones)
|
||||
/// </summary>
|
||||
/// <param name="gameObject">The GameObject to identify</param>
|
||||
/// <returns>The character's display name as a string</returns>
|
||||
public string GetCharacterDisplayName(GameObject gameObject)
|
||||
{
|
||||
if (gameObject == null) return string.Empty;
|
||||
|
||||
if (gameObjectToCharacterName.TryGetValue(gameObject.name, out CharacterName characterName))
|
||||
{
|
||||
return characterName.ToString().Replace("_", " ");
|
||||
}
|
||||
|
||||
// If we can't find it directly, try to be smarter about it by checking if it contains character names
|
||||
string objName = gameObject.name.ToUpper();
|
||||
|
||||
foreach (CharacterName name in System.Enum.GetValues(typeof(CharacterName)))
|
||||
{
|
||||
string nameStr = name.ToString();
|
||||
if (objName.Contains(nameStr.Replace("_", " ")) || objName.Contains(nameStr))
|
||||
{
|
||||
return nameStr.Replace("_", " ");
|
||||
}
|
||||
}
|
||||
|
||||
Debug.LogWarning($"Unknown character GameObject: {gameObject.name}");
|
||||
return gameObject.name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a GameObject is a valid character prefab
|
||||
/// </summary>
|
||||
public bool IsValidCharacter(GameObject gameObject)
|
||||
{
|
||||
if (gameObject == null) return false;
|
||||
return gameObjectToCharacterName.ContainsKey(gameObject.name) ||
|
||||
FindMatchingCharacterName(gameObject.name) != null;
|
||||
}
|
||||
|
||||
private string FindMatchingCharacterName(string objectName)
|
||||
{
|
||||
string upperName = objectName.ToUpper();
|
||||
|
||||
// Check each character name
|
||||
foreach (CharacterName name in System.Enum.GetValues(typeof(CharacterName)))
|
||||
{
|
||||
string nameStr = name.ToString();
|
||||
if (upperName.Contains(nameStr.Replace("_", " ")) || upperName.Contains(nameStr))
|
||||
{
|
||||
return nameStr;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Managers/CharacterPrefabManager.cs.meta
Normal file
2
Assets/Scripts/Managers/CharacterPrefabManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9e68f8b4fdb1d08dda588c0052764d0c
|
||||
34
Assets/Scripts/Managers/EventSystemManager.cs
Normal file
34
Assets/Scripts/Managers/EventSystemManager.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityEngine.InputSystem;
|
||||
using UnityEngine.InputSystem.UI;
|
||||
using UnityEngine.EventSystems;
|
||||
|
||||
public class EventSystemManager : Singleton<EventSystemManager>
|
||||
{
|
||||
[Header("Component References")]
|
||||
public EventSystem eventSystem;
|
||||
public InputSystemUIInputModule inputSystemUIInputModule;
|
||||
|
||||
public void SetCurrentSelectedGameObject(GameObject newSelectedGameObject)
|
||||
{
|
||||
eventSystem.SetSelectedGameObject(newSelectedGameObject);
|
||||
Button newSelectable = newSelectedGameObject.GetComponent<Button>();
|
||||
newSelectable.Select();
|
||||
newSelectable.OnSelect(null);
|
||||
}
|
||||
|
||||
public void UpdateActionAssetToFocusedPlayer()
|
||||
{
|
||||
//PlayerController focusedPlayerController = GameObject.Find("GameManager").GetComponent<GameManager>().GetFocusedPlayerController();
|
||||
//inputSystemUIInputModule.actionsAsset = focusedPlayerController.GetActionAsset();
|
||||
}
|
||||
|
||||
public InputActionAsset GetInputActionAsset()
|
||||
{
|
||||
return inputSystemUIInputModule.actionsAsset;
|
||||
}
|
||||
|
||||
}
|
||||
11
Assets/Scripts/Managers/EventSystemManager.cs.meta
Normal file
11
Assets/Scripts/Managers/EventSystemManager.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 85cf9a79fae9bd944b754292d3abb94e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
401
Assets/Scripts/Managers/GameManager.cs
Normal file
401
Assets/Scripts/Managers/GameManager.cs
Normal file
@@ -0,0 +1,401 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityEngine.Video;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
public enum GameMode
|
||||
{
|
||||
SinglePlayer,
|
||||
LocalMultiplayer,
|
||||
CashSystem
|
||||
}
|
||||
|
||||
public class GameManager : MonoBehaviour
|
||||
{ //Game Mode
|
||||
public GameMode currentGameMode;
|
||||
|
||||
//Single Player
|
||||
public GameObject inScenePlayer;
|
||||
|
||||
//Local Multiplayer
|
||||
public GameObject playerPrefab;
|
||||
public GameObject homeScreen;
|
||||
public GameObject splashScreen;
|
||||
public int numberOfPlayers;
|
||||
|
||||
public Transform spawnRingCenter;
|
||||
public float spawnRingRadius;
|
||||
|
||||
//Spawned Players
|
||||
|
||||
private bool isPaused;
|
||||
|
||||
|
||||
public static GameManager Instance;
|
||||
public ModeSelection modeSelection;
|
||||
public WaitingSceneManager waitingSceneManager;
|
||||
|
||||
private int enemiesDefeated = 0;
|
||||
|
||||
public AudioSource audioSource;
|
||||
public AudioSource sfxSource;
|
||||
|
||||
///this should carry the value of the selected players from the modeselection script and make it available in another scene(game scene)
|
||||
public Dictionary<string, PlayerInfo> selectedPlayerDevice;
|
||||
|
||||
[SerializeField]
|
||||
private Slider progressBar;
|
||||
|
||||
public RawImage rawImage;
|
||||
public VideoPlayer videoPlayer;
|
||||
private bool isVideoFinished = false;
|
||||
|
||||
public event Action SplashSequenceCompleted;
|
||||
|
||||
public GameObject loadingUI; // Reference to the Loading UI Canvas
|
||||
|
||||
// private GameSettings gameSettings;
|
||||
// void OnEnable(){
|
||||
// //If Key is present, then load
|
||||
// if (PlayerPrefs.HasKey(GameSettingsKey))
|
||||
// {
|
||||
// gameSettings = LoadGameSettings(); //Load
|
||||
// }
|
||||
// // else{
|
||||
// // DefaultSettings(); //Set Volume and Controls Settings
|
||||
// // // SaveGameSettings();
|
||||
// // }
|
||||
// }
|
||||
|
||||
private GameSettings gameSettings;
|
||||
void LoadAudioSetings(){
|
||||
if (PlayerPrefs.HasKey(GameSettingsKey))
|
||||
{
|
||||
gameSettings = LoadGameSettings(); //Load
|
||||
}
|
||||
else
|
||||
{
|
||||
gameSettings = new GameSettings(); // Create default settings if none exist
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return; // prevent duplicate initialization
|
||||
}
|
||||
|
||||
Instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
|
||||
if (sfxSource == null)
|
||||
{
|
||||
sfxSource = gameObject.AddComponent<AudioSource>();
|
||||
sfxSource.playOnAwake = false;
|
||||
}
|
||||
|
||||
if (selectedPlayerDevice == null)
|
||||
selectedPlayerDevice = new Dictionary<string, PlayerInfo>();
|
||||
|
||||
audioSource = GetComponent<AudioSource>();
|
||||
}
|
||||
|
||||
|
||||
|
||||
void Start()
|
||||
{
|
||||
|
||||
isPaused = false;
|
||||
|
||||
//SetupBasedOnGameState();
|
||||
//SetupUI();
|
||||
|
||||
|
||||
// Set the video player's target texture to the raw image's texture
|
||||
videoPlayer.targetTexture = new RenderTexture(Screen.width, Screen.height, 24);
|
||||
rawImage.texture = videoPlayer.targetTexture;
|
||||
|
||||
// Record when splash started for minimum duration check
|
||||
splashStartTime = Time.realtimeSinceStartup;
|
||||
|
||||
// Subscribe to the videoPlayer's completion event
|
||||
videoPlayer.loopPointReached += OnVideoFinished;
|
||||
|
||||
}
|
||||
|
||||
|
||||
//BG Music
|
||||
public void PlayBackgroundMusic()
|
||||
{
|
||||
//audio play
|
||||
LoadAudioSetings();
|
||||
Debug.Log($"BG MUSIC PLAYING {gameSettings.GameplayVolume_value}");
|
||||
audioSource.volume = gameSettings.GameplayVolume_value;
|
||||
audioSource.Play();
|
||||
}
|
||||
|
||||
|
||||
//UI click
|
||||
public void PlaySound(AudioClip sfxClip)
|
||||
{
|
||||
if (sfxClip == null)
|
||||
{
|
||||
Debug.LogWarning("Attempted to play null audio clip");
|
||||
return;
|
||||
}
|
||||
|
||||
if (sfxSource == null)
|
||||
{
|
||||
Debug.LogWarning("sfxSource is null, attempting to recreate");
|
||||
sfxSource = gameObject.AddComponent<AudioSource>();
|
||||
sfxSource.playOnAwake = false;
|
||||
}
|
||||
|
||||
LoadAudioSetings();
|
||||
sfxSource.clip = sfxClip;
|
||||
sfxSource.volume = gameSettings != null ? gameSettings.UIVolume_value : 1f;
|
||||
sfxSource.Play();
|
||||
}
|
||||
|
||||
|
||||
public void ActivateHomeScreen()
|
||||
{
|
||||
// Perform actions to activate the home screen here
|
||||
homeScreen.SetActive(true);
|
||||
|
||||
}
|
||||
|
||||
// Minimum time (seconds) the splash should display before transitioning
|
||||
private float splashStartTime;
|
||||
private const float MIN_SPLASH_DURATION = 2.0f; // At least 2 seconds
|
||||
|
||||
// Method called when the video finishes playing
|
||||
private void OnVideoFinished(VideoPlayer player)
|
||||
{
|
||||
// Set the flag to true to indicate that the video has finished playing
|
||||
if (isVideoFinished)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the video actually played for a reasonable duration
|
||||
float timeSinceSplashStart = Time.realtimeSinceStartup - splashStartTime;
|
||||
double videoLength = player != null && player.length > 0 ? player.length : 0;
|
||||
|
||||
Debug.Log($"[GameManager] loopPointReached triggered. Time since start: {timeSinceSplashStart:F2}s, Video length: {videoLength:F2}s, IsPlaying: {player?.isPlaying}");
|
||||
|
||||
// If the event fired too quickly (video didn't actually play), wait for it
|
||||
if (timeSinceSplashStart < MIN_SPLASH_DURATION && timeSinceSplashStart < videoLength * 0.5f)
|
||||
{
|
||||
Debug.LogWarning($"[GameManager] Video finished too quickly ({timeSinceSplashStart:F2}s). Starting fallback wait...");
|
||||
StartCoroutine(WaitForMinimumSplashDuration());
|
||||
return;
|
||||
}
|
||||
|
||||
isVideoFinished = true;
|
||||
|
||||
Debug.Log("[GameManager] Splash video finished. Starting scene load coroutine to avoid blocking main thread.");
|
||||
|
||||
// Start coroutine to avoid blocking the VideoPlayer callback thread
|
||||
StartCoroutine(LoadMenuAfterSplash());
|
||||
}
|
||||
|
||||
private System.Collections.IEnumerator WaitForMinimumSplashDuration()
|
||||
{
|
||||
// Wait until minimum duration has passed
|
||||
float remainingTime = MIN_SPLASH_DURATION - (Time.realtimeSinceStartup - splashStartTime);
|
||||
if (remainingTime > 0)
|
||||
{
|
||||
Debug.Log($"[GameManager] Waiting {remainingTime:F2}s for minimum splash duration...");
|
||||
yield return new WaitForSeconds(remainingTime);
|
||||
}
|
||||
|
||||
if (!isVideoFinished)
|
||||
{
|
||||
isVideoFinished = true;
|
||||
Debug.Log("[GameManager] Minimum splash duration reached. Starting scene load.");
|
||||
StartCoroutine(LoadMenuAfterSplash());
|
||||
}
|
||||
}
|
||||
|
||||
private System.Collections.IEnumerator LoadMenuAfterSplash()
|
||||
{
|
||||
ShowLoadingUI();
|
||||
|
||||
// Invoke the event - GameInitialize will handle scene loading if subscribed
|
||||
// This prevents duplicate scene load attempts
|
||||
bool hasSubscribers = SplashSequenceCompleted != null;
|
||||
SplashSequenceCompleted?.Invoke();
|
||||
|
||||
// Wait one frame to ensure UI updates before potentially heavy operations
|
||||
yield return null;
|
||||
|
||||
// Only load scene directly if no subscribers handled it
|
||||
// (GameInitialize subscribes when forceMenuDuringDevelopment is true)
|
||||
if (!hasSubscribers)
|
||||
{
|
||||
const string forcedScene = "MenuScene";
|
||||
Debug.Log("[GameManager] Loading '" + forcedScene + "' via LoadingScreen (no external subscribers).");
|
||||
LoadSceneWithLoading(forcedScene);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log("[GameManager] Splash finished - scene loading handled by subscriber.");
|
||||
}
|
||||
}
|
||||
|
||||
private void ActivateLoadingBar()
|
||||
{
|
||||
// Show the loading bar or perform any other loading-related actions here
|
||||
//SceneManager.LoadScene("MenuScene", LoadSceneMode.Single);
|
||||
|
||||
|
||||
SceneManager.sceneLoaded += OnSceneLoaded;
|
||||
}
|
||||
|
||||
public void OnSceneLoaded(Scene scene, LoadSceneMode mode){
|
||||
SceneManager.sceneLoaded -= OnSceneLoaded; // Unregister event handler
|
||||
|
||||
if (scene.name != "MenuScene")
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Debug.Log("XT Loading Bar");
|
||||
|
||||
//Find GameObject with name "UI"
|
||||
GameObject uiObject = GameObject.Find("UI");
|
||||
if (uiObject != null){
|
||||
/*foreach (Transform child in uiObject.transform){
|
||||
if ((child.name != "ModeSelection")){
|
||||
child.gameObject.SetActive(false);
|
||||
}
|
||||
}*/
|
||||
|
||||
|
||||
//Check if Announcements have been shown or not, if shown, go to Loading_Screen UI, but if not yet shown, then go to Announcements UI
|
||||
// Check if the "announcement" key exists in PlayerPrefs
|
||||
|
||||
// Get the value of the "announcement" key
|
||||
string announcementValue = PlayerPrefs.GetString("announcementStatus");
|
||||
|
||||
// Check if the value is "0", Announcements have not happened
|
||||
if (announcementValue == "0"){
|
||||
GameObject announcements_to_show = uiObject.transform.Find("Announcements")?.gameObject;
|
||||
|
||||
if (announcements_to_show != null){
|
||||
announcements_to_show.SetActive(true); //When the Menu scene is activated, show this UI
|
||||
|
||||
}else{
|
||||
Debug.Log("No Announcements screen");
|
||||
}
|
||||
}
|
||||
else{ //Announcements already happened
|
||||
|
||||
//LoadingScreen loadingscript = GetComponent<LoadingScreen>();
|
||||
//ActivateHomeScreen();
|
||||
//GameManager.Instance.gameObject.GetComponent<LoadingScreen>().UI_to_show_AfterLoading(2, "Home");
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
else{
|
||||
Debug.LogError("UI GameObject not found in MenuScene!");
|
||||
}
|
||||
}
|
||||
|
||||
public void IncrementEnemiesDefeated()
|
||||
{
|
||||
enemiesDefeated++;
|
||||
}
|
||||
|
||||
public int GetEnemiesDefeated()
|
||||
{
|
||||
return enemiesDefeated;
|
||||
}
|
||||
|
||||
|
||||
|
||||
void SetupLocalMultiplayer()
|
||||
{
|
||||
|
||||
if (inScenePlayer == true)
|
||||
{
|
||||
Destroy(inScenePlayer);
|
||||
}
|
||||
}
|
||||
|
||||
public void HideLoadingUI()
|
||||
{
|
||||
loadingUI.SetActive(false);
|
||||
}
|
||||
|
||||
|
||||
public bool HasSplashFinished => isVideoFinished;
|
||||
|
||||
public void ShowLoadingUI()
|
||||
{
|
||||
if (loadingUI == null)
|
||||
{
|
||||
if (gameObject.transform.childCount > 0)
|
||||
{
|
||||
loadingUI = gameObject.transform.GetChild(0).gameObject;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("ShowLoadingUI: No child GameObject found. Please ensure the hierarchy is set up correctly.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
loadingUI.SetActive(true);
|
||||
}
|
||||
|
||||
public void LoadSceneWithLoading(string sceneName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(sceneName))
|
||||
{
|
||||
Debug.LogError("LoadSceneWithLoading: sceneName is null or empty");
|
||||
return;
|
||||
}
|
||||
|
||||
var loadingScreen = GetComponentInChildren<LoadingScreen>(true);
|
||||
if (loadingScreen != null)
|
||||
{
|
||||
loadingScreen.UI_to_show_AfterLoading(1, sceneName);
|
||||
ShowLoadingUI();
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("LoadSceneWithLoading: LoadingScreen component not found. Falling back to direct scene load.");
|
||||
SceneManager.LoadScene(sceneName, LoadSceneMode.Single);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//Saving and Loading Game Settings
|
||||
#region
|
||||
private const string GameSettingsKey = "GameSettings";
|
||||
|
||||
public static void SaveGameSettings(GameSettings settings){
|
||||
string json = JsonUtility.ToJson(settings);
|
||||
PlayerPrefs.SetString(GameSettingsKey, json);
|
||||
PlayerPrefs.Save();
|
||||
}
|
||||
|
||||
public static GameSettings LoadGameSettings(){
|
||||
if (PlayerPrefs.HasKey(GameSettingsKey))
|
||||
{
|
||||
string json = PlayerPrefs.GetString(GameSettingsKey);
|
||||
return JsonUtility.FromJson<GameSettings>(json);
|
||||
}
|
||||
return new GameSettings(); // Return default settings if none are saved
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
11
Assets/Scripts/Managers/GameManager.cs.meta
Normal file
11
Assets/Scripts/Managers/GameManager.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ad331c3afd647a044a6343361e5401a9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
333
Assets/Scripts/Managers/MinimapEdgeIndicator.cs
Normal file
333
Assets/Scripts/Managers/MinimapEdgeIndicator.cs
Normal file
@@ -0,0 +1,333 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using System.Collections.Generic;
|
||||
|
||||
/// <summary>
|
||||
/// MinimapEdgeIndicator - Shows arrows on the edge of the minimap UI
|
||||
/// pointing toward off-screen entities (allies, enemies, cash bags, objectives).
|
||||
///
|
||||
/// Like games such as GTA, Fortnite, and PUBG - when entities are outside the
|
||||
/// minimap view, arrows appear at the border pointing in their direction.
|
||||
///
|
||||
/// Attach to the minimap RawImage UI element, or let MinimapManager create it.
|
||||
/// </summary>
|
||||
public class MinimapEdgeIndicator : MonoBehaviour
|
||||
{
|
||||
[Header("Settings")]
|
||||
[SerializeField] private RectTransform minimapRect;
|
||||
[SerializeField] private float edgePadding = 15f;
|
||||
[SerializeField] private float indicatorSize = 60f;
|
||||
[SerializeField] private float updateInterval = 0.1f;
|
||||
|
||||
[Header("Visibility")]
|
||||
[SerializeField] private bool showAllies = true;
|
||||
[SerializeField] private bool showEnemies = true;
|
||||
[SerializeField] private bool showCashBags = true;
|
||||
[SerializeField] private bool showVaults = true;
|
||||
|
||||
[Header("Colors")]
|
||||
[SerializeField] private Color allyColor = new Color(0.2f, 0.8f, 0.2f, 1f);
|
||||
[SerializeField] private Color enemyColor = new Color(1f, 0.2f, 0.2f, 1f);
|
||||
[SerializeField] private Color cashBagColor = new Color(1f, 0.85f, 0f, 1f);
|
||||
[SerializeField] private Color playerVaultColor = new Color(0.2f, 0.9f, 0.2f, 1f);
|
||||
[SerializeField] private Color enemyVaultColor = new Color(0.9f, 0.2f, 0.2f, 1f);
|
||||
|
||||
// Pool of indicator objects
|
||||
private List<RectTransform> indicatorPool = new List<RectTransform>();
|
||||
private List<Image> indicatorImages = new List<Image>();
|
||||
private int activeIndicatorCount = 0;
|
||||
|
||||
private Camera minimapCamera;
|
||||
private Transform playerTransform;
|
||||
private string playerTeamTag = "Player";
|
||||
private float lastUpdateTime;
|
||||
private Sprite arrowSprite;
|
||||
private float playerHeadingDeg; // Cached player Y rotation for GTA V style
|
||||
|
||||
// Cached references for performance
|
||||
private CashSystemManager cachedCSM;
|
||||
private bool csmSearched = false;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// Find minimap camera
|
||||
MinimapManager mm = FindObjectOfType<MinimapManager>();
|
||||
if (mm != null)
|
||||
{
|
||||
minimapCamera = mm.GetComponent<Camera>();
|
||||
}
|
||||
|
||||
// Auto-find minimap rect if not assigned
|
||||
if (minimapRect == null)
|
||||
{
|
||||
minimapRect = GetComponent<RectTransform>();
|
||||
|
||||
// Try parent if this component doesn't have RectTransform
|
||||
if (minimapRect == null)
|
||||
{
|
||||
minimapRect = GetComponentInParent<RectTransform>();
|
||||
}
|
||||
}
|
||||
|
||||
// Create arrow sprite once
|
||||
arrowSprite = CreateArrowSprite();
|
||||
|
||||
// Create initial pool
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
CreateIndicator();
|
||||
}
|
||||
|
||||
Debug.Log("[MinimapEdgeIndicator] Initialized - will show arrows for off-screen entities");
|
||||
}
|
||||
|
||||
private void CreateIndicator()
|
||||
{
|
||||
GameObject indicatorObj = new GameObject("EdgeIndicator");
|
||||
indicatorObj.transform.SetParent(transform, false);
|
||||
|
||||
RectTransform rect = indicatorObj.AddComponent<RectTransform>();
|
||||
rect.sizeDelta = new Vector2(indicatorSize, indicatorSize);
|
||||
rect.localScale = Vector3.one;
|
||||
|
||||
Image img = indicatorObj.AddComponent<Image>();
|
||||
img.sprite = arrowSprite;
|
||||
img.color = enemyColor;
|
||||
img.raycastTarget = false;
|
||||
|
||||
// Add outline for better visibility
|
||||
Outline outline = indicatorObj.AddComponent<Outline>();
|
||||
outline.effectColor = new Color(0, 0, 0, 0.8f);
|
||||
outline.effectDistance = new Vector2(1, 1);
|
||||
|
||||
indicatorObj.SetActive(false);
|
||||
|
||||
indicatorPool.Add(rect);
|
||||
indicatorImages.Add(img);
|
||||
}
|
||||
|
||||
private Sprite CreateArrowSprite()
|
||||
{
|
||||
int size = 48; // Higher resolution for sharper arrow
|
||||
Texture2D tex = new Texture2D(size, size);
|
||||
tex.filterMode = FilterMode.Bilinear;
|
||||
Color[] colors = new Color[size * size];
|
||||
|
||||
for (int i = 0; i < colors.Length; i++) colors[i] = Color.clear;
|
||||
|
||||
Vector2 center = new Vector2(size / 2f, size / 2f);
|
||||
|
||||
// Draw a clean filled arrow/chevron pointing right
|
||||
// (rotation applied at runtime to point toward off-screen targets)
|
||||
for (int y = 0; y < size; y++)
|
||||
{
|
||||
for (int x = 0; x < size; x++)
|
||||
{
|
||||
float nx = (x - center.x) / (size / 2f); // -1 to 1
|
||||
float ny = (y - center.y) / (size / 2f); // -1 to 1
|
||||
|
||||
// Filled arrow pointing right: tip at nx=0.8, base fans from nx=-0.4
|
||||
// The arrow body fills from base to tip
|
||||
if (nx >= -0.4f && nx <= 0.8f)
|
||||
{
|
||||
float t = (nx + 0.4f) / 1.2f; // 0 at base, 1 at tip
|
||||
float allowedY = 0.6f * (1f - t); // Max width at base, 0 at tip
|
||||
|
||||
if (Mathf.Abs(ny) <= allowedY)
|
||||
{
|
||||
// Anti-alias the edges
|
||||
float edgeDist = allowedY - Mathf.Abs(ny);
|
||||
float alpha = Mathf.Clamp01(edgeDist * 4f);
|
||||
colors[y * size + x] = new Color(1, 1, 1, alpha);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tex.SetPixels(colors);
|
||||
tex.Apply();
|
||||
|
||||
return Sprite.Create(tex, new Rect(0, 0, size, size), new Vector2(0.5f, 0.5f));
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (Time.time - lastUpdateTime < updateInterval) return;
|
||||
lastUpdateTime = Time.time;
|
||||
|
||||
UpdateIndicators();
|
||||
}
|
||||
|
||||
private void UpdateIndicators()
|
||||
{
|
||||
// Find player
|
||||
FindPlayer();
|
||||
if (playerTransform == null || minimapCamera == null || minimapRect == null) return;
|
||||
|
||||
// Hide all first
|
||||
activeIndicatorCount = 0;
|
||||
for (int i = 0; i < indicatorPool.Count; i++)
|
||||
{
|
||||
indicatorPool[i].gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
Vector3 playerPos = playerTransform.position;
|
||||
float viewSize = minimapCamera.orthographicSize;
|
||||
|
||||
// Cache player heading for GTA V rotating minimap
|
||||
playerHeadingDeg = playerTransform.eulerAngles.y;
|
||||
|
||||
// Get minimap UI half-size
|
||||
Vector2 halfSize = minimapRect.rect.size / 2f;
|
||||
|
||||
// Track all characters using static registry (zero-allocation, replaces FindObjectsOfType)
|
||||
if (showAllies || showEnemies)
|
||||
{
|
||||
foreach (var member in TeamMember.AllMembers)
|
||||
{
|
||||
if (member == null || member.transform == playerTransform) continue;
|
||||
|
||||
bool isAlly = member.gameObject.CompareTag(playerTeamTag);
|
||||
|
||||
if (isAlly && showAllies)
|
||||
{
|
||||
ShowIndicatorIfOffscreen(member.transform.position, playerPos, viewSize, halfSize, allyColor);
|
||||
}
|
||||
else if (!isAlly && showEnemies)
|
||||
{
|
||||
ShowIndicatorIfOffscreen(member.transform.position, playerPos, viewSize, halfSize, enemyColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cash bags
|
||||
if (showCashBags)
|
||||
{
|
||||
var cashBags = EntityRegistry<CashBag>.GetAll();
|
||||
for (int i = 0; i < cashBags.Count; i++)
|
||||
{
|
||||
if (cashBags[i] == null) continue;
|
||||
ShowIndicatorIfOffscreen(cashBags[i].transform.position, playerPos, viewSize, halfSize, cashBagColor);
|
||||
}
|
||||
}
|
||||
|
||||
// Vaults
|
||||
if (showVaults)
|
||||
{
|
||||
TeamVault[] vaults = FindObjectsOfType<TeamVault>();
|
||||
foreach (var vault in vaults)
|
||||
{
|
||||
if (vault == null) continue;
|
||||
Color vColor = vault.TeamTag == playerTeamTag ? playerVaultColor : enemyVaultColor;
|
||||
ShowIndicatorIfOffscreen(vault.transform.position, playerPos, viewSize, halfSize, vColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowIndicatorIfOffscreen(Vector3 worldPos, Vector3 playerPos, float viewSize, Vector2 halfSize, Color color)
|
||||
{
|
||||
// Calculate relative position in world space
|
||||
Vector3 relativePos = worldPos - playerPos;
|
||||
Vector2 relativePos2D = new Vector2(relativePos.x, relativePos.z);
|
||||
|
||||
// GTA V STYLE: Rotate by negative player heading so edge positions
|
||||
// are in "minimap space" (which rotates with the player).
|
||||
// Without this, arrows point to wrong edges when the map is rotated.
|
||||
float headingRad = -playerHeadingDeg * Mathf.Deg2Rad;
|
||||
float cos = Mathf.Cos(headingRad);
|
||||
float sin = Mathf.Sin(headingRad);
|
||||
Vector2 rotated = new Vector2(
|
||||
relativePos2D.x * cos - relativePos2D.y * sin,
|
||||
relativePos2D.x * sin + relativePos2D.y * cos
|
||||
);
|
||||
|
||||
// Convert world distance to minimap UI space
|
||||
float worldToUIScale = halfSize.y / viewSize;
|
||||
Vector2 minimapPos = rotated * worldToUIScale;
|
||||
|
||||
// Check if within minimap bounds
|
||||
bool isWithinBounds = Mathf.Abs(minimapPos.x) <= halfSize.x * 0.85f &&
|
||||
Mathf.Abs(minimapPos.y) <= halfSize.y * 0.85f;
|
||||
|
||||
if (isWithinBounds) return; // Already visible on minimap, don't show edge indicator
|
||||
|
||||
// Calculate angle for arrow rotation (in rotated/minimap space)
|
||||
float angle = Mathf.Atan2(rotated.y, rotated.x) * Mathf.Rad2Deg;
|
||||
|
||||
// Clamp to edge of minimap
|
||||
Vector2 normalizedDir = rotated.normalized;
|
||||
|
||||
// Find intersection with minimap edge
|
||||
float maxX = halfSize.x - edgePadding;
|
||||
float maxY = halfSize.y - edgePadding;
|
||||
|
||||
float tX = normalizedDir.x != 0 ? Mathf.Abs(maxX / normalizedDir.x) : float.MaxValue;
|
||||
float tY = normalizedDir.y != 0 ? Mathf.Abs(maxY / normalizedDir.y) : float.MaxValue;
|
||||
float t = Mathf.Min(tX, tY);
|
||||
|
||||
Vector2 clampedPos = normalizedDir * t;
|
||||
|
||||
// Get or create indicator
|
||||
if (activeIndicatorCount >= indicatorPool.Count)
|
||||
{
|
||||
CreateIndicator();
|
||||
}
|
||||
|
||||
RectTransform indicator = indicatorPool[activeIndicatorCount];
|
||||
Image img = indicatorImages[activeIndicatorCount];
|
||||
|
||||
indicator.gameObject.SetActive(true);
|
||||
indicator.sizeDelta = new Vector2(indicatorSize, indicatorSize); // Force size every frame
|
||||
indicator.anchoredPosition = clampedPos;
|
||||
indicator.localRotation = Quaternion.Euler(0, 0, angle);
|
||||
img.color = color;
|
||||
|
||||
activeIndicatorCount++;
|
||||
}
|
||||
|
||||
private void FindPlayer()
|
||||
{
|
||||
// Cache CashSystemManager lookup
|
||||
if (!csmSearched)
|
||||
{
|
||||
cachedCSM = FindObjectOfType<CashSystemManager>();
|
||||
csmSearched = true;
|
||||
}
|
||||
|
||||
// In Cash System mode, find controlled character
|
||||
if (cachedCSM != null && cachedCSM.CurrentControlledCharacter != null)
|
||||
{
|
||||
playerTransform = cachedCSM.CurrentControlledCharacter.transform;
|
||||
playerTeamTag = cachedCSM.CurrentControlledCharacter.tag;
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback to Player tag
|
||||
if (playerTransform == null)
|
||||
{
|
||||
GameObject player = GameObject.FindGameObjectWithTag("Player");
|
||||
if (player != null)
|
||||
{
|
||||
playerTransform = player.transform;
|
||||
playerTeamTag = "Player";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates and attaches an edge indicator to a minimap UI element
|
||||
/// </summary>
|
||||
public static MinimapEdgeIndicator AttachTo(RectTransform minimapUI)
|
||||
{
|
||||
if (minimapUI == null) return null;
|
||||
|
||||
MinimapEdgeIndicator existing = minimapUI.GetComponent<MinimapEdgeIndicator>();
|
||||
if (existing != null) return existing;
|
||||
|
||||
MinimapEdgeIndicator indicator = minimapUI.gameObject.AddComponent<MinimapEdgeIndicator>();
|
||||
indicator.minimapRect = minimapUI;
|
||||
|
||||
Debug.Log("[MinimapEdgeIndicator] Attached to " + minimapUI.name);
|
||||
return indicator;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Managers/MinimapEdgeIndicator.cs.meta
Normal file
2
Assets/Scripts/Managers/MinimapEdgeIndicator.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b8eb2b75fd219a32dba1c27c666d9555
|
||||
376
Assets/Scripts/Managers/MinimapExpandController.cs
Normal file
376
Assets/Scripts/Managers/MinimapExpandController.cs
Normal file
@@ -0,0 +1,376 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityEngine.EventSystems;
|
||||
using System.Collections;
|
||||
|
||||
/// <summary>
|
||||
/// MinimapExpandController — Tap the minimap to expand it to a full-screen map view.
|
||||
/// Tap again or press the close button to return to mini view.
|
||||
/// Attach to the minimap RawImage UI element.
|
||||
///
|
||||
/// In expanded mode:
|
||||
/// - Camera zooms out to show entire play area
|
||||
/// - Map stops rotating (north-up for readability)
|
||||
/// - Semi-transparent overlay dims the game behind
|
||||
/// - Close button appears in the corner
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(RawImage))]
|
||||
public class MinimapExpandController : MonoBehaviour, IPointerClickHandler
|
||||
{
|
||||
[Header("Expand Settings")]
|
||||
[SerializeField] private float expandedZoom = 200f; // Camera ortho size when expanded
|
||||
[SerializeField] private float expandDuration = 0.25f; // Animation duration
|
||||
[SerializeField] private float overlayAlpha = 0.6f; // Background dim overlay
|
||||
|
||||
[Header("Expanded Size (screen fraction)")]
|
||||
[SerializeField] private float expandedScreenFraction = 0.85f; // 85% of screen
|
||||
|
||||
[Header("Close Button")]
|
||||
[SerializeField] private Sprite closeButtonSprite; // Optional: custom close icon
|
||||
[SerializeField] private float closeButtonSize = 50f;
|
||||
|
||||
// ─── Runtime ─────────────────────────────────────────────
|
||||
private RectTransform _rectTransform;
|
||||
private MinimapManager _minimapManager;
|
||||
private Camera _minimapCam;
|
||||
|
||||
// Saved mini-view state
|
||||
private Vector2 _miniAnchorMin;
|
||||
private Vector2 _miniAnchorMax;
|
||||
private Vector2 _miniOffsetMin;
|
||||
private Vector2 _miniOffsetMax;
|
||||
private Vector2 _miniPivot;
|
||||
private Vector2 _miniSizeDelta;
|
||||
private Vector3 _miniAnchoredPos;
|
||||
private float _miniZoom;
|
||||
private bool _miniRotateWithPlayer;
|
||||
private Vector3 _miniCameraOffset;
|
||||
|
||||
// Expanded state
|
||||
private bool _isExpanded = false;
|
||||
private bool _isAnimating = false;
|
||||
private GameObject _overlay;
|
||||
private GameObject _closeButton;
|
||||
private Canvas _parentCanvas;
|
||||
|
||||
// ─── Lifecycle ───────────────────────────────────────────
|
||||
|
||||
private void Start()
|
||||
{
|
||||
_rectTransform = GetComponent<RectTransform>();
|
||||
_parentCanvas = GetComponentInParent<Canvas>();
|
||||
|
||||
// Find MinimapManager
|
||||
_minimapManager = FindObjectOfType<MinimapManager>();
|
||||
if (_minimapManager != null)
|
||||
_minimapCam = _minimapManager.GetComponent<Camera>();
|
||||
|
||||
// Save initial mini-view layout
|
||||
SaveMiniState();
|
||||
|
||||
// Create overlay (hidden by default)
|
||||
CreateOverlay();
|
||||
CreateCloseButton();
|
||||
}
|
||||
|
||||
// ─── Click Handler ───────────────────────────────────────
|
||||
|
||||
public void OnPointerClick(PointerEventData eventData)
|
||||
{
|
||||
if (_isAnimating) return;
|
||||
|
||||
if (_isExpanded)
|
||||
Collapse();
|
||||
else
|
||||
Expand();
|
||||
}
|
||||
|
||||
// ─── Expand / Collapse ───────────────────────────────────
|
||||
|
||||
public void Expand()
|
||||
{
|
||||
if (_isExpanded || _isAnimating) return;
|
||||
_isExpanded = true;
|
||||
|
||||
SaveMiniState();
|
||||
|
||||
// Show overlay
|
||||
if (_overlay != null) _overlay.SetActive(true);
|
||||
if (_closeButton != null) _closeButton.SetActive(true);
|
||||
|
||||
// Bring minimap on top
|
||||
_rectTransform.SetAsLastSibling();
|
||||
if (_closeButton != null) _closeButton.transform.SetAsLastSibling();
|
||||
|
||||
// Switch camera to full-map mode
|
||||
if (_minimapManager != null)
|
||||
{
|
||||
_minimapManager.rotateWithPlayer = false;
|
||||
_minimapManager.SetZoom(expandedZoom);
|
||||
}
|
||||
|
||||
// Animate the RectTransform to fill screen
|
||||
StartCoroutine(AnimateExpand());
|
||||
}
|
||||
|
||||
public void Collapse()
|
||||
{
|
||||
if (!_isExpanded || _isAnimating) return;
|
||||
_isExpanded = false;
|
||||
|
||||
// Hide overlay
|
||||
if (_overlay != null) _overlay.SetActive(false);
|
||||
if (_closeButton != null) _closeButton.SetActive(false);
|
||||
|
||||
// Restore camera
|
||||
if (_minimapManager != null)
|
||||
{
|
||||
_minimapManager.rotateWithPlayer = _miniRotateWithPlayer;
|
||||
_minimapManager.SetZoom(_miniZoom);
|
||||
}
|
||||
|
||||
// Animate back to mini
|
||||
StartCoroutine(AnimateCollapse());
|
||||
}
|
||||
|
||||
// ─── State Save/Restore ──────────────────────────────────
|
||||
|
||||
private void SaveMiniState()
|
||||
{
|
||||
_miniAnchorMin = _rectTransform.anchorMin;
|
||||
_miniAnchorMax = _rectTransform.anchorMax;
|
||||
_miniOffsetMin = _rectTransform.offsetMin;
|
||||
_miniOffsetMax = _rectTransform.offsetMax;
|
||||
_miniPivot = _rectTransform.pivot;
|
||||
_miniSizeDelta = _rectTransform.sizeDelta;
|
||||
_miniAnchoredPos = _rectTransform.anchoredPosition;
|
||||
|
||||
if (_minimapManager != null)
|
||||
{
|
||||
_miniRotateWithPlayer = _minimapManager.rotateWithPlayer;
|
||||
_miniCameraOffset = _minimapManager.offset;
|
||||
}
|
||||
|
||||
if (_minimapCam != null)
|
||||
_miniZoom = _minimapCam.orthographicSize;
|
||||
}
|
||||
|
||||
private void RestoreMiniState()
|
||||
{
|
||||
_rectTransform.anchorMin = _miniAnchorMin;
|
||||
_rectTransform.anchorMax = _miniAnchorMax;
|
||||
_rectTransform.offsetMin = _miniOffsetMin;
|
||||
_rectTransform.offsetMax = _miniOffsetMax;
|
||||
_rectTransform.pivot = _miniPivot;
|
||||
_rectTransform.sizeDelta = _miniSizeDelta;
|
||||
_rectTransform.anchoredPosition = _miniAnchoredPos;
|
||||
}
|
||||
|
||||
// ─── Animations ──────────────────────────────────────────
|
||||
|
||||
private IEnumerator AnimateExpand()
|
||||
{
|
||||
_isAnimating = true;
|
||||
|
||||
// Capture start state
|
||||
Vector2 startAnchorMin = _rectTransform.anchorMin;
|
||||
Vector2 startAnchorMax = _rectTransform.anchorMax;
|
||||
Vector2 startOffsetMin = _rectTransform.offsetMin;
|
||||
Vector2 startOffsetMax = _rectTransform.offsetMax;
|
||||
Vector2 startPivot = _rectTransform.pivot;
|
||||
|
||||
// Target: centered, filling expandedScreenFraction of screen
|
||||
float margin = (1f - expandedScreenFraction) / 2f;
|
||||
Vector2 targetAnchorMin = new Vector2(margin, margin);
|
||||
Vector2 targetAnchorMax = new Vector2(1f - margin, 1f - margin);
|
||||
Vector2 targetOffsetMin = Vector2.zero;
|
||||
Vector2 targetOffsetMax = Vector2.zero;
|
||||
Vector2 targetPivot = new Vector2(0.5f, 0.5f);
|
||||
|
||||
float elapsed = 0f;
|
||||
while (elapsed < expandDuration)
|
||||
{
|
||||
elapsed += Time.unscaledDeltaTime;
|
||||
float t = Mathf.SmoothStep(0f, 1f, elapsed / expandDuration);
|
||||
|
||||
_rectTransform.anchorMin = Vector2.Lerp(startAnchorMin, targetAnchorMin, t);
|
||||
_rectTransform.anchorMax = Vector2.Lerp(startAnchorMax, targetAnchorMax, t);
|
||||
_rectTransform.offsetMin = Vector2.Lerp(startOffsetMin, targetOffsetMin, t);
|
||||
_rectTransform.offsetMax = Vector2.Lerp(startOffsetMax, targetOffsetMax, t);
|
||||
_rectTransform.pivot = Vector2.Lerp(startPivot, targetPivot, t);
|
||||
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Ensure final values are exact
|
||||
_rectTransform.anchorMin = targetAnchorMin;
|
||||
_rectTransform.anchorMax = targetAnchorMax;
|
||||
_rectTransform.offsetMin = targetOffsetMin;
|
||||
_rectTransform.offsetMax = targetOffsetMax;
|
||||
_rectTransform.pivot = targetPivot;
|
||||
|
||||
_isAnimating = false;
|
||||
}
|
||||
|
||||
private IEnumerator AnimateCollapse()
|
||||
{
|
||||
_isAnimating = true;
|
||||
|
||||
// Capture current (expanded) state
|
||||
Vector2 startAnchorMin = _rectTransform.anchorMin;
|
||||
Vector2 startAnchorMax = _rectTransform.anchorMax;
|
||||
Vector2 startOffsetMin = _rectTransform.offsetMin;
|
||||
Vector2 startOffsetMax = _rectTransform.offsetMax;
|
||||
Vector2 startPivot = _rectTransform.pivot;
|
||||
|
||||
float elapsed = 0f;
|
||||
while (elapsed < expandDuration)
|
||||
{
|
||||
elapsed += Time.unscaledDeltaTime;
|
||||
float t = Mathf.SmoothStep(0f, 1f, elapsed / expandDuration);
|
||||
|
||||
_rectTransform.anchorMin = Vector2.Lerp(startAnchorMin, _miniAnchorMin, t);
|
||||
_rectTransform.anchorMax = Vector2.Lerp(startAnchorMax, _miniAnchorMax, t);
|
||||
_rectTransform.offsetMin = Vector2.Lerp(startOffsetMin, _miniOffsetMin, t);
|
||||
_rectTransform.offsetMax = Vector2.Lerp(startOffsetMax, _miniOffsetMax, t);
|
||||
_rectTransform.pivot = Vector2.Lerp(startPivot, _miniPivot, t);
|
||||
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Ensure exact mini state restored
|
||||
RestoreMiniState();
|
||||
|
||||
_isAnimating = false;
|
||||
}
|
||||
|
||||
// ─── UI Creation ─────────────────────────────────────────
|
||||
|
||||
private void CreateOverlay()
|
||||
{
|
||||
if (_parentCanvas == null) return;
|
||||
|
||||
_overlay = new GameObject("MinimapExpandOverlay");
|
||||
_overlay.transform.SetParent(_parentCanvas.transform, false);
|
||||
|
||||
RectTransform overlayRect = _overlay.AddComponent<RectTransform>();
|
||||
overlayRect.anchorMin = Vector2.zero;
|
||||
overlayRect.anchorMax = Vector2.one;
|
||||
overlayRect.offsetMin = Vector2.zero;
|
||||
overlayRect.offsetMax = Vector2.zero;
|
||||
|
||||
Image overlayImage = _overlay.AddComponent<Image>();
|
||||
overlayImage.color = new Color(0f, 0f, 0f, overlayAlpha);
|
||||
overlayImage.raycastTarget = true;
|
||||
|
||||
// Clicking the overlay also closes the expanded map
|
||||
Button overlayButton = _overlay.AddComponent<Button>();
|
||||
overlayButton.transition = Selectable.Transition.None;
|
||||
overlayButton.onClick.AddListener(Collapse);
|
||||
|
||||
_overlay.SetActive(false);
|
||||
}
|
||||
|
||||
private void CreateCloseButton()
|
||||
{
|
||||
if (_parentCanvas == null) return;
|
||||
|
||||
_closeButton = new GameObject("MinimapCloseButton");
|
||||
_closeButton.transform.SetParent(_parentCanvas.transform, false);
|
||||
|
||||
RectTransform btnRect = _closeButton.AddComponent<RectTransform>();
|
||||
btnRect.anchorMin = new Vector2(1f, 1f);
|
||||
btnRect.anchorMax = new Vector2(1f, 1f);
|
||||
btnRect.pivot = new Vector2(1f, 1f);
|
||||
btnRect.sizeDelta = new Vector2(closeButtonSize, closeButtonSize);
|
||||
btnRect.anchoredPosition = new Vector2(-20f, -20f);
|
||||
|
||||
Image btnImage = _closeButton.AddComponent<Image>();
|
||||
|
||||
if (closeButtonSprite != null)
|
||||
{
|
||||
btnImage.sprite = closeButtonSprite;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create a simple "X" close icon procedurally
|
||||
btnImage.color = new Color(1f, 1f, 1f, 0.9f);
|
||||
btnImage.sprite = CreateCloseIcon();
|
||||
}
|
||||
|
||||
Button btn = _closeButton.AddComponent<Button>();
|
||||
btn.targetGraphic = btnImage;
|
||||
btn.onClick.AddListener(Collapse);
|
||||
|
||||
// Add "X" text as fallback
|
||||
GameObject textObj = new GameObject("CloseText");
|
||||
textObj.transform.SetParent(_closeButton.transform, false);
|
||||
|
||||
RectTransform textRect = textObj.AddComponent<RectTransform>();
|
||||
textRect.anchorMin = Vector2.zero;
|
||||
textRect.anchorMax = Vector2.one;
|
||||
textRect.offsetMin = Vector2.zero;
|
||||
textRect.offsetMax = Vector2.zero;
|
||||
|
||||
Text closeText = textObj.AddComponent<Text>();
|
||||
closeText.text = "✕";
|
||||
closeText.fontSize = (int)(closeButtonSize * 0.6f);
|
||||
closeText.alignment = TextAnchor.MiddleCenter;
|
||||
closeText.color = Color.white;
|
||||
closeText.font = Font.CreateDynamicFontFromOSFont("Arial", 24);
|
||||
|
||||
_closeButton.SetActive(false);
|
||||
}
|
||||
|
||||
private Sprite CreateCloseIcon()
|
||||
{
|
||||
int size = 64;
|
||||
Texture2D tex = new Texture2D(size, size);
|
||||
tex.filterMode = FilterMode.Bilinear;
|
||||
Color[] colors = new Color[size * size];
|
||||
|
||||
// Semi-transparent dark circle
|
||||
Vector2 center = new Vector2(size / 2f, size / 2f);
|
||||
float radius = size / 2f;
|
||||
|
||||
for (int y = 0; y < size; y++)
|
||||
{
|
||||
for (int x = 0; x < size; x++)
|
||||
{
|
||||
float dist = Vector2.Distance(new Vector2(x, y), center);
|
||||
if (dist <= radius)
|
||||
{
|
||||
float edgeFade = Mathf.Clamp01((radius - dist) * 2f / radius);
|
||||
colors[y * size + x] = new Color(0.2f, 0.2f, 0.2f, 0.8f * edgeFade);
|
||||
}
|
||||
else
|
||||
{
|
||||
colors[y * size + x] = Color.clear;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tex.SetPixels(colors);
|
||||
tex.Apply();
|
||||
|
||||
return Sprite.Create(tex, new Rect(0, 0, size, size), new Vector2(0.5f, 0.5f));
|
||||
}
|
||||
|
||||
// ─── Public API ──────────────────────────────────────────
|
||||
|
||||
/// <summary>Is the minimap currently expanded?</summary>
|
||||
public bool IsExpanded => _isExpanded;
|
||||
|
||||
/// <summary>Toggle between expanded and mini views.</summary>
|
||||
public void Toggle()
|
||||
{
|
||||
if (_isExpanded) Collapse();
|
||||
else Expand();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (_overlay != null) Destroy(_overlay);
|
||||
if (_closeButton != null) Destroy(_closeButton);
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Managers/MinimapExpandController.cs.meta
Normal file
2
Assets/Scripts/Managers/MinimapExpandController.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 769db2db0160aec2dba653f888e91439
|
||||
208
Assets/Scripts/Managers/MinimapManager.cs
Normal file
208
Assets/Scripts/Managers/MinimapManager.cs
Normal file
@@ -0,0 +1,208 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
public class MinimapManager : MonoBehaviour
|
||||
{
|
||||
[Header("Camera/RenderTexture Settings")]
|
||||
[Tooltip("Optional: If set, the minimap camera will render into this RenderTexture.")]
|
||||
public RenderTexture targetTexture;
|
||||
[Tooltip("Optional: If set, this RawImage's texture will be assigned to the target RenderTexture.")]
|
||||
public RawImage targetRawImage;
|
||||
[Tooltip("Layers rendered by the minimap camera (include 'Minimap' layer here).")]
|
||||
public LayerMask cullingMask = ~0;
|
||||
|
||||
[Header("Follow Settings")]
|
||||
public Vector3 offset = new Vector3(0f, 50f, 0f);
|
||||
[Tooltip("GTA V style: map rotates with player heading, player icon stays centered pointing up.")]
|
||||
public bool rotateWithPlayer = true;
|
||||
[Tooltip("How fast the minimap rotation follows the player heading. Higher = snappier.")]
|
||||
public float rotationSmoothSpeed = 10f;
|
||||
|
||||
[Header("Zoom Settings")]
|
||||
public float defaultZoom = 50f;
|
||||
public float zoomLerpSpeed = 5f;
|
||||
|
||||
private Transform player;
|
||||
private Camera minimapCam;
|
||||
private float targetZoom;
|
||||
|
||||
private bool isCashSystemMode = false;
|
||||
private float refreshTimer = 0f;
|
||||
private float refreshInterval = 1f;
|
||||
|
||||
/// <summary>
|
||||
/// The camera's current smoothed Y heading angle.
|
||||
/// MinimapMarker reads this to match the player chevron rotation exactly.
|
||||
/// </summary>
|
||||
public static float SmoothedHeading { get; private set; }
|
||||
|
||||
void Awake()
|
||||
{
|
||||
minimapCam = GetComponent<Camera>();
|
||||
if (minimapCam == null)
|
||||
{
|
||||
Debug.LogError("MinimapManager must be placed on a Camera object.");
|
||||
enabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
minimapCam.orthographic = true;
|
||||
minimapCam.orthographicSize = defaultZoom;
|
||||
minimapCam.nearClipPlane = 0.01f;
|
||||
minimapCam.farClipPlane = 1000f;
|
||||
minimapCam.clearFlags = CameraClearFlags.SolidColor;
|
||||
minimapCam.backgroundColor = new Color(0, 0, 0, 0);
|
||||
|
||||
// Ensure Minimap layer is visible
|
||||
int minimapLayer = LayerMask.NameToLayer("Minimap");
|
||||
if (minimapLayer >= 0)
|
||||
{
|
||||
cullingMask |= (1 << minimapLayer);
|
||||
Debug.Log($"[MinimapManager] Found Minimap layer: {minimapLayer}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// FALLBACK: If Minimap layer doesn't exist, use layer 31 (where markers fall back to)
|
||||
cullingMask |= (1 << 31);
|
||||
Debug.LogWarning("[MinimapManager] 'Minimap' layer not found! Using layer 31 fallback. " +
|
||||
"Create 'Minimap' layer in Project Settings for best results.");
|
||||
}
|
||||
|
||||
// Legacy layer support
|
||||
int iconsLayer = LayerMask.NameToLayer("minimapIcons");
|
||||
if (iconsLayer >= 0)
|
||||
{
|
||||
cullingMask |= (1 << iconsLayer);
|
||||
}
|
||||
|
||||
minimapCam.cullingMask = cullingMask;
|
||||
|
||||
if (targetTexture != null)
|
||||
{
|
||||
minimapCam.targetTexture = targetTexture;
|
||||
if (targetRawImage != null)
|
||||
{
|
||||
targetRawImage.texture = targetTexture;
|
||||
}
|
||||
}
|
||||
targetZoom = defaultZoom;
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
// Check game mode
|
||||
isCashSystemMode = SelectionOptions.Instance != null && SelectionOptions.Instance.isCashSystemSelected;
|
||||
|
||||
FindPlayer();
|
||||
|
||||
// IMPORTANT: Ensure MinimapMarkerManager exists for markers in ALL modes
|
||||
MinimapMarkerManager.EnsureExists();
|
||||
|
||||
// Create edge indicators for off-screen entities
|
||||
if (targetRawImage != null)
|
||||
{
|
||||
MinimapEdgeIndicator.AttachTo(targetRawImage.rectTransform);
|
||||
|
||||
// Add click-to-expand controller
|
||||
if (targetRawImage.GetComponent<MinimapExpandController>() == null)
|
||||
targetRawImage.gameObject.AddComponent<MinimapExpandController>();
|
||||
}
|
||||
|
||||
Debug.Log($"[MinimapManager] Initialized - Cash System Mode: {isCashSystemMode}. Using Markers Only.");
|
||||
Debug.Log("[MinimapManager] IMPORTANT: Main Camera must EXCLUDE 'Minimap' layer from Culling Mask!");
|
||||
}
|
||||
|
||||
void LateUpdate()
|
||||
{
|
||||
if (player == null)
|
||||
{
|
||||
// Try to find player periodically
|
||||
FindPlayer();
|
||||
return;
|
||||
}
|
||||
|
||||
// Periodic check to switch targets if control changed
|
||||
refreshTimer += Time.deltaTime;
|
||||
if (refreshTimer >= refreshInterval)
|
||||
{
|
||||
refreshTimer = 0f;
|
||||
FindPlayer();
|
||||
}
|
||||
|
||||
// --- Camera follow ---
|
||||
transform.position = player.position + offset;
|
||||
|
||||
if (rotateWithPlayer)
|
||||
{
|
||||
// Smooth rotation for GTA V feel — no jerky snapping
|
||||
Quaternion targetRotation = Quaternion.Euler(90f, player.eulerAngles.y, 0f);
|
||||
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, Time.deltaTime * rotationSmoothSpeed);
|
||||
|
||||
// Expose the ACTUAL smoothed angle so markers can match perfectly
|
||||
SmoothedHeading = transform.eulerAngles.y;
|
||||
}
|
||||
else
|
||||
{
|
||||
transform.rotation = Quaternion.Euler(90f, 0f, 0f);
|
||||
SmoothedHeading = 0f;
|
||||
}
|
||||
|
||||
// Smooth zoom
|
||||
minimapCam.orthographicSize = Mathf.Lerp(
|
||||
minimapCam.orthographicSize,
|
||||
targetZoom,
|
||||
Time.deltaTime * zoomLerpSpeed
|
||||
);
|
||||
}
|
||||
|
||||
private void FindPlayer()
|
||||
{
|
||||
// In Cash System mode, try to find current controlled character
|
||||
if (isCashSystemMode)
|
||||
{
|
||||
CashSystemManager csm = FindObjectOfType<CashSystemManager>();
|
||||
if (csm != null && csm.CurrentControlledCharacter != null)
|
||||
{
|
||||
player = csm.CurrentControlledCharacter.transform;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to Player tag
|
||||
if (player == null)
|
||||
{
|
||||
GameObject p = GameObject.FindGameObjectWithTag("Player");
|
||||
if (p != null)
|
||||
{
|
||||
player = p.transform;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === Public APIs ===
|
||||
public void SetTarget(Transform newTarget)
|
||||
{
|
||||
player = newTarget;
|
||||
}
|
||||
|
||||
public void SetZoom(float zoomSize)
|
||||
{
|
||||
targetZoom = Mathf.Max(1f, zoomSize);
|
||||
}
|
||||
|
||||
public void ZoomIn(float amount = 5f)
|
||||
{
|
||||
SetZoom(targetZoom - amount);
|
||||
}
|
||||
|
||||
public void ZoomOut(float amount = 5f)
|
||||
{
|
||||
SetZoom(targetZoom + amount);
|
||||
}
|
||||
|
||||
// Kept for compatibility, but does nothing now as markers manage themselves
|
||||
public void ForceRefresh()
|
||||
{
|
||||
FindPlayer();
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Managers/MinimapManager.cs.meta
Normal file
2
Assets/Scripts/Managers/MinimapManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7824f007a8e1ff1d28a9b02acd4e1526
|
||||
351
Assets/Scripts/Managers/MinimapMarker.cs
Normal file
351
Assets/Scripts/Managers/MinimapMarker.cs
Normal file
@@ -0,0 +1,351 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// MinimapMarker - Creates a 3D marker visible to the minimap camera.
|
||||
/// This follows the same pattern as TeamIndicator but renders on a minimap-specific layer.
|
||||
/// Attach to character prefabs or add dynamically via MinimapMarkerManager.
|
||||
///
|
||||
/// IMPORTANT: You MUST create a layer called "Minimap" in Unity Project Settings
|
||||
/// and configure your Main Camera to NOT render this layer (uncheck it in Culling Mask).
|
||||
/// </summary>
|
||||
public class MinimapMarker : MonoBehaviour
|
||||
{
|
||||
[Header("Marker Settings")]
|
||||
[SerializeField] private float heightOffset = 5f; // Height above character for minimap visibility
|
||||
[SerializeField] private float markerSize = 6f; // Size of marker in world units
|
||||
|
||||
[Header("Team Colors")]
|
||||
[SerializeField] private Color allyColor = new Color(0.2f, 0.8f, 0.2f, 1f); // Green
|
||||
[SerializeField] private Color enemyColor = new Color(0.9f, 0.2f, 0.2f, 1f); // Red
|
||||
[SerializeField] private Color controlledColor = Color.white; // White (GTA V style)
|
||||
|
||||
[Header("Layer Settings")]
|
||||
[SerializeField] private string minimapLayerName = "Minimap";
|
||||
|
||||
private GameObject markerObject;
|
||||
private MeshRenderer markerRenderer;
|
||||
private TeamMember teamMember;
|
||||
private string myTeamTag;
|
||||
private bool isCurrentlyControlled = false;
|
||||
private bool hasConfiguredLayer = false;
|
||||
|
||||
// Cached textures to avoid regeneration
|
||||
private static Texture2D cachedCircleTexture;
|
||||
private static Texture2D cachedArrowTexture;
|
||||
|
||||
// Static reference to current player team tag
|
||||
private static string currentPlayerTeamTag = "Player";
|
||||
|
||||
private bool _markerCreated;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// Works in ALL modes (Cash System, Practice, etc.)
|
||||
teamMember = GetComponent<TeamMember>();
|
||||
myTeamTag = gameObject.tag;
|
||||
|
||||
CreateMarker();
|
||||
UpdateMarkerColor();
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
if (markerObject == null) return;
|
||||
|
||||
// Ensure layer is correctly set (handles late layer creation)
|
||||
if (!hasConfiguredLayer)
|
||||
{
|
||||
ConfigureLayer();
|
||||
}
|
||||
|
||||
// Check if this character is currently controlled
|
||||
bool wasControlled = isCurrentlyControlled;
|
||||
isCurrentlyControlled = teamMember != null && teamMember.IsPlayerControlled;
|
||||
|
||||
if (isCurrentlyControlled)
|
||||
{
|
||||
currentPlayerTeamTag = myTeamTag;
|
||||
|
||||
// PARENT to player so position + yaw follow exactly (no lag/jitter)
|
||||
if (markerObject.transform.parent != transform)
|
||||
markerObject.transform.SetParent(transform, false);
|
||||
|
||||
// Local position: straight up; local rotation: flat quad facing up
|
||||
// The minimap camera rotates with the player, so the marker just
|
||||
// sits above the player and the camera handles heading visuals.
|
||||
markerObject.transform.localPosition = Vector3.up * heightOffset;
|
||||
markerObject.transform.localRotation = Quaternion.Euler(90f, 0f, 0f);
|
||||
|
||||
markerObject.transform.localScale = Vector3.one * (markerSize * 2f);
|
||||
}
|
||||
else
|
||||
{
|
||||
// AI / enemy markers: also parent to owner so they can't drift
|
||||
if (markerObject.transform.parent != transform)
|
||||
markerObject.transform.SetParent(transform, false);
|
||||
|
||||
markerObject.transform.localPosition = Vector3.up * heightOffset;
|
||||
markerObject.transform.localRotation = Quaternion.Euler(90f, 0f, 0f);
|
||||
markerObject.transform.localScale = Vector3.one * markerSize;
|
||||
}
|
||||
|
||||
// Update color/shape if control status changed
|
||||
if (wasControlled != isCurrentlyControlled)
|
||||
{
|
||||
UpdateMarkerVisuals();
|
||||
}
|
||||
|
||||
markerObject.SetActive(true);
|
||||
}
|
||||
|
||||
private void CreateMarker()
|
||||
{
|
||||
// Guard: only ever create ONE marker per component instance
|
||||
if (_markerCreated && markerObject != null) return;
|
||||
|
||||
markerObject = GameObject.CreatePrimitive(PrimitiveType.Quad);
|
||||
markerObject.name = $"{gameObject.name}_MinimapMarker";
|
||||
|
||||
// Parent to owner immediately so it follows position + rotation
|
||||
markerObject.transform.SetParent(transform, false);
|
||||
markerObject.transform.localPosition = Vector3.up * heightOffset;
|
||||
markerObject.transform.localRotation = Quaternion.Euler(90f, 0f, 0f);
|
||||
markerObject.transform.localScale = Vector3.one * markerSize;
|
||||
|
||||
// Remove collider - we don't need it
|
||||
Collider col = markerObject.GetComponent<Collider>();
|
||||
if (col != null) Destroy(col);
|
||||
|
||||
// Configure layer
|
||||
ConfigureLayer();
|
||||
|
||||
// Get renderer and create material
|
||||
markerRenderer = markerObject.GetComponent<MeshRenderer>();
|
||||
|
||||
Material markerMaterial = new Material(Shader.Find("Sprites/Default"));
|
||||
if (markerMaterial != null)
|
||||
{
|
||||
markerRenderer.material = markerMaterial;
|
||||
}
|
||||
|
||||
_markerCreated = true;
|
||||
Debug.Log($"[MinimapMarker] Created marker for {gameObject.name} on layer {markerObject.layer}");
|
||||
}
|
||||
|
||||
private void ConfigureLayer()
|
||||
{
|
||||
int minimapLayer = LayerMask.NameToLayer(minimapLayerName);
|
||||
|
||||
if (minimapLayer == -1)
|
||||
{
|
||||
// Layer not found - this is a critical setup issue
|
||||
// Log error once and use a high layer number that's less likely to be rendered
|
||||
if (!hasConfiguredLayer)
|
||||
{
|
||||
Debug.LogError($"[MinimapMarker] CRITICAL: Layer '{minimapLayerName}' not found! " +
|
||||
"Please create this layer in Project Settings > Tags and Layers. " +
|
||||
"IMPORTANT: Set your Main Camera's Culling Mask to EXCLUDE the 'Minimap' layer!");
|
||||
}
|
||||
// Use layer 31 as fallback (typically unused, less likely to show on main camera)
|
||||
markerObject.layer = 31;
|
||||
}
|
||||
else
|
||||
{
|
||||
markerObject.layer = minimapLayer;
|
||||
hasConfiguredLayer = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateMarkerVisuals()
|
||||
{
|
||||
if (markerRenderer == null) return;
|
||||
|
||||
Color targetColor;
|
||||
Texture2D texture;
|
||||
|
||||
// Currently controlled character
|
||||
if (isCurrentlyControlled)
|
||||
{
|
||||
targetColor = controlledColor;
|
||||
texture = GetArrowTexture(); // Directional arrow
|
||||
}
|
||||
// Determine if ally or enemy relative to player
|
||||
else if (myTeamTag == currentPlayerTeamTag)
|
||||
{
|
||||
targetColor = allyColor;
|
||||
texture = GetCircleTexture(); // Round circle
|
||||
}
|
||||
else
|
||||
{
|
||||
targetColor = enemyColor;
|
||||
texture = GetCircleTexture(); // Round circle
|
||||
}
|
||||
|
||||
markerRenderer.material.color = targetColor;
|
||||
markerRenderer.material.mainTexture = texture;
|
||||
}
|
||||
|
||||
// Get cached circle texture
|
||||
private Texture2D GetCircleTexture()
|
||||
{
|
||||
if (cachedCircleTexture == null)
|
||||
{
|
||||
cachedCircleTexture = GenerateCircleTexture();
|
||||
}
|
||||
return cachedCircleTexture;
|
||||
}
|
||||
|
||||
// Get cached arrow texture
|
||||
private Texture2D GetArrowTexture()
|
||||
{
|
||||
if (cachedArrowTexture == null)
|
||||
{
|
||||
cachedArrowTexture = GenerateArrowTexture();
|
||||
}
|
||||
return cachedArrowTexture;
|
||||
}
|
||||
|
||||
// Generate a clean solid dot — GTA V style (filled circle with soft edge)
|
||||
private Texture2D GenerateCircleTexture()
|
||||
{
|
||||
int size = 64;
|
||||
Texture2D tex = new Texture2D(size, size);
|
||||
Color[] colors = new Color[size * size];
|
||||
float center = size / 2f;
|
||||
float radius = size / 2f - 4f; // Slightly smaller for clean edges
|
||||
|
||||
for (int y = 0; y < size; y++)
|
||||
{
|
||||
for (int x = 0; x < size; x++)
|
||||
{
|
||||
float dist = Vector2.Distance(new Vector2(x, y), new Vector2(center, center));
|
||||
|
||||
if (dist <= radius - 1.5f)
|
||||
{
|
||||
// Solid fill
|
||||
colors[y * size + x] = Color.white;
|
||||
}
|
||||
else if (dist <= radius + 1.5f)
|
||||
{
|
||||
// Anti-aliased edge
|
||||
float alpha = 1f - Mathf.Clamp01((dist - radius + 1.5f) / 3f);
|
||||
colors[y * size + x] = new Color(1, 1, 1, alpha);
|
||||
}
|
||||
else
|
||||
{
|
||||
colors[y * size + x] = Color.clear;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tex.SetPixels(colors);
|
||||
tex.Apply();
|
||||
return tex;
|
||||
}
|
||||
|
||||
// Generate a GTA V-style chevron (V-shape) pointing up
|
||||
// Sharp pointed front, open back — gives instant directional clarity
|
||||
private Texture2D GenerateArrowTexture()
|
||||
{
|
||||
int size = 64;
|
||||
Texture2D tex = new Texture2D(size, size);
|
||||
Color[] colors = new Color[size * size];
|
||||
|
||||
// Clear transparency
|
||||
for (int i = 0; i < colors.Length; i++) colors[i] = Color.clear;
|
||||
|
||||
float cx = size / 2f;
|
||||
float cy = size / 2f;
|
||||
float thickness = 5f; // Chevron line thickness
|
||||
|
||||
// Chevron is two angled lines meeting at a point (top center)
|
||||
// Left arm: from bottom-left to top-center
|
||||
// Right arm: from bottom-right to top-center
|
||||
Vector2 tip = new Vector2(cx, size * 0.85f); // Top tip
|
||||
Vector2 bottomLeft = new Vector2(size * 0.15f, size * 0.25f); // Bottom left
|
||||
Vector2 bottomRight = new Vector2(size * 0.85f, size * 0.25f); // Bottom right
|
||||
|
||||
for (int y = 0; y < size; y++)
|
||||
{
|
||||
for (int x = 0; x < size; x++)
|
||||
{
|
||||
Vector2 p = new Vector2(x, y);
|
||||
|
||||
// Distance to left arm line segment
|
||||
float distLeft = DistanceToSegment(p, bottomLeft, tip);
|
||||
// Distance to right arm line segment
|
||||
float distRight = DistanceToSegment(p, bottomRight, tip);
|
||||
|
||||
float minDist = Mathf.Min(distLeft, distRight);
|
||||
|
||||
if (minDist <= thickness)
|
||||
{
|
||||
// Solid white with anti-aliased edges
|
||||
float alpha = 1f - Mathf.Clamp01((minDist - thickness + 1.5f) / 1.5f);
|
||||
alpha = Mathf.Max(alpha, 0f);
|
||||
if (minDist < thickness - 1f) alpha = 1f;
|
||||
colors[y * size + x] = new Color(1, 1, 1, alpha);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tex.SetPixels(colors);
|
||||
tex.Apply();
|
||||
return tex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Distance from point P to line segment AB
|
||||
/// </summary>
|
||||
private static float DistanceToSegment(Vector2 p, Vector2 a, Vector2 b)
|
||||
{
|
||||
Vector2 ab = b - a;
|
||||
float t = Mathf.Clamp01(Vector2.Dot(p - a, ab) / Vector2.Dot(ab, ab));
|
||||
Vector2 closest = a + t * ab;
|
||||
return Vector2.Distance(p, closest);
|
||||
}
|
||||
|
||||
private void UpdateMarkerColor()
|
||||
{
|
||||
UpdateMarkerVisuals();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (markerObject != null)
|
||||
{
|
||||
Destroy(markerObject);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Force update the marker color (call after team changes)
|
||||
/// </summary>
|
||||
public void RefreshMarker()
|
||||
{
|
||||
myTeamTag = gameObject.tag;
|
||||
UpdateMarkerColor();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set marker visibility
|
||||
/// </summary>
|
||||
public void SetVisible(bool visible)
|
||||
{
|
||||
if (markerObject != null)
|
||||
{
|
||||
markerObject.SetActive(visible);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Static method to update all markers when player changes
|
||||
/// </summary>
|
||||
public static void RefreshAllMarkers()
|
||||
{
|
||||
foreach (var marker in FindObjectsOfType<MinimapMarker>())
|
||||
{
|
||||
marker.RefreshMarker();
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Managers/MinimapMarker.cs.meta
Normal file
2
Assets/Scripts/Managers/MinimapMarker.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 42cd135aef6efab3493efe951976dec3
|
||||
182
Assets/Scripts/Managers/MinimapMarkerManager.cs
Normal file
182
Assets/Scripts/Managers/MinimapMarkerManager.cs
Normal file
@@ -0,0 +1,182 @@
|
||||
using UnityEngine;
|
||||
using System.Collections;
|
||||
|
||||
/// <summary>
|
||||
/// MinimapMarkerManager - Automatically adds MinimapMarker components to all characters.
|
||||
/// Works in BOTH Cash System mode AND Practice mode.
|
||||
/// Attach to a manager object in the scene or it will be created automatically.
|
||||
/// </summary>
|
||||
public class MinimapMarkerManager : MonoBehaviour
|
||||
{
|
||||
[Header("Settings")]
|
||||
[SerializeField] private bool autoSetupOnStart = true;
|
||||
[SerializeField] private float refreshInterval = 2f;
|
||||
|
||||
private static MinimapMarkerManager instance;
|
||||
public static MinimapMarkerManager Instance => instance;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (instance != null && instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
instance = this;
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// REMOVED: Cash System only check - now works in ALL modes
|
||||
// This allows minimap markers in Practice mode too
|
||||
|
||||
// Configure Main Camera to NOT render minimap layers
|
||||
ConfigureMainCamera();
|
||||
|
||||
if (autoSetupOnStart)
|
||||
{
|
||||
StartCoroutine(SetupMarkersDelayed());
|
||||
}
|
||||
|
||||
Debug.Log("[MinimapMarkerManager] Initialized - works in all game modes");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configure Main Camera to exclude minimap marker layers
|
||||
/// </summary>
|
||||
private void ConfigureMainCamera()
|
||||
{
|
||||
Camera mainCam = Camera.main;
|
||||
if (mainCam == null) return;
|
||||
|
||||
int minimapLayer = LayerMask.NameToLayer("Minimap");
|
||||
|
||||
if (minimapLayer >= 0)
|
||||
{
|
||||
// Remove Minimap layer from main camera culling mask
|
||||
mainCam.cullingMask &= ~(1 << minimapLayer);
|
||||
Debug.Log($"[MinimapMarkerManager] Excluded 'Minimap' layer from Main Camera");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Remove layer 31 (fallback) from main camera culling mask
|
||||
mainCam.cullingMask &= ~(1 << 31);
|
||||
Debug.Log("[MinimapMarkerManager] Excluded layer 31 (fallback) from Main Camera");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private IEnumerator SetupMarkersDelayed()
|
||||
{
|
||||
// Wait for characters to spawn
|
||||
yield return new WaitForSeconds(1.5f);
|
||||
|
||||
SetupAllMarkers();
|
||||
|
||||
// Periodic refresh for newly spawned characters
|
||||
while (enabled)
|
||||
{
|
||||
yield return new WaitForSeconds(refreshInterval);
|
||||
RefreshMarkers();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add MinimapMarker to all characters that don't have one
|
||||
/// </summary>
|
||||
public void SetupAllMarkers()
|
||||
{
|
||||
// Find all team members (characters)
|
||||
TeamMember[] allTeamMembers = FindObjectsOfType<TeamMember>();
|
||||
foreach (var member in allTeamMembers)
|
||||
{
|
||||
AddMarkerIfMissing(member.gameObject);
|
||||
}
|
||||
|
||||
// Find all characters with HealthNew (covers AI enemies too)
|
||||
HealthNew[] allCharacters = FindObjectsOfType<HealthNew>();
|
||||
foreach (var character in allCharacters)
|
||||
{
|
||||
AddMarkerIfMissing(character.gameObject);
|
||||
}
|
||||
|
||||
// Find all CashBags and add object markers (only relevant in Cash System)
|
||||
CashBag[] allCashBags = FindObjectsOfType<CashBag>();
|
||||
foreach (var cashBag in allCashBags)
|
||||
{
|
||||
AddObjectMarkerIfMissing(cashBag.gameObject);
|
||||
}
|
||||
|
||||
// Find all TeamVaults and add object markers
|
||||
TeamVault[] allVaults = FindObjectsOfType<TeamVault>();
|
||||
foreach (var vault in allVaults)
|
||||
{
|
||||
AddObjectMarkerIfMissing(vault.gameObject);
|
||||
}
|
||||
|
||||
Debug.Log($"[MinimapMarkerManager] Setup markers: {allTeamMembers.Length} characters, " +
|
||||
$"{allCashBags.Length} cashbags, {allVaults.Length} vaults");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check for new characters and objects and add markers
|
||||
/// </summary>
|
||||
public void RefreshMarkers()
|
||||
{
|
||||
// Only add markers to NEW objects that don't have them yet
|
||||
// The "IfMissing" check prevents duplicates
|
||||
TeamMember[] allTeamMembers = FindObjectsOfType<TeamMember>();
|
||||
foreach (var member in allTeamMembers)
|
||||
{
|
||||
AddMarkerIfMissing(member.gameObject);
|
||||
}
|
||||
|
||||
// NOTE: We intentionally skip HealthNew scan to avoid double-adding
|
||||
// to characters that already got a marker via TeamMember above.
|
||||
|
||||
// Skip CashBag/Vault refresh — they get their marker once in SetupAllMarkers
|
||||
// and the component stays on the prefab/instance. Re-scanning caused duplicates.
|
||||
|
||||
// Refresh colors on all markers
|
||||
MinimapMarker.RefreshAllMarkers();
|
||||
}
|
||||
|
||||
private void AddObjectMarkerIfMissing(GameObject obj)
|
||||
{
|
||||
if (obj.GetComponent<MinimapObjectMarker>() == null)
|
||||
{
|
||||
obj.AddComponent<MinimapObjectMarker>();
|
||||
Debug.Log($"[MinimapMarkerManager] Added MinimapObjectMarker to {obj.name}");
|
||||
}
|
||||
}
|
||||
|
||||
private void AddMarkerIfMissing(GameObject character)
|
||||
{
|
||||
if (character.GetComponent<MinimapMarker>() == null)
|
||||
{
|
||||
character.AddComponent<MinimapMarker>();
|
||||
Debug.Log($"[MinimapMarkerManager] Added MinimapMarker to {character.name}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Manually add marker to a specific character
|
||||
/// </summary>
|
||||
public void AddMarker(GameObject character)
|
||||
{
|
||||
AddMarkerIfMissing(character);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Static helper to ensure manager exists in scene
|
||||
/// </summary>
|
||||
public static void EnsureExists()
|
||||
{
|
||||
if (instance == null)
|
||||
{
|
||||
GameObject managerObj = new GameObject("MinimapMarkerManager");
|
||||
managerObj.AddComponent<MinimapMarkerManager>();
|
||||
Debug.Log("[MinimapMarkerManager] Created MinimapMarkerManager automatically");
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Managers/MinimapMarkerManager.cs.meta
Normal file
2
Assets/Scripts/Managers/MinimapMarkerManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4dc46171ed0cac350b87d157de84b922
|
||||
313
Assets/Scripts/Managers/MinimapObjectMarker.cs
Normal file
313
Assets/Scripts/Managers/MinimapObjectMarker.cs
Normal file
@@ -0,0 +1,313 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// MinimapObjectMarker - Creates minimap markers for cash bags and vaults.
|
||||
/// Uses different shapes than character markers for differentiation.
|
||||
/// CashBag = Square, Vaults = Diamond
|
||||
///
|
||||
/// IMPORTANT: You MUST create a layer called "Minimap" in Unity Project Settings
|
||||
/// and configure your Main Camera to NOT render this layer.
|
||||
/// </summary>
|
||||
public class MinimapObjectMarker : MonoBehaviour
|
||||
{
|
||||
public enum ObjectType
|
||||
{
|
||||
CashBag,
|
||||
PlayerVault,
|
||||
EnemyVault
|
||||
}
|
||||
|
||||
[Header("Marker Settings")]
|
||||
[SerializeField] private float heightOffset = 3f;
|
||||
[SerializeField] private float markerSize = 5f;
|
||||
[SerializeField] private ObjectType objectType = ObjectType.CashBag;
|
||||
|
||||
[Header("Colors")]
|
||||
[SerializeField] private Color cashBagColor = new Color(1f, 0.85f, 0f, 1f); // Gold/Yellow
|
||||
[SerializeField] private Color playerVaultColor = new Color(0.2f, 0.8f, 0.2f, 1f); // Green
|
||||
[SerializeField] private Color enemyVaultColor = new Color(0.9f, 0.2f, 0.2f, 1f); // Red
|
||||
|
||||
[Header("Layer Settings")]
|
||||
[SerializeField] private string minimapLayerName = "Minimap";
|
||||
|
||||
private GameObject markerObject;
|
||||
private MeshRenderer markerRenderer;
|
||||
private bool hasConfiguredLayer = false;
|
||||
private bool _markerCreated = false;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
AutoDetectType();
|
||||
|
||||
if (!_markerCreated)
|
||||
CreateMarker();
|
||||
|
||||
UpdateMarkerColor();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
// Re-create marker if it was destroyed (e.g. pooled object returned and re-enabled)
|
||||
if (markerObject == null)
|
||||
{
|
||||
_markerCreated = false;
|
||||
AutoDetectType();
|
||||
CreateMarker();
|
||||
UpdateMarkerColor();
|
||||
}
|
||||
else
|
||||
{
|
||||
markerObject.SetActive(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
// Hide marker when the source object is disabled (pooled/despawned)
|
||||
if (markerObject != null)
|
||||
markerObject.SetActive(false);
|
||||
}
|
||||
|
||||
private void AutoDetectType()
|
||||
{
|
||||
// Check if this is a CashBag
|
||||
if (GetComponent<CashBag>() != null)
|
||||
{
|
||||
objectType = ObjectType.CashBag;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is a TeamVault
|
||||
TeamVault vault = GetComponent<TeamVault>();
|
||||
if (vault != null)
|
||||
{
|
||||
objectType = vault.TeamTag == "Player" ? ObjectType.PlayerVault : ObjectType.EnemyVault;
|
||||
}
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
if (markerObject == null) return;
|
||||
|
||||
// Ensure layer is correctly set
|
||||
if (!hasConfiguredLayer)
|
||||
{
|
||||
ConfigureLayer();
|
||||
}
|
||||
|
||||
// Marker is parented, so local position keeps it above owner
|
||||
// No need to update position every frame — it follows automatically
|
||||
// Only update rotation for the correct shape appearance
|
||||
if (objectType == ObjectType.CashBag)
|
||||
{
|
||||
markerObject.transform.localRotation = Quaternion.Euler(90f, 0f, 0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
markerObject.transform.localRotation = Quaternion.Euler(90f, 45f, 0f);
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateMarker()
|
||||
{
|
||||
// GUARD: prevent duplicate markers
|
||||
if (_markerCreated && markerObject != null) return;
|
||||
|
||||
// Also destroy any stale marker children from previous lifecycle
|
||||
for (int i = transform.childCount - 1; i >= 0; i--)
|
||||
{
|
||||
var child = transform.GetChild(i);
|
||||
if (child.name.Contains("MinimapObjectMarker"))
|
||||
Destroy(child.gameObject);
|
||||
}
|
||||
|
||||
// Use different heights per type to prevent Z-fighting
|
||||
float h = heightOffset;
|
||||
switch (objectType)
|
||||
{
|
||||
case ObjectType.CashBag: h = heightOffset; break;
|
||||
case ObjectType.PlayerVault: h = heightOffset + 1f; break;
|
||||
case ObjectType.EnemyVault: h = heightOffset + 2f; break;
|
||||
}
|
||||
|
||||
markerObject = GameObject.CreatePrimitive(PrimitiveType.Quad);
|
||||
markerObject.name = $"{gameObject.name}_MinimapObjectMarker";
|
||||
|
||||
// Parent to owner so it follows position automatically
|
||||
markerObject.transform.SetParent(transform, false);
|
||||
markerObject.transform.localPosition = Vector3.up * h;
|
||||
|
||||
// Initial rotation based on type
|
||||
if (objectType == ObjectType.CashBag)
|
||||
markerObject.transform.localRotation = Quaternion.Euler(90f, 0f, 0f);
|
||||
else
|
||||
markerObject.transform.localRotation = Quaternion.Euler(90f, 45f, 0f);
|
||||
|
||||
markerObject.transform.localScale = Vector3.one * markerSize;
|
||||
|
||||
// Remove collider
|
||||
Collider col = markerObject.GetComponent<Collider>();
|
||||
if (col != null) Destroy(col);
|
||||
|
||||
// Configure layer
|
||||
ConfigureLayer();
|
||||
|
||||
// Get renderer and create material
|
||||
markerRenderer = markerObject.GetComponent<MeshRenderer>();
|
||||
|
||||
Material markerMaterial = new Material(Shader.Find("Sprites/Default"));
|
||||
if (markerMaterial != null)
|
||||
{
|
||||
markerRenderer.material = markerMaterial;
|
||||
}
|
||||
|
||||
_markerCreated = true;
|
||||
Debug.Log($"[MinimapObjectMarker] Created {objectType} marker for {gameObject.name}");
|
||||
}
|
||||
|
||||
private void ConfigureLayer()
|
||||
{
|
||||
int minimapLayer = LayerMask.NameToLayer(minimapLayerName);
|
||||
|
||||
if (minimapLayer == -1)
|
||||
{
|
||||
if (!hasConfiguredLayer)
|
||||
{
|
||||
Debug.LogError($"[MinimapObjectMarker] CRITICAL: Layer '{minimapLayerName}' not found! " +
|
||||
"Please create this layer in Project Settings > Tags and Layers. " +
|
||||
"Main Camera must EXCLUDE this layer from its Culling Mask!");
|
||||
}
|
||||
// Use layer 31 as fallback
|
||||
markerObject.layer = 31;
|
||||
}
|
||||
else
|
||||
{
|
||||
markerObject.layer = minimapLayer;
|
||||
hasConfiguredLayer = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateMarkerColor()
|
||||
{
|
||||
if (markerRenderer == null) return;
|
||||
|
||||
Color targetColor;
|
||||
|
||||
switch (objectType)
|
||||
{
|
||||
case ObjectType.CashBag:
|
||||
targetColor = cashBagColor;
|
||||
break;
|
||||
case ObjectType.PlayerVault:
|
||||
targetColor = playerVaultColor;
|
||||
break;
|
||||
case ObjectType.EnemyVault:
|
||||
targetColor = enemyVaultColor;
|
||||
break;
|
||||
default:
|
||||
targetColor = Color.white;
|
||||
break;
|
||||
}
|
||||
|
||||
markerRenderer.material.color = targetColor;
|
||||
|
||||
// Set texture — required for Sprites/Default in URP to render visibly
|
||||
Texture2D tex = (objectType == ObjectType.CashBag)
|
||||
? GetSquareTexture()
|
||||
: GetDiamondTexture();
|
||||
markerRenderer.material.mainTexture = tex;
|
||||
}
|
||||
|
||||
// ─── Texture Generation (cached statics, same pattern as MinimapMarker) ───
|
||||
|
||||
private static Texture2D _cachedSquareTex;
|
||||
private static Texture2D _cachedDiamondTex;
|
||||
|
||||
private static Texture2D GetSquareTexture()
|
||||
{
|
||||
if (_cachedSquareTex == null)
|
||||
_cachedSquareTex = GenerateSquareTexture();
|
||||
return _cachedSquareTex;
|
||||
}
|
||||
|
||||
private static Texture2D GetDiamondTexture()
|
||||
{
|
||||
if (_cachedDiamondTex == null)
|
||||
_cachedDiamondTex = GenerateDiamondTexture();
|
||||
return _cachedDiamondTex;
|
||||
}
|
||||
|
||||
private static Texture2D GenerateSquareTexture()
|
||||
{
|
||||
int size = 64;
|
||||
Texture2D tex = new Texture2D(size, size);
|
||||
Color[] colors = new Color[size * size];
|
||||
float border = 4f;
|
||||
|
||||
for (int y = 0; y < size; y++)
|
||||
{
|
||||
for (int x = 0; x < size; x++)
|
||||
{
|
||||
bool inside = x >= border && x < size - border && y >= border && y < size - border;
|
||||
colors[y * size + x] = inside ? Color.white : Color.clear;
|
||||
}
|
||||
}
|
||||
tex.SetPixels(colors);
|
||||
tex.Apply();
|
||||
return tex;
|
||||
}
|
||||
|
||||
private static Texture2D GenerateDiamondTexture()
|
||||
{
|
||||
int size = 64;
|
||||
Texture2D tex = new Texture2D(size, size);
|
||||
Color[] colors = new Color[size * size];
|
||||
float center = size / 2f;
|
||||
float halfSize = center - 4f;
|
||||
|
||||
for (int y = 0; y < size; y++)
|
||||
{
|
||||
for (int x = 0; x < size; x++)
|
||||
{
|
||||
float dx = Mathf.Abs(x - center);
|
||||
float dy = Mathf.Abs(y - center);
|
||||
float dist = dx + dy; // Manhattan distance → diamond shape
|
||||
|
||||
if (dist <= halfSize - 1.5f)
|
||||
colors[y * size + x] = Color.white;
|
||||
else if (dist <= halfSize + 1.5f)
|
||||
{
|
||||
float alpha = 1f - Mathf.Clamp01((dist - halfSize + 1.5f) / 3f);
|
||||
colors[y * size + x] = new Color(1, 1, 1, alpha);
|
||||
}
|
||||
else
|
||||
colors[y * size + x] = Color.clear;
|
||||
}
|
||||
}
|
||||
tex.SetPixels(colors);
|
||||
tex.Apply();
|
||||
return tex;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (markerObject != null)
|
||||
{
|
||||
Destroy(markerObject);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetType(ObjectType type)
|
||||
{
|
||||
objectType = type;
|
||||
UpdateMarkerColor();
|
||||
}
|
||||
|
||||
public void SetVisible(bool visible)
|
||||
{
|
||||
if (markerObject != null)
|
||||
{
|
||||
markerObject.SetActive(visible);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Managers/MinimapObjectMarker.cs.meta
Normal file
2
Assets/Scripts/Managers/MinimapObjectMarker.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 54d706008a28da80291f384e14e26ebf
|
||||
85
Assets/Scripts/Managers/OldUIMenuManager.cs
Normal file
85
Assets/Scripts/Managers/OldUIMenuManager.cs
Normal file
@@ -0,0 +1,85 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
public class OldUIMenuBehaviour : MonoBehaviour
|
||||
{
|
||||
|
||||
/*
|
||||
[Header("References")]
|
||||
public Camera UICamera;
|
||||
private List<PlayerController> activePlayerControllers;
|
||||
private List<UIMenuPlayerDeviceDisplayBehaviour> spawnedUIPlayerDeviceDisplayBehaviours;
|
||||
|
||||
[Header("Placeholder GameObjects")]
|
||||
public GameObject[] placeholderGameObjects;
|
||||
|
||||
[Header("Player Panel Settings")]
|
||||
public GameObject UIPlayerDeviceDisplayPrefab;
|
||||
public Transform UIPlayerDeviceDisplayRoot;
|
||||
|
||||
public void SetupUIMenuPlayerPanelList()
|
||||
{
|
||||
DestroyPlaceholderObjects();
|
||||
GetCurrentPlayerDatas();
|
||||
SetupCurrentPlayerUIMenuPanels();
|
||||
}
|
||||
|
||||
void DestroyPlaceholderObjects()
|
||||
{
|
||||
for(int i = 0; i < placeholderGameObjects.Length; i++)
|
||||
{
|
||||
Destroy(placeholderGameObjects[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void GetCurrentPlayerDatas()
|
||||
{
|
||||
activePlayerControllers = GameManager.Instance.GetActivePlayerControllers();
|
||||
}
|
||||
|
||||
void SetupCurrentPlayerUIMenuPanels()
|
||||
{
|
||||
|
||||
spawnedUIPlayerDeviceDisplayBehaviours = new List<UIMenuPlayerDeviceDisplayBehaviour>();
|
||||
|
||||
for(int i = 0; i < activePlayerControllers.Count; i++)
|
||||
{
|
||||
GameObject spawnedUIPlayerDeviceDisplayPanel = Instantiate(UIPlayerDeviceDisplayPrefab, UIPlayerDeviceDisplayRoot.position, UIPlayerDeviceDisplayRoot.rotation) as GameObject;
|
||||
spawnedUIPlayerDeviceDisplayPanel.transform.SetParent(UIPlayerDeviceDisplayRoot, false);
|
||||
|
||||
PlayerInput spawnedPlayerInput = activePlayerControllers[i].GetPlayerInput();
|
||||
|
||||
int spawnedPlayerIndex = spawnedPlayerInput.playerIndex;
|
||||
string spawnedPlayerDevicePath = spawnedPlayerInput.devices[0].ToString();
|
||||
|
||||
|
||||
|
||||
spawnedUIPlayerDeviceDisplayPanel.GetComponent<PlayerDeviceRebindBehaviour>().playerIndex = spawnedPlayerIndex;
|
||||
spawnedUIPlayerDeviceDisplayBehaviours.Add(spawnedUIPlayerDeviceDisplayPanel.GetComponent<UIMenuPlayerDeviceDisplayBehaviour>());
|
||||
spawnedUIPlayerDeviceDisplayBehaviours[i].SetPlayerDeviceDisplay(spawnedPlayerIndex, spawnedPlayerDevicePath);
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateCurrentPlayerUIMenuPanels()
|
||||
{
|
||||
for(int i = 0; i < spawnedUIPlayerDeviceDisplayBehaviours.Count; i++)
|
||||
{
|
||||
PlayerInput spawnedPlayerInput = activePlayerControllers[i].GetPlayerInput();
|
||||
string spawnedPlayerDevicePath = spawnedPlayerInput.devices[0].ToString();
|
||||
|
||||
spawnedUIPlayerDeviceDisplayBehaviours[i].UpdatePlayerDeviceDisplay(spawnedPlayerDevicePath);
|
||||
}
|
||||
}
|
||||
|
||||
public void ToggleMenu(bool newState)
|
||||
{
|
||||
if(newState == true)
|
||||
{
|
||||
UpdateCurrentPlayerUIMenuPanels();
|
||||
}
|
||||
|
||||
UICamera.enabled = newState;
|
||||
}
|
||||
*/
|
||||
}
|
||||
11
Assets/Scripts/Managers/OldUIMenuManager.cs.meta
Normal file
11
Assets/Scripts/Managers/OldUIMenuManager.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cfad211f8c6ebe348bfc0d302db7da52
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
330
Assets/Scripts/Managers/PlayerManagerNew.cs
Normal file
330
Assets/Scripts/Managers/PlayerManagerNew.cs
Normal file
@@ -0,0 +1,330 @@
|
||||
using System.Linq;
|
||||
using UnityEngine.InputSystem.Users;
|
||||
using UnityEngine.InputSystem;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEngine.UI;
|
||||
using System.Collections.Generic;
|
||||
using static UnityEngine.AudioSettings;
|
||||
using static UnityEngine.EventSystems.EventTrigger;
|
||||
using System;
|
||||
|
||||
|
||||
public class PlayerManagerNew : MonoBehaviour
|
||||
{
|
||||
public Button pauseButton;
|
||||
public Button resumeButton;
|
||||
public Button retryButton;
|
||||
public Button quitButton;
|
||||
public GameObject joystickButton;
|
||||
public GameObject pauseScreen;
|
||||
public bool pause;
|
||||
public bool retry;
|
||||
public Transform spawnPoint1;
|
||||
public Transform spawnPoint2;
|
||||
bool charactersMovedToSpawnPoints;
|
||||
|
||||
GameStatsManager game_statsManager;
|
||||
private GameObject player;
|
||||
private GameObject enemy;
|
||||
public GameObject rewardsScreen;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
//this instntiste the characters from the selection scene
|
||||
SelectionOptions.Instance.eventSytem = GameObject.Find("EventManager");
|
||||
|
||||
// Skip spawning for Cash System mode - CashSystemManager handles its own team spawning
|
||||
if (SelectionOptions.Instance.isCashSystemSelected)
|
||||
{
|
||||
Debug.Log("[PlayerManagerNew] Skipping SpawnPlayers - Cash System mode uses CashSystemManager.SpawnTeams");
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectionOptions.Instance.SpawnPlayers();
|
||||
}
|
||||
//SelectionOptions.Instance.eventSytem.SetActive(false);
|
||||
pause = false;
|
||||
|
||||
try{
|
||||
game_statsManager = (GameObject.Find("GameStatsManager")).GetComponent<GameStatsManager>();
|
||||
//Debug.Log("GameStatsManager");
|
||||
|
||||
}catch(Exception e){
|
||||
Debug.Log("Error: " + e.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
AudioSource audioSource;
|
||||
void Start()
|
||||
{
|
||||
retry = false;
|
||||
charactersMovedToSpawnPoints = false;
|
||||
|
||||
if (UnityEngine.Application.platform == RuntimePlatform.Android ||
|
||||
UnityEngine.Application.platform == RuntimePlatform.IPhonePlayer)
|
||||
{
|
||||
InitializeMobileIngameUI();
|
||||
}
|
||||
audioSource = GetComponent<AudioSource>();
|
||||
LoadAudioSetings();
|
||||
audioSource.Play();
|
||||
|
||||
enemy = GameObject.FindGameObjectWithTag("Enemy");
|
||||
player = GameObject.FindGameObjectWithTag("Player");
|
||||
}
|
||||
|
||||
|
||||
private GameSettings gameSettings;
|
||||
|
||||
private void LoadAudioSetings()
|
||||
{
|
||||
if (audioSource == null) return;
|
||||
|
||||
gameSettings = PlayerPrefs.HasKey(GameSettingsKey)
|
||||
? LoadGameSettings()
|
||||
: new GameSettings();
|
||||
|
||||
// Ensure volume is not zero (default to 1 if zero or negative)
|
||||
float volume = gameSettings.GameplayVolume_value;
|
||||
if (volume <= 0f) volume = 1f;
|
||||
audioSource.volume = volume;
|
||||
Debug.Log($"[PlayerManagerNew] Loaded GameplayVolume_value: {gameSettings.GameplayVolume_value}, Applied: {audioSource.volume}");
|
||||
}
|
||||
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (retry)
|
||||
{
|
||||
retry = false;
|
||||
enemy.SetActive(true);
|
||||
player.SetActive(true);
|
||||
enemy.transform.position = new Vector3(5.32f,0,0);
|
||||
player.transform.position = new Vector3(7.32f,0,0);
|
||||
|
||||
player.GetComponent<HealthNew>().ResetHealth();
|
||||
enemy.GetComponent<HealthNew>().ResetHealth();
|
||||
player.GetComponent<HealthNew>().TakeDamage(0);
|
||||
enemy.GetComponent<HealthNew>().TakeDamage(0);
|
||||
}
|
||||
|
||||
if (!charactersMovedToSpawnPoints)
|
||||
MoveCharactersToSpawnPoints();
|
||||
|
||||
MonitorRounds();
|
||||
}
|
||||
private void InitializeMobileIngameUI()
|
||||
{
|
||||
pauseButton.onClick.AddListener(Pause);
|
||||
joystickButton.gameObject.SetActive(true);
|
||||
|
||||
// NOTE: Block/Dodge buttons are no longer wired here.
|
||||
// Attach DeviantMobile.Input.MobileControlManagerAction to the respective UI objects
|
||||
// and map tap(s) to the desired Input Actions.
|
||||
// This removes direct onClick listeners and unifies mobile input via the Input System.
|
||||
}
|
||||
// Block/Dodge methods removed. Use MobileControlManager on the UI to trigger Input Actions instead.
|
||||
//the below pause, quit, retry, resume,continue, restart functions work only for mobile, for pc they are placed in the healthnew script(due to complex inputsystem required for pc they had to be there)
|
||||
public void Pause()
|
||||
{
|
||||
pause = true;
|
||||
pauseScreen.SetActive(true);
|
||||
if(SelectionOptions.Instance.isAISelected)
|
||||
GameObject.FindGameObjectWithTag("Enemy").GetComponent<CharacterAIController>().attack = false;
|
||||
else
|
||||
GameObject.FindGameObjectWithTag("Enemy").GetComponent<PlayerScript>().attack = false;
|
||||
GameObject.FindGameObjectWithTag("Player").GetComponent<PlayerScript>().attack = false;
|
||||
if (UnityEngine.Application.platform == RuntimePlatform.Android ||
|
||||
UnityEngine.Application.platform == RuntimePlatform.IPhonePlayer)
|
||||
{
|
||||
resumeButton.onClick.AddListener(Resume);
|
||||
retryButton.onClick.AddListener(Retry);
|
||||
quitButton.onClick.AddListener(Quit);
|
||||
}
|
||||
}
|
||||
|
||||
public void Resume()
|
||||
{
|
||||
pause = false;
|
||||
Time.timeScale = 1f;
|
||||
if(SelectionOptions.Instance.isAISelected)
|
||||
GameObject.FindGameObjectWithTag("Enemy").GetComponent<CharacterAIController>().attack = true;
|
||||
else
|
||||
GameObject.FindGameObjectWithTag("Enemy").GetComponent<PlayerScript>().attack = true;
|
||||
GameObject.FindGameObjectWithTag("Player").GetComponent<PlayerScript>().attack = true;
|
||||
pauseScreen.SetActive(false);
|
||||
}
|
||||
|
||||
public void Continue(){
|
||||
game_statsManager.match_end(); //Go to Home
|
||||
game_statsManager.addMatch();
|
||||
}
|
||||
|
||||
public void Quit()
|
||||
{
|
||||
// game_statsManager.match_end();
|
||||
//Reset Missions
|
||||
// game_statsManager.ResetCompletedMissions();
|
||||
// game_statsManager.addMatch();
|
||||
|
||||
game_statsManager.match_quit(); //Go to Home
|
||||
|
||||
// SceneManager.LoadSceneAsync(1);
|
||||
}
|
||||
|
||||
public void Retry()
|
||||
{
|
||||
pause = false;
|
||||
retry = true;
|
||||
player.GetComponent<RoundsScript>().currentRound = 0;
|
||||
player.GetComponent<RoundsScript>().playerWins = 0; player.GetComponent<RoundsScript>().enemyWins = 0; player.GetComponent<RoundsScript>().roundsToPlay = 3;
|
||||
enemy.GetComponent<RoundsScript>().currentRound = 0;
|
||||
enemy.GetComponent<RoundsScript>().playerWins = 0; enemy.GetComponent<RoundsScript>().enemyWins = 0; enemy.GetComponent<RoundsScript>().roundsToPlay = 3;
|
||||
enemy.GetComponent<StaminaSystem>().currentStamina = 100;
|
||||
player.GetComponent<StaminaSystem>().currentStamina = 100;
|
||||
player.GetComponent<RoundsScript>().isRoundActive = false;
|
||||
enemy.GetComponent<RoundsScript>().isRoundActive = false;
|
||||
rewardsScreen.SetActive(false);
|
||||
pauseScreen.SetActive(false);
|
||||
// just reinforce and make sure the characters are active, if not active the startneround won't execute well, especially the coroutines in the player
|
||||
enemy.SetActive(true);
|
||||
player.SetActive(true);
|
||||
player.GetComponent<RoundsScript>().StartNewRound();
|
||||
|
||||
//Reset Missions
|
||||
// game_statsManager.ResetCompletedMissions();
|
||||
game_statsManager.ResetMissions_onRetry();
|
||||
}
|
||||
|
||||
|
||||
public void MoveCharactersToSpawnPoints()
|
||||
{
|
||||
if (charactersMovedToSpawnPoints) return;
|
||||
|
||||
// Find game objects with tags "Enemy" and "Player"
|
||||
GameObject enemy = GameObject.FindGameObjectWithTag("Enemy");
|
||||
GameObject player = GameObject.FindGameObjectWithTag("Player");
|
||||
|
||||
if (enemy != null && player != null)
|
||||
{
|
||||
// Set initial positions
|
||||
enemy.transform.position = new Vector3(5.32f, 0, 0);
|
||||
player.transform.position = new Vector3(7.32f, 0, 0);
|
||||
|
||||
// Make player face the enemy
|
||||
player.transform.LookAt(enemy.transform);
|
||||
|
||||
charactersMovedToSpawnPoints = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("Waiting for players to spawn...");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void MonitorRounds()
|
||||
{
|
||||
if (pause || player == null || enemy == null) return;
|
||||
|
||||
// Cache components safely
|
||||
var playerRounds = player.GetComponent<RoundsScript>();
|
||||
var enemyRounds = enemy.GetComponent<RoundsScript>();
|
||||
var playerHealth = player.GetComponent<HealthNew>();
|
||||
var enemyHealth = enemy.GetComponent<HealthNew>();
|
||||
|
||||
if (playerRounds == null || enemyRounds == null || playerHealth == null || enemyHealth == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (playerRounds.IsTimerMode())
|
||||
{
|
||||
if (playerRounds.isRoundActive)
|
||||
{
|
||||
//only player updates the timer
|
||||
playerRounds.currentTime -= Time.deltaTime;
|
||||
playerRounds.UpdateTimerText();
|
||||
|
||||
if (playerRounds.currentTime <= 0f)
|
||||
{
|
||||
StartCoroutine(playerRounds.EndRound());
|
||||
}
|
||||
|
||||
if (playerHealth.isDead)
|
||||
{
|
||||
playerHealth.isDead = false;
|
||||
if(SelectionOptions.Instance.isAISelected)
|
||||
GameObject.FindGameObjectWithTag("Enemy").GetComponent<CharacterAIController>().attack = false;
|
||||
else
|
||||
GameObject.FindGameObjectWithTag("Enemy").GetComponent<PlayerScript>().attack = false;
|
||||
|
||||
GameObject.FindGameObjectWithTag("Player").GetComponent<PlayerScript>().attack = false;
|
||||
enemyRounds.enemyWins++;
|
||||
playerRounds.enemyWins++;
|
||||
|
||||
StartCoroutine(playerRounds.RespawnDeadPlayer(GameObject.FindGameObjectWithTag("Player")));
|
||||
|
||||
}
|
||||
else if (enemyHealth.isDead)
|
||||
{
|
||||
enemyHealth.isDead = false;
|
||||
playerRounds.playerWins++;
|
||||
enemyRounds.playerWins++;
|
||||
if (SelectionOptions.Instance.isAISelected)
|
||||
GameObject.FindGameObjectWithTag("Enemy").GetComponent<CharacterAIController>().attack = false;
|
||||
else
|
||||
GameObject.FindGameObjectWithTag("Enemy").GetComponent<PlayerScript>().attack = false;
|
||||
|
||||
GameObject.FindGameObjectWithTag("Player").GetComponent<PlayerScript>().attack = false;
|
||||
StartCoroutine(enemyRounds.RespawnDeadPlayer(GameObject.FindGameObjectWithTag("Enemy")));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
//this works for kill mode
|
||||
// Check for round end conditions, such as health reaching zero
|
||||
|
||||
if (playerRounds.isRoundActive && playerHealth.isDead)
|
||||
{
|
||||
playerHealth.isDead = false;
|
||||
enemyRounds.enemyWins++;
|
||||
playerRounds.enemyWins++;
|
||||
StartCoroutine(playerRounds.EndRound());
|
||||
}
|
||||
else if (enemyRounds.isRoundActive && enemyHealth.isDead)
|
||||
{
|
||||
enemyHealth.isDead = false;
|
||||
playerRounds.playerWins++;
|
||||
enemyRounds.playerWins++;
|
||||
//use player(instead of enemy) since it's the one responsible for rounds
|
||||
StartCoroutine(playerRounds.EndRound());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#region
|
||||
private const string GameSettingsKey = "GameSettings";
|
||||
public static void SaveGameSettings(GameSettings settings){
|
||||
string json = JsonUtility.ToJson(settings);
|
||||
PlayerPrefs.SetString(GameSettingsKey, json);
|
||||
PlayerPrefs.Save();
|
||||
}
|
||||
|
||||
public static GameSettings LoadGameSettings(){
|
||||
if (PlayerPrefs.HasKey(GameSettingsKey))
|
||||
{
|
||||
string json = PlayerPrefs.GetString(GameSettingsKey);
|
||||
return JsonUtility.FromJson<GameSettings>(json);
|
||||
}
|
||||
return new GameSettings(); // Return default settings if none are saved
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
11
Assets/Scripts/Managers/PlayerManagerNew.cs.meta
Normal file
11
Assets/Scripts/Managers/PlayerManagerNew.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c5dca44fc1bd6d64b9bdc170f1e560db
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
666
Assets/Scripts/Managers/SceneLoader.cs
Normal file
666
Assets/Scripts/Managers/SceneLoader.cs
Normal file
@@ -0,0 +1,666 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.AddressableAssets;
|
||||
using UnityEngine.ResourceManagement.AsyncOperations;
|
||||
using UnityEngine.ResourceManagement.ResourceLocations;
|
||||
using UnityEngine.ResourceManagement.ResourceProviders;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEngine.AddressableAssets.ResourceLocators;
|
||||
|
||||
/// <summary>
|
||||
/// Centralized scene loader that supports:
|
||||
/// - Built-in scenes (always tries first)
|
||||
/// - Remote Addressables (old system - fallback)
|
||||
/// - Custom AssetBundles (new system - Editor: built-in, Android: remote bundles)
|
||||
/// </summary>
|
||||
public static class SceneLoader
|
||||
{
|
||||
/// <summary>
|
||||
/// Wrapper abstraction that mimics AsyncOperation while backing either a SceneManager load or an Addressables handle.
|
||||
/// </summary>
|
||||
public sealed class SceneLoadHandle : CustomYieldInstruction
|
||||
{
|
||||
private readonly string _sceneName;
|
||||
private readonly Action<SceneLoadHandle> _onCompleted;
|
||||
private readonly LoadSceneMode _mode;
|
||||
|
||||
private readonly AsyncOperation _unityAsyncOp;
|
||||
private AsyncOperationHandle<SceneInstance>? _addressableHandle;
|
||||
private AsyncOperation _activationOperation;
|
||||
|
||||
private bool _allowSceneActivation;
|
||||
private bool _activationRequested;
|
||||
private bool _completed;
|
||||
|
||||
// Constructor for pending initialization (no operation yet)
|
||||
internal SceneLoadHandle(string sceneName, LoadSceneMode mode, bool initialActivation, Action<SceneLoadHandle> onCompleted)
|
||||
{
|
||||
_sceneName = sceneName;
|
||||
_mode = mode;
|
||||
_allowSceneActivation = initialActivation;
|
||||
_onCompleted = onCompleted;
|
||||
}
|
||||
|
||||
internal SceneLoadHandle(string sceneName, LoadSceneMode mode, AsyncOperation unityAsyncOp, bool initialActivation, Action<SceneLoadHandle> onCompleted)
|
||||
{
|
||||
_sceneName = sceneName;
|
||||
_mode = mode;
|
||||
_unityAsyncOp = unityAsyncOp;
|
||||
_onCompleted = onCompleted;
|
||||
|
||||
if (_unityAsyncOp != null)
|
||||
{
|
||||
_unityAsyncOp.allowSceneActivation = initialActivation;
|
||||
_unityAsyncOp.completed += _ => Complete();
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError($"[SceneLoader] Unity AsyncOperation is null for '{sceneName}'.");
|
||||
Complete();
|
||||
}
|
||||
}
|
||||
|
||||
internal SceneLoadHandle(string sceneName, LoadSceneMode mode, AsyncOperationHandle<SceneInstance> addressableHandle, bool initialActivation, Action<SceneLoadHandle> onCompleted)
|
||||
{
|
||||
_sceneName = sceneName;
|
||||
_mode = mode;
|
||||
_addressableHandle = addressableHandle;
|
||||
_allowSceneActivation = initialActivation;
|
||||
_onCompleted = onCompleted;
|
||||
|
||||
if (!addressableHandle.IsValid())
|
||||
{
|
||||
Debug.LogError($"[SceneLoader] Addressables handle is invalid for '{sceneName}'.");
|
||||
Complete();
|
||||
return;
|
||||
}
|
||||
|
||||
addressableHandle.Completed += OnAddressableLoaded;
|
||||
|
||||
if (initialActivation)
|
||||
{
|
||||
TryBeginActivation();
|
||||
}
|
||||
}
|
||||
|
||||
// Update this handle with an Addressables operation
|
||||
internal void SetAddressableHandle(AsyncOperationHandle<SceneInstance> addressableHandle)
|
||||
{
|
||||
_addressableHandle = addressableHandle;
|
||||
|
||||
if (!addressableHandle.IsValid())
|
||||
{
|
||||
Debug.LogError($"[SceneLoader] Addressables handle is invalid for '{_sceneName}'.");
|
||||
Complete();
|
||||
return;
|
||||
}
|
||||
|
||||
addressableHandle.Completed += OnAddressableLoaded;
|
||||
|
||||
if (_allowSceneActivation)
|
||||
{
|
||||
TryBeginActivation();
|
||||
}
|
||||
}
|
||||
|
||||
public override bool keepWaiting => !isDone;
|
||||
|
||||
public string SceneName => _sceneName;
|
||||
|
||||
public bool allowSceneActivation
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_unityAsyncOp != null)
|
||||
{
|
||||
return _unityAsyncOp.allowSceneActivation;
|
||||
}
|
||||
|
||||
return _allowSceneActivation;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (_unityAsyncOp != null)
|
||||
{
|
||||
_unityAsyncOp.allowSceneActivation = value;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_allowSceneActivation == value)
|
||||
return;
|
||||
|
||||
_allowSceneActivation = value;
|
||||
if (value)
|
||||
{
|
||||
TryBeginActivation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public float progress
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_unityAsyncOp != null)
|
||||
{
|
||||
return _unityAsyncOp.progress;
|
||||
}
|
||||
|
||||
if (!_addressableHandle.HasValue || !_addressableHandle.Value.IsValid())
|
||||
return 0f;
|
||||
|
||||
float baseProgress = _addressableHandle.Value.PercentComplete;
|
||||
|
||||
if (_activationOperation != null)
|
||||
{
|
||||
return Mathf.Lerp(Mathf.Min(baseProgress, 0.9f), 1f, _activationOperation.progress);
|
||||
}
|
||||
|
||||
if (!_allowSceneActivation && baseProgress >= 0.9f)
|
||||
{
|
||||
return 0.9f;
|
||||
}
|
||||
|
||||
return baseProgress;
|
||||
}
|
||||
}
|
||||
|
||||
public bool isDone
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_unityAsyncOp != null)
|
||||
{
|
||||
return _unityAsyncOp.isDone;
|
||||
}
|
||||
|
||||
if (_activationOperation != null)
|
||||
{
|
||||
return _activationOperation.isDone;
|
||||
}
|
||||
|
||||
if (!_addressableHandle.HasValue || !_addressableHandle.Value.IsValid())
|
||||
{
|
||||
return _completed;
|
||||
}
|
||||
|
||||
if (!_allowSceneActivation)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var handle = _addressableHandle.Value;
|
||||
if (handle.Status == AsyncOperationStatus.Failed)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (handle.Status == AsyncOperationStatus.Succeeded && handle.Result.Scene.IsValid() && handle.Result.Scene.isLoaded)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public AsyncOperationHandle<SceneInstance>? AddressableHandle => _addressableHandle;
|
||||
|
||||
private void OnAddressableLoaded(AsyncOperationHandle<SceneInstance> handle)
|
||||
{
|
||||
if (handle.Status == AsyncOperationStatus.Failed)
|
||||
{
|
||||
Debug.LogError($"[SceneLoader] Addressables failed to load '{_sceneName}': {handle.OperationException?.Message}");
|
||||
Complete();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_allowSceneActivation)
|
||||
{
|
||||
BeginActivation(handle);
|
||||
}
|
||||
}
|
||||
|
||||
private void TryBeginActivation()
|
||||
{
|
||||
if (!_addressableHandle.HasValue || !_addressableHandle.Value.IsValid())
|
||||
return;
|
||||
|
||||
if (_activationRequested)
|
||||
return;
|
||||
|
||||
var handle = _addressableHandle.Value;
|
||||
if (handle.Status == AsyncOperationStatus.Succeeded)
|
||||
{
|
||||
BeginActivation(handle);
|
||||
}
|
||||
}
|
||||
|
||||
private void BeginActivation(AsyncOperationHandle<SceneInstance> handle)
|
||||
{
|
||||
if (_activationRequested)
|
||||
return;
|
||||
|
||||
_activationRequested = true;
|
||||
|
||||
try
|
||||
{
|
||||
_activationOperation = handle.Result.ActivateAsync();
|
||||
if (_activationOperation != null)
|
||||
{
|
||||
_activationOperation.completed += _ =>
|
||||
{
|
||||
if (_mode == LoadSceneMode.Single)
|
||||
{
|
||||
SceneManager.SetActiveScene(handle.Result.Scene);
|
||||
}
|
||||
Complete();
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_mode == LoadSceneMode.Single)
|
||||
{
|
||||
SceneManager.SetActiveScene(handle.Result.Scene);
|
||||
}
|
||||
Complete();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogException(ex);
|
||||
Complete();
|
||||
}
|
||||
}
|
||||
|
||||
internal void Complete()
|
||||
{
|
||||
if (_completed)
|
||||
return;
|
||||
|
||||
_completed = true;
|
||||
_onCompleted?.Invoke(this);
|
||||
}
|
||||
}
|
||||
|
||||
private static SceneLoadHandle _currentHandle;
|
||||
private static string _currentSceneName;
|
||||
private static bool _addressablesInitialized;
|
||||
private static AsyncOperationHandle<IResourceLocator>? _initializationHandle;
|
||||
private static readonly object _addressablesInitLock = new();
|
||||
|
||||
// Custom AssetBundle system (new system - replaces Addressables)
|
||||
private static bool _customAssetBundleInitialized;
|
||||
|
||||
public static bool IsLoading => _currentHandle != null && !_currentHandle.isDone;
|
||||
public static string CurrentSceneName => _currentSceneName;
|
||||
public static SceneLoadHandle CurrentOperation => _currentHandle;
|
||||
|
||||
/// <summary>
|
||||
/// Load a scene once, preferring build scenes but falling back to custom AssetBundles or Addressables.
|
||||
/// For "Game" scene: Uses built-in in Editor, remote AssetBundle in Android build.
|
||||
/// </summary>
|
||||
public static SceneLoadHandle LoadSceneAsyncOnce(
|
||||
string sceneName,
|
||||
LoadSceneMode mode = LoadSceneMode.Single,
|
||||
bool allowSceneActivation = true)
|
||||
{
|
||||
if (string.IsNullOrEmpty(sceneName))
|
||||
{
|
||||
Debug.LogError("SceneLoader: sceneName is null or empty.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (IsLoading)
|
||||
{
|
||||
if (string.Equals(_currentSceneName, sceneName, StringComparison.Ordinal))
|
||||
{
|
||||
Debug.LogWarning($"SceneLoader: '{sceneName}' is already loading. Returning existing handle.");
|
||||
return _currentHandle;
|
||||
}
|
||||
|
||||
Debug.LogWarning($"SceneLoader: A different scene ('{_currentSceneName}') is already loading. Ignoring new request '{sceneName}'.");
|
||||
return null;
|
||||
}
|
||||
|
||||
_currentSceneName = sceneName;
|
||||
|
||||
// DISABLED: AssetBundle loading for Game scene
|
||||
// Uncomment the block below to re-enable custom AssetBundle loading on Android
|
||||
/*
|
||||
#if !UNITY_EDITOR
|
||||
// CRITICAL FIX: On Android, ALWAYS use custom AssetBundle system for Game scene
|
||||
// Don't check Application.CanStreamedLevelBeLoaded - it's unreliable!
|
||||
if (sceneName == "Game")
|
||||
{
|
||||
Debug.Log("[SceneLoader] Android build detected - Loading Game scene via custom AssetBundle system...");
|
||||
var initHandle = new SceneLoadHandle(sceneName, mode, allowSceneActivation, OnHandleCompleted);
|
||||
_currentHandle = initHandle;
|
||||
|
||||
if (GameManager.Instance != null)
|
||||
{
|
||||
GameManager.Instance.StartCoroutine(LoadGameSceneViaCustomAssetBundle(mode, allowSceneActivation, initHandle));
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("[SceneLoader] GameManager.Instance is null! Cannot load via custom AssetBundles.");
|
||||
ResetState();
|
||||
return null;
|
||||
}
|
||||
|
||||
return initHandle;
|
||||
}
|
||||
#endif
|
||||
*/
|
||||
|
||||
// 1. Try built-in scene first (works in Editor for development)
|
||||
if (Application.CanStreamedLevelBeLoaded(sceneName))
|
||||
{
|
||||
var asyncOp = SceneManager.LoadSceneAsync(sceneName, mode);
|
||||
if (asyncOp == null)
|
||||
{
|
||||
Debug.LogError($"SceneLoader: SceneManager returned null AsyncOperation for '{sceneName}'.");
|
||||
ResetState();
|
||||
return null;
|
||||
}
|
||||
|
||||
asyncOp.allowSceneActivation = allowSceneActivation;
|
||||
_currentHandle = new SceneLoadHandle(sceneName, mode, asyncOp, allowSceneActivation, OnHandleCompleted);
|
||||
Debug.Log($"[SceneLoader] Started built-in async load for '{sceneName}'.");
|
||||
return _currentHandle;
|
||||
}
|
||||
|
||||
// 2. For other scenes not in build: Use Addressables (fallback)
|
||||
Debug.Log($"[SceneLoader] Scene '{sceneName}' not in build settings.");
|
||||
|
||||
// DISABLED: Addressables fallback - make sure all scenes are in Build Settings
|
||||
// Uncomment the block below to re-enable Addressables fallback loading
|
||||
/*
|
||||
#if !UNITY_EDITOR
|
||||
// This should not be reached for Game scene anymore (handled above)
|
||||
Debug.LogWarning($"[SceneLoader] Scene '{sceneName}' not found in build settings and not Game scene!");
|
||||
#endif
|
||||
|
||||
// 3. Fallback: Try Addressables for other scenes
|
||||
Debug.Log($"[SceneLoader] Attempting Addressables load for '{sceneName}'...");
|
||||
var addressablesHandle = new SceneLoadHandle(sceneName, mode, allowSceneActivation, OnHandleCompleted);
|
||||
_currentHandle = addressablesHandle;
|
||||
|
||||
if (GameManager.Instance != null)
|
||||
{
|
||||
GameManager.Instance.StartCoroutine(InitializeAndLoadAddressableScene(sceneName, mode, allowSceneActivation, addressablesHandle));
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("[SceneLoader] GameManager.Instance is null! Cannot start coroutine for Addressables init.");
|
||||
ResetState();
|
||||
return null;
|
||||
}
|
||||
|
||||
return addressablesHandle;
|
||||
*/
|
||||
// Since AssetBundles/Addressables are disabled, log error and return null
|
||||
Debug.LogError($"[SceneLoader] Scene '{sceneName}' is NOT in Build Settings! Add it to Build Settings to load.");
|
||||
ResetState();
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void OnHandleCompleted(SceneLoadHandle handle)
|
||||
{
|
||||
if (handle != null)
|
||||
{
|
||||
var addrHandleNullable = handle.AddressableHandle;
|
||||
if (addrHandleNullable.HasValue)
|
||||
{
|
||||
var addrHandle = addrHandleNullable.Value;
|
||||
if (addrHandle.IsValid() && addrHandle.Status == AsyncOperationStatus.Failed)
|
||||
{
|
||||
Debug.LogError($"SceneLoader: Addressables load failed for '{handle.SceneName}'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_currentHandle = null;
|
||||
_currentSceneName = null;
|
||||
}
|
||||
|
||||
// Coroutine to load Game scene via custom AssetBundle system (Android builds only)
|
||||
private static IEnumerator LoadGameSceneViaCustomAssetBundle(
|
||||
LoadSceneMode mode,
|
||||
bool allowSceneActivation,
|
||||
SceneLoadHandle handle)
|
||||
{
|
||||
Debug.Log("[SceneLoader] Initializing custom AssetBundle system...");
|
||||
|
||||
// Initialize custom AssetBundle system if not already done
|
||||
if (!_customAssetBundleInitialized)
|
||||
{
|
||||
yield return CustomAssetBundleSystem.RemoteAssetBundleManager.Instance.InitializeAsync();
|
||||
|
||||
if (CustomAssetBundleSystem.RemoteAssetBundleManager.Instance.IsInitialized)
|
||||
{
|
||||
_customAssetBundleInitialized = true;
|
||||
Debug.Log("[SceneLoader] ✓ Custom AssetBundle system initialized!");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("[SceneLoader] ✗ Failed to initialize custom AssetBundle system!");
|
||||
handle.Complete();
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
|
||||
// Load the Game scene from AssetBundle
|
||||
Debug.Log("[SceneLoader] Loading Game scene from remote AssetBundle...");
|
||||
bool sceneLoaded = false;
|
||||
|
||||
yield return CustomAssetBundleSystem.RemoteAssetBundleManager.Instance.LoadSceneAsync("Game", (success) =>
|
||||
{
|
||||
sceneLoaded = success;
|
||||
});
|
||||
|
||||
if (sceneLoaded)
|
||||
{
|
||||
Debug.Log("[SceneLoader] ✓ Game scene loaded successfully from AssetBundle!");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("[SceneLoader] ✗ Failed to load Game scene from AssetBundle!");
|
||||
}
|
||||
|
||||
// Complete the handle
|
||||
handle.Complete();
|
||||
}
|
||||
|
||||
// Coroutine to handle async Addressables initialization without blocking main thread
|
||||
private static System.Collections.IEnumerator InitializeAndLoadAddressableScene(
|
||||
string sceneName,
|
||||
LoadSceneMode mode,
|
||||
bool allowSceneActivation,
|
||||
SceneLoadHandle handle)
|
||||
{
|
||||
Debug.Log($"[SceneLoader] Coroutine: Starting async Addressables init for '{sceneName}'...");
|
||||
|
||||
// Start the async init task
|
||||
var initTask = EnsureAddressablesInitializedAsync();
|
||||
|
||||
// Wait until the task completes without blocking
|
||||
while (!initTask.IsCompleted)
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Check result
|
||||
bool initSuccess = false;
|
||||
try
|
||||
{
|
||||
initSuccess = initTask.Result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[SceneLoader] Addressables initialization threw exception: {ex.Message}");
|
||||
Debug.LogException(ex);
|
||||
}
|
||||
|
||||
if (!initSuccess)
|
||||
{
|
||||
Debug.LogError($"[SceneLoader] Addressables init failed. Cannot load '{sceneName}'.");
|
||||
ResetState();
|
||||
yield break;
|
||||
}
|
||||
|
||||
Debug.Log("[SceneLoader] Addressables initialized. Locating scene address...");
|
||||
|
||||
if (!TryLocateSceneAddress(sceneName, out var address))
|
||||
{
|
||||
Debug.LogError($"[SceneLoader] Scene '{sceneName}' not found in Addressables catalog.");
|
||||
ResetState();
|
||||
yield break;
|
||||
}
|
||||
|
||||
Debug.Log($"[SceneLoader] Loading scene '{sceneName}' from address '{address}'...");
|
||||
var addressableHandle = Addressables.LoadSceneAsync(address, mode, activateOnLoad: false);
|
||||
handle.SetAddressableHandle(addressableHandle);
|
||||
}
|
||||
|
||||
private static void ResetState()
|
||||
{
|
||||
_currentHandle = null;
|
||||
_currentSceneName = null;
|
||||
}
|
||||
|
||||
internal static async System.Threading.Tasks.Task<bool> EnsureAddressablesInitializedAsync()
|
||||
{
|
||||
// Quick check without lock first
|
||||
if (_addressablesInitialized)
|
||||
return true;
|
||||
|
||||
AsyncOperationHandle<IResourceLocator> handle;
|
||||
|
||||
// Lock only for checking/starting initialization
|
||||
lock (_addressablesInitLock)
|
||||
{
|
||||
if (_addressablesInitialized)
|
||||
return true;
|
||||
|
||||
if (!_initializationHandle.HasValue || !_initializationHandle.Value.IsValid())
|
||||
{
|
||||
_initializationHandle = Addressables.InitializeAsync();
|
||||
}
|
||||
|
||||
handle = _initializationHandle.Value;
|
||||
|
||||
if (!handle.IsValid())
|
||||
{
|
||||
Debug.LogError("[SceneLoader] Addressables initialization returned an invalid handle.");
|
||||
_initializationHandle = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Await OUTSIDE the lock to avoid CS1996 error
|
||||
try
|
||||
{
|
||||
await handle.Task;
|
||||
|
||||
// Capture status immediately after await, BEFORE any lock or release
|
||||
bool succeeded = false;
|
||||
AsyncOperationStatus status = AsyncOperationStatus.None;
|
||||
string operationException = null;
|
||||
|
||||
if (handle.IsValid())
|
||||
{
|
||||
status = handle.Status;
|
||||
succeeded = status == AsyncOperationStatus.Succeeded;
|
||||
|
||||
// Capture exception details if available
|
||||
if (handle.OperationException != null)
|
||||
{
|
||||
operationException = handle.OperationException.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
// Lock again to update shared state and release
|
||||
lock (_addressablesInitLock)
|
||||
{
|
||||
if (handle.IsValid())
|
||||
{
|
||||
Addressables.Release(handle);
|
||||
}
|
||||
|
||||
_initializationHandle = null;
|
||||
|
||||
if (succeeded)
|
||||
{
|
||||
_addressablesInitialized = true;
|
||||
Debug.Log("[SceneLoader] Addressables initialized successfully (async).");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Log detailed failure information
|
||||
Debug.LogError($"[SceneLoader] Addressables initialization failed. Status: {status}");
|
||||
if (!string.IsNullOrEmpty(operationException))
|
||||
{
|
||||
Debug.LogError($"[SceneLoader] Addressables OperationException: {operationException}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("[SceneLoader] No OperationException available. Check catalog URL, network connectivity, and Addressables build settings.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
lock (_addressablesInitLock)
|
||||
{
|
||||
if (_initializationHandle.HasValue && _initializationHandle.Value.IsValid())
|
||||
{
|
||||
Addressables.Release(_initializationHandle.Value);
|
||||
}
|
||||
|
||||
_initializationHandle = null;
|
||||
}
|
||||
Debug.LogError($"[SceneLoader] Addressables async init exception: {ex.GetType().Name} - {ex.Message}");
|
||||
Debug.LogError($"[SceneLoader] Full exception details:\n{ex.ToString()}");
|
||||
Debug.LogException(ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryLocateSceneAddress(string sceneName, out string address)
|
||||
{
|
||||
address = null;
|
||||
|
||||
foreach (var candidate in EnumerateAddressCandidates(sceneName))
|
||||
{
|
||||
foreach (var locator in Addressables.ResourceLocators)
|
||||
{
|
||||
if (locator.Locate(candidate, typeof(SceneInstance), out IList<IResourceLocation> locations) && locations != null && locations.Count > 0)
|
||||
{
|
||||
address = candidate;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> EnumerateAddressCandidates(string sceneName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sceneName)) yield break;
|
||||
|
||||
yield return sceneName;
|
||||
|
||||
if (!sceneName.EndsWith(".unity", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
yield return sceneName + ".unity";
|
||||
}
|
||||
|
||||
yield return $"Assets/Scenes/{sceneName}.unity";
|
||||
yield return $"Assets/{sceneName}.unity";
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Managers/SceneLoader.cs.meta
Normal file
2
Assets/Scripts/Managers/SceneLoader.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b9469282d79f819879f2e7f0c5843dc8
|
||||
75
Assets/Scripts/Managers/SplashVideoPlayer.cs
Normal file
75
Assets/Scripts/Managers/SplashVideoPlayer.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Video;
|
||||
|
||||
/// <summary>
|
||||
/// Minimal script to ensure a VideoPlayer uses a StreamingAssets video path at runtime.
|
||||
/// It sets the VideoPlayer.url to the file inside StreamingAssets and auto-plays once prepared.
|
||||
/// No scene management, no events – keep those externally.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(VideoPlayer))]
|
||||
public class SplashVideoPlayer : MonoBehaviour
|
||||
{
|
||||
[Tooltip("Relative video file name inside StreamingAssets (e.g. 'uncaptured_splash.mp4').")]
|
||||
public string videoFileName = "uncaptured_splash.mp4";
|
||||
|
||||
[Tooltip("Fallback video file for Unity editor (WebM/VP8 for Linux compatibility).")]
|
||||
public string editorVideoFileName = "uncaptured_splash_editor.webm";
|
||||
|
||||
[Tooltip("Automatically play once prepared.")]
|
||||
public bool autoPlay = true;
|
||||
|
||||
private VideoPlayer _videoPlayer;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_videoPlayer = GetComponent<VideoPlayer>();
|
||||
ConfigureVideoPlayer();
|
||||
SetUrl();
|
||||
_videoPlayer.prepareCompleted += OnPrepared;
|
||||
_videoPlayer.errorReceived += OnError;
|
||||
_videoPlayer.Prepare();
|
||||
}
|
||||
|
||||
private void ConfigureVideoPlayer()
|
||||
{
|
||||
_videoPlayer.playOnAwake = false; // We control when to play
|
||||
_videoPlayer.source = VideoSource.Url;
|
||||
// Choose a render mode that fits your setup; here we assume RenderTexture or Camera Near Plane set in inspector
|
||||
}
|
||||
|
||||
private void SetUrl()
|
||||
{
|
||||
// Use WebM for editor (better Linux support), MP4 for builds
|
||||
string fileToUse = videoFileName;
|
||||
#if UNITY_EDITOR
|
||||
if (!string.IsNullOrEmpty(editorVideoFileName))
|
||||
{
|
||||
fileToUse = editorVideoFileName;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (string.IsNullOrEmpty(fileToUse))
|
||||
{
|
||||
Debug.LogWarning("SplashVideoPlayer: videoFileName is empty.");
|
||||
return;
|
||||
}
|
||||
string path = System.IO.Path.Combine(Application.streamingAssetsPath, fileToUse);
|
||||
if (!path.StartsWith("file://"))
|
||||
path = "file://" + path;
|
||||
_videoPlayer.url = path;
|
||||
Debug.Log($"[SplashVideoPlayer] Loading video: {path}");
|
||||
}
|
||||
|
||||
private void OnPrepared(VideoPlayer vp)
|
||||
{
|
||||
if (autoPlay)
|
||||
{
|
||||
vp.Play();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnError(VideoPlayer vp, string message)
|
||||
{
|
||||
Debug.LogWarning($"SplashVideoPlayer: video error -> {message}");
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Managers/SplashVideoPlayer.cs.meta
Normal file
2
Assets/Scripts/Managers/SplashVideoPlayer.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f7d8dffc7ab07cd9aae4f7c595a504b3
|
||||
125
Assets/Scripts/Managers/SplitScreenManager.cs
Normal file
125
Assets/Scripts/Managers/SplitScreenManager.cs
Normal file
@@ -0,0 +1,125 @@
|
||||
using UnityEngine;
|
||||
using Cinemachine;
|
||||
|
||||
public class SplitScreenManager : MonoBehaviour
|
||||
{
|
||||
private Camera mainCamera;
|
||||
private Camera mainCamera2;
|
||||
private CinemachineFreeLook vcam1;
|
||||
private CinemachineFreeLook vcam2;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
SetupCameras();
|
||||
}
|
||||
|
||||
void SetupCameras()
|
||||
{
|
||||
mainCamera = GameObject.Find("MainCamera").GetComponent<Camera>();
|
||||
mainCamera2 = GameObject.Find("MainCamera2").GetComponent<Camera>();
|
||||
vcam1 = GameObject.Find("CinemachineCamera").GetComponent<CinemachineFreeLook>();
|
||||
vcam2 = GameObject.Find("CinemachineCamera2").GetComponent<CinemachineFreeLook>();
|
||||
|
||||
if (SelectionOptions.Instance.isPlayerSelected)
|
||||
{
|
||||
ConfigureSplitScreen();
|
||||
}
|
||||
else
|
||||
{
|
||||
ConfigureSingleScreen();
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigureSplitScreen()
|
||||
{
|
||||
// Main Cameras Setup
|
||||
mainCamera.enabled = true;
|
||||
mainCamera2.enabled = true;
|
||||
|
||||
mainCamera.rect = new Rect(0, 0, 0.5f, 1);
|
||||
mainCamera2.rect = new Rect(0.5f, 0, 0.5f, 1);
|
||||
|
||||
// Set culling masks
|
||||
int commonLayers = LayerMask.GetMask("Default", "Ground", "UI");
|
||||
mainCamera.cullingMask = commonLayers | LayerMask.GetMask("Player One");
|
||||
mainCamera2.cullingMask = commonLayers | LayerMask.GetMask("Player Two");
|
||||
|
||||
// Virtual Cameras Setup
|
||||
vcam1.gameObject.SetActive(true);
|
||||
vcam2.gameObject.SetActive(true);
|
||||
|
||||
// Find and assign players
|
||||
var player = GameObject.FindGameObjectWithTag("Player");
|
||||
var enemy = GameObject.FindGameObjectWithTag("Enemy");
|
||||
|
||||
if (player && enemy)
|
||||
{
|
||||
SetPlayerLayers(player, enemy);
|
||||
AssignCameraTargets(player, enemy);
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigureSingleScreen()
|
||||
{
|
||||
mainCamera.enabled = true;
|
||||
mainCamera.rect = new Rect(0, 0, 1, 1);
|
||||
mainCamera2.enabled = false;
|
||||
|
||||
vcam1.gameObject.SetActive(true);
|
||||
vcam2.gameObject.SetActive(false);
|
||||
|
||||
var player = GameObject.FindGameObjectWithTag("Player");
|
||||
if (player)
|
||||
{
|
||||
vcam1.Follow = player.transform;
|
||||
vcam1.LookAt = FindHeadTransform(player) ?? player.transform;
|
||||
}
|
||||
}
|
||||
|
||||
void SetPlayerLayers(GameObject player, GameObject enemy)
|
||||
{
|
||||
SetLayerRecursively(player, LayerMask.NameToLayer("Player One"));
|
||||
SetLayerRecursively(enemy, LayerMask.NameToLayer("Player Two"));
|
||||
}
|
||||
|
||||
void SetLayerRecursively(GameObject obj, int layer)
|
||||
{
|
||||
if (obj == null) return;
|
||||
obj.layer = layer;
|
||||
foreach (Transform child in obj.transform)
|
||||
{
|
||||
SetLayerRecursively(child.gameObject, layer);
|
||||
}
|
||||
}
|
||||
|
||||
void AssignCameraTargets(GameObject player, GameObject enemy)
|
||||
{
|
||||
// Follow = root (orbit center), LookAt = head (aim point)
|
||||
vcam1.Follow = player.transform;
|
||||
vcam1.LookAt = FindHeadTransform(player) ?? player.transform;
|
||||
vcam2.Follow = enemy.transform;
|
||||
vcam2.LookAt = FindHeadTransform(enemy) ?? enemy.transform;
|
||||
}
|
||||
|
||||
private Transform FindHeadTransform(GameObject character)
|
||||
{
|
||||
string[] headNames = { "Head", "head", "Bip001 Head", "mixamorig:Head", "Bip01 Head" };
|
||||
foreach (string headName in headNames)
|
||||
{
|
||||
Transform head = FindChildRecursive(character.transform, headName);
|
||||
if (head != null) return head;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Transform FindChildRecursive(Transform parent, string name)
|
||||
{
|
||||
foreach (Transform child in parent)
|
||||
{
|
||||
if (child.name == name) return child;
|
||||
Transform found = FindChildRecursive(child, name);
|
||||
if (found != null) return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Managers/SplitScreenManager.cs.meta
Normal file
2
Assets/Scripts/Managers/SplitScreenManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ac2dcc09f1c9869b785f30c68eed39ad
|
||||
247
Assets/Scripts/Managers/TeamIndicator.cs
Normal file
247
Assets/Scripts/Managers/TeamIndicator.cs
Normal file
@@ -0,0 +1,247 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
/// <summary>
|
||||
/// TeamIndicator - Creates an overhead team marker above characters to differentiate teammates from enemies.
|
||||
/// Attach to character prefabs or add dynamically via TeamIndicatorManager.
|
||||
/// </summary>
|
||||
public class TeamIndicator : MonoBehaviour
|
||||
{
|
||||
[Header("Indicator Settings")]
|
||||
[SerializeField] private float heightOffset = 2.5f;
|
||||
[SerializeField] private float indicatorSize = 0.3f;
|
||||
|
||||
[Header("Team Colors")]
|
||||
[SerializeField] private Color allyColor = new Color(0.2f, 0.8f, 0.2f, 1f); // Green
|
||||
[SerializeField] private Color enemyColor = new Color(0.9f, 0.2f, 0.2f, 1f); // Red
|
||||
[SerializeField] private Color controlledColor = new Color(0.2f, 0.6f, 1f, 1f); // Blue for currently controlled
|
||||
|
||||
[Header("Visual Options")]
|
||||
[SerializeField] private bool showOnAllies = true;
|
||||
[SerializeField] private bool showOnEnemies = true;
|
||||
[SerializeField] private bool hideWhenControlled = true;
|
||||
[SerializeField] private float fadeDistance = 50f; // Fade out at this distance
|
||||
|
||||
private GameObject indicatorObject;
|
||||
private SpriteRenderer spriteRenderer;
|
||||
private Camera mainCamera;
|
||||
private TeamMember teamMember;
|
||||
private string myTeamTag;
|
||||
private bool isCurrentlyControlled = false;
|
||||
|
||||
// Static reference to find the current player for team comparison
|
||||
private static Transform currentPlayerTransform;
|
||||
private static string currentPlayerTeamTag = "Player";
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// Only activate in Cash System mode
|
||||
if (SelectionOptions.Instance == null || !SelectionOptions.Instance.isCashSystemSelected)
|
||||
{
|
||||
enabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
mainCamera = Camera.main;
|
||||
teamMember = GetComponent<TeamMember>();
|
||||
myTeamTag = gameObject.tag;
|
||||
|
||||
CreateIndicator();
|
||||
UpdateIndicatorColor();
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
if (indicatorObject == null || mainCamera == null) return;
|
||||
|
||||
// Update position to stay above character
|
||||
indicatorObject.transform.position = transform.position + Vector3.up * heightOffset;
|
||||
|
||||
// Billboard effect - always face camera
|
||||
indicatorObject.transform.rotation = Quaternion.LookRotation(
|
||||
indicatorObject.transform.position - mainCamera.transform.position
|
||||
);
|
||||
|
||||
// Check if this character is currently controlled
|
||||
bool wasControlled = isCurrentlyControlled;
|
||||
isCurrentlyControlled = teamMember != null && teamMember.IsPlayerControlled;
|
||||
|
||||
// Update static reference if we're the controlled character
|
||||
if (isCurrentlyControlled)
|
||||
{
|
||||
currentPlayerTransform = transform;
|
||||
currentPlayerTeamTag = myTeamTag;
|
||||
}
|
||||
|
||||
// Update color if control status changed
|
||||
if (wasControlled != isCurrentlyControlled)
|
||||
{
|
||||
UpdateIndicatorColor();
|
||||
}
|
||||
|
||||
// Handle visibility based on settings
|
||||
UpdateVisibility();
|
||||
|
||||
// Fade based on distance from camera
|
||||
UpdateFade();
|
||||
}
|
||||
|
||||
private void CreateIndicator()
|
||||
{
|
||||
// Create indicator game object
|
||||
indicatorObject = new GameObject($"{gameObject.name}_TeamIndicator");
|
||||
indicatorObject.transform.position = transform.position + Vector3.up * heightOffset;
|
||||
|
||||
// Add sprite renderer for the indicator
|
||||
spriteRenderer = indicatorObject.AddComponent<SpriteRenderer>();
|
||||
spriteRenderer.sprite = CreateCircleSprite();
|
||||
spriteRenderer.sortingOrder = 100; // Ensure it renders on top
|
||||
|
||||
// Set initial size
|
||||
indicatorObject.transform.localScale = Vector3.one * indicatorSize;
|
||||
}
|
||||
|
||||
private Sprite CreateCircleSprite()
|
||||
{
|
||||
// Create a simple diamond/arrow shaped indicator texture
|
||||
int size = 64;
|
||||
Texture2D texture = new Texture2D(size, size);
|
||||
Color transparent = new Color(0, 0, 0, 0);
|
||||
|
||||
// Fill with transparent
|
||||
for (int x = 0; x < size; x++)
|
||||
{
|
||||
for (int y = 0; y < size; y++)
|
||||
{
|
||||
texture.SetPixel(x, y, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw a downward pointing triangle/arrow
|
||||
int centerX = size / 2;
|
||||
int topY = size - 8;
|
||||
int bottomY = 8;
|
||||
|
||||
for (int y = bottomY; y <= topY; y++)
|
||||
{
|
||||
float progress = (float)(y - bottomY) / (topY - bottomY);
|
||||
int halfWidth = (int)(progress * (size / 2 - 4));
|
||||
|
||||
for (int x = centerX - halfWidth; x <= centerX + halfWidth; x++)
|
||||
{
|
||||
// Create a border effect
|
||||
bool isBorder = Mathf.Abs(x - (centerX - halfWidth)) < 3 ||
|
||||
Mathf.Abs(x - (centerX + halfWidth)) < 3 ||
|
||||
y == bottomY || y >= topY - 2;
|
||||
|
||||
Color col = isBorder ? Color.white : Color.white;
|
||||
col.a = isBorder ? 1f : 0.8f;
|
||||
texture.SetPixel(x, y, col);
|
||||
}
|
||||
}
|
||||
|
||||
texture.Apply();
|
||||
texture.filterMode = FilterMode.Bilinear;
|
||||
|
||||
return Sprite.Create(texture, new Rect(0, 0, size, size), new Vector2(0.5f, 0f), size);
|
||||
}
|
||||
|
||||
private void UpdateIndicatorColor()
|
||||
{
|
||||
if (spriteRenderer == null) return;
|
||||
|
||||
// Currently controlled character
|
||||
if (isCurrentlyControlled)
|
||||
{
|
||||
spriteRenderer.color = controlledColor;
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine if ally or enemy relative to player
|
||||
bool isAlly = (myTeamTag == currentPlayerTeamTag);
|
||||
|
||||
if (isAlly)
|
||||
{
|
||||
spriteRenderer.color = allyColor;
|
||||
}
|
||||
else
|
||||
{
|
||||
spriteRenderer.color = enemyColor;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateVisibility()
|
||||
{
|
||||
if (spriteRenderer == null) return;
|
||||
|
||||
bool shouldShow = true;
|
||||
|
||||
// Hide if this is the controlled character and setting is enabled
|
||||
if (hideWhenControlled && isCurrentlyControlled)
|
||||
{
|
||||
shouldShow = false;
|
||||
}
|
||||
|
||||
// Check ally/enemy visibility settings
|
||||
bool isAlly = (myTeamTag == currentPlayerTeamTag);
|
||||
if (isAlly && !showOnAllies) shouldShow = false;
|
||||
if (!isAlly && !showOnEnemies) shouldShow = false;
|
||||
|
||||
indicatorObject.SetActive(shouldShow);
|
||||
}
|
||||
|
||||
private void UpdateFade()
|
||||
{
|
||||
if (spriteRenderer == null || mainCamera == null) return;
|
||||
|
||||
float distance = Vector3.Distance(mainCamera.transform.position, transform.position);
|
||||
|
||||
// Calculate alpha based on distance
|
||||
float alpha = Mathf.Clamp01(1f - (distance / fadeDistance));
|
||||
alpha = Mathf.Max(alpha, 0.3f); // Minimum alpha
|
||||
|
||||
Color currentColor = spriteRenderer.color;
|
||||
currentColor.a = alpha;
|
||||
spriteRenderer.color = currentColor;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (indicatorObject != null)
|
||||
{
|
||||
Destroy(indicatorObject);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Force update the indicator color (call after team changes)
|
||||
/// </summary>
|
||||
public void RefreshIndicator()
|
||||
{
|
||||
myTeamTag = gameObject.tag;
|
||||
UpdateIndicatorColor();
|
||||
UpdateVisibility();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set indicator visibility
|
||||
/// </summary>
|
||||
public void SetVisible(bool visible)
|
||||
{
|
||||
if (indicatorObject != null)
|
||||
{
|
||||
indicatorObject.SetActive(visible);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Static method to update all indicators when player changes
|
||||
/// </summary>
|
||||
public static void RefreshAllIndicators()
|
||||
{
|
||||
foreach (var indicator in FindObjectsOfType<TeamIndicator>())
|
||||
{
|
||||
indicator.RefreshIndicator();
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Managers/TeamIndicator.cs.meta
Normal file
2
Assets/Scripts/Managers/TeamIndicator.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a4f83da1465d312d58d75f25c92a37ce
|
||||
116
Assets/Scripts/Managers/TeamIndicatorManager.cs
Normal file
116
Assets/Scripts/Managers/TeamIndicatorManager.cs
Normal file
@@ -0,0 +1,116 @@
|
||||
using UnityEngine;
|
||||
using System.Collections;
|
||||
|
||||
/// <summary>
|
||||
/// TeamIndicatorManager - Automatically adds TeamIndicator components to all characters in Cash System mode.
|
||||
/// Attach to a manager object in the scene or it will be created by CashSystemManager.
|
||||
/// </summary>
|
||||
public class TeamIndicatorManager : MonoBehaviour
|
||||
{
|
||||
[Header("Settings")]
|
||||
[SerializeField] private bool autoSetupOnStart = true;
|
||||
[SerializeField] private float refreshInterval = 2f; // Check for new characters periodically
|
||||
|
||||
private static TeamIndicatorManager instance;
|
||||
public static TeamIndicatorManager Instance => instance;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (instance != null && instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
instance = this;
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// Only activate in Cash System mode
|
||||
if (SelectionOptions.Instance == null || !SelectionOptions.Instance.isCashSystemSelected)
|
||||
{
|
||||
enabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (autoSetupOnStart)
|
||||
{
|
||||
StartCoroutine(SetupIndicatorsDelayed());
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator SetupIndicatorsDelayed()
|
||||
{
|
||||
// Wait for characters to spawn
|
||||
yield return new WaitForSeconds(1f);
|
||||
|
||||
SetupAllIndicators();
|
||||
|
||||
// Periodic refresh for newly spawned characters
|
||||
while (enabled)
|
||||
{
|
||||
yield return new WaitForSeconds(refreshInterval);
|
||||
RefreshIndicators();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add TeamIndicator to all characters that don't have one
|
||||
/// </summary>
|
||||
public void SetupAllIndicators()
|
||||
{
|
||||
// Find all player team members
|
||||
TeamMember[] allTeamMembers = FindObjectsOfType<TeamMember>();
|
||||
foreach (var member in allTeamMembers)
|
||||
{
|
||||
AddIndicatorIfMissing(member.gameObject);
|
||||
}
|
||||
|
||||
// Find all characters with HealthNew (covers AI enemies too)
|
||||
HealthNew[] allCharacters = FindObjectsOfType<HealthNew>();
|
||||
foreach (var character in allCharacters)
|
||||
{
|
||||
AddIndicatorIfMissing(character.gameObject);
|
||||
}
|
||||
|
||||
Debug.Log($"[TeamIndicatorManager] Setup indicators on {allTeamMembers.Length + allCharacters.Length} characters");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check for new characters and add indicators
|
||||
/// </summary>
|
||||
public void RefreshIndicators()
|
||||
{
|
||||
TeamMember[] allTeamMembers = FindObjectsOfType<TeamMember>();
|
||||
foreach (var member in allTeamMembers)
|
||||
{
|
||||
AddIndicatorIfMissing(member.gameObject);
|
||||
}
|
||||
|
||||
HealthNew[] allCharacters = FindObjectsOfType<HealthNew>();
|
||||
foreach (var character in allCharacters)
|
||||
{
|
||||
AddIndicatorIfMissing(character.gameObject);
|
||||
}
|
||||
|
||||
// Refresh colors on all indicators
|
||||
TeamIndicator.RefreshAllIndicators();
|
||||
}
|
||||
|
||||
private void AddIndicatorIfMissing(GameObject character)
|
||||
{
|
||||
if (character.GetComponent<TeamIndicator>() == null)
|
||||
{
|
||||
character.AddComponent<TeamIndicator>();
|
||||
Debug.Log($"[TeamIndicatorManager] Added TeamIndicator to {character.name}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Manually add indicator to a specific character
|
||||
/// </summary>
|
||||
public void AddIndicator(GameObject character)
|
||||
{
|
||||
AddIndicatorIfMissing(character);
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Managers/TeamIndicatorManager.cs.meta
Normal file
2
Assets/Scripts/Managers/TeamIndicatorManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 789cc5a6d5988a62897ce4fdfa9f85ea
|
||||
26
Assets/Scripts/Managers/UIManager.cs
Normal file
26
Assets/Scripts/Managers/UIManager.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
public class UIManager : Singleton<UIManager>
|
||||
{
|
||||
|
||||
[Header("Sub-Behaviours")]
|
||||
public UIMenuBehaviour uiMenuBehaviour;
|
||||
|
||||
public void SetupManager()
|
||||
{
|
||||
uiMenuBehaviour.SetupBehaviour();
|
||||
}
|
||||
|
||||
public void UpdateUIMenuState(bool newState)
|
||||
{
|
||||
uiMenuBehaviour.UpdateUIMenuState(newState);
|
||||
}
|
||||
|
||||
public void MenuButtonOptionSelected(int newPanelID)
|
||||
{
|
||||
uiMenuBehaviour.SwitchUIContextPanels(newPanelID);
|
||||
}
|
||||
|
||||
}
|
||||
11
Assets/Scripts/Managers/UIManager.cs.meta
Normal file
11
Assets/Scripts/Managers/UIManager.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fc247b7a8a1989a4ab9818ada173c4d1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
1441
Assets/Scripts/Managers/WaitingSceneManager.cs
Normal file
1441
Assets/Scripts/Managers/WaitingSceneManager.cs
Normal file
File diff suppressed because it is too large
Load Diff
2
Assets/Scripts/Managers/WaitingSceneManager.cs.meta
Normal file
2
Assets/Scripts/Managers/WaitingSceneManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9675e81858c4220659e7521121f27f45
|
||||
8
Assets/Scripts/Managers/bin.meta
Normal file
8
Assets/Scripts/Managers/bin.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7b4855dc12c6bad4f869658edab62e33
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Scripts/Managers/bin/Debug.meta
Normal file
8
Assets/Scripts/Managers/bin/Debug.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a5123f251458f2445a58598199a3a083
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Scripts/Managers/bin/Debug/net8.0.meta
Normal file
8
Assets/Scripts/Managers/bin/Debug/net8.0.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3353833f893a50bc0bbad0f7af88ab61
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Scripts/Managers/obj.meta
Normal file
8
Assets/Scripts/Managers/obj.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3b6b9142bc8c6e2cc8da2a0da721d921
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user