- 添加 MainForm

- 添加 ResolutionAdapterComponent 进行屏幕分辨率适配
This commit is contained in:
SepComet
2026-02-28 18:39:20 +08:00
parent 61a096cbc6
commit dfd37778b6
39 changed files with 1365 additions and 20 deletions
@@ -1,31 +1,25 @@
//------------------------------------------------------------
// Game Framework
// Copyright © 2013-2021 Jiang Yin. All rights reserved.
// Homepage: https://gameframework.cn/
// Feedback: mailto:ellan@gameframework.cn
//------------------------------------------------------------
using CustomComponent;
using CustomComponent;
using GeometryTD.CustomComponent;
using UnityEngine;
/// <summary>
/// 游戏入口。
/// </summary>
public partial class GameEntry : MonoBehaviour
public partial class GameEntry
{
public static BuiltinDataComponent BuiltinData { get; private set; }
public static HPBarComponent HPBar { get; private set; }
public static UIRouterComponent UIRouter { get; private set; }
public static EventNodeComponent EventNode { get; private set; }
public static CombatNodeComponent CombatNode { get; private set; }
public static ShopNodeComponent ShopNode { get; private set; }
public static ResolutionAdapterComponent ResolutionAdapter { get; private set; }
private static void InitCustomComponents()
{
BuiltinData = UnityGameFramework.Runtime.GameEntry.GetComponent<BuiltinDataComponent>();
@@ -34,5 +28,11 @@ public partial class GameEntry : MonoBehaviour
EventNode = UnityGameFramework.Runtime.GameEntry.GetComponent<EventNodeComponent>();
CombatNode = UnityGameFramework.Runtime.GameEntry.GetComponent<CombatNodeComponent>();
ShopNode = UnityGameFramework.Runtime.GameEntry.GetComponent<ShopNodeComponent>();
ResolutionAdapter = UnityGameFramework.Runtime.GameEntry.GetComponent<ResolutionAdapterComponent>();
if (ResolutionAdapter == null)
{
UnityGameFramework.Runtime.Log.Warning(
"ResolutionAdapterComponent is missing. Please add it in Launcher scene and inject UI roots.");
}
}
}
@@ -0,0 +1,445 @@
using System.Collections.Generic;
using GameFramework.Event;
using UnityEngine;
using UnityEngine.UI;
using UnityGameFramework.Runtime;
namespace GeometryTD.CustomComponent
{
/// <summary>
/// Keeps gameplay camera and UI in a fixed design aspect, then fills extra area with black masks.
/// </summary>
public class ResolutionAdapterComponent : GameFrameworkComponent
{
private const float DefaultCanvasPlaneDistance = 100f;
[SerializeField] private Vector2 _referenceResolution = new Vector2(2560f, 1600f);
[SerializeField] private bool _adaptUiCanvasToViewport = true;
[SerializeField] private bool _enableBlackMask = true;
[SerializeField] private List<Transform> _uiRoots = new List<Transform>();
private readonly List<Canvas> _canvasBuffer = new List<Canvas>(32);
private readonly List<Canvas> _trackedCanvases = new List<Canvas>(32);
private readonly HashSet<Canvas> _trackedCanvasSet = new HashSet<Canvas>();
private Camera _mainCamera;
private Rect _targetViewport = new Rect(0f, 0f, 1f, 1f);
private int _cachedScreenWidth = -1;
private int _cachedScreenHeight = -1;
private bool _canvasCacheDirty = true;
private bool _uiEventSubscribed;
private bool _missingRootWarned;
private Canvas _maskCanvas;
private RectTransform _leftMask;
private RectTransform _rightMask;
private RectTransform _topMask;
private RectTransform _bottomMask;
private void Start()
{
TryEnsureUiEventSubscribed();
ApplyAdaptation(true);
}
private void Update()
{
TryEnsureUiEventSubscribed();
ApplyAdaptation(false);
}
private void OnDestroy()
{
UnsubscribeUiEvents();
if (_mainCamera != null)
{
_mainCamera.rect = new Rect(0f, 0f, 1f, 1f);
}
}
private void ApplyAdaptation(bool force)
{
if (_referenceResolution.x <= 0f || _referenceResolution.y <= 0f)
{
return;
}
Camera resolvedCamera = ResolveMainCamera();
bool cameraChanged = resolvedCamera != _mainCamera;
bool screenChanged = Screen.width != _cachedScreenWidth || Screen.height != _cachedScreenHeight;
bool needRecalculate = force || cameraChanged || screenChanged;
if (needRecalculate)
{
Camera lastCamera = _mainCamera;
if (lastCamera != null && lastCamera != resolvedCamera)
{
lastCamera.rect = new Rect(0f, 0f, 1f, 1f);
}
_mainCamera = resolvedCamera;
_cachedScreenWidth = Screen.width;
_cachedScreenHeight = Screen.height;
_targetViewport = CalculateViewport(_cachedScreenWidth, _cachedScreenHeight);
ApplyCameraViewport();
UpdateMaskLayout();
}
if (!_adaptUiCanvasToViewport)
{
return;
}
if (!needRecalculate && !_canvasCacheDirty)
{
return;
}
ApplyCanvasAdaptation();
}
private Camera ResolveMainCamera()
{
if (GameEntry.Scene != null && GameEntry.Scene.MainCamera != null)
{
return GameEntry.Scene.MainCamera;
}
if (Camera.main != null)
{
return Camera.main;
}
if (_mainCamera != null && _mainCamera.isActiveAndEnabled)
{
return _mainCamera;
}
return null;
}
private Rect CalculateViewport(int width, int height)
{
if (width <= 0 || height <= 0)
{
return new Rect(0f, 0f, 1f, 1f);
}
float referenceAspect = _referenceResolution.x / _referenceResolution.y;
float screenAspect = (float)width / height;
if (Mathf.Approximately(referenceAspect, screenAspect))
{
return new Rect(0f, 0f, 1f, 1f);
}
if (screenAspect > referenceAspect)
{
float viewportWidth = referenceAspect / screenAspect;
return new Rect((1f - viewportWidth) * 0.5f, 0f, viewportWidth, 1f);
}
float viewportHeight = screenAspect / referenceAspect;
return new Rect(0f, (1f - viewportHeight) * 0.5f, 1f, viewportHeight);
}
private void ApplyCameraViewport()
{
if (_mainCamera == null)
{
return;
}
_mainCamera.rect = _targetViewport;
}
private void ApplyCanvasAdaptation()
{
if (_mainCamera == null)
{
return;
}
if (_uiRoots == null || _uiRoots.Count == 0)
{
if (!_missingRootWarned)
{
_missingRootWarned = true;
Log.Warning(
"ResolutionAdapterComponent missing injected roots. Assign UI/HPBar root transforms in scene.");
}
return;
}
if (_canvasCacheDirty)
{
RebuildCanvasCache();
}
for (int i = _trackedCanvases.Count - 1; i >= 0; i--)
{
Canvas canvas = _trackedCanvases[i];
if (canvas == null)
{
_trackedCanvases.RemoveAt(i);
_canvasCacheDirty = true;
continue;
}
ApplyCanvasSettings(canvas);
}
}
private void RebuildCanvasCache()
{
_canvasCacheDirty = false;
_trackedCanvases.Clear();
_trackedCanvasSet.Clear();
foreach (var root in _uiRoots)
{
CollectCanvases(root);
}
}
private void CollectCanvases(Transform root)
{
if (root == null)
{
return;
}
root.GetComponentsInChildren(true, _canvasBuffer);
foreach (Canvas canvas in _canvasBuffer)
{
if (canvas == null || canvas == _maskCanvas || canvas.renderMode == RenderMode.WorldSpace)
{
continue;
}
if (_trackedCanvasSet.Add(canvas))
{
_trackedCanvases.Add(canvas);
}
}
_canvasBuffer.Clear();
}
private void ApplyCanvasSettings(Canvas canvas)
{
if (canvas == _maskCanvas || canvas.renderMode == RenderMode.WorldSpace)
{
return;
}
canvas.renderMode = RenderMode.ScreenSpaceCamera;
canvas.worldCamera = _mainCamera;
canvas.planeDistance = ResolveCanvasPlaneDistance(_mainCamera);
CanvasScaler scaler = canvas.GetComponent<CanvasScaler>();
if (scaler != null)
{
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = _referenceResolution;
scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
scaler.matchWidthOrHeight = 0f;
}
}
private static float ResolveCanvasPlaneDistance(Camera camera)
{
if (camera == null)
{
return DefaultCanvasPlaneDistance;
}
float minDistance = camera.nearClipPlane + 0.05f;
float maxDistance = camera.farClipPlane - 0.05f;
if (maxDistance <= minDistance)
{
return minDistance;
}
return Mathf.Clamp(DefaultCanvasPlaneDistance, minDistance, maxDistance);
}
private void TryEnsureUiEventSubscribed()
{
if (_uiEventSubscribed || GameEntry.Event == null)
{
return;
}
GameEntry.Event.Subscribe(OpenUIFormSuccessEventArgs.EventId, OnUIFormChanged);
GameEntry.Event.Subscribe(CloseUIFormCompleteEventArgs.EventId, OnUIFormChanged);
_uiEventSubscribed = true;
_canvasCacheDirty = true;
}
private void UnsubscribeUiEvents()
{
if (!_uiEventSubscribed || GameEntry.Event == null)
{
return;
}
GameEntry.Event.Unsubscribe(OpenUIFormSuccessEventArgs.EventId, OnUIFormChanged);
GameEntry.Event.Unsubscribe(CloseUIFormCompleteEventArgs.EventId, OnUIFormChanged);
_uiEventSubscribed = false;
}
private void OnUIFormChanged(object sender, GameEventArgs e)
{
_canvasCacheDirty = true;
}
private void UpdateMaskLayout()
{
if (!_enableBlackMask)
{
SetMaskVisible(false, false);
return;
}
EnsureMaskObjects();
if (_maskCanvas == null)
{
return;
}
float horizontalPadding = Mathf.Max(0f, (1f - _targetViewport.width) * 0.5f * _cachedScreenWidth);
float verticalPadding = Mathf.Max(0f, (1f - _targetViewport.height) * 0.5f * _cachedScreenHeight);
bool showVerticalMask = horizontalPadding > 0.5f;
bool showHorizontalMask = verticalPadding > 0.5f;
SetMaskVisible(showVerticalMask, showHorizontalMask);
if (showVerticalMask)
{
SetVerticalMaskRect(_leftMask, true, horizontalPadding);
SetVerticalMaskRect(_rightMask, false, horizontalPadding);
}
if (showHorizontalMask)
{
SetHorizontalMaskRect(_topMask, true, verticalPadding);
SetHorizontalMaskRect(_bottomMask, false, verticalPadding);
}
}
private void EnsureMaskObjects()
{
if (_maskCanvas != null)
{
return;
}
GameObject maskRoot = new GameObject(
"ResolutionMask",
typeof(RectTransform),
typeof(Canvas),
typeof(CanvasScaler),
typeof(GraphicRaycaster));
maskRoot.transform.SetParent(transform, false);
_maskCanvas = maskRoot.GetComponent<Canvas>();
_maskCanvas.renderMode = RenderMode.ScreenSpaceOverlay;
_maskCanvas.overrideSorting = true;
_maskCanvas.sortingOrder = short.MaxValue;
CanvasScaler scaler = maskRoot.GetComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ConstantPixelSize;
scaler.scaleFactor = 1f;
GraphicRaycaster raycaster = maskRoot.GetComponent<GraphicRaycaster>();
raycaster.enabled = false;
RectTransform rootRect = maskRoot.GetComponent<RectTransform>();
rootRect.anchorMin = Vector2.zero;
rootRect.anchorMax = Vector2.one;
rootRect.anchoredPosition = Vector2.zero;
rootRect.sizeDelta = Vector2.zero;
_leftMask = CreateMaskRect("LeftMask", rootRect);
_rightMask = CreateMaskRect("RightMask", rootRect);
_topMask = CreateMaskRect("TopMask", rootRect);
_bottomMask = CreateMaskRect("BottomMask", rootRect);
}
private static RectTransform CreateMaskRect(string maskName, Transform parent)
{
GameObject maskObject = new GameObject(maskName, typeof(RectTransform), typeof(Image));
maskObject.transform.SetParent(parent, false);
Image image = maskObject.GetComponent<Image>();
image.color = Color.black;
image.raycastTarget = false;
return maskObject.GetComponent<RectTransform>();
}
private void SetMaskVisible(bool showVerticalMask, bool showHorizontalMask)
{
if (_maskCanvas == null)
{
return;
}
bool visible = showVerticalMask || showHorizontalMask;
_maskCanvas.enabled = visible;
if (_leftMask != null)
{
_leftMask.gameObject.SetActive(showVerticalMask);
}
if (_rightMask != null)
{
_rightMask.gameObject.SetActive(showVerticalMask);
}
if (_topMask != null)
{
_topMask.gameObject.SetActive(showHorizontalMask);
}
if (_bottomMask != null)
{
_bottomMask.gameObject.SetActive(showHorizontalMask);
}
}
private static void SetVerticalMaskRect(RectTransform maskRect, bool isLeft, float width)
{
if (maskRect == null)
{
return;
}
maskRect.anchorMin = new Vector2(isLeft ? 0f : 1f, 0f);
maskRect.anchorMax = new Vector2(isLeft ? 0f : 1f, 1f);
maskRect.pivot = new Vector2(isLeft ? 0f : 1f, 0.5f);
maskRect.anchoredPosition = Vector2.zero;
maskRect.sizeDelta = new Vector2(width, 0f);
}
private static void SetHorizontalMaskRect(RectTransform maskRect, bool isTop, float height)
{
if (maskRect == null)
{
return;
}
maskRect.anchorMin = new Vector2(0f, isTop ? 1f : 0f);
maskRect.anchorMax = new Vector2(1f, isTop ? 1f : 0f);
maskRect.pivot = new Vector2(0.5f, isTop ? 1f : 0f);
maskRect.anchoredPosition = Vector2.zero;
maskRect.sizeDelta = new Vector2(0f, height);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 10f174db309f41b9abaf60bc291fbb2c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -26,6 +26,11 @@
/// 关于。
/// </summary>
AboutForm = 102,
/// <summary>
/// 主界面。
/// </summary>
MainForm = 103,
/// <summary>
/// 测试菜单。
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e1274c270d074c23aaaa30b5397a006d
timeCreated: 1772270815
+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 35168ae7e80542e3a6b97a1607c21df5
timeCreated: 1772270856
@@ -0,0 +1,27 @@
using GameFramework;
using GameFramework.Event;
namespace GeometryTD.CustomEvent
{
public class NodeCompleteEventArgs : GameEventArgs
{
public static int EventId = typeof(NodeCompleteEventArgs).GetHashCode();
public override int Id => EventId;
public NodeCompleteEventArgs()
{
}
public static NodeCompleteEventArgs Create()
{
var args = ReferencePool.Acquire<NodeCompleteEventArgs>();
return args;
}
public override void Clear()
{
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 3c8d1335edfb4fcd8b08279d3c254eff
timeCreated: 1772269787
@@ -0,0 +1,27 @@
using GameFramework;
using GameFramework.Event;
namespace GeometryTD.CustomEvent
{
public class NodeEnterEventArgs : GameEventArgs
{
public static int EventId = typeof(NodeEnterEventArgs).GetHashCode();
public override int Id => EventId;
public NodeEnterEventArgs()
{
}
public static NodeEnterEventArgs Create()
{
var args = ReferencePool.Acquire<NodeEnterEventArgs>();
return args;
}
public override void Clear()
{
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 8a42a26567434ac194a89aa5c9ae6c5d
timeCreated: 1772270138
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 709fe1ffb5c64b7087ca303ece9dfbe3
timeCreated: 1772270876
@@ -0,0 +1,27 @@
using GameFramework;
using GameFramework.Event;
namespace GeometryTD.CustomEvent
{
public class RepoButtonClickedEventArgs : GameEventArgs
{
public static int EventId = typeof(RepoButtonClickedEventArgs).GetHashCode();
public override int Id => EventId;
public RepoButtonClickedEventArgs()
{
}
public static RepoButtonClickedEventArgs Create()
{
var args = ReferencePool.Acquire<RepoButtonClickedEventArgs>();
return args;
}
public override void Clear()
{
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: cdad529781d948de8c745c9285a8dabf
timeCreated: 1772270941
@@ -0,0 +1,27 @@
using GameFramework;
using GameFramework.Event;
namespace GeometryTD.CustomEvent
{
public class ReturnButtonClickedEventArgs : GameEventArgs
{
public static int EventId = typeof(ReturnButtonClickedEventArgs).GetHashCode();
public override int Id => EventId;
public ReturnButtonClickedEventArgs()
{
}
public static ReturnButtonClickedEventArgs Create()
{
var args = ReferencePool.Acquire<ReturnButtonClickedEventArgs>();
return args;
}
public override void Clear()
{
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: fa47a9da112d431f866cb5f3309ad17a
timeCreated: 1772270885
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 7775d869bf1a4026ae3bd5041db6cace
timeCreated: 1772270829
@@ -1,6 +1,9 @@
using GameFramework.Event;
using GameFramework.Fsm;
using GameFramework.Procedure;
using GeometryTD.CustomEvent;
using GeometryTD.Definition;
using GeometryTD.UI;
using UnityGameFramework.Runtime;
namespace GeometryTD.Procedure
@@ -11,6 +14,8 @@ namespace GeometryTD.Procedure
public bool GameStart { get; set; }
private RepoFormUseCase _repoFormUseCase;
#region FSM
protected override void OnInit(IFsm<IProcedureManager> procedureOwner)
@@ -22,11 +27,19 @@ namespace GeometryTD.Procedure
{
base.OnEnter(procedureOwner);
GameEntry.Event.Subscribe(NodeCompleteEventArgs.EventId, OnNodeComplete);
GameEntry.Event.Subscribe(NodeEnterEventArgs.EventId, OnNodeEnter);
GameStart = false;
GameEntry.EventNode.OnInit();
GameEntry.CombatNode.OnInit(LevelThemeType.Plain);
GameEntry.ShopNode.OnInit();
_repoFormUseCase = new RepoFormUseCase();
GameEntry.UIRouter.BindUIUseCase(UIFormType.RepoForm, _repoFormUseCase);
GameEntry.UIRouter.OpenUI(UIFormType.MainForm);
GameEntry.UIRouter.OpenUI(UIFormType.TestMenuForm);
}
@@ -48,9 +61,26 @@ namespace GeometryTD.Procedure
protected override void OnLeave(IFsm<IProcedureManager> procedureOwner, bool isShutdown)
{
GameEntry.CombatNode.OnShutdown();
GameEntry.Event.Unsubscribe(NodeEnterEventArgs.EventId, OnNodeEnter);
GameEntry.Event.Unsubscribe(NodeCompleteEventArgs.EventId, OnNodeComplete);
base.OnLeave(procedureOwner, isShutdown);
}
#endregion
private void OnNodeEnter(object sender, GameEventArgs e)
{
if (!(e is NodeEnterEventArgs)) return;
GameEntry.UIRouter.CloseUI(UIFormType.TestMenuForm);
}
private void OnNodeComplete(object sender, GameEventArgs e)
{
if (!(e is NodeCompleteEventArgs)) return;
GameEntry.UIRouter.OpenUI(UIFormType.TestMenuForm);
}
}
}
}
@@ -0,0 +1,6 @@
namespace GeometryTD.UI
{
public class MainFormContext : UIContext
{
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 50722ebad52b4f619bd33883598842ea
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,79 @@
using GeometryTD.CustomEvent;
using GeometryTD.Definition;
using GameFramework.Event;
using UnityGameFramework.Runtime;
namespace GeometryTD.UI
{
public class MainFormController : UIFormControllerCommonBase<MainFormContext, MainForm>
{
protected override UIFormType UIFormTypeId => UIFormType.MainForm;
protected override void RefreshUI(MainForm form, MainFormContext context)
{
form.RefreshUI(context);
}
protected override void SubscribeCustomEvents()
{
GameEntry.Event.Subscribe(ReturnButtonClickedEventArgs.EventId, OnReturnButtonClicked);
GameEntry.Event.Subscribe(RepoButtonClickedEventArgs.EventId, OnRepoButtonClicked);
}
protected override void UnsubscribeCustomEvents()
{
GameEntry.Event.Unsubscribe(ReturnButtonClickedEventArgs.EventId, OnReturnButtonClicked);
GameEntry.Event.Unsubscribe(RepoButtonClickedEventArgs.EventId, OnRepoButtonClicked);
}
public override int? OpenUI(object userData = null)
{
if (userData is MainFormContext context)
{
return OpenUIInternal(context);
}
if (userData != null)
{
Log.Warning("MainFormController.OpenUI() userData type is invalid.");
return null;
}
return OpenUIInternal(BuildDefaultContext());
}
public override void BindUseCase(IUIUseCase useCase)
{
if (useCase != null)
{
Log.Warning("MainFormController does not use a use case.");
}
}
private static MainFormContext BuildDefaultContext()
{
return new MainFormContext();
}
private void OnReturnButtonClicked(object sender, GameEventArgs e)
{
if (!(sender is MainForm) || !(e is ReturnButtonClickedEventArgs))
{
return;
}
//TODO:游戏 UI 返回菜单 UI
//GameEntry.UIRouter.CloseUI(UIFormType.MainForm);
}
private void OnRepoButtonClicked(object sender, GameEventArgs e)
{
if (!(sender is MainForm) || !(e is RepoButtonClickedEventArgs))
{
return;
}
GameEntry.UIRouter.OpenUI(UIFormType.RepoForm);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 88a17d9010c944a8a0123f31ed09f7d9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,5 +1,5 @@
using GeometryTD.CustomEvent;
using GeometryTD.Definition;
using GeometryTD.UI;
using UnityGameFramework.Runtime;
namespace GeometryTD.UI
@@ -40,7 +40,8 @@ namespace GeometryTD.UI
}
// TODO: 执行 Requirements 校验、CostEffects/RewardEffects 结算与后续节点推进。
GameEntry.Event.Fire(this, NodeCompleteEventArgs.Create());
return m_CurrentEvent.Options[optionIndex];
}
}
}
}
@@ -0,0 +1,49 @@
using GeometryTD.CustomEvent;
using UnityGameFramework.Runtime;
namespace GeometryTD.UI
{
public class MainForm : UGuiForm
{
private MainFormContext _context;
public void RefreshUI(MainFormContext context)
{
_context = context;
if (_context == null)
{
return;
}
}
public void OnReturnButtonClick()
{
GameEntry.Event.Fire(this, ReturnButtonClickedEventArgs.Create());
}
public void OnRepoButtonClick()
{
GameEntry.Event.Fire(this, RepoButtonClickedEventArgs.Create());
}
protected override void OnOpen(object userData)
{
base.OnOpen(userData);
if (userData is MainFormContext context)
{
RefreshUI(context);
return;
}
Log.Warning("MainForm requires MainFormContext as userData.");
}
protected override void OnClose(bool isShutdown, object userData)
{
_context = null;
base.OnClose(isShutdown, userData);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 838d81ed05914e248972f7ad7df7787d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: