Besplatan online tečaj UNITY – PISANJE PROGRAMSKOG KODA

Photo of author

By Nenad Crnko

U prethodna četiri nastavka uvodnog serijala o razvoju vlastitih aplikacija i drugih vrsta projekata u alatu, Unity prošli smo kroz različite dijelove razvojne okoline i tek se na ponekim mjestima dotaknuli programskog koda, koji se nalazi u pozadini cijele priče. U današnjem završnom tekstu iz ovog serijala, detaljnije ćemo demonstrirati pisanje programskog koda tako se u vlastitim projektima lakše snalazite i u korištenju skripti.

Illustration by Diana Hlevnjak on PolarVectors

Prvi dio

Drugi dio

Treći dio

Četvrti dio

Peti dio besplatnog online UNITY tečaja za programiranje igara

Do pratećeg programskog koda za određeni dio modela može se doći na različite načine. Jedan od načina je označavanja željenog objekta u razvojnoj okolini, nakon čega se  u dijaloškom okviru Inspector može kliknuti na neku od dostupnih skripti. Kao rezultat prethodne operacije u hijerarhijskom pregledu dijelova projekta automatski se otvara dio Scripts s pregledom svih skripti i automatskim označavanje relevantne skripte.

Pristup do skripte preko dijaloškog okvira Inspector.

Nakon klika na istaknutu skriptu u prozoru Inspector prikazuje se njezin sadržaj, kao što je to vidljivo na sljedećoj slici. Zapravo, ako želimo biti precizniji, kod velikih skripti vidi se samo “početni komad skripte”, ali je i to dovoljno za prepoznavanje o čemu se radi u određenoj skripti.

Pregled sadržaja izabrane skripte u razvojnoj okolini.

Za uređivanje programskog koda treba kliknuti na naziv skripte i onda iz padajućeg izbornika izabrati naredbu Open. Alternativno se to isto može napraviti klikom na gumb Open u dijaloškom okviru Inspector.

Otvaranje sadržaja skripte za uređivanje.

U oba slučaja u nastavku se otvara prozor s mogućnošću odabira programa za uređivanje izvornog koda.

Odabir alata za uređivanje programskog koda u skripti.

Za uređivanje izvornog koda možete koristi najobičniji Windows alat Notepad (Blok za pisanje), ali i neki drugi alat instaliran na računalo. Ako se bavite programiranjem, onda na računalu vjerojatno već imate instaliran neki od naprednijih alata za takvu namjenu, kao što su Microsoft Visual Studio ili Microsoft Visual Studio Code. Nema nikakvog razloga da ne koristite neki od alternativnih alata, jer tako imate na raspolaganju dodatne mogućnosti za pisanje koda i pronalaženje/rješavanje eventualnih pogrešaka. Kod svakog sljedećeg pokušaja uređivanje programskog koda, automatski će se ponoviti podrazumijevani odabir alata za njegovo uređivanje.

Na primjer, ako za uređivanje programskog koda izaberete Microsoftov općenamjenski alat za programere (Visual Studio Code – VSC), nakon učitavanja programskog koda skripte, automatski će biti prepoznate i označene sve naredbe programskog jezika C#. Da bi čitava stvar bila još ljepša, VSC omogućava preuzimanje nekoliko dodataka za pisanje Unity skripti (označenih na sljedećoj slici), nakon čega razvoj postaje još jednostavniji i udobniji.

Instalacija VSC dodataka za brže i udobnije uređivanje programskog koda.

Drugi način za pristup skriptama iz trenutno aktivnog Unity projekta je odabir naredbe Show in Explorer na opciji Scripts u okviru dijaloškom okvira Project. Na taj način automatski se pristupa mapi na disku gdje se nalaze sve pripadajuće Unity skripte.

Pristup programskom kodu preko mape u kojoj se nalaze sve skripte iz projekta.

Bez obzira na izabrani alat za uređivanje programskog koda, Unity preko odgovarajućih meta datoteka (smještenih u istoj mapi gdje se nalaze i datoteke s programskim kodom) zna kada su napravljene promjene u programskom kodu pa automatski izvodi sinhronizaciju prikaza u razvojnoj okolini.

Na primjer, ako je prema prethodnim slikama u VSC učitan sadržaj datoteke EnemyController.cs, u toj datoteci se nalazi sljedeći dio koda:

…
[Header("Parameters")]
[Tooltip("The Y height at which the enemy will be automatically killed (if it falls off of the level)")]
public float selfDestructYHeight = -20f;
[Tooltip("The distance at which the enemy considers that it has reached its current path destination point")]
public float pathReachingRadius = 2f;
[Tooltip("The speed at which the enemy rotates")]
public float orientationSpeed = 10f;
[Tooltip("Delay after death where the GameObject is destroyed (to allow for animation)")]
public float deathDuration = 0f;
…

Probajte sad u njemu izmijeniti vrijednosti -20f i 10f na neke druge vrijednosti – na primjer -25f i 15f, a onda u alatu VSC napravite spremanje novog sadržaja datoteke. Sad se vratite u razvojnu okolinu Unity (koje je već od prije aktivna u pozadini). Primijetit ćete da se nešto događa zbog prikaza odgovarajućeg oblika kursora, to jest da se automatski izvodi sinhronizacija izmjena u programskom kodu. Kao rezultat takve operacije u razvojnoj okolini vidjet ćete prikazano novo stanje programskog koda.

Prikaz novog stanja programskog koda nakon automatske sinhronizacije.

Kod svakog pisanja programskog koda prije ili poslije mogu se pojaviti pogreške. Što se točno događa u tom slučaju, to jest kako Unity reagira na neispravan programski kod? Najjednostavnije je to ponovo demonstrirati na primjeru.

U datoteci EnemyController.cs nalazi se i sljedeći dio programskog koda:

…
int m_PathDestinationNodeIndex;
EnemyManager m_EnemyManager;
ActorsManager m_ActorsManager;
Health m_Health;
Actor m_Actor;
Collider[] m_SelfColliders;
GameFlowManager m_GameFlowManager;
bool m_WasDamagedThisFrame;
float m_LastTimeWeaponSwapped = Mathf.NegativeInfinity;
int m_CurrentWeaponIndex;
WeaponController m_CurrentWeapon;
WeaponController[] m_Weapons;
NavigationModule m_NavigationModule;
…

Napravimo sad namjerno dvije greške u kodu, tako da u prva dva reda napravimo zamjenu ispravnog tipa varijable s nepostojećim tipom :

Int56 m_PathDestinationNodeIndex;
EnemyManagers m_EnemyManager;

Nakon spremanja promjena u alatu VSC i automatske sinhronizacije sadržaja datoteke u razvojnoj okolini, probajmo pokrenuti izvođenje modela. To više nije moguće, jer razvojna okolina javlja da u programskom kodu postoje pogreške koje treba ispraviti.

Primjer programskog koda s greškama.

Na samom dnu razvojne okoline u statusnom redu prikazuje se objašnjenje prve uočene pogreške s oznakom točnog mjesta gdje se nalazi greška. Ako u VSC-u sadržaj prvog reda ponovo vratimo u njegov početni oblik, nakon ponavljanja sinhronizacije prikazuje se obavijest o drugoj greški. Tek kad se i ona ispravi ponovo se može pokrenuti izvođenje modela učitanog u razvojnu okolinu.

Programski kod Unity skripti u pravilu je organiziran u klase koje su spremljene u odgovarajuće datoteke na disku. Pogledajmo ponovo na primjeru datoteke EnemyController.cs glavne dijelove klase. Zbog ograničenosti u prostoru nećemo prikazati cijeli sadržaj klase, nego samo njezine karakteristične dijelove.

Na samom početku navode se moduli neophodni za normalno izvođenje klase. U ovom primjeru to je dio:

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
using UnityEngine.Events;

[RequireComponent(typeof(Health), typeof(Actor), typeof(NavMeshAgent))]

Nakon toga slijedi deklaracija klase te deklaracija neophodnih struktura i varijabli (prikazan je samo dio originalnog koda) od kojih one zadužene za komunikaciju s okolinom mogu imati oznaku public.

public class EnemyController : MonoBehaviour
{
   [System.Serializable]
   public struct RendererIndexData
   {
        public Renderer renderer;
        public int materialIndex;

    public RendererIndexData(Renderer renderer, int index)
    {
        this.renderer = renderer;
        this.materialIndex = index;
    }
}

[Header("Parameters")]
[Tooltip("The Y height at which the enemy will be automatically killed (if it falls off of the level)")]
public float selfDestructYHeight = -25f;
[Tooltip("The distance at which the enemy considers that it has reached its current path destination point")]
public float pathReachingRadius = 2f;
[Tooltip("The speed at which the enemy rotates")]
public float orientationSpeed = 15f;
[Tooltip("Delay after death where the GameObject is destroyed (to allow for animation)")]
public float deathDuration = 0f;
…

public PatrolPath patrolPath { get; set; }
public GameObject knownDetectedTarget => m_DetectionModule.knownDetectedTarget;
public bool isTargetInAttackRange => m_DetectionModule.isTargetInAttackRange;
public bool isSeeingTarget => m_DetectionModule.isSeeingTarget;
public bool hadKnownTarget => m_DetectionModule.hadKnownTarget;
public NavMeshAgent m_NavMeshAgent { get; private set; }
public DetectionModule m_DetectionModule { get; private set; }

int m_PathDestinationNodeIndex;
EnemyManager m_EnemyManager;
ActorsManager m_ActorsManager;
Health m_Health;
Actor m_Actor;
Collider[] m_SelfColliders;
GameFlowManager m_GameFlowManager;
bool m_WasDamagedThisFrame;
float m_LastTimeWeaponSwapped = Mathf.NegativeInfinity;
int m_CurrentWeaponIndex;
WeaponController m_CurrentWeapon;
WeaponController[] m_Weapons;
NavigationModule m_NavigationModule;

Preostali dio klase sastoji se od funkcija koje ponovo mogu biti deklarirane tako da budu javno dostupne i drugim skriptama iz modela, ili da tako da mogu koristiti samo u okviru klase. Evo nekoliko primjera:

    void Start()
    {
        m_EnemyManager = FindObjectOfType<EnemyManager>();
        DebugUtility.HandleErrorIfNullFindObject<EnemyManager, EnemyController>(m_EnemyManager, this);

        m_ActorsManager = FindObjectOfType<ActorsManager>();
        DebugUtility.HandleErrorIfNullFindObject<ActorsManager, EnemyController>(m_ActorsManager, this);

        m_EnemyManager.RegisterEnemy(this);

        m_Health = GetComponent<Health>();
        DebugUtility.HandleErrorIfNullGetComponent<Health, EnemyController>(m_Health, this, gameObject);

        m_Actor = GetComponent<Actor>();
        DebugUtility.HandleErrorIfNullGetComponent<Actor, EnemyController>(m_Actor, this, gameObject);

        m_NavMeshAgent = GetComponent<NavMeshAgent>();
        m_SelfColliders = GetComponentsInChildren<Collider>();

        m_GameFlowManager = FindObjectOfType<GameFlowManager>();
        DebugUtility.HandleErrorIfNullFindObject<GameFlowManager, EnemyController>(m_GameFlowManager, this);

        // Subscribe to damage & death actions
        m_Health.onDie += OnDie;
        m_Health.onDamaged += OnDamaged;

        // Find and initialize all weapons
        FindAndInitializeAllWeapons();
        var weapon = GetCurrentWeapon();
        weapon.ShowWeapon(true);

        var detectionModules = GetComponentsInChildren<DetectionModule>();
        DebugUtility.HandleErrorIfNoComponentFound<DetectionModule, EnemyController>(detectionModules.Length, this, gameObject);
        DebugUtility.HandleWarningIfDuplicateObjects<DetectionModule, EnemyController>(detectionModules.Length, this, gameObject);
        // Initialize detection module
        m_DetectionModule = detectionModules[0];
        m_DetectionModule.onDetectedTarget += OnDetectedTarget;
        m_DetectionModule.onLostTarget += OnLostTarget;
        onAttack += m_DetectionModule.OnAttack;

        var navigationModules = GetComponentsInChildren<NavigationModule>();
        DebugUtility.HandleWarningIfDuplicateObjects<DetectionModule, EnemyController>(detectionModules.Length, this, gameObject);
        // Override navmesh agent data
        if (navigationModules.Length > 0)
        {
            m_NavigationModule = navigationModules[0];
            m_NavMeshAgent.speed = m_NavigationModule.moveSpeed;
            m_NavMeshAgent.angularSpeed = m_NavigationModule.angularSpeed;
            m_NavMeshAgent.acceleration = m_NavigationModule.acceleration;
        }

        foreach (var renderer in GetComponentsInChildren<Renderer>(true))
        {
            for (int i = 0; i < renderer.sharedMaterials.Length; i++)
            {
                if (renderer.sharedMaterials[i] == eyeColorMaterial)
                {
                    m_EyeRendererData = new RendererIndexData(renderer, i);
                }

                if (renderer.sharedMaterials[i] == bodyMaterial)
                {
                    m_BodyRenderers.Add(new RendererIndexData(renderer, i));
                }
            }
        }

        m_BodyFlashMaterialPropertyBlock = new MaterialPropertyBlock();

        // Check if we have an eye renderer for this enemy
        if (m_EyeRendererData.renderer != null)
        {
            m_EyeColorMaterialPropertyBlock = new MaterialPropertyBlock();
            m_EyeColorMaterialPropertyBlock.SetColor("_EmissionColor", defaultEyeColor);
            m_EyeRendererData.renderer.SetPropertyBlock(m_EyeColorMaterialPropertyBlock, m_EyeRendererData.materialIndex);
        }
    }

    void Update()
    {
        EnsureIsWithinLevelBounds();

        m_DetectionModule.HandleTargetDetection(m_Actor, m_SelfColliders);

        Color currentColor = onHitBodyGradient.Evaluate((Time.time - m_LastTimeDamaged) / flashOnHitDuration);
        m_BodyFlashMaterialPropertyBlock.SetColor("_EmissionColor", currentColor);
        foreach (var data in m_BodyRenderers)
        {
            data.renderer.SetPropertyBlock(m_BodyFlashMaterialPropertyBlock, data.materialIndex);
        }

        m_WasDamagedThisFrame = false;
    }

…
    public void OrientTowards(Vector3 lookPosition)
    {
        Vector3 lookDirection = Vector3.ProjectOnPlane(lookPosition - transform.position, Vector3.up).normalized;
        if (lookDirection.sqrMagnitude != 0f)
        {
            Quaternion targetRotation = Quaternion.LookRotation(lookDirection);
            transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, Time.deltaTime * orientationSpeed);
        }
    }
…

Neke od funkcija Unity izvodi automatski uvijek kad se za tim ukaže potreba. Na primjer, takve su funkcije Start koja se izvodi na samom početku svake klase (kako bi se inicijalizirale različite neophodne vrijednosti za funkcioniranje svake klase), ili funkcija Update zadužena za konstantno izvođenje dijelova koda (kako bi se ažurirao prikazani sadržaj odgovarajućeg dijela modela).

Nadamo se da vam je dosadašnjih 5 tekstova o sustavu Unity bar djelomično pokazalo njegovu snagu i mogućnosti te da ćete nakon ovih osnova sami nastaviti s dodatnim proširivanjem znanja.