UNITY’de Dosya ve Klasörleri Yönetmek (File Management), XML Kullanımı ve Şifrelenmiş Dosya Oluşturmak (Encryption)

Yayınlandı: 15 Ağustos 2013 yasirkula tarafından Oyun Tasarımı, UNITY 3D içinde

Hepinize merhaba,

Bu uzun derste Unity üzerinden C# kullanarak dört ana konu üzerinde çalışacağız: Sistemdeki dosyaları yönetmek, klasörleri yönetmek, XML kullanımını basit anlamda görmek (XML dosyaları karmaşık save dosyaları için birebirdir) ve oluşturduğumuz dosyaları şifreleyerek içeriğinin sadece makineler tarafından anlaşılabilmesini sağlamak. Ders 5 parçadan oluşan uzun bir makalenin çevirisidir. Kaynağa gitmek için tıklayın: http://www.fizixstudios.com/labs/do/view/id/unity-file-management-and-xml

Tutoriale başlamadan önce Unity’nin arayüzüne, nasıl script oluşturulacağına, C#’ın yapısına ve Unity’nin GUI sistemine basit anlamda aşikar olmanız sizin için çok iyi olacaktır.

Hazırsanız başlayalım…

Bölüm 1: Giriş

Tutorial boyunca aşağıdaki işlemleri yapmaya çalışacağız:

  1. Dosya konumlarının (path) var olup olmadığını kontrol etmek
  2. Yeni klasör oluşturmak ve mevcut bir klasörün içeriğine bakmak
  3. Yeni dosya oluşturmak ve mevcut bir dosyanın içine bakmak
  4. Windows ve Apple Mac’teki bazı dahili pencereleri keşfetmek (Dosya Aç penceresi (dialogue) gibi)
  5. XML dosyaları oluşturup üzerinde işlemler yapmak
  6. Şifreleme (encryption) (düz metin dosyalarını insanlar tarafından okunamayacak şifreli bir şekle sokmak)

Verilerle uğraşırken çoğunlukla XML kullanacağız çünkü XML’in yerleşmiş bir yapısı var ve böylece kullanması, okuması ve yönetmesi daha rahat.

Dosya yönetimi çoğu ciddi oyun projesi için çok önemli bir parçadır. Onun sayesinde pek çok güzel şeyi yapmak mümkündür:

  1. Gelişmiş kayıt (save) sistemlerinin temelini oluşturur (ve kaydedilmiş bir oyunu yüklemenin (load) )
  2. Oyun süresince geçici (temporary) dosyalar oluşturup onlarla uğraşabilirsiniz
  3. Daha sonra kullanmak üzere veri saklayabilirsiniz (cache)
  4. Oyununuz için veritabanları oluşturabilirsiniz. Örneğin Civilization tarzı bir oyun yapıyor olsanız her askerî ünitenin özelliklerini ayrı birer XML dosyasında saklayabilirsiniz.
  5. Dördüncü maddeyi bir adım daha ileri taşıyacak olursak; ayrıca oyununuzu sonradan genişletebilme imkanınız da olur; kullanıcı modları, genişleme paketleri ya da indirilebilir içerikler gibi. Eğer mod yapma işiyle uğraştıysanız, Civilization’ı ele alalım, tonla XML dosyasıyla karşı karşıya kalmışsınızdır: üniteler için, binalar için ve hatta oyun arayüzü (UI) için bile…

Tabi bu dediğimiz şeyler için gelişmiş bir XML sistemine sahip olmanız lazım ve bu sistemi kodlamayı anlatmaya çalışsak çok uzun bir tutorial yazmak zorunda kalırdık. Biz bu gelişmiş sistemleri yazma işlemini size bırakacağız ve size asıl önemli olan şeyi, bu işin ‘temel’ini öğreteceğiz.

Tutorial boyunca XML dosyalarımızı elle oluşturacağız, yani XML Serialization sistemini kullanmayacağız. Bu sistem her ne kadar işi çoğunlukla otomatiğe dökse de biz kaputun içine dalıp her şeyi elle yazacağız. Böylesinin daha faydalı olacağını düşünüyorum.

XML Serialization yöntemi XML işlemlerini oldukça kolaylaştırabilir ama kusursuz değildir. Örneğin private değişkenleri bu sistemle serialize edemezsiniz. Bu yüzden her iki sistemi de bilmek sizin için daha iyi. Böylece içinde bulunduğunuz duruma göre iki sistemden uygun olanını seçebilirsiniz.

Projeyi Oluşturmak

İlk iş olarak projemizi oluşturalım:

  1. Yeni bir proje oluşturun. Herhangi bir asset pack import etmeyin
  2. “gui” adında yeni bir Empty GameObject oluşturun
  3. “fileGUI” adında yeni bir C# scripti oluşturun
  4. “fileManager” adında yeni bir C# scripti oluşturun
  5. fileGUI scriptini “gui” GameObject’ine component olarak ekleyin

Scriptlerimizi Oluşturmak

Bu tutorialde GUI kodlarını fileGUI scriptinde, dosya yönetimi kodlarını da fileManager scriptinde tutacağız. İkisini bir arada da yazabilirdik ama ciddi projelerde birbirinden farklı işleri yapan kodları bu şekilde birbirinden ayırmak genelde sizin için daha kolaylaştırıcı olur.

fileManager Classı

İlk önce scripti düzenleyerek aşağıdaki şekle sokalım:

using UnityEngine;
using System.Collections;
using System.IO;
using UnityEditor;

public class fileManager : MonoBehaviour
{
	private static fileManager instance; // fileManager class'ının bir kopyası (Instance)
	private string path; // Uygulamaya giden yolu (path) depolar
	
	// constructor, eğer yoksa yeni bir fileManager instance'si oluşturmaya yarar
	public static fileManager Instance
	{
		get
		{
			if( instance == null )
			{
				instance = new GameObject( "fileManager" ).AddComponent( "fileManager" ) as fileManager;
			}
			
			return instance;
		}
	}
	
	// uygulamadan çıkılırken çağırılır
	public void OnApplicationQuit()
	{
		destroyInstance();
	}
	
	// fileManager instance'sini yok etmeye yarar
	public void destroyInstance()
	{
		print( "Instance yok edildi" );
		instance = null;
	}
	
	// file manager'ı (dosya yöneticisi) çalıştırır
	public void initialize()
	{
		print( "Dosya yöneticisi çalıştırıldı" );
	}
}

Kodu inceleyecek olursak:

1. fileManager class’ı içerisinde iki property (değişken) oluşturuyoruz: fileManager, yani bir fileManager Instance’sini depolayan değişken ve path, yani uygulamaya giden yolu depolayan değişken (uygulamanın bilgisayarda yüklendiği konum)

2. Başka scriptler tarafından fileManager scriptinin çağrılabilmesi için, yeni bir fileManager instance’si oluşturmaya yarayan bir constructor’ımız var. Yeni bir instance sadece mevcut bir instance var olmadığı taktirde oluşturulur, aksi halde mevcut instance döndürülür.
ÇEVİRMEN NOTU: Bu aşamayı tam anlamayabilirsiniz çünkü çok sık rastgeldiğimiz bir constructor değil. Açıklamayı felan boşverin, bilmeniz gereken şu: Bu constructor ve “instance” adındaki değişken sayesinde bu scripte başka scriptler tarafından çok rahat erişiyoruz. Tek yapmamız gereken “fileManager.Instance” yazmak ve hop! Script elimizin altında.

3. Uygulamadan çıkılırken otomatik olarak çağrılan bir OnApplicationQuit() metodumuz var

4. destroyInstance() metodu ile elimizdeki instance’yi uygulamadan çıkarken yok ediyoruz

5. initialize() metodumuzun içini ise ders boyunca doldurarak burada dosya yönetimi işlemleri yapacağız

fileGUI Class’ı

Scripti aşağıdaki gibi düzenleyin:

using UnityEngine;
using System.Collections;

public class fileGUI : MonoBehaviour
{
	void Start ()
	{
		print( "Dosya Yöneticisi Arayüzü çalıştırılıyor." );
		
		// Yeni bir fileManager instance'si oluştur ve onu çalıştır
		fileManager.Instance.initialize();
	}
	
	void OnGUI()
	{

	}
}

Bu script iki metoda (fonksiyon) sahip:

1. Start() metodu: bu metod oyun başlayınca tek sefere mahsus otomatik olarak çalıştırılır. Önce kodun çalıştığına dair konsola bir mesaj yazdırıyoruz (print) ve ardından fileManager class’ını çalıştırıyoruz. Bunun için fileManager.cs’deki initialize() metodunu kullanıyoruz. Nasıl bir syntax kullandığımıza dikkat edin. Önce fileManager.Instance kodu ile scripte erişiyoruz ve ardından scriptteki initialize() metodunu çalıştırıyoruz. Tıpkı şunun gibi:

fileManager.Instance.calistirilacakMetodAdi();

2. OnGUI() metodu: bu metod ekranda bazı butonlar ve label’lar (düz yazı) gösterecek.

Bu noktada oyunu test ederseniz konsoldaki yazılar vasıtasıyla GUI ve fileManager class’larının düzgün çalıştığını görebilirsiniz. Ayrıca Hierarchy panelinde fileManager adında yeni bir objenin oluştuğunu görebilirsiniz (mevcut bir fileManager instance’si olmadığı için fileManager class’ı constructor’ındaki Instantiate metodunu çağırdı).

Uygulamaya Giden Yol (Path)

Uygulamaya giden yoldan kastımız oyunun işletim sisteminde tam olarak nerede (hangi konumda) kurulu olduğudur. Dosyaları yönetirken uygulamaya giden bu yolu bilmek faydalıdır. PC’de ve Mac’de oyunun yüklü olduğu konum dışında yerlerde de dosya oluşturabilirsiniz (ama bu bazı güvenlik sorunlarını da beraberinde getirebilir). Yine de çoğu durumda dosyalarımızı oyunun yüklü olduğu konumda oluşturmak en iyisidir.

Unity’de çeşitli path’ler bulunmaktadır ve bunlardan dataPath oyunun yüklü olduğu konumu depolar ve biz de haliyle bu değişken ile çalışacağız. Diğer tüm path’leri de şekildeki gibi görebilirsiniz:

Application.dataPath // Oyunun yüklü olduğu konumu verir
Application.streamingAssetsPath // Bilmiyorum 😛
Application.persistentDataPath // Save dosyaları gibi oyun silinince gitmesi istenmeyen dosyaların depolanması gereken bir konum döndürür
Application.temporaryCachePath // Geçici (temporary) bir konum döndürür. Oyun kapatılınca buradaki dosyalara elveda deriz 

Bu değişkenler hakkında daha detaylı bilgiyi Unity Script Reference’de bulabilirsiniz.

ÇEVİRMEN NOTU: Bu yazının aksine ben kendi projelerinizde Application.persistentDataPath’i kullanmanızı öneriyorum. Çünkü Application.dataPath mobil cihazlarda sorun çıkarıyor.

Uygulamaya giden yolu depolamak ve daha sonra kullanmak için fileManager’daki path değişkenini kullanacağız. Bunun için fileManager C# scriptindeki initialize() metodunu şekildeki gibi güncelleyin:

print( "Dosya yöneticisi çalıştırıldı" );
path = Application.dataPath;
print( "Path: " + path );

Artık initialize() metodu konsola “Dosya yöneticisi çalıştırıldı” yazısı yazdıracak, path değişkenine değerini atayacak ve konsola bu değeri, yani uygulamaya giden yolu yazdıracak.

Bölüm 2: Klasörleri Yönetmek

Unity ile çeşitli amaçlara hizmet etmek için klasörler oluşturabilirsiniz. Bir klasör ya da dosya oluşturmadan, düzenlemeden veya silmeden önce o klasör ya da dosyanın var olup olmadığını kontrol etmeniz çok önemlidir. Aksi taktirde hata alabilirsiniz. Bundan ötürü bir klasörün var olup olmadığını test etmek için ufak bir metod yazalım. Bu metodu fileManager scriptine ekleyin:

// girilen hedef klasörün sistemde var olup olmadığına bakar
private bool checkDirectory( string directory )
{
	if( Directory.Exists( path + "/" + directory ) )
		return true;
	else
		return false;
}

Bu metod Directory.Exists() komutunu kullanarak içine parametre olarak girilen directory’nin (bir klasöre giden yol) gerçekten var olup olmadığına bakar. Gerçekten varsa true, yoksa false döndürür.

Fark edeceğiniz üzere directory parametresini kullanmadan önce path değişkenini kullanıyoruz. Bu sayede sadece oyunun yüklü olduğu konumdaki klasörlerle iş yapıyoruz. Bunun için de directory değişkeni sadece bakmak istediğimiz klasörün adından oluşmalı: “gamedata” ya da “gamedata/saves” gibi…

Klasör Oluşturmak

Şimdi klasör oluşturma işlemine bakalım. Tutorial boyunca oluşturacağımız veri dosyalarını depolamak için “gamedata” adında bir klasöre ihtiyacımız var. Öncelikle bu klasörün var olup olmadığına bakalım ve eğer yoksa yeni bir klasör oluşturalım. Klasör oluşturma işlemini gerçekleştirmek için fileManager class’ına aşağıdaki metodu ekleyin:

// yeni bir klasör oluşturur
private void createDirectory( string directory )
{
	print( "Klasör oluşturuluyor: " + directory );
	Directory.CreateDirectory( path + "/" + directory );
}

Gördüğünüz gibi Directory.CreateDirectory() komutunu kullanarak yeni bir klasör oluşturuyoruz. Bu metodu test etmek için initialize() fonksiyonuna aşağıdaki kodu ekleyin:

// Eğer gamedata klasörü yoksa yeni bir tane oluştur
if( !checkDirectory( "gamedata" ) )
	createDirectory( "gamedata" );

Bu kod parçası checkDirectory ve createDirectory metodlarımızı kullanarak “gamedata” klasörünün varlığını kontrol ediyor ve böyle bir klasör yoksa yeni bir tane oluşturuyor. Eğer yeni klasör oluşturulursa bunu createDirectory() metodundaki print() komutunun konsola çıktı vermesiyle fark edebilirsiniz. Project paneline sağ tıklayarak yenileyin (Refresh). Artık “gamedata” klasörünü görebilirsiniz.
Normal şartlarda bir dosya ya da klasörün var olup olmadığını kontrol etmenin en uygun yeri bu dosya yönetimi işlemlerini yaptığınız metodlardır. Bizim durumumuzda bu createDirectory() metodu oluyor. Şimdi createDirectory() metodunu açarak şekildeki gibi düzenleyin:

// yeni bir klasör oluşturur
private void createDirectory( string directory )
{
	if( !checkDirectory( directory ) )
	{
		print( "Klasör oluşturuluyor: " + directory );
		Directory.CreateDirectory( path + "/" + directory );
	}
	else
	{
		print( "Hata: Zaten mevcut olan " + directory + " klasörünü tekrar oluşturmaya çalışıyorsun!" );
	}
}

Yaptığımız bu basit değişiklikle hedef klasörün zaten var olup olmadığına bakıyoruz ve eğer girilen klasör zaten varsa konsola uyarı mesajı veriyoruz ve başka birşey yapmıyoruz.

Klasör Silmek

File manager class’ımızı açıp içine şu metodu ekleyin:

// bir klasörü siler
private void deleteDirectory( string directory )
{
	if( checkDirectory( directory ) )
	{
		print( "Klasör siliniyor: " + directory );
		Directory.Delete( path + "/" + directory, true );
	}
	else
	{
		print( "Hata: Var olmayan " + directory + " klasörünü silmeye çalışıyorsun!" );
	}
}

Önceki metodlarda olduğu gibi üzerinde işlem yapılacak klasörün adını string olarak giriyoruz ve bir if ifadesi ile klasörün var olup olmadığını kontrol ediyoruz. Eğer klasör varsa onu siliyoruz, yoksa konsola hata mesajı veriyoruz.

Directory.Delete() fonksiyonuna dikkatinizi çekmek isterim. Bu fonksiyon iki parametre alıyor.
Parametre 1: Silmek istediğimiz klasöre giden yol
Parametre 2: Klasörün içindeki dosya veya klasörleri de silip silmemeye karar vermeye yarayan bir boolean

Eğer kodu test etmek istiyorsanız initialize() metodunun içerisinde kullanın. Ancak sonrasında yazdığınız bu test etme kodunu silmeyi unutmayın.

Klasör Taşımak

Her zaman olduğu gibi önce kodu veriyorum:

// bir klasörü taşır
private void moveDirectory( string originalDestination, string newDestination )
{
	if( checkDirectory( originalDestination ) && !checkDirectory( newDestination ) )
	{
		print( "Klasör taşınıyor: " + originalDestination );
		Directory.Move( path + "/" + originalDestination, path + "/" + newDestination );
	}
	else
	{
		print( "Hata: Ya varolmayan bir klasörü taşımaya çalışıyorsun ya da hedef noktada zaten aynı klasör mevcut" );
	}
}

Bu metod da iki parametre alıyor: Taşınacak klasörün konumu ve bu klasörün taşınmasını istediğimiz konum. Metod sadece klasör yerinde duruyorsa ve taşınmasını istediğimiz konumda aynı isimli klasör yoksa çalışıyor.

Kodu test etmek için:

  1. Project panelinde “saves” adında yeni bir klasör oluşturun
  2. initialize() metoduna aşağıdaki kodu ekleyin:
// Eğer Project panelinde "saves" adında bir ana klasör varsa onu gamedata klasörünün içine taşı
if( checkDirectory( "saves" ) )
{
	moveDirectory( "saves", "gamedata/saves" );
}

Alt Klasörleri Listelemek

Çeşitli nedenlerden ötürü ya da can sıkıntısından bir klasörün içindeki tüm alt klasörleri görmek isteyebilirsiniz. Bu işleme başlamadan önce “gamedata” klasörünüzün içinde aşağıdaki iki alt klasörü oluşturun:

– saves
– xml

Şimdi listeleme metodumuzu yazalım:

// bir klasörün içindeki alt klasörleri bulur
public string[] findSubDirectories( string directory )
{
	if( checkDirectory( directory ) )
	{
		print( directory + " klasörünün alt klasörlerine bakılıyor" );
		
		return Directory.GetDirectories( path + "/" + directory );
	}
	else
	{
		return new string[0];
	}
}

Metodu test etmek için initialize() metoduna aşağıdaki kodu ekleyin:

// "gamedata"daki alt klasörleri bul ve listele 
string [] subdirectoryList = findSubDirectories( "gamedata" );

foreach( string subdirectory in subdirectoryList )
{
	print( "Bulundu: " + subdirectory );
} 

Eğer şimdi Play butonuna basarsanız konsolda “saves” ve “xml” klasörlerinin isimlerini göreceksiniz. Fark edeceğiniz üzere klasörlerin sadece isimleri print edilmedi, onlara giden yol (path) print edildi. Bu metodun böyle bir sonuç döndürmesinin sebebi bu şekilde recursive işlem yapabilme olanağı. Yani bir klasörün içindeki alt klasörlerin de alt klasörlerine kolayca bakabilme olanağı. Örneğin bir strateji oyunun bu recursive işlemi yaparak aşağıdaki gibi bir çıktı elde edebilirdiniz:

gamedata/
gamedata/xml
gamedata/xml/uniteler/
gamedata/xml/uniteler/kara
gamedata/xml/uniteler/hava
gamedata/xml/uniteler/piyade
gamedata/xml/binalar/

Directory.GetDirectories() fonksiyonuna ayrıca arama kriterleri de ekleyebilirsiniz. Örneğin findSubDirectories() metodundaki Directory.GetDirectories() komutunu şekildeki gibi değiştirebilirsiniz:

Directory.GetDirectories(path + "/" + directory, "x*");

Bu değişikliği yaparsanız ismi ‘x’ harfiyle başlayan alt klasörleri bulursunuz. Buradaki * bir joker karakterdir ve “herhangi birşey” anlamındadır.

Klasördeki Dosyaları Listelemek

Nasıl alt klasörleri listeleyeceğimizi daha yeni öğrendik, peki klasördeki dosyaları listelemek istersek napacağız? Bunun için GetFiles() metodunu kullanacağız. Bu metodu kullanan yeni fonksiyonumuzu fileManager scriptinize ekleyin:

// bir konumda (path) yer alan dosyaları döndürür
public string[] findFiles( string directory )
{
	if( checkDirectory( directory ) )
	{
		print( directory + " klasöründeki dosyalara bakılıyor" );
		
		return Directory.GetFiles( path + "/" + directory );
	}
	else
	{
		return new string[0];
	}
}

Ardından “gamedata/xml” klasörünün içinde birkaç dosya oluşturun ve initialize() metoduna şekildeki eklemeyi yapın:

// "gamedata/xml" klasöründeki dosyaları listele
string[] fileList = findFiles( "gamedata/xml" );

foreach( string file in fileList )
{
	print( "Dosya bulundu: " + file );
}

Alt klasör listeleme metodunda olduğu gibi dosya ararken de arama kriteri belirleyebilirsiniz. Bunun için Directory.GetFiles() metodunu aşağıdaki gibi kullanmalısınız:

Directory.GetFiles( path + "/" + directory, aramaKriteri );

Bitirmeden Önce

Öncelikle initialize() fonksiyonundaki test için kullandığınız metodları silin. Kod şöyle gözüksün:

// file manager'ı (dosya yöneticisi) çalıştırır
public void initialize()
{
	print( "Dosya yöneticisi çalıştırıldı" );
	path = Application.dataPath;
	print( "Path: " + path );
	
	// Eğer gamedata klasörü yoksa yeni bir tane oluştur
	if( !checkDirectory( "gamedata" ) )
		createDirectory( "gamedata" );
}

Ardından xml klasöründe test için oluşturduğunuz tüm dosyaları silin.

Bazı önemli notlar
Belki fark etmişsinizdir; kullandığımız metodlar kusursuz değiller. Örneğin parametre olarak boş bir string girdiğimizde metodlarımız sorun çıkarmaya başlar. Bunun için gerçek bir uygulamada girilen parametrelerin kontrolü yapılmalıdır.

Ayrıca kullandığımız bu metodlar dışında klasörleri yönetmek için daha oldukça metod var. Biz burada en çok kullanılanlarını işledik ama tüm bu metodları görmek için tıklayın: http://msdn.microsoft.com/en-us/library/8ta62wh3

Bölüm 3: Dosyaları Yönetmek

Bu bölümde işleyeceğimiz konular şunlar: dosya oluşturmak, dosya okumak, dosya açma penceresi vasıtasıyla dosya seçmek, dosya silmek ve dosya içeriği güncellemek.

Dosyanın Var Olup Olmadığını Kontrol Etmek

Aynı klasörlerde olduğu gibi, dosyalarla işlem yaparken de önce dosyaların yerinde olup olmadığını kontrol etmek önemlidir. Bunun için daha önceden oluşturduğumuz checkDirectory() metoduna benzer, checkFile() adında yeni bir metod oluşturalım:

// girilen dosyanın var olup olmadığını kontrol eder
public bool checkFile( string filePath )
{
	if( File.Exists( path + "/" + filePath ) )
		return true;
	else
		return false;
}

Sanırım bunu açıklamaya gerek yok çünkü kod checkDirectory()’e gereğinden fazla benziyor zaten.

Dosya Oluşturmak

Dosya oluşturmak için kullanacağımız metodun dört parametresi olacak: dosyanın oluşturulacağı konum, dosyanın adı, dosyanın uzantısı ve dosyanın içeriği. fileManager class’ına metodumuzu aşağıdaki gibi ekleyin:

// yeni bir dosya oluşturur
public void createFile( string directory, string filename, string filetype, string fileData )
{
	print( "Oluşturuluyor: " + directory + "/" + filename + "." + filetype );
	
	if( checkDirectory( directory ) )
	{
		if( !checkFile( directory + "/" + filename + "." + filetype ) )
		{
			// Dosyayı oluştur
			File.WriteAllText( path + "/" + directory + "/" + filename + "." + filetype, fileData );
		}
		else
		{
			print( filename + " dosyası " + path + "/" + directory + " konumunda zaten var!" );
		}
	}
	else
	{
		print( directory + " konumu var olmadığından dosya oluşturulamıyor" );
	}
}

Burda neler mi yapıyoruz:

  1. Dört string alıyoruz; dosya konumu, dosya adı, dosya uzantısı ve dosyada tutulacak veri
  2. Dosya konumunun yerinde olup olmadığını kontrol ediyoruz
  3. Girilen konumda aynı isimli başka bir dosya olmadığından emin oluyoruz
  4. Dosyayı File.WriteAllText() fonksiyonunu kullanarak oluşturuyoruz

Eğer initialize() metoduna aşağıdaki kodu eklersek bu yeni fonksiyonumuzu test etmiş oluruz:

if( !checkFile( "gamedata/test.gamedata" ) )
	createFile( "gamedata", "test", "gamedata", "Dosyanın içeriği" );

Oyun başlarken gamedata klasöründe test.gamedata dosyası oluşturulacak. Eğer dosyayı Notepad ile açarsanız “Dosyanın içeriği” yazısıyla karşılaşacaksınız.

Dosya Okumak

Bir dosyayı okuyup içeriğini döndürmek için aşağıdaki metodu scriptimize ekleyelim:

// bir dosyayı okur ve içeriğini döndürür
public string readFile( string directory, string filename, string filetype )
{
	print( "Okunuyor: " + directory + "/" + filename + "." + filetype );
	
	if( checkDirectory( directory ) )
	{
		if( checkFile( directory + "/" + filename + "." + filetype ) )
		{
			// Dosyayı oku
			return File.ReadAllText( path + "/" + directory + "/" + filename + "." + filetype );
		}
		else
		{
			print( path + "/" + directory + " konumunda " + filename + " diye bir dosya yok!" );
			return null;
		}
	}
	else
	{
		print( directory + " konumu var olmadığından dosya okunamıyor" );
		return null;
	}
}

Bu metod dosyanın bulunduğu klasörü, dosyanın adını ve uzantısını parametre olarak alıp bize dosyanın içeriğini string olarak döndürür (tabi eğer girilen konum ve dosya yerindeyse).

Kodu test etmek için initialize() metoduna aşağıdaki komutu ekleyin:

print( readFile( "gamedata", "test", "gamedata" ) );

Az önce oluşturduğumuz test.gamedata dosyası okunup içeriği konsola print edilecek.

Dosya Silmek

Bu iş için gerekli olan metodumuz:

// bir dosyayı siler
public void deleteFile( string filePath )
{
	if( File.Exists( path + "/" + filePath ) )
		File.Delete( path + "/" + filePath );
	else
		print( "dosya silinemiyor çünkü ortalarda yok!" );
}

Kodu açıklama ihtiyacı hissetmiyorum çünkü çok basit. Dosya silmek için File.Delete() fonksiyonunu kullandığımıza dikkat edin. Şimdi kodu test edelim:

deleteFile("gamedata/test.gamedata");

Eğer bu komutu initialize() metoduna eklerseniz oyun başlayınca test.gamedata dosyası silinir.

Dosya İçeriğini Güncellemek

Bir dosyayı iki farklı şekilde güncelleyebiliriz: eski veriyi tamamen silip dosyaya sadece yeni veriyi aktarabiliriz ya da eski veriyi silmez, aksine onun sonuna yeni veriyi uhularız. Durmayın ve yeni metodumuzu kodunuza ekleyin:

// bir dosyanın içeriğini günceller
public void updateFile( string directory, string filename, string filetype, string fileData, string mode )
{
	print( "Güncelleniyor: " + directory + "/" + filename + "." + filetype );
	
	if( checkDirectory( directory ) )
	{
		if( checkFile( directory + "/" + filename + "." + filetype ) )
		{
			if( mode == "replace" )
				File.WriteAllText( path + "/" + directory + "/" + filename + "." + filetype, fileData );
			else if( mode == "append" )
				File.AppendAllText( path + "/" + directory + "/" + filename + "." + filetype, fileData );
		}
		else
		{
			print( path + "/" + directory + " konumunda " + filename + " diye bir dosya yok!" );
		}
	}
	else
	{
		print( directory + " konumu var olmadığından dosya güncellenemiyor" );
	}
}

Bu yeni metod createFile() metoduna çok benziyor. Yalnız bu metodda “mode” adında yeni bir parametre var. Eğer bu parametrenin değeri “replace” olursa yeni veri eskisinin yerine geçer, “append” olursa yeni veri eskisinin sonuna eklenir, eski veri silinmez. Metodu çağırmak için:

updateFile( "gamedata", "test", "gamedata", "dosyanın yeni içeriği", "replace" );

veya

updateFile( "gamedata", "test", "gamedata", "dosyanın yeni içeriği", "append" );

Dosya Açma Penceresi

Son olarak da dosya açma penceresiyle uğraşacağız. Bunun için fileGUI scriptine geçin ve OnGUI() metoduna aşağıdaki kodu ekleyin:

if( GUI.Button( new Rect( 30, 30, 150, 30 ), "Open File" ) )
{
	string openPath = EditorUtility.OpenFilePanel( "Open File", Application.dataPath + "/gamedata", "gamedata" );
}

Yazdığımız kodun çalışması için yeni bir kütüphane kullanmamız gerekiyor. Bunun için scriptin en tepesine aşağıdaki kodu ekleyin:

using UnityEditor;

ÇEVİRMEN NOTU: Burada EditorUtility class’ının bir fonksiyonunu kullanıyoruz. Bu class özeldir ve sadece Unity editöründeyken çalışır. Yani bu oyununuzu PC’ye, mobile ya da başka herhangi bir platforma build ettiğinizde bu fonksiyon artık çalışmaz. Bu yüzden bence hiç de kullanışlı değil ama sizin belki işinize yarar, onu bilemem.

Artık oyun ekranında “Open File” diye bir buton olacak ve buna tıklarsanız dosya açma penceresi gelecek. Bu pencereyi EditorUtility.OpenFilePanel() metodu ile açtık. Bu metod üç parametre alır:

  1. Pencerenin başlığı
  2. Pencere açıldığında karşımıza ilk gelecek olan konum (path)
  3. Görünmesini istediğimiz dosya türleri (örneğin xml, gamedata, dat veya başka herhangi birşey)

Burada üçüncü parametre isteğe bağlı. Eğer birşey girmezseniz hedefteki tüm dosyalar görünür.

Şimdi EditorUtility.OpenFilePanel() fonksiyonunun altına aşağıdaki kodu ekleyin:

if( openPath.Length != 0 )
{
	fileManager.Instance.processFile( openPath );
}

Böylece eğer dosya açma penceresinde bir dosya seçilmişse fileManagement class’ındaki processFile() metodu çalıştırılacak. Henüz processFile() diye bir metodumuz yok diyebilirsiniz. Endişelenmeyin, o metodu tam da şimdi oluşturmak üzereyiz:

// dosya açma penceresinde seçilmiş bir dosyayı okur
public void processFile( string filepath )
{
	print( "dosya işleniyor " + filepath );
	string fileContents = File.ReadAllText( filepath );
	print( "Dosyanın içeriği: " + fileContents );
}

Bu metod seçtiğimiz dosyayı açar ve içeriğini konsola print eder.

Bölüm 4: XML Dosyaları

Bu bölümde XML dosyaları oluşturmayı veya mevcut bir XML dosyasını okumayı göreceğiz.

XML Dosyası Oluşturmak

Bir XML dosyası okumadan önce, okuyacak bir XML’imizin olması için XML oluşturucu bir metod yazmalıyız. Normalde hazır XML fonksiyonlarını kullanacağız ama ilk seferde XML’imizi deneme amaçlı olarak tamamen el ile yazacağız.

Bunun için öncelikle aşağıdaki metodu fileManager’a ekleyin:

// yeni bir XML dosyası oluşturur
public void createXMLFile( string directory, string filename, string filetype, string fileData, string mode )
{
	print( directory + " konumunda XML dosyası oluşturuluyor" );
	
	if( checkDirectory( directory ) )
	{
		if( mode == "plaintext" )
			File.WriteAllText( path + "/" + directory + "/" + filename + "." + filetype, fileData );
	}
	else
	{
		print( directory + " konumu var olmadığından dosya oluşturulamıyor" );
	}
}

Bu metodumuz 5 parametre almakta:

  1. XML dosyasının oluşturulacağı konum (path)
  2. XML dosyasının adı
  3. XML dosyasının türü (böylece dilersek .xml dışında uzantılarla da çalışabiliriz)
  4. XML dosyasının string şeklinde içeriği
  5. XML oluşturma modu parametresi. Değeri şimdilik “plaintext” (düzyazı)

Buradaki “mode” parametresi önemli çünkü bir sonraki bölümde şifreleme işlemiyle uğraşırken değeri “encrypt” olacak ve ona göre metodun içinde farklı işlemler yapacağız. Yani eğer değeri “plaintext” olursa XML dosyasını direkt okurken değeri “encrypt” ise önce dosyayı deşifre edecek, sonra içeriğini okuyacağız.

Düzyazı şeklinde XML oluşturmak fark edeceğiniz üzere createFile() metoduyla aynı işliyor. Yine File.WriteAllText() metodunu kullanıyoruz.

Şimdi bize oluşturacağımız XML dosyasının içine yazacak veri lazım. Başta dediğim gibi, hazır fonksiyonları kullanmadan önce tüm XML’i deneme amaçlı elle yazacağız (yani zaten XML formatında olan bir string kullanacağız). Ardından oluşturulan bu XML verisini string şeklinde alıp createXMLFile() metodunda XML oluştururken kullanacağız. Şimdi öylesine bir XML verisi oluşturmaya yarayan buildXMLData() metodumuzu yazalım:

// bir XML yapısı oluşturur ve bunu döndürür
public string buildXMLData()
{
	print( "XML oluşturuluyor" );
	
	string xmlString;
	
	xmlString = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
	xmlString += "<player>\n";
	xmlString += "<profile>\n";
	xmlString += "<playername>Bob</playername>\n";
	xmlString += "<hp>3500</hp>\n";
	xmlString += "<mp>500</mp>\n";
	xmlString += "<level>15</level>\n";
	xmlString += "</profile>\n";
	xmlString += "<inventory>\n";
	xmlString += "<item name=\"item 1\" qty=\"2\" />\n";
	xmlString += "<item name=\"item 2\" qty=\"11\" />\n";
	xmlString += "<item name=\"item 3\" qty=\"1\" />\n";
	xmlString += "<item name=\"item 4\" qty=\"5\" />\n";
	xmlString += "</inventory>\n";
	xmlString += "</player>\n";
	
	return xmlString;
}

Bu metod bize bir RPG karakterinin özelliklerinin ve envanterinin depolandığı bir XML döndürüyor. Gördüğünüz gibi karakterin özelliklerini (profile) ve envanterini (inventory) birbirinden farklı şekilde depoluyoruz. Yazdığımız bu XML dosyası şuna benzer duracaktır:

<?xml version="1.0" encoding="utf-8"?>
<player>
	<profile>
		<playername>Bob</playername>
		<hp>3500</hp> <mp>500</mp>
		<level>15</level>
	</profile>
	<inventory>
		<item name="item 1" qty="2" />
		<item name="item 2" qty="11" />
		<item name="item 3" qty="1" />
		<item name="item 4" qty="5" />
	</inventory>
</player>

Şimdi initialize() metodunu güncelleyerek aşağıdaki hâli almasını sağlayın:

// file manager'ı (dosya yöneticisi) çalıştırır
public void initialize()
{
	print( "Dosya yöneticisi çalıştırıldı" );
	path = Application.dataPath;
	print( "Path: " + path );
	
	// Eğer gamedata klasörü yoksa yeni bir tane oluştur
	if( !checkDirectory( "gamedata" ) )
		createDirectory( "gamedata" );
		
	// XML dosyası oluştur
	createXMLFile( "gamedata/saves", "mysave", "xml", buildXMLData(), "plaintext" );
}

Böylece createXMLFile() metodunu kullanarak “gamedata/saves” klasöründe “mysave” adında “.xml” uzantılı, şifrelenmemiş (plaintext) ve içeriğini buildXMLData() metodunun döndürdüğü string’den alan bir XML dosyası oluşturuyoruz. Eğer uygulamayı test ederseniz saves klasöründe oluşturulmuş mysave.xml dosyasını bulabilirsiniz.

XML Dosyasındaki Veriyi Okumak

XML dosyasını okumak için (XML parsing) yeni bir metod oluşturacağız. Ama önce metodda kullanacağımız tüm komutların düzgün çalışması için gerekli kütüphaneleri import edelim:

using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Xml;

Ardından metodumuzu yazalım:

// bir XML dosyasının içeriğini okur
public void parseXMLFile( string directory, string filename, string filetype, string mode )
{
	print( directory + " konumundaki XML okunuyor" );
	
	
	
	// Düz yazı şeklindeki XML dosyasını oku
	if( mode == "plaintext" )
	{
		XmlDocument xmlDoc = new XmlDocument();
		xmlDoc.Load( path + "/" + directory + "/" + filename + "." + filetype );
		
		// Önce "profile" elemanındaki veriyi oku
		XmlNodeList profileList = xmlDoc.GetElementsByTagName( "profile" );

		foreach( XmlNode profileInfo in profileList )
		{
			XmlNodeList profileContent = profileInfo.ChildNodes;
			
			foreach( XmlNode profileItems in profileContent )
			{
				if( profileItems.Name == "playername" )
					print( "playername: " + profileItems.InnerText );
				else if( profileItems.Name == "hp" )
					print( "hp: " + profileItems.InnerText );
				else if( profileItems.Name == "mp" )
					print( "mp: " + profileItems.InnerText );
				else if( profileItems.Name == "level" )
					print( "level: " + profileItems.InnerText );
			}
		}
	}
	
	// Envanter verisini okuma işlemini birazdan burada yapacağız
	
}

Bu metod XML verisini işlerken şekildeki yolu izliyor:

  1. xmlDoc adında yeni bir XMLDocument objesi oluşturuyoruz
  2. XML dosyamızı bu değişkenin içine yüklüyoruz (Load ediyoruz)
  3. profileList adında bir XML Node List oluşturuyoruz ve XML dosyasındaki tüm <profile> ve </profile> etiketlerini, içlerindeki verilerle birlikte bu değişkene GetElementsByTagName() fonksiyonu vasıtasıyla alıyoruz.
  4. profileList bir array (dizi) ve bir for() döngüsü ile bu dizideki tüm <profile> tag’larını ayrı ayrı işleme tabi tutuyoruz (aslında XML’imizde tek bir <profile> </profile> tag’ı olduğu için profileList array’i sadece bir elemandan oluşuyor ve teknik anlamda bir for döngüsüne ihtiyacımız yoktu. Ama pratik olması açısından biz böyle yaptık.).
  5. profileContent adında yeni bir XmlNodeList türünde değişken oluşturuyoruz ve <profile> tag’ının içindeki tüm alt etiketleri (child node) bu değişkene aktarıyoruz.
  6. profileContent şu anda aslında “playername”, “hp”, “mp” ve “level” etiketlerini barındırıyor. Biz bir başka for loop’u daha kullanarak tüm bu etiketlerin (XmlNode) üzerinden tek tek geçip bu esnada onlarla işlem yapabilmek için üzerinde bulunduğumuz elemanı profileItems adında bir değişkene depoluyoruz.
  7. Elimizdeki etiketin ismini (profileItems.Name) birkaç if() ifadesi ile kontrol ediyoruz ve etiket hangi if() döngüsü ile uyuşuyorsa ona göre etiketin içeriğini (profileItems.InnerText) konsola uygun şekilde yazdırıyoruz.

Bu metod şu anda envanter verisini okumuyor çünkü onların yapısı farklı olduğu için okunuş tarzı da birazcık farklı. Bunun için aşağıdaki kodu “// Envanter verisini okuma işlemini birazdan burada yapacağız” satırı yerine ekleyin:

// Sonra inventory elemanındaki veriyi oku
XmlNodeList inventoryList = xmlDoc.GetElementsByTagName( "inventory" );

foreach( XmlNode inventoryInfo in inventoryList )
{
	XmlNodeList inventoryContent = inventoryInfo.ChildNodes;
	
	foreach( XmlNode inventoryItems in inventoryContent )
	{
		print( "Item: " + inventoryItems.Attributes[ "name" ].Value + " x" + inventoryItems.Attributes[ "qty" ].Value );
	}
}

Hatırlarsanız envanter eşyalarının XML verisi etiketler arasında değil ama direkt etiketin içindeydi (<item name=”item 1″ qty=”2″ /> gibi). İşte böyle durumlarda etiketin içindeki “name” ve “qty” şeklindeki yapılara o verinin özellikleri (attribute) diyoruz ve onları string şeklinde okumak için de inventoryItems.Attributes[“attribute_ismi”].Value komutunu kullanıyoruz.

Şimdi initialize() metoduna şu satırı ekleyin:

parseXMLFile( "gamedata/saves", "mysave", "xml", "plaintext" );

Eğer şimdi uygulamayı çalıştırırsanız XML dosyasındaki veriler konsola istediğimiz şekilde print edilir.

XML Fonksiyonları Kullanarak XML Dosyası Oluşturmak

Az önce elle yazarak bir XML dosyası içeriği oluşturmuştuk ve bunu okumuştuk. Ancak XML oluştururken bize yardımcı olması için çeşitli XML oluşturma fonksiyonları da mevcut aslında.

Yazdığımız buildXMLData() metodunu değiştirerek XML verisini artık XMLDocument fonksiyonları kullanarak oluşturalım. Bunun için önce “gamedata/saves” klasöründeki “mysave.xml” dosyasını silin. Ardından da buildXMLData() metodunun içeriğini tamamen silip şekildeki gibi değiştirin:

public string buildXMLData()
{
	// XmlDocument türünde yeni bir obje oluştur ve bunu xml değişkeninde depola
	XmlDocument xml = new XmlDocument();
	
	// En dış elemanı (root) oluştur
	XmlElement rootElement = xml.CreateElement( "player" );
	xml.AppendChild( rootElement );
	
	return xml.OuterXml;
}

Önce XML dosyamızı tutacak olan “xml” adında bir değişken oluşturuyoruz.

Ardından “player” adında bir XML elemanı (etiketi) oluşturup bunu “rootElement” adındaki değişkene depoluyoruz.

Daha sonra bu “player” etiketini (rootElement’i) alıp xml dosyamızın içine yerleştiriyoruz (append). Çünkü XmlElement oluşturulduğu anda xml dosyasına yerleşmiyor. Biz onu kod yardımıyla nereye yerleştirmek istersek oraya yerleşiyor.

Son olarak oluşturduğumuz bu xml dosyasının içeriğinin tamamını string şeklinde döndürmek için xml.OuterXml komutunu kullanıyoruz.

Eğer uygulamayı çalıştırırsanız xml dosyamızın içeriğini şekildeki gibi göreceksiniz:

<player />

Yani şöyle değil:

<player>
</player>

Bunun sebebi henüz “player” etiketinin içinde hiçbir şey olmaması. Şimdi bu etiketin içini dolduracağız ve etiket birinci şekilden ikinci şekle bürünecek. O halde durmayın ve aşağıdaki kodu xml.OuterXml komutunun hemen berisine yazın:

// player (oyuncu) elemanı için profile elemanı oluştur
XmlElement profileElement = xml.CreateElement( "profile" );

// profileElement'i rootElement'a alt eleman olarak ata
rootElement.AppendChild( profileElement );

Böylece profileElement adında yeni bir XmlElement oluşturuyoruz ve bu etikete isim olarak “profile” veriyoruz. Ardından bunu tutup rootElement’in (player) içine atıyoruz (append).

Şimdi sonucu test ederseniz aşağıdaki xml dosyasıyla karşılaşacaksınız (muhtemelen hepsi tek satırda yazacaktır):

<player>
	<profile />
</player>

Gördüğünüz gibi player elemanının içinde profile adında yeni bir eleman oluştu.

Bizim XML dosyamızda, profile elemanının içinde iki etiket arasında depolanmış veriler olacak. Yani şöyle:

<playername>Bob</playername>
<hp>3500</hp>
... ve benzeri ...

Bu elemanları profile elemanına eklemek için metodumuzun içeriğini şöyle güncelleyelim:

public string buildXMLData()
{
	// XmlDocument türünde yeni bir obje oluştur ve bunu xml değişkeninde depola
	XmlDocument xml = new XmlDocument();
	
	// En dış elemanı (root) oluştur
	XmlElement rootElement = xml.CreateElement( "player" );
	xml.AppendChild( rootElement );
	
	// player (oyuncu) elemanı için profile elemanı oluştur
	XmlElement profileElement = xml.CreateElement( "profile" );
	
	// profileElement'in alt elemanları tutmak için yeni bir XmlElement tanımla
	XmlElement profileInnerElement;
	
	// playername(oyuncuAdi) elemanı oluşturup bunu profile elemanının içine göm (child yap)
	profileInnerElemen = xml.CreateElement( "playername" );
	profileInnerElement.InnerText = "Bob";
	profileElement.AppendChild( profileInnerElement );
	
	// Aynısını hp (can) elemanı için yap
	profileInnerElemen = xml.CreateElement( "hp" );
	profileInnerElement.InnerText = "3500";
	profileElement.AppendChild( profileInnerElement );
	
	// mp (büyü gücü) elemanı için de yap
	profileInnerElemen = xml.CreateElement( "mp" );
	profileInnerElement.InnerText = "500";
	profileElement.AppendChild( profileInnerElement );
	
	// Son olarak da level (bölüm) elemanı için de yap
	profileInnerElemen = xml.CreateElement( "level" );
	profileInnerElement.InnerText = "15";
	profileElement.AppendChild( profileInnerElement );
	
	// profileElement'i rootElement'a alt eleman olarak ata
	rootElement.AppendChild( profileElement );
	
	return xml.OuterXml;
}

Önce profileInnerElement adında yeni bir XmlElement oluşturuyoruz. Sonra bu elemana sırayla “playername”, “hp”, “mp” ve “level” isimlerini veriyoruz ve içeriğini profileInnerElement.InnerText komutu ile aldığı isme uygun şekilde ayarlıyoruz. Son olarak da her yeni eleman oluşturduktan sonra onu profileElement’in içine ekliyoruz (append).

Gördüğünüz gibi kod içinde kendini tekrar eden çok komut kullandık. İlerleyen safhada bu sorundan kurtulmak için yeni bir metod oluşturacağız. Artık uygulamayı çalıştırınca XML dosyasının istediğimiz kıvama yaklaştığını görebiliriz (içindeki playername, hp, mp ve level etiketleriyle).

Şimdi envanter elemanlarını oluşturmak için aşağıdaki kod kümesini “return” komutundan hemen önce yazın:

// inventory (envanter) elemanı oluştur
XmlElement inventoryElement = xml.CreateElement( "inventory" );

// Envanterde bir eşya oluştur
XmlElement inventoryItem = xml.CreateElement( "item" );
inventoryItem.SetAttribute( "name", "esya1" );
inventoryItem.SetAttribute( "qty", "2" ); // qty (quantity): miktar
inventoryElement.AppendChild( inventoryItem );

// inventoryElement'i rootElement'a alt eleman (child) olarak ata
rootElement.AppendChild( inventoryElement );

Neler yaptık:

  1. “inventory” adındaki etiketimizi depolayan inventoryElement değişkenini oluşturduk
  2. Sonra bu etiketin içinde “item” adında yeni bir etiket oluşturup bu yeni etiketi inventoryItem değişkeninde depoladık
  3. Bu elemana “name” adında bir özellik (attribute) ekledik ve değerini “esya1” verdik
  4. Ardından “qty” özelliği ekleyip değerini “2” yaptık
  5. Sonra inventoryItem’ı inventoryElement’in içerisine ekledik (append)
  6. Son olarak da inventoryElement’i rootElement’e (player) ekledik

Artık XML dosyası ilk başta elle oluşturduğumuz haline çok benziyor. Tek fark şu anda envanterde bir eleman var çünkü az önce sadece bir tane oluşturduk.

Kodu Düzenleyelim
Her ne kadar buildXMLData() metodumuz güzel çalışsa da içinde bazı şeyleri çok kez tekrar ettik. Bu tekrarlardan kurtulmak için bir XML elemanı oluşturmaya yarayan yeni bir metod yazalım. Bunu çeşitli parçalara ayırarak yapmak mümkün ama ben yapacağımız herşeyi tek bir metodda birleştireceğim. İşte o meşhur (!) metodum:

// bir XML elemanı (XmlElement) oluşturur
private XmlElement createXMLElement( XmlDocument xmlObject, string elementName, string innerValue, string[] attributeList, string[] attributeValues )
{
	XmlElement element = xmlObject.CreateElement( elementName );
	
	// içteki değeri (inner value) işle
	if( innerValue != null && innerValue != "" )
		element.InnerText = innerValue;
		
	// özellikleri (attributes) işle
	if( attributeList != null )
	{
		int i = 0;
		
		foreach( string attribute in attributeList )
		{
			element.SetAttribute( attribute, attributeValues[i] );
			i++;
		}
	}
	
	return element;
}

Metodumuz bir XmlElement döndürüyor ve 5 parametre alıyor:

  1. Bir XML dosyası (bizim durumumuzda “xml” değişkeni)
  2. Oluşturacağımız etiketin ismi
  3. Etiketin iç değeri (yani iki etiket arasındaki veri; “<etiket ismi>iç değer</etiket ismi>” gibi)
  4. Etiketin tüm özelliklerini (attribute) depolayan bir array
  5. Bu özelliklerin değerlerini (value) depolayan başka bir array

Metod şöyle çalışıyor:

  1. XmlElement element = xmlObject.CreateElement( elementName );
    Girdiğimiz isme sahip yeni bir XML elemanı oluşturuluyor
  2. element.InnerText = innerValue;
    Eğer innerValue’nin değeri null değilse açılış ve kapanış etiketleri arasına bu veri ekleniyor
  1. element.SetAttribute( attribute, attributeValues[i] );
    Eğer etiketin içine özellikler (attribute) eklemek istiyorsak (yani attributeList null değilse) bir foreach döngüsü yapıyoruz ve girilen tüm özellikleri tek tek ekliyoruz.

Bu metod daha da iyileştirilebilir ama şu anda bizim işimizi görmesi yeterli.

Şimdi buildXMLData() fonksiyonumuzda yeni oluşturduğumuz bu gıcır gıcır createXMLElement() metodunu kullanalım. Bunu yapmak için metodun içeriğini şekildeki gibi değiştirin:

// bir XML yapısı oluşturur ve bunu döndürür
public string buildXMLData()
{
	// XmlDocument türünde yeni bir obje oluştur ve bunu xml değişkeninde depola
	XmlDocument xml = new XmlDocument();
	
	// player (oyuncu) elemanı (element) oluştur
	XmlElement rootElement = createXMLElement( xml, "player", "", null, null );
	xml.AppendChild( rootElement );
	
	// Alt eleman (child) ekle
	XmlElement profileElement = createXMLElement( xml, "profile", "", null, null );
	
	// Alt elemanlar ekle
	profileElement.AppendChild( createXMLElement( xml, "playername", "Bob", null, null ) );
	profileElement.AppendChild( createXMLElement( xml, "hp", "3500", null, null ) );
	profileElement.AppendChild( createXMLElement( xml, "mp", "500", null, null ) );
	profileElement.AppendChild( createXMLElement( xml, "level", "15", null, null ) );

	// profile elemanını ana (root) elemana (yani player elemanına) alt eleman olarak ekle
	rootElement.AppendChild( profileElement );
	
	// inventory (envanter) elemanı oluştur
	XmlElement inventoryElement = createXMLElement( xml, "inventory", "", null, null );
	
	// envantere elemanlar ekle
	inventoryElement.AppendChild( createXMLElement( xml, "item", "", new string[] { "name", "qty" }, new string[] { "item 1", "2" } ) );
	inventoryElement.AppendChild( createXMLElement( xml, "item", "", new string[] { "name", "qty" }, new string[] { "item 2", "11" } ) );
	inventoryElement.AppendChild( createXMLElement( xml, "item", "", new string[] { "name", "qty" }, new string[] { "item 3", "1" } ) );
	inventoryElement.AppendChild( createXMLElement( xml, "item", "", new string[] { "name", "qty" }, new string[] { "item 4", "5" } ) );

	rootElement.AppendChild( inventoryElement );
	
	return xml.OuterXml;
}

Artık fonksiyonumuz çok daha nezih duruyor ve yeni bir eleman oluştururken createXMLElement metodumuzu kullanıyor.

Envanter elemanları oluştururken parametre olarak iki array kullanıyoruz:

new string[] { "name", "qty"}
new string[] { "item 1", "2"}

Profil için eleman oluştururken null değer giriyoruz ve bu çok güzel birşey değil. Tavsiyem ileride kendiniz çalışırken createXMLElement metodumuzu farklı görevleri yapan birkaç metoda bölün, böylece daha temiz bir kodunuz olsun.

Bölüm 5: Şifreleme (Encryption)

Bu bölümde XML dosyalarımızı şifrelemeyi göreceğiz. Böylece dosyanın içeriği sadece makine tarafından anlaşılabilir ve oyuncu tarafından istemli şekilde değiştirilemez. XML dosyasını şifrelemek ve şifreli dosyanın şifresini çözmek için ayrı ayrı metodlar yazacağız.

Aslında burada yapacağımız şifreleme işlemini sadece XML değil, tüm dosya türlerine uygulayabilirsiniz.

Şifreleme Deşifreleme Metodlarını Oluşturmak

Önce gerekli tüm kütüphanelerin import edildiğinden emin olun:

using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using System.Text;
using System.Xml;
using System.Security.Cryptography;

Şimdi aşağıdaki fonksiyonları fileManager scriptine ekleyin:

// içine girilen string'i şifreler
public string encryptData( string toEncrypt )
{
	byte[] keyArray = UTF8Encoding.UTF8.GetBytes( "12345678901234567890123456789012" );
	
	// 256-AES anahtarı
	byte[] toEncryptArray = UTF8Encoding.UTF8.GetBytes( toEncrypt );
	RijndaelManaged rDel = new RijndaelManaged();
	
	rDel.Key = keyArray;
	rDel.Mode = CipherMode.ECB;
	
	rDel.Padding = PaddingMode.PKCS7;
	
	ICryptoTransform cTransform = rDel.CreateEncryptor();
	byte[] resultArray = cTransform.TransformFinalBlock( toEncryptArray, 0, toEncryptArray.Length );
	
	return Convert.ToBase64String( resultArray, 0, resultArray.Length );
}

// içine girilen şifreli metni deşifre eder
public string decryptData( string toDecrypt )
{
	byte[] keyArray = UTF8Encoding.UTF8.GetBytes( "12345678901234567890123456789012" );
	
	// 256-AES anahtarı
	byte[] toEncryptArray = Convert.FromBase64String( toDecrypt );
	RijndaelManaged rDel = new RijndaelManaged();
	
	rDel.Key = keyArray;
	rDel.Mode = CipherMode.ECB;
	
	rDel.Padding = PaddingMode.PKCS7;
	
	ICryptoTransform cTransform = rDel.CreateDecryptor();
	byte[] resultArray = cTransform.TransformFinalBlock( toEncryptArray, 0, toEncryptArray.Length );
	
	return UTF8Encoding.UTF8.GetString( resultArray );
}

Bu metodlar Microsoft’un şifreleme örnekleri baz alınarak hazırlanmış olup şu kaynaktan alınmış, üzerinde hafifçe oynanmıştır: http://unitynoobs.blogspot.co.uk/2012/01/xml-encrypting.html

ÇEVİRMEN NOTU: Bence kriptolojiye özel bir ilginiz yoksa bu metodların nasıl çalıştığını anlamaya çalışmayın, onları sadece kullanın gitsin. Ne de olsa işlerini bir güzel yapıyorlar.

CreateXMLFile Metodunu Güncellemek

createXMLFile() metodunu şöyle güncelleyin:

// yeni bir XML dosyası oluşturur
public void createXMLFile( string directory, string filename, string filetype, string fileData, string mode )
{
	print( directory + " konumunda XML dosyası oluşturuluyor" );
	
	if( checkDirectory( directory ) )
	{
		if( mode == "plaintext" )
			File.WriteAllText( path + "/" + directory + "/" + filename + "." + filetype, fileData );
		else if( mode == "encrypt" )
		{
			fileData = encryptData( fileData );
			File.WriteAllText( path + "/" + directory + "/" + filename + "." + filetype, fileData );
		}
	}
	else
	{
		print( directory + " konumu var olmadığından dosya oluşturulamıyor" );
	}
}

Artık eğer “mode” parametresine değer olarak “encrypt” atanırsa önce XML verisini encryptData() metodu ile şifreliyor, ondan sonra XML dosyasına yazıyoruz.

ParseXMLFile() Metodunu Güncellemek

Şifrelenmiş dosyaları açabilmesini sağlamak için parseXMLFile() metodunun içeriğini şöyle değiştirin:

// bir XML dosyasının içeriğini okur
public void parseXMLFile( string directory, string filename, string filetype, string mode )
{
	print( directory + " konumundaki XML okunuyor" );
	
	XmlDocument xmlDoc = new XmlDocument();
	
	// Düz yazı şeklindeki XML dosyasını oku
	if( mode == "plaintext" )
	{
		xmlDoc.Load( path + "/" + directory + "/" + filename + "." + filetype );
	}
	else if( mode == "encrypt" )
	{
		print( "Şifrelenmiş dosya yükleniyor" );
		
		// Şifrelenmiş veriyi filedata'ya ata
		string filedata = readFile( directory, filename, filetype );
		
		// Veriyi deşifre et
		filedata = decryptData( filedata );
		
		// Geçici bir dosya oluştur
		createFile( directory + "/", "tmp_" + filename, filetype, filedata );
		
		// Geçici dosyayı oku
		xmlDoc.Load( path + "/" + directory + "/tmp_" + filename + "." + filetype );
	}
	
	// Önce profile elemanındaki veriyi oku
	XmlNodeList profileList = xmlDoc.GetElementsByTagName( "profile" );
	
	foreach( XmlNode profileInfo in profileList )
	{
		XmlNodeList profileContent = profileInfo.ChildNodes;
		
		foreach( XmlNode profileItems in profileContent )
		{
			if( profileItems.Name == "playername" )
				print( "playername: " + profileItems.InnerText );
			else if( profileItems.Name == "hp" )
				print( "hp: " + profileItems.InnerText );
			else if( profileItems.Name == "mp" )
				print( "mp: " + profileItems.InnerText );
			else if( profileItems.Name == "level" )
				print( "level: " + profileItems.InnerText );
		}
	}
	
	// Sonra inventory elemanındaki veriyi oku
	XmlNodeList inventoryList = xmlDoc.GetElementsByTagName( "inventory" );
	
	foreach( XmlNode inventoryInfo in inventoryList )
	{
		XmlNodeList inventoryContent = inventoryInfo.ChildNodes;
		
		foreach( XmlNode inventoryItems in inventoryContent )
		{
			print( "Item: " + inventoryItems.Attributes[ "name" ].Value + " x" + inventoryItems.Attributes[ "qty" ].Value );
		}
	}
	
	// Geçici dosyayı sil
	if( mode == "encrypt" )
		deleteFile( directory + "/" + "tmp_" + filename + "." + filetype );
}

Metodun üzerinde şöyle oynamalar yaptık:

  1. “plaintext”li if() ifadesinin içindeki çoğu komutu dışarı taşıdık
  2. Eğer mode “plaintext” ise XML dosyasını direkt açtık
  3. Eğer mode “encrypt” ise şifrelenmiş dosyanın içeriğini filedata’ya aktardık ve bu değişkeni decryptData() fonksiyonunda kullanarak şifrelenmiş veriyi deşifre ettik
  4. Şifrelenmiş dosyanın okunduğu if() ifadesinin içinde geçici bir XML dosyası oluşturup deşifre edilmiş veriyi içine yazdık ve bu XML dosyasını sanki şifrelenmemiş bir XML açar gibi Load metodu ile açtık

Ardından XML’deki etiketler arasındaki veriyi aynı şekilde okuduk. Yani tek fark XML dosyası şifreliyse önce onu deşifre ettik.

Son olarak da eğer “encrypt” modundaysak oluşturduğumuz bu geçici dosyayı metodun en sonunda sildik.

Şifrelenmiş XML dosyalarınızı test etmek için initialize() metodundaki XML oluşturma ve okuma metodlarında “mode” parametresinin değerini “plaintext”ten “encrypt”e çevirin.

Son Söz

Böylece bu uzun dosya yönetimi ve XML tutorialinin sonuna geldik. İhtiyacınız olabilecek çoğu dosya yönetim fonksiyonunu size göstermeye çalıştım. Tabi ki daha öğrenebileceğiniz çok şey var; XML Serialization gibi. Bu yöntem oldukça kullanışlıdır ve zor da değildir. Ayrıca hakkında yazılmış pek çok doküman vardır. Ancak kusursuz değildir. Bu yüzden de XMLDocument fonksiyonlarının nasıl çalıştığını, onlarla içli dışlı olarak gördük.

Umarım ki faydalı olmuştur. Başka derslerde görüşmek üzere!

yorum
  1. Erol Ş. dedi ki:

    serialization stream supports seeking but its length is 0 şeklinde hata alıyorum hocam neden olabilir?

    Kod kısmı şu şekilde :
    public void Save()
    {
    FileStream file = null;

    try
    {
    BinaryFormatter bf = new BinaryFormatter();
    file = File.Create(Application.persistentDataPath + “/GameData.dat”);

    if (data != null)
    bf.Serialize(file, data);
    }
    catch (Exception e)
    {
    print(Application.dataPath.ToString());
    print(e.Message.ToString());
    throw;
    }
    finally
    {
    if (file != null)
    file.Close();
    }
    }

    public void Load()
    {
    FileStream file = null;
    try
    {
    BinaryFormatter bf = new BinaryFormatter();
    file = File.Open(Application.persistentDataPath+”/GameData.dat”, FileMode.Open);

    data = (GameData)bf.Deserialize(file);

    }
    finally
    {
    if (file != null)
    file.Close();
    }
    }

    }

    [Serializable]
    class GameData
    {
    private bool isGameStartedFirstTime;
    private bool isMusicOn;
    }

    • yasirkula dedi ki:

      GameData’nın değişkenlerini public yapmayı deneyin ve Load yapmadan önce, hedef konumdaki kayıt dosyasının var olup olmadığını kontrol edin.

  2. Altay dedi ki:

    Merhaba Yasin bey. Soracağım soru belki bu konu ile alakalı olmayabilir. Ben web sitesinde resimleri göstermek için img tagını kullanıyorum. img tagının src sine resmin klasör uzantısını verdiğimde resim sayfada gözüküyor. Unity de bu işlemi yapmak için ilk önce Bir gameObject oluşturuyorum. İçine Asset dosyamdaki resimleri atıyorum. Sonra istedigim sahnede kod yardımı ile o resimleri çekiyorum GameObjectden. Bunu web sayfasındaki gibi Asset dosyasından kod yardımı ile resmin yolunu versem image Componentime diye düşündüm. Böylelikle GameObject e ihtiyaç duymam. Acaba bu işin böyle bir yolu var mı?

    • yasirkula dedi ki:

      Resources klasörünü kullanabilirsiniz. Buraya attığınız asset’lere Resources.Load ile erişebilirsiniz. Ancak buraya attığınız her asset, oyunda kullanılmasa bile build’e dahil olup oyunun boyutunu artırır o yüzden kullanmadığınız şeyleri buraya koymayın.

  3. Ömer Kulaoğlu dedi ki:

    Selamlar ! Ben mobil uygulama geliştiriyorum (IOS,Android).Uygulamamda bir obje kilitli olduğunda değeri 0 oluyor.Ödüllü reklam izletiyorum ve Objenin değerini 1 yaptırıyorum.Bu sayede kullanıcıya objeyi açmış oluyorum.Uygulama kapatılıp açıldığında start fonksiyonu ile Objenin değerini kontrol edip eğer daha önceden o değer 1 olmuş ise o objeyi kilitlememesini istiyorum.Bunun için save sistemi kullanmalıyım.Bu yazınızı okudum ama benim örneğim için fazla olduğunu düşünüyorum.Bu save sistemi için hangi yolu uygulamalıyım ?
    //[BinnaryFormat uygulanarak telefonda klasör oluşturup save alma işleminide fazla buldum ve İos telefonlarda çalışırmı öğrenemedim sizin bir bilginiz varmı acaba ?]

    İkinci bir sorum ise Uygulama içi satın alma sistemlerinde objenin ödeme sonrası kullanıcıya kayıtlı kalması nasıl sağlanıyor (Veri tabanı üzerinden mi,Şifrelenmiş save dosyalarıyla mı vb. ?)

    • yasirkula dedi ki:

      Basit değerleri kaydetmenin en kolay yolu PlayerPrefs kullanmak ancak güvenli bir çözüm değil. Daha güvenli olan BinaryFormatter’ı her platformda kullanabilirsiniz; Application.persistentDataPath klasörüne save dosyanızı kaydedebilirsiniz.

      Uygulama içi satın almalar dediğiniz gibi veritabanında tutulur, bunun için Google Play’in veya iOS App Store’un kendi SDK’leri kullanılır yani kendi veritabanınızı oluşturmanıza gerek yok. Konu hakkında bilgi almak için “Unity IAP” şeklinde arama yapabilirsiniz.

      • Ömer Kulaoğlu dedi ki:

        Cevaplarınız için çok teşekkür ederim.Sağolun.

Cevap Yazın

Bu site, istenmeyenleri azaltmak için Akismet kullanıyor. Yorum verilerinizin nasıl işlendiği hakkında daha fazla bilgi edinin.