chunk 1: core gameplay scripts scenes runtime assets
This commit is contained in:
415
Assets/Scripts/Character/TeamMember.cs
Normal file
415
Assets/Scripts/Character/TeamMember.cs
Normal file
@@ -0,0 +1,415 @@
|
||||
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, // The player's team (human + AI teammates)
|
||||
EnemyTeam // The opposing team (all AI)
|
||||
}
|
||||
|
||||
/// <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 != this.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 // Enemy Team
|
||||
{
|
||||
gameObject.tag = "Enemy";
|
||||
int enemyLayer = LayerMask.NameToLayer("Player Two");
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user