425 lines
14 KiB
C#
425 lines
14 KiB
C#
using UnityEngine;
|
|
using UnityEngine.InputSystem;
|
|
using System.Collections.Generic;
|
|
|
|
/// <summary>
|
|
/// TeamMember - Defines which team a character belongs to and who controls them.
|
|
/// This is the foundation for the unified character control system.
|
|
/// </summary>
|
|
public class TeamMember : MonoBehaviour
|
|
{
|
|
#region Static Registry
|
|
|
|
/// <summary>
|
|
/// Zero-allocation registry of all active TeamMembers.
|
|
/// Use this instead of FindObjectsOfType.
|
|
/// </summary>
|
|
private static readonly List<TeamMember> _allMembers = new List<TeamMember>(16);
|
|
public static IReadOnlyList<TeamMember> AllMembers => _allMembers;
|
|
|
|
#endregion
|
|
/// <summary>
|
|
/// Team affiliation enum
|
|
/// </summary>
|
|
public enum Team
|
|
{
|
|
PlayerTeam,
|
|
EnemyTeam1,
|
|
EnemyTeam2
|
|
}
|
|
|
|
/// <summary>
|
|
/// Control type - determines who controls this character
|
|
/// </summary>
|
|
public enum ControlType
|
|
{
|
|
HumanControlled, // Currently controlled by human player (reads input)
|
|
PlayerAI, // AI teammate on player's team
|
|
EnemyAI // AI enemy on opposing team
|
|
}
|
|
|
|
[Header("Character Data")]
|
|
[SerializeField] private CharacterStats _characterStats;
|
|
|
|
[Header("Team Settings")]
|
|
[SerializeField] private Team _team = Team.PlayerTeam;
|
|
|
|
[Header("Control Settings")]
|
|
[SerializeField] private ControlType _controlType = ControlType.EnemyAI;
|
|
|
|
/// <summary>
|
|
/// The team this character belongs to
|
|
/// </summary>
|
|
public Team CurrentTeam
|
|
{
|
|
get => _team;
|
|
set
|
|
{
|
|
_team = value;
|
|
UpdateLayerAndTag();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The control type for this character
|
|
/// </summary>
|
|
public ControlType CurrentControlType
|
|
{
|
|
get => _controlType;
|
|
set => SetControlType(value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether this character is currently controlled by the human player
|
|
/// (Backward compatible property - derives from ControlType)
|
|
/// </summary>
|
|
public bool IsPlayerControlled
|
|
{
|
|
get => _controlType == ControlType.HumanControlled;
|
|
set
|
|
{
|
|
if (value)
|
|
{
|
|
SetControlType(ControlType.HumanControlled);
|
|
}
|
|
else
|
|
{
|
|
// If turning off player control, make them AI based on team
|
|
SetControlType(_team == Team.PlayerTeam ? ControlType.PlayerAI : ControlType.EnemyAI);
|
|
}
|
|
}
|
|
}
|
|
|
|
// <summary>
|
|
// Event fired when control type changes (useful for camera, UI, input routing)
|
|
// </summary>
|
|
public event System.Action<ControlType> OnControlTypeChanged;
|
|
|
|
// <summary>
|
|
// Event fired when control status changes (backward compatible)
|
|
// </summary>
|
|
public event System.Action<bool> OnControlChanged;
|
|
|
|
// <summary>
|
|
// Event fired when team changes
|
|
// </summary>
|
|
public event System.Action<Team> OnTeamChanged;
|
|
|
|
private void Awake()
|
|
{
|
|
UpdateLayerAndTag();
|
|
}
|
|
|
|
private void OnEnable()
|
|
{
|
|
if (!_allMembers.Contains(this))
|
|
_allMembers.Add(this);
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
_allMembers.Remove(this);
|
|
}
|
|
|
|
// Track if we've done initial configuration
|
|
private bool _hasBeenConfigured = false;
|
|
|
|
/// <summary>
|
|
/// Set the control type and configure all relevant components
|
|
/// </summary>
|
|
public void SetControlType(ControlType newType)
|
|
{
|
|
// Skip if already this type AND already configured
|
|
if (_controlType == newType && _hasBeenConfigured)
|
|
{
|
|
Debug.Log($"[TeamMember] {gameObject.name}: Already configured as {newType}, skipping");
|
|
return;
|
|
}
|
|
|
|
ControlType oldType = _controlType;
|
|
_controlType = newType;
|
|
_hasBeenConfigured = true;
|
|
|
|
Debug.Log($"[TeamMember] {gameObject.name}: Control changed from {oldType} to {newType}");
|
|
|
|
// Configure components based on control type
|
|
ConfigureControlComponents(newType);
|
|
|
|
// Update layer and tag
|
|
UpdateLayerAndTag();
|
|
|
|
// Fire events
|
|
OnControlTypeChanged?.Invoke(newType);
|
|
OnControlChanged?.Invoke(newType == ControlType.HumanControlled);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Force reconfiguration even if type matches (useful for debugging)
|
|
/// </summary>
|
|
public void ForceReconfigure()
|
|
{
|
|
_hasBeenConfigured = false;
|
|
SetControlType(_controlType);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Configure all components based on control type
|
|
/// </summary>
|
|
private void ConfigureControlComponents(ControlType controlType)
|
|
{
|
|
// Get components
|
|
PlayerScript playerScript = GetComponent<PlayerScript>();
|
|
PlayerInput playerInput = GetComponent<PlayerInput>();
|
|
CharacterInputHandler inputHandler = GetComponent<CharacterInputHandler>();
|
|
CharacterAIController aiController = GetComponent<CharacterAIController>();
|
|
CharacterMovement charMovement = GetComponent<CharacterMovement>();
|
|
UnityEngine.AI.NavMeshAgent navAgent = GetComponent<UnityEngine.AI.NavMeshAgent>();
|
|
AudioListener audioListener = GetComponent<AudioListener>();
|
|
|
|
switch (controlType)
|
|
{
|
|
case ControlType.HumanControlled:
|
|
// Enable player input components
|
|
if (playerScript != null) playerScript.enabled = true;
|
|
if (playerInput != null)
|
|
{
|
|
playerInput.enabled = true;
|
|
try { playerInput.SwitchCurrentActionMap("Player Controls"); } catch { }
|
|
}
|
|
if (inputHandler != null) inputHandler.enabled = true;
|
|
if (charMovement != null) charMovement.enabled = true;
|
|
|
|
// Disable AI components
|
|
if (aiController != null) aiController.enabled = false;
|
|
if (navAgent != null) navAgent.enabled = false;
|
|
|
|
// Enable audio listener only on player-controlled
|
|
if (audioListener != null) audioListener.enabled = true;
|
|
|
|
// Disable embedded cameras (main camera will follow)
|
|
DisableEmbeddedCameras();
|
|
|
|
Debug.Log($"[TeamMember] {gameObject.name}: Configured for HUMAN control");
|
|
break;
|
|
|
|
case ControlType.PlayerAI:
|
|
case ControlType.EnemyAI:
|
|
// Disable player input components
|
|
if (playerScript != null) playerScript.enabled = false;
|
|
if (playerInput != null) playerInput.enabled = false;
|
|
if (inputHandler != null) inputHandler.enabled = false;
|
|
if (charMovement != null) charMovement.enabled = false; // AI uses NavMesh
|
|
|
|
// Enable AI components
|
|
if (aiController != null) aiController.enabled = true;
|
|
if (navAgent != null)
|
|
{
|
|
navAgent.enabled = true;
|
|
|
|
// CRITICAL: Warp agent to NavMesh IMMEDIATELY after enabling.
|
|
// Without this, there is a 1-frame gap where the agent is enabled
|
|
// but not on NavMesh, causing floating. CharacterAIController.Start()
|
|
// also warps, but it runs on the NEXT frame — too late.
|
|
UnityEngine.AI.NavMeshHit navHit;
|
|
if (UnityEngine.AI.NavMesh.SamplePosition(transform.position, out navHit, 20f, UnityEngine.AI.NavMesh.AllAreas))
|
|
{
|
|
navAgent.Warp(navHit.position);
|
|
Debug.Log($"[TeamMember] {gameObject.name}: Warped to NavMesh at Y={navHit.position.y}");
|
|
}
|
|
else
|
|
{
|
|
Debug.LogWarning($"[TeamMember] {gameObject.name}: No NavMesh found within 20m at {transform.position}!");
|
|
}
|
|
}
|
|
|
|
// Disable audio listener on AI
|
|
if (audioListener != null) audioListener.enabled = false;
|
|
|
|
// Disable embedded cameras
|
|
DisableEmbeddedCameras();
|
|
|
|
Debug.Log($"[TeamMember] {gameObject.name}: Configured for AI control ({controlType})");
|
|
break;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Disable all cameras embedded in this character prefab
|
|
/// </summary>
|
|
private void DisableEmbeddedCameras()
|
|
{
|
|
Camera[] cameras = GetComponentsInChildren<Camera>(true);
|
|
foreach (Camera cam in cameras)
|
|
{
|
|
cam.enabled = false;
|
|
}
|
|
|
|
// Also disable Cinemachine components
|
|
Cinemachine.CinemachineVirtualCameraBase[] vcams = GetComponentsInChildren<Cinemachine.CinemachineVirtualCameraBase>(true);
|
|
foreach (var vcam in vcams)
|
|
{
|
|
vcam.enabled = false;
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Check if another TeamMember is an enemy
|
|
/// </summary>
|
|
public bool IsEnemy(TeamMember other)
|
|
{
|
|
if (other == null)
|
|
return false;
|
|
|
|
return other.CurrentTeam != CurrentTeam;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check if another TeamMember is an ally
|
|
/// </summary>
|
|
public bool IsAlly(TeamMember other)
|
|
{
|
|
if (other == null) return false;
|
|
return other.CurrentTeam == this.CurrentTeam;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the layer and tag based on team
|
|
/// </summary>
|
|
private void UpdateLayerAndTag()
|
|
{
|
|
// Set appropriate tag (for compatibility with existing systems)
|
|
if (_team == Team.PlayerTeam)
|
|
{
|
|
if (_controlType == ControlType.HumanControlled)
|
|
{
|
|
gameObject.tag = "Player";
|
|
// Use Player One layer if available
|
|
int playerLayer = LayerMask.NameToLayer("Player One");
|
|
if (playerLayer >= 0) gameObject.layer = playerLayer;
|
|
}
|
|
else
|
|
{
|
|
// AI teammate - still on player team but different handling
|
|
// Keep "Player" tag for combat targeting but mark as ally
|
|
gameObject.tag = "Player";
|
|
int playerLayer = LayerMask.NameToLayer("Player One");
|
|
if (playerLayer >= 0) gameObject.layer = playerLayer;
|
|
}
|
|
}
|
|
else if (_team == Team.EnemyTeam1)
|
|
{
|
|
gameObject.tag = "EnemyTeam1";
|
|
int enemyLayer = LayerMask.NameToLayer("Player Two");
|
|
if (enemyLayer >= 0) gameObject.layer = enemyLayer;
|
|
}
|
|
else if (_team == Team.EnemyTeam2)
|
|
{
|
|
gameObject.tag = "EnemyTeam2";
|
|
int enemyLayer = LayerMask.NameToLayer("Player Three");
|
|
if (enemyLayer >= 0) gameObject.layer = enemyLayer;
|
|
}
|
|
|
|
OnTeamChanged?.Invoke(_team);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Static helper to get all team members of a specific team
|
|
/// </summary>
|
|
public static TeamMember[] GetTeamMembers(Team team)
|
|
{
|
|
var result = new List<TeamMember>();
|
|
for (int i = 0; i < _allMembers.Count; i++)
|
|
{
|
|
if (_allMembers[i] != null && _allMembers[i].CurrentTeam == team)
|
|
result.Add(_allMembers[i]);
|
|
}
|
|
return result.ToArray();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the character stats for this character
|
|
/// </summary>
|
|
public CharacterStats GetCharacterStats()
|
|
{
|
|
return _characterStats;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Set the character stats for this character
|
|
/// </summary>
|
|
public void SetCharacterStats(CharacterStats stats)
|
|
{
|
|
_characterStats = stats;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the team this character belongs to
|
|
/// </summary>
|
|
public Team GetTeam()
|
|
{
|
|
return _team;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Static helper to get the currently player-controlled character
|
|
/// </summary>
|
|
public static TeamMember GetPlayerControlledMember()
|
|
{
|
|
for (int i = 0; i < _allMembers.Count; i++)
|
|
{
|
|
if (_allMembers[i] != null && _allMembers[i].IsPlayerControlled)
|
|
return _allMembers[i];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Find the nearest enemy to this character
|
|
/// </summary>
|
|
public TeamMember FindNearestEnemy()
|
|
{
|
|
TeamMember nearest = null;
|
|
float nearestDistance = float.MaxValue;
|
|
|
|
for (int i = 0; i < _allMembers.Count; i++)
|
|
{
|
|
TeamMember other = _allMembers[i];
|
|
if (other == null || other == this || other.CurrentTeam == _team) continue;
|
|
|
|
float distance = Vector3.Distance(transform.position, other.transform.position);
|
|
if (distance < nearestDistance)
|
|
{
|
|
nearestDistance = distance;
|
|
nearest = other;
|
|
}
|
|
}
|
|
|
|
return nearest;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Find the nearest ally to this character
|
|
/// </summary>
|
|
public TeamMember FindNearestAlly()
|
|
{
|
|
TeamMember nearest = null;
|
|
float nearestDistance = float.MaxValue;
|
|
|
|
for (int i = 0; i < _allMembers.Count; i++)
|
|
{
|
|
TeamMember other = _allMembers[i];
|
|
if (other == null || other == this || other.CurrentTeam != _team) continue;
|
|
|
|
float distance = Vector3.Distance(transform.position, other.transform.position);
|
|
if (distance < nearestDistance)
|
|
{
|
|
nearestDistance = distance;
|
|
nearest = other;
|
|
}
|
|
}
|
|
|
|
return nearest;
|
|
}
|
|
}
|