Wojciech
Ratuszyński
Indie GameDev with Unity 3D
Pierwszo i trzecioosobowy kontroler gracza
Kontroler Rigidbody
Nawigacja
Nagłówek
Wstęp
Definiowanie klasy
Definiowanie zmiennych
Opis zmiennych
Definiowanie funkcji
Zakończenie
Linki warte uwagi:
Vector2
Vector3
Transform
Rigidbody
Physics

Zacznijmy od tego, że musimy zrobić pusty obiekt na scenie i nazwijmy go przykładowo "Player", dla zasady dajmy mu także tag "Player", gdyż może się to zawsze przydać w przyszłości, gdy będziemy chcieli wyszukać obiekt naszego gracza. Gdy obiekt jest gotowy, musimy zastanowić się co ma zawierać kontroler:
Moim założeniem będzie "wolna kamera" - nie będzie childem żadnego obiektu, więc będzie główną kamerą;
Drugim założeniem będzie widok z pierwszej i trzeciej osoby;
Trzecim założeniem będzie poruszanie się w każdym możliwym kierunku, skakanie, obracanie kamerą oraz sprint


Gdy mamy już mniej więcej ustalone co będzie zawierał skrypt - możemy się za niego zabrać.
using UnityEngine;
Jedyną biblioteką jaką będziemy potrzebować jest UnityEngine - zawiera one klasy takie jak Vector2/3 i pozwala na korzystanie z MonoBehavior - czyli chociażby możemy dodać skrypt do obiektu na scenie.
[RequireComponent(typeof(Rigidbody))] [RequireComponent(typeof(CapsuleCollider))] public class FirstThirdPersonController : MonoBehaviour { }
Do naszego kontrolera potrzebujemy także komponentu Rigidbody oraz CapsuleCollider, które są wymagane do poprawnego działania skryptu, dlatego je oznaczamy powyżej klasy. Po dodaniu skryptu do obiektu, komponenty zostaną dodane automatycznie.
//Zmienne konfiguracji [SerializeField] private ControllerType controllerType = ControllerType.FirstPerson; [SerializeField] private float lookSpeed = 2.0f, moveSpeed = 3.5f; [SerializeField] private float jumpPower = 5.0f; [SerializeField] private float capsuleHeight = 1.8f, capsuleRadius = 0.3f; [SerializeField] private float rigidbodyMass = 100.0f; [SerializeField] private float cameraHeight = 1.75f, cameraDistance = 2.5f, cameraMinLook = -90.0f, cameraMaxLook = 90.0f; //Zmienne obliczeniowe private Vector2 look = Vector2.zero; private Vector3 move = Vector3.zero; private bool isGrounded = true, isSprinting = false; //Komponenty private MeshRenderer renderer = null; private CapsuleCollider capsule = null; private Rigidbody rigidbody = null; private Transform camera = null; public enum ControllerType { FirstPerson, ThirdPerson }
Zmienne i komponenty - te wartości dziele sobie w kodzieli w kolejności takiej jaka jest powyżej. Część służy do konfiguracji, a część to wartości, na których będą obliczanie kierunki czy rotacja. Dlaczego [SerializeField]? Z prostego powodu. Zmienne, które tworze są prywatne, nie będą one widoczne dla innych klas z racji, że tylko ten skrypt będzie z nich korzystał. [SerializeField] powoduje, że zmienne prywatne są wyświetlane w inspektorze Unity, dzięki czemu mamy do nich łatwy dostęp.

controllerType - jest to wartość, w której wybieramy tryb kamery z listy enum(która znajduje się na samym dole kodu powyżej), czy ma być to pierwszoosobowa, bądź trzecioosobowa
lookSpeed - szybkość obracania kamery
moveSpeed - szybkość poruszania się postaci
jumpPower - moc skoku gracza
capsuleHeight - wysokość collidera
capsuleRadius - promieć collidera
rigidbodyMass - masa postaci
cameraHeight - wyokość kamery względem postaci
cameraDistance - odległość kamery od postaci w trybie trzecioosobowym
cameraMinLook - minimalna wartość rotacji kamery(jak nisko możemy patrzeć w dół)
cameraMaxLook - maksymalna wartość rotacji kamery(jak wysoko możemy patrzeć w górę)

look - zmienna zawierająca informację o kierunku, w którym patrzy postać
move - zmienna zawierająca informację o kierunku poruszania się
isGrounded - zmienna zawierająca informację o tym czy gracz ma pod sobą inny collider(obiekt)
isSprinting - zmienna zawierająca informację o tym czy gracz biegnie

renderer - zawiera komponent zarysu postaci
Dodane w celu wyłączania/włączania przy zmianie trybów FP/TP
capsule - zawiera komponent collidera postaci
Dodane w celu ustawienia wysokości, pozycji oraz promienia collideru
rigidbody - zawiera komponent fizyczny postaci
Dodane w celu kontrolowanie fizyki obiektu
camera - zawiera obiekt kamery
Dodane w celu kontrolowania kamery

Po dodaniu naszego skryptu ze zmiennymi do obiektu naszym oczom ukaże się to: Wszystkie wartości w inspektorze są gotowe do zmiany i konfiguracji.

Do naszego skryptu będzie potrzebnych kilka funkcji:
//Funkcja wywoływana po aktywacji tego skryptu, w celu zatwierdzenia konfiguracji komponentów. private void OnEnable() {}
//Funkcja wywoływana co klatkę. private void Update() {}
//Funkcja, która służy do pobierania danych wejściowych od gracza. private void UpdateInput() {}
//Funkcja zatwierdzająca obliczenia wykonywane na postaci. private void UpdateTransform() {}
//Funkcja zatwierdzająca obliczanie wykonywane na kamerze. private void UpdateCamera() {}
//Funkcja sprawdzająca czy pod postacią znajduje się podłoże. Jeśli pod postacią znajduje się collider funkcja zwraca wartość true, jeśli pod postacią nie ma collidera funkcja zwraca wartość false. private bool TransformSigth () {return false;}
//Funkcja sprawdzająca czy kamera przechodzi przez ścianę. Jeśli kamera natrafia na ścianę zwraca dystans pomiędzy ścianą a postacią, jeśli nie zwraca maksymalny dystans. private float CameraSigth(float newDistance) {return 0.0f;}
Gdy wszystkie funkcje znajdą się w naszej klasie możemy zacząć je uzupełniać i zaczniemy od OnEnable:
private void OnEnable() { //Jeśli w dziecku tego obiektu komponent MeshRenderer, wtedy go "przechwytuje" do zmiennej. if (GetComponentInChildren<MeshRenderer>()) renderer = GetComponentInChildren<MeshRenderer>(); capsule = GetComponent<CapsuleCollider>(); rigidbody = GetComponent<Rigidbody>(); camera = Camera.main.transform; capsule.height = capsuleHeight; capsule.center = new Vector3(0.0f, capsuleHeight * 0.5f, 0.0f); capsule.radius = capsuleRadius; rigidbody.mass = 100.0f; //Ważna regułka blokująca rotację fizyczną postaci w komponencie Rigidbody rigidbody.constraints = RigidbodyConstraints.FreezeRotation; } private void Update() { //Funkcja ta wywołuje co klatkę wszystkie funkcje, które się w niej znajdują - można w ten sposób rozdzielić poszczególne elementy kodu, dzięki czemu jest bardziej czytelniejszy i w jednym miejscu nie ma zbyt dużo kodu UpdateInput(); UpdateTransform(); UpdateCamera(); } private void UpdateInput() { //Blokujemy kursor i dla pewności wyłączamy widoczność, bo Unity lubi się bugować Cursor.lockState = CursorLockMode.Locked; Cursor.visible = false; //Do zmiennej look(zmienna ta zawiera kąt rzeczywisty w osi X oraz Y) dodajemy wartość pobraną z myszki i mnożymy ją przez zmienną lookSpeed look += new Vector2(Input.GetAxis("Mouse X"), -Input.GetAxis("Mouse Y")) * lookSpeed; //Przypisujemy zmiennej move wartość pobraną z klawiatury, po czym całą wartość normalizujemy, dzięki czemu postać będzie się poruszała tak samo szybko podczas straf'a, jak zwyczajnie do przody - polecam przetestować bez .normalized move = new Vector3(Input.GetAxis("Horizontal"), 0.0f, Input.GetAxis("Vertical")).normalized; //Sprawdza czy postać jest na podłożu, żeby nie można było skakać w powietrzu. Być może w tym poradniku zrobimy w przyszłości skok w powietrzy, żeby było dynamiczniej :) if (isGrounded) { isSprinting = Input.GetKey(KeyCode.LeftShift); if (Input.GetKeyDown(KeyCode.Space)) move.y = jumpPower; } else { isSprinting = false; } //Sprawdza czy postać sprintuje, jeśli tak, to zwiększamy jej prędkość dwukrotnie, jeśli nie, to ustawiamy na 1.0f if (isSprinting) move *= 2.0f; //Ograniczamy minimalny i maksymalny kąt, żeby kamera nie obracała się zbyt nisko/wysoko look.y = Mathf.Clamp(look.y, cameraMinLook, cameraMaxLook); //Obliczenie, które powoduje, że kąt nigdy nie będzie mniejszy od 0, ale też większy od 360. Najlepiej ograniczyć, żeby look.x nie osiągnęło maksymalnej wielkości jaką może mieć zmienna typu float. if (look.x < 0.0f) look.x = look.x + 360.0f; else if (look.x > 360.0f) look.x = look.x - 360.0f; //Wywołuje kolejną funkcję w funkcji UpdateInput, jest to celowo tu umieszczone, ponieważ SwitchMode jest funkcją, która wymaga naciśnięcia klawisza - nie wpłynie to na działanie, jeśli umieścimy to w funckji Update, ale robimy to dla estetyki kody. SwitchMode(); } private void UpdateTransform() { //Wywołuje funkcję TransformSigth, która zwraca wartość dotyczącą tego czy gracz stoi na podłożu. isGrounded = TransformSigth(); //Ustawiamy prędkość w komponencie Rigidbody, po pierwsze przydzielamy mu grawitacją, żeby spadał względem osi Y - Vector3.up zawiera wartości (0.0f, 1.0f, 0.0f), transform.forward zawiera wartość w jakim kierunku jest skierowany gracz - jest to zależne od rotacji, może mieć wartość (0.0f, 0.0f, 1.0f), a może mieć wartość (1.0f, 0.0f, 0.0f) - tak samo transform.right, tylko zawiera wartość w jakim kierunku jest "na prawo" od postaci. Możemy odwrócić wartość tych wektorów stawiając poprzedzający -, dzięki czemu osiągniemy kierunek "na lewo". Mnożemy te wartości przez move, czyli wartość pobrana z klawiatury. rigidbody.velocity = Vector3.up * (rigidbody.velocity.y + move.y) + transform.forward * move.z * moveSpeed + transform.right * move.x * moveSpeed; //Przypisujemy rotację postaci, eulerAngles to rzeczywisty kąt postaci mierzony w normalnej skali. transform.eulerAngles = new Vector3(0.0f, look.x, 0.0f); } private void UpdateCamera() { //Przypisuje wartość rotacji kamerze. camera.eulerAngles = new Vector3(look.y, look.x, 0.0f); //Ustawia kamerę w pozycji gracza i dodatkowo na wysokości zmiennej cameraHeight, która ma wartość 1.75f. camera.position = transform.position + new Vector3(0.0f, cameraHeight, 0.0f); //Funkcja switch, która działą podobnie jak instrukcja warunkowa i posiada kasety, w których jest kod do wykonania. switch (controllerType) { //Jeśli zmienna controllerType jest równca ControllerType.ThirdPerson, wtedy wykonywany jest kod poniżej, jeśli jest inne zostanie wykonany kod, którego nie spełnia żadna inna kaseta. Po wykonaniu instrukcji cała funkcja zostaje przerwana przez return, które zwraca wartość, choć nie musi. Możemy także dodać nową kasetę wpisując: //case ControllerType.FirstPerson: //camera.position += camera.forward * 0.1f; //return; case ControllerType.ThirdPerson: camera.position -= camera.forward * CameraSigth(cameraDistance); return; default: camera.position += camera.forward * 0.1f; return; } } private void SwitchMode() { //Instrukcja zostanie sprawdzona tylko raz, gdy naciśniemy klawisz H, ponieważ jest to GetKeyDown, a nie GetKey - różnica jest taka, ze GetKey sprawdza czy klawisz jest naciśnięty, a GetKeyDown sprawdza czy klawisz został naciśnięty. Jest także funkcja GetKeyUp, która sprawdza czy klawisz został podniesiony. if(Input.GetKeyDown(KeyCode.H)) { //Instrukcja, któa sprawdza czy zmienna controllerType jest różna od wartości ControllerType.FirstPerson - jeśli jest różna, wtedy przypisuje jej inną wartość. if (controllerType != ControllerType.FirstPerson) { controllerType = ControllerType.FirstPerson; //Jeśli do zmiennej renderer jest przpisany komponent, oraz zmienna komponentu renderer jest różna od wartości UnityEngine.Rendering.ShadowCastingMode.ShadowsOnly. Jeśli jest różna włącza się tryb renderowania wyłącznie cieni bez obiektu, dzięki czemu postać zostaje ukryta w pierwszej osobie. Poniżej w innym przypadku renderer zostaje ustawiony, że renderuje cienie i postać. if (renderer && renderer.shadowCastingMode != UnityEngine.Rendering.ShadowCastingMode.ShadowsOnly) renderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.ShadowsOnly; return; } else if (controllerType != ControllerType.ThirdPerson) { controllerType = ControllerType.ThirdPerson; if (renderer && renderer.shadowCastingMode != UnityEngine.Rendering.ShadowCastingMode.On) renderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.On; return; } } } private bool TransformSigth() { //Tworzymy zmienną lokalną, która posiada informacje o trafionym punkcie przez castowanie. RaycastHit hit; //Wywoływujemy gotową funkcję fizyczną Physics zawartą w UnityEngine. Możemy użyć różnych argumentów - zależnie od tego do czego chcemy jej użyć. Jeśli castowana kula trafi na obiekt, wtedy zwraca dystans od trafionego punkty, dzięki czemu kamera nie "wpadnie" w ścianę. if (Physics.SphereCast(transform.position + transform.up * capsuleRadius, capsuleRadius - 0.1f, Vector3.down, out hit, 0.5f)) return true; else return false; } private float CameraSigth(float newDistance) { RaycastHit hit; if (Physics.Raycast(camera.position, -camera.forward, out hit, newDistance)) return Vector3.Distance(camera.position, hit.point); else return newDistance; }
Ostatecznie skrypt wizualizuje się w ten sposób: Cały skrypt(bez komentarzy):
using UnityEngine; [RequireComponent(typeof(Rigidbody))] [RequireComponent(typeof(CapsuleCollider))] public class FirstThirdPersonController : MonoBehaviour { [SerializeField] private ControllerType controllerType = ControllerType.FirstPerson; [SerializeField] private float lookSpeed = 2.0f, moveSpeed = 3.5f; [SerializeField] private float jumpPower = 5.0f; [SerializeField] private float capsuleHeight = 1.8f, capsuleRadius = 0.3f; [SerializeField] private float rigidbodyMass = 100.0f; [SerializeField] private float cameraHeight = 1.75f, cameraDistance = 2.5f, cameraMinLook = -90.0f, cameraMaxLook = 90.0f; private Vector2 look = Vector2.zero; private Vector3 move = Vector3.zero; private bool isGrounded = true, isSprinting = false; private MeshRenderer renderer = null; private CapsuleCollider capsule = null; private Rigidbody rigidbody = null; private Transform camera = null; public enum ControllerType { FirstPerson, ThirdPerson } private void OnEnable() { if (GetComponentInChildren<MeshRenderer>()) renderer = GetComponentInChildren<MeshRenderer>(); capsule = GetComponent<CapsuleCollider>(); rigidbody = GetComponent<Rigidbody>(); camera = Camera.main.transform; capsule.height = capsuleHeight; capsule.center = new Vector3(0.0f, capsuleHeight * 0.5f, 0.0f); capsule.radius = capsuleRadius; rigidbody.mass = 100.0f; rigidbody.constraints = RigidbodyConstraints.FreezeRotation; } private void Update() { UpdateInput(); UpdateTransform(); UpdateCamera(); } private void UpdateInput() { Cursor.lockState = CursorLockMode.Locked; Cursor.visible = false; look += new Vector2(Input.GetAxis("Mouse X"), -Input.GetAxis("Mouse Y")) * lookSpeed; move = new Vector3(Input.GetAxis("Horizontal"), 0.0f, Input.GetAxis("Vertical")).normalized; if (isGrounded) { isSprinting = Input.GetKey(KeyCode.LeftShift); if (isSprinting) move *= 2.0f; if (Input.GetKeyDown(KeyCode.Space)) move.y = jumpPower; } look.y = Mathf.Clamp(look.y, cameraMinLook, cameraMaxLook); if (look.x < 0.0f) look.x = look.x + 360.0f; else if (look.x > 360.0f) look.x = look.x - 360.0f; SwitchMode(); } private void UpdateTransform() { isGrounded = TransformSigth(); rigidbody.velocity = Vector3.up * (rigidbody.velocity.y + move.y) + transform.forward * move.z * moveSpeed + transform.right * move.x * moveSpeed; transform.eulerAngles = new Vector3(0.0f, look.x, 0.0f); } private void UpdateCamera() { camera.eulerAngles = new Vector3(look.y, look.x, 0.0f); camera.position = transform.position + new Vector3(0.0f, cameraHeight, 0.0f); switch (controllerType) { case ControllerType.ThirdPerson: camera.position -= camera.forward * CameraSigth(cameraDistance); return; default: camera.position += camera.forward * 0.1f; return; } } private void SwitchMode() { if(Input.GetKeyDown(KeyCode.H)) { if (controllerType != ControllerType.FirstPerson) { controllerType = ControllerType.FirstPerson; if (renderer && renderer.shadowCastingMode != UnityEngine.Rendering.ShadowCastingMode.ShadowsOnly) renderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.ShadowsOnly; return; } else if (controllerType != ControllerType.ThirdPerson) { controllerType = ControllerType.ThirdPerson; if (renderer && renderer.shadowCastingMode != UnityEngine.Rendering.ShadowCastingMode.On) renderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.On; return; } } } private bool TransformSigth() { RaycastHit hit; if (Physics.SphereCast(transform.position + transform.up * capsuleRadius, capsuleRadius - 0.1f, Vector3.down, out hit, 0.5f)) return true; else return false; } private float CameraSigth(float newDistance) { RaycastHit hit; if (Physics.Raycast(camera.position, -camera.forward, out hit, newDistance)) return Vector3.Distance(camera.position, hit.point); else return newDistance; } }