Exemples de Scripts Unity C#

Exemples de scripts Unity C# couvrant le mouvement du joueur, les systèmes UI, la physique, les animations et les mécaniques de jeu

Key Facts

Category
Game Development
Items
3
Format Families
video

Sample Overview

Exemples de scripts Unity C# couvrant le mouvement du joueur, les systèmes UI, la physique, les animations et les mécaniques de jeu This sample set belongs to Game Development and can be used to test related workflows inside Elysia Tools.

💻 Contrôleur de Joueur csharp

🟢 simple ⭐⭐

Scripts de base pour le mouvement du joueur, le saut et le contrôleur de personnage

⏱️ 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);
        }
    }
}

💻 Systèmes UI csharp

🟡 intermediate ⭐⭐⭐

Gestion UI complète incluant les menus, HUD, barres de vie et systèmes d'inventaire

⏱️ 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;
}

💻 Physique et Animations csharp

🔴 complex ⭐⭐⭐⭐

Interactions physiques avancées, contrôleurs d'animation et machines à états

⏱️ 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);
        }
    }
}