Görüntü Yapısı
Emülatörümüzün temel fonksiyonlarını tamamladıktan sonra, ekrana görüntü vereceğimiz yapımızın tanımına geçebiliriz. Öncelikle kullanacağımız yapıları modülümüze ekleyelim:
use minifb::{Key, Scale, Window, WindowOptions};
minifb daha önce de bahsettiğimiz gibi en yaygın işletim sistemlerinde bir pencere açıp içerisine bir şeyler çizebileceğimiz basit bir paket. Aynı zamanda basılan tuşları da alabildiği için bizim için biçilmiş kaftan. Yapıları kullanıma aldıktan sonra bazı sabitleri tanımlayalım:
const WIDTH: usize = 64;
const HEIGHT: usize = 32;
const FOREGROUND_COLOR: u32 = 0x5294e2;
const BACKGROUND_COLOR: u32 = 0x282c34;
CHIP-8 64x32 piksel boyutunda bir ekrana sahip. Bu nedenle genişlik (WIDTH
)
ve yükseklik (HEIGHT
) olarak iki sabit tanımlaması yaptık. Ayrıca arka
plan rengi ve ön plan rengi olmak üzere iki 32-bit cinsinden sayı
tanımladık. minifb 32-bit cinsinden olan değerleri ekrana cizebilir,
isterseniz burada farklı renkler seçebilirsiniz.
Rust'ta sabit tanımlamaları const
anahtar kelimesiyle yapılır. Tüm
sabitler 'static
ömrüne sahiptir.
Artık görüntü yapımızı tanımlayabiliriz:
pub struct Display {
buffer: [[u8; WIDTH]; HEIGHT],
window: Window,
}
CHIP-8 siyah beyaz bir ekrana sahip olduğundan görüntü buffer'ı 8-bitlik
64x32 boyutunda bir matris. Bu buffer içerisinde dolu pikseller için 1, boş
pikseller için 0 değerini kullanacağız. window
alanı için de minifb'nin
Window
tipini kullanıyoruz.
Görüntü Metodlarının Uygulanması
impl Display {
}
Bloğunu oluşturarak görüntü metodlarını eklemeye başlayalım.
pub fn new() -> Self {
Self {
buffer: [[0; WIDTH]; HEIGHT],
window: Window::new(
"Rust ile CHIP-8",
WIDTH,
HEIGHT,
WindowOptions {
scale: Scale::X16,
..WindowOptions::default()
},
)
.expect("Pencere oluşturulurken hata oluştu"),
}
}
new
metodumuz yeni bir Display
instance'ı oluşturmaya yarıyor. Burada
kullanmış olduğumuz Self
; Dislay
ile aynı anlama geliyor. İsterseniz
impl
bloğunda tekrar tekrar tip ismini yazmak yerine Self
anahtar
kelimesini kullanabilirsiniz. Unutmayın ki birinci harf büyük olmalı ve bu
anahtar kelime, metod argümanlarında kullanılan self
ile
karıştırılmamalıdır.
WindowOptions
yapısı tamamen açık (public) alanlara sahip bir yapı. Bu nedenle bu yapı
için herhangi bir yardımcı metod olmadan direkt instance oluşturabiliriz.
Biz varsayılan (Default
)
özelliği eklenmiş bu yapının, sadece scale
alanını değiştireceğimizden,
geri kalan alanların varsayılan değeri alması için ..WindowOptions::default()
yazımını kullandık. Rust'ta kullanılan bu yazım, yapının tanımlanmamış
diğer alanları için varsayılan değerleri almasını sağlıyor. 64x32 piksel
modern bir bilgisayarda çok küçük olacağı için, scale
ile belirlediğimiz
değerle, minifb paketi görütüyü belirlenen oranda arttırıyor (bizim
belirlediğimiz X16 oranında).
Metodumuzda şu an için herhangi bir hata yönetimi yapmadığımızdan dolayı,
pencere oluşturulurken karşılaşacağımız olası bir hata durumunda expect
ile panikleyip çıkıyoruz.
Yardımcı Metodlar
pub fn is_open(&self) -> bool {
self.window.is_open() && !self.window.is_key_down(Key::Escape)
}
pub fn clear(&mut self) {
self.buffer = [[0; WIDTH]; HEIGHT];
}
is_open
metodu adından da anlaşılabileceği gibi, penceremizin açık olup
olmadığını kontrol ediyor. Aynı zamanda Esc
tuşunun basılı olup
olmadığını da kontrol eden bu metod bool
dönüyor. Bu sayede oyuncu
istediği zaman Esc
tuşuna basarak pencereyi kapatabilir ve emülatörü
sonlandırabilir.
clear
metodu, yapımız içerisinde yer alan buffer'ı temizlemeye yarıyor.
Draw Instruction'ı ile Çizim
CHIP-8 draw instruction'ı daha önce belirlediğimiz gibi:
/// 0xDxyn DRW: `x` ve `y` registerinden başayarak `n` adet byte sprite'ı ekranda
/// gösterir. Çakışma (collision) durumu `F` registerinde tutulur.
Draw(Register, Register, u8),
Şeklinde yapıyor. Aynı zamanda CHIP-8 sadece bu instruction ile collision (spriteların birbiriyle çakışma) durumunu kontrol edebiliyor. Bu işlemi şu şekilde yapabiliriz:
pub fn draw(&mut self, x: usize, y: usize, sprite: &[u8]) -> u8 {
let mut collision = 0;
let mut xi: usize;
let mut yj: usize;
for (j, sprite) in sprite.iter().enumerate() {
for i in 0..8 {
xi = (x + i) % WIDTH;
yj = (y + j) % HEIGHT;
if sprite & (0x80 >> i) != 0 {
if self.buffer[yj][xi] == 1 {
collision = 1;
}
self.buffer[yj][xi] ^= 1;
}
}
}
self.draw_screen();
collision
}
sprite
bellekten gelen 8-bitlik verilere sahip bir slice. Öncelikle
for döngümüz her bir sprite için j
sayacıyla çalışıyor. Ardından ikinci
for döngüsünde 8-bitlik verinin her bir bitini kontrol etmeye başlıyoruz.
xi
ve yj
ile yazılacak bit'in ekrandaki pozisyonu belirleniyor.
Ardından her bir bit: 0x80: 10000000
üzerinde çalıştığımız bit kadar (i
)
sağa kaydırılarak sprite ile XOR işlemi sayesinde herhangi bir çizilecek
piksel varsa 1, yoksa 0 sonucu elde ediliyor. Ardından daha önce aynı
pozisyonda herhangi sprite çizildiyse çakışma değeri olan collision
1
yapılıyor.
Son olarak CHIP-8'de spritelar ekrana XOR ile çizildiği için, üzerinde çalıştığımız piksel 1 ile XOR işlemi ile belirleniyor.
buffer
'ın Pencereye Çizimi
minifb &[u32]
tipinden bir yapıyı ekrana çizebilir. Bu nedenle
[[0; WIDTH]; HEIGHT]
tipinden olan matrisimizi [u32]
tipden bir array'a
çevirmemiz gerekli. Bu işlemi daha önce sabit olarak belirlediğimiz
arkaplan ve önplan rengini de kullanarak şu şekilde yapabiliriz:
pub fn draw_screen(&mut self) {
let mut buffer = [0; WIDTH * HEIGHT];
let mut loc = 0;
for y in 0..HEIGHT {
for x in 0..WIDTH {
buffer[loc] = if self.buffer[y][x] == 1 {
FOREGROUND_COLOR
} else {
BACKGROUND_COLOR
};
loc += 1;
}
}
self.window.update_with_buffer(&buffer).unwrap();
}
Referanstan-referansa Dönüştürme ile Pencere Alanının Alımı
İleride yazacağımız emülasyon döngüsü için, Display
yapımız içerisinde
yer alan window
alanına ulaşabilmemiz gerekli. Bu işlemi Rust'ın
referanstan-referansa çevirme işlemi yapan
AsMut
özelliği ile yapacağız. Display
yapımıza AsMut
özelliğini katalım ve
as_mut
çağrıldığında Window
tipini dönelim:
impl AsMut<Window> for Display {
fn as_mut(&mut self) -> &mut Window {
&mut self.window
}
}