Оценка кода для игры на Unity

Рейтинг: 0Ответов: 1Опубликовано: 03.05.2023

Добрый день!

Особо не знаю куда обращаться, так что залетел на первый сайт, который мне выдал поисковик по запросу "Как можно посоветоваться с "программистами")

Тут больше не вопрос, так как не знаю что спрашивать)

Но суть такова

С другом пытаемся осваивать азы Unity и соответственно C#. Сами в этом новички полные и с кодом раньше работали классе в 3-4 в PascalABC. Хотелось бы просто услышать мнение, советы, правки, указания на ошибки и т.д. по коду. Точнее несколько скриптов. Скорее всего до конца это не работает как надо, но мы стараемся, честно) Я вот только сегодня наконец понял, что такое параметры в методе и как их можно применять.

Смотрите, основной код ИИ противника (EnemyAIWithFOV.cs) был сделан с огромной помощью ChatGPT, но остальная часть была написана процентов на 90 своими силами, поэтому могут быть огромные ошибки в логике и подобной теме. На комментарии не обращайте внимание. Когда я дорабатывал скрипт, то делал пометки для друга, что бы он понял, что я менял.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class EnemyAIwithFOV : MonoBehaviour
{


    [Header("Переменные для настройки баланса")]
    public attackEvent attackEvent;

    [Space]

    [Header("Абсолютное обноружение")]
    [Tooltip("Минимальная дистанция при которой юнит заметит другого юнита (так же влияет на абсолютную дистанцию срабатывания вокруг)")]
    [Range(1, 3)] public int minDistance = 1; // через нее же и настраивать маленькую зону обзора
    [Header("Настройка дистанции атаки")]
    [Tooltip("Дистанция атаки (сама атака в разработке)))")]
    [Range(0, 20)] public float attackRadius = 10;
    [Header("Настройка дистанции обзора")]
    [Tooltip("Дистанция обзора")]
    [Range(1, 40)] public float sightRadius = 16;
    [Header("Настройка угла обзора")]
    [Tooltip("Угол обзора")]
    [Range(0, 360)] public int fieldOfViewAngle = 60; // угол обзора
    private int absoluteSign = 2; // это тип будет дистанция при которой он полюбому увидит даже если не в радиусе( на всякий скрыл, тк калибровка у нее по максимуму простая)

    [Space]
    [Space]
    [Space]

    [Header("Цели для выбора приоритета")]
    [Tooltip("Сюда добавить главную цель преследования для юнита")]
    public Transform primaryTarget;
    [Tooltip("Сюда добавить второстепенную цель преследования для юнита")]
    public Transform secondaryTarget;


    [Header("Атрибуты")]
    [Tooltip("Сюда перетащить компонент с NavMeshAgent")]
    public NavMeshAgent nav;

    [Space]
    [Space]
    [Space]

    [Header("Цели")]

    [Space]

    [Header("Текущая цель")]
    [SerializeField] private Transform currentTarget;
    [Header("Лист персонажей находящихся в радиусе")]
    [SerializeField] private List<Transform> targetsInRange = new List<Transform>();
    [Header("Лист персонажей находящихся в видимости")]
    [SerializeField] private List<Transform> targetsInSight = new List<Transform>();

    public Animator EnemyAnim;
    public bool attack;
    public bool attackPlayer;
    public bool attackStella;



    public void SearchTargetPosition()
    {
        Collider[] hitColliders = Physics.OverlapSphere(transform.position, attackRadius);
        foreach (Collider hitCollider in hitColliders)
        {
            if (hitCollider.CompareTag("Enemy") || hitCollider.CompareTag("Player"))
            {
                targetsInRange.Add(hitCollider.transform);
            }
        }
    }

    private void Start()
    {
        EnemyAnim = GetComponent<Animator>();
        nav = GetComponent<NavMeshAgent>();
        SearchTargetPosition(); //  тоже просто вынес отдельно (там внизу все методы)
        attack = false;
        attackPlayer = false;
        attackStella = false;

    }

    

    public void Attack()
    {
        StartCoroutine("AttackWithTime");
        
        EnemyAnim.SetTrigger("Attack");
    }

    public IEnumerator AttackWithTime()
    {
        yield return new WaitForSeconds(0.8f);
        int attackTarget = attackEvent.targetToDamage;
        attackEvent.AttackEnemy(attackTarget);
        yield return new WaitForSeconds(1.6f);
    }

    private void Update()
    {

        absoluteSign = minDistance;

        Collider[] hitColliders = Physics.OverlapSphere(transform.position, attackRadius);
        foreach (Collider hitCollider in hitColliders)
        {
            if (hitCollider.CompareTag("Enemy") || hitCollider.CompareTag("Player"))
            {

                Transform target = hitCollider.transform;
                Vector3 targetDirection = (target.position - transform.position).normalized;

                if (currentTarget != null && Vector3.Distance(nav.transform.position, currentTarget.position) <= attackRadius)
                {
                    nav.isStopped = true;
                    EnemyAnim.SetBool("nav.isStoped", nav.isStopped);

                }
                else
                {
                    nav.isStopped = false;
                    EnemyAnim.SetBool("nav.isStoped", nav.isStopped);
                }
                foreach (Collider enemy in hitColliders)
                {
                    if (enemy.CompareTag("Player") && currentTarget == CompareTag("Player"))
                    {
                        attack = true;
                        attackPlayer = true;
                        Attack();
                    }
                    else
                    {
                        attack = false;
                        attackPlayer = false;
                        attackStella = false;
                        StartCoroutine("AttackWithTime");
                    }
                    if (enemy.CompareTag("Enemy") && currentTarget == CompareTag("Enemy"))
                    {
                        attack = true;
                        attackStella = true;
                        Attack();
                    }
                    else
                    {
                        attack = false;
                        attackPlayer = false;
                        attackStella = false;
                        StartCoroutine("AttackWithTime");
                    }

                    
                    transform.LookAt(new Vector3(currentTarget.position.x, transform.position.y, currentTarget.position.z));

                    
                }
            }
        }

        if (currentTarget == null || !targetsInRange.Contains(currentTarget))
        {
            currentTarget = GetNewTarget();
        }

        if (currentTarget != null && Vector3.Distance(transform.position, currentTarget.position) >= minDistance)
        {
            TransformNavMesh();//тут я просто навмеш в отдельный метод вынес, что бы проще его потом было изменять под нужды
        }
        targetsInSight.Clear();
        Collider[] targetsInViewRadius = Physics.OverlapSphere(transform.position, sightRadius); // сфера тут осталась, там потом просто ограничение будет стоять, что перед объектом в -угол обзора/2 и +угол обзора/2 будет чекать только
        Collider[] targetInAbsolutRadius = Physics.OverlapSphere(transform.position, absoluteSign);
        foreach (Collider targetCollider in targetInAbsolutRadius) // тут как раз таки триггер для срабатывания маленькой зоны на игроке, что бы прям близко нельзя было подобраться
        {
            if (targetCollider.CompareTag("Player"))
            {
                targetsInSight.Add(targetCollider.transform);
            }
        }
        foreach (Collider targetCollider in targetsInViewRadius)
        {
            if (targetCollider.CompareTag("Enemy") || targetCollider.CompareTag("Player"))
            {
                Transform target = targetCollider.transform;
                Vector3 targetDirection = (target.position - transform.position).normalized;
                float targetDistance = Vector3.Distance(transform.position, target.position);

                if (targetDistance <= sightRadius && Vector3.Angle(transform.forward, targetDirection) < fieldOfViewAngle / 2 || targetDistance <= absoluteSign) // ну вот как раз ограничение это
                {

                    RaycastHit hit; // а это просто проверка на препятствие
                    Debug.DrawLine(transform.position, target.position);
                    if (Physics.Raycast(transform.position, targetDirection, out hit, targetDistance))
                    {
                        if (hit.transform == target)
                        {
                            targetsInSight.Add(target);
                        }
                    }
                }
            }
        }
        if (secondaryTarget != null && targetsInSight.Contains(secondaryTarget) && !targetsInSight.Contains(primaryTarget))
        {
            currentTarget = secondaryTarget; // тут шаришь, ничего не менялось, просто выбор целей
        }
        else if (currentTarget == secondaryTarget && !targetsInSight.Contains(secondaryTarget))
        {
            currentTarget = primaryTarget;
        }
        if (currentTarget != null && Vector3.Distance(transform.position, currentTarget.position) > sightRadius)
        {
            currentTarget = null;
        }
    }
    private Transform GetNewTarget()
    {
        Transform newTarget = null;

        if (primaryTarget != null)
        {
            newTarget = primaryTarget;
        }
        if (secondaryTarget != null && targetsInSight.Contains(secondaryTarget))
        {
            newTarget = secondaryTarget;
        }
        if (newTarget == null && targetsInRange.Count > 0)
        {
            newTarget = targetsInRange[Random.Range(0, targetsInRange.Count)];
        }
        return newTarget;
    }

    public void TransformNavMesh()
    {
        nav.SetDestination(currentTarget.transform.position); // тут в целом даже одна строчка вышла))

    }

    private void OnDrawGizmosSelected()
    {
        Gizmos.color = Color.red;
        Gizmos.DrawWireSphere(transform.position, attackRadius);

        Gizmos.color = Color.cyan;
        Gizmos.DrawWireSphere(transform.position, minDistance);

        Gizmos.color = Color.yellow;
        Gizmos.DrawWireSphere(transform.position, sightRadius);

        //тута просто для визуализации вынес две линии угла обзора
        Gizmos.color = Color.green;
        Vector3 rightDir = Quaternion.AngleAxis(fieldOfViewAngle / 2, Vector3.up) * transform.forward;
        Vector3 leftDir = Quaternion.AngleAxis(-fieldOfViewAngle / 2, Vector3.up) * transform.forward;
        Gizmos.DrawLine(transform.position, transform.position + rightDir * sightRadius);
        Gizmos.DrawLine(transform.position, transform.position + leftDir * sightRadius);

    }
}

Это скрипт ИИ противника. Тут получается намешано дофига всего). Основная его задача бежать в сторону некого Обелиска, остановиться и атаковать (он всегда знает где он), но если в поле зрения попадается игрок, то он бежит к нему и атакует. Сама атака до конца не реализована и ей я занимался вчера и сегодня. Первый раз пробовал ссылаться и на переменные и методы из других скриптов и работал с параметрами).

Тут само действие внесения урона

public void Attack()
        {
            StartCoroutine("AttackWithTime");
            
            EnemyAnim.SetTrigger("Attack");
        }
    
        public IEnumerator AttackWithTime()
        {
            yield return new WaitForSeconds(0.8f);
            int attackTarget = attackEvent.targetToDamage;
            attackEvent.AttackEnemy(attackTarget);
            yield return new WaitForSeconds(1.6f);
        }

Атака реализована для меня максимально запутана, Получается он получает сведенья, что в радиусе атаки присутствуют колайдеры с нужными тегами, создает "состояние" что он атакует и кого, а после передает данные вот в этот скрипт.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;



public class attackEvent : MonoBehaviour
{
    public healthBar healthBar;
    public healthStella healthStella;
    public float minDamage;
    public float maxDamage;
    public float curretDamage;
    public EnemyAIwithFOV enemyScript;
    public bool attackPlayer;
    public bool attackStella;
    public int targetToDamage;

    public void Start()
    {
        attackPlayer = false;
        attackStella = false;
    }

    public void Update()
    {
        CheckTargetForDamage();     
    }

    public void CheckTargetForDamage()
    {
        if (enemyScript.attack == true && enemyScript.attackPlayer == true)
        {
            attackPlayer = true;
            
        }
        else attackPlayer = false;

        if (enemyScript.attack == true && enemyScript.attackStella == true)
        {
            attackStella = true;
        }
        else attackStella = false;

        if (attackPlayer)
        {
            targetToDamage = 1;
            Debug.Log("Враг бьет игрока");
        }
        if (attackStella)
        {
            targetToDamage = 2;
            Debug.Log("Враг бьет обелиск");
        }
        if (!attackPlayer && !attackStella)
        {
            targetToDamage = 0;
            Debug.Log("Враг никого не бьет");
        }
    }

    public void Damage(float minDamage, float maxDamage, out float curretDamage)
    {
        curretDamage = Random.Range(minDamage, maxDamage);
    }

    public void SetDamage()
    {
        Damage(minDamage, maxDamage, out curretDamage);

    }

    public void AttackEnemy(int whoToBeat)
    {
            SetDamage();
        if (whoToBeat == 1)
        {
            healthBar.GetDamage();
        }
        if (whoToBeat == 2)
        {
            healthStella.GetDamage();
        }
            
            
        
    }


}

В нем он проверяет кого бьет противник и так же присваивает для цели "типо" индекс (возможно это можно было реализовать через массивы, но я пока плохо в них разбираюсь)

public void CheckTargetForDamage()
        {
            if (enemyScript.attack == true && enemyScript.attackPlayer == true)
            {
                attackPlayer = true;
                
            }
            else attackPlayer = false;
    
            if (enemyScript.attack == true && enemyScript.attackStella == true)
            {
                attackStella = true;
            }
            else attackStella = false;
    
            if (attackPlayer)
            {
                targetToDamage = 1;
                Debug.Log("Враг бьет игрока");
            }
            if (attackStella)
            {
                targetToDamage = 2;
                Debug.Log("Враг бьет обелиск");
            }
            if (!attackPlayer && !attackStella)
            {
                targetToDamage = 0;
                Debug.Log("Враг никого не бьет");
            }
        }
    

Так же там генерируется случайный урон

И после из этого скрипта все отправляется в показатели здоровья целей.

Это ХП для игрока

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.UI;
    
    
    public class healthBar : MonoBehaviour
    {
        
        [Header("Максимальное ХП")]
        public int MaxHealth = 100;
        public float healthValue;
        private int MinHealth = 0;
        public Slider HealthBar;
        public attackEvent attackEvent;
    
        public void Start()
        {
            attackEvent = GetComponent<attackEvent>();
            HealthBar.maxValue = MaxHealth;
            HealthBar.minValue = MinHealth;
            healthValue = Mathf.Clamp(healthValue, MinHealth, MaxHealth);
            
    
        }
    
        public void Update()
     {
       HealthBar.value = healthValue;
     }
    
        public void ChangeHealth(out float curretHP, float damage)
        {
            curretHP = healthValue;
            curretHP -= damage;
            healthValue = curretHP;
            
        }
    
    
        public void GetDamage()
        {
    
            float SetDamage = attackEvent.curretDamage;
            ChangeHealth(out healthValue,SetDamage);
    
        }
    }

Это ХП Обелиска

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


public class healthStella : MonoBehaviour
{

    [Header("Максимальное ХП")]
    public float healthValueStella = 100;
    public attackEvent attackEvent;
    public string curretStellaHealth;

    public void Start()
    {
        attackEvent = GetComponent<attackEvent>();
    }

        public void Update()
    {
        HealthStella();
    }

    public void HealthStella()
    {
        healthValueStella.ToString();
        curretStellaHealth = healthValueStella + " Stella Health";
    }

    public void ChangeHealth(out float curretHP, float damage)
    {
        curretHP = healthValueStella;
        curretHP -= damage;
        healthValueStella = curretHP;

    }


    public void GetDamage()
    {

        float SetDamage = attackEvent.curretDamage;
        ChangeHealth(out healthValueStella, SetDamage);

    }

}

Эт просто для выведение на экране ХП Обелиска

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

public class screemHealthStella : MonoBehaviour
{
    public healthStella healthStella;
    public Text healthText;
    void Update()
    {
        
        healthText.text = healthStella.curretStellaHealth;
    }
}

Буду очень рад, если подскажете, как можно сделать получше, что использовать, как сократить, как сделать более круче или более гибко и податливо для изменений.Скорее всего данные скрипты очень не оптимизированы.

Если прочли и просмотрели все, то отдельное спасибо)

Ответы

▲ 0Принят

ОЧЕНЬ много букв, самые главные намешано дофига всего. Ты с трудом нашел ошибку, другой человек вряд ли сориентируется, да и ты сам спустя время тоже. Комментариев перебор, даже мешают, нужен просто нормальный нейминг.

Поля класса поделены Space, верный признак намешенности, что бы в этом ориентироваться, нужно дробить класс на маленькие с одной единственной ответственностью. Например обнаружение:

[Serializable]
public class Perception : MonoBehaviour
{
    [SerializeField] private float _seeRange = 16;
    [SerializeField] private float _viewAngle = 90f;
    [Space]
    [SerializeField] private float _senseRange = 2;

    public bool IsDetected (Transform observer, Transform target)
    {
        Vector2 observerPosition = new Vector2(observer.position.x, observer.position.z);
        Vector2 targetPosition = new Vector2(target.position.x, target.position.z);
        Vector3 direction = observer.forward;
        Vector2 observerDirection = new Vector2(direction.x, direction.z);
        return IsDetected(observerPosition, observerDirection, targetPosition);
    }

    public bool IsDetected (Vector2 observerPosition, Vector2 observerDirection, Vector2 targetPosition)
    {
        Vector2 delta = targetPosition - observerPosition;
        float deltaSqrMagnitude = delta.sqrMagnitude;
        if (deltaSqrMagnitude > _seeRange * _seeRange) // сравнение длины без вычисление квадратного корня
            return false; // out of see range
        if (deltaSqrMagnitude < _senseRange * _senseRange )
            return true; // in sense range
        float dot = Vector2.Dot(delta.normalized, observerDirection.normalized); // Cos of delta-direction corner
        float deltaAngle = Mathf.Acos(dot) * Mathf.Rad2Deg; // degree of delta-direction corner
        return deltaAngle < _viewAngle * 0.5f; // onView
    }
}

И Perception это просто поле твоего класса вместо одной из кучек полей и методов.


И никаких комментариев к именам нет! от слова совсем! только функционалу! Весь нейминг в контексте, как Perception.SeeRange, а EnemyAIwithFOV.MinDistance - угадай с ста попыток.

И так далее...