poniedziałek, 24 kwietnia 2017

[DSP2017] 17# Romans z Pythonem cz.1 (porządek, matematyka, sterowanie, kolekcje, funkcje, obiekty)

Dziś napiszę dla odmiany coś o Pythonie. Nie porzucam rzeczywistości rozszerzonej, HoloLens i Unity, które obecnie nadal pozostają tematem wiodącym. Przyszedł mi do głowy natomiast taki mini-cykl związany z Pythonem i pewnymi rzeczami naokoło niego, który wprowadzi pewne urozmaicenie, a jednocześnie będzie w zgodzie z regułami DSP 2017.

Czemu w ogóle warto uczyć się Pythona? Python jest językiem uniwersalnego zastosowania o popularności niewiele ustępującej C# i jest często używany obok języka R w uczeniu maszynowym i Data Science (w aplikacjach serwerowych i webowych też, ale nie jest to już takie wyjątkowe). Jest też głównym językiem używanym do programowania Raspberry Pi (jak się na nim postawi Windows 10 IoT Core to można pisać i w C#, co też zrobiłem w zeszłym roku i wcześniej, ale mówię jak jest generalnie). Visual Studio 2017 i 2015 oferuje wsparcie w postaci Python Tools, a ostatnio Python stał się językiem natywnie wspieranym przez SQL Server 2017, który przy wsparciu dla procesorów graficznych przeobraża się w wydajną platformę do budowania aplikacji AI.

python_logoball-python

Do początkowych zabaw z językiem może wystarczyć nam nawet sama przeglądarka internetowa jeśli wybierzemy środowisko Jupyter. Myślę, że wpis Nadal w kosmicznym klimacie – Jupyter krótko i treściwie wprowadza w używanie tego narzędzia. Jupitera możemy postawić także u siebie i również wtedy korzystać z przeglądarki internetowej, co też uczyniłem z uwagi na rozłączanie wersji on-line po okresie bezczynności. Instalujemy wtedy najpierw popularne środowisko Anaconda, które znacząco ułatwia zarządzanie pakietami i instalacjami Pythona.

A teraz trochę uwag odnośnie poznawania podstaw Pythona, a także trochę ciekawostek  i rzeczy, które nieraz trudno będzie odnaleźć w innych językach.

 

1#  Czystość i porządek

Za pomocą wcięć sterujemy zagnieżdżaniem bloków. Obchodzimy się bez nawiasów.

image

2#  Łatwiejsza matematyka

Pamięta ktoś silnię ze szkoły podstawowej?  W standardowej bibliotece math jest do niej funkcja.

image

Operator potęgi w samym języku

image

Bajka. Nielimitowana precyzja dla int. Liczby ograniczone jedynie pamięcią maszyny.

image

Na poziomie języka możemy definiować liczby urojone i zespolone, to nieczęsto się zdarza !

image

Mało tego, dzięki pakietowi cmath pierwiastek z –1 istnieje jako liczba urojona:

image

Pakiet cmath pozwala wyliczyć pewne wartości dla liczb zespolonych np. fazę.

Po co nam takie liczby? Może do faktur się nie przydadzą, ale już do obliczeń związanych z elektrycznością jak najbardziej. Dodatkowo przypomina mi się jak w zeszłym roku używałem szybkiej transformaty Fouriera do rozkładu dźwięku na zakresy częstotliwości. Tam też były liczby zespolone, tyle że w Javie czy C# trzeba sobie jakoś samemu je zamodelować.

Float duży zakres i dokładność.

Klasa Decimal w standardowym module decimal.

image

Wartości niecałkowite należy zawsze podawać w cudzysłowie, inaczej otrzymamy coś takiego:

image

Ułamki można też definiować za pomocą klasy Fraction ze standardowego modułu fractions.

 

3#  Warunki i pętle

Zamiast if else można krócej, wystarczy elif.

image

Oprócz for jest też oczywiście pętla while.

image

4# Kolekcje

string - Unicode, nie ma oddzielnego typu char, po prostu mamy 1-znakowy string

image

formatowanie napisów (jeden z wielu wariantów):

image

bytes - reprezentacja bajtowa napisów string, w zapisie przed cudzysłowem piszemy literę b

image

a teraz zdekodujmy misia:

image

list - listy

image

Ujemne indeksy list są możliwe! Indeks ostatniego elementu możemy alternatywnie opatrzyć wartością –1, a indeks każdego elementu przed nim jest o 1 mniejszy. Nie zaleca się tradycyjnego z innych języków wyliczania ostatniego indeksu jako długość listy - 1.

image

W Pythonie nawet zakres od 1 do –1 ma sens:

image

Bardzo poręczne są też przedziały otwarte:

image

A jak skopiować listę? Wystarczy:

image

Przy okazji widzimy operator is, który w Pythonie sprawdza czy zmienne wskazują na ten sam obiekt. Z kolei operator == dokonuje porównywania zawartości.

Mnożenie listy w Pythonie, zwłaszcza podczas jej inicjowania, ma sens:

image

Usuwanie elementu o wskazanym indeksie z listy jest nieco dziwne, bo w takim przypadku używamy operatora del:

image

dict - słowniki

image

tuple - krotki

image

Przecinek czasem robi różnicę:

image

Przy okazji widzimy funkcję type do sprawdzania typu.

Jak zamienić wartości zmiennych w jednej linii?

image

Szybka zamiana jednych struktur w inne, np. listy krotek w słownik:

image

range - zakres

Python to przykład języka z zakresami, które mogą kojarzyć się z Objective-C czy Swift.

image

image

set - zbiór

image

Co ciekawe pustego zbioru nie utworzymy za pomocą {}, a jedynie przez konstruktor set.

image

5# Funkcje

Do ich definiowania używamy słówka def.

image

Można definiować domyślne wartości dla parametrów podobnie jak choćby w C#.

6# Obiekty

Wszystko jest obiektem (w tym typy prymitywne i funkcje). Każdy obiekt ma swoje id, nawet prosta liczba:

image

Język jest dynamiczny, ale ściśle typowany. Nie ma niejawnych konwersji typów. Popatrzmy na wcześniej zdefiniowaną funkcję sum. Jak podałem jej teraz 2 stringi, to ich suma wyraziła się w konkatenacji. Pokazuje to siłę języka. Jednak jeśli podam tej funkcji różne typy jak string i int, to automatyczna konwersja nie nastąpi i wystąpi błąd.

image

Zakresy nazw zmiennych:

  • local - wewnątrz bieżącej funkcji
  • enclosing - wewnątrz jakiejkolwiek zagnieżdżonej funkcji
  • global - na najwyższym poziomie modułu (o modułach przy innej okazji)
  • built-in - dostarczana przez moduły wbudowane

O ile poniższy kod nie budzi wątpliwości:

image

o tyle ten poniżej jest niespodzianką:

image

Otóż wewnątrz funkcji x zostało potraktowane jako nowa lokalna zmienna. Ale jest sposób by temu zaradzić. Można jawnie powiedzieć, żeby x wewnątrz funkcji było traktowane jako nazwa globalna:

image

Na ten raz wystarczy. Następnym razem, jeśli będzie to odcinek o Pythonie, to pójdziemy w bardziej zaawansowane konstrukcje języka albo poeksperymentujemy z narzędziami… Stay tuned.

sobota, 22 kwietnia 2017

[DSP2017] 16# Idą po mnie, więc strzelam - ćwiczenia obrony wirtualnej

Gra Survival Shooter okazała się na tyle sympatyczna i wciągająca, że przeszedłem przez ostatnie 2 części jej tutoriala:

Poprzedni stan gry przeniosłem do folderu SurvivalShooter 1 enemy (Unity Tutorial), natomiast to, co powstało z całego tutoriala trafiło do SurvivalShooter (Unity Tutorial). Czego się nauczyłem?

Po pierwsze i najważniejsze  - jak rozmnażać wroga. Teraz podczas grania nie dość, że atakują mnie trzy rodzaje stworów, to przychodzą z różnych miejsc i mogą się namnażać, jeśli nie będę nadążać z ich odstrzałem.

game1

W Prefabs wrzuciłem sobie ZomBear ze skończonej wersji gry, podobnie jak przy Zombunny podpiąłem znane już skrypty EnemyAttack, EnemyHealth i EnemyMovement. Poza innymi dźwiękami, inną liczbą punktów odbieranego mi życia czy ewentualnie inną prędkością poruszania się w NavMeshAgent różnic generalnie nie ma. To taki sam wróg, jak poprzednio, tylko wygląda jak miś zombi w różowym. Dostał też dokładnie taki sam animator – EnemyAC. Zauważmy, że całą konfigurację nie robię na obiekcie na scenie, tylko na prefabrykacie. To konieczność, jeśli chcemy zombi dynamicznie dodawać podczas gry. Aha, ostatnim razem Zombunny z Hierarchy przeciągnąłem do Prefabs czyniąc z niego prefabrykat. Teraz mogę więc dla porządku usunąć obiekt Zombunny z Hierarchy, bo też będzie dodawany dynamicznie.

Win1

No i przyszła pora na grubego zwierza znanego tutaj pod nazwą Hellephant, czyli jakby nieco oryginalny słoń, tyle że przez h –Winking smileJego animacje są inne, ale chcemy mieć dokładnie taką samą logikę animatora co w EnemyAC. Tworzymy więc w Animations Animator Override Controller o nazwie HellephantAOC. W jego polu Controller ustawiamy EnemyAC, a na pola Move, Idle i Death przeciągamy animacje o odpowiadających nazwach z Hellephant w Characters.

Win2

Prefabrykat Hellephant podobnie jak poprzednio wgrywam do Prefabs ze skończonej wersji gry, ustawiam na nim skrypty typowe dla wroga oraz animator na stworzony przed chwilą HellephantAOC. Gruby zwierz ma trzy życia oraz za każdym razem zabiera z mojego życia 20 punktów. Trzeba się go będzie wyjątkowo strzec.

image

Teraz jak wskazać miejsca, w których będą powstawać kolejne zombi? Tworzymy trzy puste obiekty Game o nazwach ZombunnySpawnPoint, ZombearSpawnPoint i HellephantSpawnPoint. W każdym z nich ustawiam inną pozycję i rotację, a także inny kolor etykiety (klikamy w ikonę z lewej od checkboxa w inspektorze). Poniżej przykładowo widzimy ustawienia dla ZombunnySpawnPoint. Ustawienie kolorowej etykiety pomaga w odnalezieniu obiektu na scenie.

Win5

Teraz potrzebujemy czegoś, co by nam cyklicznie i dynamicznie tworzyło obiekty zombi w określonych miejscach sceny. To zadanie spełnia skrypt EnemyManager przypięty do pustego obiektu Game o takiej samej nazwie. Oto jego kod:

public class EnemyManager : MonoBehaviour
{
    public PlayerHealth playerHealth;
    public GameObject enemy;
    public float spawnTime = 3f;
    public Transform[] spawnPoints;


    void Start ()
    {
        InvokeRepeating ("Spawn", spawnTime, spawnTime);
    }


    void Spawn ()
    {
        if(playerHealth.currentHealth <= 0f)
        {
            return;
        }

        int spawnPointIndex = Random.Range (0, spawnPoints.Length);

        Instantiate (enemy, spawnPoints[spawnPointIndex].position, spawnPoints[spawnPointIndex].rotation);
    }
}

Widzimy, że metodą Instantiate dodajemy obiekt na scenę we wskazanym miejscu i określonej rotacji. InvokeRepeating z kolei realizuje zadanie timera w Unity. Co ciekawe w opisywanej sytuacji podpinamy trzy instancje tego samego skryptu EnemyManager do obiektu EnemyManager. Na pole PlayerHealth przeciągamy Player z Hierarchy. Interesujące jest wskazanie wroga. Nie mamy go na scenie, więc na pole Enemy przeciągamy odpowiedni prefabrylat z Prefabs w Assets. Każda instancja EnemyManager może mieć inny okres namnażania (tworzenia i przerwy). Gruby zwierz znany jako Hellephant z racji swojego śmiertelnego dla nas zagrożenia trzy razy wolniej się powiela niż mniejsze i słabsze gatunki zombi.

Win4

Przejdźmy teraz do mniej ważnej, ale potrzebnej rzeczy: ekranu zakończenia gry. Pojawia się on po utracie życia przez gracza zanim nastąpi restart gry.

game2

Aby go mieć do HUDCanvas dodajemy obiekt ScreenFader typu Image oraz pole tekstowe o nazwie GameOverText. Kanał alfa koloru tła obrazka i koloru tekstu ustawiamy na zero.

Win6

W ekranie końca gry najciekawszą rzeczą nie jest on sam, a jego animacja. Przy zaznaczonym obiekcie HUDCanvas w Hierarchy z menu Window wybieramy Animation. Okno Animation dokujemy sobie obok Game, tworzymy w nim animację GameOverClip, którą zapisujemy do folderu Animations w Assets. Przez Add Property wybieramy, co chcemy animować, mogą to być właściwości składowych. Obsługa timeline jest dość intuicyjna. Czerwona linia to punkt czasowy, w którym jesteśmy i który możemy przesuwać. Powinniśmy być w trybie nagrywania, co symbolizuje oznaczenie niektórych przycisków na czerwono. W panelu z właściwościami  możemy edytować wartości dla punktu czasowego i ramki, w której jesteśmy. Ramkę dla danego punktu czasowego i właściwości można dodać za pomocą przycisku nad właściwościami. Można też zaznaczyć wszystkie punkty danej ramki i przesunąć na timeline do innego punktu czasowego. Podobnie można uczynić z grupą ramek. Wydaje się to proste i intuicyjne. Osobiście nasuwają mi się stare wspomnienia odnośnie animacji w Blend dla aplikacji XAML sprzed ładnych paru lat. Aha, w inspektorze GameOverClip wyłączamy zapętlenie (odznaczamy check przy Loop Time).

Win7

Jak sprawić, by po tym jak utracimy życie, animacja GameOverClip została włączona? Otwieramy animator HUDCanvas, widzimy w nim już stan GameOverClip. Tworzymy nowy pusty stan o nazwie Empty i ustawiamy go na domyślny. Tworzymy tranzycję z Empty do GameOverClip. Definiujemy parametr typu trigger o nazwie GameOver. Na właściwościach tranzycji dodajemy warunek na ten trigger i odznaczamy Has Exit Time.

Win8

Na koniec do HUDCanvas przypinamy skrypt GameOverManager o zawartości:

public class GameOverManager : MonoBehaviour
{
    public PlayerHealth playerHealth;


    Animator anim;


    void Awake()
    {
        anim = GetComponent<Animator>();
    }


    void Update()
    {
        if (playerHealth.currentHealth <= 0)
        {
            anim.SetTrigger("GameOver");
        }
    }
}

Dla stworzenia bardziej klimatycznego nastroju kogoś osaczonego przez zombi możemy w BackgroundMusic w AudioSource włączyć opcję Play On Awake, by ciągle od początku nam grało.

Gra się całkiem, całkiem, ale to jedynie tutorial z Unity. Trzeba będzie pomyśleć jak to zadoptować do HoloLens, ale o tym w następnych odcinkach. Niewykluczone, że zgłębię jeszcze coś z Unity, zresztą zobaczamy. A na razie do zobaczenia.

[DSP2017] 15# Albo zombi, albo ja - ćwiczenia obrony wirtualnej

Minęło parę wieczorów podczas których przeprowadziłem ćwiczenia ze strzelania na poligonie gry  Survival Shooter.  Przeszedłem przez kolejne cztery kroki tutoriala:

Podzielę się nabytą wiedzą w skondensowanej formie.

W scenie gry przybył slider z serduszkiem pokazujący ile nam jeszcze pozostało życia, a także licznik punktów jeśli zestrzelę wroga (na razie zawsze tylko jedno zombi).

game1

Suwak z serduszkiem to HealthSlider w HUDCanvas (Canvas do grafiki 2D), licznik to pole tekstowe ScoreText.

Win1

Jak zombi do nas się zbliży, to w końcu nastąpi kolizja i cała scena błyśnie na czerwono, a my stracimy trochę punktów z naszego życia, co pokaże suwak z serduszkiem.

game2

Czerwone tło sceny to nic innego jak obiekt DamageImage.

Win3

Skrypt PlayerHealth przypięty do obiektu Player odpowiada za zapalanie tła sceny na kolor czerwony, a także oferuje funkcjonalność odbierania nam części życia i nas zabicia. Oto najważniejsze jego fragmenty:

    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;

        playerAudio.Play ();

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


    void Death ()
    {
        isDead = true;

        playerShooting.DisableEffects ();

        anim.SetTrigger ("Die");

        playerAudio.clip = deathClip;
        playerAudio.Play ();

        playerMovement.enabled = false;
        playerShooting.enabled = false;
    }

Z kolei skrypt EnemyAttack przypięty do Zombunny odnajduje obiekt gracza i sprawdza czy następuje z nim kolizja. Jeśli tak, minął odpowiedni interwał czasowy między atakami i wróg ma jeszcze życie (o czym później, na początku można ten warunek pominąć), to przystępuje do ataku i jeśli gracz ma jeszcze życie, to odbiera mu jego kawałek wywołując w tym celu jego własną funkcję!  Jeśli skończyło się nam życie, odpalany jest trigger, który w animatorze wyzwala animację naszego upadku i umieramy. Realizują to poniższe fragmenty kodu:

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

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


    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 ();
        }

        if(playerHealth.currentHealth <= 0)
        {
            anim.SetTrigger ("PlayerDead");
        }
    }


    void Attack ()
    {
        timer = 0f;

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

I wszystko powinno iść zgodnie z planem, ale scena nie zapalała się na czerwono i nie tracę życia po dopadnieciu mnie przez zombi. Jedynie jestem przesuwany. Ustaliłem za pomocą Debug.Log, że nie dochodzi do kolizji jak trzeba. Minęły dwa wieczory i po porównywaniu ustawień, poleceń z tutoriala, odwiedzenia forum Unity, odkryłem w końcu, że collider-y na zombi mi się rozjechały. Kapsuła była za duża w stosunku do sfery. Jak poprawiłem, by obie miały tą samą średnicę (0.8), to nagle zaczęło wszystko działać!

Win2

Zgodnie z tym, jeśli nic nie robię, to zombi mnie dopada, wysysa stopniowo życie i podam na podłogę.

game3

Aby gra tak się zawsze nie kończyła, uzbroiłem się w broń, która może zabić zombi zanim będzie za blisko i zacznie mnie uśmiercać.

game4

Broń GunParticles trafia z Assets prefabs do GunBarrelEnd w obiekcie Player. Dodatkowo GunBarrelEnd wzbogacamy o obiekty LineRenderer (linia), Light (żółte światło), dźwięk wystrzału oraz skrypt PlayerShooting.

Win4

Ten ostatni odpowiada za oddanie strzału, gdy naciśniemy przycisk oznaczony jako “Fire1” (mapowany jako lewy Ctrl) i minie minimalny czas odstępu między wystrzałami:

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

        if(Input.GetButton ("Fire1") && timer >= timeBetweenBullets && Time.timeScale != 0)
        {
            Shoot ();
        }

        if(timer >= timeBetweenBullets * effectsDisplayTime)
        {
            DisableEffects ();
        }
    }


    public void DisableEffects ()
    {
        gunLine.enabled = false;
        gunLight.enabled = false;
    }


    void Shoot ()
    {
        timer = 0f;

        gunAudio.Play ();

        gunLight.enabled = true;

        gunParticles.Stop ();
        gunParticles.Play ();

        gunLine.enabled = true;
        gunLine.SetPosition (0, transform.position);

        shootRay.origin = transform.position;
        shootRay.direction = transform.forward;

        if(Physics.Raycast (shootRay, out shootHit, range, shootableMask))
        {
            EnemyHealth enemyHealth = shootHit.collider.GetComponent <EnemyHealth> ();
            if(enemyHealth != null)
            {
                enemyHealth.TakeDamage (damagePerShot, shootHit.point);
            }
            gunLine.SetPosition (1, shootHit.point);
        }
        else
        {
            gunLine.SetPosition (1, shootRay.origin + shootRay.direction * range);
        }
    }

Jeśli podczas strzelania trafione zostanie zombi, to jest mu odbierana część jego życia za pomocą funkcji TakeDamage z jego własnego skryptu EnemyHealth:

void Update ()
    {
        if(isSinking)
        {
            transform.Translate (-Vector3.up * sinkSpeed * Time.deltaTime);
        }
    }


    public void TakeDamage (int amount, Vector3 hitPoint)
    {
        if(isDead)
            return;

        enemyAudio.Play ();

        currentHealth -= amount;
           
        hitParticles.transform.position = hitPoint;
        hitParticles.Play();

        if(currentHealth <= 0)
        {
            Death ();
        }
    }


    void Death ()
    {
        isDead = true;

        capsuleCollider.isTrigger = true;

        anim.SetTrigger ("Dead");

        enemyAudio.clip = deathClip;
        enemyAudio.Play ();
    }


    public void StartSinking ()
    {
        GetComponent <UnityEngine.AI.NavMeshAgent> ().enabled = false;
        GetComponent <Rigidbody> ().isKinematic = true;
        isSinking = true;
        ScoreManager.score += scoreValue;
        Destroy (gameObject, 2f);
    }

Oczywiście jak skończy się życie dla zombi, to jest uśmiercane funkcją Death.

game5

Po śmiertelnym trafieniu zombi robi obrót do tyłu, jakby tonęło zanim ostatecznie upadnie na podłogę. Odpowiedzialna jest za to funkcja StartSinking, która pozbawia zombi przed śmiercią grawitacji, dolicza nam punkty za jego zabicie (statyczne pole w skrypcie ScoreManager przypiętym do ScoreText), a na końcu powoduje zniknięcie ciała (martwy obiekt zombi jest usuwany ze sceny). Wszystko super, ale co wywołuje StartSinking? Sprawcą jest czas podczas agonii wroga. W odpowiednim punkcie czasowym animacji Death modelu zombi w sekcji Events dodajemy wywołanie wspomnianej wcześniej funkcji.

image

Życie wroga uwzględniamy jeszcze modyfikując nieco skrypt EnemyMovement, ale to już drobniejsza rzecz, podobnie jak skrypt odświeżający zdobyte punkty.

Mam nadzieję, że udało mi się przedstawić esencję walki przy pomocy bronii palnej z zombi. Aktualny stan gry z tutoriala zapisałem sobie na github jako SurvivalShooter (Unity Tutorial). Póki co mam zawsze tylko jednego wroga, po jego zestrzeleniu dostaję 10 punktów i… potem mogę sobie nic nie robić na scenie, nic mnie już nie napadnie. Ta sielanka zostanie zburzona w następnym odcinku, a zatem do następnego razu.