Ejemplos de Scripts Unity C#

Ejemplos de scripts Unity C# cubriendo movimiento de jugador, sistemas UI, física, animaciones y mecánicas de juego

💻 Controlador de Jugador csharp

🟢 simple ⭐⭐

Scripts básicos de movimiento de jugador, salto y controlador de personaje

⏱️ 20 min 🏷️ unity, csharp, player controller, movement
Prerequisites: Basic C#, Unity Editor, Physics concepts
// Unity C# Player Controller Script

using UnityEngine;
using UnityEngine.InputSystem;

// 1. Basic Player Movement Controller
public class PlayerMovement : MonoBehaviour
{
    [Header("Movement Settings")]
    [SerializeField] private float moveSpeed = 5f;
    [SerializeField] private float jumpForce = 10f;
    [SerializeField] private float groundCheckDistance = 0.1f;
    [SerializeField] private LayerMask groundLayer;

    private Rigidbody rb;
    private bool isGrounded;
    private Vector2 moveInput;
    private bool jumpInput;

    private void Awake()
    {
        rb = GetComponent<Rigidbody>();

        // Freeze rotation to prevent tipping over
        if (rb != null)
        {
            rb.constraints = RigidbodyConstraints.FreezeRotation;
        }
    }

    private void Update()
    {
        CheckGroundStatus();
        HandleInput();
    }

    private void FixedUpdate()
    {
        MovePlayer();
        HandleJump();
    }

    private void HandleInput()
    {
        // Get input from Input System
        moveInput = new Vector2(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical"));

        if (Input.GetButtonDown("Jump"))
        {
            jumpInput = true;
        }
    }

    private void MovePlayer()
    {
        if (rb == null) return;

        // Calculate movement direction
        Vector3 moveDirection = new Vector3(moveInput.x, 0f, moveInput.y).normalized;

        // Apply movement
        Vector3 targetVelocity = moveDirection * moveSpeed;

        // Smoothly change velocity
        rb.velocity = new Vector3(
            targetVelocity.x,
            rb.velocity.y,
            targetVelocity.z
        );

        // Rotate player to face movement direction
        if (moveDirection != Vector3.zero)
        {
            Quaternion toRotation = Quaternion.LookRotation(moveDirection, Vector3.up);
            transform.rotation = Quaternion.Lerp(transform.rotation, toRotation, Time.deltaTime * 10f);
        }
    }

    private void HandleJump()
    {
        if (jumpInput && isGrounded && rb != null)
        {
            rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse);
            jumpInput = false;
        }
    }

    private void CheckGroundStatus()
    {
        isGrounded = Physics.CheckSphere(transform.position, groundCheckDistance, groundLayer);
    }

    private void OnDrawGizmosSelected()
    {
        // Draw ground check sphere
        Gizmos.color = Color.green;
        Gizmos.DrawWireSphere(transform.position, groundCheckDistance);
    }
}

// 2. Advanced Player Controller with CharacterController
public class AdvancedPlayerController : MonoBehaviour
{
    [Header("Movement Settings")]
    [SerializeField] private float walkSpeed = 3f;
    [SerializeField] private float runSpeed = 6f;
    [SerializeField] private float jumpHeight = 2f;
    [SerializeField] private float gravity = -9.81f;
    [SerializeField] private float groundDistance = 0.4f;
    [SerializeField] private Transform groundCheck;
    [SerializeField] private LayerMask groundMask;

    [Header("Camera Settings")]
    [SerializeField] private float mouseSensitivity = 100f;
    [SerializeField] private float maxLookAngle = 90f;

    private CharacterController controller;
    private Vector3 velocity;
    private bool isGrounded;
    private float xRotation = 0f;
    private Camera playerCamera;

    private void Start()
    {
        controller = GetComponent<CharacterController>();
        playerCamera = Camera.main;

        // Lock cursor for FPS control
        if (Application.isPlaying)
        {
            Cursor.lockState = CursorLockMode.Locked;
        }
    }

    private void Update()
    {
        HandleMouseLook();
        HandleMovement();
        HandleJump();
    }

    private void HandleMouseLook()
    {
        if (playerCamera == null) return;

        float mouseX = Input.GetAxis("Mouse X") * mouseSensitivity * Time.deltaTime;
        float mouseY = Input.GetAxis("Mouse Y") * mouseSensitivity * Time.deltaTime;

        // Rotate player around Y axis
        transform.Rotate(Vector3.up, mouseX);

        // Rotate camera around X axis
        xRotation -= mouseY;
        xRotation = Mathf.Clamp(xRotation, -maxLookAngle, maxLookAngle);
        playerCamera.transform.localRotation = Quaternion.Euler(xRotation, 0f, 0f);
    }

    private void HandleMovement()
    {
        if (controller == null) return;

        isGrounded = Physics.CheckSphere(groundCheck.position, groundDistance, groundMask);

        if (isGrounded && velocity.y < 0)
        {
            velocity.y = -2f;
        }

        float x = Input.GetAxis("Horizontal");
        float z = Input.GetAxis("Vertical");

        Vector3 move = transform.right * x + transform.forward * z;

        // Check if running
        bool isRunning = Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift);
        float currentSpeed = isRunning ? runSpeed : walkSpeed;

        controller.Move(move * currentSpeed * Time.deltaTime);
    }

    private void HandleJump()
    {
        if (controller == null) return;

        if (Input.GetButtonDown("Jump") && isGrounded)
        {
            velocity.y = Mathf.Sqrt(jumpHeight * -2f * gravity);
        }

        // Apply gravity
        velocity.y += gravity * Time.deltaTime;
        controller.Move(velocity * Time.deltaTime);
    }
}

// 3. Third Person Controller
public class ThirdPersonController : MonoBehaviour
{
    [Header("Movement Settings")]
    [SerializeField] private float moveSpeed = 6f;
    [SerializeField] private float rotationSpeed = 500f;
    [SerializeField] private float jumpForce = 8f;
    [SerializeField] private LayerMask groundLayer;
    [SerializeField] private float groundCheckRadius = 0.5f;

    [Header("Camera Settings")]
    [SerializeField] private Transform cameraTarget;
    [SerializeField] private float cameraDistance = 5f;
    [SerializeField] private float cameraHeight = 2f;
    [SerializeField] private float cameraSmoothTime = 0.1f;

    private Animator animator;
    private Rigidbody rb;
    private Vector3 moveInput;
    private bool isGrounded;
    private Vector3 cameraVelocity;
    private Vector3 currentCameraOffset;

    private void Awake()
    {
        animator = GetComponent<Animator>();
        rb = GetComponent<Rigidbody>();

        // Set initial camera position
        if (cameraTarget != null)
        {
            currentCameraOffset = new Vector3(0, cameraHeight, -cameraDistance);
        }
    }

    private void Update()
    {
        HandleInput();
        UpdateAnimations();
        UpdateCamera();
    }

    private void FixedUpdate()
    {
        MoveCharacter();
        CheckGroundStatus();
    }

    private void HandleInput()
    {
        float horizontal = Input.GetAxis("Horizontal");
        float vertical = Input.GetAxis("Vertical");

        moveInput = new Vector3(horizontal, 0f, vertical).normalized;
    }

    private void MoveCharacter()
    {
        if (rb == null) return;

        // Calculate movement direction relative to camera
        Vector3 cameraForward = Vector3.ProjectOnPlane(Camera.main.transform.forward, Vector3.up).normalized;
        Vector3 cameraRight = Vector3.ProjectOnPlane(Camera.main.transform.right, Vector3.up).normalized;

        Vector3 moveDirection = cameraForward * moveInput.z + cameraRight * moveInput.x;

        // Apply movement
        Vector3 targetVelocity = moveDirection * moveSpeed;
        targetVelocity.y = rb.velocity.y;

        rb.velocity = targetVelocity;

        // Rotate character to face movement direction
        if (moveDirection != Vector3.zero)
        {
            Quaternion targetRotation = Quaternion.LookRotation(moveDirection);
            transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotationSpeed * Time.fixedDeltaTime);
        }
    }

    private void UpdateAnimations()
    {
        if (animator == null) return;

        // Update movement parameters
        animator.SetFloat("MoveX", moveInput.x, 0.1f, Time.deltaTime);
        animator.SetFloat("MoveZ", moveInput.z, 0.1f, Time.deltaTime);
        animator.SetBool("IsGrounded", isGrounded);
        animator.SetFloat("Speed", moveInput.magnitude, 0.1f, Time.deltaTime);
    }

    private void UpdateCamera()
    {
        if (cameraTarget == null || Camera.main == null) return;

        // Calculate target camera position
        Vector3 targetPosition = cameraTarget.position + currentCameraOffset;

        // Smooth camera movement
        Camera.main.transform.position = Vector3.SmoothDamp(
            Camera.main.transform.position,
            targetPosition,
            ref cameraVelocity,
            cameraSmoothTime
        );

        // Look at player
        Camera.main.transform.LookAt(cameraTarget.position);
    }

    private void CheckGroundStatus()
    {
        isGrounded = Physics.CheckSphere(transform.position, groundCheckRadius, groundLayer);

        if (isGrounded && Input.GetButtonDown("Jump") && rb != null)
        {
            rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse);
            if (animator != null)
            {
                animator.SetTrigger("Jump");
            }
        }
    }

    public void SetCameraOffset(Vector3 offset)
    {
        currentCameraOffset = offset;
    }

    private void OnDrawGizmosSelected()
    {
        if (cameraTarget != null)
        {
            // Draw camera target position
            Gizmos.color = Color.blue;
            Gizmos.DrawWireSphere(cameraTarget.position + currentCameraOffset, 0.2f);
        }

        // Draw ground check sphere
        Gizmos.color = Color.green;
        Gizmos.DrawWireSphere(transform.position, groundCheckRadius);
    }
}

// 4. 2D Platformer Controller
public class PlatformerController : MonoBehaviour
{
    [Header("Movement Settings")]
    [SerializeField] private float moveSpeed = 8f;
    [SerializeField] private float jumpForce = 16f;
    [SerializeField] private float coyoteTime = 0.2f;
    [SerializeField] private float jumpBufferTime = 0.2f;
    [SerializeField] private float fallMultiplier = 2.5f;
    [SerializeField] private float lowJumpMultiplier = 2f;
    [SerializeField] private LayerMask groundLayer;
    [SerializeField] private Transform groundCheck;
    [SerializeField] private float groundCheckRadius = 0.1f;

    private Rigidbody2D rb;
    private bool isGrounded;
    private float coyoteTimeCounter;
    private float jumpBufferCounter;
    private bool jumpInputReleased;
    private float horizontalInput;

    private void Awake()
    {
        rb = GetComponent<Rigidbody2D>();
    }

    private void Update()
    {
        HandleInput();
        CheckGroundStatus();
        HandleJump();
    }

    private void FixedUpdate()
    {
        MovePlayer();
        ApplyBetterJumping();
    }

    private void HandleInput()
    {
        horizontalInput = Input.GetAxisRaw("Horizontal");

        if (Input.GetButtonDown("Jump"))
        {
            jumpBufferCounter = jumpBufferTime;
        }

        if (Input.GetButtonUp("Jump"))
        {
            jumpInputReleased = true;
        }
    }

    private void CheckGroundStatus()
    {
        bool wasGrounded = isGrounded;
        isGrounded = Physics2D.OverlapCircle(groundCheck.position, groundCheckRadius, groundLayer);

        // Update coyote time
        if (isGrounded)
        {
            coyoteTimeCounter = coyoteTime;
            if (!wasGrounded && rb.velocity.y < -0.1f)
            {
                // Landing
                OnLand();
            }
        }
        else
        {
            coyoteTimeCounter -= Time.deltaTime;
        }

        // Update jump buffer
        if (jumpBufferCounter > 0f)
        {
            jumpBufferCounter -= Time.deltaTime;
        }
    }

    private void HandleJump()
    {
        if (jumpBufferCounter > 0f && coyoteTimeCounter > 0f)
        {
            Jump();
            jumpBufferCounter = 0f;
        }
    }

    private void Jump()
    {
        if (rb != null)
        {
            rb.velocity = new Vector2(rb.velocity.x, jumpForce);
            OnJump();
        }
    }

    private void MovePlayer()
    {
        if (rb == null) return;

        // Apply horizontal movement
        rb.velocity = new Vector2(horizontalInput * moveSpeed, rb.velocity.y);
    }

    private void ApplyBetterJumping()
    {
        if (rb == null || rb.velocity.y < 0f)
        {
            // Falling - increase gravity
            rb.velocity += Vector2.up * Physics2D.gravity.y * (fallMultiplier - 1) * Time.deltaTime;
        }
        else if (rb.velocity.y > 0f && jumpInputReleased)
        {
            // Rising but jump button released - decrease gravity for low jump
            rb.velocity += Vector2.up * Physics2D.gravity.y * (lowJumpMultiplier - 1) * Time.deltaTime;
        }
    }

    private void OnJump()
    {
        jumpInputReleased = false;
        // Add jump effects, sounds, etc.
        Debug.Log("Jump!");
    }

    private void OnLand()
    {
        // Add landing effects, sounds, etc.
        Debug.Log("Landed!");
    }

    private void OnDrawGizmosSelected()
    {
        if (groundCheck != null)
        {
            Gizmos.color = isGrounded ? Color.green : Color.red;
            Gizmos.DrawWireSphere(groundCheck.position, groundCheckRadius);
        }
    }
}

// 5. Vehicle Controller
public class VehicleController : MonoBehaviour
{
    [Header("Vehicle Settings")]
    [SerializeField] private float maxSpeed = 20f;
    [SerializeField] private float acceleration = 10f;
    [SerializeField] private float brakeForce = 15f;
    [SerializeField] private float turnSpeed = 100f;
    [SerializeField] private float reverseSpeed = 10f;
    [SerializeField] private float driftFactor = 0.95f;

    [Header("Wheels")]
    [SerializeField] private Transform[] frontWheels;
    [SerializeField] private Transform[] rearWheels;
    [SerializeField] private float wheelRotationSpeed = 10f;

    private Rigidbody rb;
    private float currentSpeed;
    private float horizontalInput;
    private float verticalInput;
    private bool isBraking;
    private float steeringAngle;

    private void Awake()
    {
        rb = GetComponent<Rigidbody>();

        // Set up rigidbody for vehicle physics
        if (rb != null)
        {
            rb.centerOfMass = new Vector3(0, -0.5f, 0);
        }
    }

    private void Update()
    {
        HandleInput();
        UpdateWheelVisuals();
    }

    private void FixedUpdate()
    {
        MoveVehicle();
        HandleSteering();
        ApplyDrag();
    }

    private void HandleInput()
    {
        horizontalInput = Input.GetAxis("Horizontal");
        verticalInput = Input.GetAxis("Vertical");
        isBraking = Input.GetKey(KeyCode.Space) || Input.GetButton("Brake");
    }

    private void MoveVehicle()
    {
        if (rb == null) return;

        currentSpeed = Vector3.Dot(transform.forward, rb.velocity);

        // Calculate target speed
        float targetSpeed = 0f;
        if (verticalInput != 0f)
        {
            targetSpeed = verticalInput > 0f ? maxSpeed : -reverseSpeed;
        }

        // Apply acceleration or braking
        if (isBraking || (verticalInput == 0f && currentSpeed > 0.1f))
        {
            // Braking
            if (currentSpeed > 0.1f)
            {
                rb.velocity *= (1 - brakeForce * Time.fixedDeltaTime);
            }
        }
        else if (Mathf.Abs(targetSpeed - currentSpeed) > 0.1f)
        {
            // Acceleration
            float forceDirection = targetSpeed > currentSpeed ? 1f : -1f;
            Vector3 force = transform.forward * forceDirection * acceleration * rb.mass;
            rb.AddForce(force, ForceMode.Force);
        }
    }

    private void HandleSteering()
    {
        if (rb == null) return;

        // Only steer when moving
        if (Mathf.Abs(currentSpeed) > 0.1f)
        {
            steeringAngle = horizontalInput * turnSpeed * Time.fixedDeltaTime;

            // Reduce steering at high speeds
            float speedFactor = Mathf.InverseLerp(0f, maxSpeed, Mathf.Abs(currentSpeed));
            steeringAngle *= (1f - speedFactor * 0.7f);

            // Apply rotation
            Quaternion turnRotation = Quaternion.Euler(0f, steeringAngle, 0f);
            rb.MoveRotation(rb.rotation * turnRotation);

            // Apply drift effect when turning at speed
            if (Mathf.Abs(steeringAngle) > 0.1f && currentSpeed > maxSpeed * 0.6f)
            {
                Vector3 driftVelocity = transform.right * steeringAngle * currentSpeed * 0.1f;
                rb.velocity = Vector3.Lerp(rb.velocity, rb.velocity + driftVelocity, driftFactor * Time.fixedDeltaTime);
            }
        }
    }

    private void ApplyDrag()
    {
        if (rb == null) return;

        // Apply air resistance
        Vector3 dragForce = -rb.velocity * 0.5f;
        rb.AddForce(dragForce, ForceMode.Force);
    }

    private void UpdateWheelVisuals()
    {
        // Rotate wheels based on vehicle speed
        float wheelRotation = currentSpeed * wheelRotationSpeed * Time.deltaTime;

        foreach (Transform wheel in frontWheels)
        {
            if (wheel != null)
            {
                wheel.Rotate(Vector3.right, wheelRotation);

                // Steer front wheels
                Vector3 currentRotation = wheel.localEulerAngles;
                currentRotation.y = steeringAngle * 50f; // Visual steering multiplier
                wheel.localEulerAngles = currentRotation;
            }
        }

        foreach (Transform wheel in rearWheels)
        {
            if (wheel != null)
            {
                wheel.Rotate(Vector3.right, wheelRotation);
            }
        }
    }

    public float GetCurrentSpeed()
    {
        return currentSpeed;
    }

    public float GetSpeedPercentage()
    {
        return Mathf.InverseLerp(-reverseSpeed, maxSpeed, currentSpeed);
    }

    private void OnDrawGizmosSelected()
    {
        if (rb != null)
        {
            // Draw center of mass
            Gizmos.color = Color.red;
            Gizmos.DrawSphere(rb.worldCenterOfMass, 0.2f);
        }
    }
}

💻 Sistemas UI csharp

🟡 intermediate ⭐⭐⭐

Gestión UI completa incluyendo menús, HUD, barras de vida y sistemas de inventario

⏱️ 30 min 🏷️ unity, csharp, ui, unity ui
Prerequisites: Intermediate C#, Unity UI System, Event Systems
// Unity C# UI Systems

using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using TMPro;
using System.Collections;
using System.Collections.Generic;

// 1. Main Menu Manager
public class MainMenuManager : MonoBehaviour
{
    [Header("Menu Panels")]
    [SerializeField] private GameObject mainMenuPanel;
    [SerializeField] private GameObject settingsPanel;
    [SerializeField] private GameObject creditsPanel;
    [SerializeField] private GameObject levelSelectPanel;

    [Header("Buttons")]
    [SerializeField] private Button playButton;
    [SerializeField] private Button settingsButton;
    [SerializeField] private Button creditsButton;
    [SerializeField] private Button exitButton;
    [SerializeField] private Button backButton;

    [Header("Animation Settings")]
    [SerializeField] private float fadeInDuration = 0.5f;
    [SerializeField] private float fadeOutDuration = 0.5f;

    private Stack<GameObject> menuStack = new Stack<GameObject>();
    private CanvasGroup currentCanvasGroup;

    private void Start()
    {
        InitializeButtons();
        ShowMainMenu();
    }

    private void InitializeButtons()
    {
        if (playButton != null)
            playButton.onClick.AddListener(OnPlayClicked);

        if (settingsButton != null)
            settingsButton.onClick.AddListener(OnSettingsClicked);

        if (creditsButton != null)
            creditsButton.onClick.AddListener(OnCreditsClicked);

        if (exitButton != null)
            exitButton.onClick.AddListener(OnExitClicked);

        if (backButton != null)
            backButton.onClick.AddListener(OnBackClicked);
    }

    private void ShowMainMenu()
    {
        ShowPanel(mainMenuPanel);
    }

    private void ShowPanel(GameObject panel)
    {
        if (panel == null) return;

        // Hide current panel if exists
        if (currentCanvasGroup != null)
        {
            StartCoroutine(FadeOut(currentCanvasGroup));
        }

        // Show new panel
        panel.SetActive(true);
        currentCanvasGroup = panel.GetComponent<CanvasGroup>();

        if (currentCanvasGroup == null)
        {
            currentCanvasGroup = panel.AddComponent<CanvasGroup>();
        }

        StartCoroutine(FadeIn(currentCanvasGroup));
        menuStack.Push(panel);
    }

    private IEnumerator FadeIn(CanvasGroup canvasGroup)
    {
        canvasGroup.alpha = 0f;
        canvasGroup.interactable = false;
        canvasGroup.blocksRaycasts = false;

        float elapsedTime = 0f;
        while (elapsedTime < fadeInDuration)
        {
            canvasGroup.alpha = Mathf.Lerp(0f, 1f, elapsedTime / fadeInDuration);
            elapsedTime += Time.deltaTime;
            yield return null;
        }

        canvasGroup.alpha = 1f;
        canvasGroup.interactable = true;
        canvasGroup.blocksRaycasts = true;
    }

    private IEnumerator FadeOut(CanvasGroup canvasGroup)
    {
        canvasGroup.interactable = false;
        canvasGroup.blocksRaycasts = false;

        float elapsedTime = 0f;
        while (elapsedTime < fadeOutDuration)
        {
            canvasGroup.alpha = Mathf.Lerp(1f, 0f, elapsedTime / fadeOutDuration);
            elapsedTime += Time.deltaTime;
            yield return null;
        }

        canvasGroup.alpha = 0f;
        canvasGroup.gameObject.SetActive(false);
    }

    public void OnPlayClicked()
    {
        Debug.Log("Play clicked");
        // Load game scene or show level select
        ShowPanel(levelSelectPanel);
    }

    public void OnSettingsClicked()
    {
        Debug.Log("Settings clicked");
        ShowPanel(settingsPanel);
    }

    public void OnCreditsClicked()
    {
        Debug.Log("Credits clicked");
        ShowPanel(creditsPanel);
    }

    public void OnBackClicked()
    {
        if (menuStack.Count > 1)
        {
            menuStack.Pop(); // Remove current panel

            if (currentCanvasGroup != null)
            {
                StartCoroutine(FadeOut(currentCanvasGroup));
            }

            GameObject previousPanel = menuStack.Peek();
            previousPanel.SetActive(true);

            currentCanvasGroup = previousPanel.GetComponent<CanvasGroup>();
            if (currentCanvasGroup == null)
            {
                currentCanvasGroup = previousPanel.AddComponent<CanvasGroup>();
            }

            currentCanvasGroup.alpha = 1f;
            currentCanvasGroup.interactable = true;
            currentCanvasGroup.blocksRaycasts = true;
        }
    }

    public void OnExitClicked()
    {
        Debug.Log("Exit clicked");
        #if UNITY_EDITOR
            UnityEditor.EditorApplication.isPlaying = false;
        #else
            Application.Quit();
        #endif
    }
}

// 2. HUD Manager
public class HUDManager : MonoBehaviour
{
    [Header("Health UI")]
    [SerializeField] private Slider healthBar;
    [SerializeField] private TextMeshProUGUI healthText;
    [SerializeField] private Image healthFill;
    [SerializeField] private Gradient healthGradient;

    [Header("Stamina UI")]
    [SerializeField] private Slider staminaBar;
    [SerializeField] private TextMeshProUGUI staminaText;

    [Header("Ammo UI")]
    [SerializeField] private TextMeshProUGUI ammoText;
    [SerializeField] private Image weaponIcon;

    [Header("Experience UI")]
    [SerializeField] private Slider experienceBar;
    [SerializeField] private TextMeshProUGUI levelText;

    [Header("Crosshair")]
    [SerializeField] private GameObject crosshair;

    [Header("Damage Flash")]
    [SerializeField] private Image damageFlash;
    [SerializeField] private float flashDuration = 0.2f;

    private void Awake()
    {
        // Initialize UI elements
        if (healthBar != null)
        {
            healthBar.minValue = 0;
            healthBar.maxValue = 100;
            healthBar.value = 100;
        }

        if (staminaBar != null)
        {
            staminaBar.minValue = 0;
            staminaBar.maxValue = 100;
            staminaBar.value = 100;
        }

        if (experienceBar != null)
        {
            experienceBar.minValue = 0;
            experienceBar.maxValue = 100;
            experienceBar.value = 0;
        }
    }

    public void UpdateHealth(float currentHealth, float maxHealth)
    {
        float healthPercentage = (currentHealth / maxHealth) * 100f;

        if (healthBar != null)
        {
            healthBar.value = healthPercentage;

            // Update health fill color
            if (healthFill != null && healthGradient != null)
            {
                healthFill.color = healthGradient.Evaluate(healthPercentage / 100f);
            }
        }

        if (healthText != null)
        {
            healthText.text = `${Mathf.Round(currentHealth)} / ${maxHealth}`;
        }

        // Show damage flash when health decreases
        if (currentHealth < maxHealth)
        {
            StartCoroutine(ShowDamageFlash());
        }
    }

    public void UpdateStamina(float currentStamina, float maxStamina)
    {
        float staminaPercentage = (currentStamina / maxStamina) * 100f;

        if (staminaBar != null)
        {
            staminaBar.value = staminaPercentage;
        }

        if (staminaText != null)
        {
            staminaText.text = `${Mathf.Round(currentStamina)}%`;
        }
    }

    public void UpdateAmmo(int currentAmmo, int maxAmmo, string weaponName)
    {
        if (ammoText != null)
        {
            ammoText.text = `${currentAmmo} / ${maxAmmo}`;
        }

        // Update weapon icon based on weapon name
        // This would typically load sprites from resources
    }

    public void UpdateExperience(float currentXP, float maxXp, int level)
    {
        float xpPercentage = (currentXP / maxXp) * 100f;

        if (experienceBar != null)
        {
            experienceBar.value = xpPercentage;
        }

        if (levelText != null)
        {
            levelText.text = `Level ${level}`;
        }
    }

    public void SetCrosshairActive(bool active)
    {
        if (crosshair != null)
        {
            crosshair.SetActive(active);
        }
    }

    private IEnumerator ShowDamageFlash()
    {
        if (damageFlash != null)
        {
            damageFlash.enabled = true;
            Color flashColor = damageFlash.color;
            flashColor.a = 0.5f;
            damageFlash.color = flashColor;

            float elapsedTime = 0f;
            while (elapsedTime < flashDuration)
            {
                float alpha = Mathf.Lerp(0.5f, 0f, elapsedTime / flashDuration);
                flashColor.a = alpha;
                damageFlash.color = flashColor;
                elapsedTime += Time.deltaTime;
                yield return null;
            }

            damageFlash.enabled = false;
        }
    }
}

// 3. Inventory System
public class InventoryUI : MonoBehaviour
{
    [Header("Inventory UI")]
    [SerializeField] private GameObject inventoryPanel;
    [SerializeField] private Transform itemSlotContainer;
    [SerializeField] private GameObject itemSlotPrefab;
    [SerializeField] private int inventorySize = 20;

    [Header("Item Description")]
    [SerializeField] private Image itemIcon;
    [SerializeField] private TextMeshProUGUI itemName;
    [SerializeField] private TextMeshProUGUI itemDescription;
    [SerializeField] private TextMeshProUGUI itemStats;

    private List<InventorySlot> itemSlots = new List<InventorySlot>();
    private int selectedSlot = -1;

    private void Start()
    {
        InitializeInventorySlots();
        CloseInventory();
    }

    private void InitializeInventorySlots()
    {
        for (int i = 0; i < inventorySize; i++)
        {
            GameObject slotObj = Instantiate(itemSlotPrefab, itemSlotContainer);
            InventorySlot slot = slotObj.GetComponent<InventorySlot>();

            if (slot != null)
            {
                slot.Initialize(i, this);
                itemSlots.Add(slot);
            }
        }
    }

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.I))
        {
            ToggleInventory();
        }
    }

    public void ToggleInventory()
    {
        bool isActive = inventoryPanel.activeSelf;

        if (isActive)
        {
            CloseInventory();
        }
        else
        {
            OpenInventory();
        }
    }

    public void OpenInventory()
    {
        inventoryPanel.SetActive(true);
        Time.timeScale = 0f; // Pause game
        Cursor.lockState = CursorLockMode.None;
        Cursor.visible = true;

        // Update slot displays
        foreach (var slot in itemSlots)
        {
            slot.UpdateDisplay();
        }
    }

    public void CloseInventory()
    {
        inventoryPanel.SetActive(false);
        Time.timeScale = 1f; // Resume game
        Cursor.lockState = CursorLockMode.Locked;
        Cursor.visible = false;
    }

    public void SelectSlot(int slotIndex)
    {
        if (selectedSlot == slotIndex)
        {
            selectedSlot = -1;
            ClearItemDescription();
        }
        else
        {
            selectedSlot = slotIndex;
            UpdateSelectedSlot();
            ShowItemDescription(slotIndex);
        }
    }

    private void UpdateSelectedSlot()
    {
        for (int i = 0; i < itemSlots.Count; i++)
        {
            itemSlots[i].SetSelected(i == selectedSlot);
        }
    }

    private void ShowItemDescription(int slotIndex)
    {
        // This would typically get item data from the inventory system
        // For now, we'll use placeholder data
        InventorySlot slot = itemSlots[slotIndex];

        if (slot.HasItem())
        {
            // Show item details
            if (itemName != null)
                itemName.text = "Sample Item";

            if (itemDescription != null)
                itemDescription.text = "This is a sample item description.";

            if (itemStats != null)
                itemStats.text = "Damage: 10\nDefense: 5\nValue: 100";
        }
        else
        {
            ClearItemDescription();
        }
    }

    private void ClearItemDescription()
    {
        if (itemName != null)
            itemName.text = "";

        if (itemDescription != null)
            itemDescription.text = "";

        if (itemStats != null)
            itemStats.text = "";
    }

    public void UseSelectedItem()
    {
        if (selectedSlot >= 0 && selectedSlot < itemSlots.Count)
        {
            itemSlots[selectedSlot].UseItem();
        }
    }
}

// 4. Inventory Slot Component
public class InventorySlot : MonoBehaviour, IPointerClickHandler
{
    [Header("Slot UI")]
    [SerializeField] private Image itemIcon;
    [SerializeField] private Image background;
    [SerializeField] private TextMeshProUGUI quantityText;
    [SerializeField] private Color selectedColor = Color.yellow;
    [SerializeField] private Color normalColor = Color.white;

    private int slotIndex;
    private InventoryUI inventoryUI;
    private bool isSelected;
    private ItemData currentItem;

    public void Initialize(int index, InventoryUI ui)
    {
        slotIndex = index;
        inventoryUI = ui;
        isSelected = false;
        UpdateDisplay();
    }

    public void UpdateDisplay()
    {
        if (currentItem != null)
        {
            itemIcon.sprite = currentItem.icon;
            itemIcon.enabled = true;

            if (currentItem.stackSize > 1)
            {
                quantityText.text = currentItem.quantity.ToString();
                quantityText.gameObject.SetActive(true);
            }
            else
            {
                quantityText.gameObject.SetActive(false);
            }
        }
        else
        {
            itemIcon.enabled = false;
            quantityText.gameObject.SetActive(false);
        }
    }

    public bool HasItem()
    {
        return currentItem != null;
    }

    public void SetItem(ItemData item)
    {
        currentItem = item;
        UpdateDisplay();
    }

    public void UseItem()
    {
        if (currentItem != null)
        {
            Debug.Log(`Using item: ${currentItem.itemName}`);
            // Implement item usage logic here
        }
    }

    public void SetSelected(bool selected)
    {
        isSelected = selected;

        if (background != null)
        {
            background.color = isSelected ? selectedColor : normalColor;
        }
    }

    public void OnPointerClick(PointerEventData eventData)
    {
        inventoryUI.SelectSlot(slotIndex);
    }
}

// 5. Item Data Structure
[System.Serializable]
public class ItemData
{
    public string itemName;
    public string description;
    public Sprite icon;
    public int quantity;
    public int maxStackSize;
    public int itemID;
    public ItemType itemType;
    public ItemStats stats;
}

[System.Serializable]
public class ItemStats
{
    public int damage;
    public int defense;
    public int health;
    public int mana;
    public float speed;
    public int value;
}

public enum ItemType
{
    Weapon,
    Armor,
    Consumable,
    Material,
    Quest
}

// 6. Dialogue System UI
public class DialogueUI : MonoBehaviour
{
    [Header("Dialogue UI")]
    [SerializeField] private GameObject dialoguePanel;
    [SerializeField] private TextMeshProUGUI speakerName;
    [SerializeField] private TextMeshProUGUI dialogueText;
    [SerializeField] private GameObject responseContainer;
    [SerializeField] private GameObject responseButtonPrefab;
    [SerializeField] private Image speakerAvatar;

    [Header("Typing Effect")]
    [SerializeField] private float typingSpeed = 0.05f;
    [SerializeField] private AudioClip[] typingSounds;

    private DialogueManager dialogueManager;
    private AudioSource audioSource;
    private bool isTyping;
    private bool canSkip;

    private void Awake()
    {
        dialogueManager = GetComponent<DialogueManager>();
        audioSource = GetComponent<AudioSource>();
        dialoguePanel.SetActive(false);
    }

    private void Update()
    {
        // Skip typing on click
        if (Input.GetMouseButtonDown(0) && isTyping && canSkip)
        {
            SkipTyping();
        }
    }

    public void StartDialogue(DialogueData dialogue)
    {
        dialoguePanel.SetActive(true);
        speakerName.text = dialogue.speakerName;

        if (speakerAvatar != null && dialogue.speakerAvatar != null)
        {
            speakerAvatar.sprite = dialogue.speakerAvatar;
        }

        StartCoroutine(DisplayDialogue(dialogue));
    }

    private IEnumerator DisplayDialogue(DialogueData dialogue)
    {
        // Clear previous responses
        foreach (Transform child in responseContainer.transform)
        {
            Destroy(child.gameObject);
        }

        // Type out dialogue text
        yield return StartCoroutine(TypeText(dialogueText, dialogue.dialogueText));

        // Wait for player to finish reading
        yield return new WaitUntil(() => Input.GetMouseButtonDown(0));

        // Show response options if available
        if (dialogue.responses != null && dialogue.responses.Count > 0)
        {
            ShowResponses(dialogue.responses);
        }
        else
        {
            EndDialogue();
        }
    }

    private IEnumerator TypeText(TextMeshProUGUI textComponent, string text)
    {
        isTyping = true;
        canSkip = true;
        textComponent.text = "";

        for (int i = 0; i < text.Length; i++)
        {
            textComponent.text += text[i];

            // Play typing sound occasionally
            if (typingSounds != null && typingSounds.Length > 0 && i % 3 == 0)
            {
                PlayTypingSound();
            }

            yield return new WaitForSeconds(typingSpeed);
        }

        isTyping = false;
    }

    private void SkipTyping()
    {
        StopAllCoroutines();
        isTyping = false;
    }

    private void ShowResponses(List<DialogueResponse> responses)
    {
        foreach (var response in responses)
        {
            GameObject buttonObj = Instantiate(responseButtonPrefab, responseContainer.transform);
            Button button = buttonObj.GetComponent<Button>();
            TextMeshProUGUI buttonText = buttonObj.GetComponentInChildren<TextMeshProUGUI>();

            if (buttonText != null)
            {
                buttonText.text = response.responseText;
            }

            button.onClick.AddListener(() => OnResponseSelected(response));
        }
    }

    private void OnResponseSelected(DialogueResponse response)
    {
        if (dialogueManager != null)
        {
            dialogueManager.SelectResponse(response);
        }
    }

    private void EndDialogue()
    {
        dialoguePanel.SetActive(false);

        if (dialogueManager != null)
        {
            dialogueManager.EndDialogue();
        }
    }

    private void PlayTypingSound()
    {
        if (audioSource != null && typingSounds != null && typingSounds.Length > 0)
        {
            AudioClip randomSound = typingSounds[Random.Range(0, typingSounds.Length)];
            audioSource.PlayOneShot(randomSound, 0.5f);
        }
    }
}

// 7. Dialogue Data Structure
[System.Serializable]
public class DialogueData
{
    public string speakerName;
    public string dialogueText;
    public Sprite speakerAvatar;
    public List<DialogueResponse> responses;
    public AudioClip voiceClip;
}

[System.Serializable]
public class DialogueResponse
{
    public string responseText;
    public int nextDialogueID;
    public int relationshipChange;
    public bool endConversation;
}

💻 Física y Animaciones csharp

🔴 complex ⭐⭐⭐⭐

Interacciones físicas avanzadas, controladores de animación y máquinas de estado

⏱️ 40 min 🏷️ unity, csharp, physics, animation
Prerequisites: Advanced C#, Unity Physics, Animation Systems
// Unity C# Physics and Animations

using UnityEngine;
using System.Collections;

// 1. Advanced Physics Controller
public class AdvancedPhysicsController : MonoBehaviour
{
    [Header("Physics Settings")]
    [SerializeField] private float mass = 1f;
    [SerializeField] private float drag = 0.5f;
    [SerializeField] private float angularDrag = 0.5f;
    [SerializeField] private LayerMask groundLayer;
    [SerializeField] private float groundCheckDistance = 0.1f;
    [SerializeField] private float slopeLimit = 45f;

    [Header("Force Application")]
    [SerializeField] private float jumpForce = 10f;
    [SerializeField] private float wallJumpForce = 15f;
    [SerializeField] private Vector3 centerOfMassOffset = Vector3.zero;

    private Rigidbody rb;
    private Collider mainCollider;
    private bool isGrounded;
    private Vector3 groundNormal;
    private Vector3 wallNormal;

    private void Awake()
    {
        rb = GetComponent<Rigidbody>();
        mainCollider = GetComponent<Collider>();

        // Configure rigidbody
        if (rb != null)
        {
            rb.mass = mass;
            rb.drag = drag;
            rb.angularDrag = angularDrag;
            rb.centerOfMass += centerOfMassOffset;
            rb.interpolation = RigidbodyInterpolation.Interpolate;
            rb.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic;
        }
    }

    private void FixedUpdate()
    {
        CheckGroundStatus();
        ApplyGravity();
        StabilizeRotation();
    }

    private void CheckGroundStatus()
    {
        // Sphere cast for ground detection
        Vector3 sphereCenter = transform.position + (Vector3.up * groundCheckDistance);
        RaycastHit hit;
        bool wasGrounded = isGrounded;

        isGrounded = Physics.SphereCast(
            sphereCenter,
            groundCheckDistance,
            Vector3.down,
            out hit,
            groundCheckDistance * 2f,
            groundLayer
        );

        if (isGrounded)
        {
            groundNormal = hit.normal;

            // Check if on a valid slope
            float slopeAngle = Vector3.Angle(groundNormal, Vector3.up);
            if (slopeAngle > slopeLimit)
            {
                isGrounded = false;
            }

            // Snap to ground if close enough
            if (hit.distance < groundCheckDistance * 0.5f)
            {
                Vector3 targetPosition = transform.position;
                targetPosition.y = hit.point.y + groundCheckDistance;

                if (rb != null)
                {
                    rb.MovePosition(targetPosition);
                }
            }
        }
        else
        {
            groundNormal = Vector3.up;
        }

        // Handle landing and leaving ground events
        if (isGrounded && !wasGrounded)
        {
            OnLand();
        }
        else if (!isGrounded && wasGrounded)
        {
            OnLeaveGround();
        }
    }

    private void ApplyGravity()
    {
        if (!isGrounded && rb != null)
        {
            // Apply custom gravity
            Vector3 gravity = Physics.gravity;
            rb.AddForce(gravity * mass, ForceMode.Acceleration);
        }
    }

    private void StabilizeRotation()
    {
        // Keep character upright
        if (isGrounded && rb != null)
        {
            Quaternion targetRotation = Quaternion.FromToRotation(transform.up, groundNormal) * transform.rotation;
            rb.MoveRotation(Quaternion.Slerp(transform.rotation, targetRotation, Time.fixedDeltaTime * 10f));
        }
    }

    private void OnLand()
    {
        Debug.Log("Character landed on ground");
        // Reset velocity on landing
        if (rb != null)
        {
            rb.velocity = new Vector3(rb.velocity.x, 0f, rb.velocity.z);
        }
    }

    private void OnLeaveGround()
    {
        Debug.Log("Character left ground");
    }

    public void Jump()
    {
        if (isGrounded && rb != null)
        {
            Vector3 jumpDirection = groundNormal * jumpForce;
            rb.AddForce(jumpDirection, ForceMode.Impulse);
        }
    }

    public void WallJump(Vector3 wallJumpDirection)
    {
        if (rb != null)
        {
            Vector3 jumpVector = (wallJumpDirection + Vector3.up).normalized * wallJumpForce;
            rb.velocity = new Vector3(rb.velocity.x, 0f, rb.velocity.z); // Reset vertical velocity
            rb.AddForce(jumpVector, ForceMode.Impulse);
        }
    }

    private void OnCollisionEnter(Collision collision)
    {
        // Detect wall collision for wall jump
        if (!isGrounded)
        {
            foreach (ContactPoint contact in collision.contacts)
            {
                float angle = Vector3.Angle(contact.normal, Vector3.up);
                if (angle > 60f && angle < 120f)
                {
                    wallNormal = contact.normal;
                    break;
                }
            }
        }
    }

    private void OnDrawGizmosSelected()
    {
        // Draw ground check sphere
        Gizmos.color = isGrounded ? Color.green : Color.red;
        Vector3 sphereCenter = transform.position + (Vector3.up * groundCheckDistance);
        Gizmos.DrawWireSphere(sphereCenter, groundCheckDistance);

        // Draw center of mass
        if (rb != null)
        {
            Gizmos.color = Color.blue;
            Gizmos.DrawSphere(rb.worldCenterOfMass, 0.1f);
        }
    }
}

// 2. Custom Animation Controller
public class CustomAnimationController : MonoBehaviour
{
    [Header("Animation Settings")]
    [SerializeField] private Animator animator;
    [SerializeField] private float transitionDuration = 0.25f;
    [SerializeField] private LayerMask animationLayer;

    [Header("Blend Trees")]
    [SerializeField] private AnimationCurve movementBlendCurve;
    [SerializeField] private AnimationCurve rotationBlendCurve;

    private Dictionary<string, int> animationParameters = new Dictionary<string, int>();
    private Dictionary<string, AnimationClip> animationClips = new Dictionary<string, AnimationClip>();

    // Animation States
    private enum AnimationState
    {
        Idle,
        Walk,
        Run,
        Jump,
        Fall,
        Land,
        Attack,
        Hurt,
        Death
    }

    private AnimationState currentState = AnimationState.Idle;
    private AnimationState targetState = AnimationState.Idle;
    private float stateTransitionTime = 0f;

    private void Awake()
    {
        InitializeAnimator();
    }

    private void InitializeAnimator()
    {
        if (animator == null)
        {
            animator = GetComponent<Animator>();
        }

        // Cache parameter IDs for performance
        if (animator != null)
        {
            animationParameters["Speed"] = Animator.StringToHash("Speed");
            animationParameters["Direction"] = Animator.StringToHash("Direction");
            animationParameters["IsGrounded"] = Animator.StringToHash("IsGrounded");
            animationParameters["Jump"] = Animator.StringToHash("Jump");
            animationParameters["Attack"] = Animator.StringToHash("Attack");
            animationParameters["Hurt"] = Animator.StringToHash("Hurt");
            animationParameters["Death"] = Animator.StringToHash("Death");
        }
    }

    private void Update()
    {
        UpdateStateTransitions();
        UpdateAnimationParameters();
    }

    private void UpdateStateTransitions()
    {
        if (currentState != targetState)
        {
            stateTransitionTime += Time.deltaTime;

            if (stateTransitionTime >= transitionDuration)
            {
                currentState = targetState;
                stateTransitionTime = 0f;
            }
        }
    }

    private void UpdateAnimationParameters()
    {
        if (animator == null) return;

        // Update movement parameters
        Vector3 moveInput = GetMovementInput();
        float speed = moveInput.magnitude;
        float direction = GetMovementDirection(moveInput);

        animator.SetFloat(animationParameters["Speed"], speed);
        animator.SetFloat(animationParameters["Direction"], direction);

        // Update boolean parameters
        animator.SetBool(animationParameters["IsGrounded"], IsGrounded());
    }

    private Vector3 GetMovementInput()
    {
        float horizontal = Input.GetAxis("Horizontal");
        float vertical = Input.GetAxis("Vertical");
        return new Vector3(horizontal, 0f, vertical);
    }

    private float GetMovementDirection(Vector3 input)
    {
        if (input == Vector3.zero) return 0f;

        Vector3 forward = transform.forward;
        Vector3 right = transform.right;

        float forwardAmount = Vector3.Dot(input.normalized, forward);
        float rightAmount = Vector3.Dot(input.normalized, right);

        return Mathf.Atan2(rightAmount, forwardAmount) * Mathf.Rad2Deg;
    }

    private bool IsGrounded()
    {
        // Implement ground check logic
        return Physics.Raycast(transform.position + Vector3.up * 0.1f, Vector3.down, 0.2f);
    }

    public void PlayAnimation(string animationName, float transitionTime = 0.25f)
    {
        if (animator == null) return;

        animator.CrossFadeInFixedTime(animationName, transitionTime, animationLayer);
    }

    public void TriggerJump()
    {
        if (animator != null && IsGrounded())
        {
            animator.SetTrigger(animationParameters["Jump"]);
            targetState = AnimationState.Jump;
        }
    }

    public void TriggerAttack()
    {
        if (animator != null)
        {
            animator.SetTrigger(animationParameters["Attack"]);
            targetState = AnimationState.Attack;
        }
    }

    public void TriggerHurt()
    {
        if (animator != null)
        {
            animator.SetTrigger(animationParameters["Hurt"]);
            targetState = AnimationState.Hurt;
        }
    }

    public void TriggerDeath()
    {
        if (animator != null)
        {
            animator.SetTrigger(animationParameters["Death"]);
            targetState = AnimationState.Death;
            enabled = false; // Disable further updates
        }
    }

    public void OnAnimationEvent(string eventName)
    {
        // Handle animation events
        switch (eventName)
        {
            case "AttackStart":
                OnAttackStart();
                break;
            case "AttackHit":
                OnAttackHit();
                break;
            case "AttackEnd":
                OnAttackEnd();
                break;
            case "Footstep":
                OnFootstep();
                break;
        }
    }

    private void OnAttackStart()
    {
        Debug.Log("Attack animation started");
        // Enable attack hitbox, etc.
    }

    private void OnAttackHit()
    {
        Debug.Log("Attack hit frame");
        // Deal damage to targets
    }

    private void OnAttackEnd()
    {
        Debug.Log("Attack animation ended");
        // Disable attack hitbox, return to idle
        targetState = AnimationState.Idle;
    }

    private void OnFootstep()
    {
        Debug.Log("Footstep sound");
        // Play footstep sound effect
    }
}

// 3. Interactive Object System
public class InteractiveObject : MonoBehaviour, IInteractable
{
    [Header("Interaction Settings")]
    [SerializeField] private string interactionPrompt = "Press E to interact";
    [SerializeField] private float interactionDistance = 3f;
    [SerializeField] private bool requiresKey = false;
    [SerializeField] private string requiredKey = "Key";

    [Header("Visual Feedback")]
    [SerializeField] private Material highlightMaterial;
    [SerializeField] private ParticleSystem interactionEffect;
    [SerializeField] private AudioClip interactionSound;

    private Material originalMaterial;
    private MeshRenderer meshRenderer;
    private bool isHighlighted = false;
    private bool isInteractable = true;

    public virtual void Awake()
    {
        meshRenderer = GetComponent<MeshRenderer>();
        if (meshRenderer != null)
        {
            originalMaterial = meshRenderer.material;
        }
    }

    public virtual bool CanInteract(GameObject interactor)
    {
        return isInteractable && Vector3.Distance(transform.position, interactor.transform.position) <= interactionDistance;
    }

    public virtual void StartInteraction(GameObject interactor)
    {
        if (!CanInteract(interactor)) return;

        if (requiresKey)
        {
            Inventory inventory = interactor.GetComponent<Inventory>();
            if (inventory == null || !inventory.HasItem(requiredKey))
            {
                Debug.Log(`Requires key: ${requiredKey}`);
                return;
            }
        }

        OnInteraction(interactor);
        PlayInteractionEffects();
    }

    public virtual void StopInteraction(GameObject interactor)
    {
        // Override in derived classes
    }

    protected virtual void OnInteraction(GameObject interactor)
    {
        Debug.Log(`${gameObject.name} interacted with by ${interactor.name}`);
    }

    public virtual void ShowInteractionPrompt()
    {
        if (isHighlighted) return;

        HighlightObject();
        isHighlighted = true;
    }

    public virtual void HideInteractionPrompt()
    {
        if (!isHighlighted) return;

        RestoreObject();
        isHighlighted = false;
    }

    private void HighlightObject()
    {
        if (meshRenderer != null && highlightMaterial != null)
        {
            meshRenderer.material = highlightMaterial;
        }
    }

    private void RestoreObject()
    {
        if (meshRenderer != null && originalMaterial != null)
        {
            meshRenderer.material = originalMaterial;
        }
    }

    private void PlayInteractionEffects()
    {
        // Play particle effect
        if (interactionEffect != null)
        {
            ParticleSystem effect = Instantiate(interactionEffect, transform.position, Quaternion.identity);
            Destroy(effect.gameObject, effect.main.duration);
        }

        // Play sound
        if (interactionSound != null)
        {
            AudioSource.PlayClipAtPoint(interactionSound, transform.position);
        }
    }

    public void SetInteractable(bool interactable)
    {
        isInteractable = interactable;
    }

    public string GetInteractionPrompt()
    {
        return interactionPrompt;
    }
}

// 4. Ragdoll System
public class RagdollController : MonoBehaviour
{
    [Header("Ragdoll Settings")]
    [SerializeField] private bool enableRagdollOnDeath = true;
    [SerializeField] private float ragdollForce = 100f;
    [SerializeField] private float blendDuration = 0.5f;

    [Header("Rigidbody Parts")]
    [SerializeField] private Rigidbody[] ragdollRigidbodies;
    [SerializeField] private ConfigurableJoint[] ragdollJoints;

    private Animator animator;
    private bool isRagdoll = false;
    private Coroutine ragdollBlendCoroutine;

    private void Awake()
    {
        animator = GetComponent<Animator>();
        InitializeRagdoll();
    }

    private void InitializeRagdoll()
    {
        // Disable ragdoll parts initially
        foreach (Rigidbody rb in ragdollRigidbodies)
        {
            if (rb != null)
            {
                rb.isKinematic = true;
                rb.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic;
            }
        }
    }

    public void EnableRagdoll(Vector3 forceDirection)
    {
        if (isRagdoll) return;

        isRagdoll = true;

        // Disable animator
        if (animator != null)
        {
            animator.enabled = false;
        }

        // Enable rigidbodies and apply forces
        foreach (Rigidbody rb in ragdollRigidbodies)
        {
            if (rb != null)
            {
                rb.isKinematic = false;

                // Apply explosion-like force
                rb.AddForce(forceDirection * ragdollForce + Random.insideUnitSphere * ragdollForce * 0.5f,
                    ForceMode.Impulse);
            }
        }

        // Start blend coroutine
        if (ragdollBlendCoroutine != null)
        {
            StopCoroutine(ragdollBlendCoroutine);
        }
        ragdollBlendCoroutine = StartCoroutine(BlendToRagdoll());
    }

    public void DisableRagdoll()
    {
        if (!isRagdoll) return;

        isRagdoll = false;

        // Disable rigidbodies
        foreach (Rigidbody rb in ragdollRigidbodies)
        {
            if (rb != null)
            {
                rb.isKinematic = true;
            }
        }

        // Enable animator
        if (animator != null)
        {
            animator.enabled = true;
        }

        // Start blend coroutine
        if (ragdollBlendCoroutine != null)
        {
            StopCoroutine(ragdollBlendCoroutine);
        }
        ragdollBlendCoroutine = StartCoroutine(BlendToAnimator());
    }

    private IEnumerator BlendToRagdoll()
    {
        float elapsedTime = 0f;

        while (elapsedTime < blendDuration)
        {
            elapsedTime += Time.deltaTime;
            yield return null;
        }

        // Clean up any leftover coroutine
        if (ragdollBlendCoroutine != null)
        {
            StopCoroutine(ragdollBlendCoroutine);
        }
    }

    private IEnumerator BlendToAnimator()
    {
        float elapsedTime = 0f;

        // Get ragdoll pose
        Dictionary<Transform, Quaternion> ragdollPose = new Dictionary<Transform, Quaternion>();
        foreach (Rigidbody rb in ragdollRigidbodies)
        {
            if (rb != null)
            {
                ragdollPose[rb.transform] = rb.transform.rotation;
            }
        }

        while (elapsedTime < blendDuration)
        {
            elapsedTime += Time.deltaTime;
            float blendProgress = elapsedTime / blendDuration;

            // Blend back to animator pose
            foreach (var pair in ragdollPose)
            {
                if (animator != null)
                {
                    Transform animatorBone = animator.GetBoneTransform(pair.Key.name);
                    if (animatorBone != null)
                    {
                        pair.Key.rotation = Quaternion.Slerp(pair.Value, animatorBone.rotation, blendProgress);
                    }
                }
            }

            yield return null;
        }

        // Clean up any leftover coroutine
        if (ragdollBlendCoroutine != null)
        {
            StopCoroutine(ragdollBlendCoroutine);
        }
    }

    public void ApplyExplosion(Vector3 explosionCenter, float explosionForce, float explosionRadius)
    {
        foreach (Rigidbody rb in ragdollRigidbodies)
        {
            if (rb != null)
            {
                Vector3 direction = rb.position - explosionCenter;
                float distance = direction.magnitude;

                if (distance < explosionRadius)
                {
                    float force = (1 - distance / explosionRadius) * explosionForce;
                    rb.AddForce(direction.normalized * force, ForceMode.Impulse);
                }
            }
        }
    }

    private void OnDrawGizmosSelected()
    {
        // Draw ragdoll rigidbodies
        Gizmos.color = Color.red;
        foreach (Rigidbody rb in ragdollRigidbodies)
        {
            if (rb != null)
            {
                Gizmos.DrawWireSphere(rb.position, 0.1f);
            }
        }
    }
}

// 5. Interaction Interface
public interface IInteractable
{
    bool CanInteract(GameObject interactor);
    void StartInteraction(GameObject interactor);
    void StopInteraction(GameObject interactor);
    void ShowInteractionPrompt();
    void HideInteractionPrompt();
    string GetInteractionPrompt();
}

// 6. Interaction Manager
public class InteractionManager : MonoBehaviour
{
    [Header("Interaction Settings")]
    [SerializeField] private Camera playerCamera;
    [SerializeField] private float interactionRange = 3f;
    [SerializeField] private LayerMask interactionLayer;
    [SerializeField] private KeyCode interactionKey = KeyCode.E;

    [Header("UI")]
    [SerializeField] private GameObject interactionPrompt;
    [SerializeField] private TextMeshProUGUI promptText;

    private IInteractable currentInteractable;
    private GameObject currentInteractor;

    private void Update()
    {
        CheckForInteractables();
        HandleInteractionInput();
    }

    private void CheckForInteractables()
    {
        Vector3 rayOrigin = playerCamera.transform.position;
        Vector3 rayDirection = playerCamera.transform.forward;

        RaycastHit hit;

        if (Physics.Raycast(rayOrigin, rayDirection, out hit, interactionRange, interactionLayer))
        {
            IInteractable interactable = hit.collider.GetComponent<IInteractable>();

            if (interactable != null && interactable.CanInteractable(currentInteractor))
            {
                if (currentInteractable != interactable)
                {
                    // Hide previous prompt
                    if (currentInteractable != null)
                    {
                        currentInteractable.HideInteractionPrompt();
                    }

                    // Show new prompt
                    currentInteractable = interactable;
                    currentInteractable.ShowInteractionPrompt();
                    UpdateInteractionPrompt();
                }
            }
            else
            {
                HideCurrentPrompt();
            }
        }
        else
        {
            HideCurrentPrompt();
        }
    }

    private void HandleInteractionInput()
    {
        if (Input.GetKeyDown(interactionKey) && currentInteractable != null)
        {
            currentInteractable.StartInteraction(currentInteractor);
        }
        else if (Input.GetKeyUp(interactionKey) && currentInteractable != null)
        {
            currentInteractable.StopInteraction(currentInteractor);
        }
    }

    private void UpdateInteractionPrompt()
    {
        if (promptText != null && currentInteractable != null)
        {
            promptText.text = currentInteractable.GetInteractionPrompt();
        }

        if (interactionPrompt != null)
        {
            interactionPrompt.SetActive(true);
        }
    }

    private void HideCurrentPrompt()
    {
        if (currentInteractable != null)
        {
            currentInteractable.HideInteractionPrompt();
            currentInteractable = null;
        }

        if (interactionPrompt != null)
        {
            interactionPrompt.SetActive(false);
        }
    }
}