Perspektywa pierwszoosobowa- dodawanie funkcji zdarzeń w animacji.
Ten temat bazuje na kodzie z poprzedniego tematu (zobacz) i dotyczy wywołania funkcji z kodu programu w momencie wykonywania wybranej klatki animacji. Przykład: w grze jest animowana postać rzucająca oszczepem. Powstaje problem, w którym momencie animacji wyrzucić oszczep? Ułatwieniem pozwalającym rozwiązać postawione zadanie, jest wykorzystanie zdarzeń wywoływanych w określonej animacji dla wybranej klatki ruchu animowanego obiektu.
Kliknij na obrazku aby zobaczyć krótki film
Dodajemy animację rzutu
W Blenderze do naszej postaci tworzymy nową animację rzutu oszczepem lub innym dowolnym przedmiotem.
Eksportujemy zmodyfikowaną postać jako plik *.fbx i importujemy w Unity. Ja swoją animację nazwałem Rzut. W oknie importu animacji ustawiamy właściwość pętli na pustą (Loop Time)
Przechodzimy do Animatora i w grafie dodajemy nowy stan animacji. Przypisujemy mu nazwę Rzut i robimy połączenia z innymi stanami. Patrz poniższa ilustracja.
Stan animacji rzutu wywołamy spełnieniem takich warunków: postać musi mieć w reku włócznię oraz poprzednia animacja rzutu musi być zakończona. Oznacza to, że w kodzie programu dla wywołania rzutu muszą być spełnione jednocześnie oba logiczne warunki.
Dodajemy nowy parametr wyzwalacza (Trigger), przypisujemy mu nazwę trRzut.
Wyzwalacze mają tę właściwość, że zaraz po wykonaniu automatycznie przyjmują stan false. Pisząc kod nie musimy tego stanu zmieniać na false.
W grafie przejść animatora ustawiamy jak poniżej
Zmiany w skrypcie AnimacjaDzikiego
Przechodzimy do skryptu AnimacjaDzikiego i dodajemy nowy element typu wyliczeniowego o nazwie RZUT. Patrz poniżej
Wskazówka:
public class AnimacjaDzikiego : MonoBehaviour
{
//typ wyliczeniowy dla animacji
public enum STAN_ANIMACJI {
CZEKAJ,//stan 0
SPACER,//stan 1
BIEG,//stan 2
CZOLGANIE,//stan 3
SKOK,//stan 4
RZUT//stan 5
}
public float wspolczynnikZmianyPredkosci = 1;
Animator animator;
W skrypcie dopisujemy trzy nowe funkcje. Pierwsze dwie będą wywoływane w odpowiednich klatkach animacji eventsObrocDzide() i eventsRzucaj(). Trzecia UstawGotowoscRzutu() będzie wywołana w skrypcie KolizjeZasoby(). Po kolei wykonujemy zmiany w skryptach.
Pełny kod skryptu AnimacjaDzikiego() po zmianach.
Wskazówka:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class AnimacjaDzikiego : MonoBehaviour
{
//typ wyliczeniowy dla animacji
public enum STAN_ANIMACJI {
CZEKAJ,//stan 0
SPACER,//stan 1
BIEG,//stan 2
CZOLGANIE,//stan 3
SKOK,//stan 4
RZUT//stan 5
}
public float wspolczynnikZmianyPredkosci = 1;
Animator animator;
STAN_ANIMACJI stanAnimacji = STAN_ANIMACJI.CZEKAJ;
public void eventsObrocDzide()
{
//obróć dzidę grotem do rzutu
GameObject d=GetComponent<KolizjeZasoby>().BronObracana;
//w zalezności jak jest zorientowna siatka, kąt rotacji może być inny
d.transform.Rotate(new Vector3(0,180,0));
}
public void eventsRzucaj()
{
GetComponent<KolizjeZasoby>().RzucajDzida();
}
public void UstawGotowoscRzutu()
{
//jesli ma coś w ręce to może tym rzucać
animator.SetTrigger("trRzut");
}
public STAN_ANIMACJI StanAnimacji
{
get { return stanAnimacji; }
set {
stanAnimacji=value;
//ustwa stan animacji z indeksu aktywnego typu wyliczeniowego
animator.SetInteger("intStanAnimacji", (int)stanAnimacji);
}
}
// Start is called before the first frame update
void Start()
{
animator = GetComponent<Animator>();
}
public void PrzelaczAnimacje(float _constV)
{
if (Input.GetAxis("Spacer") != 0)
{
StanAnimacji = STAN_ANIMACJI.SPACER;
//ustaw predkośc na stała
wspolczynnikZmianyPredkosci = _constV;
}
if (Input.GetAxis("Bieg") != 0)
{
StanAnimacji = STAN_ANIMACJI.BIEG;
//zwiększ predkośc podczas biegu 2.5 razy
wspolczynnikZmianyPredkosci = 2.5f* _constV;
}
if (Input.GetAxis("Czolgaj") != 0)
{
StanAnimacji = STAN_ANIMACJI.CZOLGANIE;
//zmniejsz prędkośc podczas czołgania 2 razy
wspolczynnikZmianyPredkosci = 0.5f* _constV;
}
}
// Update is called once per frame
void Update()
{
}
}
Przechodzimy do importowanego zasobu fbx postaci z animacjami i podpinamy zdarzenia w wybranych klatkach. Wybieramy pierwsza klatkę dla funkcji eventsObrocDzide(). Patrz poniżej
Klatkę odpowiedzialną za rzut wybieram taką, w której kość dłoni ma zwrot wektora odpowiadający kątowi rzutu. W kodzie programu odczytamy ten wektor ułożenia kości, co znacznie skróci implementację jak i zrezygnujemy z niepotrzebnych dodatkowych obliczeń.
Powtarzamy poprzednie czynności dla osadzenia kolejnego zdarzenia w klatce animacji. Patrz poniżej
Na zakończenie zatwierdź zmiany klawiszem Apply.
Modyfikujemy skrypt RuchBohatera()
Przechodzimy do skryptu RuchBohatera() i dopisujemy funkcję Rzucaj(). Rzut będzie wykonywany po wciśnięciu klawisza Lewy CTRL. Funkcja Rzut() jest wywoływana w metodzie Update(). Pełny kod skryptu po zmianie.
Wskazówka:
using System.Collections;
using System.Collections.Generic;
using Unity.Android.Gradle.Manifest;
using UnityEngine;
using static AnimacjaDzikiego;
public class RuchBohatera : MonoBehaviour
{
public CharacterController kontroler;
public Transform kamera;
private bool fGrunt;
//przyspieszenie grawitacyjne
public float g = -9.8f;
//współczynnik prędkości
const float constV = 2.5f;
public float v= constV,
wysSkoku=2.0f;
Vector3 wektorPredkosci = Vector3.zero;
public float wygladzenie = 0.1f;
float buforBiezacegoWygladzania;
public void Rzucaj()
{
//lewy CTRL
if (Input.GetAxis("Fire1") != 0)
{
GetComponent<AnimacjaDzikiego>().StanAnimacji = STAN_ANIMACJI.RZUT;
}
}
public void Skok()
{
if (fGrunt && Input.GetAxis("Jump") != 0)
{
//v=Pierwiastek(2gh)
wektorPredkosci.y = Mathf.Sqrt(-2f * g * wysSkoku);
GetComponent<AnimacjaDzikiego>().StanAnimacji
= STAN_ANIMACJI.SKOK;
}
}
void Spadaj()
{
wektorPredkosci.y += g * Time.deltaTime;
if (fGrunt && wektorPredkosci.y < 0)
{
//nie spadaj szybciej niż 54m/s
wektorPredkosci.y = -54f;
}
kontroler.Move(wektorPredkosci * Time.deltaTime);
}
void Ruch()
{
float poziom = Input.GetAxis("Horizontal");
float pion = Input.GetAxis("Vertical");
Vector3 kierunek = new Vector3(poziom, 0, pion).normalized;
//czy ruch, powiedzmy wiecej niż 5 centymetrów
if (kierunek.magnitude >= 0.05f)
{
//obrót postaci
//zwróc kąt w radianach z płaszczyzny 2D- oś X a Z
//zamień radiany na stopnie
float katPatrzenia = Mathf.Atan2(kierunek.x, kierunek.z)
*Mathf.Rad2Deg+
kamera.eulerAngles.y;
//wygladzaj obrot,
//predkosc wygladzania wyslij do bufora
float kat = Mathf.SmoothDampAngle(
transform.eulerAngles.y,
katPatrzenia,
ref wygladzenie,
buforBiezacegoWygladzania
);
transform.rotation = Quaternion.Euler(0, kat, 0);
Vector3 kierunekRuchu = Quaternion.Euler(0, katPatrzenia, 0)
*Vector3.forward;
kontroler.Move(kierunekRuchu.normalized * v * Time.deltaTime);
//jest ruch to domyslnie ustawiaj na spacer
if (GetComponent<AnimacjaDzikiego>().StanAnimacji == STAN_ANIMACJI.CZEKAJ)
{
GetComponent<AnimacjaDzikiego>().StanAnimacji = STAN_ANIMACJI.SPACER;
GetComponent<AnimacjaDzikiego>().wspolczynnikZmianyPredkosci = constV;
}
GetComponent<AnimacjaDzikiego>().PrzelaczAnimacje(constV);
v = GetComponent<AnimacjaDzikiego>().wspolczynnikZmianyPredkosci;
}
else
{
//pozostałe animacje przełacz
//w skrypcie AnimacjaDzikiego
GetComponent<AnimacjaDzikiego>().StanAnimacji = STAN_ANIMACJI.CZEKAJ;
}
}
// Start is called before the first frame update
void Start()
{
//blokuj kursor na srodku ekranu
Cursor.lockState = CursorLockMode.Locked;
//ukryj kursor
//standardowo klawisz Esc przywróci widok kursora
Cursor.visible = false;
}
// Update is called once per frame
void Update()
{
//sprawdzaj czy jestes nad dowolnym podlozem
fGrunt = kontroler.isGrounded;
Ruch();
Skok();
Rzucaj();
Spadaj();
}
}
Modyfikacja skryptu KolizjeZasoby()
Przechodzimy do skryptu KolizjeZasoby(). Dodajemy zmienną na obracaną w ręce broń (GameObject bronObracana=null;) Tworzymy publiczną metodę ustawiania i odczytywani dla zmiennej bronObracana. Piszemy funkcję RzucajDzida() i wykonujemy zmiany w funkcji KolizjaDzida().
Pełny kod skryptu po zmianach podaję poniżej
Wskazówka:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static AnimacjaDzikiego;
public class KolizjeZasoby : MonoBehaviour
{
[SerializeField] float silaWartosc = 0;
[SerializeField] GameObject slotBroniPrawy;
//tablica prefabrykatów broni
[SerializeField] GameObject[] prafbBroni;
GameObject bronObracana=null;
public GameObject BronObracana
{
get { return bronObracana; }
set { bronObracana = value; }
}
public void RzucajDzida()
{
//zrob rzucana dzide z prefabrykatu
GameObject d = Instantiate(prafbBroni[0],
BronObracana.transform.position,
BronObracana.transform.rotation);
//usun obrocona dzide z reki
Destroy(BronObracana);
BronObracana = null;
//ustaw predkośc oszczepu na 12 m/s
d.GetComponent<Rigidbody>().velocity = d.transform.forward * 12.0f;
//dla rzucanej dzidy
//ustaw warstwe na niekolidującą z graczem
LayerMask maskuj = LayerMask.GetMask("Gracz");
d.GetComponent<CapsuleCollider>().excludeLayers = maskuj;
}
private void KolizjaDzida(ControllerColliderHit hit)
{
GameObject dzida=hit.gameObject;
//usuń dzidę ze swiata gry
Destroy(dzida);
//daj dzide do reki bohatera
GameObject d = Instantiate(prafbBroni[0],
slotBroniPrawy.transform.position,
slotBroniPrawy.transform.rotation);
d.transform.SetParent(slotBroniPrawy.transform);
d.GetComponent<Rigidbody>().useGravity = false;
d.GetComponent<Rigidbody>().isKinematic = true;
//dla podniesionej dzidy
//ustaw warstwe na niekolidującą z graczem
LayerMask maskuj = LayerMask.GetMask("Gracz");
d.GetComponent<CapsuleCollider>().excludeLayers = maskuj;
GetComponent<AnimacjaDzikiego>().UstawGotowoscRzutu();
BronObracana=d;
}
private void OnControllerColliderHit(ControllerColliderHit hit)
{
Rigidbody rb=hit.collider.attachedRigidbody;
if (rb != null)
{
if (hit.gameObject.tag == "dzida")
{KolizjaDzida(hit); return; }
//nie jest to dzida to przesuń ciało
Vector3 silaKierunek =hit.gameObject.transform.position-
transform.position;
//zeruj pion
silaKierunek.y = 0;
//wyznacz jednostkowy wektor kierunku wektora
silaKierunek.Normalize();
//przekaz siłe uderzenia
rb.AddForceAtPosition(silaKierunek * silaWartosc,
transform.position,
ForceMode.Impulse);
}
}
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
}
Możemy uruchomić scenę i sprawdzić efekt działania po wykonanych zmianach.
Jeżeli przyjrzysz się lotowi dzidy, to zauważysz, że włócznia nie ustawia się do trajektorii lotu. Fizyka parabolicznego lotu dział poprawnie (prefabrykat dzidy ma dołączony komponent ciał sztywnego- Rigidbody), ale włócznia się nie ustawia stycznie do paraboli lotu.
Skrypt ruchu po torze parabolicznym
Aby ustawienie włóczni do trajektorii lotu było prawidłowe, należy na bieżąco znać zmiany kierunku wektora prędkości w ruchu ciała w rzuci ukośnym. Można to obliczać lub w prosty sposób odczytać wektor aktualnej prędkość. Znormalizować ten wektor, a z normalnej wektora wyznaczyć kąt ułożenia włóczni.
Tworzymy nowy skrypt o nazwie RuchWloczni(). Podpinamy do prefabrykatu dzidy
Kod skryptu RuchDzidy()
Wskazówka:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Animations.Rigging;
public class RuchDzidy : MonoBehaviour
{
public bool fRuch = false;
Rigidbody rb;
private void Start()
{
rb = GetComponent<Rigidbody>();
rb.useGravity = true;
rb.isKinematic = false;
}
void Update()
{
if (!fRuch) return;
//obracaj zgodnie z kierunkiem wektora predkosci
transform.rotation = Quaternion.LookRotation(rb.velocity.normalized);
}
private void OnCollisionEnter(Collision collision)
{
fRuch = false;
//wbij sie w cel na przykład w grunt
rb.velocity = Vector3.zero;
rb.useGravity = false;
rb.isKinematic = true;
//pozwól aby gracz mógł zabrać rzuconą dzidę
//usuń brak kolizji z graczem
LayerMask maskuj = LayerMask.GetMask("Default");
GetComponent<CapsuleCollider>().excludeLayers = maskuj;
}
}
Przechodzimy do skryptu KolizjeZasoby(), i w funkcji RzucajDzida() dodajemy tę linijkę kodu
Wskazówka:
d.GetComponent<RuchDzidy>().fRuch = true;
Kod funkcji po zmianie
Wskazówka:
public void RzucajDzida()
{
//zrob rzucana dzide z prefabrykatu
GameObject d = Instantiate(prafbBroni[0],
BronObracana.transform.position,
BronObracana.transform.rotation);
//usun obrocona dzide z reki
Destroy(BronObracana);
BronObracana = null;
d.GetComponent<RuchDzidy>().fRuch = true;
//ustaw predkośc oszczepu na 12 m/s
d.GetComponent<Rigidbody>().velocity = d.transform.forward * 12.0f;
//dla rzucanej dzidy
//ustaw warstwe na niekolidującą z graczem
LayerMask maskuj = LayerMask.GetMask("Gracz");
d.GetComponent<CapsuleCollider>().excludeLayers = maskuj;
}
Uruchom scenę i sprawdź lot rzuconej włóczni.
Lot włóczni jest poprawny. Dodatkowo wyłącznie grawitacji i ustawienie kinematyki na true po kolizji dzidy z dowolnym przedmiotem symuluje efekt wbicia w cel. Dodawanych zmian we właściwościach rzucanego przedmiotu może być bardzo dużo. Wszystko zależy od pomysłów i potrzeb sceny.