Merhaba sevgili yazılımperver dostlarım, uzun süredir devam eden C++ yazılarımızla C++ 17 ile birlikte gelen birçok özelliğe bakarak birlikte keşfettik, bundan sonra da C++ 20 ile gelen özelliklere bakıyor olacağız. Bunun yanında, bir süre önce de sizlere duyurusunu yaptığım, hatta örnek bir uygulamayı sizlerle paylaştığım uEngine4 de artık yumurtadan çıktı.
uEngine4 – Matrix, Düşen Harf Uygulaması
Özellikle etkileşimli basit oyun, görselleştirme ve harita/CBS (Coğrafi Bilgi Sistemi) çoklu platform uygulamaları için kullanabileceğiniz kütüphanem kullanılabilecek seviyeye geldi. Elbette daha önünde uzun bir yolu var ama bu yolu hep birlikte katediyor olacağız.
İçerik
Amaç
Öncelikle amacımıza tekrar bir göz atalım. uEngine4, www.yazilimperver.com sayfasında paylaştığım konuları içeren, C++ ile görselleştirme, 2B grafik, oyun ve harita benzeri etkileşimli uygulamalar geliştirmek isteyenlere yol göstermek, ilk adım olmak ve fikir vermek için geliştirilen bir kütüphane.
Daha önce, OpenGL, SFML, SDL2 derken birçok C++ tabanlı görselleştirme kütüphanesini, çoklu platformlar için görsel uygulamalar geliştirmede kullandım. Hatta, benzerlerini de profesyonel hayatta geliştirdim. Şu bir gerçek ki, bu işlere yeni başlayan arkadaşlar için, bu kütüphanelerin sunulan API’lerini anlayıp, zihinlerindeki projeleri hemen hayata geçirmeleri özellikle OpenGL ve SDL kullanılması durumunda biraz vakit alabiliyor, SFML belki diğerlerinden bir tık önde olabilir. Bu kütüphane ile birlikte bu ilk eşiği kolay bir şekilde atlatıp, Godot ve benzeri üst seviye bir araç da kullanmadan ama çok da alt seviyeye inmeden ve tabi ki C++ kullanarak bu ve benzeri uygulamaları geliştirmenize yardımcı olmasını umut ediyorum.
Öncelikle, motorun kaynak kodlarına, örnek uygulamalara ve dokümanlara aşağıdaki adresten ulaşabilirsiniz. Bu sebeple bu adresi hemen, sık kullanılanlar ve takip edilenler arasına ekliyoruz 🙂
https://github.com/yazilimperver/uEngine4
Genel Tasarım
Genel tasarım olarak aslında, kabiliyetleri büyük bir proje içerisinde koymaktansa, küçük küçük projelere bölmeyi tercih ettim (en azından şimdilik). Her ne kadar bu, idame işini biraz zorlaştırmış gibi görünse de uzun vadede hem okunabilirlik hem de kullanımı kolaylaştırdığını düşünüyorum. Uygulama dışındaki bütün kabiliyetleri, statik kütüphane olarak oluşturdum. İlk başta VS2022 projesi oluşturup onun üzerinden gitsem de, sonradan artık tamamen CMake kullanımına döndüm (Vs2022’yi de ara ara güncelleyeceğim).
Kaynak kodlar, “code/src” altında konuşlandırılmış durumda. Diğer konuşlandırmalar için “doc/projectRepository.md” altında verilmiş durumda.
Üçüncü parti kütüphaneler “code/src/ext” altında konuşlanmış durumda. Bunlar açıkçası daha çok VS2022 için ayarlanmış olduğu için tam anlamıyla CMake kullanımına uygun değil ama önümüzdeki yazılarda bunları da olması gerektiği hale getireceğiz.
Kodlama standardı olarak da Google C++ kılavuzunu temelde baz aldım, detayları “doc/codingGuideline.md” altında özetlemeye çalıştım.
C++ olarak, C++ 20 dahil en son özellikleri kullanmayı planlıyorum. Birim test şu an çok olmasa da, birim testleri de peyderpey ekleyeceğiz, ayrıca GitHub Actions’ı da kullanıma almayı planlıyorum.
Son olarak, çeşitli sebeplerle dahil ettiğim ya da yazdığımı küçük, büyük yardımcı sınıfları da bu repo altında topluyor olacağım (zamanlama, aktif nesneler, thread, vb.).
Şimdi biraz daha bileşenlere eğilmeye başlayalım ve bu yazımızın temelini oluşturacak olan “Painter” kütüphanelerine bakalım.
“Painter” Bileşeni
“Painter” kabiliyetinin temel çıkış noktası, 2B grafiksel çizimler için, basit ve hızlı bir şekilde kullanıma uygun bir çerçeve sunmaktı. Bunun için de uzun süre kullandığım Qt’de bulunan ve bu amaçla geliştirilmiş olan QPainter sınıfını referans aldım. QPainter temelde, grafiksel kullanıcı arayüzü uygulamalarının ihtiyacı olan çizimleri, optimize ve platform bağımsız bir şekilde çizilmesine olanak sağlayan bir sınıf. Oldukça çeşitli çizim kabiliyetleri sunuyor. Detaylar için https://doc.qt.io/qt-6/qpainter.html#details adresine bir göz atabilirsiniz.
Peki uEngine4’te sunulan (ve sunulması planlanan) kabiliyetler neler:
- Temel geometrik nesnelerin çizilmesi,
- Doku/resim çizimleri,
- Metin çizimleri,
- Temel transformasyonlar,
- Bütün bunların kullanılan platform bağımlı kısımlarından, kullanıcı sınıfları soyutlamak.
İlk olarak SDL2 kullanarak bu sınıfı geliştirdim (basic_engine/painter). Bu sınıf tamamen SDL2 ve ilgili 3. parti kütüphaneleri kullanıyor ve temelde yukarıda saydığım kabiliyetlerin tamamı sağlanmış durumda. Bu sebeple, çoklu platform temel uygulamalar için ihtiyacı büyük miktarda görecektir. Bununla birlikte, SDL2’den gelen bir takım ufak tefek kısıtları da ne yazık ki bünyesinde barındırıyor (bütün sahneye ilişkin transformasyonların yapılamaması (Ör. döndürme), çizgi stillerinin direk sunulmaması (noktalı, çizgili vb.).
Bu sebeple, kolları sıvazlayıp, bu sınıfın bir de OpenGL kullanan halini de kütüphaneye ekledim (gl/gl_painter). Şu an için halen 1.x’ten gelen bir takım sabit pipeline (“fixed pipeline”) API’leri olsa da, daha modern OpenGL kullanan bir hale de ihtiyaç halinde dönüştürülebilir ama mevcut hali de bir çok ihtiyacı göreceğini düşünüyorum. Doku/resim çizimi ve metin gösterimlerine ilişkin API’ler eksik, bunlara yönelik önceki çalışmalarımda kullandığım bitmap font ya da freefont kullanımına ilişkin kabiliyetleri de dahil etmek için geliştirme çalışmalarım devam ediyor. İnşallah, yakın zamanda, onları da ekliyor olacağım. Şimdi gelelim Painter’a ilişkin temel kavramlara değinmeye.
Pen/Brush
Painter sınıflarını kullanırken en çok başvuracağımız ve önemli olan iki kavram: kalem ve fırça (“Pen, Brush”) kabiliyetleri. QPainter aracılığı ile çizilen her bir şekil, bu nesneler aracılığı ile belirlenmekte.
Painter’da aynı anda aktif tek bir kalem ve fırça stili bulunabilir. Ayarlanan son kalem ve fırça, sonraki çizilen bütün çizimleri etkiler. Kalem temel olarak çizilecek olan çizgisel unsurları etkilerken (çizgi, poligon’un dış hatları, metinler), fırça ise daha çok dolgusal unsurların çizimlerini etkiler (poligonun, dikdörtgenin iç dolgusu, vb.).
Peki, ne tarz çizim özelliklerini değiştirebiliyoruz? Hemen bakalım:
Kalem için:
- Renk, genişlik,
- “Stroke” rengi ve genişliği
- “Stroke” çizginin arka planında bulunan ve sınırlarını göstermek için kullanılan biçimleri tarifler,
- Bunun genişliğini 2’den küçük vermeniz halinde çizilmez.
- Çizgi ve “Stroke” stili
- Bu kabiliyet şimdilik sadece GL painter tarafından sunuluyor ve glLineStipple ile gerçekleniyor,
- Aktif kalemi ayarlamak için AssignPen ve mevcut kalemi almak için de ActivePen API’leri kullanılabilir.
Örneğin aşağıda, sarı renk ve siyah “stroke” rengi ile çizilen bir çizgi verilmiştir. Tahmin edebileceğiniz üzere, temelde yapılan aslında iki kere çizim yapmak.
Fırça için:
- Renk,
- Fırça Stili,
- Bu kabiliyet şimdilik sadece GL Painter tarafından sunuluyor ve glPolygonStipple ile gerçekleniyor,
- Aktif fırçayı ayarlamak için AssignBrush ve mevcut kalemi almak için de ActiveBrush API’leri kullanılabilir.
Örneğin aşağıda, “stroke” ve dolgu ayarları ile çizdirilmiş basit geometrileri görebilirsiniz:
Çizim Kabiliyetleri
Kalem ve fırça kavramlarına baktıktan sonra, bu gençleri kullanarak neler çizebiliriz ona bakalım. Şimdilik ağırlığı SDL Painter’a vereceğim.
- Nokta ile başlayalım. Bu amaçla DrawPoint(const Point2d& point) API’sini kullanabilirsiniz (stil için kalem kullanır),
- Çizgi çizimi için, DrawLine(const Point2d& start, const Point2d& end) API’sini kullanabilirsiniz (stil için kalem kullanır),
- Dikdörtgen çizimleri için;
- DrawRectangle(const SDL_Rect& rect), bu API ile normal dikdörtgenleri çizdirebilirsiniz (stil için hem kalem hem de fırça kullanılıyor),
- DrawRRectangle(const SDL_Rect& rect, int16_t radius), bu API ile yumuşak köşeli dikdörtgenleri çizdirebilirsiniz (stil için hem kalem hem de fırça kullanılıyor). Yuvarlaklık, radius ile verilmekte,
- Poligon çizimleri için;
- DrawPolygon(const Point2d* polygon, uint32_t vertexCount), bu API ile poligonlar çizdirebilirsiniz (stil için hem kalem hem de fırça kullanılıyor),
- DrawPolyline(const Point2d* polygon, uint32_t vertexCount), bu API ile içleri boş olan poligon ya da çoklu çizgiler çizdirebilirsiniz (stil için kalem kullanır),
- Elips/Çember çizimleri için:
- DrawEllipse(const Point2d& point, int16_t radiusX, int16_t radiusY), bu API ile elips çizebilirsiniz (stil için hem kalem hem de fırça kullanılıyor),
- Çember için x/y eksenine aynı değerleri girmeniz yeterli.
- DrawEllipse(const Point2d& point, int16_t radiusX, int16_t radiusY), bu API ile elips çizebilirsiniz (stil için hem kalem hem de fırça kullanılıyor),
- Resim çizimleri için:
- Şu an bu amaçla oldukça fazla API sunuluyor ama temelde iki grup API var: SDL_Texture kullanan ve Sprite nesnesi kullanan.
- DrawTexture(SDL_Texture* texture, …), SDL_Texture* alan API’ler temelde, SDL2 tarafından sunulan, SDL_Texture nesnelerini çizdirmek için kullanılabilir. API’ler resimleri döndürmek için gerekli açıyı, resim genişlik ve yükseklik bilgilerini alan API’ler sunulmakta,
- DrawSprite(const Sprite* sprite), Sprite alan API’ler temelde, oyunlar için resimleri temsil etmek için tanımlanan “Sprite”‘ları çizdirmek için sunduğum Sprite sınıfını kullanmakta. Burada detaylarına girmeyeceğim ama temelde bir önceki türe benzer çıktı sağlasa da, bu sınıfı kullananların işini kolaylaştırmakta,
- Şu an bu amaçla oldukça fazla API sunuluyor ama temelde iki grup API var: SDL_Texture kullanan ve Sprite nesnesi kullanan.
Evet dostlar temel olarak elimizdeki çizim API’leri bunlar. Kodu incelediğinizde, arka planda özellikle geometrik çizimler için gfx_primitives
kütüphanesinin kullanıldığını görebilirsiniz. Bu kütüphane zamanında geliştirilen ve bildiğim kadarıyla artık idame edilmeyen bir kütüphane. İlgili dosyaları, projeye direk dahil ettim, çünkü bir takım güncelleme yapma ihtiyacı da doğdu açıkçası.
Metin Kabiliyetleri
Şimdi gelelim metin çizim kabiliyetlerine. OpenGL ile çalışan takipçilerim, metin/font çizimleri için çok farklı yöntemlerin olduğunu bilecekler. Bu kütüphane için amacım aslında temel metin gösterimini kolay bir şekilde sunmak. Açık konuşmak gerekirse, SDL bu konuda OpenGL’e göre daha fazla seçenek sunuyor gibi (tabi şimdilik :). Metin çizdirmek için de hem “gfx_primitives” hem de SDL_FontCache kütüphanelerini kullandım. İlgili basit font çizimleri için ikincisi ise biraz daha kabiliyetli metin çizimleri için.
GL Painter sınıfımızın da henüz bir GL Font yeteneği bulunmuyor ama bu yakın amanda değişecek inşallah.
Hadi mevcut API’lere bir göz atalım. Resim çizimlerinde olduğu gibi metin çizimleri için de iki grup metin çizim API’si bulunmakta, bunları Basit ve İleri seviye olarak ikiye gruplandırabiliriz. Bunların temel farklılıkları, sunduğu kabiliyetler:
Basit Font Çizimi
- Bu API’ler hızlıca metin çizdirmek için, .fnt uzantılı fontları kullanır. Bu fontlara etiket vererek atıfta bulunuyoruz “fontLabel”,
- Metin çizdirmeden önce en az bir fontu kaydetmeniz gerekiyor. Bu amaçla da RegisterBasicFont API’sini kullanabilirsiniz. Aşağıda buna ilişkin örnek bir kod parçası bulabilirsiniz. Bunu uygulamanın başında bir kere yapmanız yeterli. Son iki parametre, karakter genişlik ve yüksekliğini ifade eder,
1 2 |
mPainter.RegisterBasicFont("Font_7_13_Bold", "./7x13B.fnt", 7, 13); mPainter.RegisterBasicFont("Font_10_20", "./10x20.fnt", 10, 20); |
- Uygulama içerisinde, metin çizdirmeden önce de, ilgili fontlar, font etiketleri verilerek SetActiveBasicFont ile Painter nesnesine atanmalıdır,
- Çizim için ise void BasicText(const Point2d& upperLeft, std::string_view text) API’si kullanılabilir. İlk parametre, metin konumu (metnin sol üst köşesi), ikincisi ise de çizdirilecek olan metindir.
- Aşağıda bu API’leri nasıl kullanabileceğinize ilişkin örnek kullanımları bulabilirsiniz:
1 2 3 4 5 6 7 8 9 10 11 12 |
mPainter.AssignPen(Pen{ Color::Black }); mPainter.SetActiveBasicFont("Font_7_13_Bold"); mPainter.BasicText(screenCenter + Point2d{ -300, -240 }, "[Basit] Merhaba Dunya_Siyah"); mPainter.SetActiveBasicFont("Font_10_20"); std::string text{ "Merhaba Dunya_Merkez" }; auto fontSizeInfo = mPainter.ActiveBasicFontSizeInfo("Font_10_20"); mPainter.BasicText(screenCenter - Point2d{ text.size() /2 * fontSizeInfo.first, fontSizeInfo.second/2 }, "[Basit] Merhaba Dunya_Merkez"); mPainter.SetActiveBasicFont("Font_7_13_Bold"); mPainter.AssignPen(Pen{ Color::Red }); mPainter.BasicText(screenCenter + Point2d{ +110, +220 }, "[Basit] Merhaba Dunya_Kirmazi"); |
İleri Font Çizimi
- Bu API’ler daha can canlı metinler çizdirmek için, .ttf uzantılı fontları kullanır,
- Bir önceki grup API’de olduğu gibi, fontun kaydedilmesi gerekir. Bunun için RegisterFont API’si kullanılabilir. Buna ilişkin örnekleri aşağıda görebilirsiniz. Bir önceki gruptan farklı olarak burada, italik, bold gibi özellikleri/nitelikleri de verebildiğimizi görebilirsiniz
1 2 3 4 5 6 7 8 |
// 10 olcekli, Bold ve Italik font kaydedelim mPainter.RegisterFont("FreeSans_Italic_10", "fonts/FreeSans.ttf", 10, Painter::FontStyle::Bold | Painter::FontStyle::Italic); // Aynı fontun 20 olcekli, normal halini kaydedelim mPainter.RegisterFont("FreeSans_Normal_20", "fonts/FreeSans.ttf", 20); // Aynı fontun 12 olcekli, Bold ve Italik halini kaydedelim mPainter.RegisterFont("FreeSans_ItalicBold_12", "fonts/FreeSans.ttf", 12, Painter::FontStyle::Bold | Painter::FontStyle::Italic); |
- Kullanmadan önce SetActiveBasicFont ile Painter nesnesine atanmalıdır,
- Çizim için ise iki farklı API kullanılabilir. Bunlardan ilki, atanan fonta göre metni verilecek konumu referans alarak ve seçilecek hizalamaya göre çizer. İkinci API’si bunların yanında, metni bir sütuna sığdırarak çizdirmenize olanak sağlar ki bunun da birçok kullanım alanını bulabilirsiniz:
- template <class … Args>
void Text(const Point2d& point, Alignment alignment, std::string_view formattedText, Args … args); - template <class … Args>
void TextInColumn(const Point2d& point, Alignment alignment, int32_t columnWidth, std::string_view formattedText, Args … args);
- template <class … Args>
- Aşağıda bu API’leri nasıl kullanabileceğinize ilişkin örnek kullanımları bulabilirsiniz:
1 2 3 4 5 6 7 |
mPainter.SetActiveFont("FreeSans_ItalicBold_12"); mPainter.AssignPen(Pen{ Color::Magenta }); mPainter.Text(screenCenter + Point2d{ 0, -200 }, Painter::Alignment::Center, "[Ileri] Merhaba Dunya Bold Italic_Magenta Center %d", 2022); mPainter.SetActiveFont("FreeSans_Italic_10"); mPainter.AssignPen(Pen{ Color::Black }); mPainter.TextInColumn(screenCenter + Point2d{ -310, -180 }, Painter::Alignment::Left, 200, "[Ileri] Uzun bir yaziyi diyelimki gormek istediniz artik bunu da basmaniz oldukca mumkun. Elbette coklu satir destegi de \n\n gordugunuz gibi mumkun ;) %d", 2022); |
Yukarıdaki çizim API’leri ile elde edeceğiniz çıktılar aşağıdaki gibi olacaktır:
Örnek Kullanım
Evet dostlar, API’leri bir çoğuna baktık. Bunları en iyi öğrenme şekli elbette bunlar ile oynamak. Yukarıdaki örnek kullanımları da içeren “sdl_example” uygulamasını muhakkak inceleyiniz (bu örneğe ilişkin bir yazı kısa da olsa yazmayı planlıyorum). sdl_example içerisinde motorun bir çok kabiliyeti sunulmakta, bunlar arasında geçiş için F11 ve F12 tuşlarını kullanabilirsiniz. Painter kabiliyetlerini gösteren uygulamanın ekran görüntüsünün aşağıdaki gibi olmasını bekliyoruz:
Evet sevgili dostlar, uEngine4’ün önemli bir bileşenini birlikte keşfettik. Bu kütüphaneyi kullanmadan bu tarz çizimleri yapmaya çalışarak aslında nasıl bir soyutlama sunulduğunu daha net görebilirsiniz. Umarım sizler için faydalı olur, bir sonraki yazımda görüşmek dileğiyle bol kodlu günler diliyorum.
Pingback: uEngine4 Serüveni - BasicGLPainter - II - Yazılımperver'in Dünyası