poniedziałek, 29 maja 2017

[DSP 2017] 29# HoloSurvivalShooter - zjedzony przez zombi i… nowo narodzony

Czas biegnie nieubłaganie, dziś najwyższa pora napisać coś z obranej przeze mnie tematyki na tegoroczny DSP. Zacząłem składać ten tekst w niedzielę na wieczór, a kończę dziś rano popijając kawę.

Przez ostatnich parę dni wymyślałem realizację utraty zdrowia i życia przez grającego. Chodziło o to, by dochodziło do kolizji pomiędzy nim a zombi. Zobaczmy mini klip pokazujący taką sytuację:

A co jeśli dostanę tyle razy, że skończy mi się życie?  Obejrzyjmy mini klip dwa:

Myślałem że zaczęły mnie kąsać misie, a zaczął mnie gryźć duży Hellephant, którego nie zobaczyłem. No cóż nieuwaga kosztuje, zgasło światło, skończyło mi się życie. Ale po kilku sekundach… mamy restart gry i znów mam zdrowie 100 i zero zdobytych punktów.  Szkoda że tak nie ma w realu (albo jeszcze nie wiemy jak włączyć taki magiczny przycisk).

Jak zbudowałem taką funkcjonalność?  Znowu nie było najłatwiej, co doprowadziło do założenia przeze mnie wątku Collision between player with glasses (camera) and hologram character na Windows Mixed Reality Developer Forum. Wywiązała się ciekawa rozmowa z osobą, która już wcześniej realizowała coś takiego przy pomocy sześcianu z colliderem przypiętym do obiektu kamery. Postanowiłem spróbować coś w tym kierunku. Znalazłem jeszcze równie interesujący artykuł How to Create User Location Hotspots to Trigger Events with the HoloLens, gdzie jest używany nawet sam kulisty collider bezpośrednio przypięty do kamery. To prostszy przypadek, zderzamy się gdzieś w powietrzu, niekoniecznie na podłodze, ale pobudza znakomicie też naszą wyobraźnię. Kto nie chciałby sterować kolorem lampy w pokoju wchodząc w jego odpowiedni region?

Wróćmy jednak do naszej gry. Jak już wspominałem do obiektu kamery przypiąłem dodatkowy obiekt o nazwie Player. Aktualnie jest to sześcian, natomiast niewykluczone, że zmienię go kiedyś na walec. Każdy sześcian dostaje automatycznie collider, który w tym przypadku dodatkowo przeskalowałem.

image

Wyłączyłem widzialność sześcianu odznaczając Mesh Renderer, nie jest to konieczne, ale nie chciałem, by cokolwiek z niego było kiedykolwiek widoczne. Do rozwiązania tutaj był też dość istotny problem. Jak zmierzyć odległość kamery od podłoża (czy też raczej od najbliższej poziomej płaszczyzny, kiedyś może to udoskonalę). Funkcja Raycast zatrzymuje się domyślnie na pierwszym znalezionym obiekcie collider. W tym przypadku byłby to mój sześcian. Jak zrobić by jego collider był niewidoczny dla funkcji Raycast?  Umieściłem Player w warstwie IgnoreRaycast. A oto kod skryptu PlayerUpdater, który dopasowuje wysokość i położenie sześcianu do położenia kamery:

public class PlayerUpdater : MonoBehaviour
{
    Camera mainCamera;   

    void Awake()
    {
         mainCamera = Camera.main;       
    }

    void Update()
    {
        RaycastHit hit;
        var headPosition = mainCamera.transform.position;       
        var downDirection = new Vector3(0, -1, 0);

        if (Physics.Raycast(headPosition, downDirection, out hit))
        {
            var difference = headPosition - hit.point;
            var distanceInY = Mathf.Abs(difference.y);           

            gameObject.transform.localPosition = new Vector3(0, -0.5f * distanceInY, 0);
            gameObject.transform.localScale = new Vector3(1, distanceInY, 1);                                 
        }              
    }
}

Jednym z najbardziej kluczowych dla logiki gry jest znany nam już skrypt PlayerHealth. Tutaj w takiej postaci:

public class PlayerHealth : MonoBehaviour
{
    public int startingHealth = 100;
    public int currentHealth;
    //public Slider healthSlider;
    //public Image damageImage;
    public AudioClip deathClip;
    //public float flashSpeed = 5f;
    //public Color flashColour = new Color(1f, 0f, 0f, 0.1f);

    AudioSource playerAudio;   
    PlayerShooting playerShooting;

    bool isDead;
    bool damaged;


    void Awake ()
    {
        playerAudio = GetComponent <AudioSource> ();       
        playerShooting = GetComponentInChildren <PlayerShooting> ();
        currentHealth = startingHealth;       
    }


    void Update ()
    {
        if(damaged)
        {
            //damageImage.color = flashColour;
        }
        else
        {
            //damageImage.color = Color.Lerp (damageImage.color, Color.clear, flashSpeed * Time.deltaTime);
        }
        damaged = false;
    }


    public void TakeDamage (int amount)
    {
        damaged = true;

        currentHealth -= amount;

        //healthSlider.value = currentHealth;
        HealthManager.health = currentHealth;

        playerAudio.Play ();

        if(currentHealth <= 0 && !isDead)
        {
            Death ();
        }
    }


    void Death ()
    {
        isDead = true;

        playerShooting.DisableEffects ();       

        playerAudio.clip = deathClip;
         playerAudio.Play ();
       
        playerShooting.enabled = false;       
    }   
}

Jak widzimy informacje o stanie życia gracza przekazywane są do statycznego pola health skryptu HealthManager. Jest on przypięty do pola tekstowego HealthText, zrealizowanego podobnie do ScoreText.

C2

Kod HealthManager przedstawia się następująco:

public class HealthManager : MonoBehaviour
{
    public static int health;


    Text text;


    void Awake ()
    {
        text = GetComponent <Text> ();
        health = 100;
    }


    void Update ()
    {
        text.text = "Health: " + health;       
    }   
}

Skrypt EnemyAttack przypięty do każdego modelu zombi jest bardzo ważny. To on sprawia, że w razie wykrycia kolizji między obiektem Player (niewidoczny sześcian pod kamerą) a zombi nastąpi odpowiedni dźwięk i utrata punktów życia, a w skrajnym przypadku śmierć.

image

Skrypt EnemyAttack niewiele różni się tutaj od wersji znanej z oryginału gry:

public class EnemyAttack : MonoBehaviour
{
    public float timeBetweenAttacks = 0.5f;
    public int attackDamage = 10;
   
    GameObject player;    
    PlayerHealth playerHealth;
    EnemyHealth enemyHealth;
    bool playerInRange;
    float timer;


     void Awake ()
    {
        player = GameObject.FindGameObjectWithTag ("Player");        

        playerHealth = player.GetComponent <PlayerHealth> ();
        enemyHealth = GetComponent<EnemyHealth>();       
    }


    void OnTriggerEnter (Collider other)
    {
        //Debug.Log("OnTriggerEnter");       

        if (other.gameObject == player)
         {
            playerInRange = true;
        }
    }


    void OnTriggerExit (Collider other)
    {
        //Debug.Log("OnTriggerExit");       

        if (other.gameObject == player)
        {
            playerInRange = false;
        }
    }


     void Update ()
    {
        timer += Time.deltaTime;

        if(timer >= timeBetweenAttacks && playerInRange && enemyHealth.currentHealth > 0)
        {
            Attack ();
        }       
    }


    void Attack ()
    {
        timer = 0f;

        if(playerHealth.currentHealth > 0)
        {
            playerHealth.TakeDamage (attackDamage);
        }
    }
}

A jak przywrócić się z powrotem do życia? Realizuje to skrypt GameOverManager przypięty do obiektu Canvas nad HealthText:

public class GameOverManager : MonoBehaviour
{
     public PlayerHealth playerHealth;      
     public float restartDelay = 5f;

   
     float restartTimer;


     void Update()
     {
        
         if (playerHealth.currentHealth <= 0)
         {           
             restartTimer += Time.deltaTime;
            
             if (restartTimer >= restartDelay)
            {
                 SceneManager.LoadScene(0);

                ScoreManager.score = 0;
                 HealthManager.health = 100;
             }
         }
    }

}

Aktualne źródła gry tradycyjnie już w gałęzi HoloSurvivalShooter na github, a poprzednia wersja trafiła do HoloSurvivalShooter3.  

To wersja wstępna, może uda się jeszcze coś tutaj udoskonalić, zobaczymy. Przygoda powoli dobiega końca, ale dziś nie sumuję, zostawię to na dzień ostatni. Zresztą tak naprawdę… nic nie musi się kończyć, ta kwestia dotyczy jedynie brandu DSP w tytułach i używanej kategorii postów –Smile

Brak komentarzy: