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

środa, 24 maja 2017

[DSP 2017] 28# BUILD 2017 odc.5 (Windows, UWP, desktop, Fluent Design, My People)

Kolejne 8 filmów obejrzane. Nie ma może czegoś zupełnie nowego, ale znajdziemy takie perełki jak pokaz implementowanych obecnie nowych możliwości aplikacji UWP na wewnętrznej kompilacji Windows (m.in uruchamianie z linii komed i uruchamianie wielu instancji w osobnych procesach), Windows Template Studio, darmowy pakiet Telerik z kontrolkami dla UWP (zwłaszcza data grid z niego gości na niejednej prezentacji w tym roku), integrację z People Hub (notyfikacje są naprawdę cool!),  obsługę plików SVG w XAML i nie tylko…  Oczywiście nie chcę się zamykać tylko na taką tematykę, na mojej liście oczekuje niejeden film dotyczący ML, AI, Azure, Python, R, IoT, MR czy Web…  Jednak z uwagi na pewien sentyment do .NET pozwoliłem sobie rzucić na niego teraz okiem. Z tych spraw nadal zostało co najmniej jeszcze kilka godnych uwagi pozycji, ale o tym następnym razem jak znów powrócę wirtualnie na Build.

 

Tip, tricks, and secrets: Building a great UWP app for PC

image

masa nieprawdziwych opinii o UWP:

image

image

drag & drop: międzypokoleniowa współpraca

image

data grid od Telerika

image

image

każde okno w innym wątku, niezależne od siebie

picture in picture w Windows

image

Płatności w Microsoft Wallet w desktopowym Windows

Secret… service w UWP!

image

poza tym sql bridge - aplikacja konsolowa

Windows Template Studio

image

image

image

Po utworzeniu lista to do

image

pakiet nuget Telerik.UI.for.UniversalWindowsPlatform przeznaczony dla aplikacji UWP zawiera między innymi data grid (free and Open Source !)

image

UWP nie jest dziś jeszcze kompletny, ale w zależności od feedbacku zostanie określony backlog i rozwój kolejnych funkcjonalności w kolejnym updacie.

prototyp: uruchamianie UWP z linii komend

image

Dwie niezależne instancje aplikacji UWP uruchomione z linii komend z innymi parametrami. Do tej pory było to możliwe, ale w ramach jednego procesu.

image

Alias i automatyczne uruchamianie przy starcie systemu

image

nowa izolowana instancja

image

image

aby rozmawiać bezpośrednio z SQL Server po tcp

image

stary SqlClient w UWP dzięki .NET Standard

image

 

Build Amazing Apps with Fluent Design

image

image

nawigacja teraz znacznie prościej

image

image

image

image

image

przezroczysty pasek górny

image

image

image

image

image

image

image

image

image

image

image

image

image

image

image

image

image

image

http://aka.ms/buildcast

 

Ten things you didn’t know about Visual Studio 2017 for building .NET UWP apps

image

Projekt VS 2017 UWP:  referencje do pakietów nuget w samym pliku projektu

image

image

image

image

Refleksja w UWP dzięki .NET Standard

image

image

 

My People: the Taskbar is a Window to your app

image

image

image

image    image

image

przypinanie osoby do paska zadań

image

specjalne notyfikacje dla osób znajdujących się na pasku zadań

image

notyfikacja dobra na zamawianie pizzy –Winking smile

image

 

Desktop Bridge Apps & User Transition

image

image

pakiet nuget DesktopBridge.Helpers

image

 

Vector iconography: Using SVG images in your app

image

image

image

Demo C++:  m.in obsługa animacji

 

What’s new for multi-tasking in UWP?

image

 

UWP Apps file access improvements

image

image

image