Bohater
Część sekcji Architektura gier w Unity ścieżki Unity w Coddy. Lekcja 41 z 50.
W tym rozdziale zbudujesz Arena Survivor, ostatnią grę na ścieżce. Fale wrogów wdzierają się na otoczoną murem arenę, a ty odpierasz je mieczem: są tu szeregowi przeciwnicy, którzy idą w twoją stronę, oraz kule, które czekają, a potem wykonują szarżę. Po pokonaniu fali zostaje ulepszenie, HUD pokazuje twoje zdrowie i numer fali, a gdy skończy ci się zdrowie, rozgrywka dobiega końca i zapisywany jest twój najlepszy wynik. Gra wykorzystuje narzędzia z tej sekcji tam, gdzie pasują: abstrakcyjny Enemy z dwoma rodzajami przeciwników, IDamageable dla wszystkiego, co może otrzymywać obrażenia, fale jako zasób ScriptableObject, przedmioty do podniesienia obsługiwane przez interfejs, HUD nasłuchujący wyłącznie zdarzeń C# oraz zapis w formacie JSON. Wszystkie dwanaście skryptów znajduje się w projekcie od samego początku. Kod ma na razie tylko Hero, a kolejne lekcje dodają go do następnych skryptów.
Miecz bohatera trafia w to, co znajduje się przed nim, więc podczas zamachu trzeba sprawdzić, czy cel jest „z przodu”. Vector3.Dot(a, b) mnoży odpowiadające sobie składowe dwóch wektorów i dodaje wyniki: a.x * b.x + a.y * b.y + a.z * b.z. Dla dwóch kierunków o długości 1 jest to cosinus kąta między nimi: 1, gdy wskazują ten sam kierunek, 0.866 przy 30 stopniach, 0.707 przy 45 stopniach, 0 dla kierunków prostopadłych i -1, gdy wskazują przeciwne kierunki. Zatem „w granicach 30 stopni od kierunku, w którym patrzę” oznacza „iloczyn skalarny wynosi co najmniej 0.866” — bez obliczania kąta. Ten robot wartownik podnosi alarm, gdy gracz znajduje się w odległości do 10 jednostek i w jego polu widzenia o kącie 30 stopni:
public Transform player;
public float range = 10f;
void Update()
{
Vector3 toPlayer = player.position - transform.position;
toPlayer.y = 0f;
bool near = toPlayer.magnitude <= range;
bool inView = Vector3.Dot(transform.forward, toPlayer.normalized) >= 0.866f;
if (near && inView)
{
Debug.Log("Alarm!");
}
}Oba kierunki muszą mieć długość 1. transform.forward zawsze ma taką długość, ale długość toPlayer odpowiada odległości, więc najpierw jest normalizowany; w przeciwnym razie gracz znajdujący się dwa razy dalej dałby dwa razy większy iloczyn skalarny. Wartość y jest wcześniej ustawiana na 0, więc pozycja gracza na wyższym pomoście jest uwzględniana na podstawie jego położenia na mapie, a nie wysokości. Znak pozwala odróżnić przód od tyłu: iloczyn skalarny większy od 0 oznacza dowolne miejsce w przedniej połowie, a mniejszy od 0 — dowolne miejsce z tyłu.
Wyzwanie
ŚredniRozpocznij Arena Survivor z mieczem bohatera. Ruch jest już zaimplementowany: akcja Move porusza bohaterem za pomocą SimpleMove i obraca go w kierunku, w którym się porusza. Wokół niego stoi pięć manekinów treningowych oznaczonych tagiem Enemy. W Inspectorze ustawiono dla Hero wartość reach (2) i swingCooldown (0.5), a także cięcie wyświetlane podczas zamachu.
- Spacja lub kliknięcie lewym przyciskiem myszy wykonuje zamach, najwyżej raz na
swingCooldownsekund: naciśnięcie przed upływem czasu odnowienia nic nie robi. Odczytaj klawisze za pomocąKeyboard.currentiMouse.current, a następnie wywołajSwing(). - W
Swing(), po wyświetleniu cięcia, zniszcz każdy obiekt oznaczony tagiemEnemy, który znajduje się w zasięgureachi przed bohaterem. W zasięgu znajduje się każdy obiekt z colliderem, któryPhysics.OverlapSpherewykryje wokół bohatera w promieniureach. Przed bohaterem oznacza, że iloczyn skalarny kierunku do obiektu wzdłuż podłoża i wektora skierowanego do przodu bohatera wynosi co najmniej 0.5: do 60 stopni w obie strony.
Pozostaw pozostałe skrypty bez zmian. Naciśnij Play, podejdź do manekinów i wykonaj zamach, a następnie naciśnij Check.
Spróbuj swoich sił
using System.Collections;
using UnityEngine;
using UnityEngine.InputSystem;
public class Hero : MonoBehaviour
{
[SerializeField] private float speed = 5f;
[SerializeField] private float reach = 2f;
[SerializeField] private float swingCooldown = 0.5f;
[SerializeField] private GameObject slash;
private CharacterController controller;
private InputAction moveAction;
void Start()
{
controller = GetComponent<CharacterController>();
moveAction = InputSystem.actions.FindAction("Move");
}
void Update()
{
Vector2 input = moveAction.ReadValue<Vector2>();
Vector3 move = new Vector3(input.x, 0f, input.y);
controller.SimpleMove(move * speed);
if (move != Vector3.zero)
{
transform.rotation = Quaternion.LookRotation(move);
}
// Naciśnięcie spacji lub lewego przycisku myszy wykonuje zamach, nie częściej niż raz na swingCooldown sekund
}
void Swing()
{
StartCoroutine(ShowSlash());
// zniszcz wszystko oznaczone tagiem Enemy, co znajduje się w zasięgu i przed bohaterem
}
IEnumerator ShowSlash()
{
slash.SetActive(true);
yield return new WaitForSeconds(0.1f);
slash.SetActive(false);
}
}
Ta lekcja zawiera krótki quiz. Zacznij lekcję, żeby na niego odpowiedzieć i śledzić swoje postępy.
Wszystkie lekcje w sekcji Architektura gier w Unity
1Komponenty, nie hierarchie
Małe komponentyPodstawowy skrypt przeciwnikaKomponenty abstrakcyjneWspółpracujące komponentyPodsumowanie: modułowa wieżyczka4Dane serializowane
Klasy serializowalneListy w InspectorzeWyliczenia w InspectorzePodsumowanie: tabela łupów10Projekt: Arena przetrwania
BohaterTypy wrogów2Interfejsy w Unity
IDamageableInterfejsy i TryGetComponentGetComponentsPodsumowanie: klawisz interakcji5ScriptableObjects
Zasoby danychStatystyki broniWspółdzielone ustawieniaFale jako danePowtórka: baza danych przedmiotów8Kod wielokrotnego użytku
Metody rozszerzająceGeneryczny singletonGeneryczne metody pomocniczePodsumowanie: FindNearest