17 września oczywiście nie ma nic wspólnego z dzisiejszym zadaniem oprócz tytułowego “ciosu w plecy”. Jak już się domyślacie będziemy implementować do naszej standardowej sceny atak z różnych stron. Zapewne graliście w jakąś skradankę, rpga czy cokolwiek, co pozwala nam na zadanie dodatkowego damage’u przeciwnikowi. W Skyrimie cios z sztyletu mógł dochodzić nawet do 45 mnożnika ataku podstawowego (nawet więcej, ale co tam).

Tym tematem miałem zająć się już na początku listopada, jako trening zdobytej wiedzy od Tutora (pozdro Maks 😉 ). Wyszło jednak jak wyszło i temat podejmuję dopiero (uwaga! abstrakcja) w grudniu przed startem bloga. Powodowane jest to oczywiście wolnym czasem i chęcią zrobienia czegokolwiek krótkiego. Zanim jeszcze przejdziemy do sedna sprawy muszę uprzedzić, że ten wątek będzie rozbudowywany, bo sposoby jak i szczegóły są mnogie. Ciekawi? Zaczynajmy!

Po pierwsze: ja używam zwykle najnowszych wersji Unity ze względu na masę ciekawych funkcji (obecnie 2019.2.5f1), jednak jeżeli będzie wymagana wersja powyżej LTS 2018.4.9f1, to będzie to jasne napisane na samym początku. Zawsze będę próbował też stworzyć coś alternatywnego. W tym przypadku nawet wcześniejsze wersje będą w porządku.

Po drugie: dostosowana paczka pod serię QU (patrz obecna wersja!) dla nowego quick’a – zawsze można zacząć od nowego albo pobrać całość (!) repo z Githuba – używane paczki dodatkowo 2D Sprite, 2D Tilemap Editor, Android Logcat, Mathematics, PolyBrush, Post Processing, ProBuilder, Shader Graph. Do tego skorzystamy ze Standard Assets od Unity Tech. oraz jakiegokolwiek modelu przeciwnika, ja znalazłem akurat fajnego psa – w Asset Store: Monster&&LowPolygon Mobile_Dog. Jeżeli nie będzie za free, to wystarczy pobrać tylko zmiany w 3 commicie. Jeśli zaś nie chce Ci się pobierać assetów, po prostu użyj swoich/innych.

Ze względów technicznych na razie tylko skrypcik – różni się od tego opisywanego tutaj tym, że ma więcej opcji co do zasobów, typów npc’a i typów obrażeń.

Później chyba zaimplementuję wyświetlanie reklamy -> pobieranie, więc no offend, ale jakoś hosting musi być opłacany ;

Ok, (przyjmijmy, że) mamy zwykłego gracza do poruszania się oraz model wroga bez żadnego skryptu.

Zadanie

[JEŻELI KOPIUJESZ KOD STĄD NIE ZAPOMNIJ O WYKASOWANIU KOMENTARZY Z POLSKIMI ZNAKAMI!]

Zdefiniujmy najpierw problem: Jeżeli gracz znajduje się za plecami przeciwnika, to może zadać cios wzmocniony.

Pierwszy krok – podstawowe dane dla gracza/przeciwnika.

public CreatureController otherCreature; 
// public dla testów, później można zmienić na private

public float creatureHealth = 100;
public float creatureBasicDamage = 20;

W zasadzie wystarczy tylko tyle. Jednak można dodać już teraz kilka zmiennych, które ułatwią nam życie w przyszłości, a także pozwolą testować dodatkowe funkcje.

public float maxCreatureHealth = 100;
public float creatureHealthRecoverSpeed = 2;
public float nextRecover = 0; 

public eCreatureState _CREATURE_STATE = eCreatureState.neutral;   
public eAttackType _ATTACK_TYPE = eAttackType.melee;

[System.Flags] // pozwala ustawić konkretną flagę w trakcie działania.
public enum eCreatureState  // fajny trick na późniejsze działania z bitami
{
    // Decimal          // Binary
    none = 0,           // 00000000
    player = 1,         // 00000001
    enemy = 2,          // 00000010
    neutral = 4,        // 00000100
    all = 0xFFFFFFF     // 11111111111111111111111111111111
}

[System.Flags]
public enum eAttackType // type of an attack
{
    // Decimal          // Binary
    none = 0,           // 00000000
    melee = 1,          // 00000001
    ranged = 2,         // 00000010
    magic = 4,          // 00000100
    all = 0xFFFFFFF     // 11111111111111111111111111111111
}

// to wszystko pozwala nie tylko szybko sprawdzić kim jest postać, ale też
// błyskawicznie zmienić jej status na inny, np. z przyjaciela na wroga
// o czym może kiedyś będzie.

Start() i FixedUpdate()

Czemu Start, a nie Awake? Gdyby chodziło o zarządzanie grą, Awake() jest zdecydowanie lepsze, jednak tutaj nie trzeba nic wielkiego. FixedUpdate jest o tyle tu ważny, że ujednolici odstępy w wywoływaniu funkcji, więc przy odnawianiu życia będzie wydajniej.

void Start()
{
    SetCreatureState(); // funkcja, która wskaże nam jaki jest status postaci
    nextRecover = Time.time; // czas startowy, od tej pory możemy odzyskiwać hp
}

void FixedUpdate()
{
    CheckHealth();      // funkcja sprawdzająca czy postać nie umarła
    if (creatureHealth < maxCreatureHealth && Time.time > nextRecover + 1)
    {
        RecoverResources(); // jeżeli możemy się uzdrowić, to zróbmy to
        nextRecover = Time.time;
    }
    if (_CREATURE_STATE == eCreatureState.player && (Input.GetKey(KeyCode.Space)))
    {   
        // sprawdzamy, czy jesteśmy graczem, żeby móc atakować (póki co)
        // używamy spacji, aby mieć pewność co do ataku
        Attack(SearchVector()); // atakujemy i przy okazji sprawdzamy 
        // czy zadaliśmy cios w plecy
    }
}

Żyjesz?

Sprawdzamy czy postać ma jakieś hapsy.

void CheckHealth()
{
    if (creatureHealth <= 0)
        Destroy(gameObject, 1);
} 

Oczywiście dbamy o czystość, więc jest też OnDestroy()

void OnDestroy()
{
    Debug.Log("CreatureController - OnDestroy: Creature " + name + " destroyed!");
}  

No i się poszedł lapek p***... no nic będzie we wtorek update ze screenami i gitem, jak już ładowarkę ogarnę.

Triggery

Musimy przecież sprawdzić komu zadajemy obrażenia. Jako, że mamy skrypt uniwersalny, to (co ciekawe) oprócz wrogów i gracza uwzględniamy broń - np. strzałę. Ta ma całkowicie odrębny tag, ale za to dodatkowe funkcje, o których w przyszłości. Wchodzimy w trigger:

void OnTriggerEnter(Collider other)
{
    if (other.tag == "Enemy" || other.tag == "Player" || other.tag == "Weapon" )
    {
        if (other.tag == "Weapon")
        {
            // na przyszłość
            return;
        }
        if(other.GetComponent()) 
            otherCreature = other.GetComponent();
        // uploadujemy naszego wroga
    }
}

I wychodzimy, usuwając dane o przeciwniku:

void OnTriggerExit(Collider other)
{
    if (otherCreature == null)
        return;
    if (other.transform == otherCreature.transform)
        otherCreature = null;
}

Attack() - Cios w plecy!

Tutaj po prostu wykonujemy zamach, wypuszczamy strzałę, rzucamy czar etc. i w zależności od tego gdzie się znajdujemy, to albo zadajemy zwiększone obrażenia, albo i nie ;).

void Attack(bool multiply)
{    
    if (otherCreature == null)
        return;
    float damageMultiplier = 1;
    if (multiply)
    {
        switch (_ATTACK_TYPE)
        {
            case eAttackType.none:
                Debug.Log("CreatureController - Attack: Attack type has not been set!");
                break;
            case eAttackType.melee:
                damageMultiplier = 1f;
                break;
            case eAttackType.ranged:   // typ zależy od aktualnie używanej broni
                damageMultiplier = 3f; // u nas będziemy zmieniać ręcznie 
                break;
            default:
                Debug.Log("CreatureController - Attack: Attack type is not specified!");
                damageMultiplier = 1f;
                break;
        }
        Debug.Log("Attack will be multiplied by " + damageMultiplier);
    }
    otherCreature.creatureHealth -= creatureBasicDamage * damageMultiplier;
    Debug.Log("CreatureController - Attack: Creature " + otherCreature.name + " damaged for " + (creatureBasicDamage * damageMultiplier) + " by " + name);
    // testujemy czy wszystko działa
}

"Trudna" funkcja - w rzeczywistości nie musimy mieć nic poza dwoma obiektami. A w zasadzie, to ich pozycjami. Obliczamy cosinusa (łatwiej nim operować, bo jest symetryczny względem osi OY). Tutaj oczywiście wzór na cos b/c, gdzie b.. a w sumie, tak jak widać na obrazku. Naszego obliczonego cosinusa porównujemy do.. no właśnie, już od nas zależy od kiedy "zaczynają się" plecy. Jak dla mnie powyżej 0.7-0.8 jest całkiem rozsądną opcją (od około - π/4 do π/4 parametru x)

trójkąty i kwadraty
prosta trygonometria
A tak wygląda wykres cosinusa
bool SearchVector()
{
    if (otherCreature == null) // cios w powietrze
        return false;
    Vector3 otherPos = otherCreature.transform.position;
    Vector3 distance = otherPos - transform.position;
    float cos = ((distance.x * otherPos.x) + (distance.z * otherPos.z)) /
                (Mathf.Sqrt(Mathf.Pow(distance.x, 2) + Mathf.Pow(distance.y, 2)) + 
                Mathf.Sqrt(Mathf.Pow(otherPos.x, 2) + Mathf.Pow(otherPos.y, 2)));
    return cos > 0.7f ? true : false;
}

I w sumie to tyle, na rozmowie z Maksem trwało to w sumie dłużej, a tak, cyk jeden wzorek, kilka zmiennych i gotowe. Jednak to nie koniec na dzisiaj.

Inne funkcje

Przy pomocy tagu obiektu nadajemy mu status - później w skryptach można to zmienić/wczytać.

void SetCreatureState()
{
    switch(tag)
    {
        case null:
            Debug.Log("CreatureController - SetCreatureState: Tag is not specified. Set as none creature."); 
            _CREATURE_STATE = eCreatureState.none;
            break;
        case "Player":
            _CREATURE_STATE = eCreatureState.player;
            break;
        case "Enemy":
            _CREATURE_STATE = eCreatureState.enemy;
            break;
        case "Neutral":
            _CREATURE_STATE = eCreatureState.neutral;
            break;
        default:
            Debug.Log("CreatureController - SetCreatureState: Tag is not specified. Set as neutral creature.");
            _CREATURE_STATE = eCreatureState.neutral;
            break;
     }
}

Szybka funkcja do odnawiania życia/energii/krzyków 😉

void RecoverResources()
{
    creatureHealth = maxCreatureHealth > creatureHealth ? creatureHealth + creatureHealthRecoverSpeed : maxCreatureHealth;
    // szybki if - jeśli życie < maks to dodawaj, inaczej życie = maks życia
    // tutaj później dorzucimy kondychę i manę 
}

I to koniec na dzisiaj. Jeśli masz jakieś uwagi, z chęcią przeczytam komentarze. Jeśli zaś masz pomysły.. cóż dodamy do roadmapy 8).