diff --git a/code/_full.html b/code/_full.html index 83033ce..e546644 100644 --- a/code/_full.html +++ b/code/_full.html @@ -31,14 +31,10 @@ - + - -
diff --git a/code/_single.html b/code/_sa.html similarity index 99% rename from code/_single.html rename to code/_sa.html index 90e5922..01e7996 100644 --- a/code/_single.html +++ b/code/_sa.html @@ -32,6 +32,9 @@ + + diff --git a/code/_sa_full.html b/code/_sa_full.html new file mode 100644 index 0000000..9fce89e --- /dev/null +++ b/code/_sa_full.html @@ -0,0 +1,2005 @@ + + + + + + ++
광고 서비스 코드의 일부입니다. 프로젝트에 따라 달라지는 광고사 SDK와 상관없이 사용할 수 있게 만들어져있습니다.
+
+| |-- ScreenShotOptionExtension.cs
+| `-- Widgets/
+| `-- WidgetScreenShot.cs
+|-- U5.Service.Ads/
+| `-- Scripts/
+| |-- AdBannerPositionType.cs
+| |-- AdControl.cs
+| |-- AdServiceImpl.cs
+| `-- Impl/
+| |-- AdServiceEditor.cs
+| |-- Admob/
+| | |-- AdServiceAdmob.cs
+| | |-- AdServiceAdmob_Banner.cs
+| | |-- AdServiceAdmob_Interstitial.cs
+| | |-- AdServiceAdmob_RewardVideo.cs
+| | `-- FBAdSettings.cs
+| |-- AppLovin/
+| | |-- AdServiceAppLovin.cs
+| | |-- AdServiceAppLovin_Banner.cs
+| | |-- AdServiceAppLovin_Interstitial.cs
+| | `-- AdServiceAppLovin_RewardVideo.cs
+| |-- CrazyGames/
+| | |-- AdServiceCrazy.cs
+| | |-- AdServiceCrazy_Banner.cs
+| | |-- AdServiceCrazy_Interstitial.cs
+| | `-- AdServiceCrazy_RewardVideo.cs
+| |-- GameDistribution/
+| | |-- AdServiceGameDistribution.cs
+| | |-- AdServiceGameDistribution_Banner.cs
+| | |-- AdServiceGameDistribution_Interstitial.cs
+| | `-- AdServiceGameDistribution_RewardVideo.cs
+| |-- IronSource/
+| | |-- AdServiceIronSource.cs
+| | |-- AdServiceIronSource_Banner.cs
+| | |-- AdServiceIronSource_Interstitial.cs
+| | `-- AdServiceIronSource_RewardVideo.cs
+| `-- Mintegral/
+| |-- AdServiceMintegral.cs
+| |-- AdServiceMintegral_Banner.cs
+| |-- AdServiceMintegral_Interstitial.cs
+| `-- AdServiceMintegral_RewardVideo.cs
+|-- U5.Service.Analytics/
+| `-- Scripts/
+| |-- AnalyticService.cs
+
+
+using UnityEngine;
+#if UNITY_WEBGL && !UNITY_EDITOR
+using D = Unit5.WebPlayerDebug;
+#else
+using D = Unit5.DebugTool;
+#endif
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Text;
+
+using Unit5.Relays;
+
+//----------
+namespace Unit5
+{
+
+//----------
+sealed public partial class AdControl : SingletonMB
+{
+
+ //----------
+ public enum AdState
+ {
+ None,
+
+ AdLoaded,
+ AdFailedToLoad,
+ AdOpening,
+ AdFailedToShow,
+ UserEarnedReward,
+ AdClosed,
+
+ AdWaitReward,
+ }
+
+ //----------
+ AdServiceImpl _impl = null;
+
+ //----------
+ public AdServiceImpl impl => _impl;
+
+ public bool available => (_impl != null && _impl.available);
+ public bool isInited => (_impl != null && _impl.isInited);
+
+ //----------
+ public IRelayLink onInitialized => _impl?.onInitialized;
+ public IRelayLink onOpenedAd => _impl?.onOpenedAd;
+
+ public int reqCountBanner => (_impl != null ? _impl.reqCountBanner : 0);
+ public int reqCountInterstitial => (_impl != null ? _impl.reqCountInterstitial : 0);
+ public int reqCountRewardVideo => (_impl != null ? _impl.reqCountRewardVideo : 0);
+
+ //----------
+ public bool GetGDPRConsent()
+ {
+ if(_impl == null)return false;
+ return _impl.GetGDPRConsent();
+ }
+ public void SetGDPRConsent(bool confirm)
+ {
+ if(_impl == null)return;
+ _impl.SetGDPRConsent(confirm);
+ }
+
+ //----------
+ public void Init(AdServiceImpl impl, Dictionary options=null, bool disabledDefaultAd=false, bool checkGDPRConsent=false, bool autoFetch=true, Action onInitialized=null)
+ {
+ _impl = impl;
+ _impl.Init(options, disabledDefaultAd, checkGDPRConsent, autoFetch, onInitialized);
+ }
+ public void Init(Dictionary options=null, bool disabledDefaultAd=false, bool checkGDPRConsent=false, bool autoFetch=true, Action onInitialized=null)
+ {
+ if(_impl == null)
+ {
+#if ADS_NONE
+ _impl = new AdServiceImpl();
+#elif ADS_ADMOB
+ _impl = new AdServiceAdmob();
+#elif UNITY_EDITOR || ADS_FAKE
+ _impl = new AdServiceEditor();
+#elif ADS_APPLOVIN
+ _impl = new AdServiceAppLovin();
+#elif ADS_IRONSOURCE
+ _impl = new AdServiceIronSource();
+#elif ADS_MINTEGRAL
+ _impl = new AdServiceMintegral();
+#elif ADS_CRAZYGAMES
+ _impl = new AdServiceCrazyGames();
+#elif ADS_GAMEDISTRIBUTION
+ _impl = new AdServiceGameDistribution();
+#else
+ _impl = new AdServiceImpl();
+#endif
+ }
+
+ _impl.Init(options, disabledDefaultAd, checkGDPRConsent, autoFetch, onInitialized);
+ }
+
+ //----------
+ public bool showBanner => (_impl != null && _impl.showBanner);
+ public bool isVisibleBanner => (_impl != null && _impl.isVisibleBanner);
+ public int bannerHeight => (_impl != null ? _impl.bannerHeight : -1);
+
+ public void SetFirstBannerDelay(int duration)
+ {
+ if(_impl == null)return;
+ _impl.SetFirstBannerDelay(duration);
+ }
+ public void SetBannerPosition(AdBannerPositionType position)
+ {
+ if(_impl == null)return;
+ _impl.SetBannerPosition(position);
+ }
+
+ public void ShowBanner(bool forceReload=false)
+ {
+ if(_impl == null)return;
+ _impl.ShowBanner(forceReload);
+ }
+ public void HideBanner(bool forceDestroy=false)
+ {
+ if(_impl == null)return;
+ _impl.HideBanner(forceDestroy);
+ }
+
+ //----------
+ public bool isInterstitialTimeout => (_impl != null ? _impl.isInterstitialTimeout : false);
+
+ public bool CanShowInterstitial()
+ {
+ if(_impl == null)return false;
+ return _impl.CanShowInterstitial();
+ }
+
+ public void ShowInterstitial(Action onComplete=null, int timeout=10)
+ {
+ if(_impl == null)
+ {
+ onComplete(false);
+ return;
+ }
+
+ _impl.ShowInterstitial(onComplete, timeout);
+ }
+ public void CancelInterstitial()
+ {
+ if(_impl == null)return;
+ _impl.CancelInterstitial();
+ }
+ public void FetchInterstitial()
+ {
+ if(_impl == null)return;
+ _impl.FetchInterstitial();
+ }
+
+ //----------
+ public bool isRewardVideoTimeout => (_impl != null ? _impl.isRewardVideoTimeout : false);
+
+ public bool CanShowRewardVideo()
+ {
+ if(_impl == null)return false;
+ return _impl.CanShowRewardVideo();
+ }
+
+ public void ShowRewardVideo(Action onComplete=null, int timeout=10)
+ {
+ if(_impl == null)
+ {
+ onComplete(false);
+ return;
+ }
+
+ _impl.ShowRewardVideo(onComplete, timeout);
+ }
+ public void CancelRewardVideo()
+ {
+ if(_impl == null)return;
+ _impl.CancelRewardVideo();
+ }
+ public void FetchRewardVideo()
+ {
+ if(_impl == null)return;
+ _impl.FetchRewardVideo();
+ }
+
+ //----------
+ public void FetchAll()
+ {
+ if(_impl == null)return;
+ _impl.FetchAll();
+ }
+
+ //----------
+ public void SetNoAdsPurchased(bool purchased)
+ {
+ if(_impl == null)return;
+ _impl.SetNoAdsPurchased(purchased);
+ }
+
+ //----------
+ public void OpenTestSuite()
+ {
+ if(_impl == null)return;
+ _impl.OpenTestSuite();
+ }
+}
+
+}
+
+ 크로스 프로모션 서비스 코드의 일부입니다. 배너/동영상 위젯 등의 컴포넌트와 유틸리티들과 서버 측 서비스 코드로 구성되어 있습니다.
+
+| | |-- GoogleImpl.cs
+| | `-- UnityImpl.cs
+| `-- MultiAnalyticsImpl.cs
+|-- U5.Service.CrossPromotion/
+| |-- Assets/
+| | |-- LAni_cp_LEFT.anim
+| | |-- LAni_cp_RIGHT.anim
+| | |-- Merriweather-Regular-UNIT5CP.ttf
+| | |-- RobotoCondensed-Regular-slim.ttf
+| | |-- UNIT5_CP_frame_512.png
+| | `-- UNIT5_CP_frame_512.psd
+| `-- Scripts/
+| |-- PromotionControl.cs
+| |-- PromotionInfo.cs
+| |-- PromotionReward.cs
+| `-- Widgets/
+| |-- WidgetCrossBanner.cs
+| |-- WidgetCrossInterstitial.cs
+| `-- WidgetCrossVideo.cs
+|-- U5.Service.IAP/
+| |-- Editor/
+| | `-- StoreSettingsInspector.cs
+
+
+using UnityEngine;
+#if UNITY_WEBGL && !UNITY_EDITOR
+using D = Unit5.WebPlayerDebug;
+#else
+using D = Unit5.DebugTool;
+#endif
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+
+using Unit5;
+using Unit5.MiniJSON;
+
+//----------
+namespace Unit5.Service
+{
+
+//----------
+sealed public class PromotionControl
+{
+
+ //----------
+ const string PROMOTION_URL = "_PROMOTION_URL";
+ const string PROMOTION_CACHE = "_PROMOTION_CACHE";
+
+ //----------
+ static public bool isReady{get; private set;}
+ static public bool hasData{get; private set;}
+
+ //----------
+ static public List infos{get; private set;}
+
+ //----------
+ static public PromotionInfo Get(bool notInstalled=false)
+ {
+ if(!hasData)return null;
+
+ //
+ List samples = (notInstalled ? GetNotInstalled() : infos);
+ if(samples.IsNullOrEmpty())samples = infos;
+ if(samples.IsNullOrEmpty())return null;
+
+ if(samples.Count == 1)return samples[0];
+
+ //
+ if(notInstalled)return GetNotInstalled().Choice();
+
+ int[] weights = samples.Select((d) => d.weight).ToArray();
+ int weightTotal = weights.Sum();
+
+ int dataIndex = MathUtil.RandomChoice(weights, weightTotal);
+ dataIndex = Mathf.Max(0, Mathf.Min(infos.Count - 1, dataIndex));
+ return infos[dataIndex];
+ }
+ static public PromotionInfo Get(string appId)
+ {
+ if(!hasData)return null;
+
+ return infos.Find(i => i.appId == appId);
+ }
+
+ static public List GetNotInstalled()
+ {
+ if(!hasData)return null;
+
+ return infos.Where(i => !IsInstalled(i)).ToList();
+ }
+
+ static public List GetAll(PromotionReward type)
+ {
+ if(!hasData)return null;
+
+ return infos.Where(i => i.reward == type).ToList();
+ }
+ static public List GetAll(bool hasExtra)
+ {
+ if(!hasData)return null;
+
+ return infos.Where(i => !string.IsNullOrEmpty(i.extra)).ToList();
+ }
+ static public List GetAll(PromotionReward type, bool hasExtra)
+ {
+ if(!hasData)return null;
+
+ return infos.Where(i => i.reward == type && !string.IsNullOrEmpty(i.extra)).ToList();
+ }
+
+ //----------
+ static public void CheckData(string url, Action callback)
+ {
+ if(string.IsNullOrEmpty(url))
+ {
+ callback(false);
+ return;
+ }
+
+ if(hasData)
+ {
+ callback(true);
+ return;
+ }
+
+ //
+ string oldCacheFilepath = EncryptedPlayerPrefs.GetString(PROMOTION_CACHE, string.Empty);
+ string newCacheFilepath = RequestUtil.GetCacheFilepath(url);
+
+ FetchData(url, newCacheFilepath, (bool success) => {
+ if(success)
+ {
+ if(!string.IsNullOrEmpty(oldCacheFilepath))
+ {
+ StreamingAssetUtil.RemoveFile(oldCacheFilepath);
+ }
+
+ EncryptedPlayerPrefs.SetString(PROMOTION_URL, url);
+ EncryptedPlayerPrefs.SetString(PROMOTION_CACHE, newCacheFilepath);
+ }
+
+ callback((success && hasData));
+ });
+ }
+
+ //----------
+ static public void FetchData(string url, Action callback)
+ {
+ FetchData(url, null, callback);
+ }
+ static public void FetchData(string url, string cacheFilepath, Action callback)
+ {
+ RequestUtil.Text(url, cacheFilepath, (string data) => {
+#if !NO_DEBUG
+ D.Log($"[UNIT5::PromotionControl] FetchData:: url={url} / cache={cacheFilepath} / data={data}");
+#endif
+
+#if UNITY_EDITOR
+ if(string.IsNullOrEmpty(data))
+ {
+ MakeTestData();
+
+ callback(true);
+ return;
+ }
+#endif
+
+ if(string.IsNullOrEmpty(data))
+ {
+ isReady = false;
+ hasData = false;
+
+ callback(false);
+ }
+ else
+ {
+ isReady = true;
+
+ try
+ {
+ Dictionary json = Json.Deserialize(data) as Dictionary;
+
+ string appId = UnityUtil.GetApplicationID();
+
+ infos = json.GetDictionaryList("p")
+ .Select((d) => new PromotionInfo(d))
+ .Where((i) => i.appBundleId != appId)
+ .ToList()
+ ;
+
+ hasData = (infos.Count > 0);
+ }
+ catch
+ {
+ hasData = false;
+ }
+
+ callback(hasData);
+ }
+ });
+ }
+
+#if UNITY_EDITOR
+ static void MakeTestData()
+ {
+ isReady = true;
+ hasData = true;
+
+ infos = new List(){
+ new PromotionInfo(){
+ appBundleId = "com.ftt.cubie.aos",
+ appId = "com.ftt.cubie.aos",
+ appScheme = "cubieadventure",
+
+ weight = 1,
+
+ appName = "Cubie Adventure",
+ appDesc = "Adventure with your Cubie and Cupet friends!\nFrom cute looks and immersive gameplay! Cubie Adventure welcomes you!",
+ appLink = "http://onelink.to/mjnhpp",
+
+ imgThumb = "https://share.unit5soft.com/_crosspromotion/ca_thumb.png",
+ imgBanner = "https://share.unit5soft.com/_crosspromotion/ca_banner.jpg",
+ imgPopup = "https://share.unit5soft.com/_crosspromotion/ca_full.jpg",
+
+ vidClip = "https://share.unit5soft.com/_crosspromotion/ca_clip.mp4",
+
+ reward = PromotionReward.None,
+
+ extra = "",
+ },
+ };
+ }
+#endif
+
+ //----------
+ static public bool IsInstalled(PromotionInfo info)
+ {
+#if !UNITY_EDITOR && (UNITY_IOS || UNITY_TVOS || UNITY_IPHONE)
+ return UnityUtil.IsInstalled(info.appScheme);
+#else
+ return UnityUtil.IsInstalled(info.appId);
+#endif
+ }
+
+ //----------
+ static public bool IsMarked(string appId)
+ {
+ return (EncryptedPlayerPrefs.GetInt("_XP_" + appId, 0) == 1);
+ }
+ static public void MarkApp(string appId)
+ {
+ EncryptedPlayerPrefs.SetInt("_XP_" + appId, 1);
+ }
+}
+
+}
+
+ 값이나 상태의 변경 시 발생하는 화면 처리를 단순히 하기 위한 모듈의 일부입니다. 구매버튼, 다국어 이미지 등 프로젝트와 관계없이 일반화가 가능한 많은 부분을 모듈화하여 사용했습니다.
+
+| |-- BaseN.cs
+| |-- StringGenerator.cs
+| `-- ZBase32.cs
+|-- U5.Data.Binding/
+| |-- Editor/
+| | |-- BindingInfoInspector.cs
+| | `-- Components/
+| | |-- BindActionInspector.cs
+| | |-- BindGameObjectToggleInspector.cs
+| | |-- BindSpriteInspector.cs
+| | |-- BindTextMeshInspector.cs
+| | |-- BindTextureInspector.cs
+| | |-- BindUIButtonInspector.cs
+| | |-- BindUILabelInspector.cs
+| | |-- BindUIProgressBarInspector.cs
+| | |-- BindUISpanInspector.cs
+| | |-- BindUISpriteInspector.cs
+| | |-- BindUITextureInspector.cs
+| | |-- BindUIToggleInspector.cs
+| | `-- DataSetterInspector.cs
+| `-- Scripts/
+| |-- BindingFieldType.cs
+| |-- BindingInfo.cs
+| |-- BindingInfos.cs
+| |-- Components/
+| | |-- BindAction.cs
+| | |-- BindGameObjectToggle.cs
+| | |-- BindSprite.cs
+| | |-- BindTextMesh.cs
+| | |-- BindTexture.cs
+| | |-- BindUIButton.cs
+| | |-- BindUILabel.cs
+| | |-- BindUIProgressBar.cs
+| | |-- BindUISpan.cs
+| | |-- BindUISprite.cs
+| | |-- BindUITexture.cs
+| | `-- BindUIToggle.cs
+| |-- DataContext.cs
+| |-- DataSetter.cs
+| |-- IDataContext.cs
+| |-- IDataProvider.cs
+| `-- PropertyData.cs
+|-- U5.Debug/
+| `-- Scripts/
+| |-- DebugTool.cs
+
+
+using UnityEngine;
+#if UNITY_WEBGL && !UNITY_EDITOR
+using D = Unit5.WebPlayerDebug;
+#else
+using D = Unit5.DebugTool;
+#endif
+using TMPro;
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+
+//----------
+namespace Unit5
+{
+
+//----------
+public class BindTextMesh : DataSetter
+{
+
+ //----------
+ [Space(10)]
+ [SerializeField] TextMeshPro lblText = null;
+ [SerializeField] UILocalizedLabel lblLocalize = null;
+ [SerializeField] TweenLabelCounter tweenLabel = null;
+
+ [Space(10)]
+ [SerializeField] string format;
+
+ [Space(10)]
+ [SerializeField] float duration = 1.0f;
+ [SerializeField] float amountPerDuration = 0.0f;
+ [SerializeField] float durationMin = 0.3f;
+ [SerializeField] float durationMax = 2.1f;
+
+ //----------
+ string text;
+
+ //----------
+ override protected void OnValueChanged(string newValue)
+ {
+ UpdateText(newValue);
+ }
+
+ //----------
+ void OnLocalize()
+ {
+ UpdateText(text);
+ }
+
+ //----------
+ void UpdateText(string text)
+ {
+ if(lblLocalize != null)
+ {
+ UpdateLocalize(text);
+ }
+ else if(tweenLabel == null)
+ {
+ UpdateDirect(text);
+ }
+ else
+ {
+ UpdateTween(text);
+ }
+
+ this.text = text;
+ }
+
+ //----------
+ void UpdateLocalize(string text)
+ {
+ lblLocalize.text = text;
+ }
+ void UpdateDirect(string text)
+ {
+ if(string.IsNullOrEmpty(format))
+ {
+ lblText.text = text;
+ }
+ else
+ {
+ if(format.StartsWith("@"))
+ {
+ lblText.text = Localization2.Get(format, text);
+ }
+ else
+ {
+ lblText.text = string.Format(format, text);
+ }
+ }
+ }
+ void UpdateTween(string text)
+ {
+ int v = (string.IsNullOrEmpty(text) ? 0 : int.Parse(text));
+
+ if(isReady)
+ {
+ float d = (amountPerDuration > 0.0f ? Mathf.Abs(v - tweenLabel.currentCount) / amountPerDuration : 1.0f) * duration;
+ if(durationMin > 0.0f)d = Mathf.Max(durationMin, d);
+ if(durationMax > 0.0f)d = Mathf.Min(durationMax, d);
+
+ tweenLabel.Play(d, tweenLabel.currentCount, v);
+ }
+ else
+ {
+ tweenLabel.from = tweenLabel.to = v;
+ tweenLabel.Sample(1.0f, true);
+
+ isReady = true;
+ }
+ }
+}
+
+}
+
+ 화면의 전체나 일부를 캡하는 위젯입니다. 협업 개발의 편의성을 위해 많은 부분을 위젯 형태로 만들어 인스펙터에서 조작하는 것만으로 처리가 가능하게 했습니다.
+
+| | |-- QueryBuilder.cs
+| | `-- SQLiteORM.cs
+| `-- SQLiteSimple.cs
+|-- U5.ScreenShot/
+| |-- Editor/
+| | `-- WidgetScreenShotInspector.cs
+| `-- Scripts/
+| |-- AnchorHorizontal.cs
+| |-- AnchorVertical.cs
+| |-- ScreenShotControl.cs
+| |-- ScreenShotOption.cs
+| |-- ScreenShotOptionExtension.cs
+| `-- Widgets/
+| `-- WidgetScreenShot.cs
+|-- U5.Service.Ads/
+| `-- Scripts/
+| |-- AdBannerPositionType.cs
+
+
+using UnityEngine;
+#if UNITY_WEBGL && !UNITY_EDITOR
+using D = Unit5.WebPlayerDebug;
+#else
+using D = Unit5.DebugTool;
+#endif
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+
+//----------
+namespace Unit5
+{
+
+//----------
+sealed public class WidgetScreenShot : MonoBehaviour
+{
+
+ //----------
+ public enum CropMode
+ {
+ Option = 0,
+ Area,
+ }
+
+ //----------
+ [Space(10)]
+ [SerializeField] Camera[] cameras = null;
+
+ [Space(10)]
+ [SerializeField] [DelayedAttribute] int size = 320;
+
+ [Space(10)]
+ [SerializeField] CropMode cropMode = CropMode.Option;
+ [SerializeField] ScreenShotOption cropOption = null;
+ [SerializeField] SpriteRenderer cropArea = null;
+
+ [Space(10)]
+ [SerializeField] Texture2D overlay = null;
+ [SerializeField] ScreenShotOption merge = null;
+
+ //----------
+ [NonSerialized] [HideInInspector] public Texture2D lastScreenShot;
+
+ //----------
+ public bool isEnabled => (!cameras.IsNullOrEmpty());
+
+ public ScreenShotOption crop => (cropMode == CropMode.Option ? cropOption : CalcArea());
+ ScreenShotOption CalcArea()
+ {
+ ScreenShotOption option = (cropOption == null ? new ScreenShotOption() : cropOption.Clone());
+ if(cropArea == null)return option;
+
+ float fh = Camera.main.orthographicSize * 2f;
+ float fw = fh / Camera.main.pixelHeight * Camera.main.pixelWidth;
+ float iw = cropArea.bounds.size.x;
+ float ih = cropArea.bounds.size.y;
+
+ option.anchorX = AnchorHorizontal.Left;
+ option.anchorY = AnchorVertical.Top;
+ option.width = iw / fw;
+ option.height = ih / fh;
+ option.offsetX = ((fw - iw) * 0.5f - (Camera.main.transform.position.x - cropArea.bounds.center.x)) / fw;
+ option.offsetY = ((fh - ih) * 0.5f - (Camera.main.transform.position.y - cropArea.bounds.center.y)) / fh;
+ return option;
+ }
+
+ //----------
+ void OnDestroy()
+ {
+ Clear();
+ }
+
+ //----------
+ public void Clear()
+ {
+ if(lastScreenShot != null)GameObject.Destroy(lastScreenShot);
+ lastScreenShot = null;
+ }
+
+ //----------
+ public Texture2D TakeScreenShot()
+ {
+ lastScreenShot = TakeCroppedScreenShot(Screen.width, Screen.height);
+
+ return lastScreenShot;
+ }
+
+ //----------
+ public Texture2D TakeCroppedScreenShot(int width, int height)
+ {
+ int w = size;
+ int h = Mathf.RoundToInt(size * (height / (float)width));
+
+ return TakeCroppedScreenShot(w, h, crop);
+ }
+ public Texture2D TakeCroppedScreenShot(int width, int height, ScreenShotOption option)
+ {
+ Texture2D image = CaptureFrame(width, height);
+ if(option.width == 1.0f && option.height == 1.0f && option.offsetX == 0.0f && option.offsetY == 0.0f)
+ {
+ if(overlay != null)
+ {
+ MergeOverlay(ref image);
+ }
+
+ return image;
+ }
+
+ Texture2D result = CropImage(ref image, option);
+
+#if UNITY_EDITOR
+ DestroyImmediate(image);
+#else
+ Destroy(image);
+#endif
+
+ if(overlay != null)
+ {
+ MergeOverlay(ref result);
+ }
+
+ return result;
+ }
+
+ //----------
+ public Texture2D CaptureFrame(int width, int height)
+ {
+ RenderTexture rt = new RenderTexture(width, height, 16, RenderTextureFormat.ARGB32);
+ foreach(Camera cam in cameras)
+ {
+ RenderTexture tt = cam.targetTexture;
+ cam.targetTexture = rt;
+ cam.Render();
+ cam.targetTexture = tt;
+ }
+ RenderTexture.active = rt;
+
+ Texture2D image = new Texture2D(width, height, TextureFormat.RGB24, false);
+ image.ReadPixels(new Rect(0, 0, width, height), 0, 0, false);
+ image.Apply();
+
+ RenderTexture.active = null;
+#if UNITY_EDITOR
+ DestroyImmediate(rt);
+#else
+ Destroy(rt);
+#endif
+
+ return image;
+ }
+
+ public Texture2D CropImage(ref Texture2D source, ScreenShotOption option)
+ {
+ Rect rect = option.Crop(source.width, source.height);
+
+ Texture2D result = new Texture2D((int)rect.width, (int)rect.height, TextureFormat.RGBA32, false);
+ result.SetPixels(source.GetPixels((int)rect.x, (int)rect.y, (int)rect.width, (int)rect.height));
+ result.Apply();
+
+ return result;
+ }
+
+ public Texture2D ResizeImage(ref Texture2D source, int width, int height)
+ {
+ Texture2D result = new Texture2D(width, height, source.format, false);
+
+ float w = (float)width;
+ float h = (float)height;
+
+ for(int y = 0; y < result.height; y += 1)
+ {
+ for(int x = 0; x < result.width; x += 1)
+ {
+ result.SetPixel(x, y, source.GetPixelBilinear(x / w, y / h));
+ }
+ }
+ result.Apply();
+ return result;
+ }
+
+ public void MergeOverlay(ref Texture2D source)
+ {
+ MergeOverlay(ref source, merge);
+ }
+ public void MergeOverlay(ref Texture2D source, ScreenShotOption option)
+ {
+ int ow = source.width;
+ int oh = Mathf.FloorToInt(source.width * (overlay.height / (float)overlay.width));
+
+ Rect rect = option.Place(source.width, source.height, ow, oh);
+ ow = (int)rect.width;
+ oh = (int)rect.height;
+
+ Texture2D resizedOverlay = ResizeImage(ref overlay, ow, oh);
+
+ int sx = Mathf.Max(0, Mathf.Min(source.width - ow, (int)rect.x));
+ int sy = Mathf.Max(0, Mathf.Min(source.height - oh, (int)rect.y));
+ int sw = Mathf.Min(source.width, sx + ow);
+ int sh = Mathf.Min(source.height, sy + oh);
+
+ int px;
+ int py;
+ Color sourceColor;
+ Color overlayColor;
+ Color finalColor;
+ for(int x = sx; x < sw; x += 1)
+ {
+ for(int y = sy; y < sh; y += 1)
+ {
+ px = x - sx;
+ py = y - sy;
+
+ sourceColor = source.GetPixel(x, y);
+ overlayColor = resizedOverlay.GetPixel(px, py);
+ finalColor = Color.Lerp(sourceColor, overlayColor, overlayColor.a / 1.0f);
+ source.SetPixel(x, y, finalColor);
+ }
+ }
+
+ source.Apply();
+
+#if UNITY_EDITOR
+ DestroyImmediate(resizedOverlay);
+#else
+ Destroy(resizedOverlay);
+#endif
+ }
+}
+
+}
+
+
+//====================
+// 블럭 게임의 일부입니다
+//====================
+ void RestoreGameStatus(bool passAppResume=false)
+ {
+ string data = GameData.GetGameData();
+ GameData.ClearGameData();
+
+ // NOTE: 튜토리얼 이후 보드상태, 가운데에 4놓여있고 다음유닛은 4
+ // data = "E?|1|6|0|1|1|0;2:2:0|0;2:2:6||0:2|13|0";
+
+ // NOTE: 가운데 x인 환경
+ // data = "E?|1|8|17|3|6|0;6:0:0|0;7:2:6,0;6:2:5,0;4:2:4,0;5:2:3,0;2:2:2,0;6:2:1,0;5:0:6|||14|0";
+
+ // D.Log(data);
+ // DebugTool.Report(data, false);
+
+ if(!string.IsNullOrEmpty(data))
+ {
+ MakeSavedLevel(data, passAppResume);
+ }
+ else
+ {
+ if(Game.skipLevelId > 0)
+ {
+ int skippedGoal = Game.skipLevelId;
+
+ int lastClearGoal = GameData.GetLastClearedGoal(GameSettings.initialGoalA);
+ if(lastClearGoal < skippedGoal)
+ {
+ GameData.SetLastPlayedGoal(skippedGoal);
+ GameData.SetLastClearedGoal(skippedGoal);
+ }
+
+ int skippedLevel = 1;
+ for(int i = 0; i < skippedGoal + 10; i += 1)
+ {
+ if(skippedGoal != GetGoalLevel(i + 1))continue;
+
+ skippedLevel = i + 1;
+ break;
+ }
+
+ int nextLevel = skippedLevel + 1;
+
+ OpenLevelStart(nextLevel, () => {
+ MakeLevel(nextLevel, null, skippedGoal);
+ });
+ }
+ else
+ {
+ OpenLevelStart(1);
+ }
+ }
+
+ // D.Log(GameData.GetGameData());
+ }
+ void SaveGameStatus(bool isPlaying=true)
+ {
+ VUnit goalUnit = model.units.Find((u) => (u.type == UnitType.Block && u.level == goal));
+ List remainUnits = model.units.Where((u) => (u != goalUnit && u != model.currUnit)).ToList();
+
+ string serializedNextUnit = (model.currUnit == null ? "" : model.currUnit.GetSerializedData());
+ if(model.isItemMode && model.keepedUnit != null)serializedNextUnit = model.keepedUnit.GetSerializedData();
+ string serializedRemainUnits = remainUnits.Select((u) => u.GetSerializedData()).Join(",");
+ string serializedGoalUnit = (goalUnit == null ? "" : goalUnit.GetSerializedData());
+ string serializedQueueUnits = unitQueue.Select((u) => $"{((int)u.type)}:{u.level}").Join(",");
+
+ string serializedReviceCount = GameData.GetReviveCount().ToString();
+
+ GameData.SetGameData($"E?|{stage}|{goal}|{GameData.GetMergeCount()}|{GameData.GetUserLevel()}|{GameData.GetScore()}|{serializedNextUnit}|{serializedRemainUnits}|{serializedGoalUnit}|{serializedQueueUnits}|{GameData.guestId}|{serializedReviceCount}");
+ }
+}
+
+//====================
+ void MergeCoins(List mergeFrom, VUnit mergeTo)
+ {
+ mergeTo.SetDirty();
+
+ Vector3 targetPos = mergeTo.localPos;
+
+ //
+ int mergeCount = mergeFrom.Count;
+
+ int coinIncrAmount = 0;
+ for(int i = 0; i < mergeCount; i += 1)
+ {
+ coinIncrAmount += mergeFrom[i].level - 1 + 3;
+ }
+
+ if(mergeFrom.Count == 0)
+ {
+ mergeTo.PlayAnim(VUnit.ANIM_STATE_MERGE_OUT, () => {
+ mergeTo.IncrLevel(coinIncrAmount);
+
+ mergeTo.PlayAnim(VUnit.ANIM_STATE_MERGE_NEW);
+ });
+ mergeTo.SetDirty(false);
+ }
+ else
+ {
+ for(int i = 0; i < mergeCount; i += 1)
+ {
+ if(i < mergeCount - 1)
+ {
+ MergeTo(mergeFrom[i], targetPos);
+ }
+ else
+ {
+ MergeTo(mergeFrom[i], targetPos, () => {
+ mergeTo.PlayAnim(VUnit.ANIM_STATE_MERGE_OUT, () => {
+ mergeTo.IncrLevel(coinIncrAmount);
+
+ mergeTo.PlayAnim(VUnit.ANIM_STATE_MERGE_NEW);
+ });
+ mergeTo.SetDirty(false);
+ });
+ }
+ }
+ }
+
+ //
+ Effect.Spawn(VfxEffectType.Merge, targetPos);
+ Effect.Play(model.GetComboSFX());
+ Effect.Vibrate((model.GetCombo() > 2 ? VibrationStyle.HEAVY : VibrationStyle.MEDIUM));
+
+ //
+ model.ReorderUnits(mergeTo);
+ }
+
+//====================
+ async UniTask WaitEndOfMovingAsync()
+ {
+ while(true)
+ {
+ await UniTask.Yield();
+
+ int bCount = model.units.Count((u) => u.isMoving);
+
+ int kCount = model.removedUnits.Count((u) => u.isMoving);
+ if(kCount == 0 && !model.removedUnits.IsNullOrEmpty())
+ {
+ model.removedUnits.ForEach((u) => u.Release());
+ model.removedUnits = new List();
+ }
+
+ if(bCount + kCount == 0)break;
+ }
+
+ model.ReorderUnits();
+
+ CheckArrows();
+
+ SaveGameStatus();
+ }
+
+ async UniTaskVoid CheckMoveAsync(VUnit unit)
+ {
+ navigationController.ToggleBlockerPanel(true);
+
+ //
+ int currentMergeCount = GameData.GetMergeCount();
+
+ //
+ isWaitingDropAndHit = true;
+ currDroppedUnit = unit;
+
+ //
+ if(model.units.Any((u) => u.isMoving))
+ {
+ await WaitEndOfMovingAsync();
+ }
+
+ //
+ while(true)
+ {
+ int changed = 0;
+
+ //
+ if(DropUnit())
+ {
+ changed += 1;
+ await WaitEndOfMovingAsync();
+ }
+
+ //
+ if(CheckHit())
+ {
+ changed += 1;
+ await WaitEndOfMovingAsync();
+ }
+
+ //
+ if(changed == 0)break;
+ }
+
+ // NOTE: 딜레이 없고, 콤보는 한번 drop 에서만
+ model.UpdateCombo();
+
+ await WaitEndOfMovingAsync();
+
+ //
+ navigationController.ToggleBlockerPanel(false);
+
+ //
+ isWaitingDropAndHit = false;
+ currDroppedUnit = null;
+
+ //
+ model.CalcActionIds();
+
+ //
+ CheckArrows();
+
+ SaveGameStatus();
+
+ //
+ if(model.CheckDead() || model.isTimeOver)
+ {
+ WillGameOver();
+
+ if(model.isTimeOver)
+ {
+ OnTimeOver();
+ }
+ else
+ {
+ Effect.Spawn(VfxUIEffectType.BoardFull, () => {
+ OnGameOver();
+ });
+ }
+ }
+ else
+ {
+ // int x = (model.IsEmptyCol(model.centerCol, true) ? model.centerCol : -1);
+ // NOTE: 마지막 떨어진 곳 사용, 안되면 빈곳
+ int x = model.GetEmptyCol(model.lastCol);
+
+ VUnit spawnedUnit = (x == -1 ? null : SpawnUnit(x));
+ GameData.AddEarnedUnit(spawnedUnit.level);
+
+ model.currUnit = spawnedUnit;
+
+ //
+ env.ShowNextUnit(GetNextUnit(true));
+
+ FillNextUnits();
+
+ model.ReorderUnits(model.currUnit, true);
+
+ CheckArrows();
+
+ SaveGameStatus();
+ }
+ }
+
+
+//====================
+// 포커 게임의 일부입니다.
+//====================
+ void ReplaceCard(Card target)
+ {
+ DoReplaceCardAsync(target).Forget();
+ }
+ async UniTaskVoid DoReplaceCardAsync(Card target)
+ {
+ CancellationToken ct = gameObject.GetCancellationTokenOnDestroy();
+
+ //
+ target.order = 301;
+
+ int x = target.gx;
+ int y = target.gy;
+
+ float delay = 0.0f;
+
+ await MoveBoardCardToTrash.Create(target, Vector3.zero, ref delay).Play().AwaitForComplete(cancellationToken: ct);
+
+ model.RemoveCard(target);
+
+ //
+ Card card = null;
+ Sequence seq = DOTween.Sequence();
+
+ bool hasNextCard = model.hasNextCard;
+ if(hasNextCard)
+ {
+ card = model.GetCardFromDeck(x, y);
+
+ MoveDeckCardToBoard.Create(seq, card, new Vector3(env.GetRealX(x), env.GetRealY(y), 0), card.gy * env.cols + card.gx, ref delay);
+
+ hasNextCard = model.hasNextCard;
+ }
+ if(hasNextCard)
+ {
+ card = model.GetCardFromDeck(-1, -1, true);
+
+ if(!card.isFront)OpenDeckCard.Create(seq, card, ref delay);
+ }
+ if(seq.Duration() > 0.0f)
+ {
+ await seq.Play().AwaitForComplete(cancellationToken: ct);
+ }
+
+ //
+ model.UpdateOrders();
+
+ //
+ if(!hasNextCard)
+ {
+ WillGameOver();
+
+ OnOutOfCards();
+ }
+ }
+
+//====================
+ async UniTaskVoid DoNextAsync(List hands)
+ {
+ IncrQuest(hands.Count);
+
+ //
+ await DoMoveHandsToTrashAsync(hands);
+
+ await DoDropBoardCardsAsync();
+
+ await DoMoveDeckToBoardAsync();
+
+ //
+ if(!model.hasNextCard)
+ {
+ WillGameOver();
+ OnOutOfCards();
+ }
+ }
+ async UniTask DoMoveHandsToTrashAsync(List hands)
+ {
+ List cards;
+ HandType result = HandSolver.Solve(ref hands, out cards);
+
+ if(hands[0].data.Equals(CardData.JOKER))hands[0].SetData(cards[0]);
+ hands.Sort();
+
+ if(result != HandType.ROYAL_STRAIGHT_FLUSH_NO_WILD && result != HandType.ROYAL_STRAIGHT_FLUSH && result != HandType.STRAIGHT_FLUSH && result != HandType.STRAIGHT)hands.Reverse();
+
+ for(int i = 0; i < hands.Count; i += 1)hands[i].order = Mathf.RoundToInt(301 + i);
+
+ Dictionary> groups = new Dictionary>();
+ Dictionary ranks;
+ int[] rks;
+ switch(result)
+ {
+ case HandType.ROYAL_STRAIGHT_FLUSH_NO_WILD:
+ case HandType.FIVE_OF_A_KIND:
+ case HandType.ROYAL_STRAIGHT_FLUSH:
+ case HandType.STRAIGHT_FLUSH:
+ case HandType.FLUSH:
+ case HandType.STRAIGHT:
+ groups[1] = hands;
+ break;
+
+ case HandType.FOUR_OF_A_KIND:
+ case HandType.THREE_OF_A_KIND:
+ case HandType.ONE_PAIR:
+ ranks = new Dictionary();
+ hands.ForEach((c) => {
+ if(!ranks.ContainsKey(c.data.rank))ranks[c.data.rank] = 0;
+ ranks[c.data.rank] += 1;
+ });
+ rks = ranks.Keys.OrderBy((r) => -ranks[r]).ToArray();
+
+ groups[1] = hands.Where((c) => (c.data.rank == rks[0])).ToList();
+ break;
+
+ case HandType.FULL_HOUSE:
+ case HandType.TWO_PAIR:
+ ranks = new Dictionary();
+ hands.ForEach((c) => {
+ if(!ranks.ContainsKey(c.data.rank))ranks[c.data.rank] = 0;
+ ranks[c.data.rank] += 1;
+ });
+ rks = ranks.Keys.OrderBy((r) => -ranks[r]).ToArray();
+
+ groups[1] = hands.Where((c) => (c.data.rank == rks[0])).ToList();
+ groups[2] = hands.Where((c) => (c.data.rank == rks[1])).ToList();
+ break;
+ }
+
+ Sequence seq = DOTween.Sequence();
+
+ float margin = -env.cardMargin;
+ float cardWidth = -env.cardWidth;
+ float groupScale = -env.groupScale;
+ float sx = ((hands.Count - 1) * margin) * 0.5f;
+ float delay = 0.0f;
+
+ env.SetHandBackground(true);
+ env.SetHandGroup1(false);
+ env.SetHandGroup2(false);
+ if(groups.ContainsKey(1))
+ {
+ env.SetHandGroup1(
+ true,
+ groups[1].Select((c) => new Vector3(sx + hands.IndexOf(c) * cardWidth, 0.0f, 0.0f)).Avg(),
+ new Vector3(groups[1].Count * groupScale, groupScale, 0.0f)
+ );
+ }
+ if(groups.ContainsKey(2))
+ {
+ env.SetHandGroup2(
+ true,
+ groups[2].Select((c) => new Vector3(sx + hands.IndexOf(c) * cardWidth, 0.0f, 0.0f)).Avg(),
+ new Vector3(groups[2].Count * groupScale, groupScale, 0.0f)
+ );
+ }
+ ShowResult.Create(seq, env.handBackground);
+
+ for(int i = 0; i < hands.Count; i += 1)
+ {
+ MoveBoardCardToResult.Create(seq, hands[i], new Vector3(sx + i * 6.0f, 0.0f, 0.0f), ref delay);
+ }
+ await seq.Play().AwaitForComplete(cancellationToken: ct);
+
+ await UniTask.Delay(600, cancellationToken: ct);
+
+ hands.ForEach((c) => c.Release());
+
+ env.SetHandBackground(false);
+ }
+ async UniTask DoDropBoardCardsAsync()
+ {
+ Card card = null;
+
+ //
+ float delay = 0.0f;
+
+ Sequence seq = DOTween.Sequence();
+
+ for(int y = env.rows - 1; y > -1; y -= 1)
+ {
+ for(int x = 0; x < env.cols; x += 1)
+ {
+ card = model.GetCard(x, y);
+ if(card == null)continue;
+
+ int ny = -1;
+ for(int by = y + 1; by < env.rows; by += 1)
+ {
+ if(env.IsInside(x, by) && model.GetCard(x, by) != null)break;
+
+ ny = by;
+ }
+ if(ny == -1)continue;
+
+ if(card.gy == ny)continue;
+
+ model.MoveCard(card, x, ny);
+
+ DropBoardCard.Create(seq, card, env.GetRealY(card.gy), card.gy - y, ref delay);
+ }
+ }
+ await seq.Play().AwaitForComplete(cancellationToken: ct);
+
+ //
+ model.UpdateOrders();
+ }
+ async UniTask DoMoveDeckToBoardAsync()
+ {
+ Card card = null;
+
+ //
+ float delay = 0.0f;
+
+ Sequence seq = DOTween.Sequence();
+
+ bool hasNextCard = model.hasNextCard;
+ if(hasNextCard)
+ {
+ int loopLimit = model.gridPositions.Count;
+ int x, y;
+ for(int i = 0; i < loopLimit; i += 1)
+ {
+ x = model.gridPositions[i].x;
+ y = model.gridPositions[i].y;
+
+ if(model.GetCard(x, y) != null)continue;
+
+ //
+ card = model.GetCardFromDeck(x, y);
+
+ if(card.isFront)
+ {
+ MoveDeckCardToBoard.Create(seq, card, new Vector3(env.GetRealX(x), env.GetRealY(y), 0), card.gy * env.cols + card.gx, delay);
+ delay += MoveDeckCardToBoard.NextDelay();
+ }
+ else
+ {
+ OpenDeckCard.Create(seq, card, delay);
+ MoveDeckCardToBoard.Create(seq, card, new Vector3(env.GetRealX(x), env.GetRealY(y), 0), card.gy * env.cols + card.gx, delay + OpenDeckCard.NextDelay());
+ delay += MoveDeckCardToBoard.NextDelay();
+ }
+
+ hasNextCard = model.hasNextCard;
+
+ //
+ if(!hasNextCard)break;
+ }
+ }
+
+ if(hasNextCard)
+ {
+ card = model.GetCardFromDeck(-1, -1, true);
+
+ if(!card.isFront)OpenDeckCard.Create(seq, card, ref delay);
+ }
+
+ if(seq.Duration() > 0.0f)
+ {
+ await seq.Play().AwaitForComplete(cancellationToken: ct);
+ }
+
+ //
+ model.UpdateOrders();
+ }
+ async UniTask _DoMoveTrashToDeck()
+ {
+ model.MovePoolToDeck();
+
+ //
+ float delay = 0.0f;
+
+ Sequence seq = DOTween.Sequence();
+
+ for(int i = 0; i < model.decks.Count; i += 1)
+ {
+ MoveTrashCardToDeck.Create(model.decks[i], new Vector3(env.deckX + i * env.deckSpan, env.deckY, 0), ref delay);
+ }
+ await seq.Play().AwaitForComplete(cancellationToken: ct);
+
+ //
+ await OpenDeckCard.Create(model.GetCardFromDeck(-1, -1, true)).Play().AwaitForComplete(cancellationToken: ct);
+ }
+
+
+//====================
+// 퍼즐 게임의 일부입니다.
+//====================
+ HashSet checkedPairs = new HashSet();
+ HashSet cachedRemovableUnits = new HashSet();
+
+ VUnit[] targetUnits;
+
+ bool waitRemoveConnectUnits = false;
+
+ void UpdateJoints()
+ {
+ bool hasRemovableUnits = (!removableUnits.IsNullOrEmpty());
+ if(hasRemovableUnits)removableUnitJoints = new List();
+
+ cachedRemovableUnits.Clear();
+ for(int i = 0; i < removableUnits.Count; i += 1)cachedRemovableUnits.Add(removableUnits[i]);
+
+ checkedPairs.Clear();
+ int pairId = 0;
+
+ VUnit unit1 = null;
+ VUnit unit2 = null;
+ UnitBallJoint joint = null;
+
+ Vector3 pos1 = Constants.V0;
+ Vector3 pos2 = Constants.V0;
+
+ bool forceRemoveJoint = false;
+ float dist = 0.0f;
+ int distCache = 0;
+ float distPxl = 0.0f;
+ float distSpanPxl = 0.0f;
+ int jointType = 0;
+
+ targetUnits = units.Where((u) => (u.type == UnitType.Ball)).ToArray();
+
+ int total = targetUnits.Length;
+ int i, j;
+ for(i = 0; i < total; i += 1)
+ {
+ unit1 = targetUnits[i];
+
+ for(j = total - 1; j > -1; j -= 1)
+ {
+ if(i == j)continue;
+
+ //
+ unit2 = targetUnits[j];
+
+ pairId = (1 + Mathf.Max(unit1.hash, unit2.hash)) * 1000 + (1 + Mathf.Min(unit1.hash, unit2.hash));
+ if(checkedPairs.Contains(pairId))continue;
+ checkedPairs.Add(pairId);
+
+ if(!unit1.Equals(unit2))continue;
+
+ if(!VUnit.CanConnect(unit1, unit2))
+ {
+ RemoveJoint(pairId);
+ continue;
+ }
+
+ if(!unit1.isReady || !unit2.isReady)
+ {
+ RemoveJoint(pairId);
+ continue;
+ }
+
+ if(unit1.willRemove || unit2.willRemove)
+ {
+ RemoveJoint(pairId);
+ continue;
+ }
+
+ //
+ forceRemoveJoint = (hasRemovableUnits ? (cachedRemovableUnits.Contains(unit1) || cachedRemovableUnits.Contains(unit2)) : false);
+
+ pos1 = unit1.pos;
+ pos2 = unit2.pos;
+
+ dist = Vector3.Distance(pos1, pos2);
+ if(dist > env.unitDistanceMax || forceRemoveJoint)
+ {
+ RemoveJoint(pairId, forceRemoveJoint);
+ continue;
+ }
+
+ distCache = Mathf.FloorToInt(dist * 10000);
+ if(cachedJointDistances.ContainsKey(pairId) && cachedJointDistances[pairId] == distCache)continue;
+ cachedJointDistances[pairId] = distCache;
+
+ distPxl = dist * env.TO_PXL;
+ distSpanPxl = distPxl - env.unitPxlSize;
+
+ //
+ if(!cachedJoints.ContainsKey(pairId))cachedJoints[pairId] = SpawnJoint(env.canvas, unit1.shape);
+ joint = cachedJoints[pairId];
+
+ jointType = env.CalcJointType(distSpanPxl);
+ joint.UpdateLinks(unit1.shape, jointType, pos1, pos2, (distPxl - env.unitSpansEx[jointType]) * env.TO_UNT);
+ }
+ }
+
+ if(hasRemovableUnits)
+ {
+ waitRemoveConnectUnits = true;
+
+ units = units.Except(removableUnits).ToList();
+
+ units.ForEach((u) => {
+ u.isEnabled = false;
+ u.ResetAllVelocity();
+ });
+
+ //
+ removableUnits.ForEach((u) => u.isEnabled = false);
+
+ RemoveConnected(
+ new List(removableUnits.ToArray()),
+ new List(removableUnitJoints.ToArray())
+ );
+
+ removableUnits.Clear();
+ removableConnectedUnits.Clear();
+ removableUnitJoints.Clear();
+ }
+ else
+ {
+ if(waitRemoveConnectUnits)
+ {
+ units.ForEach((u) => u.isEnabled = true);
+
+ waitRemoveConnectUnits = false;
+ }
+ }
+ }
+ void UpdateRemovableUnitJoints()
+ {
+ if(removableConnectedUnits.IsNullOrEmpty())return;
+
+ //
+ checkedPairs.Clear();
+ int pairId = 0;
+
+ VUnit unit1 = null;
+ VUnit unit2 = null;
+ UnitBallJoint joint = null;
+
+ Vector3 pos1 = Constants.V0;
+ Vector3 pos2 = Constants.V0;
+
+ float dist = 0.0f;
+ int distCache = 0;
+ float distPxl = 0.0f;
+ float distSpanPxl = 0.0f;
+ int jointType = 0;
+
+ int total = removableConnectedUnits.Count;
+ int i, j;
+ for(i = 0; i < total; i += 1)
+ {
+ unit1 = removableConnectedUnits[i];
+
+ for(j = total - 1; j > -1; j -= 1)
+ {
+ if(i == j)continue;
+
+ //
+ unit2 = removableConnectedUnits[j];
+
+ pairId = (1 + Mathf.Max(unit1.hash, unit2.hash)) * 1000 + (1 + Mathf.Min(unit1.hash, unit2.hash));
+ if(checkedPairs.Contains(pairId))continue;
+ checkedPairs.Add(pairId);
+
+ //
+ pos1 = unit1.pos;
+ pos2 = unit2.pos;
+
+ dist = Vector3.Distance(pos1, pos2);
+ if(dist > env.unitDistanceMax)
+ {
+ RemoveJoint(pairId);
+ continue;
+ }
+
+ distCache = Mathf.FloorToInt(dist * 10000);
+ if(cachedJointDistances.ContainsKey(pairId) && cachedJointDistances[pairId] == distCache)continue;
+ cachedJointDistances[pairId] = distCache;
+
+ distPxl = dist * env.TO_PXL;
+ distSpanPxl = distPxl - env.unitPxlSize;
+
+ //
+ if(!cachedJoints.ContainsKey(pairId))cachedJoints[pairId] = SpawnJoint(env.canvas, unit1.shape);
+ joint = cachedJoints[pairId];
+
+ jointType = env.CalcJointType(distSpanPxl);
+ joint.UpdateLinks(unit1.shape, jointType, pos1, pos2, (distPxl - env.unitSpansEx[jointType]) * env.TO_UNT);
+ }
+ }
+ }
+
+ void RemoveJoint(int pairId, bool forceRemoveJoint=false)
+ {
+ if(cachedJointDistances.ContainsKey(pairId))cachedJointDistances.Remove(pairId);
+ if(cachedJoints.ContainsKey(pairId))
+ {
+ if(forceRemoveJoint)
+ {
+ removableUnitJoints.Add(cachedJoints[pairId]);
+ }
+ else
+ {
+ cachedJoints[pairId].Release();
+ }
+
+ cachedJoints.Remove(pairId);
+ }
+ }
+ void RemoveConnected(List units, List joints)
+ {
+ // TODO: 이 흐름을 더 합리적으로
+ List sideEffectedUnits = new List();
+
+ units.ForEach((u) => {
+ if(u.type != UnitType.Stone && u.type != UnitType.Minion)
+ {
+ sideEffectedUnits.AddRange(FindNeighborUnits(units, u, env.explodeDistanceMax));
+ }
+
+ KillUnit(u);
+
+ ReleaseUnit(u);
+ });
+ joints.ForEach((j) => j.Release(false));
+
+ //
+ sideEffectedUnits = sideEffectedUnits.Distinct().Where((u) => (u != null && !u.willRemove)).ToList();
+ if(sideEffectedUnits.IsNullOrEmpty())
+ {
+ units.ForEach((u) => u.isEnabled = true);
+
+ waitRemoveConnectUnits = false;
+ }
+ else
+ {
+ TaskUtil.DelayCall(0.15f, () => {
+ sideEffectedUnits.ForEach((u) => {
+ // if(u.type == UnitType.Ball && (u.behavior == UnitBehaviorType.Anchor || u.behavior == UnitBehaviorType.Freeze))
+ if(u.behavior == UnitBehaviorType.Anchor || u.behavior == UnitBehaviorType.Freeze)
+ {
+ ActKillUnit(u);
+ }
+ else if(u.type == UnitType.Bomb)
+ {
+ ActBomb(u);
+ }
+ else if(u.type == UnitType.Stone)
+ {
+ ActKillUnit(u);
+ }
+ });
+ });
+ }
+ }
+
+
+//====================
+// BusJam 류의 퍼즐 게임의 일부입니다.
+//====================
+ async UniTaskVoid CheckReady(Action onChecked=null)
+ {
+ waitGameReady = true;
+
+ //
+ ctsCheckReady = new CancellationTokenSource();
+ var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(ctsCheckReady.Token, gameObject.GetCancellationTokenOnDestroy());
+
+ // unit이 움직이고
+ if(refUnits.Any((g) => g.isBusy))
+ {
+ await UniTask.WaitUntil(() => !refUnits.Any((g) => g.isBusy), cancellationToken: linkedTokenSource.Token);
+
+ UpdateMovable();
+ }
+
+ //
+ while(true)
+ {
+ // box가 클리어면 나가고 다음것 들어오고
+ if(boxes[0].isComplete)
+ {
+ MoveBox();
+
+ if(boxes.Any((g) => g.isBusy))
+ {
+ await UniTask.WaitUntil(() => !boxes.Any((g) => g.isBusy), cancellationToken: linkedTokenSource.Token);
+
+ UpdateMovable();
+ }
+
+ boxes.RemoveAt(0);
+
+ // 새로운 box에 맞는 unit이 슬롯에 있으면 이동시키고
+ if(!boxes.IsNullOrEmpty())
+ {
+ CheckSlotUnitMove();
+
+ if(refUnits.Any((g) => g.isBusy))
+ {
+ await UniTask.WaitUntil(() => !refUnits.Any((g) => g.isBusy), cancellationToken: linkedTokenSource.Token);
+
+ UpdateMovable();
+ }
+ }
+
+ // 다음 box가 없으면 클리어
+ if(boxes.IsNullOrEmpty())
+ {
+ ResetCancellationTokenSource();
+
+ WillGameClear();
+ return;
+ }
+ }
+
+ if(!boxes[0].isComplete)break;
+ }
+
+ // slot이 꽉찼으면 게임오버
+ if(env.slot.slotRemain == 0)
+ {
+ ResetCancellationTokenSource();
+
+ WillGameOver();
+ return;
+ }
+
+ // unit 상태 변경
+ UpdateSecretState();
+ UpdateChainState();
+ UpdateFreezeState();
+ UpdateBombState();
+
+ await UniTask.WaitUntil(() => !refUnits.Any((g) => g.isBusy), cancellationToken: linkedTokenSource.Token);
+
+ // bomb터진 unit 있으면 게임오버
+ if(refUnits.Any((u) => u.isDead))
+ {
+ ResetCancellationTokenSource();
+
+ WillGameOver();
+ return;
+ }
+
+ // lock 상태 변경
+ if(lockGroups.Count > 0)
+ {
+ UpdateLockGroupState();
+
+ if(refUnits.Any((g) => g.isBusy))
+ {
+ await UniTask.WaitUntil(() => !refUnits.Any((g) => g.isBusy), cancellationToken: linkedTokenSource.Token);
+ }
+ if(refSpawners.Any((g) => g.isBusy))
+ {
+ await UniTask.WaitUntil(() => !refSpawners.Any((g) => g.isBusy), cancellationToken: linkedTokenSource.Token);
+ }
+ }
+
+ // spawner 동작
+ if(refSpawners.Count > 0 && refSpawners.Any((g) => g.canSpawn))
+ {
+ SpawnUnit();
+
+ // unit 상태 변경: 이건 생성된 유닛의 이동이 secret해제 애니랑 관계없다는 가정
+ UpdateSecretState();
+
+ if(refUnits.Any((g) => g.isBusy))
+ {
+ await UniTask.WaitUntil(() => !refUnits.Any((g) => g.isBusy), cancellationToken: linkedTokenSource.Token);
+
+ UpdateMovable();
+ }
+ if(refSpawners.Any((g) => g.isBusy))
+ {
+ await UniTask.WaitUntil(() => !refSpawners.Any((g) => g.isBusy), cancellationToken: linkedTokenSource.Token);
+ }
+ }
+
+ //
+ waitGameReady = false;
+
+ //
+ // NOTE: 최초 레벨 생성할때와 continue에서만 사용
+ if(onChecked != null)onChecked();
+ }
+
+ +
광고 서비스 코드의 일부입니다. 프로젝트에 따라 달라지는 광고사 SDK와 상관없이 사용할 수 있게 만들어져있습니다.
+
+| |-- ScreenShotOptionExtension.cs
+| `-- Widgets/
+| `-- WidgetScreenShot.cs
+|-- U5.Service.Ads/
+| `-- Scripts/
+| |-- AdBannerPositionType.cs
+| |-- AdControl.cs
+| |-- AdServiceImpl.cs
+| `-- Impl/
+| |-- AdServiceEditor.cs
+| |-- Admob/
+| | |-- AdServiceAdmob.cs
+| | |-- AdServiceAdmob_Banner.cs
+| | |-- AdServiceAdmob_Interstitial.cs
+| | |-- AdServiceAdmob_RewardVideo.cs
+| | `-- FBAdSettings.cs
+| |-- AppLovin/
+| | |-- AdServiceAppLovin.cs
+| | |-- AdServiceAppLovin_Banner.cs
+| | |-- AdServiceAppLovin_Interstitial.cs
+| | `-- AdServiceAppLovin_RewardVideo.cs
+| |-- CrazyGames/
+| | |-- AdServiceCrazy.cs
+| | |-- AdServiceCrazy_Banner.cs
+| | |-- AdServiceCrazy_Interstitial.cs
+| | `-- AdServiceCrazy_RewardVideo.cs
+| |-- GameDistribution/
+| | |-- AdServiceGameDistribution.cs
+| | |-- AdServiceGameDistribution_Banner.cs
+| | |-- AdServiceGameDistribution_Interstitial.cs
+| | `-- AdServiceGameDistribution_RewardVideo.cs
+| |-- IronSource/
+| | |-- AdServiceIronSource.cs
+| | |-- AdServiceIronSource_Banner.cs
+| | |-- AdServiceIronSource_Interstitial.cs
+| | `-- AdServiceIronSource_RewardVideo.cs
+| `-- Mintegral/
+| |-- AdServiceMintegral.cs
+| |-- AdServiceMintegral_Banner.cs
+| |-- AdServiceMintegral_Interstitial.cs
+| `-- AdServiceMintegral_RewardVideo.cs
+|-- U5.Service.Analytics/
+| `-- Scripts/
+| |-- AnalyticService.cs
+
+
+using UnityEngine;
+#if UNITY_WEBGL && !UNITY_EDITOR
+using D = Unit5.WebPlayerDebug;
+#else
+using D = Unit5.DebugTool;
+#endif
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Text;
+
+using Unit5.Relays;
+
+//----------
+namespace Unit5
+{
+
+//----------
+sealed public partial class AdControl : SingletonMB
+{
+
+ //----------
+ public enum AdState
+ {
+ None,
+
+ AdLoaded,
+ AdFailedToLoad,
+ AdOpening,
+ AdFailedToShow,
+ UserEarnedReward,
+ AdClosed,
+
+ AdWaitReward,
+ }
+
+ //----------
+ AdServiceImpl _impl = null;
+
+ //----------
+ public AdServiceImpl impl => _impl;
+
+ public bool available => (_impl != null && _impl.available);
+ public bool isInited => (_impl != null && _impl.isInited);
+
+ //----------
+ public IRelayLink onInitialized => _impl?.onInitialized;
+ public IRelayLink onOpenedAd => _impl?.onOpenedAd;
+
+ public int reqCountBanner => (_impl != null ? _impl.reqCountBanner : 0);
+ public int reqCountInterstitial => (_impl != null ? _impl.reqCountInterstitial : 0);
+ public int reqCountRewardVideo => (_impl != null ? _impl.reqCountRewardVideo : 0);
+
+ //----------
+ public bool GetGDPRConsent()
+ {
+ if(_impl == null)return false;
+ return _impl.GetGDPRConsent();
+ }
+ public void SetGDPRConsent(bool confirm)
+ {
+ if(_impl == null)return;
+ _impl.SetGDPRConsent(confirm);
+ }
+
+ //----------
+ public void Init(AdServiceImpl impl, Dictionary options=null, bool disabledDefaultAd=false, bool checkGDPRConsent=false, bool autoFetch=true, Action onInitialized=null)
+ {
+ _impl = impl;
+ _impl.Init(options, disabledDefaultAd, checkGDPRConsent, autoFetch, onInitialized);
+ }
+ public void Init(Dictionary options=null, bool disabledDefaultAd=false, bool checkGDPRConsent=false, bool autoFetch=true, Action onInitialized=null)
+ {
+ if(_impl == null)
+ {
+#if ADS_NONE
+ _impl = new AdServiceImpl();
+#elif ADS_ADMOB
+ _impl = new AdServiceAdmob();
+#elif UNITY_EDITOR || ADS_FAKE
+ _impl = new AdServiceEditor();
+#elif ADS_APPLOVIN
+ _impl = new AdServiceAppLovin();
+#elif ADS_IRONSOURCE
+ _impl = new AdServiceIronSource();
+#elif ADS_MINTEGRAL
+ _impl = new AdServiceMintegral();
+#elif ADS_CRAZYGAMES
+ _impl = new AdServiceCrazyGames();
+#elif ADS_GAMEDISTRIBUTION
+ _impl = new AdServiceGameDistribution();
+#else
+ _impl = new AdServiceImpl();
+#endif
+ }
+
+ _impl.Init(options, disabledDefaultAd, checkGDPRConsent, autoFetch, onInitialized);
+ }
+
+ //----------
+ public bool showBanner => (_impl != null && _impl.showBanner);
+ public bool isVisibleBanner => (_impl != null && _impl.isVisibleBanner);
+ public int bannerHeight => (_impl != null ? _impl.bannerHeight : -1);
+
+ public void SetFirstBannerDelay(int duration)
+ {
+ if(_impl == null)return;
+ _impl.SetFirstBannerDelay(duration);
+ }
+ public void SetBannerPosition(AdBannerPositionType position)
+ {
+ if(_impl == null)return;
+ _impl.SetBannerPosition(position);
+ }
+
+ public void ShowBanner(bool forceReload=false)
+ {
+ if(_impl == null)return;
+ _impl.ShowBanner(forceReload);
+ }
+ public void HideBanner(bool forceDestroy=false)
+ {
+ if(_impl == null)return;
+ _impl.HideBanner(forceDestroy);
+ }
+
+ //----------
+ public bool isInterstitialTimeout => (_impl != null ? _impl.isInterstitialTimeout : false);
+
+ public bool CanShowInterstitial()
+ {
+ if(_impl == null)return false;
+ return _impl.CanShowInterstitial();
+ }
+
+ public void ShowInterstitial(Action onComplete=null, int timeout=10)
+ {
+ if(_impl == null)
+ {
+ onComplete(false);
+ return;
+ }
+
+ _impl.ShowInterstitial(onComplete, timeout);
+ }
+ public void CancelInterstitial()
+ {
+ if(_impl == null)return;
+ _impl.CancelInterstitial();
+ }
+ public void FetchInterstitial()
+ {
+ if(_impl == null)return;
+ _impl.FetchInterstitial();
+ }
+
+ //----------
+ public bool isRewardVideoTimeout => (_impl != null ? _impl.isRewardVideoTimeout : false);
+
+ public bool CanShowRewardVideo()
+ {
+ if(_impl == null)return false;
+ return _impl.CanShowRewardVideo();
+ }
+
+ public void ShowRewardVideo(Action onComplete=null, int timeout=10)
+ {
+ if(_impl == null)
+ {
+ onComplete(false);
+ return;
+ }
+
+ _impl.ShowRewardVideo(onComplete, timeout);
+ }
+ public void CancelRewardVideo()
+ {
+ if(_impl == null)return;
+ _impl.CancelRewardVideo();
+ }
+ public void FetchRewardVideo()
+ {
+ if(_impl == null)return;
+ _impl.FetchRewardVideo();
+ }
+
+ //----------
+ public void FetchAll()
+ {
+ if(_impl == null)return;
+ _impl.FetchAll();
+ }
+
+ //----------
+ public void SetNoAdsPurchased(bool purchased)
+ {
+ if(_impl == null)return;
+ _impl.SetNoAdsPurchased(purchased);
+ }
+
+ //----------
+ public void OpenTestSuite()
+ {
+ if(_impl == null)return;
+ _impl.OpenTestSuite();
+ }
+}
+
+}
+
+ 크로스 프로모션 서비스 코드의 일부입니다. 배너/동영상 위젯 등의 컴포넌트와 유틸리티들과 서버 측 서비스 코드로 구성되어 있습니다.
+
+| | |-- GoogleImpl.cs
+| | `-- UnityImpl.cs
+| `-- MultiAnalyticsImpl.cs
+|-- U5.Service.CrossPromotion/
+| |-- Assets/
+| | |-- LAni_cp_LEFT.anim
+| | |-- LAni_cp_RIGHT.anim
+| | |-- Merriweather-Regular-UNIT5CP.ttf
+| | |-- RobotoCondensed-Regular-slim.ttf
+| | |-- UNIT5_CP_frame_512.png
+| | `-- UNIT5_CP_frame_512.psd
+| `-- Scripts/
+| |-- PromotionControl.cs
+| |-- PromotionInfo.cs
+| |-- PromotionReward.cs
+| `-- Widgets/
+| |-- WidgetCrossBanner.cs
+| |-- WidgetCrossInterstitial.cs
+| `-- WidgetCrossVideo.cs
+|-- U5.Service.IAP/
+| |-- Editor/
+| | `-- StoreSettingsInspector.cs
+
+
+using UnityEngine;
+#if UNITY_WEBGL && !UNITY_EDITOR
+using D = Unit5.WebPlayerDebug;
+#else
+using D = Unit5.DebugTool;
+#endif
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+
+using Unit5;
+using Unit5.MiniJSON;
+
+//----------
+namespace Unit5.Service
+{
+
+//----------
+sealed public class PromotionControl
+{
+
+ //----------
+ const string PROMOTION_URL = "_PROMOTION_URL";
+ const string PROMOTION_CACHE = "_PROMOTION_CACHE";
+
+ //----------
+ static public bool isReady{get; private set;}
+ static public bool hasData{get; private set;}
+
+ //----------
+ static public List infos{get; private set;}
+
+ //----------
+ static public PromotionInfo Get(bool notInstalled=false)
+ {
+ if(!hasData)return null;
+
+ //
+ List samples = (notInstalled ? GetNotInstalled() : infos);
+ if(samples.IsNullOrEmpty())samples = infos;
+ if(samples.IsNullOrEmpty())return null;
+
+ if(samples.Count == 1)return samples[0];
+
+ //
+ if(notInstalled)return GetNotInstalled().Choice();
+
+ int[] weights = samples.Select((d) => d.weight).ToArray();
+ int weightTotal = weights.Sum();
+
+ int dataIndex = MathUtil.RandomChoice(weights, weightTotal);
+ dataIndex = Mathf.Max(0, Mathf.Min(infos.Count - 1, dataIndex));
+ return infos[dataIndex];
+ }
+ static public PromotionInfo Get(string appId)
+ {
+ if(!hasData)return null;
+
+ return infos.Find(i => i.appId == appId);
+ }
+
+ static public List GetNotInstalled()
+ {
+ if(!hasData)return null;
+
+ return infos.Where(i => !IsInstalled(i)).ToList();
+ }
+
+ static public List GetAll(PromotionReward type)
+ {
+ if(!hasData)return null;
+
+ return infos.Where(i => i.reward == type).ToList();
+ }
+ static public List GetAll(bool hasExtra)
+ {
+ if(!hasData)return null;
+
+ return infos.Where(i => !string.IsNullOrEmpty(i.extra)).ToList();
+ }
+ static public List GetAll(PromotionReward type, bool hasExtra)
+ {
+ if(!hasData)return null;
+
+ return infos.Where(i => i.reward == type && !string.IsNullOrEmpty(i.extra)).ToList();
+ }
+
+ //----------
+ static public void CheckData(string url, Action callback)
+ {
+ if(string.IsNullOrEmpty(url))
+ {
+ callback(false);
+ return;
+ }
+
+ if(hasData)
+ {
+ callback(true);
+ return;
+ }
+
+ //
+ string oldCacheFilepath = EncryptedPlayerPrefs.GetString(PROMOTION_CACHE, string.Empty);
+ string newCacheFilepath = RequestUtil.GetCacheFilepath(url);
+
+ FetchData(url, newCacheFilepath, (bool success) => {
+ if(success)
+ {
+ if(!string.IsNullOrEmpty(oldCacheFilepath))
+ {
+ StreamingAssetUtil.RemoveFile(oldCacheFilepath);
+ }
+
+ EncryptedPlayerPrefs.SetString(PROMOTION_URL, url);
+ EncryptedPlayerPrefs.SetString(PROMOTION_CACHE, newCacheFilepath);
+ }
+
+ callback((success && hasData));
+ });
+ }
+
+ //----------
+ static public void FetchData(string url, Action callback)
+ {
+ FetchData(url, null, callback);
+ }
+ static public void FetchData(string url, string cacheFilepath, Action callback)
+ {
+ RequestUtil.Text(url, cacheFilepath, (string data) => {
+#if !NO_DEBUG
+ D.Log($"[UNIT5::PromotionControl] FetchData:: url={url} / cache={cacheFilepath} / data={data}");
+#endif
+
+#if UNITY_EDITOR
+ if(string.IsNullOrEmpty(data))
+ {
+ MakeTestData();
+
+ callback(true);
+ return;
+ }
+#endif
+
+ if(string.IsNullOrEmpty(data))
+ {
+ isReady = false;
+ hasData = false;
+
+ callback(false);
+ }
+ else
+ {
+ isReady = true;
+
+ try
+ {
+ Dictionary json = Json.Deserialize(data) as Dictionary;
+
+ string appId = UnityUtil.GetApplicationID();
+
+ infos = json.GetDictionaryList("p")
+ .Select((d) => new PromotionInfo(d))
+ .Where((i) => i.appBundleId != appId)
+ .ToList()
+ ;
+
+ hasData = (infos.Count > 0);
+ }
+ catch
+ {
+ hasData = false;
+ }
+
+ callback(hasData);
+ }
+ });
+ }
+
+#if UNITY_EDITOR
+ static void MakeTestData()
+ {
+ isReady = true;
+ hasData = true;
+
+ infos = new List(){
+ new PromotionInfo(){
+ appBundleId = "com.ftt.cubie.aos",
+ appId = "com.ftt.cubie.aos",
+ appScheme = "cubieadventure",
+
+ weight = 1,
+
+ appName = "Cubie Adventure",
+ appDesc = "Adventure with your Cubie and Cupet friends!\nFrom cute looks and immersive gameplay! Cubie Adventure welcomes you!",
+ appLink = "http://onelink.to/mjnhpp",
+
+ imgThumb = "https://share.unit5soft.com/_crosspromotion/ca_thumb.png",
+ imgBanner = "https://share.unit5soft.com/_crosspromotion/ca_banner.jpg",
+ imgPopup = "https://share.unit5soft.com/_crosspromotion/ca_full.jpg",
+
+ vidClip = "https://share.unit5soft.com/_crosspromotion/ca_clip.mp4",
+
+ reward = PromotionReward.None,
+
+ extra = "",
+ },
+ };
+ }
+#endif
+
+ //----------
+ static public bool IsInstalled(PromotionInfo info)
+ {
+#if !UNITY_EDITOR && (UNITY_IOS || UNITY_TVOS || UNITY_IPHONE)
+ return UnityUtil.IsInstalled(info.appScheme);
+#else
+ return UnityUtil.IsInstalled(info.appId);
+#endif
+ }
+
+ //----------
+ static public bool IsMarked(string appId)
+ {
+ return (EncryptedPlayerPrefs.GetInt("_XP_" + appId, 0) == 1);
+ }
+ static public void MarkApp(string appId)
+ {
+ EncryptedPlayerPrefs.SetInt("_XP_" + appId, 1);
+ }
+}
+
+}
+
+ 값이나 상태의 변경 시 발생하는 화면 처리를 단순히 하기 위한 모듈의 일부입니다. 구매버튼, 다국어 이미지 등 프로젝트와 관계없이 일반화가 가능한 많은 부분을 모듈화하여 사용했습니다.
+
+| |-- BaseN.cs
+| |-- StringGenerator.cs
+| `-- ZBase32.cs
+|-- U5.Data.Binding/
+| |-- Editor/
+| | |-- BindingInfoInspector.cs
+| | `-- Components/
+| | |-- BindActionInspector.cs
+| | |-- BindGameObjectToggleInspector.cs
+| | |-- BindSpriteInspector.cs
+| | |-- BindTextMeshInspector.cs
+| | |-- BindTextureInspector.cs
+| | |-- BindUIButtonInspector.cs
+| | |-- BindUILabelInspector.cs
+| | |-- BindUIProgressBarInspector.cs
+| | |-- BindUISpanInspector.cs
+| | |-- BindUISpriteInspector.cs
+| | |-- BindUITextureInspector.cs
+| | |-- BindUIToggleInspector.cs
+| | `-- DataSetterInspector.cs
+| `-- Scripts/
+| |-- BindingFieldType.cs
+| |-- BindingInfo.cs
+| |-- BindingInfos.cs
+| |-- Components/
+| | |-- BindAction.cs
+| | |-- BindGameObjectToggle.cs
+| | |-- BindSprite.cs
+| | |-- BindTextMesh.cs
+| | |-- BindTexture.cs
+| | |-- BindUIButton.cs
+| | |-- BindUILabel.cs
+| | |-- BindUIProgressBar.cs
+| | |-- BindUISpan.cs
+| | |-- BindUISprite.cs
+| | |-- BindUITexture.cs
+| | `-- BindUIToggle.cs
+| |-- DataContext.cs
+| |-- DataSetter.cs
+| |-- IDataContext.cs
+| |-- IDataProvider.cs
+| `-- PropertyData.cs
+|-- U5.Debug/
+| `-- Scripts/
+| |-- DebugTool.cs
+
+
+using UnityEngine;
+#if UNITY_WEBGL && !UNITY_EDITOR
+using D = Unit5.WebPlayerDebug;
+#else
+using D = Unit5.DebugTool;
+#endif
+using TMPro;
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+
+//----------
+namespace Unit5
+{
+
+//----------
+public class BindTextMesh : DataSetter
+{
+
+ //----------
+ [Space(10)]
+ [SerializeField] TextMeshPro lblText = null;
+ [SerializeField] UILocalizedLabel lblLocalize = null;
+ [SerializeField] TweenLabelCounter tweenLabel = null;
+
+ [Space(10)]
+ [SerializeField] string format;
+
+ [Space(10)]
+ [SerializeField] float duration = 1.0f;
+ [SerializeField] float amountPerDuration = 0.0f;
+ [SerializeField] float durationMin = 0.3f;
+ [SerializeField] float durationMax = 2.1f;
+
+ //----------
+ string text;
+
+ //----------
+ override protected void OnValueChanged(string newValue)
+ {
+ UpdateText(newValue);
+ }
+
+ //----------
+ void OnLocalize()
+ {
+ UpdateText(text);
+ }
+
+ //----------
+ void UpdateText(string text)
+ {
+ if(lblLocalize != null)
+ {
+ UpdateLocalize(text);
+ }
+ else if(tweenLabel == null)
+ {
+ UpdateDirect(text);
+ }
+ else
+ {
+ UpdateTween(text);
+ }
+
+ this.text = text;
+ }
+
+ //----------
+ void UpdateLocalize(string text)
+ {
+ lblLocalize.text = text;
+ }
+ void UpdateDirect(string text)
+ {
+ if(string.IsNullOrEmpty(format))
+ {
+ lblText.text = text;
+ }
+ else
+ {
+ if(format.StartsWith("@"))
+ {
+ lblText.text = Localization2.Get(format, text);
+ }
+ else
+ {
+ lblText.text = string.Format(format, text);
+ }
+ }
+ }
+ void UpdateTween(string text)
+ {
+ int v = (string.IsNullOrEmpty(text) ? 0 : int.Parse(text));
+
+ if(isReady)
+ {
+ float d = (amountPerDuration > 0.0f ? Mathf.Abs(v - tweenLabel.currentCount) / amountPerDuration : 1.0f) * duration;
+ if(durationMin > 0.0f)d = Mathf.Max(durationMin, d);
+ if(durationMax > 0.0f)d = Mathf.Min(durationMax, d);
+
+ tweenLabel.Play(d, tweenLabel.currentCount, v);
+ }
+ else
+ {
+ tweenLabel.from = tweenLabel.to = v;
+ tweenLabel.Sample(1.0f, true);
+
+ isReady = true;
+ }
+ }
+}
+
+}
+
+ 화면의 전체나 일부를 캡하는 위젯입니다. 협업 개발의 편의성을 위해 많은 부분을 위젯 형태로 만들어 인스펙터에서 조작하는 것만으로 처리가 가능하게 했습니다.
+
+| | |-- QueryBuilder.cs
+| | `-- SQLiteORM.cs
+| `-- SQLiteSimple.cs
+|-- U5.ScreenShot/
+| |-- Editor/
+| | `-- WidgetScreenShotInspector.cs
+| `-- Scripts/
+| |-- AnchorHorizontal.cs
+| |-- AnchorVertical.cs
+| |-- ScreenShotControl.cs
+| |-- ScreenShotOption.cs
+| |-- ScreenShotOptionExtension.cs
+| `-- Widgets/
+| `-- WidgetScreenShot.cs
+|-- U5.Service.Ads/
+| `-- Scripts/
+| |-- AdBannerPositionType.cs
+
+
+using UnityEngine;
+#if UNITY_WEBGL && !UNITY_EDITOR
+using D = Unit5.WebPlayerDebug;
+#else
+using D = Unit5.DebugTool;
+#endif
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+
+//----------
+namespace Unit5
+{
+
+//----------
+sealed public class WidgetScreenShot : MonoBehaviour
+{
+
+ //----------
+ public enum CropMode
+ {
+ Option = 0,
+ Area,
+ }
+
+ //----------
+ [Space(10)]
+ [SerializeField] Camera[] cameras = null;
+
+ [Space(10)]
+ [SerializeField] [DelayedAttribute] int size = 320;
+
+ [Space(10)]
+ [SerializeField] CropMode cropMode = CropMode.Option;
+ [SerializeField] ScreenShotOption cropOption = null;
+ [SerializeField] SpriteRenderer cropArea = null;
+
+ [Space(10)]
+ [SerializeField] Texture2D overlay = null;
+ [SerializeField] ScreenShotOption merge = null;
+
+ //----------
+ [NonSerialized] [HideInInspector] public Texture2D lastScreenShot;
+
+ //----------
+ public bool isEnabled => (!cameras.IsNullOrEmpty());
+
+ public ScreenShotOption crop => (cropMode == CropMode.Option ? cropOption : CalcArea());
+ ScreenShotOption CalcArea()
+ {
+ ScreenShotOption option = (cropOption == null ? new ScreenShotOption() : cropOption.Clone());
+ if(cropArea == null)return option;
+
+ float fh = Camera.main.orthographicSize * 2f;
+ float fw = fh / Camera.main.pixelHeight * Camera.main.pixelWidth;
+ float iw = cropArea.bounds.size.x;
+ float ih = cropArea.bounds.size.y;
+
+ option.anchorX = AnchorHorizontal.Left;
+ option.anchorY = AnchorVertical.Top;
+ option.width = iw / fw;
+ option.height = ih / fh;
+ option.offsetX = ((fw - iw) * 0.5f - (Camera.main.transform.position.x - cropArea.bounds.center.x)) / fw;
+ option.offsetY = ((fh - ih) * 0.5f - (Camera.main.transform.position.y - cropArea.bounds.center.y)) / fh;
+ return option;
+ }
+
+ //----------
+ void OnDestroy()
+ {
+ Clear();
+ }
+
+ //----------
+ public void Clear()
+ {
+ if(lastScreenShot != null)GameObject.Destroy(lastScreenShot);
+ lastScreenShot = null;
+ }
+
+ //----------
+ public Texture2D TakeScreenShot()
+ {
+ lastScreenShot = TakeCroppedScreenShot(Screen.width, Screen.height);
+
+ return lastScreenShot;
+ }
+
+ //----------
+ public Texture2D TakeCroppedScreenShot(int width, int height)
+ {
+ int w = size;
+ int h = Mathf.RoundToInt(size * (height / (float)width));
+
+ return TakeCroppedScreenShot(w, h, crop);
+ }
+ public Texture2D TakeCroppedScreenShot(int width, int height, ScreenShotOption option)
+ {
+ Texture2D image = CaptureFrame(width, height);
+ if(option.width == 1.0f && option.height == 1.0f && option.offsetX == 0.0f && option.offsetY == 0.0f)
+ {
+ if(overlay != null)
+ {
+ MergeOverlay(ref image);
+ }
+
+ return image;
+ }
+
+ Texture2D result = CropImage(ref image, option);
+
+#if UNITY_EDITOR
+ DestroyImmediate(image);
+#else
+ Destroy(image);
+#endif
+
+ if(overlay != null)
+ {
+ MergeOverlay(ref result);
+ }
+
+ return result;
+ }
+
+ //----------
+ public Texture2D CaptureFrame(int width, int height)
+ {
+ RenderTexture rt = new RenderTexture(width, height, 16, RenderTextureFormat.ARGB32);
+ foreach(Camera cam in cameras)
+ {
+ RenderTexture tt = cam.targetTexture;
+ cam.targetTexture = rt;
+ cam.Render();
+ cam.targetTexture = tt;
+ }
+ RenderTexture.active = rt;
+
+ Texture2D image = new Texture2D(width, height, TextureFormat.RGB24, false);
+ image.ReadPixels(new Rect(0, 0, width, height), 0, 0, false);
+ image.Apply();
+
+ RenderTexture.active = null;
+#if UNITY_EDITOR
+ DestroyImmediate(rt);
+#else
+ Destroy(rt);
+#endif
+
+ return image;
+ }
+
+ public Texture2D CropImage(ref Texture2D source, ScreenShotOption option)
+ {
+ Rect rect = option.Crop(source.width, source.height);
+
+ Texture2D result = new Texture2D((int)rect.width, (int)rect.height, TextureFormat.RGBA32, false);
+ result.SetPixels(source.GetPixels((int)rect.x, (int)rect.y, (int)rect.width, (int)rect.height));
+ result.Apply();
+
+ return result;
+ }
+
+ public Texture2D ResizeImage(ref Texture2D source, int width, int height)
+ {
+ Texture2D result = new Texture2D(width, height, source.format, false);
+
+ float w = (float)width;
+ float h = (float)height;
+
+ for(int y = 0; y < result.height; y += 1)
+ {
+ for(int x = 0; x < result.width; x += 1)
+ {
+ result.SetPixel(x, y, source.GetPixelBilinear(x / w, y / h));
+ }
+ }
+ result.Apply();
+ return result;
+ }
+
+ public void MergeOverlay(ref Texture2D source)
+ {
+ MergeOverlay(ref source, merge);
+ }
+ public void MergeOverlay(ref Texture2D source, ScreenShotOption option)
+ {
+ int ow = source.width;
+ int oh = Mathf.FloorToInt(source.width * (overlay.height / (float)overlay.width));
+
+ Rect rect = option.Place(source.width, source.height, ow, oh);
+ ow = (int)rect.width;
+ oh = (int)rect.height;
+
+ Texture2D resizedOverlay = ResizeImage(ref overlay, ow, oh);
+
+ int sx = Mathf.Max(0, Mathf.Min(source.width - ow, (int)rect.x));
+ int sy = Mathf.Max(0, Mathf.Min(source.height - oh, (int)rect.y));
+ int sw = Mathf.Min(source.width, sx + ow);
+ int sh = Mathf.Min(source.height, sy + oh);
+
+ int px;
+ int py;
+ Color sourceColor;
+ Color overlayColor;
+ Color finalColor;
+ for(int x = sx; x < sw; x += 1)
+ {
+ for(int y = sy; y < sh; y += 1)
+ {
+ px = x - sx;
+ py = y - sy;
+
+ sourceColor = source.GetPixel(x, y);
+ overlayColor = resizedOverlay.GetPixel(px, py);
+ finalColor = Color.Lerp(sourceColor, overlayColor, overlayColor.a / 1.0f);
+ source.SetPixel(x, y, finalColor);
+ }
+ }
+
+ source.Apply();
+
+#if UNITY_EDITOR
+ DestroyImmediate(resizedOverlay);
+#else
+ Destroy(resizedOverlay);
+#endif
+ }
+}
+
+}
+
+
+//====================
+// 블럭 게임의 일부입니다
+//====================
+ void RestoreGameStatus(bool passAppResume=false)
+ {
+ string data = GameData.GetGameData();
+ GameData.ClearGameData();
+
+ // NOTE: 튜토리얼 이후 보드상태, 가운데에 4놓여있고 다음유닛은 4
+ // data = "E?|1|6|0|1|1|0;2:2:0|0;2:2:6||0:2|13|0";
+
+ // NOTE: 가운데 x인 환경
+ // data = "E?|1|8|17|3|6|0;6:0:0|0;7:2:6,0;6:2:5,0;4:2:4,0;5:2:3,0;2:2:2,0;6:2:1,0;5:0:6|||14|0";
+
+ // D.Log(data);
+ // DebugTool.Report(data, false);
+
+ if(!string.IsNullOrEmpty(data))
+ {
+ MakeSavedLevel(data, passAppResume);
+ }
+ else
+ {
+ if(Game.skipLevelId > 0)
+ {
+ int skippedGoal = Game.skipLevelId;
+
+ int lastClearGoal = GameData.GetLastClearedGoal(GameSettings.initialGoalA);
+ if(lastClearGoal < skippedGoal)
+ {
+ GameData.SetLastPlayedGoal(skippedGoal);
+ GameData.SetLastClearedGoal(skippedGoal);
+ }
+
+ int skippedLevel = 1;
+ for(int i = 0; i < skippedGoal + 10; i += 1)
+ {
+ if(skippedGoal != GetGoalLevel(i + 1))continue;
+
+ skippedLevel = i + 1;
+ break;
+ }
+
+ int nextLevel = skippedLevel + 1;
+
+ OpenLevelStart(nextLevel, () => {
+ MakeLevel(nextLevel, null, skippedGoal);
+ });
+ }
+ else
+ {
+ OpenLevelStart(1);
+ }
+ }
+
+ // D.Log(GameData.GetGameData());
+ }
+ void SaveGameStatus(bool isPlaying=true)
+ {
+ VUnit goalUnit = model.units.Find((u) => (u.type == UnitType.Block && u.level == goal));
+ List remainUnits = model.units.Where((u) => (u != goalUnit && u != model.currUnit)).ToList();
+
+ string serializedNextUnit = (model.currUnit == null ? "" : model.currUnit.GetSerializedData());
+ if(model.isItemMode && model.keepedUnit != null)serializedNextUnit = model.keepedUnit.GetSerializedData();
+ string serializedRemainUnits = remainUnits.Select((u) => u.GetSerializedData()).Join(",");
+ string serializedGoalUnit = (goalUnit == null ? "" : goalUnit.GetSerializedData());
+ string serializedQueueUnits = unitQueue.Select((u) => $"{((int)u.type)}:{u.level}").Join(",");
+
+ string serializedReviceCount = GameData.GetReviveCount().ToString();
+
+ GameData.SetGameData($"E?|{stage}|{goal}|{GameData.GetMergeCount()}|{GameData.GetUserLevel()}|{GameData.GetScore()}|{serializedNextUnit}|{serializedRemainUnits}|{serializedGoalUnit}|{serializedQueueUnits}|{GameData.guestId}|{serializedReviceCount}");
+ }
+}
+
+//====================
+ void MergeCoins(List mergeFrom, VUnit mergeTo)
+ {
+ mergeTo.SetDirty();
+
+ Vector3 targetPos = mergeTo.localPos;
+
+ //
+ int mergeCount = mergeFrom.Count;
+
+ int coinIncrAmount = 0;
+ for(int i = 0; i < mergeCount; i += 1)
+ {
+ coinIncrAmount += mergeFrom[i].level - 1 + 3;
+ }
+
+ if(mergeFrom.Count == 0)
+ {
+ mergeTo.PlayAnim(VUnit.ANIM_STATE_MERGE_OUT, () => {
+ mergeTo.IncrLevel(coinIncrAmount);
+
+ mergeTo.PlayAnim(VUnit.ANIM_STATE_MERGE_NEW);
+ });
+ mergeTo.SetDirty(false);
+ }
+ else
+ {
+ for(int i = 0; i < mergeCount; i += 1)
+ {
+ if(i < mergeCount - 1)
+ {
+ MergeTo(mergeFrom[i], targetPos);
+ }
+ else
+ {
+ MergeTo(mergeFrom[i], targetPos, () => {
+ mergeTo.PlayAnim(VUnit.ANIM_STATE_MERGE_OUT, () => {
+ mergeTo.IncrLevel(coinIncrAmount);
+
+ mergeTo.PlayAnim(VUnit.ANIM_STATE_MERGE_NEW);
+ });
+ mergeTo.SetDirty(false);
+ });
+ }
+ }
+ }
+
+ //
+ Effect.Spawn(VfxEffectType.Merge, targetPos);
+ Effect.Play(model.GetComboSFX());
+ Effect.Vibrate((model.GetCombo() > 2 ? VibrationStyle.HEAVY : VibrationStyle.MEDIUM));
+
+ //
+ model.ReorderUnits(mergeTo);
+ }
+
+//====================
+ async UniTask WaitEndOfMovingAsync()
+ {
+ while(true)
+ {
+ await UniTask.Yield();
+
+ int bCount = model.units.Count((u) => u.isMoving);
+
+ int kCount = model.removedUnits.Count((u) => u.isMoving);
+ if(kCount == 0 && !model.removedUnits.IsNullOrEmpty())
+ {
+ model.removedUnits.ForEach((u) => u.Release());
+ model.removedUnits = new List();
+ }
+
+ if(bCount + kCount == 0)break;
+ }
+
+ model.ReorderUnits();
+
+ CheckArrows();
+
+ SaveGameStatus();
+ }
+
+ async UniTaskVoid CheckMoveAsync(VUnit unit)
+ {
+ navigationController.ToggleBlockerPanel(true);
+
+ //
+ int currentMergeCount = GameData.GetMergeCount();
+
+ //
+ isWaitingDropAndHit = true;
+ currDroppedUnit = unit;
+
+ //
+ if(model.units.Any((u) => u.isMoving))
+ {
+ await WaitEndOfMovingAsync();
+ }
+
+ //
+ while(true)
+ {
+ int changed = 0;
+
+ //
+ if(DropUnit())
+ {
+ changed += 1;
+ await WaitEndOfMovingAsync();
+ }
+
+ //
+ if(CheckHit())
+ {
+ changed += 1;
+ await WaitEndOfMovingAsync();
+ }
+
+ //
+ if(changed == 0)break;
+ }
+
+ // NOTE: 딜레이 없고, 콤보는 한번 drop 에서만
+ model.UpdateCombo();
+
+ await WaitEndOfMovingAsync();
+
+ //
+ navigationController.ToggleBlockerPanel(false);
+
+ //
+ isWaitingDropAndHit = false;
+ currDroppedUnit = null;
+
+ //
+ model.CalcActionIds();
+
+ //
+ CheckArrows();
+
+ SaveGameStatus();
+
+ //
+ if(model.CheckDead() || model.isTimeOver)
+ {
+ WillGameOver();
+
+ if(model.isTimeOver)
+ {
+ OnTimeOver();
+ }
+ else
+ {
+ Effect.Spawn(VfxUIEffectType.BoardFull, () => {
+ OnGameOver();
+ });
+ }
+ }
+ else
+ {
+ // int x = (model.IsEmptyCol(model.centerCol, true) ? model.centerCol : -1);
+ // NOTE: 마지막 떨어진 곳 사용, 안되면 빈곳
+ int x = model.GetEmptyCol(model.lastCol);
+
+ VUnit spawnedUnit = (x == -1 ? null : SpawnUnit(x));
+ GameData.AddEarnedUnit(spawnedUnit.level);
+
+ model.currUnit = spawnedUnit;
+
+ //
+ env.ShowNextUnit(GetNextUnit(true));
+
+ FillNextUnits();
+
+ model.ReorderUnits(model.currUnit, true);
+
+ CheckArrows();
+
+ SaveGameStatus();
+ }
+ }
+
+
+//====================
+// 포커 게임의 일부입니다.
+//====================
+ void ReplaceCard(Card target)
+ {
+ DoReplaceCardAsync(target).Forget();
+ }
+ async UniTaskVoid DoReplaceCardAsync(Card target)
+ {
+ CancellationToken ct = gameObject.GetCancellationTokenOnDestroy();
+
+ //
+ target.order = 301;
+
+ int x = target.gx;
+ int y = target.gy;
+
+ float delay = 0.0f;
+
+ await MoveBoardCardToTrash.Create(target, Vector3.zero, ref delay).Play().AwaitForComplete(cancellationToken: ct);
+
+ model.RemoveCard(target);
+
+ //
+ Card card = null;
+ Sequence seq = DOTween.Sequence();
+
+ bool hasNextCard = model.hasNextCard;
+ if(hasNextCard)
+ {
+ card = model.GetCardFromDeck(x, y);
+
+ MoveDeckCardToBoard.Create(seq, card, new Vector3(env.GetRealX(x), env.GetRealY(y), 0), card.gy * env.cols + card.gx, ref delay);
+
+ hasNextCard = model.hasNextCard;
+ }
+ if(hasNextCard)
+ {
+ card = model.GetCardFromDeck(-1, -1, true);
+
+ if(!card.isFront)OpenDeckCard.Create(seq, card, ref delay);
+ }
+ if(seq.Duration() > 0.0f)
+ {
+ await seq.Play().AwaitForComplete(cancellationToken: ct);
+ }
+
+ //
+ model.UpdateOrders();
+
+ //
+ if(!hasNextCard)
+ {
+ WillGameOver();
+
+ OnOutOfCards();
+ }
+ }
+
+//====================
+ async UniTaskVoid DoNextAsync(List hands)
+ {
+ IncrQuest(hands.Count);
+
+ //
+ await DoMoveHandsToTrashAsync(hands);
+
+ await DoDropBoardCardsAsync();
+
+ await DoMoveDeckToBoardAsync();
+
+ //
+ if(!model.hasNextCard)
+ {
+ WillGameOver();
+ OnOutOfCards();
+ }
+ }
+ async UniTask DoMoveHandsToTrashAsync(List hands)
+ {
+ List cards;
+ HandType result = HandSolver.Solve(ref hands, out cards);
+
+ if(hands[0].data.Equals(CardData.JOKER))hands[0].SetData(cards[0]);
+ hands.Sort();
+
+ if(result != HandType.ROYAL_STRAIGHT_FLUSH_NO_WILD && result != HandType.ROYAL_STRAIGHT_FLUSH && result != HandType.STRAIGHT_FLUSH && result != HandType.STRAIGHT)hands.Reverse();
+
+ for(int i = 0; i < hands.Count; i += 1)hands[i].order = Mathf.RoundToInt(301 + i);
+
+ Dictionary> groups = new Dictionary>();
+ Dictionary ranks;
+ int[] rks;
+ switch(result)
+ {
+ case HandType.ROYAL_STRAIGHT_FLUSH_NO_WILD:
+ case HandType.FIVE_OF_A_KIND:
+ case HandType.ROYAL_STRAIGHT_FLUSH:
+ case HandType.STRAIGHT_FLUSH:
+ case HandType.FLUSH:
+ case HandType.STRAIGHT:
+ groups[1] = hands;
+ break;
+
+ case HandType.FOUR_OF_A_KIND:
+ case HandType.THREE_OF_A_KIND:
+ case HandType.ONE_PAIR:
+ ranks = new Dictionary();
+ hands.ForEach((c) => {
+ if(!ranks.ContainsKey(c.data.rank))ranks[c.data.rank] = 0;
+ ranks[c.data.rank] += 1;
+ });
+ rks = ranks.Keys.OrderBy((r) => -ranks[r]).ToArray();
+
+ groups[1] = hands.Where((c) => (c.data.rank == rks[0])).ToList();
+ break;
+
+ case HandType.FULL_HOUSE:
+ case HandType.TWO_PAIR:
+ ranks = new Dictionary();
+ hands.ForEach((c) => {
+ if(!ranks.ContainsKey(c.data.rank))ranks[c.data.rank] = 0;
+ ranks[c.data.rank] += 1;
+ });
+ rks = ranks.Keys.OrderBy((r) => -ranks[r]).ToArray();
+
+ groups[1] = hands.Where((c) => (c.data.rank == rks[0])).ToList();
+ groups[2] = hands.Where((c) => (c.data.rank == rks[1])).ToList();
+ break;
+ }
+
+ Sequence seq = DOTween.Sequence();
+
+ float margin = -env.cardMargin;
+ float cardWidth = -env.cardWidth;
+ float groupScale = -env.groupScale;
+ float sx = ((hands.Count - 1) * margin) * 0.5f;
+ float delay = 0.0f;
+
+ env.SetHandBackground(true);
+ env.SetHandGroup1(false);
+ env.SetHandGroup2(false);
+ if(groups.ContainsKey(1))
+ {
+ env.SetHandGroup1(
+ true,
+ groups[1].Select((c) => new Vector3(sx + hands.IndexOf(c) * cardWidth, 0.0f, 0.0f)).Avg(),
+ new Vector3(groups[1].Count * groupScale, groupScale, 0.0f)
+ );
+ }
+ if(groups.ContainsKey(2))
+ {
+ env.SetHandGroup2(
+ true,
+ groups[2].Select((c) => new Vector3(sx + hands.IndexOf(c) * cardWidth, 0.0f, 0.0f)).Avg(),
+ new Vector3(groups[2].Count * groupScale, groupScale, 0.0f)
+ );
+ }
+ ShowResult.Create(seq, env.handBackground);
+
+ for(int i = 0; i < hands.Count; i += 1)
+ {
+ MoveBoardCardToResult.Create(seq, hands[i], new Vector3(sx + i * 6.0f, 0.0f, 0.0f), ref delay);
+ }
+ await seq.Play().AwaitForComplete(cancellationToken: ct);
+
+ await UniTask.Delay(600, cancellationToken: ct);
+
+ hands.ForEach((c) => c.Release());
+
+ env.SetHandBackground(false);
+ }
+ async UniTask DoDropBoardCardsAsync()
+ {
+ Card card = null;
+
+ //
+ float delay = 0.0f;
+
+ Sequence seq = DOTween.Sequence();
+
+ for(int y = env.rows - 1; y > -1; y -= 1)
+ {
+ for(int x = 0; x < env.cols; x += 1)
+ {
+ card = model.GetCard(x, y);
+ if(card == null)continue;
+
+ int ny = -1;
+ for(int by = y + 1; by < env.rows; by += 1)
+ {
+ if(env.IsInside(x, by) && model.GetCard(x, by) != null)break;
+
+ ny = by;
+ }
+ if(ny == -1)continue;
+
+ if(card.gy == ny)continue;
+
+ model.MoveCard(card, x, ny);
+
+ DropBoardCard.Create(seq, card, env.GetRealY(card.gy), card.gy - y, ref delay);
+ }
+ }
+ await seq.Play().AwaitForComplete(cancellationToken: ct);
+
+ //
+ model.UpdateOrders();
+ }
+ async UniTask DoMoveDeckToBoardAsync()
+ {
+ Card card = null;
+
+ //
+ float delay = 0.0f;
+
+ Sequence seq = DOTween.Sequence();
+
+ bool hasNextCard = model.hasNextCard;
+ if(hasNextCard)
+ {
+ int loopLimit = model.gridPositions.Count;
+ int x, y;
+ for(int i = 0; i < loopLimit; i += 1)
+ {
+ x = model.gridPositions[i].x;
+ y = model.gridPositions[i].y;
+
+ if(model.GetCard(x, y) != null)continue;
+
+ //
+ card = model.GetCardFromDeck(x, y);
+
+ if(card.isFront)
+ {
+ MoveDeckCardToBoard.Create(seq, card, new Vector3(env.GetRealX(x), env.GetRealY(y), 0), card.gy * env.cols + card.gx, delay);
+ delay += MoveDeckCardToBoard.NextDelay();
+ }
+ else
+ {
+ OpenDeckCard.Create(seq, card, delay);
+ MoveDeckCardToBoard.Create(seq, card, new Vector3(env.GetRealX(x), env.GetRealY(y), 0), card.gy * env.cols + card.gx, delay + OpenDeckCard.NextDelay());
+ delay += MoveDeckCardToBoard.NextDelay();
+ }
+
+ hasNextCard = model.hasNextCard;
+
+ //
+ if(!hasNextCard)break;
+ }
+ }
+
+ if(hasNextCard)
+ {
+ card = model.GetCardFromDeck(-1, -1, true);
+
+ if(!card.isFront)OpenDeckCard.Create(seq, card, ref delay);
+ }
+
+ if(seq.Duration() > 0.0f)
+ {
+ await seq.Play().AwaitForComplete(cancellationToken: ct);
+ }
+
+ //
+ model.UpdateOrders();
+ }
+ async UniTask _DoMoveTrashToDeck()
+ {
+ model.MovePoolToDeck();
+
+ //
+ float delay = 0.0f;
+
+ Sequence seq = DOTween.Sequence();
+
+ for(int i = 0; i < model.decks.Count; i += 1)
+ {
+ MoveTrashCardToDeck.Create(model.decks[i], new Vector3(env.deckX + i * env.deckSpan, env.deckY, 0), ref delay);
+ }
+ await seq.Play().AwaitForComplete(cancellationToken: ct);
+
+ //
+ await OpenDeckCard.Create(model.GetCardFromDeck(-1, -1, true)).Play().AwaitForComplete(cancellationToken: ct);
+ }
+
+
+//====================
+// 퍼즐 게임의 일부입니다.
+//====================
+ HashSet checkedPairs = new HashSet();
+ HashSet cachedRemovableUnits = new HashSet();
+
+ VUnit[] targetUnits;
+
+ bool waitRemoveConnectUnits = false;
+
+ void UpdateJoints()
+ {
+ bool hasRemovableUnits = (!removableUnits.IsNullOrEmpty());
+ if(hasRemovableUnits)removableUnitJoints = new List();
+
+ cachedRemovableUnits.Clear();
+ for(int i = 0; i < removableUnits.Count; i += 1)cachedRemovableUnits.Add(removableUnits[i]);
+
+ checkedPairs.Clear();
+ int pairId = 0;
+
+ VUnit unit1 = null;
+ VUnit unit2 = null;
+ UnitBallJoint joint = null;
+
+ Vector3 pos1 = Constants.V0;
+ Vector3 pos2 = Constants.V0;
+
+ bool forceRemoveJoint = false;
+ float dist = 0.0f;
+ int distCache = 0;
+ float distPxl = 0.0f;
+ float distSpanPxl = 0.0f;
+ int jointType = 0;
+
+ targetUnits = units.Where((u) => (u.type == UnitType.Ball)).ToArray();
+
+ int total = targetUnits.Length;
+ int i, j;
+ for(i = 0; i < total; i += 1)
+ {
+ unit1 = targetUnits[i];
+
+ for(j = total - 1; j > -1; j -= 1)
+ {
+ if(i == j)continue;
+
+ //
+ unit2 = targetUnits[j];
+
+ pairId = (1 + Mathf.Max(unit1.hash, unit2.hash)) * 1000 + (1 + Mathf.Min(unit1.hash, unit2.hash));
+ if(checkedPairs.Contains(pairId))continue;
+ checkedPairs.Add(pairId);
+
+ if(!unit1.Equals(unit2))continue;
+
+ if(!VUnit.CanConnect(unit1, unit2))
+ {
+ RemoveJoint(pairId);
+ continue;
+ }
+
+ if(!unit1.isReady || !unit2.isReady)
+ {
+ RemoveJoint(pairId);
+ continue;
+ }
+
+ if(unit1.willRemove || unit2.willRemove)
+ {
+ RemoveJoint(pairId);
+ continue;
+ }
+
+ //
+ forceRemoveJoint = (hasRemovableUnits ? (cachedRemovableUnits.Contains(unit1) || cachedRemovableUnits.Contains(unit2)) : false);
+
+ pos1 = unit1.pos;
+ pos2 = unit2.pos;
+
+ dist = Vector3.Distance(pos1, pos2);
+ if(dist > env.unitDistanceMax || forceRemoveJoint)
+ {
+ RemoveJoint(pairId, forceRemoveJoint);
+ continue;
+ }
+
+ distCache = Mathf.FloorToInt(dist * 10000);
+ if(cachedJointDistances.ContainsKey(pairId) && cachedJointDistances[pairId] == distCache)continue;
+ cachedJointDistances[pairId] = distCache;
+
+ distPxl = dist * env.TO_PXL;
+ distSpanPxl = distPxl - env.unitPxlSize;
+
+ //
+ if(!cachedJoints.ContainsKey(pairId))cachedJoints[pairId] = SpawnJoint(env.canvas, unit1.shape);
+ joint = cachedJoints[pairId];
+
+ jointType = env.CalcJointType(distSpanPxl);
+ joint.UpdateLinks(unit1.shape, jointType, pos1, pos2, (distPxl - env.unitSpansEx[jointType]) * env.TO_UNT);
+ }
+ }
+
+ if(hasRemovableUnits)
+ {
+ waitRemoveConnectUnits = true;
+
+ units = units.Except(removableUnits).ToList();
+
+ units.ForEach((u) => {
+ u.isEnabled = false;
+ u.ResetAllVelocity();
+ });
+
+ //
+ removableUnits.ForEach((u) => u.isEnabled = false);
+
+ RemoveConnected(
+ new List(removableUnits.ToArray()),
+ new List(removableUnitJoints.ToArray())
+ );
+
+ removableUnits.Clear();
+ removableConnectedUnits.Clear();
+ removableUnitJoints.Clear();
+ }
+ else
+ {
+ if(waitRemoveConnectUnits)
+ {
+ units.ForEach((u) => u.isEnabled = true);
+
+ waitRemoveConnectUnits = false;
+ }
+ }
+ }
+ void UpdateRemovableUnitJoints()
+ {
+ if(removableConnectedUnits.IsNullOrEmpty())return;
+
+ //
+ checkedPairs.Clear();
+ int pairId = 0;
+
+ VUnit unit1 = null;
+ VUnit unit2 = null;
+ UnitBallJoint joint = null;
+
+ Vector3 pos1 = Constants.V0;
+ Vector3 pos2 = Constants.V0;
+
+ float dist = 0.0f;
+ int distCache = 0;
+ float distPxl = 0.0f;
+ float distSpanPxl = 0.0f;
+ int jointType = 0;
+
+ int total = removableConnectedUnits.Count;
+ int i, j;
+ for(i = 0; i < total; i += 1)
+ {
+ unit1 = removableConnectedUnits[i];
+
+ for(j = total - 1; j > -1; j -= 1)
+ {
+ if(i == j)continue;
+
+ //
+ unit2 = removableConnectedUnits[j];
+
+ pairId = (1 + Mathf.Max(unit1.hash, unit2.hash)) * 1000 + (1 + Mathf.Min(unit1.hash, unit2.hash));
+ if(checkedPairs.Contains(pairId))continue;
+ checkedPairs.Add(pairId);
+
+ //
+ pos1 = unit1.pos;
+ pos2 = unit2.pos;
+
+ dist = Vector3.Distance(pos1, pos2);
+ if(dist > env.unitDistanceMax)
+ {
+ RemoveJoint(pairId);
+ continue;
+ }
+
+ distCache = Mathf.FloorToInt(dist * 10000);
+ if(cachedJointDistances.ContainsKey(pairId) && cachedJointDistances[pairId] == distCache)continue;
+ cachedJointDistances[pairId] = distCache;
+
+ distPxl = dist * env.TO_PXL;
+ distSpanPxl = distPxl - env.unitPxlSize;
+
+ //
+ if(!cachedJoints.ContainsKey(pairId))cachedJoints[pairId] = SpawnJoint(env.canvas, unit1.shape);
+ joint = cachedJoints[pairId];
+
+ jointType = env.CalcJointType(distSpanPxl);
+ joint.UpdateLinks(unit1.shape, jointType, pos1, pos2, (distPxl - env.unitSpansEx[jointType]) * env.TO_UNT);
+ }
+ }
+ }
+
+ void RemoveJoint(int pairId, bool forceRemoveJoint=false)
+ {
+ if(cachedJointDistances.ContainsKey(pairId))cachedJointDistances.Remove(pairId);
+ if(cachedJoints.ContainsKey(pairId))
+ {
+ if(forceRemoveJoint)
+ {
+ removableUnitJoints.Add(cachedJoints[pairId]);
+ }
+ else
+ {
+ cachedJoints[pairId].Release();
+ }
+
+ cachedJoints.Remove(pairId);
+ }
+ }
+ void RemoveConnected(List units, List joints)
+ {
+ // TODO: 이 흐름을 더 합리적으로
+ List sideEffectedUnits = new List();
+
+ units.ForEach((u) => {
+ if(u.type != UnitType.Stone && u.type != UnitType.Minion)
+ {
+ sideEffectedUnits.AddRange(FindNeighborUnits(units, u, env.explodeDistanceMax));
+ }
+
+ KillUnit(u);
+
+ ReleaseUnit(u);
+ });
+ joints.ForEach((j) => j.Release(false));
+
+ //
+ sideEffectedUnits = sideEffectedUnits.Distinct().Where((u) => (u != null && !u.willRemove)).ToList();
+ if(sideEffectedUnits.IsNullOrEmpty())
+ {
+ units.ForEach((u) => u.isEnabled = true);
+
+ waitRemoveConnectUnits = false;
+ }
+ else
+ {
+ TaskUtil.DelayCall(0.15f, () => {
+ sideEffectedUnits.ForEach((u) => {
+ // if(u.type == UnitType.Ball && (u.behavior == UnitBehaviorType.Anchor || u.behavior == UnitBehaviorType.Freeze))
+ if(u.behavior == UnitBehaviorType.Anchor || u.behavior == UnitBehaviorType.Freeze)
+ {
+ ActKillUnit(u);
+ }
+ else if(u.type == UnitType.Bomb)
+ {
+ ActBomb(u);
+ }
+ else if(u.type == UnitType.Stone)
+ {
+ ActKillUnit(u);
+ }
+ });
+ });
+ }
+ }
+
+
+//====================
+// BusJam 류의 퍼즐 게임의 일부입니다.
+//====================
+ async UniTaskVoid CheckReady(Action onChecked=null)
+ {
+ waitGameReady = true;
+
+ //
+ ctsCheckReady = new CancellationTokenSource();
+ var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(ctsCheckReady.Token, gameObject.GetCancellationTokenOnDestroy());
+
+ // unit이 움직이고
+ if(refUnits.Any((g) => g.isBusy))
+ {
+ await UniTask.WaitUntil(() => !refUnits.Any((g) => g.isBusy), cancellationToken: linkedTokenSource.Token);
+
+ UpdateMovable();
+ }
+
+ //
+ while(true)
+ {
+ // box가 클리어면 나가고 다음것 들어오고
+ if(boxes[0].isComplete)
+ {
+ MoveBox();
+
+ if(boxes.Any((g) => g.isBusy))
+ {
+ await UniTask.WaitUntil(() => !boxes.Any((g) => g.isBusy), cancellationToken: linkedTokenSource.Token);
+
+ UpdateMovable();
+ }
+
+ boxes.RemoveAt(0);
+
+ // 새로운 box에 맞는 unit이 슬롯에 있으면 이동시키고
+ if(!boxes.IsNullOrEmpty())
+ {
+ CheckSlotUnitMove();
+
+ if(refUnits.Any((g) => g.isBusy))
+ {
+ await UniTask.WaitUntil(() => !refUnits.Any((g) => g.isBusy), cancellationToken: linkedTokenSource.Token);
+
+ UpdateMovable();
+ }
+ }
+
+ // 다음 box가 없으면 클리어
+ if(boxes.IsNullOrEmpty())
+ {
+ ResetCancellationTokenSource();
+
+ WillGameClear();
+ return;
+ }
+ }
+
+ if(!boxes[0].isComplete)break;
+ }
+
+ // slot이 꽉찼으면 게임오버
+ if(env.slot.slotRemain == 0)
+ {
+ ResetCancellationTokenSource();
+
+ WillGameOver();
+ return;
+ }
+
+ // unit 상태 변경
+ UpdateSecretState();
+ UpdateChainState();
+ UpdateFreezeState();
+ UpdateBombState();
+
+ await UniTask.WaitUntil(() => !refUnits.Any((g) => g.isBusy), cancellationToken: linkedTokenSource.Token);
+
+ // bomb터진 unit 있으면 게임오버
+ if(refUnits.Any((u) => u.isDead))
+ {
+ ResetCancellationTokenSource();
+
+ WillGameOver();
+ return;
+ }
+
+ // lock 상태 변경
+ if(lockGroups.Count > 0)
+ {
+ UpdateLockGroupState();
+
+ if(refUnits.Any((g) => g.isBusy))
+ {
+ await UniTask.WaitUntil(() => !refUnits.Any((g) => g.isBusy), cancellationToken: linkedTokenSource.Token);
+ }
+ if(refSpawners.Any((g) => g.isBusy))
+ {
+ await UniTask.WaitUntil(() => !refSpawners.Any((g) => g.isBusy), cancellationToken: linkedTokenSource.Token);
+ }
+ }
+
+ // spawner 동작
+ if(refSpawners.Count > 0 && refSpawners.Any((g) => g.canSpawn))
+ {
+ SpawnUnit();
+
+ // unit 상태 변경: 이건 생성된 유닛의 이동이 secret해제 애니랑 관계없다는 가정
+ UpdateSecretState();
+
+ if(refUnits.Any((g) => g.isBusy))
+ {
+ await UniTask.WaitUntil(() => !refUnits.Any((g) => g.isBusy), cancellationToken: linkedTokenSource.Token);
+
+ UpdateMovable();
+ }
+ if(refSpawners.Any((g) => g.isBusy))
+ {
+ await UniTask.WaitUntil(() => !refSpawners.Any((g) => g.isBusy), cancellationToken: linkedTokenSource.Token);
+ }
+ }
+
+ //
+ waitGameReady = false;
+
+ //
+ // NOTE: 최초 레벨 생성할때와 continue에서만 사용
+ if(onChecked != null)onChecked();
+ }
+
+