22 Eylül 2016 Perşembe

Unreal Engine Programlama Temelleri

Unreal Engine programlama için bazı kavramları tanımlayarak işe başlayalım;

bir oyun projesinin pek çok unsurun bir araya gelerek oluştuğunu düşünürsek; örneğin görsel unsurların, seslerin ve kodların editöre import edilip paketlere ve haritalara kaydedilmesi, dahası başlangıçta yüklecek configlerin ayarlanması vs..  bunların hepsi bir araya gelerek bir Unreal Engine oyunun temelini oluşturmaktadır. 

GamePlay 
Burada GamePlay kavramı karşımıza çıkmakta. Çünkü tüm bu içerikleri yöneten kod sınıflarının oluşturduğu temel yapı kütüphanesine GamePlay modülü denmektedir.

Bir Unreal Engine oyunu bir veya daha çok GamePlay modülünü içerebilir. Bunlar ilgili class ların koleksiyonlarını tutan paketler gibi de düşünülebilir. Her oyun projesi malum en az bir GamePlay modülü içerir.

Unreal Engine GamePlay tümüyle C++ ile yazıldığından buradaki modüller, içeriklerde olduğu gibi paket dosyaları değil, DLL lerdir. 

Aşağıdaki resme bakarak olay daha net anlaşılabilir:



Modulename API 
Unreal Engine C++ kodlamasında önemli konulardan biri de makrolardır. Detayına daha sonra değinmeyi düşündüğüm makroların bu konu ile alakası, şayet bir modülün dış modüllere erişmesi gerekiyor ise fonksyonlar ve class ları  *_API macrosu ile belirtilmeli. (MODULENAME_API gibi.) Ancak bu şekilde belirtilen her öğe derleme süresini uzatır. Sadece ihtiyaç duyanların belirtildiğine emin olmalıyız. Örneğin bir sınıfın içinde sadece tek bir fonksyonun erişmesi gerekiyorsa tüm sınıfı işaretlemek yerine sadece o fonksyonu makro ile işaretlemeliyiz. Bu bize önemli miktarda derleme süresi kazancı sağlayacaktır.

Unreal Engine C++ ile ve tamamen OOP mantığına uygun kodlandığı için oyunu (GamePlay) oluşturan tüm sınıflar itinalı ve düzgün bir şekile, ayrı ayrı header dosyalarında tanımlanmalı, ve istenen kriterlerde işlevleri makrolar vasıtası ile motora bildirilmelidir.

En temel düzeyde bir Actor level içine konumlandırılabilecek herhangi bir GamePlay objesidir. Tüm actor ler AActor sınıfından türetilerek (miras alınarak) oluşur. AActor spawn edilebilen tüm gameplay objelerinin base class 'ıdır. Actor ler bir açıdan özel öbje tipi deposu olan Componenti oluşturan yapı taşı gibi düşünülebilir.

Component
Örneğin FOV (field of view) gibi bir kameranın fonksyonelliklerinin tümü CameraComponent de mevcuttur. Kendisi bir componenttir. Ve CameraComponent tüm kamera özelliklerini başka bir actor e vermek için ona dahil edilebilir, örneğin bir karaktere…



UObject 
Component ler farklı tipleri actorleri hareket ettirmede, render etmede, ve ila türlü çeşitli fonksyonellikleri yaratmak için kullanılırlar. Tüm objeler, componentler de dahil, tüm gameplay objeleri UObject temel sınıfından sınıfından türemiştir. Bunun anlamı, onlar doğrudan oyun ortamına instance edilemezler; mutlaka bir aktöre ait olmalıdır. 

Sahnedeki her actor veya obje bir sınıfın tek bir örneğidir. (instance). Bu aslında önemli bi konu. Çünkü C++ kod sınıfları bile Content Browser da bir ikon olarak görünürler. Ancak birini tutup sahneye bıraktığınız anda artık o sınıftan bir nesne tanımlanarak yani instance edilerek editöre, yani oyun ortamına eklenmiş olur.  

C++ kodu ile tüm obje ve actor lerin yeni genişletilmiş tipleri yaratılabilir. BluePrint sınıfları da yeni sınıflar yaratmamıza izin verir, yani actor ler. Ve BluePrint ile de aynı işlemi yapabilirsiniz; yani miras alıp genişletebilir yani extend edebilirsiniz. Ve hatta C++ ile yaratılmış yeni sınıfları BluePrint ile devralarak ikisini de kombine kullanabiliriz. 


Makrolar ile Konfigüre Edilebilir Sınıf Değişkenleri Yaratma
madem programlama terimlerinden söz ediyoruz, biraz da config dosyalarından bahsedelim.

Bir değişkeni config dosyalarından okunabilir şekilde ayarlamak için öncelikle bir sınıfın bu değişkeni içeriyor olması ve sınıf tanımının başındaki UCLASS makrosunda da Config belirteci olması gerekir:

UCLASS(Config=Game)
AExampleClass : public AActor

buradaki Game kelimesi kategori adıdır. başka bir isimde verebilirdik. Config belirteci ise bu sınıfın değişkenlerinin (variables) konfigürasyon dosyasında saklanacağını ve okunacağını belirler. Tüm kategoriler FConfigCache.ini dosyasında tanımlanmış durumdadır.
Özetle ve tekrarla: bir sınıfı Config belirteci ile dekore etmek sadece o sınıfın konfigurasyon dosyalarından okunabilen değişkenlere sahip olabileceğini ve bu ayarların hangi dosyalardan okunacağını gösterir. Özellikle belli bir değişkenin (variable), belli bir configürasyon dosyasından okunmasını / yazılmasını belirlemek için ise  UCLASS(Config=Game) belirtecine ilaveteb depşlken tanımının önüne  UPROPERTY(Config)  ifadesi belirtilmelidir.
UCLASS(Config=Game)
class AExampleClass : public AActor
{
GENERATED_UCLASS_BODY()

UPROPERTY(Config)
float ExampleVariable;
};

config dosyasının içinde ise durumlar şu şekilde:

[/Script/ModuleName.ExampleClass]
ExampleVariable=0.0f


Config Dosyalarında Kalıtım
UCLASS ve UPROPERTY config 'leri katılımla iletilir. Yani türetilmiş bir sınıf, parent sınıf içindeki tüm belirtilmiş değişkenlerini bir Config olarak okuyabilir ve dışa kaydedebilir; ve aynı configurasyon dosyası kategorisi içinde de olabilirler.

Aynı section içindeki değişkenler türetilmiş yani child class 'ın ismi altında birleşirler; örneğin ChildExampleClass için ayarları tutan bir configuration dosyamız olsun, bu class da ExampleClass adındaki bir base class dan türetilmiş olsun. ve ayarların game configuration dosyasındaki kayıt haki şöyledir :

[/Script/ModuleName.ChildExampleClass]
ExampleVariable=0.0f

Her Instance için Config:UE4 herhangi bir objenin herhangi istenen configirasyonun istenen configurasyon dosyasına kaydetme yeteneğine sahiptir. eğer PerObjectConfig belirteci UCLASS makrosu içinde kullanılmış ise bu sınıf için configurasyon bilgisi her bir instance için her bir instance in [ObjectName ClassName] şeklinde adlandırıldığı .ini içindeki section alanına kaydedilir. bu keyword türetilmiş sınıflara doğru yayılır.


Configuration dosya yapısı
her configuration kategorisi kendi ayrı dosya hiyerarşisine sahiptir. Bunlar engine-specific, project-specific, ve platform-specific durumlarında olabilir.


Configuration Kategorileri
  • Compat
  • DeviceProfiles
  • Editor
  • EditorGameAgnostic
  • EditorKeyBindings
  • EditorUserSettings
  • Engine
  • Game
  • Input
  • Lightmass
  • Scalability

Config Dosyaları Hiyerarşisi
Configuration dosya hiyerarşisi Base.ini ile okunur. Hiyerarşideki sonraki dosyaların değerleri öncekinin üzerine baskın gelerek (overriding) devam eder.

Proje özel ayarları proje dizinindeki dosyalarda olduğu sürece, Engine dizini altındaki tüm dosyalar tüm projelere uygulanır. Nihayetinde tüm projelerin özel farklılıkları ve platform farklılıkları [ProjectDirectory]/Saved/Config/[Platform]/[Category].ini dosyasına kaydolur.

aşağıda engine kategorisindeki konfigurasyon dosyaları için bir hiyerarşi örneği vardır:
  • Engine/Config/Base.ini
  • Base.ini is usually empty.
  • Engine/Config/BaseEngine.ini
  • Engine/Config/[Platform]/[Platform]Engine.ini
  • [ProjectDirectory]/Config/DefaultEngine.ini
  • [ProjectDirectory]/Config/[Platform]/[Platform]Engine.ini
  • [ProjectDirectory]/Saved/Config/[Platform]/Engine.ini
Saved dizini içindeki configuration dosyaları sadece proje özel ve platform özel farklılık ayarlarını tutar. (stack config içinde)


Configuration Dosyaları ile Çalışma 
Dosya formatı kısımlar (sections) ve Key-Value çiftleri şeklindedir. Tipik bir config dosyası section örneği:
[Section]
Key=Value
gibi...

Özel Karakterler:

+ - henüz bu özellik mevcut değil ise bir satır ekleme, (bir önceki veya daha önceki konfigurasyon dosyasından veya aynı dosyadan)

- - bir satırı kaldır. (ancak tam olarak uymak zorunda, aynı isim).

. - yeni bir property ekleme

! - bir property kaldırma. ama tam olarak aynısı olmak zorunda değil, sadece properti 'nin ismi.

. (nokta) tıpkı + gibidir. farklı olarak duplike bir satır ekleme potansiyeline sahiptir. bu DefaultInput.ini de gördüğün şekilde bağlamlarda (bindings) faydalıdır; örneğin, bottom-most bağlamının yeri effect alır, yani şöyle birşey eklersek :
[/Script/Engine.PlayerInput]
Bindings=(Name="Q",Command="Foo")
.Bindings=(Name="Q",Command="Bar")
.Bindings=(Name="Q",Command="Foo")
uygun gelen şekilde çalışacak. bir + kullanarak son satırın eklenmesi fail olacak, ve binding geçersiz olacak. configuration dosyasının kombine olmasından dolayı yukarıdaki kullanım mümkün olabilir.

Çoğu insan config dosyalarındaki noktalı virgülün açıklama satırı olduğunu düşünür, ama değildirler. Bu yaklaşım özellikle konmuştur. Teknik olarak her bir karakter farklı bir key - value çiftini temsil eder. Noktalı virgül de tipik olarak yeni bir satırın başladığının göstergesidir. Comment satırı dursa da tersine yeni satır başlangıcıdır. 


İtiraf ediyorum ki config dosyaları konusu sıkıcı bir konu. Detayını öğrenmek başlarda ne kadar gerekli bilmiyorum. Dahası siz editörden oyununuz için ayarlar ile oynadığınızda bu config dosyaları otomatik olarak yaratılıyor. Öyleyse niçin uğraşıyoruz dediğiniz duyar gibiyim... Çünkü bu dosyaların C++ sınıf değişkenlerini parametrik hale getirebiliyor olması, yani işin programlamasına dolaylı yoldan da olsa katılabiliyor olmaları (dinamik veri sağlayabilmeleri yönünden) ilgimizi çekiyor ve daha işin başında teorik olarak da olsa konu başlığı olmayı hak ediyor.



Blueprint Function Libraries
BluePrint ile derinlemesine uğraştıkça giderek daha yapısal yaklaşımlara ve kendi bazı hazır fonksyonlarımızı tanımlama ihtiyacı doğacaktır. Çoğu durumda belli bir state (durum) bağımlılığı gerektirmeyen ve yeniden kullanılarak işleri kolaylaştıracak bu fonksyonları bir araya getirmek isteyeceğiz. Nasıl ki normal programlamada her projede sıkça kullandığımız yardımcı kodlarımızı bir kütüphane haline getiriyor ve tekrar tekrar kullanıyor isek aynı şey BluePrint ortamında da geçerlidir. Bu durumda kendi Blueprint Function Library lerimizi yaratırız. Ve hatta bunu C++ kodu kullanarak da yapabiliriz!

Bu belirli bir oyun nesnesine bağlı olmayam çeşitli faydalı işler yapan static (!) fonksyonları içinde barındıran bir kütüphanedir. Belli mantıksal fonksyon set grupları gibi düşünülebilir, örneğin: AI Blueprint Library, veya, System Blueprint Library gibi...

UFUNCTION() makrosunu kullanarak BluePrint için fonksyon yaratmak çok kolaydır. Bir Actor den miras almak veya doğrudan UObject den almak yerine, tüm BluePrint kütüphaneleri UBlueprintFunctionLibrary den miras alır. Bunlar sadece static fonksyon içerebilirler.  Aşağıdaki kod bir kütüphane sınıfını ayarlamayı gösteren bi örnektir:

UCLASS()
class UAnalyticsBlueprintLibrary :
public UBlueprintFunctionLibrary
{
   GENERATED_UCLASS_BODY()
   /** Starts an analytics session without any custom attributes specified */
   UFUNCTION(BlueprintCallable, Category="Analytics")
   static bool StartSession();


gördüğnüz gibi, Blueprint Function Library si dolambaçlı / dolaylı (indirectly) olarak UCLASS() ve GENERATED_UCLASS_BODY() makroları ile yaratılmış bir UObject dir.

UFUNCTION() makrosu aynı zamanda BP ortamından çağrılaiblmesi için de gerekli tanımlamayı yapar. Blueprint Function Library içindeki fonksyonlar çağrının yan etkisinin olup olmamasına bağlı olarak BlueprintCallable veya BlueprintPure olarak tasarlanabilir.

detaylı kod için : /UE4/Engine/Plugins/Runtime/Analytics/AnalyticsBlueprintLibrary kısmına bakılabilir. burada StartSession() fonksyonunu verelim.
bool UAnalyticsBlueprintLibrary::StartSession()
{
    TSharedPtr<IAnalyticsProvider> Provider = FAnalytics::Get().GetDefaultConfiguredProvider();
    if (Provider.IsValid())
    {
        return Provider->StartSession();
    }
    else
    {
        UE_LOG(LogAnalyticsBPLib, Warning, TEXT("StartSession: Failed to get the default analytics provider. Double check your [Analytics] configuration in your INI"));
    }
    return false;
}

yukarıdaki kod UObject dışındaki bir tekil obje ile etkileşiyor. Bu BluePrint için veya UObject desteği olmayan C++ sınıfları ile etkileşmek için üçüncü party kütüphane fonksyonlarını ortaya çıkarmanın iyi bir yoludur.

aşağıdaki kod biraz genel bir işi yerine getiren Blueprint Function Library method örneğidir, bir actor için AIController bulunuyor:
AAIController* UAIBlueprintHelperLibrary::GetAIController(AActor* ControlledActor)
{
    APawn* AsPawn = Cast<APawn>(ControlledActor);
    if (AsPawn != nullptr)
    {
        return Cast<AAIController>(AsPawn->GetController());
    }
    return Cast<AAIController>(ControlledActor);
}
bu fonksyon çoklu BluePrint nodu olacak olanları alıyor, onu tekli noda yuvarlıyor. elbette bunun için BP ortamında bir fonksyon yapabilirdik, ama eğer sıklıkla çağrılacaksa C++ versiyonunu kullanmak çok daha iyi performans sağlayacaktır.


Evet arkadaşlar,
Bazı programlama teorik kavramlarına kısaca değindik. Devam eden Unreal Engine C++ yazılarında görüşmek üzere şimdilik hoşçakalın.

Yorum Gönder