sobota, 22 kwietnia 2017

[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.

Brak komentarzy: