• Как бы вы оптимизировали большую сцену в 2D игре?

    K0TlK
    @K0TlK
    Буллю людей.
    Разделить мир на ячейки, обновлять и отрисовывать только те сущности, которые находятся от игрока на определенном радиусе. Не знаю что там тебе и откуда нужно подгружать, 100 на 100 юнитов это небольшая сцена, все ее содержимое спокойно будет помещаться в оперативной памяти, с диска там ничего подгружать не нужно. Гугли spatial hashing, выбирай алгоритм, который тебе лучше подходит
    Ответ написан
    2 комментария
  • Как вращать объект бесконечно?

    K0TlK
    @K0TlK
    Буллю людей.
    public class EndlessRotation : MonoBehaviour
    {
        [SerializeField] private Vector3 _rotation;
            
        private void Update()
        {
            _rotation.x += 10 * Time.deltaTime;
            _rotation.y += 15 * Time.deltaTime;
            _rotation.z += 20 * Time.deltaTime;
    
            for (var i = 0; i < 3; i++)
            {
                if (_rotation[i] >= 360f)
                    _rotation[i] -= 360f;
            }
    
            transform.rotation = Quaternion.Euler(_rotation);
        }
    }


    Отнять 360 если вращение больше или равно 360
    Ответ написан
    9 комментариев
  • Как не использовать OnTriggerStay в моей ситуации?

    K0TlK
    @K0TlK
    Буллю людей.
    Отделяй обработку инпута и тогда таких проблем не будет.
    Weapon

    public class Weapon : MonoBehaviour
    {
        public void Hide()
        {
            gameObject.SetActive(false);
        }
    }


    PlayerInput

    Отслеживает нажатие нужной кнопки в вызывает нужный ивент
    public class PlayerInput : MonoBehaviour
    {
        public event Action InteractionButtonPressed = delegate {  };
    
        private void Update()
        {
            if (Input.GetKeyDown(KeyCode.E))
            {
                InteractionButtonPressed();
            }
        }
    }


    PickUpArea

    Область подбора. При входе в триггер добавляет айтем в список, при выходе удаляет. Если вызывать PickUp - вернет ближайший из айтемов
    public class PickUpArea : MonoBehaviour
    {
        private readonly List<Weapon> _closeWeapons = new();
    
        public bool HasItemsAround => _closeWeapons.Count > 0;
            
        public void PickUp(PlayableCharacter character)
        {
            var closestWeapon = GetClosestWeapon();
            _closeWeapons.Remove(closestWeapon);
            character.Give(closestWeapon);
        }
    
        private Weapon GetClosestWeapon()
        {
            Weapon closest = null;
            var minDistance = float.MaxValue;
    
            foreach (var weapon in _closeWeapons)
            {
                var distance = (transform.position - weapon.transform.position).sqrMagnitude;
                    
                if (distance < minDistance)
                {
                    closest = weapon;
                    minDistance = distance;
                }
            }
                
            return closest;
        }
    
        private void OnTriggerEnter2D(Collider2D col)
        {
            if (col.TryGetComponent(out Weapon weapon))
            {
                _closeWeapons.Add(weapon);
            }
        }
    
        private void OnTriggerExit2D(Collider2D other)
        {
            if (other.TryGetComponent(out Weapon weapon))
            {
                _closeWeapons.Remove(weapon);
            }
        }
    }


    PlayableCharacter

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

    public class PlayableCharacter : MonoBehaviour
    {
        [SerializeField] private PlayerInput _input;
        [SerializeField] private PickUpArea _pickUpArea;
        [SerializeField] private List<Weapon> _weapons = new();
    
        private void Start()
        {
            _input.InteractionButtonPressed += TryPickUp;
        }
    
        private void OnDestroy()
        {
            _input.InteractionButtonPressed -= TryPickUp;
        }
    
        public void Give(Weapon weapon)
        {
            weapon.Hide();
            _weapons.Add(weapon);
        }
    
        private void TryPickUp()
        {
            if (_pickUpArea.HasItemsAround)
            {
                _pickUpArea.PickUp(this);
            }
        }
    }

    Ответ написан
  • Почему анимация с mixamo криво работает в unity?

    K0TlK
    @K0TlK
    Буллю людей.
    63df35f7556fb004857469.jpeg

    Нажимаешь на анимации в окне проект => во вкладке animation жмешь Bake into pose => based upon - original
    Ответ написан
    23 комментария
  • При уничтожений одного обьекта уничтожаются все, как исправить?

    K0TlK
    @K0TlK
    Буллю людей.
    Потому что хп зомби у тебя статик поле. Всё, что статик - принадлежит классу, всё что не статик принадлежит конкретному объекту, класс - фабрика объектов, существует в единичном экземпляре => твое хп одно для всех => если хп == 0, то оно 0 у всех зомби. Убирай статику, делай TryGetComponent(out Zombie zombie) в OnCollisionEnter в Bullet и отнимай хп у конкретного зомби.
    Ответ написан
  • Как игнорировать одинаковые коллайдеры в Unity?

    K0TlK
    @K0TlK
    Буллю людей.
    Edit->Project Settings->Physics/Physics2D Снизу матрица, там выбираешь какие слои с какими могут соприкасаться.
    Ответ написан
    1 комментарий
  • Как использовать json?

    K0TlK
    @K0TlK
    Буллю людей.
    public GameObject[] ObjectsForDestroy;

    Что за объекты?

    Json в PlayerPrefs сохранять не надо, как и строки в принципе, сохраняй в отдельный файл.

    То, что у тебя в OnMouseDown происходит я вообще не понимаю
    Что за а? Что эта переменная вообще означает?
    .GetChild(0).GetChild(0) Когда у тебя порядок в иерархии поменяется, будешь искать ошибку.

    Про Manager, надеюсь, говорить не надо.

    TL;DR

    Создать пустую директорию
    private void CreateDirectory()
            {
                if (Directory.Exists(_directoryPath) == false)
                {
                    Directory.CreateDirectory(_directoryPath);
                }
            }


    Создать файл с нулевыми значениями
    private void CreateFile()
            {
                if (File.Exists(_path)) return;
                
                var json = JsonUtility.ToJson(CompleteStatus.Locked);
                    
                using (var writer = File.CreateText(_path))
                {
                    writer.Write(json);
                    writer.Close();
                }
            }



    Как использовать Json:

    Есть какой-то уровень:

    namespace Levels
    {
        public interface ILevel : IVisualization<ILevelView>
        {
            void Load();
            void Complete(CompleteStatus status);
        }
    }

    namespace Levels
    {
        public interface IVisualization<in TView>
        {
            void Visualize(TView view);
        }
    }



    Есть его вьюшка:

    namespace Levels
    {
        public enum CompleteStatus
        {
            Locked,
            Uncompleted,
            OneStar,
            TwoStars,
            ThreeStars
        }
        public interface ILevelView
        {
            void DrawCompletion(CompleteStatus status);
        }
    }



    Реализация уровня:

    Сохранение нужно вынести отдельно, но мне лень это делать.
    При вызове Load загружается уровень.
    При вызове Complete уровень завершается и сохраняется количество звезд на которое пройден уровень, у меня здесь максимум три звезды, если нужно сохранять какой-то счет, создавай структуру, которую будешь сохранять вместо перечисления.
    using System.IO;
    using UnityEngine;
    using UnityEngine.SceneManagement;
    
    namespace Levels
    {
        public abstract class Level : ILevel
        {
            private readonly string _name;
    
            private readonly string _path;
    
            private readonly string _directoryPath = $"{Application.persistentDataPath}/Saves";
    
            protected Level(string name)
            {
                _name = name;
                _path = $"{_directoryPath}/{_name}.json";
                CreateDirectory();
                CreateFile();
            }
    
            public void Load()
            {
                SceneManager.LoadScene(_name);
            }
    
            public void Complete(CompleteStatus status)
            {
                SaveStatus(status);
            }
    
            public void Visualize(ILevelView view)
            {
                view.DrawCompletion(LoadStatusFromJson());
            }
    
            private void CreateDirectory()
            {
                if (Directory.Exists(_directoryPath) == false)
                {
                    Directory.CreateDirectory(_directoryPath);
                }
            }
    
            private void CreateFile()
            {
                if (File.Exists(_path)) return;
                
                var json = JsonUtility.ToJson(CompleteStatus.Locked);
                    
                using (var writer = File.CreateText(_path))
                {
                    writer.Write(json);
                    writer.Close();
                }
            }
            
            private void SaveStatus(CompleteStatus status)
            {
                var json = JsonUtility.ToJson(status);
                File.WriteAllText(_path, json);
            }
            
            private CompleteStatus LoadStatusFromJson()
            {
                var file = File.ReadAllText(_path);
                return JsonUtility.FromJson<CompleteStatus>(file);
            }
            
        }
    }


    Собственно, так выглядит уровень:
    namespace Levels
    {
        public class SecondLevel : Level
        {
            public SecondLevel() : base(nameof(SecondLevel))
            {
            }
        }
    }



    Реализация вьюшки:

    т.к. это вьюшка, можно говнокодить как хочешь.
    На основании статуса у звезд устанавливается конкретный цвет и активность кнопки.
    using System;
    using UnityEngine;
    using UnityEngine.UI;
    
    namespace Levels
    {
        public class LevelView : MonoBehaviour, ILevelView
        {
            [SerializeField] private Image _firstStar, _secondStar, _thirdStar;
            [SerializeField] private Button _button;
            [SerializeField] private Color _inactiveStarColor, _activeStarColor;
            
            public void DrawCompletion(CompleteStatus status)
            {
                switch (status)
                {
                    case CompleteStatus.Locked:
                        _firstStar.color = _inactiveStarColor;
                        _secondStar.color = _inactiveStarColor;
                        _thirdStar.color = _inactiveStarColor;
                        _button.interactable = false;
                        break;
                    case CompleteStatus.Uncompleted:
                        _button.interactable = true;
                        _firstStar.color = _inactiveStarColor;
                        _secondStar.color = _inactiveStarColor;
                        _thirdStar.color = _inactiveStarColor;
                        break;
                    case CompleteStatus.OneStar:
                        _button.interactable = true;
                        _firstStar.color = _activeStarColor;
                        _secondStar.color = _inactiveStarColor;
                        _thirdStar.color = _inactiveStarColor;
                        break;
                    case CompleteStatus.TwoStars:
                        _button.interactable = true;
                        _firstStar.color = _activeStarColor;
                        _secondStar.color = _activeStarColor;
                        _thirdStar.color = _inactiveStarColor;
                        break;
                    case CompleteStatus.ThreeStars:
                        _button.interactable = true;
                        _firstStar.color = _activeStarColor;
                        _secondStar.color = _activeStarColor;
                        _thirdStar.color = _activeStarColor;
                        break;
                    default: throw new ArgumentException();
                }
            }
        }
    }



    И тест

    using System.Collections.Generic;
    using UnityEngine;
    
    namespace Levels
    {
        public class LevelList : MonoBehaviour
        {
            [SerializeField] private LevelView _levelViewPrefab;
            [SerializeField] private Transform _levelsParent;
    
            private ILevel[] _levels;
            
            private void Awake()
            {
                var first = new FirstLevel();
                var second = new SecondLevel();
                var third = new ThirdLevel();
                var fourth = new FourthLevel();
                var fifth = new FifthLevel();
                
                _levels = new ILevel[]
                {
                    first, second, third, fourth, fifth
                };
    
                //test
                first.Complete(CompleteStatus.OneStar);
                second.Complete(CompleteStatus.TwoStars);
                third.Complete(CompleteStatus.ThreeStars);
                fourth.Complete(CompleteStatus.Uncompleted);
                fifth.Complete(CompleteStatus.Locked);
    
                VisualizeLevels(_levels);
            }
    
            private void VisualizeLevels(IEnumerable<ILevel> levels)
            {
                foreach (var level in levels)
                {
                    var view = Instantiate(_levelViewPrefab, _levelsParent);
                    level.Visualize(view);
                }
            }
        }
    }



    Так выглядит:
    63144a1583e29173450114.jpeg
    Ответ написан
    1 комментарий
  • Потерялся в сцене в Unity (3D). Как найти то что делал?

    K0TlK
    @K0TlK
    Буллю людей.
    Не гоняйте пацаны. Дважды кликни по любому объекту в иерархии и тебя к нему перекинет.
    Ответ написан
    1 комментарий
  • Как посчитать количество пройденных кругов?

    K0TlK
    @K0TlK
    Буллю людей.
    Не знаю что ты там намудрил, твой код абсолютно нечитабельный.
    Прибавляешь к вращению, чтобы крутилось в одну сторону, вычитаешь, чтобы крутилось в другую. Полный круг - 360 градусов. Вращение делишь на 360 = количество полных кругов. Математика 6 класс привет.

    Кодстайл, который используется в дотнет.

    public class Lever : MonoBehaviour
    {
        private float _rotation = 0f;
    
        private int LapsCount => (int)(_rotation / 360);
    
        private void Rotate(float angle)
        {
            _rotation += angle;
            transform.rotation = Quaternion.Euler(new Vector3(0, 0, _rotation));
        }
    
        private void Update()
        {
            if (Input.GetKey(KeyCode.A))
            {
                Rotate(100f * Time.deltaTime);
            }
    
            if (Input.GetKey(KeyCode.D))
            {
                Rotate(-100f * Time.deltaTime);
            }
    
            if (Input.GetKeyDown(KeyCode.Space))
            {
                Debug.Log(LapsCount);
            }
        }
    }
    Ответ написан
    Комментировать
  • Не останавливается корутина. Что нужно дописать чтобы ее остановить?

    K0TlK
    @K0TlK
    Буллю людей.
    Hsys

    PCont

    Много времени сохранил себе, сократив названия?

    TakeDamage

    Что это вообще такое и зачем оно нужно?

    DamageCondition

    Метод назван так, будто он должен что-то возвращать, но возвращает он ничего.

    Почему вообще урон от пик персонаж наносит сам себе, а не пики ему?

    Ты хочешь сделать миллион компонентов и потом навешивать их по-одному на персонажа? Не надо так делать. В итоге у тебя будет миллион компонентов, которые зависят друг от друга и, чтобы потом собрать другого персонажа, тебе придется искать всё, из чего он состоит. Делай все через интерфейсы.

    Отдельно здоровье

    namespace Coroutines
    {
        public interface IHealth
        {
            bool IsOver { get; }
            void Lose(float amount);
            void Restore(float amount);
        }
    }


    using System;
    using UnityEngine;
    
    namespace Coroutines
    {
        public class Health : IHealth
        {
            private readonly float _max;
            private readonly float _min;
            private float _current;
    
            public Health(float max, float min = 0)
            {
                _max = max;
                _min = min;
                _current = _max;
            }
    
            public bool IsOver => _current <= _min;
            
            public void Lose(float amount)
            {
                if (amount <= 0) throw new ArgumentException();
                
                SetCurrent(_current - amount);
            }
    
            public void Restore(float amount)
            {
                if (amount <= 0) throw new ArgumentException();
    
                SetCurrent(_current + amount);
            }
    
            private void SetCurrent(float amount)
            {
                _current = Mathf.Clamp(amount, _min, _max);
            }
        }
    }



    Отдельно персонаж

    namespace Coroutines
    {
        public interface IDamageable
        {
            bool IsDead { get; }
            void ApplyDamage(float amount);
        }
    }


    using UnityEngine;
    
    namespace Coroutines
    {
        public class Character : MonoBehaviour, IDamageable
        {
            [SerializeField] private float _maxHp = 100f;
    
            private IHealth _health;
    
            public bool IsDead => _health.IsOver;
    
            private void Awake()
            {
                _health = new Health(_maxHp);
            }
    
            public void ApplyDamage(float amount) => _health.Lose(amount);
        }
    }



    И отдельно ловушка

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    namespace Coroutines
    {
        public class Trap : MonoBehaviour
        {
            [SerializeField] private float _damage = 10f;
            [SerializeField] private float _delay = 2f;
            
            private readonly Dictionary<IDamageable, Coroutine> _targets = new();
    
            private void OnTriggerEnter2D(Collider2D other)
            {
                if (other.TryGetComponent(out IDamageable damageable))
                {
                    var coroutine = StartCoroutine(Damaging(damageable));
                    _targets.Add(damageable, coroutine);
                }
            }
    
            private void OnTriggerExit2D(Collider2D other)
            {
                if (other.TryGetComponent(out IDamageable damageable))
                {
                    var coroutine = _targets[damageable];
                    StopCoroutine(coroutine);
                    _targets.Remove(damageable);
                }
            }
    
            private IEnumerator Damaging(IDamageable target)
            {
                while (target.IsDead == false)
                {
                    target.ApplyDamage(_damage);
                    yield return new WaitForSeconds(_delay);
                }
            }
    
        }
    }



    Как именно должен дамаг наноситься ты не сказал, поэтому у меня каждому персонажу, что зашел в триггер, урон наносится отдельно. На входе в триггер персонаж добавляется в список и стартуется корутина, на выходе корутина завершается и персонаж из списка удаляется. Чтобы завершить определенную корутину, ее нужно кэшировать куда-то.
    Ответ написан
    3 комментария
  • Почему у объекта может импульс применяться неправильно?

    K0TlK
    @K0TlK
    Буллю людей.
    Тебе не нужна здесь физика и AddForce тем более. Просто двигай пулю, изменяя позицию, лови столкновение через OnCollisionEnter и отражай направление движения.

    Пуля

    using UnityEngine;
    
    namespace Bullets
    {
        [RequireComponent(typeof(Rigidbody2D), typeof(Collider2D))]
        public class Bullet : MonoBehaviour, IBullet
        {
            [SerializeField] private float _speed = 5f;
            [SerializeField] private int _damage = 10;
            [SerializeField] private int _maxReflections = 5;
    
            private Rigidbody2D _rigidbody;
            private Vector2 _direction = Vector2.zero;
    
            private void Awake()
            {
                _rigidbody = GetComponent<Rigidbody2D>();
                _rigidbody.isKinematic = true;
                _rigidbody.useFullKinematicContacts = true;
            }
            
            public void Shoot(Vector2 direction)
            {
                _direction = direction.normalized;
            }
    
            private void FixedUpdate()
            {
                if (_direction == Vector2.zero) return;
    
                var deltaMovement = _direction * _speed * Time.deltaTime;
    
                _rigidbody.MovePosition(_rigidbody.position + deltaMovement);
            }
    
            private void OnCollisionEnter2D(Collision2D other)
            {
                if (other.transform.TryGetComponent(out IDamageable damageable))
                {
                    damageable.ApplyDamage(_damage);
                    Destroy();
                    return;
                }
    
                var normal = other.GetContact(0).normal;
                Reflect(normal);
            }
    
            private void Destroy()
            {
                Destroy(gameObject);
            }
    
            private void Reflect(Vector2 normal)
            {
                if (_maxReflections - 1 == 0) Destroy();
    
                _maxReflections--;
                _direction = Vector2.Reflect(_direction, normal).normalized;
            }
        }
    }



    И убогий скрипт оружия

    using UnityEngine;
    
    namespace Bullets
    {
        public class SomeGun : MonoBehaviour
        {
            [SerializeField] private Bullet _bulletPrefab;
            [SerializeField] private Transform _bulletOrigin;
            [SerializeField] private Camera _camera;
    
            private void Update()
            {
                LookAtMouse(Input.mousePosition);
    
                if (Input.GetKeyDown(KeyCode.Mouse0))
                {
                    Shoot();
                }
            }
    
            private void LookAtMouse(Vector3 mousePosition)
            {
                var gunScreenPosition = _camera.WorldToScreenPoint(transform.position);
                
                var direction = mousePosition - gunScreenPosition;
    
                direction.Scale(new Vector3(1, 1, 0));
    
                transform.up = direction;
            }
    
            private void Shoot()
            {
                var bullet = Instantiate(_bulletPrefab, _bulletOrigin.position, Quaternion.identity);
    
                bullet.Shoot(transform.up);
            }
        }
    }



    Физика здесь абсолютно не нужна, как и рейкасты.
    Ответ написан
    2 комментария
  • Как можно сократить код в OnTriggerEnter2D?

    K0TlK
    @K0TlK
    Буллю людей.
    Убрать фигурные скобочки. А если использовать TryGetComponent, то еще больше сократишь.
    private void OnTriggerEnter2D(Collider2D coll) 
        {
            if (coll.gameObject.CompareTag("Player"))
            {
                _isPlayerInside= true;
            }
        }


    ==>

    private void OnTriggerEnter2D(Collider2D coll) 
        {
            if (coll.TryGetComponent(out Player player) _isPlayerInside = true;
        }


    А можно пойти дальше и вообще всё в одну строку уместить.

    private void OnTriggerEnter2D(Collider2D coll) { if (coll.TryGetComponent(out Player player)) _isPlayerInside = true; }


    Ну как? Читабельно? Не нужно всё пытаться сократить. У тебя был читабельный метод из четырех строк, но я, путем сокращения, превратил его в нечитабельное месиво символов. Сокращать нужно, когда у тебя явно есть повтор кода или в одном методе столько строк, что, чтобы разобраться как он работает, тебе приходится делать комментарии в этом методе.
    Ответ написан
    2 комментария
  • Как правильно сделать след от пули при повороте?

    K0TlK
    @K0TlK
    Буллю людей.
    Ответ написан
    Комментировать
  • Зачем нужно прописывать float rotationY = transform.localEulerAngles.y; (unity)?

    K0TlK
    @K0TlK
    Буллю людей.
    Эффект будет тот же

    Нет не будет.
    В случае с rotationY, если у тебя было ненулевое вращение по оси Y, то оно сохранится, т.е. Если localEulerAngles.y было 90, то оно и останется 90. Если же просто 0 написать, то localEulerAngles.y тоже станет 0.
    Ответ написан
    Комментировать
  • Как можно улучшить код этот прыжка?

    K0TlK
    @K0TlK
    Буллю людей.
    Убери проверку по тэгу.
    transform.position замени на Rigidbody.MovePosition.
    Таймер из апдейта в корутину перемести и в Jump ее запускай. Чтобы остановить прыжок, закэшируй корутину и останавливай ее, когда нужно.
    40 вынеси в отдельное поле.
    Это у тебя не PhysicsJump, физики здесь никакой нет, это анимированный прыжок, переименуй. IsOnGrounded - "является на приземленный". grounded достаточно. У тебя IsOnGrounded всегда противоположно IsJumping. Зачем здесь два флага?
    Это лишний монобех, перемести код отсюда в Movement свой. Если движение у тебя вдоль поверхности, то нормаль, которую ты получаешь при движении, можно использовать в качестве замены флагов grounded и jumping (если нормаль не нулевая, то можно прыгать, если нулевая, то прыжок происходит). Соответственно, теперь тебе не нужны тэги, т.к. землю ты проверяешь через нормаль, флаги, по той же причине, не нужен апдейт, т.к. анимация происходит в корутине, не нужен лишний монобех, чтобы не делать из объекта кучу монобехов. Остается только паблик метод Jump и корутина.
    Ответ написан
    5 комментариев
  • Соответсвует ли код принципам солид?

    K0TlK
    @K0TlK
    Буллю людей.
    Разделять код на миллион монобехов != ооп. Единственный способ достичь правильного ооп в юнити - отделять всю логику от движка, а монобехи использовать как вьюшку, чтобы отображать всякое. Но реализуемый для тебя способ - использовать интерфейсы, под каждый паблик метод отдельный интерфейс, взаимодействовать через эти интерфейсы, учиться нормально прокидывать зависимости: всегда должна быть какая-то точка входа, где будут инициализироваться компоненты и передаваться все зависимости.
    Virtual методы = плохо, класс в идеале должен быть либо abstract либо sealed.
    Protected поля = public поля = нарушение инкапсуляции/иммутабельности.
    PlayerHealth, PlayerEnergy, Health - это все одно и то же с разными реализациями, есть интерфейс IHealth все под него и просто этот интерфейс реализуешь.
    AudioPlayer вообще какой-то ужас. Во-первых, почему GameObject takeDamageSourseObject и т.д Почему GameObject, если ты потом получаешь у него компонент AudioSource, ты можешь конкретные компоненты в инспекторе передавать, т.е. не GameObject, a AudioSource и GetComponent потом делать не нужно будет. Во-вторых, из всего этого можно было сделать один компонент, в котором будет один метод Play, который будет принимать AudioSource и проигрывать его. Либо напрямую пропихивать AudioSource и воспроизводить звук.
    Нет какого-то единого кодстайла, где-то есть нижнее подчеркивание, где-то нет, где-то есть _cs где-то нет, для чего эта _cs я так и не понял. Где-то сериализуемое приватное поле, где-то тупо паблик.
    Про солид вообще смысла говорить нет. Интерфейсов пара штук на весь проект. Зависимости нормально не прокидываются, всё через поля.
    Это так, навскидку. Думаю, если зайти в папку bot, то можно будет диссертацию написать по содержимому этой папки.
    Ответ написан
    4 комментария
  • Почему бот идет к цели не всегда?

    K0TlK
    @K0TlK
    Буллю людей.
    Любой искусственный интеллект, возможности которого выходят за рамки "принеси подай иди подальше не мешай", проще будет сделать через Behaviour Tree. Без него, чем сложнее у тебя будет бот, тем больше кода придется писать и тем больше будет каких-то непонятных условий, что у тебя, собственно, и проявляется в апдейте, полностью состоящем из if else if else if else. С Behaviour Tree все куда проще, есть много разных классов, отвечающих за что-то одно, все они совмещаются в разные деревья.
    Пример я покажу с использованием ассета Behaviour Designer, т.к., если я буду делать свою реализацию BT, то, боюсь, места для текста в ответе не хватит. Стоит Designer 90$, к счастью, пиратить софт очень плохо и так делают только плохие люди.

    Для того, чтобы бот ходил, нужно направление движения, соответственно, это направление нужно откуда-то брать:
    using UnityEngine;
    
    namespace BehaviourTree
    {
        public interface IMovementInput
        {
            Vector3 Direction { get; }
        }
    }


    Ну и, собственно, сам бот:
    using UnityEngine;
    
    namespace BehaviourTree
    {
        [RequireComponent(typeof(Rigidbody))]
        public class Bot : MonoBehaviour
        {
            [SerializeField] private MonoBehaviour _botInput = null;
            [SerializeField] private float _speed = 10f;
    
            private Rigidbody _rb;
    
            private IMovementInput BotInput => (IMovementInput) _botInput;
    
            private void OnValidate()
            {
                if (_botInput is IMovementInput) return;
    
                Debug.LogError($"{nameof(_botInput)} should implement {nameof(IMovementInput)}");
                _botInput = null;
            }
    
            private void Awake()
            {
                _rb = GetComponent<Rigidbody>();
            }
    
            private void FixedUpdate()
            {
                var velocity = new Vector3(BotInput.Direction.x, 0, BotInput.Direction.z) * _speed;
                _rb.velocity = velocity;
            }
    
            public void LookAt(Vector3 direction)
            {
                var rotation = Quaternion.LookRotation(direction);
    
                transform.rotation = rotation;
            }
        }
    }


    Инжектим интерфейс ввода через инспектор и в FixedUpdate задаем скорость. Чтобы посмотреть в каком-то направлении, нужно вызвать метод LookAt. Чтобы прокинуть интерфейс через инспектор нужен какой-то монобех, который будет реализовывать интерфейс. Вот он:
    using UnityEngine;
    
    namespace BehaviourTree
    {
        public class BotInput : MonoBehaviour, IMovementInput
        {
            public Vector3 Direction { get; set; }
        }
    }

    В дальнейшем через этот BotInput я буду перемещать бота, просто задавая нужное направление. С ботом все, теперь можно накинуть Bot на какой-нибудь капсюль на сцене, на него же повесить BotInput и в поле BotInput компонента Bot поместить тот самый BotInput.

    62c017ffaf2a1016283427.png

    Теперь нужна какая-то цель, чтобы бот за ней шел. Интерфейс:
    using UnityEngine;
    
    namespace BehaviourTree
    {
        public interface ITarget
        {
            Vector3 Position { get; }
        }
    }

    Реализация:
    using UnityEngine;
    
    namespace BehaviourTree
    {
        public class SomeTarget : MonoBehaviour, ITarget
        {
            public Vector3 Position => transform.position;
        }
    }

    Все то же самое, что и с ботом. Поместить на какой-нибудь капсюль, отличный от капсюля бота.

    Ну а теперь BT. Добавляешь своему боту компонент Behaviour Tree: в инспекторе бота -> Add Component -> Behaviour Designer -> Behaviour Tree.
    Далее нужно как-то туда добавить инпут бота, для этого создаем скрипт:
    using BehaviorDesigner.Runtime;
    
    namespace BehaviourTree.Brain
    {
        public class SharedBotInput : SharedVariable<BotInput>
        {
            public static implicit operator SharedBotInput(BotInput input) => new SharedBotInput {Value = input};
        }
    }

    Теперь BotInput можно прокидывать в переменные Behaviour Designer. Кстати о них. Открываем Tools -> Behaviour Designer -> Editor, далее тыкаем на бота, к которому прикреплен компонент Behaviour Tree, в дизайнере открываем Variables в поле Name вводим botInput, в поле Type выбираем BotInput, нажимает Add. Далее в инспекторе бота перетаскиваем компонент BotInput в поле botInput компонента Behaviour Tree.

    62c018177e9a0543052458.png

    Теперь в Behavoiur Designer можно устанавливать значение Direction компонента BotInput.
    Сначала я покажу весь код, что я написал для управления ботом, а потом то, как это все выглядит в дизайнере.
    Бот будет очень простой - он будет просто ходить в рандомном направлении, а если увидит свой таргет, то будет тупо бежать в него.

    Собственно, хождение в рандомном направлении:
    using BehaviorDesigner.Runtime;
    using BehaviorDesigner.Runtime.Tasks;
    using UnityEngine;
    
    namespace BehaviourTree.Brain
    {
        public class SetRandomDirection : Action
        {
            public SharedVector3 Direction;
    
            public override TaskStatus OnUpdate()
            {
                Direction.Value = Vector3.Scale(Random.insideUnitSphere.normalized, new Vector3(1, 0, 1));
                return TaskStatus.Success;
            }
        }
    }

    Наследуясь от Action, SetRandomDirection появляется в нодах(Tasks) дизайнера в выпадающем меню Actions
    Теперь нужно как-то установить направление движения бота.
    using BehaviorDesigner.Runtime;
    using BehaviorDesigner.Runtime.Tasks;
    
    namespace BehaviourTree.Brain
    {
        public class SetInput : Action
        {
            public SharedBotInput SharedInput;
            public SharedVector3 Input;
    
            public override TaskStatus OnUpdate()
            {
                SharedInput.Value.Direction = Input.Value;
                return TaskStatus.Success;
            }
        }
    }

    Собственно, здесь все понятно. Берем значение из Input и присваиваем его в SharedInput.
    Поворот в определенном направлении:
    using BehaviorDesigner.Runtime.Tasks;
    
    namespace BehaviourTree.Brain
    {
        public class LookAtDirection : Action
        {
            public SharedBotInput Direction;
            public Bot Bot;
    
            public override TaskStatus OnUpdate()
            {
                Bot.LookAt(Direction.Value.Direction);
                return TaskStatus.Success;
            }
        }
    }

    Далее идет нахождение цели и следование за ней, соответственно цель эту надо тоже добавить в дизайнер.
    using BehaviorDesigner.Runtime;
    
    namespace BehaviourTree.Brain
    {
        public class SharedTarget : SharedVariable<SomeTarget>
        {
            public static implicit operator SharedTarget(SomeTarget target) => new SharedTarget {Value = target};
        }
    }

    Установка направления к цели:
    using BehaviorDesigner.Runtime;
    using BehaviorDesigner.Runtime.Tasks;
    using UnityEngine;
    
    namespace BehaviourTree.Brain
    {
        public class SetDirectionToTarget : Action
        {
            public SomeTarget Target;
            public Bot Origin;
            public SharedVector3 Direction;
    
            public override TaskStatus OnUpdate()
            {
                var direction = (Target.Position - Origin.transform.position).normalized;
                Direction.Value = new Vector3(direction.x, 0, direction.z);
                return TaskStatus.Success;
            }
        }
    }


    Теперь нужно как-то дать возможность боту увидеть цель. Интерфейс:
    namespace BehaviourTree.Brain
    {
        public interface IEye
        {
            bool InSight(ITarget target);
        }
    }

    Реализация:
    using UnityEngine;
    
    namespace BehaviourTree.Brain
    {
        public class Eye : IEye
        {
            private readonly float _fov;
            private readonly Transform _origin;
    
            public Eye(float fov, Transform origin)
            {
                _fov = fov;
                _origin = origin;
            }
    
            public bool InSight(ITarget target)
            {
                var direction = target.Position - _origin.position;
    
                var angle = Mathf.Acos(Vector3.Dot(_origin.forward.normalized, direction.normalized)) * Mathf.Rad2Deg;
    
                return angle <= _fov;
            }
        }
    }

    Здесь все просто. Глаз, у которого есть Field of View в методе InSight проверяем, входит ли позиция таргета в этот фов. Проверяется все очень просто. Находим направление от бота к таргету, вычисляем угол между этим вектором направления и направлением взгляда бота, сравниваем. Чтобы вычислить угол нужно знать всего лишь формулу скалярного произведения векторов из нее выводим то, что у меня в коде умножение на Mathf.Rad2Deg потому что Mathf.Acos возвращает угол в радианах, а я его перевожу в градусы.
    Ну и последнее - нахождение цели:

    Все таки нужно было писать свою реализацию BT, потому что места все равно не хватило, продолжение в комментах.
    Ответ написан
    5 комментариев
  • Как отслеживать состояние компонента через систему событий в Unity?

    K0TlK
    @K0TlK
    Буллю людей.
    соответственно, нет точного понимания, когда именно произойдет изменение свойства Interactable

    Это твоя проблема. Изменять внутреннее состояние объекта напрямую извне нельзя. Делай обертку, которая будет вызывать ивент, когда канвасгруп становится интеракивным/не интерактивным.

    Напишу тот же пример, что и на SO тебе написал.
    public class SomeCanvas : MonoBehaviour
    {
        public event Action<bool> ActiveStateChanged;
            
        [SerializeField] private CanvasGroup _canvasGroup;
    
        public void Activate()
        {
            SetActiveState(true);
        }
    
        public void Deactivate()
        {
            SetActiveState(false);
        }
    
        private void SetActiveState(bool state)
        {
            if (_canvasGroup.interactable == state) return;
            
            _canvasGroup.interactable = state;
            ActiveStateChanged?.Invoke(state);
        }
    }
    
    public class NotSomeCanvas : MonoBehaviour
    {
        [SerializeField] private SomeCanvas _someCanvas;
    
        private void OnEnable()
        {
            _someCanvas.ActiveStateChanged += LogActiveState;
        }
    
        private void OnDisable()
        {
            _someCanvas.ActiveStateChanged -= LogActiveState;
        }
    
        private void LogActiveState(bool state)
        {
            print($"Current state - {state}");
        }
    }


    SomeCanvas - обертка, через него делаешь интерактивным/не интерактивным свой канвасгруп, он вызывает ивент, на ивент подписываешь нужные методы. Можешь разделить на 2 ивента Activated/Deactivated чтобы не делать проверки на false или true, как тебе удобно
    Ответ написан
    Комментировать
  • Что я не так делаю с интерфейсами?

    K0TlK
    @K0TlK
    Буллю людей.
    Что я не так делаю с интерфейсами?

    Всё.
    У тебя у одного интерфейса слишком много ответственностей у него и Warside какой-то есть и дамаг может принимать и умереть может.

    Разделяй этот интерфейс на несколько.

    namespace Health
    {
        public interface IHealth
        {
            void Lose(int amount);
            void Restore(int amount);
        }
        
        public interface IMutable<out T>
        {
            T Current { get; }
        }
        
        public interface IFinal
        {
            event Action Over;
        }
    }


    Получается такой Health:

    using System;
    using UnityEngine;
    
    namespace Health
    {
        public class Health : IHealth, IFinal, IMutable<int>
        {
            public event Action Over;
            private readonly int _max;
            private const int Min = 0;
    
            public Health(int max)
            {
                _max = max;
                Current = _max;
            }
            
            public int Current { get; private set; }
            
            public void Lose(int amount)
            {
                SetCurrent(Current - amount);
            }
    
            public void Restore(int amount)
            {
                SetCurrent(Current + amount);
            }
    
            private void SetCurrent(int amount)
            {
                Current = Mathf.Clamp(amount, Min, _max);
                
                if (Current == Min) Over?.Invoke();
            }
            
        }
    }


    Health не должен быть отдельным компонентом, который будет висеть на условном рыцаре. Рыцарь будет содержать в себе этот Health, но напрямую хп ему изменять никто не будет, поэтому нужен еще один интерфейс IDamageable:
    namespace Health
    {
        public interface IDamageable
        {
            void ApplyDamage(int amount);
        }
    }


    И сам рыцарь:
    using UnityEngine;
    
    namespace Health
    {
        public class Knight : MonoBehaviour, IDamageable
        {
            [SerializeField] private int _maxHealth = 100;
            private Health _health;
    
            private void Awake()
            {
                _health = new Health(_maxHealth);
            }
    
            private void OnEnable()
            {
                _health.Over += Die;
            }
    
            private void OnDisable()
            {
                _health.Over -= Die;
            }
    
            public void ApplyDamage(int amount)
            {
                _health.Lose(amount);
                Debug.Log($"Damaged, hp left - {_health.Current}");
            }
    
            private void Die()
            {
                Debug.Log("Died");
                Destroy(gameObject);
            }
        }
    }


    Теперь, чтобы нанести урон рыцарю, нужно получить компонент IDamageable и вызвать его метод ApplyDamage:
    using UnityEngine;
    
    namespace Health
    {
        public class Enemy : MonoBehaviour
        {
            [SerializeField] private int _damage = 50;
            
            private void OnTriggerEnter2D(Collider2D other)
            {
                if (other.TryGetComponent(out IDamageable damageable))
                {
                    damageable.ApplyDamage(_damage);
                }
            }
        }
    }


    Всё. Используй TryGetComponent и тогда не нужно будет делать миллион проверок является ли что-то null.
    Warside твой должен висеть на рыцаре, а не на хп, поэтому делай отдельный интерфейс под этот Warside.
    Ответ написан
    5 комментариев
  • Можно ли сделать приватное свойство видимым в инспекторе?

    K0TlK
    @K0TlK
    Буллю людей.
    [field: SerializeField] public int Number { get; private set; }
    Ответ написан
    1 комментарий