• Как можно сократить код в 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; }


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

    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
    Буллю людей.
    Во-первых, velocity не нужно умножать на deltaTime. Во-вторых, ты применяешь скорость перед тем, как у тебя инпут обработается(когда keyCodeMovement == false), перемести строчку rb.velocity = ... в самый низ этого условия после обработки инпута. В-третьих, у тебя одновременно может быть нажата только одна кнопка, либо a либо w либо s либо d. else if (Input.GetKey(KeyCode.W)) else здесь убери. Ну и в-четвертых, он работает некорректно, потому что он некорректен изначально.

    Можешь сделать то, что я написал выше, а можешь сделать нормально. Есть два путя, для сильных и для слабых. В обоих случаях есть общее. Два интерфейса, которые описывают движение и инпут:
    using UnityEngine;
    
    namespace Movement
    {
        public interface IMovementInput
        {
            Vector2 Direction { get; }
        }
        
        public interface IMovement
        {
            void Move(Vector2 direction);
        }
    }


    И само передвижение:
    using UnityEngine;
    
    namespace Movement
    {
        public class Movement : IMovement
        {
            private readonly Rigidbody2D _origin;
            private readonly float _speed;
    
            public Movement(Rigidbody2D origin, float speed)
            {
                _origin = origin;
                _speed = speed;
            }
    
            public void Move(Vector2 direction)
            {
                if (direction == Vector2.zero) return;
                
                _origin.velocity = direction * _speed;
            }
        }
    }


    Далее путь для сильных.
    Есть два разных объекта, которые обрабатывают инпут - передвижение только на стрелочках:
    using UnityEngine;
    
    namespace Movement.ForStrongGigaChad
    {
        public class Arrows : IMovementInput
        {
            public Vector2 Direction => GetDirection();
    
            private Vector2 GetDirection()
            {
                var x = 0f;
                var y = 0f;
    
                if (Input.GetKey(KeyCode.LeftArrow))
                {
                    x = -1f;
                } else if (Input.GetKey(KeyCode.RightArrow))
                {
                    x = 1f;
                }
    
                if (Input.GetKey(KeyCode.UpArrow))
                {
                    y = 1f;
                } else if (Input.GetKey(KeyCode.DownArrow))
                {
                    y = -1f;
                }
                
    
                return new Vector2(x, y);
            }
        }
    }

    и передвижение на стрелочках и на WASD:
    using UnityEngine;
    
    namespace Movement.ForStrongGigaChad
    {
        public class WASDArrows : IMovementInput
        {
            public Vector2 Direction => GetDirection();
    
            private Vector2 GetDirection()
            {
                var x = Input.GetAxis("Horizontal");
                var y = Input.GetAxis("Vertical");
    
                
                return new Vector2(x, y);
            }
        }
    }


    Все очень просто, надеюсь до этого момента ничего объяснять не нужно.
    Далее идет игрок:
    using UnityEngine;
    
    namespace Movement.ForStrongGigaChad
    {
        [RequireComponent(typeof(Rigidbody2D))]
        public class Player : MonoBehaviour
        {
            private float _speed;
            private IMovementInput _input;
            private IMovement _movement;
    
            public void Init(IMovementInput input, float speed)
            {
                _input = input;
                _speed = speed;
                
                var rb = GetComponent<Rigidbody2D>();
    
                _movement = new Movement(
                    rb,
                    _speed);
            }
    
            private void FixedUpdate()
            {
                _movement.Move(_input.Direction);
                
            }
        }
    }


    Здесь все тоже просто. В FixedUpdate происходит движение. Метод Init - метод инициализации, после спавна этого игрока, нужно вызывать этот метод и передавать зависимости. Чтобы спавнить этого персонажа нужна фабрика. Вот ее интерфейс:
    using UnityEngine;
    
    namespace Movement.ForStrongGigaChad
    {
        public interface IFactory<out TObject>
        where TObject : MonoBehaviour
        {
            TObject Spawn();
        }
    }

    И его реализация:
    using UnityEngine;
    
    namespace Movement.ForStrongGigaChad
    {
        public class PlayerFactory : IFactory<Player>
        {
            private readonly IMovementInput _input;
            private readonly float _speed;
            private readonly Player _prefab;
    
            public PlayerFactory(IMovementInput input, Player prefab, float speed)
            {
                _input = input;
                _prefab = prefab;
                _speed = speed;
            }
    
            public Player Spawn()
            {
                var obj = Object.Instantiate(_prefab);
                obj.Init(_input, _speed);
                return obj;
            }
        }
    }


    Здесь все тоже предельно просто. Принимает нужные зависимости через конструктор и, при вызове метода Spawn спавнит игрока и инициализирует его. И, чтобы все это было на сцене, нужно где-то это все спавнить:
    using UnityEngine;
    
    namespace Movement.ForStrongGigaChad
    {
        public class Root : MonoBehaviour
        {
            [SerializeField] private Player _playerPrefab;
            [SerializeField] private float _firstPlayerSpeed = 10f;
            [SerializeField] private float _secondPlayerSpeed = 10f;
    
            private void Awake()
            {
                var playerOne = new PlayerFactory(
                    new Arrows(),
                    _playerPrefab,
                    _firstPlayerSpeed)
                    .Spawn();
    
                var player = new PlayerFactory(
                    new WASDArrows(),
                    _playerPrefab,
                    _secondPlayerSpeed)
                    .Spawn();
            }
        }
    }

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

    Но есть и способ для слабых. Инпут со стрелочек:
    using UnityEngine;
    
    namespace Movement.ForWeak
    {
        public class Arrows : MonoBehaviour, IMovementInput
        {
            public Vector2 Direction => GetDirection();
    
            private Vector2 GetDirection()
            {
                var x = 0f;
                var y = 0f;
    
                if (Input.GetKey(KeyCode.LeftArrow))
                {
                    x = -1f;
                } else if (Input.GetKey(KeyCode.RightArrow))
                {
                    x = 1f;
                }
    
                if (Input.GetKey(KeyCode.UpArrow))
                {
                    y = 1f;
                } else if (Input.GetKey(KeyCode.DownArrow))
                {
                    y = -1f;
                }
                
    
                return new Vector2(x, y);
            }
        }
    }

    инпут для WASD:
    using UnityEngine;
    
    namespace Movement.ForWeak
    {
        public class WASDArrows : MonoBehaviour, IMovementInput
        {
            public Vector2 Direction => GetDirection();
    
            private Vector2 GetDirection()
            {
                var x = Input.GetAxis("Horizontal");
                var y = Input.GetAxis("Vertical");
    
                
                return new Vector2(x, y);
            }
        }
    }


    Отличаются они от первой версии тем, что отнаследованы от MonoBehaviour, поэтому их нужно вешать на какой-то объект на сцене. И собственно сам игрок:
    using UnityEngine;
    
    namespace Movement.ForWeak
    {
        public class Player : MonoBehaviour
        {
            [SerializeField] private MonoBehaviour _input = null;
            [SerializeField] private float _speed = 10f;
    
            private IMovement _movement;
            
            private IMovementInput Input => (IMovementInput) _input;
    
            private void OnValidate()
            {
                if (_input is IMovementInput) return;
                
                Debug.LogError($"{nameof(_input)} should implement {nameof(IMovementInput)}");
                _input = null;
            }
    
            private void Awake()
            {
                var rb = GetComponent<Rigidbody2D>();
    
                _movement = new Movement(
                    rb,
                    _speed);
            }
    
            private void FixedUpdate()
            {
                _movement.Move(Input.Direction);
            }
        }
    }

    Здесь все просто. На сцене сделать 2 объекта, навешать на них скрипт Player, вставить нужный инпут в поля в инспекторе, установить каждому нужную скорость. Будет все работать точно так же, как и первый вариант. Благодаря OnValidate я инжекчу интерфейс через испектор. Вот. А ну и еще, у тебя XPos и YPos нулю никогда не будут равны, следовательно, стоять на месте персонаж не может, обнуляй их перед тем, как инпут считывать.
    Ответ написан
    3 комментария
  • Что я не так делаю с интерфейсами?

    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.
    Ответ написан
    2 комментария
  • Можно ли сделать приватное свойство видимым в инспекторе?

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

    K0TlK
    @K0TlK
    Буллю людей.
    Код твой изменять не буду, сам все перепишешь на моем примере.

    Начнем с низов. Есть ввод от игрока. Для этого ввода вводим интерфейс:
    namespace SmoothMovement
    {
        public interface IPlayerInput
        {
            float Acceleration { get; }
        }
    }


    Далее нам нужно сглаживать этот Acceleration, значит вводим еще один интерфейс:
    namespace SmoothMovement
    {
        public interface ISmoothAcceleration
        {
            float Smooth(float acceleration, float input);
        }
    }


    Далее реализуем IPlayerInput:
    using UnityEngine;
    
    namespace SmoothMovement
    {
        public class MobileInput : MonoBehaviour, IPlayerInput
        {
            public float Acceleration { get; private set; }
    
            [SerializeField] private float _minAcceleration = -1f;
            [SerializeField] private float _maxAcceleration = 1f;
            [SerializeField] private float _smoothMultiplier = 5f;
            [Range(0, 1)] [SerializeField] private float _fadingSpeed = 0.01f;
    
            private ISmoothAcceleration _smoothing;
    
            private void Awake()
            {
                _smoothing = new SmoothedAcceleration(_minAcceleration, _maxAcceleration, _smoothMultiplier, _fadingSpeed);
            }
    
            private void Update()
            {
                Acceleration = _smoothing.Smooth(Acceleration, Input.acceleration.x);
            }
        }
    }


    В апдейте присваиваем свойству Acceleration сглаженное значение инпута. Далее само сглаживание:
    using UnityEngine;
    
    namespace SmoothMovement
    {
        public class SmoothedAcceleration : ISmoothAcceleration
        {
            private readonly float _multiplier;
            private readonly float _minValue;
            private readonly float _maxValue;
            private readonly float _fadingSpeed;
    
            
            public SmoothedAcceleration(float minValue, float maxValue, float multiplier, float fadeSpeed)
            {
                _minValue = minValue;
                _maxValue = maxValue;
                _multiplier = multiplier;
                _fadingSpeed = fadeSpeed;
            }
            
            public float Smooth(float acceleration, float input)
            {
                if (input == 0)
                {
                    acceleration = Mathf.Lerp(acceleration, 0, _fadingSpeed);
                    return acceleration;
                }
                
                acceleration += input * _multiplier * Time.deltaTime;
                acceleration = Mathf.Clamp(acceleration, _minValue, _maxValue);
    
                return acceleration;
            }
        }
    }


    Есть минимальные и максимальное значение ускорения, множитель - чем он больше, тем быстрее разгоняться будет и скорость затухания ускорения - чем больше тем быстрее ускорение будет стремиться к нулю. И тест:
    using UnityEngine;
    
    namespace SmoothMovement
    {
        public class TestMovement : MonoBehaviour
        {
            [SerializeField] private MonoBehaviour _input = null;
            [SerializeField] private float _speed = 10f;
            
            
            private IPlayerInput Input => (IPlayerInput)_input;
    
    
            private void OnValidate()
            {
                if (_input is IPlayerInput) return;
                
                Debug.LogError($"{nameof(_input)} should implement {nameof(IPlayerInput)}");
                _input = null;
            }
    
            private void FixedUpdate()
            {
                Move(Input.Acceleration);
            }
    
            private void Move(float direction)
            {
                if (direction == 0) return;
                
                var position = transform.position;
                position.x += direction * _speed * Time.deltaTime;
                transform.position = position;
            }
        }
    }

    Инжектим IPlayerInput через инспектор, двигаем геймобжект. С вращением делай сам что-нибудь, я не знаю как у тебя там что должно вращаться
    Ответ написан
    Комментировать
  • Плавное заполнение слайдер. Как сделать?

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

    using System.Collections;
    using UnityEngine;
    using UnityEngine.UI;
    
    public class SliderFilling : MonoBehaviour
    {
        [SerializeField] private Slider _slider = null;
        [SerializeField] private float _fillTime = 3f;
    
        private void Update()
        {
            if (Input.GetKeyDown(KeyCode.Space))
            {
                StartCoroutine(FillValue(0.5f));
            }
        }
    
        private IEnumerator FillValue(float value)
        {
            var estimateTime = 0f;
            
            while(estimateTime < _fillTime)
            {
                estimateTime += Time.deltaTime;
                _slider.value = Mathf.Lerp(0, value, estimateTime / _fillTime);
                yield return null;
            }
        }
    }
    Ответ написан
  • Почему Unity складывает числа через раз?

    K0TlK
    @K0TlK
    Буллю людей.
    Скорее всего ошибка появляется из-за того, что ты нарушаешь инкапсуляцию. Объект должен быть иммутабельным, т.е. извне его поля и свойства никто не должен изменять, он сам должен изменять свое состояние. У тебя же поля Speed и другие изменяются в нескольких местах, поэтому очень сложно определить источник ошибки.
    В качестве примера приведу систему опыта.

    using System;
    using System.Collections.Generic;
    
    namespace Assets.Scripts.Experience
    {
        public class Leveling
        {
            public event Action<LevelingData> Updated;
    
            private Dictionary<int, int> _levelToExprerience = new Dictionary<int, int>()
            {
                {0, 100 },
                {1, 500 },
                {2, 1000 },
                {3, 2000 }
            };
    
            public int CurrentLevel { get; private set; } = 0;
            public int CurrentExperience { get; private set; } = 0;
            public int ExperienceToLevelUp => _levelToExprerience[CurrentLevel];
    
            public void AddExperience(int amount)
            {
                IncreaseExperience(amount);
                UpdateData();
            }
    
            private void IncreaseExperience(int amount)
            {
                if (amount < 0) throw new ArgumentOutOfRangeException(nameof(amount) + " can't be less than 0");
    
                if (CurrentExperience + amount >= ExperienceToLevelUp)
                {
                    var expLeft = CurrentExperience + amount - ExperienceToLevelUp;
                    LevelUp();
                    IncreaseExperience(expLeft);
                    return;
                }
    
                CurrentExperience += amount;
            }
    
            private void LevelUp()
            {
                CurrentLevel++;
                ResetExperience();
            }
    
            private void ResetExperience()
            {
                CurrentExperience = 0;
            }
    
            private void UpdateData()
            {
                var data = new LevelingData(CurrentLevel, CurrentExperience, ExperienceToLevelUp);
                Updated?.Invoke(data);
            }
    
        }
    
        public struct LevelingData
        {
            public LevelingData(int level, int experience, int experienceToLevelUp)
            {
                Level = level;
                Experience = experience;
                ExperienceToLevelUp = experienceToLevelUp;
            }
    
            public int Level { get; private set; }
            public int Experience { get; private set; }
            public int ExperienceToLevelUp { get; private set; }
        }
    }


    Есть класс Leveling, поля и свойства которого извне никак не изменить. Все взаимодействие с ним происходит через метод AddExperience, который вызывает приватный метод IncreaseExperience, который делает то, что нужно, т.е. проверяет на 0, добавляет нужное количество опыта, повышает уровень и т.д. После увеличения опыта, вызывается событие Updated, которое говорит, что модель обновилась. Теперь можно легко прикрутить отображение уровней, опыта и т.д.

    using UnityEngine;
    using UnityEngine.UI;
    
    namespace Assets.Scripts.Experience
    {
        public class Test : MonoBehaviour
        {
            [SerializeField] private Text _experience = null;
            [SerializeField] private Text _experienceToLevelUp = null;
            [SerializeField] private Text _level = null;
    
            private Leveling _leveling = new Leveling();
    
            private void OnEnable()
            {
                _leveling.Updated += UpdateUI;
            }
    
            private void OnDisable()
            {
                _leveling.Updated -= UpdateUI;
            }
    
            private void Update()
            {
                if (Input.GetKeyDown(KeyCode.Space))
                {
                    _leveling.AddExperience(100);
                }
    
                if (Input.GetKeyDown(KeyCode.LeftShift))
                {
                    _leveling.AddExperience(300);
                }
    
                if (Input.GetKeyDown(KeyCode.LeftControl))
                {
                    _leveling.AddExperience(-100);
                }
            }
    
            private void UpdateUI(LevelingData data)
            {
                _experience.text = data.Experience.ToString();
                _experienceToLevelUp.text = data.ExperienceToLevelUp.ToString();
                _level.text = data.Level.ToString();
            }
        }
    }


    В OnEnable подписываем метод UpdateUI на событие Updated, теперь, когда будет вызываться событие Updated, текстовые поля будут обновляться. Теперь, если прикрепить компонент Test на какой-нибудь объект, закинуть в поля с текстом текст и запустить, то при нажатии пробела будет добавляться 100 опыта, при нажатии шифта 300, при нажатии контрола будет выводиться ошибка.

    А искать ошибку в твоем коде никто не будет, т.к. проще все заново переписать.
    Ответ написан
  • Почему OnTriggerStay2D работает не сразу?

    K0TlK
    @K0TlK
    Буллю людей.
    Потому что OnTriggerStay вызывается не каждый кадр. Отдели ввод игрока. WakeUp - костыль, которого не должно быть. Вот примерно то, что тебе надо:
    public class KeyboardInput : MonoBehaviour, IPlayerInput
    {
        public event Action ActionButtonPressed;
    
        private void Update()
        {
            if (Input.GetKeyDown(KeyCode.F))
            {
                ActionButtonPressed?.Invoke();
            }
        }
    }
    
    public interface IPlayerInput
    {
        public event Action ActionButtonPressed;
    }


    Отдельно инпут.

    [RequireComponent(typeof(Collider2D))]
    public class DialogueTrigger : MonoBehaviour
    {
        [SerializeField] private KeyboardInput _input;
    
        private void OnTriggerEnter2D(Collider2D collision)
        {
            if (collision.TryGetComponent(out Player player))
            {
                _input.ActionButtonPressed += StartDialogue;
            }
        }
    
        private void OnTriggerExit2D(Collider2D collision)
        {
            if (collision.TryGetComponent(out Player player))
            {
                _input.ActionButtonPressed -= StartDialogue;
                EndDialogue();
            }
        }
    
        private void StartDialogue()
        {
            Debug.Log("DialogueStarted");
        }
    
        private void EndDialogue()
        {
            Debug.Log("DialogueEnded");
        }
    
    }


    Отдельно все остальное. На входе подписываемся, на выходе отписываемся. Все. Не нужен никакой OnTriggerStay и WakeUp.
    Ответ написан
    8 комментариев
  • Как отследить, что объект находится около точки в 2d на unity?

    K0TlK
    @K0TlK
    Буллю людей.
    Проверяй дистанцию между платформой и точкой
    var distance = (point.position - transform.position).magnitude;

    Vector3.magnitude Возвращает длину вектора. Вот пример:
    public class Example : MonoBehaviour
    {
        [SerializeField] private Transform _pointA, _pointB = null;
        [SerializeField] private float _speed = 10f;
        [SerializeField] private float _closeDistance = 0.2f;
    
        private Transform _currentPoint;
    
        private void Start()
        {
            _currentPoint = _pointA;
        }
    
        private void FixedUpdate()
        {
            MoveTo(_currentPoint.position);
        }
    
        private void MoveTo(Vector3 position)
        {
            var nextPosition = transform.position;
            var delta = (position - transform.position).normalized;
    
            delta *= _speed * Time.deltaTime;
    
            nextPosition += delta;
    
            transform.position = nextPosition;
    
            UpdatePoint();
        }
    
        private void UpdatePoint()
        {
            var distance = (_currentPoint.position - transform.position).magnitude;
            if (distance <= _closeDistance)
            {
                SwitchPoint();
            }
        }
    
        private void SwitchPoint()
        {
            _currentPoint = _currentPoint == _pointA ? _pointB : _pointA;
        }
    
    }


    В UpdatePoint я рассчитываю расстояние между точкой и позицией платформы и, если оно меньше максимальной дистанции приближения, то меняю точку, к которой стремится платформа.

    С тем же успехом можно сравнивать позиции платформы и точки, к которой она стремится и, если они равны, менять точку.
    Ответ написан
    Комментировать
  • Как нажимать на кнопки в 3D?

    K0TlK
    @K0TlK
    Буллю людей.
    Ты делаешь рейкаст куда-то вперед от какого-то трансформа, а тебе нужно делать рейкаст в сторону курсора. Camera.ScreenPointToRay возвращает луч, который идет от камеры к месту на экране. Пример:
    var ray = Camera.main.ScreenPointToRay(Input.mousePosition);
    if (Physics.Raycast(ray, out RaycastHit hit, Mathf.Infinity))
    {
        Debug.DrawRay(hit.point, hit.normal * 10, Color.red, 10f);
    }


    И не надо так использовать тэги. Когда у тебя будет не 4 кнопки, а 40, то в методе OnButtonClick у тебя будет 40 условий? А если ты написал тэг с ошибкой, то будешь искать потом откуда ошибки валят? Выдели эти кнопки отдельно, получай нужный компонент при рейкасте и вызывай метод OnButtonClick уже у отдельного компонента.
    public interface IButton
    {
        public void OnClick();
    }
    
    public class YellowButton : IButton
    {
        public void OnClick()
        {
            Debug.Log("Yellow");
        }
    }
    
    public class Example : MonoBehaviour
    {
        private void Update()
        {
            if (Input.GetMouseButtonDown(0))
            {
                var ray = Camera.main.ScreenPointToRay(Input.mousePosition);
                if (Physics.Raycast(ray, out RaycastHit hit, Mathf.Infinity))
                {
                    if(hit.transform.TryGetComponent(out IButton button))
                    {
                        button.OnClick();
                    }
                }
            }
    
        }
    }


    Также, каждый компонент может иметь метод OnMouseDown, который вызывается, когда игрок нажимает на него. Можно создать компонент кнопки и при нажатии на него будет вызываться ивент, на который можно подписать нужные методы.
    public class Button : MonoBehaviour
    {
        public event Action ButtonPressed;
    
        private void OnMouseDown()
        {
            ButtonPressed?.Invoke();
        }
    
    }
    
    public class ButtonHandler : MonoBehaviour
    {
        [SerializeField] private Button _button;
    
        private void OnEnable()
        {
            _button.ButtonPressed += DoStuff;
        }
    
        private void OnDisable()
        {
            _button.ButtonPressed -= DoStuff;
        }
    
        private void DoStuff()
        {
    
        }
    }
    Ответ написан
    Комментировать
  • Почему деньги не вычитаются при покупке из переменной?

    K0TlK
    @K0TlK
    Буллю людей.
    Какой раз ты уже задаешь этот вопрос? Третий? Ты ничему не научишься, если будешь просто копипастить код, который кто-то написал за тебя. Ты где-то нашел код, скопировал его и пытаешься изменить, не понимая того, как все работает.
    Ты и не отнимаешь эти деньги из первого скрипта, ты загружаешь данные из json в BuyCharacter, изменяешь их и сохраняешь.
    Тебе сюда и сюда
    Ответ написан
  • Почему может не реагировать объект на условие нажатия кнопки?

    K0TlK
    @K0TlK
    Буллю людей.
    OnTriggerEnter вызывается один раз, когда объект входит в триггер. Соответственно, все эти проверки выполняются единожды. Т.е. в одном кадре у объекта, вошедшего в триггер должен быть тэг player и должна быть нажата кнопка F. Первое условие может быть и соблюдено, если тэг правильно написан, но чтобы соблюсти второе, нужно одновременно с входом в триггер нажать кнопку F, что сделать сложно. Умники, конечно, посоветуют использовать OnTriggerStay, но не нужно этого делать. Нужно отделить ввод. Получится что-то типа этого:
    private void OnTriggerEnter(Collider other)
    {
        if (other.TryGetComponent(out Player player))
        {
            PlayerInput.ActionButtonPressed += DoStuff;
        }
    }
    
    private void OnTriggerExit(Collider other)
    {
        if (other.TryGetComponent(out Player player))
        {
            PlayerInput.ActionButtonPressed -= DoStuff;
        }
    }
    
    private void DoStuff()
    {
        //Do stuff
    }


    В OnTriggerEnter проверяется, присутствует ли компонент Player на объекте. Именно компонент, а не тэг. Не нужно делать проверки по тэгу, т.к. это строки и ты можешь ошибиться в написании этой строки, делай проверки по наличию компонента. Если компонент имеется, то подписываем нужный метод на событие ActionButtonPressed. В OnTriggerExit, соответственно, отписываемся. И в OnDestroy тоже, если объект будет уничтожен. Отписываться нужно всегда.
    И грубый пример PlayerInput ниже:
    public class PlayerInput : MonoBehaviour
    {
        public static event Action ActionButtonPressed;
    
    
        private void Update()
        {
            if (Input.GetKeyDown(KeyCode.F))
            {
                ActionButtonPressed?.Invoke();
            }
        }
    }
    Ответ написан
    Комментировать
  • Как сделать так, чтобы функция вызывалась при изменении переменной в Unity?

    K0TlK
    @K0TlK
    Буллю людей.
    Если ползунок - юнитивский слайдер, то у него есть событие onValueChanged. Подписываешь на это событие метод, который изменяет громкость и все.
    Что-то типа этого:
    [SerializeField] private Slider _slider;
    
    private void OnEnable()
    {
        _slider.onValueChanged.AddListener(ChangeVolume);
    }
    
    private void OnDisable()
    {
        _slider.onValueChanged.RemoveListener(ChangeVolume);
    }
    
    private void ChangeVolume(float amount)
    {
        //изменить громкость на значение amount
    }
    Ответ написан
    1 комментарий
  • Как убрать инверсию при слежке за курсором?

    K0TlK
    @K0TlK
    Буллю людей.
    GGWPKATASI, Еще раз объясню. Когда вы поворачиваете персонажа, вы разворачиваете только персонажа. Спрайт оружия все еще повернут туда, куда и был повернут. Вам нужно оружие тоже флипать. Сейчас покажу пример.

    Есть скрипт игрока.
    using UnityEngine;
    
    namespace Assets.Scripts.Rotation
    {
        [RequireComponent(typeof(Rigidbody2D))] // Нужен, чтобы компонент Rigidbody2D всегда был на объекте, к которому прикреплен данный скрипт
        public class Player : MonoBehaviour
        {
            [SerializeField] private float _maxSpeed; //[SerializeField] позволяет видеть private поля в инспекторе
            [SerializeField] private float _speed;
            [Range(0f, 1f)]
            [SerializeField] private float _slowDownSpeed; //Range ограничивает между 0 и 1 в данном случае
            [SerializeField] private Gun _gun;
    
            private Vector2 _movementDirection;
            private Rigidbody2D _rb;
    
            private void Start()
            {
                _rb = GetComponent<Rigidbody2D>(); // Получаем компонент, если получаете компонент таким образом, обязательно нужно делать [RequireComponent(typeof(НазваниеКомпонента))]
            }
    
            private void Update()
            {
                var horizontal = Input.GetAxis("Horizontal");
                var vertical = Input.GetAxis("Vertical");
                _movementDirection = new Vector2(horizontal, vertical);
            }
    
            private void FixedUpdate() // все действия с физикой в FixedUpdate
            {
                if (_movementDirection == Vector2.zero) SlowDown(_slowDownSpeed);
                Move(_movementDirection, _speed);
                Flip();
            }
    
            private void Flip()
            {
                var vector = Camera.main.ScreenToWorldPoint(Input.mousePosition) - transform.position; //берем вектор направления от нашей позиции до позиции мыши в мировых координатах
                if (vector.x < 0) transform.eulerAngles = new Vector3(0, 180); // Если он левее, то поворачиваем объект налево
                if (vector.x >= 0) transform.eulerAngles = new Vector2(0, 0);// или направо, если он правее
                _gun.Flip(vector.x);// поворачиваем оружие
            }
    
            private void Move(Vector2 direction, float speed)
            {
                var velocity = _rb.velocity;
                var x = velocity.x + direction.x * speed;// определяем новую скорость
                var y = velocity.y + direction.y * speed;
                velocity.x = Mathf.Clamp(x, -_maxSpeed, _maxSpeed); //ограничиваем скорость
                velocity.y = Mathf.Clamp(y, -_maxSpeed, _maxSpeed);
                _rb.velocity = velocity; // присваиваем скорость
            }
    
            private void SlowDown(float speed) // замедляем объект
            {
                var velocity = _rb.velocity;
                velocity.x = Mathf.Lerp(velocity.x, 0, speed);
                velocity.y = Mathf.Lerp(_rb.velocity.y, 0, speed);
                _rb.velocity = velocity;
            }
        }
    }


    И оружия:
    using UnityEngine;
    
    namespace Assets.Scripts.Rotation
    {
        [RequireComponent(typeof(SpriteRenderer))]
        public class Gun : MonoBehaviour
        {
            private SpriteRenderer _spriteRenderer;
    
            private void Start()
            {
                _spriteRenderer = GetComponent<SpriteRenderer>();
            }
    
            private void Update()
            {
                var difference = Camera.main.ScreenToWorldPoint(Input.mousePosition) - transform.position;
                var rotation = Mathf.Atan2(difference.y, difference.x) * Mathf.Rad2Deg;
                transform.rotation = Quaternion.Euler(0f, 0f, rotation); // ваш код поворота оружия
            }
    
            public void Flip(float direction)
            {
                _spriteRenderer.flipY = direction < 0; // отражаем спрайт по оси Y в зависимости от того, куда смотрит персонаж
            }
    
        }
    }


    Итак. Игрок. Двигается, смотрит в сторону мыши.
    Оружие. Смотрит на мышь.
    Можете потестировать, посмотреть как все работает.
    Главное у Игрока у компонента Rigidbody2D поставить gravity scale на 0.
    Всё это работает, если изначально спрайт игрока смотрит вправо, спрайт оружия смотрит вправо.
    Вот так: 623a14e2ef06f683850837.jpeg
    Мега краткое объяснение в картинках:
    Изначально оружие смотрит туда - 623a164516692952768870.jpeg
    Но когда мы поворачиваем персонажа, то оно тоже поворачивается вот так - 623a1665a5bac426619425.jpeg
    Соответственно, нужно отразить картинку по Y, чтобы ее выровнять - 623a168c06707463491882.jpeg
    Ответ написан
    Комментировать