Unity Oyun İçi Dosya Seçme/Kaydetme Penceresi (Android Destekli)

Yayınlandı: 05 Haziran 2018 yasirkula tarafından Oyun Tasarımı, UNITY 3D içinde

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!

yorum
  1. Hüseyin dedi ki:

    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.

  2. Hüseyin dedi ki:

    İ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?

  3. ali kaza dedi ki:

    İ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?

    • ali kaza dedi ki:

      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?

    • yasirkula dedi ki:

      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 ) );
      }

      • ali kaza dedi ki:

        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ı ?

      • ali kaza dedi ki:

        Şö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

      • yasirkula dedi ki:

        Oyun başladığında Destroy edeceğiniz için sıkıntı görmüyorum. İsterseniz akabinde Resources.UnloadUnusedAssets fonksiyonunu da çağırabilirsiniz.

  4. fatih dedi ki:

    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?

  5. Haktan dedi ki:

    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.

    • yasirkula dedi ki:

      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.

Cevap Yazın

Aşağıya bilgilerinizi girin veya oturum açmak için bir simgeye tıklayın:

WordPress.com Logosu

WordPress.com hesabınızı kullanarak yorum yapıyorsunuz. Çıkış  Yap /  Değiştir )

Twitter resmi

Twitter hesabınızı kullanarak yorum yapıyorsunuz. Çıkış  Yap /  Değiştir )

Facebook fotoğrafı

Facebook hesabınızı kullanarak yorum yapıyorsunuz. Çıkış  Yap /  Değiştir )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.