W tym artykule kontynuujemy prace nad opracowaniem kontrolera postaci do gry platformowej 2D w środowisku Unity, szczegółowo analizując każdy etap konfiguracji i optymalizacji sterowania.
W poprzednim artykule „ Jak stworzyć kontroler postaci 2D w Unity: część 1 ” szczegółowo omówiliśmy, jak stworzyć fundament postaci, w tym jej zachowanie fizyczne i podstawowe ruchy. Teraz czas przejść do bardziej zaawansowanych aspektów, takich jak obsługa danych wejściowych i dynamiczne podążanie za kamerą.
W tym artykule zajmiemy się konfiguracją nowego systemu wprowadzania danych Unity, tworzeniem aktywnych akcji do sterowania postacią, włączaniem skakania i zapewnianiem właściwych reakcji na polecenia gracza.
Jeśli chcesz samodzielnie zaimplementować wszystkie zmiany opisane w tym artykule, możesz pobrać gałąź repozytorium „ Character Body ”, która zawiera fundament dla tego artykułu. Alternatywnie możesz pobrać gałąź „ Character Controller ” z końcowym wynikiem.
Zanim zaczniemy pisać kod do sterowania naszą postacią, musimy skonfigurować system wprowadzania danych w projekcie. Do naszej platformówki wybraliśmy nowy system wprowadzania danych Unity, wprowadzony kilka lat temu, który pozostaje istotny ze względu na swoje zalety w porównaniu z tradycyjnym systemem.
System wejściowy oferuje bardziej modułowe i elastyczne podejście do obsługi danych wejściowych, umożliwiając programistom łatwą konfigurację elementów sterujących dla różnych urządzeń i obsługę bardziej złożonych scenariuszy wprowadzania danych bez dodatkowych nakładów na implementację.
Najpierw zainstaluj pakiet Input System. Otwórz Menedżera pakietów z menu głównego, wybierając Okno → Menedżer pakietów. W sekcji Unity Registry znajdź pakiet „Input System” i kliknij „Instaluj”.
Następnie przejdź do ustawień projektu za pomocą menu Edycja → Ustawienia projektu. Wybierz kartę Odtwarzacz, znajdź sekcję Aktywne przetwarzanie danych wejściowych i ustaw ją na „Pakiet systemu wejściowego (nowy).
Po wykonaniu tych kroków Unity poprosi Cię o ponowne uruchomienie. Po ponownym uruchomieniu wszystko będzie gotowe do skonfigurowania elementów sterujących dla naszego kapitana.
W folderze Ustawienia utwórz Akcje wejściowe za pomocą menu głównego: Zasoby → Utwórz → Akcje wejściowe . Nazwij plik „Kontrolki”.
Unity's Input System to potężne i elastyczne narzędzie do zarządzania danymi wejściowymi, które pozwala deweloperom konfigurować elementy sterujące postaci i elementów gry. Obsługuje różne urządzenia wejściowe. Tworzone przez Ciebie akcje wejściowe zapewniają scentralizowane zarządzanie danymi wejściowymi, upraszczając konfigurację i czyniąc interfejs bardziej intuicyjnym.
Kliknij dwukrotnie plik Controls , aby otworzyć go do edycji, i dodaj Mapę akcji dla kontrolki postaci o nazwie „Postać”.
Mapa akcji w Unity to zbiór akcji, które można połączyć z różnymi kontrolerami i klawiszami, aby wykonywać określone zadania w grze. To wydajny sposób na organizowanie elementów sterujących, umożliwiający deweloperom przydzielanie i dostosowywanie danych wejściowych bez przepisywania kodu. Aby uzyskać więcej szczegółów, zapoznaj się z oficjalną dokumentacją Input System .
Pierwsza akcja będzie nazywać się „Ruch”. Ta akcja określi kierunek ruchu postaci. Ustaw typ akcji na „Wartość”, a typ kontroli na „Wektor2”, aby umożliwić ruch w czterech kierunkach.
Przypisz powiązania do tej akcji, wybierając opcję Dodaj kompozyt w górę/w dół/w prawo/w lewo i przypisując znane klawisze WASD do odpowiednich kierunków.
Nie zapomnij zapisać ustawień, klikając Save Asset . Ta konfiguracja zapewnia, że możesz ponownie przypisać powiązania dla akcji „Move”, na przykład do klawiszy strzałek lub nawet joysticka gamepada.
Następnie dodaj nową akcję — „Skok”. Pozostaw typ akcji jako „Przycisk”, ale dodaj nową interakcję — „Naciśnij” i ustaw zachowanie wyzwalacza na „Naciśnij i puść”, ponieważ musimy uchwycić zarówno naciśnięcie, jak i puszczenie przycisku.
To kończy schemat kontroli postaci. Następnym krokiem jest napisanie komponentu do obsługi tych akcji.
Czas połączyć akcje wejściowe, które utworzyliśmy na potrzeby sterowania postacią, z komponentem CharacterBody
, umożliwiając postaci aktywne poruszanie się po scenie zgodnie z naszymi poleceniami sterowania.
Aby to zrobić, utworzymy skrypt odpowiedzialny za sterowanie ruchem i nazwiemy go CharacterController
dla przejrzystości. W tym skrypcie najpierw zdefiniujemy kilka podstawowych pól. Dodamy odwołanie do komponentu CharacterBody
, _characterBody
, który będzie bezpośrednio kontrolowany przez skrypt.
Ustawimy również parametry prędkości ruchu postaci ( _speed
) i wysokości skoku ( _jumpHeight
). Dodatkowo zdefiniujemy cel pola _stopJumpFactor
.
Być może zauważyłeś, że w wielu platformówkach 2D wysokość skoku może być kontrolowana. Im dłużej przycisk skoku jest przytrzymywany, tym wyżej postać skacze. Zasadniczo początkowa prędkość w górę jest stosowana na początku skoku, a prędkość ta jest zmniejszana po zwolnieniu przycisku. _stopJumpFactor
określa, o ile prędkość w górę maleje po zwolnieniu przycisku skoku.
Oto przykład kodu, który napiszemy:
// 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; }
Następnie zaimplementujemy możliwość poruszania postacią w lewo i prawo. Podczas przytrzymywania przycisku ruchu postać powinna utrzymywać określoną prędkość ruchu niezależnie od przeszkód. Aby to osiągnąć, dodamy zmienną w skrypcie, aby przechowywać bieżącą prędkość ruchu wzdłuż powierzchni (lub po prostu poziomo, gdy postać jest w powietrzu):
// CharacterController.cs private float _locomotionVelocity;
W komponencie CharacterBody
wprowadzimy metodę ustawiającą tę prędkość:
// CharacterBody.cs public void SetLocomotionVelocity(float locomotionVelocity) { Velocity = new Vector2(locomotionVelocity, _velocity.y); }
Ponieważ nasza gra nie zawiera pochyłych powierzchni, ta metoda jest dość prosta. W bardziej złożonych scenariuszach musielibyśmy uwzględnić stan ciała i nachylenie powierzchni. Na razie po prostu zachowujemy pionową składową prędkości, modyfikując jedynie poziomą współrzędną x
.
Następnie ustawimy tę wartość w metodzie Update
w każdej klatce:
// CharacterController.cs private void Update() { _characterBody.SetLocomotionVelocity(_locomotionVelocity); }
Zdefiniujemy metodę obsługi sygnałów z akcji wejściowej Move
:
// CharacterController.cs public void OnMove(InputAction.CallbackContext context) { var value = context.ReadValue<Vector2>(); _locomotionVelocity = value.x * _speed; }
Ponieważ akcja Move
jest zdefiniowana jako Vector2
, kontekst zapewni wartość wektora w zależności od tego, które klawisze zostaną naciśnięte lub zwolnione. Na przykład naciśnięcie klawisza D
spowoduje, że metoda OnMove
otrzyma wektor (1, 0). Jednoczesne naciśnięcie D
i W
spowoduje (1, 1). Zwolnienie wszystkich klawiszy spowoduje uruchomienie OnMove
z wartością (0, 0).
Dla klawisza A
wektor będzie (-1, 0). W metodzie OnMove
bierzemy składową poziomą otrzymanego wektora i mnożymy ją przez określoną prędkość ruchu, _speed
.
Najpierw musimy nauczyć komponent CharacterBody
obsługi skoków. Aby to zrobić, dodamy metodę odpowiedzialną za skok:
// CharacterBody.cs public void Jump(float jumpSpeed) { Velocity = new Vector2(_velocity.x, jumpSpeed); State = CharacterState.Airborne; }
W naszym przypadku ta metoda jest prosta: ustawia prędkość pionową i natychmiast zmienia stan postaci na Airborne
.
Następnie musimy określić prędkość, z jaką postać powinna skoczyć. Określiliśmy już wysokość skoku i wiemy, że grawitacja stale działa na ciało. Na tej podstawie można obliczyć początkową prędkość skoku za pomocą wzoru:
Gdzie h to wysokość skoku, a g to przyspieszenie grawitacyjne. Uwzględnimy również mnożnik grawitacyjny obecny w komponencie CharacterBody
. Dodamy nowe pole, aby zdefiniować początkową prędkość skoku i obliczymy ją w następujący sposób:
// CharacterController.cs private float _jumpSpeed; private void Awake() { _jumpSpeed = Mathf.Sqrt(2 * Physics2D.gravity.magnitude * _characterBody.GravityFactor * _jumpHeight); }
Będziemy potrzebować kolejnego pola, aby śledzić, czy postać aktualnie skacze, dzięki czemu będziemy mogli ograniczyć prędkość skoku w odpowiednim momencie.
Dodatkowo, jeśli gracz przytrzyma przycisk skoku do lądowania, powinniśmy sami zresetować tę flagę. Zostanie to zrobione w metodzie Update
:
// CharacterController.cs private bool _isJumping; private void Update() { if (_characterBody.State == CharacterState.Grounded) { _isJumping = false; } //... }
Teraz napiszmy metodę obsługującą akcję Jump
:
// CharacterController.cs public void OnJump(InputAction.CallbackContext context) { if (context.started) { Jump(); } else if (context.canceled) { StopJumping(); } }
Ponieważ akcja Jump
jest przyciskiem, możemy określić na podstawie kontekstu, czy naciśnięcie przycisku zostało rozpoczęte ( context.started
) czy zakończone ( context.canceled
). Na tej podstawie inicjujemy lub zatrzymujemy skok.
Oto metoda wykonania skoku:
// CharacterController.cs private void Jump() { if (_characterBody.State == CharacterState.Grounded) { _isJumping = true; _characterBody.Jump(_jumpSpeed); } }
Przed skokiem sprawdzamy, czy postać jest na ziemi. Jeśli tak, ustawiamy flagę _isJumping
i sprawiamy, że ciało skacze za pomocą _jumpSpeed
.
Teraz zaimplementujmy zachowanie polegające na zatrzymaniu skoku:
// 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); } }
Zatrzymujemy skok tylko wtedy, gdy flaga _isJumping
jest aktywna. Innym ważnym warunkiem jest to, że postać musi poruszać się w górę. Zapobiega to ograniczeniu prędkości spadania, jeśli przycisk skoku zostanie zwolniony podczas poruszania się w dół. Jeśli wszystkie warunki są spełnione, resetujemy flagę _isJumping
i zmniejszamy prędkość pionową o współczynnik _stopJumpFactor
.
Teraz, gdy wszystkie komponenty są gotowe, dodaj komponenty PlayerInput
i CharacterController
do obiektu Captain w scenie. Upewnij się, że wybierasz komponent CharacterController
, który utworzyliśmy, a nie standardowy komponent Unity przeznaczony do sterowania postaciami 3D.
W przypadku CharacterController
przypisz istniejący komponent CharacterBody
z postaci. W przypadku PlayerInput
ustaw wcześniej utworzone Controls w polu Actions
.
Następnie skonfiguruj komponent PlayerInput, aby wywołać odpowiednie metody z CharacterController. Rozwiń sekcje Events i Character w edytorze i połącz odpowiednie metody z akcjami Move i Jump.
Teraz wszystko jest gotowe, aby uruchomić grę i sprawdzić, jak wszystkie skonfigurowane komponenty ze sobą współpracują.
Teraz musimy sprawić, by kamera podążała za postacią, gdziekolwiek się uda. Unity zapewnia potężne narzędzie do zarządzania kamerą — Cinemachine .
Cinemachine to rewolucyjne rozwiązanie do sterowania kamerą w Unity, które oferuje deweloperom szeroki zakres możliwości tworzenia dynamicznych, dobrze dostrojonych systemów kamer, które dostosowują się do potrzeb rozgrywki. To narzędzie ułatwia implementację złożonych technik kamerowych, takich jak śledzenie postaci, automatyczna regulacja ostrości i wiele innych, dodając witalności i bogactwa każdej scenie.
Najpierw zlokalizuj obiekt Main Camera w scenie i dodaj do niego komponent CinemachineBrain
.
Następnie utwórz nowy obiekt w scenie o nazwie CaptainCamera . Będzie to kamera podążająca za kapitanem, tak jak profesjonalny kamerzysta. Dodaj do niej komponent CinemachineVirtualCamera
. Ustaw pole Follow na kapitana, wybierz Framing Transposer dla pola Body i ustaw parametr Lens Ortho Size na 4.
Dodatkowo będziemy potrzebować innego komponentu, aby zdefiniować przesunięcie kamery względem postaci — CinemachineCameraOffset
. Ustaw wartość Y na 1,5, a wartość Z na -15.
Sprawdźmy teraz jak kamera podąża za naszą postacią.
Myślę, że wyszło całkiem nieźle. Zauważyłem, że kamera czasami lekko się zacina. Aby to naprawić, ustawiłem pole Blend Update Method obiektu Main Camera na FixedUpdate.
Przetestujmy zaktualizowaną mechanikę. Spróbuj biegać i skakać bez przerwy. Doświadczeni gracze mogą zauważyć, że skoki nie zawsze są rejestrowane. W większości gier nie jest to problemem.
Okazuje się, że trudno przewidzieć dokładny czas lądowania, aby ponownie nacisnąć przycisk skoku. Musimy sprawić, aby gra była bardziej wyrozumiała, pozwalając graczom na lekkie naciśnięcie przycisku skoku przed lądowaniem i sprawić, aby postać skoczyła natychmiast po lądowaniu. To zachowanie jest zgodne z tym, do czego gracze są przyzwyczajeni.
Aby to wdrożyć, wprowadzimy nową zmienną _jumpActionTime
, która reprezentuje okno czasowe, w którym skok może zostać uruchomiony, jeśli nadarzy się ku temu okazja.
// CharacterController.cs [Min(0)] [SerializeField] private float _jumpActionTime = 0.1f;
Dodałem pole _jumpActionEndTime
, które oznacza koniec okna akcji skoku. Innymi słowy, dopóki _jumpActionEndTime
nie zostanie osiągnięty, postać będzie skakać, jeśli nadarzy się okazja. Zaktualizujmy również obsługę akcji 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(); } }
Gdy przycisk skoku zostanie naciśnięty, jeśli postać jest na ziemi, skacze natychmiast. W przeciwnym razie przechowujemy okno czasowe, w którym skok może być nadal wykonany.
Usuńmy sprawdzanie stanu Grounded
z samej metody Jump
.
// CharacterController.cs private void Jump() { _isJumping = true; _characterBody.Jump(_jumpSpeed); }
Zaadaptujemy również metodę stop-jumping. Jeśli przycisk został zwolniony przed lądowaniem, nie powinno dojść do żadnego skoku, więc resetujemy _jumpActionEndTime
.
// CharacterController.cs private void StopJumping() { _jumpActionEndTime = 0; //... }
Kiedy powinniśmy sprawdzić, czy postać wylądowała i wyzwolić skok? Stan CharacterBody
jest przetwarzany w FixedUpdate
, podczas gdy przetwarzanie akcji następuje później. Niezależnie od tego, czy jest to Update
czy FixedUpdate
, może wystąpić opóźnienie jednej klatki między lądowaniem a skokiem, co jest zauważalne.
Dodamy zdarzenie StateChanged
do CharacterBody
aby natychmiast zareagować na lądowanie. Pierwszy argument będzie poprzednim stanem, a drugi bieżącym stanem.
// CharacterBody.cs public event Action<CharacterState, CharacterState> StateChanged;
Dostosujemy zarządzanie stanem, aby wywołać zdarzenie zmiany stanu i przepiszemy 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); } } }
Udoskonaliłem również sposób obsługi surfaceHit
w 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; }
W CharacterController
zasubskrybujemy zdarzenie StateChanged
i dodamy procedurę jego obsługi.
// 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(); } }
Usuniemy sprawdzanie stanu Grounded
z Update
i przeniesiemy je do OnGrounded
.
// CharacterController.cs private void Update() { _characterBody.SetLocomotionVelocity(_locomotionVelocity); } private void OnGrounded() { _isJumping = false; }
Teraz dodaj kod sprawdzający, czy skok powinien zostać wywołany.
// CharacterController.cs private void OnGrounded() { _isJumping = false; if (_jumpActionEndTime > Time.unscaledTime) { _jumpActionEndTime = 0; Jump(); } }
Jeśli _jumpActionEndTime
jest większe od bieżącego czasu, oznacza to, że przycisk skoku został niedawno naciśnięty, więc resetujemy _jumpActionEndTime
i wykonujemy skok.
Teraz spróbuj ciągle skakać z postacią. Zauważysz, że przycisk skoku jest bardziej responsywny, a sterowanie postacią staje się płynniejsze. Jednak zauważyłem, że w pewnych sytuacjach, takich jak narożnik pokazany na poniższej ilustracji, stan Grounded
doświadcza niewielkiego opóźnienia, przerywając łańcuch skoków.
Aby temu zaradzić, ustawiłem pole Surface Anchor
w komponencie CharacterBody
na 0,05 zamiast 0,01. Wartość ta reprezentuje minimalną odległość od powierzchni, po której ciało przechodzi w stan Grounded
.
Być może zauważyłeś, że próba skoku podczas biegu z pionowych powierzchni nie zawsze działa. Czasami może się wydawać, że przycisk skoku nie reaguje.
To jedna z subtelności w tworzeniu kontrolera postaci do platformówek 2D. Gracze muszą mieć możliwość skakania, nawet jeśli są nieco spóźnieni z naciśnięciem przycisku skoku. Choć ta koncepcja może wydawać się dziwna, tak właśnie działa większość platformówek. Rezultatem jest postać, która wydaje się odpychać powietrze, jak pokazano na poniższej animacji.
Zaimplementujmy tę mechanikę. Wprowadzimy nowe pole do przechowywania okna czasowego (w sekundach), w którym postać może nadal skakać po utracie stanu Grounded
.
// CharacterController.cs [Min(0)] [SerializeField] private float _rememberGroundTime = 0.1f;
Dodamy również kolejne pole, w którym będzie przechowywany znacznik czasu, po upływie którego stan Grounded
zostanie „zapomniany”.
// CharacterController.cs private float _lostGroundTime;
Ten stan będzie śledzony za pomocą zdarzenia CharacterBody
. W tym celu dostosujemy handler OnStateChanged
.
// CharacterController.cs private void OnStateChanged(CharacterState previousState, CharacterState state) { if (state == CharacterState.Grounded) { OnGrounded(); } else if (previousState == CharacterState.Grounded) { _lostGroundTime = Time.unscaledTime + _rememberGroundTime; } }
Ważne jest, aby rozróżnić, czy postać straciła stan Grounded
z powodu celowego skoku, czy z innego powodu. Mamy już flagę _isJumping
, która jest wyłączana za każdym razem, gdy wywoływana jest StopJumping
aby zapobiec zbędnym akcjom.
Postanowiłem nie wprowadzać kolejnej flagi, ponieważ redundantne anulowanie skoku nie wpływa na rozgrywkę. Eksperymentuj swobodnie. Flaga _isJumping
będzie teraz czyszczona tylko wtedy, gdy postać wyląduje po skoku. Zaktualizujmy kod odpowiednio.
// 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); } }
Na koniec powtórzymy metodę 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(); } }
Teraz skakanie z pionowych powierzchni nie zakłóca już rytmu rozgrywki i wydaje się o wiele bardziej naturalne, pomimo pozornej absurdalności. Postać może dosłownie odepchnąć się od powietrza, pokonując dystans większy niż wydaje się to logiczne. Ale to jest dokładnie to, czego potrzeba do naszej platformówki.
Ostatnim szlifem jest ustawienie postaci twarzą w kierunku ruchu. Wdrożymy to w najprostszy sposób — zmieniając skalę postaci wzdłuż osi x. Ustawienie wartości ujemnej sprawi, że nasz kapitan będzie zwrócony twarzą w przeciwnym kierunku.
Najpierw zapiszmy oryginalną skalę na wypadek, gdyby różniła się od 1.
// CharacterController.cs public class CharacterController : MonoBehaviour { //... private Vector3 _originalScale; private void Awake() { //... _originalScale = transform.localScale; } }
Teraz, przy poruszaniu się w lewo lub w prawo, zastosujemy skalę dodatnią lub ujemną.
// 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; } } }
Sprawdźmy wynik.
Artykuł okazał się dość szczegółowy, ale udało nam się omówić wszystkie istotne aspekty sterowania postacią w platformówce 2D. Jako przypomnienie, możesz sprawdzić końcowy wynik w gałęzi „ Character Controller ” repozytorium.
Jeśli podobał Ci się lub uznałeś ten i poprzedni artykuł za pomocne, docenię polubienia i gwiazdki na GitHub. Nie wahaj się skontaktować, jeśli napotkasz jakiekolwiek problemy lub znajdziesz błędy. Dziękuję za uwagę!