Overview
com.111percent.scripting.percent-cloud-storage
Feature
Firebase Cloud Storage 라이브러리를 이용하여 특정 버킷에 업로드 및 다운로드 기능을 제공하는 패키지입니다.
Note
FirebaseSDK 종속성이 없습니다. (Firebase SDK 라이브러리를 사용하지 않음)
이게 뭔가요?
UnityWebRequest 객체를 패킹하여 버퍼 업로드 및 다운로드 속도를 높인 패키지입니다.
- Google FirebaseStorage 서비스를 사용할 수 있습니다.
- Unity LLAPI 기능을 구현된 API로 사용하거나 커스터마이징 하여 사용할 수 있습니다.
- 필요에 따라 GC 할당을 방지할 수 있는 Unity.Collections.NativeArray
형식을 사용할 수 있습니다.
왜 필요한가요?
기존 Percent LocalSave-Cloud 패키지(legacy Percent LocalSave Add-On)를 개선 및 UnityWebRequest 기능을 다른 방향으로 활용할 수 있도록 리뉴얼 패키지 입니다.
- FirebaseSDK 종속성이 없습니다. (Firebase SDK 라이브러리를 사용하지 않음)
- Unity LLAPI를 활용한, FirebaesStorage 기능의 오버헤드를 제거
- Unity LLAPI 내부 구현 및 Unity.Collection 라이브러리를 사용하여 GC 방지.
- 샘플 프로젝트에서 지원하는 다운로드 및 업로드 함수는 GC 할당을 적게 또는 할당하지 않음.
- UniTask 지원 (2.4.0 Upper)
- 패키지에서 제공하는 구현된 LLAPI 기능을 디폴트 또는 커스텀으로 사용 가능.
- 프로젝트에서 StreamingAssets 폴더에 파일을 다운로드할 수 있는 샘플 프로젝트가 동봉되어 있습니다. (Common)
- StreamingAssets.cs
- StreamingAssetsFileService.cs
- 파이어베이스 기능을 쉽게 사용할 수 있는 샘플 코드 제공. (Common)
- FirebaseCloud.cs
- 커스터마이즈한 PercentWebRequest를 사용할 수 있으며 특수한 상황에서도 사용할 수 있도록 API 기능을 제공합니다. (Local, Cloud, REST API etc..)
- 프로젝트에서 StreamingAssets 폴더에 파일을 다운로드할 수 있는 샘플 프로젝트가 동봉되어 있습니다. (Common)
Note
이 패키지는 FirebaseStorage 기능에 대해 종속적이지 않습니다. (Firebase Storage 서비스는 선택 사항입니다.)
- UnityWebRequest 기능을 대처 또는 사용할 수 있습니다.
- UnityWebRequest 기능을 커스터마이징 할 수 있습니다.
호환되는 패키지 목록
어떻게 사용하나요?
유니티 플레이어 환경 기능 지원
- 이 패키지는 Project Settings > Editor > EnterPlayer Mode Options 기능을 지원합니다.
- 설정 가능한 플레이 모드 기능은,
- 스크립트 상태를 초기화하고 씬을 다시 로드하는 과정을 비활성화 할 수 있습니다.
- 에디터 환경에서 실행 시 위 옵션을 비활성화 하여 테스트를 더 빠르게 할 수 있도록 지원하는 기능입니다.
- 설정 가능한 플레이 모드 기능은,
- 이 패키지는 Project Settings > Player > Other Settings > Optimization > Strip Engine Code 기능을 지원합니다.
- 관리되는 코드 스트리핑 기능은,
- Project Settings > Player > Other Settings > Optimization > Managed Stripping Level 값을 조절을 할 수 있습니다.
- 빌드 프로세스 중에 사용되지 않거나 도달할 수 없는 코드를 제거하여 애플리케이션의 최종 빌드 크기를 대폭 줄이는 기능입니다.
- 프로젝트의 C# 스크립트에서 빌드된 어셈블리(Package, .asm file) 플러그인의 일부인 어셈블리, .NET 프레임워크 어셈블리를 포함한 관리되는 어셈블리에서 코드를 제거할 수 있습니다.
- 관리되는 코드 스트리핑 기능은,
Firebase Storage Rule 설정
- 사용하는 프로젝트 또는 테스트 프로젝트의 Firebase Storage 페이지로 접속합니다.
- Storage 메뉴의 Rules 탭을 클릭합니다.
- 아래 예시는 패키지 및 샘플 코드에서 동작하는 ‘Firebase Rules’ 샘플입니다.
위 예시는 Users 폴더 내부에서만 읽기 및 쓰기권한을 활성화 합니다.
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
// Users 폴더만 읽기 및 쓰기 가능
match /Users/{allPaths=**} {
allow read: if true;
allow write: if true;
allow delete: if false;
}
}
}
예시
Note
Percent Local-Save 패키지를 사용한다면 3번 항목을 참고하여 빠르게 사용할 수 있습니다!
1. 서비스 등록 및 문자열(직렬화 데이터) 업로드 & 다운로드
CloudStorageService.Regist() 함수를 이용하여 사용할 서비스를 등록합니다.
아래 예시에서는 미리 구현된 FirebaseStorageService 클래스를 등록하는 방법에 대해 설명합니다.
using UnityEngine;
using Percent.Scripting.Json.Converters; // Percent Local-Save 패키지에 포함된 PercentJsonConvert 라이브러리 입니다.
using Percent.Scripting.CloudStorage;
using Percent.Scripting.CloudStorage.Firebase;
// 업로드 및 다운로드할 대상입니다.
public struct Settings
{
public int Data;
}
private void Awake()
{
// 클라우드 스토리지 원격 주소를 입력합니다.
// 아래 예시는 Firebase Storage 의 "perbase-v2" 프로젝트를 대상으로 합니다.
// https://console.firebase.google.com/u/0/project/perbase-v2/storage/perbase-perbase-v2/files?hl=ko
// 서비스를 등록할 '키' 값입니다. 임의의 숫자도 가능하나 중복을 허용하지 않습니다. (0, 1, 2 등등..)
var hashCode = typeof(FirebaseStorageService).GetHashCode();
var baseUrl = "https://firebasestorage.googleapis.com/v0/b/";
var bucketName = "perbase-perbase-v2";
// 아래 코드는 클라우드 스토리지(또는 커스텀 스토리지)를 등록하는 과정입니다.
CloudStorageService.Regist<FirebaseStorageService>(hashCode, $"{baseUrl}{bucketName}");
}
private Settings _settings;
private async Task Upload()
{
// 등록된 서비스를 가져옵니다.
var service = CloudStorageService.GetService<FirebaseStorageService>();
// 업로드할 데이터를 직렬화 합니다.
// 아래 예시에서는 Percent Local-Save 에서 제공하는 Json 직렬화를 사용하였으나,
// Percent Local-Save 를 사용하지 않는다면 Newtonsoft.Json.JsonConvert를 사용해주세요.
var uploadJsonTarget = PercentJsonConverter.Serialize(_settings);
// 직렬화한 데이터를 업로드 합니다.
var request = await service.UploadStringAsync(uploadJsonTarget, "Users/Settings.dat");
// 기존 코드처럼, 아래 주석된 코드와 같이 UnityWebRequest 열거형으로 성공유무를 확인할 수 있습니다.
// if (request.Result == UnityEngine.Networking.UnityWebRequest.Result.Success)
// FirebaseStorageService를 사용중인 경우 request.ErrorCode를 사용하여 더 자세한 사항을 확인할 수 있습니다.
var err = request.ErrorCode switch
{
// 업로드 또는 다운로드에 성공하였습니다.
FirebaseStorageResponseCode.OK => "성공.",
// FirebaseStorage BaseUrl 또는 Url 값을 확인해주세요.
FirebaseStorageResponseCode.BadRequest => "요청이 잘못되었습니다. 요청하는 주소 값을 확인해주세요.",
// Firebase Storage Rule 규칙을 확인해주세요.
FirebaseStorageResponseCode.Unauthorized => "인증 정보가 없거나 유효하지 않습니다. Firebase Storage Rule 규칙 또는 보안 규칙을 확인해주세요.",
FirebaseStorageResponseCode.Forbidden => "인증에는 성공하였으나 권한이 없습니다. Firebae Storage Rule 규칙 또는 보안 규칙을 확인해주세요.",
// 다운로드인 경우 다운로드할 파일이 없을 때 나타날 수 있습니다. 업로드인 경우 발생하지 않습니다.
FirebaseStorageResponseCode.NotFound => "요청한 파일을 찾을 수 없습니다.",
// 이미 업로드 또는 다운로드를 수행하고 있습니다.
FirebaseStorageResponseCode.Conflict => "요청이 서버의 상태와 충돌합니다. 작업중인 리소스에 접근하려 하고 있습니다.",
// 아래 두 오류는 현재 코드에서는 발생할 가능성이 없거나 요청 서버의 오류입니다.
FirebaseStorageResponseCode.RequestedRangeNotSatisfiable => "요청한 범위가 대상 리소스의 범위를 벗어났습니다.",
FirebaseStorageResponseCode.InternalServerError => "Firebase Storage 서버 내부 오류가 발생하였습니다.",
// 만약 값이 0 이거나 알 수 없는 오류인 경우 UnityWebRequst 오류로 대처합니다.
_ => request.Result switch
{
UnityEngine.Networking.UnityWebRequest.Result.Success => "Success", // 성공!
UnityEngine.Networking.UnityWebRequest.Result.InProgress => "InProgress", // 작업을 진행하고 있습니다.
UnityEngine.Networking.UnityWebRequest.Result.ConnectionError => "Network error", // 인터넷 접속이 원할하지 않습니다.
UnityEngine.Networking.UnityWebRequest.Result.ProtocolError => "Server response error", // 주소가 잘못되었거나 정상적인 리퀘스트(200)를 수신하지 못했습니다.
UnityEngine.Networking.UnityWebRequest.Result.DataProcessingError => "Process received error", // 통신은 성공하였으나 수신된 데이터를 처리하는데 문제가 생겼습니다.
_ => "Unknown"
}
};
}
private async Task Download()
{
// 등록된 서비스를 가져옵니다.
var service = CloudStorageService.GetService<FirebaseStorageService>();
// 해당 파일의 다운로드를 시도합니다.
var request = await service.DownloadStringAsync("Users/Settings.dat");
if (request.ErrorCode == FirebaseStorageResponseCode.OK)
{
// 다운로드에 성공하였다면 역직렬화를 시도합니다.
_settings = PercentJsonConverter.Deserialize<Settings>(request.Value);
}
}
2. 파일의 메타데이터를 다운로드하기
아래 예시에서는 FirebaseStorage 에서 Users 폴더 안의 “MyItem.percent” 파일의 메타데이터를 다운로드하는 예시 코드입니다.
private async Task DownloadMetadata()
{
var service = CloudStorageService.GetService<FirebaseStorageService>();
var request = await service.GetMetadataAsync("Users/MyItem.percent");
// 다운로드에 실패한 경우 로그를 출력합니다.
if (request.ErrorCode != FirebaseStorageResponseCode.OK)
{
if (request.ErrorCode == FirebaseStorageResponseCode.NotFound)
{
// 파일이 존재하지 않는 경우 여기서 작업을 수행합니다.
// ...
}
UnityEngine.Debug.LogError(request.ToString());
}
}
3. Percent Local-Save 패키지의 DataStorage 클래스를 클라우드 스토리지에 업로드 및 다운로드하는 방법
- Percent Local-Saved 패키지 데이터의 스트림 형식을 사용하여 데이터를 업로드 및 다운로드 합니다.
아래 예시에서는 DataStorage
using UnityEngine;
using Percent.Scripting.LocalSave;
using Percent.Scripting.CloudStorage;
using Percent.Scripting.CloudStorage.Firebase;
public struct Settings
{
public int Data;
}
private void Awake()
{
// 1번 항목의 서비스 등록과 동일합니다.
var hashCode = typeof(FirebaseStorageService).GetHashCode();
var baseUrl = "https://firebasestorage.googleapis.com/v0/b/";
var bucketName = "perbase-perbase-v2";
CloudStorageService.Regist<FirebaseStorageService>(hashCode, $"{baseUrl}{bucketName}");
// 데이터 스토리지를 생성합니다.
_storage = DataStorageFactory.Create<Settings>(default);
}
private DataStorage<Settings> _storage;
private async Task Upload()
{
// 스토리지를 업로드 합니다.
var request = await _storage.UploadFirebaseCloudAsync("Users/MyStorageData.percent");
// 만약 실패하였다면 로그를 보여줍니다.
if (request.ErrorCode != FirebaseStorageResponseCode.OK)
{
UnityEngine.Debug.LogError(request.ToString());
}
}
private async Task Download()
{
// 해당 파일의 다운로드를 시도합니다.
var request = await _storage.DownloadFirebaseCloudAsync("Users/MyStorageData.percent");
// 성공하였다면 데이터를 다시 불러옵니다.
if (request.ErrorCode == FirebaseStorageResponseCode.OK)
{
_storage.Load();
}
// 다운로드에 실패한 경우 로그를 출력합니다.
if (request.ErrorCode != FirebaseStorageResponseCode.OK)
{
if (request.ErrorCode == FirebaseStorageResponseCode.NotFound)
{
// 파일이 존재하지 않아 다운로드 받지 못한 경우 여기서 작업을 수행합니다.
// ...
}
UnityEngine.Debug.LogError(request.ToString());
}
}
4. Percent Local-Save 스토리지를 여러개 압축하여 업로드 하기
- Percent Local-Saved 패키지 데이터를 여러개 압축하여 업로드 및 다운로드 합니다.
아래 예시는 패키지 내 세 번째 샘플 폴더에 구현되어 있습니다 (03)
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.IO.Compression;
using UnityEngine;
using UnityEngine.UI;
using Percent.Sample.Common;
using Percent.Scripting.LocalSave;
#if ENABLE_UNITASK
using Task = Cysharp.Threading.Tasks.UniTask;
#else
using Task = System.Threading.Tasks.Task;
#endif
namespace Percent.Sample
{
public class Compress_SampleComponent : MonoBehaviour
{
private static DataStorage<T> GetOrCreateStorageValue<T>()
where T : new()
{
var storage = DataStorageFactory.Create<T>(default);
storage.Load(false, string.Empty);
if (storage.Value == null)
{
storage.Value = new T();
storage.Save(false, string.Empty);
}
return storage;
}
private void Awake()
{
_storages = new()
{
GetOrCreateStorageValue<MyUserData>(),
GetOrCreateStorageValue<MyGameData>(),
GetOrCreateStorageValue<MyOtherData>()
};
ForceLoadUpdateVisualizer();
_input_NickName.onValueChanged.AddListener((value) =>
{
var storage = DataStorageManager.GetStorage<MyUserData>();
storage.Value.NickName = value;
storage.Save();
});
_input_Gold.onValueChanged.AddListener((value) =>
{
var storage = DataStorageManager.GetStorage<MyUserData>();
storage.Value.Gold = ulong.Parse(value);
storage.Save();
});
_input_Score.onValueChanged.AddListener((value) =>
{
var storage = DataStorageManager.GetStorage<MyGameData>();
storage.Value.Score = uint.Parse(value);
storage.Save();
});
_input_OtherData.onValueChanged.AddListener((value) =>
{
var storage = DataStorageManager.GetStorage<MyOtherData>();
storage.Value.Data = value;
storage.Save();
});
_btn_Upload.onClick.AddListener(async () => await OnUploadWithCompressStorage());
_btn_Download.onClick.AddListener(async () => await OnDownloadWithCompressStorage());
}
[Header("MyUserData Visualizer")]
[SerializeField] private InputField _input_NickName;
[SerializeField] private InputField _input_Gold;
[Header("MyGameData Visualizer")]
[SerializeField] private InputField _input_Score;
[Header("MyOtherData Visualizer")]
[SerializeField] private InputField _input_OtherData;
[Header("Buttons")]
[SerializeField] private Button _btn_Upload;
[SerializeField] private Button _btn_Download;
private List<IDataStorage> _storages;
// System.IO.Compression ZipArchive 은 좋은 성능을 보여주지는 않습니다 ..(Minimal 500byte 를 사용)
// 가능하다면 다른 압축 프로세스 또는 오픈소스 사용을 권장합니다.
// ex:https://github.com/icsharpcode/SharpZipLib etc..
private async Task OnUploadWithCompressStorage()
{
// 저장된 데이터를 압축합니다.
// 아래 예시에서는 MemoryStream 형식으로 파일을 생성하지 않고 메모리에 압축하여 데이터를 업로드 합니다.
using var mStream = new MemoryStream();
using (var archive = new ZipArchive(mStream, ZipArchiveMode.Create, true))
{
foreach (var storage in _storages)
{
storage.Save(false, string.Empty);
// 파일 이름에 주의해주세요!
// storage.Name -> 파일이름.확장자
// storage.FileName -> 절대경로/파일이름.확장자
// 압축파일에 적재할 이름을 넣으므로 storage.Name을 사용합니다.
// 예시) zip[]
// - MyUserData.percent
// - MyGameData.percent
// - MyOtherData.percent
var entry = archive.CreateEntry(storage.Name);
var fStream = (FileStream)storage.Stream;
using var entryStream = entry.Open();
fStream.Position = 0;
await fStream.CopyToAsync(entryStream);
}
}
// 내부 로직에서 mStream.Position = 0; 을 실행하므로 포커스 위치를 변경할 필요 없습니다.
// 매개변수가 MemoryStream인 경우 내부에서 추가적인 Stream을 할당하지 않고 해당 스트림을 사용합니다.
var result = await FirebaseCloud.Service.UploadStreamAsync(mStream, "Users/Packs.dat");
UnityEngine.Debug.Log(result.ErrorCode);
}
private async Task OnDownloadWithCompressStorage()
{
// 업로드한 압축 데이터를 다운로드하고 해당 스토리지 스트림에 값을 주입합니다.
// 아래 예시에서는 압축된 데이터를 메모리에 다운로드 하고 값을 다운로드 한 값으로 변경 및 저장합니다.
using var mStream = new MemoryStream();
var result = await FirebaseCloud.Service.DownloadStreamAsync(mStream, "Users/Packs.dat");
if (result.ErrorCode == Scripting.CloudStorage.Firebase.FirebaseStorageResponseCode.OK)
{
mStream.Seek(0, SeekOrigin.Begin);
using var archive = new ZipArchive(mStream, ZipArchiveMode.Read);
foreach (var entry in archive.Entries)
{
var findStorage = _storages.FirstOrDefault(x => x.Name == entry.Name);
if (findStorage == null)
{
UnityEngine.Debug.LogWarning("이전에는 존재하였으나 현재 할당된 스토리지에서는 존재하지 않습니다! 해당 파일을 무시합니다.");
continue;
}
// 이전에 저장되어있는 데이터를 삭제합니다.
findStorage.Clear();
using var entryStream = entry.Open();
await entryStream.CopyToAsync(findStorage.Stream);
findStorage.Load(false, string.Empty); // 스트림 버퍼에 있는 데이터를 읽어 해당 값을 로드합니다.
findStorage.Save(false, string.Empty); // 읽은 데이터어 값을 저장합니다.
}
ForceLoadUpdateVisualizer();
}
UnityEngine.Debug.Log(result.ErrorCode);
}
private void ForceLoadUpdateVisualizer()
{
var param = new DataStorageSetterParameter
{
NickNameField = _input_NickName,
GoldField = _input_Gold,
ScoreField = _input_Score,
OtherDataField = _input_OtherData
};
foreach (var storage in _storages)
{
storage.Load(false, string.Empty);
var value = (IDataStorageSetter)storage.Value;
value.ReloadSettings(param);
}
}
}
}
PercentWebRequest 으로 로컬 파일을 다운로드 하기
UnityWebRequest → PercentWebRequest 로 변경하여 특정 파일을 다운로드 합니다.
아래 예시는 로컬 환경에 있는 특정 파일을 메모리에 복사하고 새로운 파일로 생성하는 코드 예시입니다.
private async Task CopyFileAsync(string filePath)
{
if (!System.IO.File.Exists(filePath))
{
return;
}
// 로컬 파일을 스트림 형식으로 복사합니다.
using var mStream = new MemoryStream();
using var request = PercentWebRequest.CreateDownloadRequest(new DownloadRequestParams
{
Url = $"file://{filePath}"
},
mStream);
await request.SendWebRequest();
var result = request.GetWebResult<PercentWebRequestResult>();
if (result.Result == UnityEngine.Networking.UnityWebRequest.Result.Success)
{
// 복사에 성공하였다면 해당 FileStream을 사용하여 대상 파일을 생성합니다.
var newName = "NewFile";
var extensions = Path.GetExtension(filePath);
var newFilePath = $"{Path.GetDirectoryName(filePath)}/{newName}{extensions}";
using (var fStream = new FileStream(newFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite))
{
mStream.Position = 0;
mStream.CopyTo(fStream);
}
}
else
{
UnityEngine.Debug.LogError(result);
}
}
UnityWebRequest 직접 구현
구현된 Unity LLAPI 사용
- FirebaseStorage 가 아닌 AWS 또는 내부 데이터의 로드 (StreamingAssets, persistentDataPath, etc..) 등 커스텀 구현으로도 사용할 수 있습니다.
- 이 코드는 해당 프로젝트 또는 어셈블리에 안전하지 않은 코드 사용을 허용해야 합니다. (Allow ‘unsafe’ Code)
Warning
Dangerous certain consequences of an action.
UnityWebRequest 직접구현에 대한 LLAPI 메모리는 보장하나 아래 옵션에 따라 Handler Buffer 에 대한 메모리 반환을 보장하지 않습니다.