Yeniden merhabalar,
Bu Unity dersinde, oyun esnasında nasıl dosya seçme veya kaydetme diyaloğu gösterebileceğimizi göreceğiz. Bu diyalog, Windows’a ilaveten Android platformunu da desteklemekte. Deneme şansım olmasa da büyük olasılıkla Mac ve Linux platformları da sorunsuz bir şekilde destekleniyordur. Ancak Universal Windows Platform (UWP) ve WebGL desteklenmemekte.
Asset Store: https://assetstore.unity.com/packages/tools/gui/runtime-file-browser-113006
Alternatif link: https://github.com/yasirkula/UnitySimpleFileBrowser/releases

Detaylar için yazının devamını okuyabilirsiniz…
Kurulum
İndirdiğiniz unitypackage‘ı Assets-Import Package yoluyla projenize import edin.
Eğer plugin’i Android’de kullanmayı düşünüyorsanız, Edit-Project Settings-Player‘dan Write Permission‘ı External (SDCard) yapın.
Kullanım
Plugin’i kullanmaya yarayan fonksiyonlar, FileBrowser sınıfının içerisinde bulunmakta. Ancak bu sınıfa erişmeden önce, kodunuzun başına using SimpleFileBrowser;
eklemeniz lazım.
A) Diyalog Göstermek
bool ShowSaveDialog( OnSuccess onSuccess, OnCancel onCancel, PickMode pickMode, bool allowMultiSelection = false, string initialPath = null, string initialFilename = null, string title = "Save", string saveButtonText = "Save" )
Dosya kaydetme diyaloğu gösterir. Plugin’in mevcut sürümünde bu fonksiyon sürekli true döndürür. Eğer şu anda gösterilmekte olan aktif bir diyalog var mı kontrol etmek isterseniz, FileBrowser.IsOpen‘ın değerine bakabilirsiniz (true ise aktif bir diyalog vardır).
onSuccess: Dosya(lar)ın kaydedileceği konum(lar) seçildikten sonra bu fonksiyon çağrılır. Buraya string[] parametre alan bir fonksiyon girilmek zorundadır. Kullanıcının dosya kaydetmek için seçtiği konum(lar) bu array’de depolanır. Eğer allowMultiSelection‘ın değeri false ise, bu array sadece 1 elemandan oluşur, yoksa 1 veya daha fazla elemandan oluşur.
onCancel: Dosya kaydetme işlemi iptal edilirse bu fonksiyon çağrılır. Bu fonksiyon bir parametre almaz. Eğer işlem iptal edilince özel bir şey yapmak istemiyorsanız, onCancel’a değer olarak null verebilirsiniz.
pickMode: Değeri PickMode.Files olursa kullanıcı sadece dosyaları seçebilir, PickMode.Folders olursa kullanıcı sadece klasörleri seçebilir, PickMode.FilesAndFolders olursa kullanıcı hem dosyaları hem de klasörleri seçebilir.
allowMultiSelection: Değeri true olursa, kullanıcı birden çok dosya/klasör seçebilir.
initialPath: Diyalog ilk açıldığında hangi klasörün içinde olacağımızı belirler. Varsayılan olarak diyalog Belgelerim ile başlar.
initialFilename: Diyalog ilk açıldığında, kaydedilecek dosya isminin girildiği input field bu değer ile doldurulur.
title: Diyaloğun başlığında yazacak olan yazıyı belirler.
saveButtonText: Diyaloğun kaydet butonunda yazacak olan yazıyı belirler.
bool ShowLoadDialog( OnSuccess onSuccess, OnCancel onCancel, PickMode pickMode, bool allowMultiSelection = false, string initialPath = null, string initialFilename = null, string title = "Load", string loadButtonText = "Select" )
Dosya seçme diyaloğu gösterir. loadButtonText, diyaloğun seç butonunda yazacak olan yazıyı belirler.
IEnumerator WaitForSaveDialog( PickMode pickMode, bool allowMultiSelection = false, string initialPath = null, string initialFilename = null, string title = "Save", string saveButtonText = "Save" )
ShowSaveDialog gibi, bu fonksiyon da dosya kaydetme diyaloğu gösterir. Ancak bu fonksiyon bir IEnumerator döndürdüğü için, yield komutu vasıtasıyla bir coroutine içerisinde bekletilebilir. Diyalog kapanana kadar yield devam eder.
Diyalog kapatıldıktan sonra, işlemin iptal edilip edilmediğini öğrenmek için FileBrowser.Success‘in değeri kontrol edilebilir. Eğer değeri true ise işlem iptal edilmemiştir. Bu durumda dosya(lar)ın kaydedileceği konum(lar) FileBrowser.Result‘ta depolanır.
IEnumerator WaitForLoadDialog( PickMode pickMode, bool allowMultiSelection = false, string initialPath = null, string initialFilename = null, string title = "Load", string loadButtonText = "Select" )
ShowLoadDialog‘un WaitForSaveDialog gibi coroutine destekleyen versiyonudur.
void HideDialog( bool invokeCancelCallback = false )
Aktif bir diyalog varsa onu zorla kapatmaya yarar. Eğer invokeCancelCallback‘in değeri true ise, o diyaloğu açan fonksiyonun onCancel‘ı çalıştırılır.
B) Diyaloğu Kişiselleştirmek
bool AddQuickLink( string name, string path, Sprite icon = null )
Diyaloğun hızlı erişim menüsüne yeni bir klasör ekler. name, bu klasörün ismini belirlerken path ise klasörün konumunu tutar. Eğer bir ikon belirlenmezse, varsayılan olarak klasör ikonu kullanılır.
void SetExcludedExtensions( params string[] excludedExtensions )
Yoksayılacak dosya uzantılarını belirler. Varsayılan olarak, kısayol uzantısı olan .lnk ve sistem geçici dosya uzantısı olan .tmp uzantılı dosyalar yoksayılır.
void SetFilters( bool showAllFilesFilter, params string[] filters )
Dosya uzantılarını filtrelemeye yarar. Eğer showAllFilesFilter’ın değeri true ise, tüm dosya uzantılarını göstermeye yarayan “All Files (.*)” isimli bir filtre de listeye eklenir. Klasör seçme/kaydetme modunda bu filtrelerin bir etkisi yoktur.
void SetFilters( bool showAllFilesFilter, params FileBrowser.Filter[] filters )
Dosya uzantılarını filtrelemenin daha gelişmiş bir yoludur. Bir önceki fonksiyonda her bir dosya uzantısı ayrı bir filtre olurken bu fonksiyonda örneğin .jpeg ve .png uzantılarını tek bir filtrede birleştirmek mümkündür. FileBrowser.Filter objesinin constructor’ı, parametre olarak filtre için bir isim ve ardından filtrelenecek uzantıları alır.
bool SetDefaultFilter( string defaultFilter )
Diyalog açıldığında varsayılan olarak aktif olacak olan filtreyi belirler.
C) Çalışma Zamanı İzinleri (Runtime Permissions) Hakkında
Android 6.0 sürümü itibariyle artık önemli bir Android fonksiyonuna erişmeden önce, çalışma zamanında bu fonksiyona erişim izni istemek zorundayız. FileBrowser fonksiyonları, çalışmadan önce otomatik olarak cihazdaki dosyalara erişim izni isterler ancak dilerseniz kendi başınıza da bu iznin durumunu sorgulayabilir veya izin isteyebilirsiniz.
Permission CheckPermission()
Cihazdaki dosyalara erişim izninin durumunu sorgular ve bir FileBrowser.Permission enum‘u döndürür. Eğer dosyalara erişim iznimiz varsa, bu enum’un değeri Permission.Granted olurken eğer henüz iznimiz yoksa Permission.ShouldAsk olur. Eğer kullanıcı karşısına gelen izin ekranını “Bir daha sorma” seçili bir şekilde reddederse veya kullanıcının cihazında aktif olan bir ebeveyn kontrol sistemi bu iznin verilmesini engelliyorsa, Permission.Denied döndürülür. Bu durumda kullanıcı izni cihazın ayarlar menüsünden elle vermek zorundadır.
Eğer dosyalara erişim iznimiz yoksa, diyalog güvenlik sebebiyle çoğu klasörün içini gösteremeyecektir.
Permission RequestPermission()
Dosyalara erişim izni ister ve sonucu bir FileBrowser.Permission enum’unda döndürür. Diyalog göstermeye yarayan fonksiyonlar, bu fonksiyonu otomatik olarak çağırırlar.
D) FileBrowserHelpers Fonksiyonları Hakkında
Bu plugin, Android 10 ve üzerinde Storage Access Framework (SAF) kullanmaktadır çünkü Android işletim sistemi bu sürümlerde dosya sistemine normal erişimi kısıtlamıştır. SAF sisteminden döndürülen konumlar, aşina olduğumuz “C:\Bir Klasör\Bir dosya.txt” şeklindeki konumlardan farklıdır (örnek: content://com.android.externalstorage.documents/tree/primary%3A/document/primary%3APictures). Bu SAF konumları File.Copy veya File.ReadAllBytes gibi normal File fonksiyonları ile çalışmazlar. Eğer oyununuzu Android’e çıkarmayacaksanız bu önemli değil. Aksi taktirde, File işlemlerinizin Android dahil tüm işletim sistemlerinde çalışması için FileBrowserHelpers sınıfının fonksiyonlarını kullanmalısınız (yani File.ReadAllBytes yerine FileBrowserHelpers.ReadAllBytes gibi). Tüm FileBrowserHelpers fonksiyonlarını şu sayfanın sonunda bulabilirsiniz: https://github.com/yasirkula/UnitySimpleFileBrowser/
E) Sıkça Sorulan Sorular
- Android’e build alırken “error: attribute android:requestLegacyExternalStorage not found” hatası alıyorum
AndroidManifest.xml‘deki android:requestLegacyExternalStorage
satırı, Android 10’da cihazın dosya sistemine tam erişim sağlar ve Storage Access Framework’ün Android 10 yerine Android 11 ve üzerinde devreye girmesini sağlar. Ancak bu satırın çalışması için, Android SDK’inizde minimum SDK 29‘un kurulu olması gerekmektedir. Eğer SDK 29 veya üzerini kurmanız kesinlikle mümkün değilse, plugin’in SimpleFileBrowser.aar dosyasını WinRAR veya 7-Zip ile açıp AndroidManifest.xml dosyasına çift tıkladıktan sonra, <application ... />
satırını silmeli ve ardından önce dosyayı, sonra da arşivi kaydetmelisiniz.
- Android’de dosya diyaloğu göstermek istediğimde Logcat’te “java.lang.ClassNotFoundException: com.yasirkula.unity.FileBrowserPermissionReceiver” hatası alıyorum
Projeniz ProGuard kullanıyor olabilir. Bu durumda, Player Settings‘ten User Proguard File seçeneğini açıp, oluşan dosyaya şu satırı ekleyin: -keep class com.yasirkula.unity.* { *; }
- Android 10 ve üzerinde hiçbir dosya göremiyorum
Bu Android sürümlerinde Storage Access Framework kullanıldığı için sistem biraz farklı çalışmakta. Kullanıcının önce diyalogdaki Browse… butonuna tıklayıp ardından hangi klasörün içeriğini gezmek istiyorsa o klasörü seçmesi gerekiyor.
Örnek Kod
using UnityEngine;
using System.Collections;
using System.IO;
using SimpleFileBrowser;
public class FileBrowserTest : MonoBehaviour
{
// Not1: FileBrowser'un döndürdüğü konumların sonunda '\' karakteri yer almaz
// Not2: FileBrowser tek seferde sadece 1 diyalog gösterebilir
void Start()
{
// Filtreleri belirle (opsiyonel)
// Eğer filtreler oyun esnasında hep aynı kalacaksa, filtreleri her seferinde
// tekrar tekrar belirlemek yerine sadece bir kere belirlemek yeterlidir
FileBrowser.SetFilters( true, new FileBrowser.Filter( "Resimler", ".jpg", ".png" ), new FileBrowser.Filter( "Metin Dosyaları", ".txt", ".pdf" ) );
// Varsayılan filtreyi belirle (opsiyonel)
// Eğer varsayılan filtre başarıyla belirlendiyse fonksiyon true döndürür
// Bu örnekte, varsayılan filtre olarak .jpg'i tutan "Resimler"i belirle
FileBrowser.SetDefaultFilter( ".jpg" );
// Yoksayılacak dosya uzantılarını belirle (opsiyonel) (varsayılan olarak .lnk ve .tmp uzantılı dosyalar yoksayılır)
// Bu fonksiyonu çağırırsanız, siz elle eklemediğiniz müddetçe .lnk ve .tmp uzantıları artık yoksayılmaz
FileBrowser.SetExcludedExtensions( ".lnk", ".tmp", ".zip", ".rar", ".exe" );
// Yeni bir hızlı erişim klasörü ekle (opsiyonel) (eğer işlem başarılı olursa true döndürülür)
// Bir hızlı erişim klasörünü sadece bir kere eklemek yeterlidir
// İsim: Kullanıcılar
// Konum: C:\Users
// İkon: varsayılan (klasör ikonu)
FileBrowser.AddQuickLink( "Kullanıcılar", "C:\\Users", null );
// Dosya kaydetme diyaloğu göster
// onSuccess: null, bir şey yapma (bir başka deyişle, bu amaçsız bir diyalog)
// onCancel: null, bir şey yapma
// Kaydetme modu: sadece dosyalar, Birden çok dosya seçebilme: kapalı (false)
// İlk konum: "C:\", Varsayılan dosya ismi: "Resim.png"
// Başlık: "Farklı Kaydet", Kaydet butonu yazısı: "Kaydet"
// FileBrowser.ShowSaveDialog( null, null, FileBrowser.PickMode.Files, false, "C:\\", "Resim.png", "Farklı Kaydet", "Kaydet" );
// Klasör seçme diyaloğu göster
// onSuccess: klasörün konumunu konsola yazdır
// onCancel: konsola "İptal edildi" yazdır
// Dosya seçme modu: sadece klasörler, Birden çok klasör seçebilme: kapalı (false)
// İlk konum: varsayılan (Belgelerim), Varsayılan dosya ismi: boş
// Başlık: "Klasör Seç", Seç butonu yazısı: "Seç"
// FileBrowser.ShowLoadDialog( ( konum ) => { Debug.Log( "Seçilen klasör: " + konum[0] ); },
// () => { Debug.Log( "İptal edildi" ); },
// FileBrowser.PickMode.Folders, false, null, null, "Klasör Seç", "Seç" );
// Coroutine örneğini çalıştır
StartCoroutine( DosyaVeKlasorSecmeDiyaloguGosterCoroutine() );
}
IEnumerator DosyaVeKlasorSecmeDiyaloguGosterCoroutine()
{
// Dosya ve klasör seçme diyaloğu göster ve kullanıcının diyaloğu kapatmasını bekle
// Dosya seçme modu: hem dosyalar hem klasörler, Birden çok dosya/klasör seçebilme: açık (true)
// İlk konum: varsayılan (Belgelerim), Varsayılan dosya ismi: boş
// Başlık: "Yüklenecek Dosya ve Klasörleri Seçin", Seç butonu yazısı: "Yükle"
yield return FileBrowser.WaitForLoadDialog( FileBrowser.PickMode.FilesAndFolders, true, null, null, "Yüklenecek Dosya ve Klasörleri Seçin", "Yükle" );
// Diyalog kapatıldı
// Konsola, kullanıcının en az 1 dosya ve/veya klasör seçip seçmediğini yazdır (FileBrowser.Success)
Debug.Log( FileBrowser.Success );
if( FileBrowser.Success )
{
// Seçilen dosya ve/veya klasör(ler)in konumunu da yazır (FileBrowser.Result) (eğer FileBrowser.Success false ise, değeri null'dır)
for( int i = 0; i < FileBrowser.Result.Length; i++ )
Debug.Log( FileBrowser.Result[i] );
// Seçilen ilk dosyanın byte'larını FileBrowserHelpers vasıtasıyla oku
// File.ReadAllBytes'in aksine, bu fonksiyon Android 10 ve üzerinde de çalışır
byte[] bytes = FileBrowserHelpers.ReadBytesFromFile( FileBrowser.Result[0] );
// Veya, ilk dosyayı persistentDataPath konumuna kopyala
string hedefKonum = Path.Combine( Application.persistentDataPath, FileBrowserHelpers.GetFilename( FileBrowser.Result[0] ) );
FileBrowserHelpers.CopyFile( FileBrowser.Result[0], hedefKonum );
}
}
}
Sonraki derslerde görüşmek dileğiyle!
Yasir Hocam!
Bir hata alıyorum. Ama bu hatayı sadece Android telefonumda alıyorum. Editörde gayet düzgün çalışıyor. Hata şu:
UnauthorizedAccessException: Access to the path “/playerData.bin” is denied.
System.IO.FileStream..ctor (System.String path, System.IO.FileMode mode, System.IO.FileAccess access, System.IO.FileShare share, System.Int32 bufferSize, System.Boolean anonymous, System.IO.FileOptions options) (at :0)
System.IO.FileStream..ctor (System.String path, System.IO.FileMode mode, System.IO.FileAccess access, System.IO.FileShare share, System.Int32 bufferSize, System.Boolean isAsync, System.Boolean anonymous) (at :0)
System.IO.FileStream..ctor (System.String path, System.IO.FileMode mode, System.IO.FileAccess access) (at :0)
(wrapper remoting-invoke-with-check) System.IO.FileStream..ctor(string,System.IO.FileMode,System.IO.FileAccess)
Save_Load.FileControl () (at :0)
GUI.Start () (at :0)
Şimdiden yardımlarınız için çok teşekkür ederim.
Hatayı veren kod nasıl bir şey?
İyi günler Yasir Hocam. Benim bir sorunum var. Oyunumdaki puanı kayıt etmek istiyorum. PlayerPrefs.setInt ile denedim olmadı. Diğer save sistemi ile denedim olmadı. Editörde kayıt ediyor ve geri erişebililiyorum. Fakat android çıktı alıp telefona kurunca telefonda puanımı kaydetmiyor. Uygulamam telefona kurulurken, bu uygulama hiç bir izin istemiyor diyor. Acaba ayrı bir kodda izin mi isteyeceğiz puan vs kayıt etmek için?
Bir izin gerekmiyor. SetInt’ten sonra PlayerPrefs.Save(); fonksiyonunu çağırmayı deneyin.
Sağolun. Var olun hocam. Çok teşekkür ederim.
İyi günler. Benim sormak istediğim bir konu var. Unity üzerinde çokça ağaç objem var. Oyun başladığında ve ağaçlardan birkaçını kestikten sonra oyunu kapatıp yeniden açtığımda kestiğim ve destroy ettiğim objelerin yeniden gelmesini istemiyorum. Bunun için nasıl bir yol izleyebilirim? Objeleri oyun başlarken spawnlamayı ve kesinlenleri spawnlamamayı düşündüm fakat oldukça fazla obje var sistemi hantallaştırır mı böyle bir yöntem?
Destroy edilenleri kaydedip oyun tekrar açıldığında destroy etmeyi de düşündüm fakat kesilen ağaç miktarı arttıkça sistem yine hantallaşacak acaba nasıl bir çözüm bulabiliriz?
Her ağacın bir id’si olur (int, string vb. olabilir), bu id her ağaç için farklı değere sahip olmak zorunda. Ardından kestiğiniz ağaçların id’lerini bir List veya HashSet’te depolayıp oyunu kaydederken bu veriyi de kaydedin. Kayıtlı oyunu yüklediğinizde ise, spawn etmek istediğiniz ağaçların id’lerinin List/HashSet’te olup olmadığına bakın ve eğer oradaysa, ağacı spawn etmeyin. İsterseniz id olarak ağaçların pozisyonlarını kullanmayı deneyebilirsiniz:
public Vector3Int AgacIDOlustur( Vector3 agacPozisyon )
{
return new Vector3Int( Mathf.RoundToInt( agacPozisyon.x * 5f ), Mathf.RoundToInt( agacPozisyon.y * 5f ), Mathf.RoundToInt( agacPozisyon.z * 5f ) );
}
Sahne içerisine yaklaşık 100 ağaç ekleyip render alıyorum. Oyun başladıktan sonra ağaç spawnlamıyorum sadece destroy işlemi yapıyorum. Her sahne yüklerken 100 ağaç spawnlarsam sistemi kastırmaz mı ?
Şöyle düşünüyorum, oyun içerisinde 120, 130 adet ağaç var. Kesilenlerin idlerini kaydedip yeniden oyun başladığında bu idlere sahip ağaçları destroy etmek. 50 ağaç kesildiyse aynı anda 50 ağacı destroy etmek oyun performansını etkilemez mi? Destroy komutunu ne kadar arayla kullanırsam bana en az zararı verir bunu öğrenmek istiyorum
Oyun başladığında Destroy edeceğiniz için sıkıntı görmüyorum. İsterseniz akabinde Resources.UnloadUnusedAssets fonksiyonunu da çağırabilirsiniz.
Assets\button\NewBehaviourScript.cs(55,52): error CS1503: Argument 1: cannot convert from ‘bool’ to ‘SimpleFileBrowser.FileBrowser.PickMode’
Assets\button\NewBehaviourScript.cs(55,59): error CS1503: Argument 2: cannot convert from ” to ‘bool’
Hocam bu hataları alıyorum ve çözemedim. Sorun ne olabilir?
Güncelleme ile fonksiyonun parametrelerinde değişiklik yapmıştım ama dersi güncellemeyi unutmuşum. Şimdi dersi güncelledim.
Assets\button\FileBrowserTest.cs(78,33): error CS0103: The name ‘Path’ does not exist in the current context
şimdi de bu hatayı alıyorum hocam 😦
using System.IO;
Selamun aleyküm, Hocam seçtiğim txt dosyasını yükle dedim nereye gittiğine dair bilgi yok,yani seçtiğim txt dosyasını verisi hangi kod kısmında acaba? txt dosyasını okuyup telefon ekranına göstermem için yardımınız gerekli. Teşekkürler.
Aleykümselam. Plugin sadece seçilen dosyanın konumunu string olarak döndürüyor, daha sonra bu dosyayı işlemek size kalmış. Örneğin dosyanın içeriğini okumak için File.ReadAllText fonksiyonunu kullanabilirsiniz.