콘텐츠로 이동

Unity UGC 시스템 구축

Unity에서 UGC(사용자 생성 콘텐츠) 시스템 구축

섹션 제목: “Unity에서 UGC(사용자 생성 콘텐츠) 시스템 구축”

UGC(User Generated Content)는 플레이어가 직접 게임 내 콘텐츠를 제작하고, 공유하고, 다른 플레이어의 콘텐츠를 다운로드해 플레이하는 시스템입니다. 레벨 에디터, 커스텀 맵, 스킨 에디터, 아이템 제작소 등이 대표적인 사례입니다.

대표적인 구현 사례:

  • 레벨 에디터: 플레이어가 직접 스테이지를 제작하고 공유 (예: Super Mario Maker, Dreams)
  • 맵 빌더: 멀티플레이어 게임에서 커스텀 맵 제작 및 업로드
  • 스킨/아이템 에디터: 캐릭터, 무기, 건물 외형 커스터마이징 후 공유
  • 플레이어 리텐션 극대화: 공식 콘텐츠 소진 후에도 커뮤니티가 게임을 지속시킴
  • 개발 비용 절감: 플레이어가 콘텐츠 생산을 분담
  • 커뮤니티 형성: 창작자와 소비자 간 유기적 생태계 구축
  • 장기 운영 지원: 새로운 업데이트 없이도 풍부한 경험 제공

UGC 시스템의 핵심은 플레이어가 제작한 데이터를 안정적으로 저장하고 복원하는 것입니다. Unity의 [Serializable] 어트리뷰트와 JsonUtility를 기반으로 데이터 구조를 설계합니다.

using System;
using System.Collections.Generic;
using UnityEngine;
// 레벨 전체를 표현하는 루트 데이터 구조
[Serializable]
public class UGCLevelData
{
public string levelId; // 고유 식별자 (GUID)
public string title; // 레벨 제목
public string authorId; // 제작자 ID
public string version; // 포맷 버전 (하위 호환용)
public long createdAt; // Unix 타임스탬프
public long updatedAt;
public List<UGCObjectData> objects = new();
public UGCLevelSettings settings = new();
}
// 레벨 내 배치된 각 오브젝트의 데이터
[Serializable]
public class UGCObjectData
{
public string objectType; // 프리팹 식별자 키
public SerializableVector3 position;
public SerializableVector3 rotation;
public SerializableVector3 scale;
public string customJson; // 오브젝트별 추가 데이터 (JSON 문자열)
}
// 레벨 전반 설정
[Serializable]
public class UGCLevelSettings
{
public string skyboxName = "default";
public string bgmId = "none";
public float gravity = -9.81f;
public int maxPlayers = 4;
}
// Unity Vector3은 JsonUtility에서 바로 직렬화되나,
// 명시적 구조체를 쓰면 외부 JSON 라이브러리와의 호환성이 높아짐
[Serializable]
public struct SerializableVector3
{
public float x, y, z;
public SerializableVector3(Vector3 v) { x = v.x; y = v.y; z = v.z; }
public Vector3 ToVector3() => new(x, y, z);
public static implicit operator SerializableVector3(Vector3 v) => new(v);
public static implicit operator Vector3(SerializableVector3 s) => s.ToVector3();
}

1.2 ScriptableObject 기반 오브젝트 레지스트리

섹션 제목: “1.2 ScriptableObject 기반 오브젝트 레지스트리”

UGC에서 배치 가능한 오브젝트 목록을 ScriptableObject로 관리하면 에디터 통합과 런타임 성능 두 가지 이점을 동시에 얻을 수 있습니다.

using UnityEngine;
using UnityEngine.AddressableAssets;
// 배치 가능한 오브젝트 하나를 정의하는 ScriptableObject
[CreateAssetMenu(menuName = "UGC/PlaceableObject")]
public class UGCObjectDefinition : ScriptableObject
{
[Header("식별 정보")]
public string objectTypeKey; // UGCObjectData.objectType과 매핑
public string displayName;
public Sprite thumbnail;
[Header("에셋 참조 (Addressables)")]
public AssetReferenceGameObject prefabReference;
[Header("제약 조건")]
public bool allowRotation = true;
public bool allowScale = false;
public Vector3 defaultScale = Vector3.one;
public int maxCountPerLevel = 100;
}
// 전체 오브젝트 목록을 관리하는 레지스트리
[CreateAssetMenu(menuName = "UGC/ObjectRegistry")]
public class UGCObjectRegistry : ScriptableObject
{
[SerializeField]
private List<UGCObjectDefinition> definitions = new();
private Dictionary<string, UGCObjectDefinition> _lookup;
private void OnEnable()
{
RebuildLookup();
}
public void RebuildLookup()
{
_lookup = new Dictionary<string, UGCObjectDefinition>();
foreach (var def in definitions)
{
if (!string.IsNullOrEmpty(def.objectTypeKey))
_lookup[def.objectTypeKey] = def;
}
}
public bool TryGet(string key, out UGCObjectDefinition def)
=> _lookup.TryGetValue(key, out def);
}
using System.IO;
using UnityEngine;
public static class UGCSerializer
{
private const string FILE_EXTENSION = ".ugc";
private static readonly string SaveDirectory =
Path.Combine(Application.persistentDataPath, "ugc_levels");
// 레벨 데이터를 JSON으로 직렬화하여 로컬에 저장
public static bool SaveLevel(UGCLevelData levelData)
{
try
{
Directory.CreateDirectory(SaveDirectory);
levelData.updatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
string json = JsonUtility.ToJson(levelData, prettyPrint: true);
string path = Path.Combine(SaveDirectory, levelData.levelId + FILE_EXTENSION);
File.WriteAllText(path, json, System.Text.Encoding.UTF8);
Debug.Log($"[UGC] 레벨 저장 완료: {path}");
return true;
}
catch (Exception ex)
{
Debug.LogError($"[UGC] 레벨 저장 실패: {ex.Message}");
return false;
}
}
// 로컬 파일에서 레벨 데이터를 역직렬화
public static bool TryLoadLevel(string levelId, out UGCLevelData levelData)
{
levelData = null;
string path = Path.Combine(SaveDirectory, levelId + FILE_EXTENSION);
if (!File.Exists(path))
{
Debug.LogWarning($"[UGC] 레벨 파일 없음: {path}");
return false;
}
try
{
string json = File.ReadAllText(path, System.Text.Encoding.UTF8);
levelData = JsonUtility.FromJson<UGCLevelData>(json);
return levelData != null;
}
catch (Exception ex)
{
Debug.LogError($"[UGC] 레벨 로드 실패: {ex.Message}");
return false;
}
}
// 저장된 모든 레벨 ID 목록 반환
public static List<string> GetSavedLevelIds()
{
var ids = new List<string>();
if (!Directory.Exists(SaveDirectory)) return ids;
foreach (var file in Directory.GetFiles(SaveDirectory, "*" + FILE_EXTENSION))
ids.Add(Path.GetFileNameWithoutExtension(file));
return ids;
}
}

2. 에셋 검증 및 샌드박스 안전성

섹션 제목: “2. 에셋 검증 및 샌드박스 안전성”

UGC 데이터를 그대로 로드하면 게임 크래시, 성능 저하, 또는 보안 문제로 이어질 수 있습니다. 반드시 로드 전 검증 단계를 거쳐야 합니다.

using System.Collections.Generic;
using UnityEngine;
public enum ValidationResult { Valid, Warning, Error }
public class UGCValidationReport
{
public ValidationResult OverallResult { get; private set; } = ValidationResult.Valid;
public List<string> Messages { get; } = new();
public void AddError(string message)
{
Messages.Add($"[오류] {message}");
OverallResult = ValidationResult.Error;
}
public void AddWarning(string message)
{
Messages.Add($"[경고] {message}");
if (OverallResult != ValidationResult.Error)
OverallResult = ValidationResult.Warning;
}
public bool IsPlayable => OverallResult != ValidationResult.Error;
}
public class UGCValidator
{
private const int MAX_OBJECTS_PER_LEVEL = 500;
private const int TITLE_MAX_LENGTH = 50;
private readonly UGCObjectRegistry _registry;
public UGCValidator(UGCObjectRegistry registry)
{
_registry = registry;
}
public UGCValidationReport Validate(UGCLevelData level)
{
var report = new UGCValidationReport();
ValidateMetadata(level, report);
ValidateObjectList(level, report);
ValidateSettings(level, report);
return report;
}
private void ValidateMetadata(UGCLevelData level, UGCValidationReport report)
{
if (string.IsNullOrWhiteSpace(level.levelId))
report.AddError("levelId가 비어있습니다.");
if (string.IsNullOrWhiteSpace(level.title))
report.AddWarning("레벨 제목이 없습니다. 기본값으로 대체됩니다.");
if (level.title?.Length > TITLE_MAX_LENGTH)
report.AddError($"제목이 너무 깁니다. (최대 {TITLE_MAX_LENGTH}자)");
// 버전 호환성 체크
if (!IsVersionCompatible(level.version))
report.AddError($"지원하지 않는 레벨 버전: {level.version}");
}
private void ValidateObjectList(UGCLevelData level, UGCValidationReport report)
{
if (level.objects == null)
{
report.AddError("오브젝트 목록이 null입니다.");
return;
}
if (level.objects.Count > MAX_OBJECTS_PER_LEVEL)
report.AddError($"오브젝트 수 초과: {level.objects.Count} / {MAX_OBJECTS_PER_LEVEL}");
var countByType = new Dictionary<string, int>();
foreach (var obj in level.objects)
{
// 알 수 없는 오브젝트 타입 체크
if (!_registry.TryGet(obj.objectType, out var def))
{
report.AddWarning($"알 수 없는 오브젝트 타입: '{obj.objectType}' - 스킵됩니다.");
continue;
}
// 타입별 최대 개수 체크
countByType.TryGetValue(obj.objectType, out int count);
countByType[obj.objectType] = count + 1;
if (countByType[obj.objectType] > def.maxCountPerLevel)
report.AddError($"'{obj.objectType}' 오브젝트 수 초과 (최대 {def.maxCountPerLevel}개)");
// 위치 범위 체크 (NaN, Infinity 방어)
if (!IsValidPosition(obj.position.ToVector3()))
report.AddError($"오브젝트 위치 값 비정상: {obj.objectType}");
}
}
private void ValidateSettings(UGCLevelData level, UGCValidationReport report)
{
float g = level.settings.gravity;
if (g < -50f || g > 50f)
report.AddWarning($"중력 값이 비정상 범위입니다: {g}");
if (level.settings.maxPlayers < 1 || level.settings.maxPlayers > 16)
report.AddError("maxPlayers는 1~16 범위여야 합니다.");
}
private bool IsValidPosition(Vector3 pos)
=> !float.IsNaN(pos.x) && !float.IsNaN(pos.y) && !float.IsNaN(pos.z)
&& !float.IsInfinity(pos.x) && !float.IsInfinity(pos.y) && !float.IsInfinity(pos.z);
private bool IsVersionCompatible(string version)
{
// 지원되는 버전 목록
var supported = new HashSet<string> { "1.0", "1.1", "2.0" };
return supported.Contains(version ?? "");
}
}

UGC 시스템에서 보안을 유지하기 위한 핵심 원칙들입니다.

허용 목록(Allowlist) 방식 사용

  • 오브젝트 타입을 레지스트리에 등록된 것만 허용하고, 미등록 타입은 무시합니다.
  • 파일 경로, 씬 이름 등 문자열 입력값은 화이트리스트와 정확히 일치하는 경우에만 허용합니다.

Addressables를 통한 에셋 격리

  • 유저가 제공한 경로로 직접 Resources.Load()를 호출하지 않습니다.
  • 모든 프리팹은 AssetReferenceGameObject로 참조하고, Addressables 카탈로그에 등록된 키만 사용합니다.

스크립트 실행 차단

  • 런타임에 유저가 코드를 업로드하거나 실행할 수 없도록 합니다.
  • Reflection을 통한 타입 생성을 UGC 경로에서 사용하지 않습니다.

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
public class UGCLevelLoader : MonoBehaviour
{
[SerializeField] private UGCObjectRegistry registry;
private readonly List<AsyncOperationHandle> _loadedHandles = new();
private readonly List<GameObject> _spawnedObjects = new();
// 레벨 데이터를 읽어 씬에 오브젝트들을 배치
public async Task<bool> LoadLevelAsync(UGCLevelData levelData)
{
// 1. 기존 레벨 정리
await UnloadCurrentLevel();
// 2. 검증
var validator = new UGCValidator(registry);
var report = validator.Validate(levelData);
if (!report.IsPlayable)
{
foreach (var msg in report.Messages)
Debug.LogError(msg);
return false;
}
foreach (var msg in report.Messages)
Debug.LogWarning(msg); // 경고는 로그만 출력
// 3. 오브젝트 순차/비동기 로딩
foreach (var objData in levelData.objects)
{
if (!registry.TryGet(objData.objectType, out var def))
continue;
await SpawnObjectAsync(def, objData);
}
return true;
}
private async Task SpawnObjectAsync(UGCObjectDefinition def, UGCObjectData data)
{
var handle = def.prefabReference.LoadAssetAsync<GameObject>();
_loadedHandles.Add(handle);
await handle.Task;
if (handle.Status != AsyncOperationStatus.Succeeded)
{
Debug.LogWarning($"[UGC] 프리팹 로드 실패: {data.objectType}");
return;
}
var instance = Instantiate(
handle.Result,
data.position,
Quaternion.Euler(data.rotation)
);
instance.transform.localScale = data.scale;
_spawnedObjects.Add(instance);
}
// 씬에 배치된 오브젝트와 로드된 에셋 해제
public async Task UnloadCurrentLevel()
{
foreach (var go in _spawnedObjects)
{
if (go != null) Destroy(go);
}
_spawnedObjects.Clear();
foreach (var handle in _loadedHandles)
{
if (handle.IsValid())
Addressables.Release(handle);
}
_loadedHandles.Clear();
// 프레임 대기로 Destroy 완료 보장
await Task.Yield();
}
}

3.2 클라우드 업로드/다운로드 (Unity Gaming Services 연동)

섹션 제목: “3.2 클라우드 업로드/다운로드 (Unity Gaming Services 연동)”
using System;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;
// Unity UGC Bridge 또는 자체 백엔드 API와 통신하는 클라이언트
public class UGCCloudClient
{
private readonly string _baseUrl;
private readonly string _authToken;
public UGCCloudClient(string baseUrl, string authToken)
{
_baseUrl = baseUrl;
_authToken = authToken;
}
// 레벨 데이터를 서버에 업로드
public async Task<bool> UploadLevelAsync(UGCLevelData levelData)
{
string json = JsonUtility.ToJson(levelData);
byte[] body = Encoding.UTF8.GetBytes(json);
using var request = new UnityWebRequest($"{_baseUrl}/levels", "POST");
request.uploadHandler = new UploadHandlerRaw(body);
request.downloadHandler = new DownloadHandlerBuffer();
request.SetRequestHeader("Content-Type", "application/json");
request.SetRequestHeader("Authorization", $"Bearer {_authToken}");
var tcs = new TaskCompletionSource<bool>();
var op = request.SendWebRequest();
op.completed += _ => tcs.SetResult(request.result == UnityWebRequest.Result.Success);
bool success = await tcs.Task;
if (!success)
Debug.LogError($"[UGC] 업로드 실패: {request.error}");
return success;
}
// 서버에서 레벨 데이터를 다운로드
public async Task<UGCLevelData> DownloadLevelAsync(string levelId)
{
using var request = UnityWebRequest.Get($"{_baseUrl}/levels/{levelId}");
request.SetRequestHeader("Authorization", $"Bearer {_authToken}");
var tcs = new TaskCompletionSource<string>();
var op = request.SendWebRequest();
op.completed += _ =>
{
if (request.result == UnityWebRequest.Result.Success)
tcs.SetResult(request.downloadHandler.text);
else
tcs.SetResult(null);
};
string responseJson = await tcs.Task;
if (string.IsNullOrEmpty(responseJson))
{
Debug.LogError($"[UGC] 다운로드 실패: {request.error}");
return null;
}
return JsonUtility.FromJson<UGCLevelData>(responseJson);
}
}

4.1 Netcode for GameObjects를 활용한 UGC 동기화

섹션 제목: “4.1 Netcode for GameObjects를 활용한 UGC 동기화”

멀티플레이어 환경에서는 모든 클라이언트가 동일한 레벨을 로드하도록 동기화해야 합니다. 핵심 원칙은 레벨 데이터 자체를 전송하지 않고, 레벨 ID만 동기화하는 것입니다.

using Unity.Netcode;
using UnityEngine;
using System.Collections;
public class UGCNetworkManager : NetworkBehaviour
{
[SerializeField] private UGCLevelLoader levelLoader;
[SerializeField] private UGCObjectRegistry registry;
// 네트워크를 통해 동기화되는 현재 레벨 ID
private NetworkVariable<Unity.Collections.FixedString64Bytes> _currentLevelId
= new(default,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
public override void OnNetworkSpawn()
{
_currentLevelId.OnValueChanged += OnLevelIdChanged;
// 늦게 접속한 클라이언트: 현재 레벨을 즉시 로드
if (!IsServer && _currentLevelId.Value.Length > 0)
StartCoroutine(LoadLevelCoroutine(_currentLevelId.Value.ToString()));
}
// 서버(호스트)가 새 레벨 로드를 요청
[ServerRpc(RequireOwnership = false)]
public void RequestLoadLevelServerRpc(string levelId, ServerRpcParams rpcParams = default)
{
// 서버에서만 레벨 ID 값 변경 (모든 클라이언트에 자동 전파)
_currentLevelId.Value = levelId;
}
private void OnLevelIdChanged(
Unity.Collections.FixedString64Bytes prev,
Unity.Collections.FixedString64Bytes next)
{
if (next.Length > 0)
StartCoroutine(LoadLevelCoroutine(next.ToString()));
}
private IEnumerator LoadLevelCoroutine(string levelId)
{
// 로컬 캐시에서 먼저 찾고, 없으면 서버에서 다운로드
if (!UGCSerializer.TryLoadLevel(levelId, out var levelData))
{
Debug.Log($"[UGC] 서버에서 레벨 다운로드 중: {levelId}");
// 실제 구현에서는 UGCCloudClient.DownloadLevelAsync() 호출
yield break;
}
var task = levelLoader.LoadLevelAsync(levelData);
yield return new WaitUntil(() => task.IsCompleted);
if (!task.Result)
Debug.LogError($"[UGC] 레벨 로드 실패: {levelId}");
}
}

4.2 오브젝트 상태 실시간 동기화

섹션 제목: “4.2 오브젝트 상태 실시간 동기화”

레벨 내 오브젝트가 변경 가능한 상태를 가지는 경우(문 개폐, 상자 파괴 등), NetworkVariable로 상태를 동기화합니다.

using Unity.Netcode;
using UnityEngine;
// UGC 레벨의 상호작용 가능한 오브젝트 예시
public class UGCInteractable : NetworkBehaviour
{
// bool 타입 NetworkVariable로 활성화 상태 동기화
private NetworkVariable<bool> _isActive = new(
true,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server
);
private Renderer _renderer;
private void Awake()
{
_renderer = GetComponent<Renderer>();
}
public override void OnNetworkSpawn()
{
_isActive.OnValueChanged += (_, newValue) => UpdateVisual(newValue);
UpdateVisual(_isActive.Value);
}
// 클라이언트가 상호작용 요청
[ServerRpc(RequireOwnership = false)]
public void ToggleServerRpc()
{
_isActive.Value = !_isActive.Value;
}
private void UpdateVisual(bool isActive)
{
if (_renderer != null)
_renderer.enabled = isActive;
}
}

항목권장 사항
Dictionary 사용JsonUtility는 Dictionary를 지원하지 않음. 별도 Key-Value 배열 구조로 변환 필요
UnityEngine 타입Color, Vector3 등은 JsonUtility로 직렬화 가능하나, Newtonsoft.Json 사용 시 커스텀 컨버터 필요
null vs 빈 컬렉션List는 항상 new() 초기화 권장. 역직렬화 후 null 체크 필수
버전 관리version 필드를 두어 구버전 데이터 마이그레이션 경로 확보
ScriptableObject 직렬화JsonUtility.FromJson()은 일반 클래스에만 동작. ScriptableObjectFromJsonOverwrite() 사용

에셋 로딩 최적화

  • Addressables의 LoadAssetsAsync<T>(label, ...) 로 동일 레이블 에셋을 한 번에 로드합니다.
  • 로드 완료 후 Addressables.Release(handle) 호출을 잊지 않아야 메모리 누수를 방지할 수 있습니다.
  • 자주 쓰이는 프리팹은 오브젝트 풀로 관리해 Instantiate/Destroy 비용을 줄입니다.

직렬화 성능

  • JsonUtility는 Unity 네이티브 구현으로 빠르지만, 복잡한 중첩 구조에는 Newtonsoft.Json(JSON.NET) 고려합니다.
  • 대형 레벨 데이터(500개 이상 오브젝트)는 비동기 스레드에서 역직렬화 후 메인 스레드에 전달합니다.

네트워크 최적화

  • 레벨 전체 JSON을 네트워크로 전송하지 않습니다. 레벨 ID만 동기화하고 각 클라이언트가 개별 다운로드하도록 합니다.
  • NetworkVariable의 값 변경은 필요한 경우에만 수행합니다. 매 프레임 업데이트는 대역폭 낭비입니다.
  • 큰 데이터 전송이 필요한 경우 CustomMessagingManager의 Named Messages를 사용합니다.

UGC 시스템은 기술 구현 외에 커뮤니티 관리 정책이 필요합니다.

  • 서버 측 검증: 클라이언트 검증은 편의를 위한 것이고, 최종 검증은 반드시 서버에서 수행합니다.
  • 신고 시스템: 부적절한 콘텐츠를 다른 플레이어가 신고할 수 있는 UI와 백엔드를 구축합니다.
  • 검토 큐: 자동 검증을 통과한 콘텐츠도 일정 신고 수 이상이면 사람이 검토하도록 파이프라인을 설계합니다.
  • 작성자 책임: 업로드 시 이용 약관 동의를 받고, 위반 콘텐츠 작성자 계정을 관리할 수 있는 구조를 갖춥니다.

UGC 데이터 스키마가 업데이트되면 기존 저장 파일을 새 형식으로 변환해야 합니다.

public static class UGCMigrator
{
public static UGCLevelData Migrate(string json)
{
// 버전 필드만 먼저 파싱
var probe = JsonUtility.FromJson<VersionProbe>(json);
return probe.version switch
{
"1.0" => MigrateV1_0(json),
"1.1" => MigrateV1_1(json),
"2.0" => JsonUtility.FromJson<UGCLevelData>(json),
_ => null // 알 수 없는 버전 — 거부
};
}
private static UGCLevelData MigrateV1_0(string json)
{
var old = JsonUtility.FromJson<UGCLevelDataV1_0>(json);
return new UGCLevelData
{
levelId = old.id, // 필드명 변경 대응
title = old.name,
authorId = old.creator,
version = "2.0",
objects = old.items?.ConvertAll(item => new UGCObjectData
{
objectType = item.type,
position = item.pos,
rotation = Vector3.zero, // v1.0에 없던 필드: 기본값
scale = Vector3.one
}) ?? new List<UGCObjectData>(),
settings = new UGCLevelSettings()
};
}
private static UGCLevelData MigrateV1_1(string json)
{
// v1.1 → v2.0 은 구조 변화가 적으므로 직접 파싱 후 조정
var data = JsonUtility.FromJson<UGCLevelData>(json);
data.version = "2.0";
return data;
}
[System.Serializable] private class VersionProbe { public string version; }
// v1.0 구조 (구버전 호환용 클래스)
[System.Serializable]
private class UGCLevelDataV1_0
{
public string id;
public string name;
public string creator;
public List<V1_0_Object> items;
}
[System.Serializable]
private class V1_0_Object
{
public string type;
public SerializableVector3 pos;
}
}

로드 흐름에 마이그레이션을 통합하세요:

public static bool TryLoadWithMigration(string levelId, out UGCLevelData levelData)
{
levelData = null;
string path = Path.Combine(SaveDirectory, levelId + ".ugc");
if (!File.Exists(path)) return false;
string json = File.ReadAllText(path);
levelData = UGCMigrator.Migrate(json);
if (levelData == null) return false;
// 마이그레이션됐으면 새 버전으로 덮어쓰기
if (levelData.version != "2.0")
SaveLevel(levelData);
return true;
}