В тази статия ние продължаваме да разработваме контролер за символи за 2D платформинг в Unity, като подробно изследваме всяка стъпка от конфигурирането и оптимизирането на контролите. В предишната статия, „ “, обсъдихме подробно как да създадем основата на героя, включително неговото физическо поведение и основно движение. Сега е време да преминем към по-напреднали аспекти, като обработка на въвеждане и динамично проследяване на камерата. Как да създадете 2D контролер на персонажи в Unity: Част 1 В тази статия ще се задълбочим в настройването на новата система за въвеждане на Unity, създаване на активни действия за контрол на героя, разрешаване на скачане и осигуряване на правилни отговори на команди на играча. Ако искате сами да приложите всички промени, описани в тази статия, можете да изтеглите клона на хранилището „ “, който съдържа основата за тази статия. Като алтернатива можете да изтеглите клона „ “ с крайния резултат. Тяло на символа Контролер на символи Настройка на системата за въвеждане Преди да започнем да пишем код за управление на нашия герой, трябва да конфигурираме системата за въвеждане в проекта. За нашия платформинг избрахме новата система за въвеждане на Unity, представена преди няколко години, която остава актуална поради предимствата си пред традиционната система. Системата за въвеждане предлага по-модулен и гъвкав подход към обработката на въвеждане, което позволява на разработчиците лесно да настройват контроли за различни устройства и да поддържат по-сложни сценарии за въвеждане без допълнителни разходи за внедряване. Първо инсталирайте пакета Input System. Отворете Package Manager от главното меню, като изберете Window → Package Manager. В секцията Unity Registry намерете пакета „Input System“ и щракнете върху „Install“. След това отидете на настройките на проекта чрез менюто Редактиране → Настройки на проекта. Изберете раздела Player, намерете секцията Active Input Handling и я задайте на „Input System Package (New)“. След като изпълните тези стъпки, Unity ще ви подкани да рестартирате. След рестартиране всичко ще бъде готово за конфигуриране на контроли за нашия капитан. Създаване на входни действия В папката създайте Действия за въвеждане чрез главното меню: . Наименувайте файла „Контроли“. Настройки Активи → Създаване → Действия за въвеждане Input System на Unity е мощен и гъвкав инструмент за управление на въвеждане, който позволява на разработчиците да конфигурират контроли за герои и игрови елементи. Поддържа различни входни устройства. Действията за въвеждане, които създавате, осигуряват централизирано управление на въвеждането, опростявайки настройката и правейки интерфейса по-интуитивен. Щракнете двукратно върху файла , за да го отворите за редактиране, и добавете за управление на знаци, наречена „Character“. Controls карта на действие Карта на действие в Unity е колекция от действия, които могат да бъдат свързани с различни контролери и клавиши за изпълнение на конкретни задачи в играта. Това е ефективен начин за организиране на контроли, позволяващ на разработчиците да разпределят и коригират входове, без да пренаписват кода. За повече подробности вижте официалната . документация на Input System Първото действие ще се нарича „Преместване“. Това действие ще определи посоката на движение на героя. Задайте на "Стойност" и на "Вектор2", за да активирате движение в четири посоки. типа действие типа контрол Задайте обвързване на това действие, като изберете Добавяне нагоре/надолу/надясно/наляво Composite и присвоете познатите WASD клавиши към съответните им посоки. Не забравяйте да запазите настройките си, като щракнете върху . Тази настройка гарантира, че можете да преназначите обвързвания за действието „Преместване“, например на клавишите със стрелки или дори на джойстика на геймпада. Запазване на актива След това добавете ново действие — „Скок“. Запазете като „Бутон“, но добавете ново — „Натискане“ и задайте на „Натискане и освобождаване“, тъй като трябва да уловим както натискането, така и освобождаването на бутона. типа действие взаимодействие поведение на задействане Това завършва схемата за контрол на героите. Следващата стъпка е да напишете компонент, който да обработва тези действия. Преместване на героя наляво и надясно Време е да свържем входните действия, които създадохме за контрол на героите, с компонента , позволявайки на героя активно да се движи през сцената според нашите контролни команди. CharacterBody За да направим това, ще създадем скрипт, отговарящ за контрола на движението, и ще го наречем за яснота. В този скрипт първо ще дефинираме някои основни полета. Ще добавим препратка към компонента , , който ще се контролира директно от скрипта. CharacterController CharacterBody _characterBody Също така ще зададем параметри за скоростта на движение на героя ( ) и височината на скока ( ). Освен това ще дефинираме предназначението на полето . _speed _jumpHeight _stopJumpFactor Може би сте забелязали, че в много 2D платформинги височината на скока може да се контролира. Колкото по-дълго се задържи бутонът за скок, толкова по-високо скача героят. По същество първоначалната скорост нагоре се прилага в началото на скока и тази скорост се намалява, когато бутонът се пусне. определя с колко намалява скоростта нагоре при отпускане на бутона за скок. _stopJumpFactor Ето пример за кода, който ще напишем: // CharacterController.cs public class CharacterController : MonoBehaviour { [SerializeField] private CharacterBody _characterBody; [Min(0)] [SerializeField] private float _speed = 5; [Min(0)] [SerializeField] private float _jumpHeight = 2.5f; [Min(1)] [SerializeField] private float _stopJumpFactor = 2.5f; } След това ще приложим възможността за преместване на героя наляво и надясно. Когато задържите натиснат бутона за движение, героят трябва да поддържа определената скорост на движение, независимо от препятствията. За да постигнем това, ще добавим променлива в скрипта за съхраняване на текущата скорост на движение по повърхността (или просто хоризонтално, когато героят е във въздуха): // CharacterController.cs private float _locomotionVelocity; В компонента ще въведем метод за задаване на тази скорост: CharacterBody // CharacterBody.cs public void SetLocomotionVelocity(float locomotionVelocity) { Velocity = new Vector2(locomotionVelocity, _velocity.y); } Тъй като нашата игра не разполага с наклонени повърхности, този метод е доста прост. В по-сложни сценарии ще трябва да отчетем състоянието на тялото и наклона на повърхността. Засега просто запазваме вертикалната компонента на скоростта, като променяме само хоризонталната координата. x След това ще зададем тази стойност в метода за всеки кадър: Update // CharacterController.cs private void Update() { _characterBody.SetLocomotionVelocity(_locomotionVelocity); } Ще дефинираме метод за обработка на сигнали от действието за въвеждане на : Move // CharacterController.cs public void OnMove(InputAction.CallbackContext context) { var value = context.ReadValue<Vector2>(); _locomotionVelocity = value.x * _speed; } Тъй като действието е дефинирано като , контекстът ще предостави векторна стойност в зависимост от това кои клавиши са натиснати или освободени. Например, натискането на клавиша ще доведе до получаване на вектора (1, 0) от метода . Едновременното натискане на и ще доведе до (1, 1). Пускането на всички клавиши ще задейства със стойност (0, 0). Move Vector2 D OnMove D W OnMove За клавиша векторът ще бъде (-1, 0). В метода вземаме хоризонталния компонент на получения вектор и го умножаваме по зададената скорост на движение, . A OnMove _speed Обучение на героя да скача Първо, трябва да научим компонента да се справя със скачането. За да направим това, ще добавим метод, отговорен за скока: CharacterBody // CharacterBody.cs public void Jump(float jumpSpeed) { Velocity = new Vector2(_velocity.x, jumpSpeed); State = CharacterState.Airborne; } В нашия случай този метод е ясен: той задава вертикалната скорост и незабавно променя състоянието на героя на . Airborne След това трябва да определим скоростта, с която героят трябва да скочи. Вече сме определили височината на скока и знаем, че гравитацията постоянно действа върху тялото. Въз основа на това първоначалната скорост на скок може да се изчисли по формулата: Където е височината на скок, а е гравитационното ускорение. Ще вземем предвид и гравитационния множител, присъстващ в компонента . Ще добавим ново поле за определяне на началната скорост на скок и ще я изчислим, както следва: h g CharacterBody // CharacterController.cs private float _jumpSpeed; private void Awake() { _jumpSpeed = Mathf.Sqrt(2 * Physics2D.gravity.magnitude * _characterBody.GravityFactor * _jumpHeight); } Ще ни трябва друго поле, за да проследим дали героят скача в момента, така че да можем да ограничим скоростта на скока в подходящия момент. Освен това, ако играчът задържи бутона за скок до кацане, трябва сами да нулираме този флаг. Това ще бъде направено в метода : Update // CharacterController.cs private bool _isJumping; private void Update() { if (_characterBody.State == CharacterState.Grounded) { _isJumping = false; } //... } Сега нека напишем метода за обработка на действието : Jump // CharacterController.cs public void OnJump(InputAction.CallbackContext context) { if (context.started) { Jump(); } else if (context.canceled) { StopJumping(); } } Тъй като действието е бутон, можем да определим от контекста дали натискането на бутона е започнало ( ) или е приключило ( ). Въз основа на това ние или започваме, или спираме скока. Jump context.started context.canceled Ето метода за изпълнение на скока: // CharacterController.cs private void Jump() { if (_characterBody.State == CharacterState.Grounded) { _isJumping = true; _characterBody.Jump(_jumpSpeed); } } Преди да скочим, проверяваме дали героят е на земята. Ако е така, задаваме флага и караме тялото да скочи с . _isJumping _jumpSpeed Сега нека внедрим поведението за спиране на скок: // CharacterController.cs private void StopJumping() { var velocity = _characterBody.Velocity; if (_isJumping && velocity.y > 0) { _isJumping = false; _characterBody.Velocity = new Vector2( velocity.x, velocity.y / _stopJumpFactor); } } Спираме скока само ако флагът е активен. Друго важно условие е персонажът да се движи нагоре. Това предотвратява ограничаване на скоростта на падане, ако бутонът за скок бъде освободен, докато се движите надолу. Ако всички условия са изпълнени, нулираме флага и намаляваме вертикалната скорост с фактор . _isJumping _isJumping _stopJumpFactor Настройка на героя Сега, когато всички компоненти са готови, добавете компонентите и към обекта в сцената. Уверете се, че сте избрали компонента , който създадохме, а не стандартния компонент Unity, предназначен за контролиране на 3D герои. PlayerInput CharacterController Captain CharacterController За присвоете съществуващия компонент от знака. За задайте предварително създадените в полето . CharacterController CharacterBody PlayerInput контроли Actions След това конфигурирайте компонента PlayerInput да извиква подходящите методи от CharacterController. Разгънете секциите Събития и Знак в редактора и свържете съответните методи с действията Преместване и Прескачане. Сега всичко е готово за стартиране на играта и тестване как всички конфигурирани компоненти работят заедно. Движение на камерата Сега трябва да накараме камерата да следва героя, където и да отиде. Unity предоставя мощен инструмент за управление на камерата — . Cinemachine Cinemachine е революционно решение за управление на камерата в Unity, което предлага на разработчиците широка гама от възможности за създаване на динамични, добре настроени камерни системи, които се адаптират към нуждите на играта. Този инструмент улеснява прилагането на сложни техники на камерата, като проследяване на герои, автоматично регулиране на фокуса и много други, добавяйки жизненост и богатство към всяка сцена. Първо, намерете обекта в сцената и добавете компонента към него. Main Camera CinemachineBrain След това създайте нов обект в сцената с име . Това ще бъде камерата, която следва капитана, точно като професионален оператор. Добавете към него компонента . Задайте полето на капитана, изберете за полето и задайте параметъра на 4. CaptainCamera CinemachineVirtualCamera Follow Framing Transposer Body Lens Ortho Size Освен това ще ни трябва още един компонент, за да дефинираме отместването на камерата спрямо героя — . Задайте стойността на 1,5 и стойността на -15. CinemachineCameraOffset Y Z Сега нека тестваме как камерата следва нашия герой. Мисля, че се получи доста добре. Забелязах, че камерата от време на време леко заеква. За да коригирам това, зададох полето Blend Update Method на обекта Main Camera на FixedUpdate. Подобряване на скокове Нека тестваме актуализираната механика. Опитайте да бягате и непрекъснато да скачате. Опитните геймъри може да забележат, че скоковете не винаги се регистрират. В повечето игри това не е проблем. Оказва се, че е трудно да се предвиди точното време на кацане, за да се натисне отново бутона за скок. Трябва да направим играта по-прощаваща, като позволим на играчите да натиснат леко скок преди кацане и да накарат героя да скочи веднага след кацане. Това поведение е в съответствие с това, с което геймърите са свикнали. За да приложим това, ще въведем нова променлива, , която представлява прозореца от време, през който скок все още може да бъде задействан, ако възникне възможност. _jumpActionTime // CharacterController.cs [Min(0)] [SerializeField] private float _jumpActionTime = 0.1f; Добавих поле , което маркира края на прозореца за действие за прескачане. С други думи, докато се достигне , персонажът ще скочи, ако се появи възможност. Нека също да актуализираме манипулатора на действие . _jumpActionEndTime _jumpActionEndTime Jump // CharacterController.cs private float _jumpActionEndTime; public void OnJump(InputAction.CallbackContext context) { if (context.started) { if (_characterBody.State == CharacterState.Grounded) { Jump(); } else { _jumpActionEndTime = Time.unscaledTime + _jumpActionTime; } } else if (context.canceled) { StopJumping(); } } При натискане на бутона за скок, ако героят е на земята, той скача веднага. В противен случай ние съхраняваме времевия прозорец, през който скокът все още може да бъде извършен. Нека премахнем проверката state от самия метод . Grounded Jump // CharacterController.cs private void Jump() { _isJumping = true; _characterBody.Jump(_jumpSpeed); } Ще адаптираме и метода за спиране. Ако бутонът е бил освободен преди кацане, не трябва да има скок, така че нулираме . _jumpActionEndTime // CharacterController.cs private void StopJumping() { _jumpActionEndTime = 0; //... } Кога трябва да проверим дали героят е кацнал и да задействаме скок? Състоянието се обработва във , докато обработката на действие се извършва по-късно. Независимо дали е или , може да възникне забавяне от един кадър между кацане и скок, което е забележимо. CharacterBody FixedUpdate Update FixedUpdate Ще добавим събитие към , за да реагира незабавно на кацане. Първият аргумент ще бъде предишното състояние, а вторият ще бъде текущото състояние. StateChanged CharacterBody // CharacterBody.cs public event Action<CharacterState, CharacterState> StateChanged; Ще коригираме управлението на състоянието, за да задействаме събитието за промяна на състоянието и ще пренапишем . FixedUpdate // CharacterBody.cs [field: SerializeField] private CharacterState _state; public CharacterState State { get => _state; private set { if (_state != value) { var previousState = _state; _state = value; StateChanged?.Invoke(previousState, value); } } } Също така усъвършенствах как се обработва във . surfaceHit FixedUpdate // CharacterBody.cs private void FixedUpdate() { //... if (_velocity.y <= 0 && slideResults.surfaceHit) { var surfaceHit = slideResults.surfaceHit; Velocity = ClipVector(_velocity, surfaceHit.normal); if (surfaceHit.normal.y >= _minGroundVertical) { State = CharacterState.Grounded; return; } } State = CharacterState.Airborne; } В ще се абонираме за събитието и ще добавим манипулатор. CharacterController StateChanged // CharacterController.cs private void OnEnable() { _characterBody.StateChanged += OnStateChanged; } private void OnDisable() { _characterBody.StateChanged -= OnStateChanged; } private void OnStateChanged(CharacterState previousState, CharacterState state) { if (state == CharacterState.Grounded) { OnGrounded(); } } Ще премахнем проверката на state от и ще я преместим в . Grounded Update OnGrounded // CharacterController.cs private void Update() { _characterBody.SetLocomotionVelocity(_locomotionVelocity); } private void OnGrounded() { _isJumping = false; } Сега добавете кода, за да проверите дали трябва да се задейства скок. // CharacterController.cs private void OnGrounded() { _isJumping = false; if (_jumpActionEndTime > Time.unscaledTime) { _jumpActionEndTime = 0; Jump(); } } Ако е по-голямо от текущото време, това означава, че бутонът за прескачане е натиснат наскоро, така че нулираме и изпълняваме прескачането. _jumpActionEndTime _jumpActionEndTime Сега опитайте непрекъснато да скачате с героя. Ще забележите, че бутонът за скок се чувства по-отзивчив и контролирането на героя става по-плавно. Въпреки това забелязах, че в определени ситуации, като ъгъла, показан на илюстрацията по-долу, състояние изпитва леко забавяне, прекъсвайки веригата за прескачане. Grounded За да се справя с това, зададох полето в компонента на 0,05 вместо на 0,01. Тази стойност представлява минималното разстояние до повърхността, за да може тялото да влезе в състояние. Surface Anchor CharacterBody Grounded Скачане от скала Може би сте забелязали, че опитът да скочите, докато бягате от вертикални повърхности, не винаги работи. Може да се почувствате сякаш бутонът за прескачане понякога не реагира. Това е една от тънкостите на разработването на за 2D платформъри. Играчите се нуждаят от способността да скачат, дори когато са закъснели с натискането на бутона за скок. Въпреки че тази концепция може да изглежда странна, така функционират повечето платформинги. Резултатът е герой, който се изтласква от въздуха, както е показано в анимацията по-долу. Character Controller Нека внедрим тази механика. Ще въведем ново поле за съхраняване на времевия прозорец (в секунди), през който персонажът все още може да скача, след като загуби състоянието . Grounded // CharacterController.cs [Min(0)] [SerializeField] private float _rememberGroundTime = 0.1f; Също така ще добавим още едно поле за съхраняване на клеймото за време, след което състояние се „забравя“. Grounded // CharacterController.cs private float _lostGroundTime; Това състояние ще бъде проследено с помощта на събитието . Ще коригираме манипулатора за тази цел. CharacterBody OnStateChanged // CharacterController.cs private void OnStateChanged(CharacterState previousState, CharacterState state) { if (state == CharacterState.Grounded) { OnGrounded(); } else if (previousState == CharacterState.Grounded) { _lostGroundTime = Time.unscaledTime + _rememberGroundTime; } } Важно е да се разграничи дали персонажът е загубил състоянието поради умишлен скок или по друга причина. Вече имаме флага , който се дезактивира при всяко извикване на , за да предотврати излишни действия. Grounded _isJumping StopJumping Реших да не въвеждам друг флаг, тъй като отмяната на излишния скок не влияе на играта. Чувствайте се свободни да експериментирате. Флагът сега ще бъде изчистен само когато персонажът се приземи след скок. Нека актуализираме съответно кода. _isJumping // CharacterController.cs private void StopJumping() { _jumpActionEndTime = 0; var velocity = _characterBody.Velocity; if (_isJumping && velocity.y > 0) { _characterBody.Velocity = new Vector2( velocity.x, velocity.y / _stopJumpFactor); } } И накрая, ще преразгледаме метода . OnJump // CharacterController.cs public void OnJump(InputAction.CallbackContext context) { if (context.started) { if (_characterBody.State == CharacterState.Grounded || (!_isJumping && _lostGroundTime > Time.unscaledTime)) { Jump(); } else { _jumpActionEndTime = Time.unscaledTime + _jumpActionTime; } } else if (context.canceled) { StopJumping(); } } Сега скачането от вертикални повърхности вече не нарушава ритъма на играта и се чувства много по-естествено, въпреки очевидната си абсурдност. Героят може буквално да отблъсне въздуха, отивайки по-далеч, отколкото изглежда логично. Но това е точно това, което е необходимо за нашия платформинг. Обръщане на персонажа Последният щрих кара героя да се изправи срещу посоката на движение. Ще приложим това по най-простия начин — като променим мащаба на знака по оста x. Задаването на отрицателна стойност ще накара нашия капитан да се изправи в обратната посока. Първо, нека съхраним оригиналната скала, в случай че се различава от 1. // CharacterController.cs public class CharacterController : MonoBehaviour { //... private Vector3 _originalScale; private void Awake() { //... _originalScale = transform.localScale; } } Сега, когато се движим наляво или надясно, ще приложим положителна или отрицателна скала. // CharacterController.cs public class CharacterController : MonoBehaviour { public void OnMove(InputAction.CallbackContext context) { //... // Change character's direction. if (value.x != 0) { var scale = _originalScale; scale.x = value.x > 0 ? _originalScale.x : -_originalScale.x; transform.localScale = scale; } } } Нека тестваме резултата. Завършване Тази статия се оказа доста подробна, но успяхме да покрием всички съществени аспекти на контрола на героите в 2D платформинг. Като напомняне, можете да проверите крайния резултат в клона „ “ на хранилището. Контролер на символи Ако сте харесали или сте намерили тази и предишната статия за полезни, ще се радвам на харесвания и звезди в GitHub. Не се колебайте да се свържете с нас, ако срещнете проблеми или откриете грешки. Благодаря ви за вниманието!