Evet sevgili yazılımperver dostlarım, haftalık yazılarımıza devam ediyoruz. Geçen hafta SDL ve SDL2/SDL3 farklarına kısa bir bakış atmıştık.
Artık bütün bunları ete kemiğe büründürmenin vakti geldi. Bir süre önce SDL ile ilgili ne var ne yok diye internete baktığımda, SDL2’ye yönelik oldukça fazla kaynak mevcut fakat bunların çoğu genelde küçük küçük kabiliyetlere değinmiş ve uygulama seviyesinde çok bilgi vermiyor gibiydi. SDL3’e yönelik de o kadar çok kaynak olmasa da, durum benzer. Türkçe kaynağın da çok olmaması da ayrı bir konu. Bu sebeple bu ve birkaç yazımı SDL’e ayıracağım.
İçerik
Kapsam
Daha fazla uzatmadan, bu örneğin amacına değinelim. Bu örneğin amacı, 2025 ve sonrasında modern C++ kullanarak, görsel uygulamalar/oyunlar geliştirebilmeniz için SDL3’e yönelik bir ön izleme sunmak ve bilgilendirmede bulunmak.
Bunu da yaparken, modern C++ ve temel yazılım geliştirme pratikleri, tasarım örüntüleri ve sağlam bir başlangıç noktası oluşturmak olacak. Elbette, alıp hemen bütün ürünlerinizde kullanılabilecek düzeyde olmayabilir, bununla birlikte çok az bir uğraş ile bunu kullanabilir hale getirmenizi hedefliyorum. Bu noktada, okunabilirlik, esneklik ve performans hususları konusunda da bir denge gözetmeye çalışacağım.
Benzeri yazılarda, SDL’e yönelik ilave kabiliyetleri de bu örnek üzerine ekleyerek devam ediyor olacağız.
Haydi kurulum ile başlayalım.
Kurulum/Oluşturma
Bir önceki yazımda bahsettiğim gibi, SDL3 ile birlikte CMake desteği çok daha iyi bir noktaya gelmiş durumda. Bununla birlikte, tek bir yerde birden fazla platform için kurulum yine de zahmetli olabiliyor. Onun için hem Linux hem de Windows için ihtiyaç duyacağınız adımları bu başlık altına ekleyeceğim. SDL2 için gerekli adımları merak ederseniz, uEngine4 reposu içerisinde hem Windows hem de Linux için gerekli adım ve betikleri bulabilirsiniz. Şimdi SDL3’e bakalım.
Öncelikle Linux’e bakalım. Temelde ilgili repoyu indirip, derlemek dışında yapmanız gereken bir şey yok:
1 2 3 4 5 6 7 |
# SDL Kütüphanesi wget https://github.com/libsdl-org/SDL/releases/download/release-3.2.14/SDL3-3.2.14.tar.gz tar xvf SDL3-3.2.14.tar.gz cd SDL3-3.2.14 cmake -S . -B build cmake --build build sudo cmake --install build --prefix /usr/local |
Windows için de benzer adımları izleyebilirsiniz.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# SDL Kütüphanesi curl.exe -L --ssl-revoke-best-effort https://github.com/libsdl-org/SDL/releases/download/release-3.2.14/SDL3-3.2.14.zip --output SDL3-3.2.14.zip tar -xvf SDL3-3.2.14.zip cd SDL3-3.2.14 # VS2022 dışında bir visual studio var ise aşağıda ilgili komutu çağırmalısınız call "C:\Program Files\Microsoft Visual Studio\2022\Professional\VC\Auxiliary\Build\vcvars64.bat" # Ninja'yı path'e eklediğinizden emin olun cmake -B build_ninja -G "Ninja Multi-Config" -DBUILD_STATIC_LIBS=ON cmake --build build_ninja --config Release cmake --install build_ninja --config Release --prefix C:/sdl3 cmake --build build_ninja --config Debug cmake --install build_ninja --config Debug --prefix C:/sdl3 |
Evet kurulumları gerçekleştirdikten sonra, örnek uygulamamıza göz atmaya başlayabiliriz. Yukarıdaki adımlara yönelik de betikleri en kısa sürede ekleyeceğim.
Temel Kabiliyetler
Şimdi projemizdeki önemli sınıflara ve bunları geliştirirken izlediğimiz ve kullandığımız yaklaşımlara göz atalım. Bunu yapmadan önce aşağıdaki adresten kodu indirmeyi unutmayın. Şu an kod hem windows hem de linux için derlenebiliyor (derlenmiyorsa, bana ulaşabilirsiniz).
https://github.com/yazilimperver/cpp-playground
Kaynak Yönetimi (RAII Kullanılarak)
Öncelikli olarak SDL kaynak yönetimine bakıyor olacağız. Burada ve benzer birçok yerde RAII prensibini kullanabilirsiniz. Aslında bakarsanız, benzer yaklaşımı OpenGL ve benzeri kütüphaneler için de kolayca kullanabilirsiniz. Peki RAII nedir?
SDL gibi kütüphaneler yanında C++’ta da bellek, dosya tanıtıcıları, mutex ve benzeri kaynakların yönetimi ve ne zaman silineceğini takip etmek zorlu olabilir. İşte bu noktada, RAII (Resource Acquisition Is Initialization) prensibi ile kaynak yönetimini hem güvenli hem de otomatik hale getirerek, bu karmaşıklığı büyük ölçüde ortadan kaldırabiliriz. RAII’nin temel fikri, bir kaynağın ömrünü bir nesnenin ömrü ile otomatik olarak hizalamak/bağlamaktır. Şöyle ki; bir nesne oluşturulduğunda kaynak elde edilir, nesne yok edildiğinde ise kaynak otomatik olarak serbest bırakılır/silinir. Aşağıda çok basit bir şekilde bu gösterilmiştir:
Modern C++’ta std::unique_ptr
, std::shared_ptr
, std::lock_guard
ve std::fstream
gibi sınıflar da RAII prensibini takip ederler. Örneğin std::lock_guard
, bir mutex’i kilitleyip otomatik olarak serbest bırakırken, std::unique_ptr
dinamik belleği güvenli bir şekilde yönetir. Bu sayede delete
veya unlock
gibi çağrıları manuel olarak yapmanıza gerek kalmaz, bu da bellek sızıntılarını ve “race condition” gibi durumları önlemeye yardımcı olur.
RAII prensibi sadece daha temiz ve okunabilir kod üretmekle kalmaz, aynı zamanda hata yönetimini kolaylaştırır. Bu yaklaşım özellikle grafik ve büyük çaplı uygulamalarda, kaynak yönetimi için de oldukça önemlidir. Örnek uygulamamızda ise RAII’yi SDL kaynak sınıfları için kullanacağız. SDLResource sınıfını da tam olarak bu amaçla geliştirdik. Bu sayede;
- Otomatik temizlik: Destructor’da kaynak otomatik olarak SDL API’leri kullanılarak serbest bırakılır,
- Modern C++: Move semantics ile kopyalamanın önüne geçiyoruz,
- Tip Güvenliği: Template ile derleme zamanında tip kontrolü yapabiliyoruz.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
template<typename T, void(*Deleter)(T*)> class SDLResource { private: T* mResource; public: explicit SDLResource(T* resource = nullptr) : mResource(resource) {} ~SDLResource() { if (mResource) { Deleter(mResource); } } // Move semantics ve diğer API'ler }; // SDLWindow, Renderer ve Texture için kullanılabilecek olan tanımlamalarımız using SDLWindow = SDLResource<SDL_Window, SDL_DestroyWindow>; using SDLRenderer = SDLResource<SDL_Renderer, SDL_DestroyRenderer>; using SDLTexture = SDLResource<SDL_Texture, SDL_DestroyTexture>; |
Olay (Event) Yönetimi (Observer Pattern’i Kullanılarak)
SDL kütüphanesi ve benzeri bir çok işletim sistemi ve pencere soyutlama kütüphanesi bir şekilde etkileşimleri size sunmak için gerekli API’leri sağlarlar. SDL kapsamında da benzer bir mekanizma mevcut.
Biz de bu örüntüyü SDL olaylarının ilgili uygulama sınıflarına geçirilmesinde kullanacağız. Peki bu bize ne sağlıyor?
- Düşük Bağımlılık (“Loose coupling”): Olayı tetikleyen ile bunu tüketenler arasından bağımsızlık,
- Genişleyebilirlik (“Extensibility”): Yeni dinleyiciler kolay eklenebilmekte,
- Konuların Ayrılması (“Separation of concerns”): Farklı farklı mekanizmaların birbirlerinden ayrılabilmesi (UI, Girdi/Çıktı, vs). Bunların her bir farklı bir dinleyici olabilir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class EventObserver { public: virtual ~EventObserver() = default; virtual void OnEvent(const SDL_Event& event) = 0; }; class EventSubject { private: std::vector<EventObserver*> mObservers; public: void AddObserver(EventObserver* observer); void RemoveObserver(EventObserver* observer); void NotifyObservers(const SDL_Event& event); }; |
SDL’e yönelik olayları dinlemek isteyen sınıflar (ki örneğimizde Sdl3Application sınıfı), EventObserver arayüzünü gerçekleyerek, olaylardan OnEvent API’si ile haberdar olabilmektedir. Dinlemek için de EventSubject sınıfının bir nesnesini uygulama sınıfı içerisinde oluşturup, sınıfın kendisini dinleyici olarak ekliyoruz.
Daha karmaşık uygulamalarda, sadece ilgili sınıfları da buraya dinleyici olarak ekleyebiliriz. İlaveten, uEngine4’teki gibi, olay tiplerine göre de özelleştirmeye gidilebilir. Örneği çok karmaşıklaştırmamak adına şu an bu şekilde ilerliyor olacağız. Sdl3Application sınıfı içerisindeki, OnEvent fonksiyonu içerisinde de, bu olayların tipine göre (event.type, SDL_EVENT_*), gerekli işlemlerin yapıldığını görebilirsiniz.
SDL_Event detayları için https://wiki.libsdl.org/SDL3/SDL_Event adresine göz atabilirsiniz.
Görsel Nesneler (Bileşen Örüntüsü) ve Bunların Oluşturulması (Fabrika Örüntüsü)
Bu başlığımızda ise, basit görsel bileşenleri nasıl oluşturacağımıza bakacağız. uEngine4 ve QT tarzı kütüphaneler bu amaçlar “Painter” dediğimiz ve temel çizim kabiliyetlerini içeren API’ler sunmaktadırlar. Oyunlarda ise son zamanlar, ECS (Entity Component System) denilen ve performansı da öne çıkaran yaklaşımlar yaygın olarak kullanılmakta. Biz ise örneğimizde, miras tabanlı bir bileşen yaklaşımı izleyeceğiz. Hemen örnek bir kod parçası ile başlayalım:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
class Component { public: GraphicalObject* mOwner = nullptr; virtual ~Component() = default; virtual void Update(float deltaTime) {} virtual void Render(Renderer& renderer) {} }; class Transform : public Component { public: float mX = 0.0F; float mY = 0.0F; float mRotation = 0.0F; float mScaleX = 1.0F; float mScaleY = 1.0F; ... }; class Velocity : public Component { public: float mVx = 0.0F; float mVy = 0.0F; ... }; class GraphicalObject { private: std::vector<std::unique_ptr<Component>> mComponents; public: template<typename T, typename... Args> T* AddComponent(Args&&... args) { ... } template<typename T> T* GetComponent() const { ... } }; |
Burada her bir görsel nesnemiz için temsili olarka GraphicalObject sınıfımız bulunmakta, daha sonra bu nesnelerimize kullanımına göre Component arayüz sınıfından türetilen bileşenleri (Transform, Velocity ve RendererComponent) ekleyerek davranışını belirleyebiliyoruz. Daha önce de ifade ettiğim gibi burada miras tabanlı bir yaklaşım izliyoruz ve Nesne Yönelimli Yazılım geliştirme pratiklerini uyguluyoruz. Bu da temel uygulamalarınız için yeterli olacaktır.
Peki bu nesneleri nasıl oluşturabiliriz?
Bunun için de yine çok bilindik bir tasarım örüntüsü olan Fabrika Örüntüsünü kullanacağız. Bu örüntüyü özellikle nesne oluşturma işlemlerinin kontrolünü soyutlamak ve genişleyebilirliği yönetmek için kullanıyoruz. Burada temel olarak, ilgili sınıfı direk oluşturmak yerine, bir fabrika sınıfı marifeti ile ilgili sınıfları oluşturmaya dayanıyor. Burada da, modern C++ ile gelen akıllı işaretçileri ve yukarıda bahsettiğim, bileşen tabanlı yaklaşımı uygulayabiliriz. Hemen ilgili sınıfımıza göz atalım:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class GraphicalObjectFactory { public: static std::unique_ptr<GraphicalObject> CreateRectangle(float x, float y) { auto rectangleObj = std::make_unique<GraphicalObject>(); rectangleObj->AddComponent<Transform>(x, y); rectangleObj->AddComponent<Velocity>(0.0f, 0.0f); auto renderStrategy = std::make_unique<RectangleRenderer>( SDL_Color{0, 255, 0, 255}, 50, 50); rectangleObj->AddComponent<RenderComponent>(std::move(renderStrategy)); return rectangleObj; } static std::unique_ptr<GraphicalObject> CreateCircle(float x, float y){ ... } static std::unique_ptr<GraphicalObject> CreateTriangle(float x, float y){ ... } }; |
Yukarıdaki fabrika sınıfı ile dikdörtgen, daire ve üçgen şekiller oluşturabiliyoruz. İlgili nesneleri de Sdl3Application::Initialize metodu içerisinde oluşturuyoruz.
Fabrika örüntüsünün faydalarına bakacak olursak:
- Enkapsülasyon (Encapsulation): Nesne oluşturma mantığı bir yerde,
- Tutarlılık: Aynı tip nesneler hep aynı şekilde yaratılmakta,
- Uyarlama: Örnekte böyle değil ama, farklı konfigürasyon dosyası ya da girdileri ile nesne oluşturma uyarlanabilir.
Sonraki Adımlar
Evet sevgili yazılımperver dostlarım. Bu yazımın sonuna geldik ama uygulamaya yönelik söyleyeceklerim henüz bitmedi. Bir sonraki yazımda, bu geometrik nesneleri nasıl görselleştireceğimize, uygulamanın nihai haline ve bir araya getirilmesine bakıyor olacağız. O zaman kadar kendinize iyi bakın ve repodaki uygulamayı incelemeyi ve çalıştırmayı unutmayalım.
Kaynaklar
- https://en.wikipedia.org/wiki/Resource_acquisition_is_initialization#:~:text=Resource%20acquisition%20is%20initialization%20(RAII,is%20tied%20to%20object%20lifetime.
- https://en.cppreference.com/w/cpp/language/raii.html
- https://www.youtube.com/watch?v=Rfu06XAhx90
- https://refactoring.guru/design-patterns/observer/cpp/example
- https://gameprogrammingpatterns.com/