Compare commits
16 Commits
c6b06bb5ee
...
d700760524
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d700760524 | ||
|
|
ac0f3a458a | ||
|
|
113b82bd46 | ||
|
|
3e5ff544bf | ||
|
|
b74af8867e | ||
|
|
b669bcc0e0 | ||
|
|
9b6100b5bb | ||
|
|
f51286468a | ||
|
|
8a4a84dadf | ||
|
|
9b2e4c915c | ||
|
|
84689a6755 | ||
|
|
41c9969996 | ||
|
|
0b22ed6d09 | ||
|
|
47fb204446 | ||
|
|
676b389244 | ||
|
|
931515193a |
@@ -15,7 +15,7 @@ MonoBehaviour:
|
||||
m_DefaultGroup: 0e030d5498bfe4ffd8443c796618c539
|
||||
m_currentHash:
|
||||
serializedVersion: 2
|
||||
Hash: fdf5dbef4b3bdd1999753be21e456785
|
||||
Hash: 00000000000000000000000000000000
|
||||
m_OptimizeCatalogSize: 0
|
||||
m_BuildRemoteCatalog: 0
|
||||
m_CatalogRequestsTimeout: 0
|
||||
@@ -58,7 +58,7 @@ MonoBehaviour:
|
||||
m_ContentStateBuildPathProfileVariableName:
|
||||
m_CustomContentStateBuildPath:
|
||||
m_ContentStateBuildPath:
|
||||
m_BuildAddressablesWithPlayerBuild: 0
|
||||
m_BuildAddressablesWithPlayerBuild: 1
|
||||
m_overridePlayerVersion: '[UnityEditor.PlayerSettings.bundleVersion]'
|
||||
m_GroupAssets:
|
||||
- {fileID: 11400000, guid: ec9d910e81be14a1484f351f20d32f6f, type: 2}
|
||||
|
||||
8
Assets/AddressableAssetsData/Android.meta
Normal file
8
Assets/AddressableAssetsData/Android.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9fb2ea5fa9e054d2daab312c3be4d727
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -21,6 +21,12 @@ MonoBehaviour:
|
||||
m_SerializedLabels:
|
||||
- drawing
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 14477b5d35d0be9439ecb935f1c4e64e
|
||||
m_Address: Donut
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels:
|
||||
- drawing
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 2043f692673a79543afed5cb879f0e04
|
||||
m_Address: Five
|
||||
m_ReadOnly: 0
|
||||
@@ -39,6 +45,24 @@ MonoBehaviour:
|
||||
m_SerializedLabels:
|
||||
- drawing
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 4474b93704045a740ba9b8114e8a5238
|
||||
m_Address: Fish
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels:
|
||||
- drawing
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 62ae112e11b695a40b889d773a36f8bd
|
||||
m_Address: Elephant
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels:
|
||||
- drawing
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 6e36ba5d4763c694289c8ca75ee81449
|
||||
m_Address: Bus
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels:
|
||||
- drawing
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: 977dc7dac5ee6b543b8ed47c2299919e
|
||||
m_Address: Airplane
|
||||
m_ReadOnly: 0
|
||||
@@ -63,6 +87,12 @@ MonoBehaviour:
|
||||
m_SerializedLabels:
|
||||
- drawing
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
- m_GUID: f3200c6715fea4a41bc71c0314b519cd
|
||||
m_Address: Frog
|
||||
m_ReadOnly: 0
|
||||
m_SerializedLabels:
|
||||
- drawing
|
||||
FlaggedDuringContentUpdateRestriction: 0
|
||||
m_ReadOnly: 0
|
||||
m_Settings: {fileID: 11400000, guid: 4a94ef317c3674edd8270e4ed15031f6, type: 2}
|
||||
m_SchemaSet:
|
||||
|
||||
28
Assets/AddressableAssetsData/ProfileDataSourceSettings.asset
Normal file
28
Assets/AddressableAssetsData/ProfileDataSourceSettings.asset
Normal file
@@ -0,0 +1,28 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 7e3976da977cb49238499ea3b4c237ae, type: 3}
|
||||
m_Name: ProfileDataSourceSettings
|
||||
m_EditorClassIdentifier: Unity.Addressables.Editor::UnityEditor.AddressableAssets.Settings.ProfileDataSourceSettings
|
||||
profileGroupTypes:
|
||||
- m_GroupTypePrefix: Built-In
|
||||
m_Variables:
|
||||
- m_Suffix: BuildPath
|
||||
m_Value: '[UnityEngine.AddressableAssets.Addressables.BuildPath]/[BuildTarget]'
|
||||
- m_Suffix: LoadPath
|
||||
m_Value: '{UnityEngine.AddressableAssets.Addressables.RuntimePath}/[BuildTarget]'
|
||||
environments: []
|
||||
currentEnvironment:
|
||||
id:
|
||||
projectId:
|
||||
projectGenesisId:
|
||||
name:
|
||||
isDefault: 0
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 550878786d0cb4f48a8afa99c49a5635
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -8,6 +8,5 @@ namespace Darkmatter.Core.Contracts.Features.GameplayFlow
|
||||
UniTask BackAsync(CancellationToken cancellationToken);
|
||||
UniTask SaveAsync(CancellationToken cancellationToken);
|
||||
UniTask NextAsync(CancellationToken cancellationToken);
|
||||
void OnApplicationPaused();
|
||||
}
|
||||
}
|
||||
|
||||
8
Assets/Darkmatter/Code/Core/Contracts/Services/Ads.meta
Normal file
8
Assets/Darkmatter/Code/Core/Contracts/Services/Ads.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 683c2f491e8042d0a46bb7b5ad497be3
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using Darkmatter.Core.Data.Dynamic.Services.Ads;
|
||||
using Darkmatter.Core.Enums.Services.Ads;
|
||||
|
||||
namespace Darkmatter.Core.Contracts.Services.Ads
|
||||
{
|
||||
public interface IAdService
|
||||
{
|
||||
bool IsInitialized { get; }
|
||||
event Action<AdFormat, AdLoadState> LoadStateChanged;
|
||||
|
||||
UniTask InitializeAsync(CancellationToken cancellationToken);
|
||||
|
||||
UniTask<bool> LoadAsync(AdFormat format, CancellationToken cancellationToken);
|
||||
bool IsReady(AdFormat format);
|
||||
UniTask<AdShowResult> ShowAsync(AdFormat format, CancellationToken cancellationToken);
|
||||
|
||||
UniTask<bool> ShowBannerAsync(BannerSize size, BannerPosition position, CancellationToken cancellationToken);
|
||||
void HideBanner();
|
||||
void DestroyBanner();
|
||||
|
||||
void SetConsent(bool hasUserConsent, bool isChildDirected);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ffaede2f106c45a68197d70ec929c7de
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3d69da96f4ccb4de5910011cfc1e07a1
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,11 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Darkmatter.Core.Contracts.Services.Music
|
||||
{
|
||||
public interface IMusicService
|
||||
{
|
||||
void Play(AudioClip clip, float volume01 = 1f, bool loop = true);
|
||||
void Stop(float fadeOutSeconds = 0f);
|
||||
void SetVolume(float volume01);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f851c65abe6f240a894610f67360fe58
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 868c1ce3a8b740a8aa4949fe685a1dcc
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace Darkmatter.Core.Data.Dynamic.Services.Ads
|
||||
{
|
||||
public readonly struct AdReward
|
||||
{
|
||||
public readonly string Type;
|
||||
public readonly double Amount;
|
||||
|
||||
public AdReward(string type, double amount)
|
||||
{
|
||||
Type = type;
|
||||
Amount = amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 784af4bbe9df4c25b5d304d8364ade62
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace Darkmatter.Core.Data.Dynamic.Services.Ads
|
||||
{
|
||||
public readonly struct AdShowResult
|
||||
{
|
||||
public readonly bool Shown;
|
||||
public readonly bool Rewarded;
|
||||
public readonly AdReward Reward;
|
||||
public readonly string Error;
|
||||
|
||||
public AdShowResult(bool shown, bool rewarded, AdReward reward, string error)
|
||||
{
|
||||
Shown = shown;
|
||||
Rewarded = rewarded;
|
||||
Reward = reward;
|
||||
Error = error;
|
||||
}
|
||||
|
||||
public static AdShowResult Success() => new(true, false, default, null);
|
||||
public static AdShowResult WithReward(AdReward reward) => new(true, true, reward, null);
|
||||
public static AdShowResult Failure(string error) => new(false, false, default, error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e2859963572b4173957a583249f5d399
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f69d14fcc0bdd4b93aeb1717d030a9ca
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace Darkmatter.Core.Data.Signals.Features.Capture
|
||||
{
|
||||
public record struct GallerySaveCompletedSignal(bool Success);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 04fbdae933fab4011879c232a1041042
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace Darkmatter.Core.Data.Signals.Features.Capture
|
||||
{
|
||||
public record struct GallerySaveStartedSignal;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cff479845a55e4dd0a6d66043c91f661
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Darkmatter.Core
|
||||
{
|
||||
public record struct OpenArtBookSignal;
|
||||
public record struct OpenArtBookSignal(Action OnClose);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4dcdb224825b4764a080e5b881b996e4
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,67 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Darkmatter.Core.Enums.Services.Ads;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Darkmatter.Core.Data.Static.Services.Ads
|
||||
{
|
||||
[CreateAssetMenu(fileName = "AdUnitCatalog", menuName = "Darkmatter/Ads/Ad Unit Catalog")]
|
||||
public class AdUnitCatalogSO : ScriptableObject
|
||||
{
|
||||
[Serializable]
|
||||
public class Entry
|
||||
{
|
||||
public AdFormat Format;
|
||||
public string AndroidUnitId;
|
||||
public string IosUnitId;
|
||||
}
|
||||
|
||||
[Header("App IDs")]
|
||||
[SerializeField] private string androidAppId;
|
||||
[SerializeField] private string iosAppId;
|
||||
|
||||
[Header("Test Mode")]
|
||||
[Tooltip("If true, returns Google sample ad unit IDs (safe for development).")]
|
||||
[SerializeField] private bool useTestUnits = true;
|
||||
|
||||
[Tooltip("Device IDs to treat as test devices (hashed IDs from logcat).")]
|
||||
[SerializeField] private List<string> testDeviceIds = new();
|
||||
|
||||
[Header("Production Unit IDs")]
|
||||
[SerializeField] private List<Entry> entries = new();
|
||||
|
||||
public string AndroidAppId => androidAppId;
|
||||
public string IosAppId => iosAppId;
|
||||
public bool UseTestUnits => useTestUnits;
|
||||
public IReadOnlyList<string> TestDeviceIds => testDeviceIds;
|
||||
|
||||
public string GetUnitId(AdFormat format, RuntimePlatform platform)
|
||||
{
|
||||
if (useTestUnits || Application.isEditor)
|
||||
{
|
||||
return GetTestUnitId(format, platform);
|
||||
}
|
||||
|
||||
foreach (var e in entries)
|
||||
{
|
||||
if (e == null || e.Format != format) continue;
|
||||
return platform == RuntimePlatform.IPhonePlayer ? e.IosUnitId : e.AndroidUnitId;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string GetTestUnitId(AdFormat format, RuntimePlatform platform)
|
||||
{
|
||||
bool ios = platform == RuntimePlatform.IPhonePlayer;
|
||||
return format switch
|
||||
{
|
||||
AdFormat.Banner => ios ? "ca-app-pub-3940256099942544/2934735716" : "ca-app-pub-3940256099942544/6300978111",
|
||||
AdFormat.Interstitial => ios ? "ca-app-pub-3940256099942544/4411468910" : "ca-app-pub-3940256099942544/1033173712",
|
||||
AdFormat.Rewarded => ios ? "ca-app-pub-3940256099942544/1712485313" : "ca-app-pub-3940256099942544/5224354917",
|
||||
AdFormat.RewardedInterstitial => ios ? "ca-app-pub-3940256099942544/6978759866" : "ca-app-pub-3940256099942544/5354046379",
|
||||
AdFormat.AppOpen => ios ? "ca-app-pub-3940256099942544/5575463023" : "ca-app-pub-3940256099942544/9257395921",
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e6352f0162df4adf99a5945e25c6bf40
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Darkmatter/Code/Core/Enums/Services/Ads.meta
Normal file
8
Assets/Darkmatter/Code/Core/Enums/Services/Ads.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 40846ca6e71249d68a99e1d226fd162d
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
11
Assets/Darkmatter/Code/Core/Enums/Services/Ads/AdFormat.cs
Normal file
11
Assets/Darkmatter/Code/Core/Enums/Services/Ads/AdFormat.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace Darkmatter.Core.Enums.Services.Ads
|
||||
{
|
||||
public enum AdFormat
|
||||
{
|
||||
Banner,
|
||||
Interstitial,
|
||||
Rewarded,
|
||||
RewardedInterstitial,
|
||||
AppOpen
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ce1e736d19fc4569af80b06274dc3935
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Darkmatter.Core.Enums.Services.Ads
|
||||
{
|
||||
public enum AdLoadState
|
||||
{
|
||||
Idle,
|
||||
Loading,
|
||||
Loaded,
|
||||
Failed
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 44f09c3955b34814abfa83407118a6a5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace Darkmatter.Core.Enums.Services.Ads
|
||||
{
|
||||
public enum BannerPosition
|
||||
{
|
||||
Top,
|
||||
Bottom,
|
||||
TopLeft,
|
||||
TopRight,
|
||||
BottomLeft,
|
||||
BottomRight,
|
||||
Center
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2223e7f2cedb4f479f7c6722e0ca7fd1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
13
Assets/Darkmatter/Code/Core/Enums/Services/Ads/BannerSize.cs
Normal file
13
Assets/Darkmatter/Code/Core/Enums/Services/Ads/BannerSize.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace Darkmatter.Core.Enums.Services.Ads
|
||||
{
|
||||
public enum BannerSize
|
||||
{
|
||||
Banner,
|
||||
LargeBanner,
|
||||
MediumRectangle,
|
||||
FullBanner,
|
||||
Leaderboard,
|
||||
SmartBanner,
|
||||
AnchoredAdaptive
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 570a6e9b47c5451c8ef78aa7a3923ead
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -7,5 +7,9 @@ namespace Darkmatter.Core.Enums.Services.Audio
|
||||
ShapeSnap = 101,
|
||||
ShapeReturn = 102,
|
||||
UiTap = 200,
|
||||
PlayButtonTap = 201,
|
||||
LevelComplete = 300,
|
||||
FireWorkLaunch = 400,
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace Darkmatter.Features.Artbook
|
||||
private readonly List<ArtbookEntry> _entries = new();
|
||||
private readonly List<Sprite> _ownedSprites = new();
|
||||
private readonly List<Texture2D> _ownedTextures = new();
|
||||
|
||||
private Action _onClose;
|
||||
private CancellationTokenSource _cts;
|
||||
private IDisposable _openSubscription;
|
||||
private int _currentSpread;
|
||||
@@ -60,10 +60,12 @@ namespace Darkmatter.Features.Artbook
|
||||
|
||||
private void HandleOpenArtbookEvent(OpenArtBookSignal signal)
|
||||
{
|
||||
_onClose = null;
|
||||
_view.Show();
|
||||
_cts?.Cancel();
|
||||
_cts?.Dispose();
|
||||
_cts = new CancellationTokenSource();
|
||||
_onClose = signal.OnClose;
|
||||
LoadAndShowAsync(_cts.Token).Forget();
|
||||
}
|
||||
|
||||
@@ -164,7 +166,7 @@ namespace Darkmatter.Features.Artbook
|
||||
|
||||
private void HandleBackButtonClicked()
|
||||
{
|
||||
_eventBus.Publish(new OpenColorBookSignal());
|
||||
_onClose?.Invoke();
|
||||
_view.Hide();
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"references": [
|
||||
"GUID:6a0a834eb41764f12ba55c3fb04a40cb",
|
||||
"GUID:c1c03c0e5b2f4412b9f2be1c20d6a9b1",
|
||||
"GUID:b4c9f7fbf1e144933a1797dc208ece5f",
|
||||
"GUID:b0214a6008ed146ff8f122a6a9c2f6cc",
|
||||
"GUID:f51ebe6a0ceec4240a699833d6309b23"
|
||||
],
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace Darkmatter.Features.Capture
|
||||
{
|
||||
[SerializeField, Range(0.1f, 2f)] private float captureScale = 1f;
|
||||
[SerializeField] private CaptureButtonView captureButtonView;
|
||||
[SerializeField] private GallerySaveView gallerySaveView;
|
||||
|
||||
public void Register(IContainerBuilder builder)
|
||||
{
|
||||
@@ -19,6 +20,9 @@ namespace Darkmatter.Features.Capture
|
||||
|
||||
if (captureButtonView != null)
|
||||
builder.RegisterEntryPoint<CaptureButtonPresenter>().WithParameter(captureButtonView);
|
||||
|
||||
if (gallerySaveView != null)
|
||||
builder.RegisterEntryPoint<GallerySavePresenter>().WithParameter(gallerySaveView);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ using Darkmatter.Core.Contracts.Features.Capture;
|
||||
using Darkmatter.Core.Contracts.Features.GameplayFlow;
|
||||
using Darkmatter.Core.Contracts.Services.Capture;
|
||||
using Darkmatter.Core.Contracts.Services.Gallery;
|
||||
using Darkmatter.Core.Data.Signals.Features.Capture;
|
||||
using Darkmatter.Libs.Observer;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Darkmatter.Features.Capture
|
||||
@@ -14,17 +16,20 @@ namespace Darkmatter.Features.Capture
|
||||
private readonly ICaptureService _captureService;
|
||||
private readonly IGalleryService _galleryService;
|
||||
private readonly IGameplaySceneRefs _refs;
|
||||
private readonly IEventBus _bus;
|
||||
private readonly CaptureConfig _config;
|
||||
|
||||
public CaptureSystem(
|
||||
ICaptureService captureService,
|
||||
IGalleryService galleryService,
|
||||
IGameplaySceneRefs refs,
|
||||
IEventBus bus,
|
||||
CaptureConfig config)
|
||||
{
|
||||
_captureService = captureService;
|
||||
_galleryService = galleryService;
|
||||
_refs = refs;
|
||||
_bus = bus;
|
||||
_config = config;
|
||||
}
|
||||
|
||||
@@ -34,15 +39,21 @@ namespace Darkmatter.Features.Capture
|
||||
if (!saveToGallery || png == null || png.Length == 0) return png;
|
||||
|
||||
var tex = new Texture2D(2, 2, TextureFormat.RGBA32, mipChain: false);
|
||||
var success = false;
|
||||
try
|
||||
{
|
||||
if (tex.LoadImage(png))
|
||||
{
|
||||
_bus.Publish(new GallerySaveStartedSignal());
|
||||
await _galleryService.SaveImageAsync(tex,
|
||||
$"colorbook_{DateTime.UtcNow:yyyyMMdd_HHmmss}.png", ct);
|
||||
success = true;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
UnityEngine.Object.Destroy(tex);
|
||||
_bus.Publish(new GallerySaveCompletedSignal(success));
|
||||
}
|
||||
return png;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using Darkmatter.Core.Data.Signals.Features.Capture;
|
||||
using Darkmatter.Libs.Observer;
|
||||
using VContainer.Unity;
|
||||
|
||||
namespace Darkmatter.Features.Capture.UI
|
||||
{
|
||||
public class GallerySavePresenter : IStartable, IDisposable
|
||||
{
|
||||
private readonly GallerySaveView _view;
|
||||
private readonly IEventBus _bus;
|
||||
|
||||
private IDisposable _startedSub;
|
||||
private IDisposable _completedSub;
|
||||
private CancellationTokenSource _popupCts;
|
||||
|
||||
public GallerySavePresenter(GallerySaveView view, IEventBus bus)
|
||||
{
|
||||
_view = view;
|
||||
_bus = bus;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
_startedSub = _bus.Subscribe<GallerySaveStartedSignal>(OnStarted);
|
||||
_completedSub = _bus.Subscribe<GallerySaveCompletedSignal>(OnCompleted);
|
||||
}
|
||||
|
||||
private void OnStarted(GallerySaveStartedSignal _)
|
||||
{
|
||||
CancelPopup();
|
||||
_view.ShowSaving();
|
||||
}
|
||||
|
||||
private void OnCompleted(GallerySaveCompletedSignal signal)
|
||||
{
|
||||
_view.HideSaving();
|
||||
if (!signal.Success) return;
|
||||
CancelPopup();
|
||||
_popupCts = new CancellationTokenSource();
|
||||
ShowPopupAsync(_popupCts.Token).Forget();
|
||||
}
|
||||
|
||||
private async UniTaskVoid ShowPopupAsync(CancellationToken ct)
|
||||
{
|
||||
_view.ShowSuccess();
|
||||
try
|
||||
{
|
||||
await UniTask.Delay(TimeSpan.FromSeconds(_view.PopupAutoHideSeconds), cancellationToken: ct);
|
||||
}
|
||||
catch (OperationCanceledException) { return; }
|
||||
_view.HideSuccess();
|
||||
}
|
||||
|
||||
private void CancelPopup()
|
||||
{
|
||||
_popupCts?.Cancel();
|
||||
_popupCts?.Dispose();
|
||||
_popupCts = null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_startedSub?.Dispose();
|
||||
_completedSub?.Dispose();
|
||||
CancelPopup();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 71d8cccc7b3a3436ba83ca3683fbea9f
|
||||
@@ -0,0 +1,40 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Darkmatter.Features.Capture.UI
|
||||
{
|
||||
public class GallerySaveView : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private GameObject savingIndicator;
|
||||
[SerializeField] private GameObject successPopup;
|
||||
[SerializeField, Min(0f)] private float popupAutoHideSeconds = 1.5f;
|
||||
|
||||
public float PopupAutoHideSeconds => popupAutoHideSeconds;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (savingIndicator != null) savingIndicator.SetActive(false);
|
||||
if (successPopup != null) successPopup.SetActive(false);
|
||||
}
|
||||
|
||||
public void ShowSaving()
|
||||
{
|
||||
if (savingIndicator != null) savingIndicator.SetActive(true);
|
||||
if (successPopup != null) successPopup.SetActive(false);
|
||||
}
|
||||
|
||||
public void HideSaving()
|
||||
{
|
||||
if (savingIndicator != null) savingIndicator.SetActive(false);
|
||||
}
|
||||
|
||||
public void ShowSuccess()
|
||||
{
|
||||
if (successPopup != null) successPopup.SetActive(true);
|
||||
}
|
||||
|
||||
public void HideSuccess()
|
||||
{
|
||||
if (successPopup != null) successPopup.SetActive(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bbcc5a579d90f4763a781379f5213d6e
|
||||
@@ -6,8 +6,10 @@ using Darkmatter.Core;
|
||||
using Darkmatter.Core.Contracts.Features.DrawingCatalog;
|
||||
using Darkmatter.Core.Contracts.Features.Loading;
|
||||
using Darkmatter.Core.Contracts.Features.Progression;
|
||||
using Darkmatter.Core.Contracts.Services.Ads;
|
||||
using Darkmatter.Core.Contracts.Services.Scenes;
|
||||
using Darkmatter.Core.Data.Signals.Features.Drawing;
|
||||
using Darkmatter.Core.Enums.Services.Ads;
|
||||
using Darkmatter.Core.Enums.Services.Scenes;
|
||||
using Darkmatter.Libs.Observer;
|
||||
using VContainer.Unity;
|
||||
@@ -20,33 +22,54 @@ public class ColorbookFlowController : IAsyncStartable, IDisposable
|
||||
private readonly ILoadingScreen _loadingScreen;
|
||||
private readonly IProgressionSystem _progression;
|
||||
private readonly ISceneService _scenes;
|
||||
private readonly IAdService _ads;
|
||||
private readonly IEventBus _bus;
|
||||
|
||||
private IDisposable _selectedSub;
|
||||
private IDisposable _returnToMainMenuSubscription;
|
||||
private bool _navigatingToGameplay;
|
||||
private CancellationTokenSource _scopeCts;
|
||||
|
||||
public ColorbookFlowController(
|
||||
IDrawingCatalogController drawingCatalog,
|
||||
ILoadingScreen loadingScreen,
|
||||
IProgressionSystem progression,
|
||||
ISceneService scenes,
|
||||
IAdService ads,
|
||||
IEventBus bus)
|
||||
{
|
||||
_drawingCatalog = drawingCatalog;
|
||||
_loadingScreen = loadingScreen;
|
||||
_progression = progression;
|
||||
_scenes = scenes;
|
||||
_ads = ads;
|
||||
_bus = bus;
|
||||
_selectedSub = _bus.Subscribe<DrawingSelectedSignal>(OnDrawingSelected);
|
||||
}
|
||||
|
||||
public async UniTask StartAsync(CancellationToken cancellation = new CancellationToken())
|
||||
{
|
||||
_scopeCts = CancellationTokenSource.CreateLinkedTokenSource(cancellation);
|
||||
_returnToMainMenuSubscription = _bus.Subscribe<ReturnToMainMenuSignal>(OnReturnToMainMenu);
|
||||
_loadingScreen.SetProgress(1f);
|
||||
await _drawingCatalog.InitializeAsync(cancellation);
|
||||
if (!_navigatingToGameplay) _loadingScreen.Hide();
|
||||
|
||||
PrewarmRewardedAdAsync(_scopeCts.Token).Forget();
|
||||
}
|
||||
|
||||
private async UniTaskVoid PrewarmRewardedAdAsync(CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!_ads.IsInitialized) await _ads.InitializeAsync(ct);
|
||||
if (!_ads.IsReady(AdFormat.Rewarded)) await _ads.LoadAsync(AdFormat.Rewarded, ct);
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
UnityEngine.Debug.LogWarning($"[ColorbookFlow] Rewarded prewarm failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnReturnToMainMenu(ReturnToMainMenuSignal signal)
|
||||
@@ -66,13 +89,20 @@ public class ColorbookFlowController : IAsyncStartable, IDisposable
|
||||
|
||||
private void OnDrawingSelected(DrawingSelectedSignal signal)
|
||||
{
|
||||
if (_navigatingToGameplay) return;
|
||||
_navigatingToGameplay = true;
|
||||
HandleSelectionAsync(signal.TemplateId).Forget();
|
||||
}
|
||||
|
||||
private async UniTaskVoid HandleSelectionAsync(string templateId)
|
||||
{
|
||||
var ct = _scopeCts?.Token ?? CancellationToken.None;
|
||||
|
||||
_loadingScreen.Show();
|
||||
_loadingScreen.SetProgress(0f);
|
||||
|
||||
await ShowRewardedAdAsync(ct);
|
||||
|
||||
var progress = new Progress<float>(p => _loadingScreen.SetProgress(p * 0.5f));
|
||||
var mappedProgress = new Progress<float>(p => _loadingScreen.SetProgress(0.5f + p * 0.25f));
|
||||
await _progression.SetLastOpenedAsync(templateId);
|
||||
@@ -81,9 +111,34 @@ public class ColorbookFlowController : IAsyncStartable, IDisposable
|
||||
cancellationToken: default);
|
||||
}
|
||||
|
||||
private async UniTask ShowRewardedAdAsync(CancellationToken ct)
|
||||
{
|
||||
const int InitTimeoutMs = 4000;
|
||||
try
|
||||
{
|
||||
if (!_ads.IsInitialized)
|
||||
{
|
||||
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
timeoutCts.CancelAfter(InitTimeoutMs);
|
||||
await _ads.InitializeAsync(timeoutCts.Token);
|
||||
}
|
||||
|
||||
if (!_ads.IsReady(AdFormat.Rewarded)) return;
|
||||
|
||||
await _ads.ShowAsync(AdFormat.Rewarded, ct);
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
UnityEngine.Debug.LogWarning($"[ColorbookFlow] Rewarded ad skipped: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_selectedSub?.Dispose();
|
||||
_returnToMainMenuSubscription?.Dispose();
|
||||
_scopeCts?.Cancel();
|
||||
_scopeCts?.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,8 @@ public class ColoringController : IColoringController, IDisposable
|
||||
|
||||
private GameObject _colorInstance;
|
||||
private GameObject _colorButtonPrefab;
|
||||
private GameObject _completionAnimationPrefab;
|
||||
private GameObject _completionAnimationInstance;
|
||||
private CompletionAnimationView _completionAnimationView;
|
||||
private readonly List<ColorRegionView> _regions = new();
|
||||
private readonly List<ColorButton> _buttons = new();
|
||||
private readonly Dictionary<string, Color> _authoredColors = new();
|
||||
@@ -89,17 +90,17 @@ public class ColoringController : IColoringController, IDisposable
|
||||
|
||||
public async UniTask PlayCompletionAnimationAsync(CancellationToken ct)
|
||||
{
|
||||
if (_completionAnimationPrefab == null) return;
|
||||
var instance = UnityEngine.Object.Instantiate(_completionAnimationPrefab, _refs.PaperRoot);
|
||||
if (_completionAnimationInstance == null || _completionAnimationView == null) return;
|
||||
if (_colorInstance != null) _colorInstance.SetActive(false);
|
||||
_completionAnimationInstance.SetActive(true);
|
||||
try
|
||||
{
|
||||
var view = instance.GetComponentInChildren<CompletionAnimationView>();
|
||||
if (view == null) return;
|
||||
await view.PlayAsync(ct);
|
||||
await _completionAnimationView.PlayAsync(ct);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (instance != null) UnityEngine.Object.Destroy(instance);
|
||||
if (_completionAnimationInstance != null)
|
||||
_completionAnimationInstance.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +141,13 @@ public class ColoringController : IColoringController, IDisposable
|
||||
|
||||
_regions.Clear();
|
||||
_authoredColors.Clear();
|
||||
_completionAnimationPrefab = null;
|
||||
|
||||
if (_completionAnimationInstance != null)
|
||||
{
|
||||
UnityEngine.Object.Destroy(_completionAnimationInstance);
|
||||
_completionAnimationInstance = null;
|
||||
_completionAnimationView = null;
|
||||
}
|
||||
|
||||
if (_colorInstance != null)
|
||||
{
|
||||
@@ -154,7 +161,12 @@ public class ColoringController : IColoringController, IDisposable
|
||||
private void InitializeColorRegions(IDrawingTemplate template, IReadOnlyDictionary<string, Color> savedColors)
|
||||
{
|
||||
_colorInstance = UnityEngine.Object.Instantiate(template.ColoringPrefab, _refs.PaperRoot);
|
||||
_completionAnimationPrefab = template.CompletionAnimationPrefab;
|
||||
if (template.CompletionAnimationPrefab != null)
|
||||
{
|
||||
_completionAnimationInstance = UnityEngine.Object.Instantiate(template.CompletionAnimationPrefab, _refs.PaperRoot);
|
||||
_completionAnimationView = _completionAnimationInstance.GetComponentInChildren<CompletionAnimationView>(includeInactive: true);
|
||||
_completionAnimationInstance.SetActive(false);
|
||||
}
|
||||
var views = _colorInstance.GetComponentsInChildren<ColorRegionView>(includeInactive: true);
|
||||
|
||||
foreach (var region in views)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using Darkmatter.Core;
|
||||
using Darkmatter.Core.Data.Signals.Features.ShapeBuilder;
|
||||
using Darkmatter.Libs.Observer;
|
||||
using VContainer.Unity;
|
||||
@@ -22,11 +23,18 @@ namespace Darkmatter.Features.Coloring.UI
|
||||
{
|
||||
_view.HideInstant();
|
||||
_assembledSub = _bus.Subscribe<ShapeAssembledSignal>(_ => _view.Show());
|
||||
_view.OnArtbookClicked += HandleArtbookClicked;
|
||||
}
|
||||
|
||||
private void HandleArtbookClicked()
|
||||
{
|
||||
_bus.Publish(new OpenArtBookSignal());
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_assembledSub?.Dispose();
|
||||
_view.OnArtbookClicked -= HandleArtbookClicked;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using PrimeTween;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Darkmatter.Features.Coloring.UI
|
||||
{
|
||||
@@ -8,6 +10,7 @@ namespace Darkmatter.Features.Coloring.UI
|
||||
{
|
||||
[SerializeField] private RectTransform spawnRoot;
|
||||
[SerializeField] private RectTransform animatedRoot;
|
||||
[SerializeField] private Button artbookButton;
|
||||
[SerializeField] private float showDuration = 0.35f;
|
||||
[SerializeField] private float hideDuration = 0.25f;
|
||||
[SerializeField] private Vector2 hiddenOffset = new(1500f, 0f);
|
||||
@@ -17,9 +20,16 @@ namespace Darkmatter.Features.Coloring.UI
|
||||
private Sequence _activeSequence;
|
||||
private bool _refsReady;
|
||||
|
||||
public event Action OnArtbookClicked;
|
||||
public RectTransform SpawnRoot => spawnRoot;
|
||||
|
||||
private void Awake() => EnsureRefs();
|
||||
private void Awake()
|
||||
{
|
||||
EnsureRefs();
|
||||
if (artbookButton != null) artbookButton.onClick.AddListener(HandleArtbookClicked);
|
||||
}
|
||||
|
||||
private void HandleArtbookClicked() => OnArtbookClicked?.Invoke();
|
||||
|
||||
private void EnsureRefs()
|
||||
{
|
||||
@@ -80,5 +90,10 @@ namespace Darkmatter.Features.Coloring.UI
|
||||
{
|
||||
KillActive();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (artbookButton != null) artbookButton.onClick.RemoveListener(HandleArtbookClicked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,17 +8,28 @@ namespace Darkmatter.Features.Coloring.UI
|
||||
public class CompletionAnimationView : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private RectTransform target;
|
||||
[SerializeField] private float duration = 0.6f;
|
||||
[SerializeField] private float duration = 1f;
|
||||
[SerializeField] private Vector3 startScale = Vector3.zero;
|
||||
[SerializeField] private Vector3 endScale = Vector3.one;
|
||||
[SerializeField] private Ease ease = Ease.OutBack;
|
||||
|
||||
[Header("Wiggle")]
|
||||
[SerializeField] private float wiggleAngle = 10f;
|
||||
[SerializeField] private float wiggleDuration = 0.25f;
|
||||
[SerializeField, Min(1)] private int wiggleCycles = 3;
|
||||
|
||||
public async UniTask PlayAsync(CancellationToken ct)
|
||||
{
|
||||
var rt = target != null ? target : transform as RectTransform;
|
||||
if (rt == null) return;
|
||||
rt.localScale = startScale;
|
||||
rt.localRotation = Quaternion.identity;
|
||||
await Tween.Scale(rt, endScale, duration, ease).ToUniTask(cancellationToken: ct);
|
||||
|
||||
await Tween.LocalRotation(rt, new Vector3(0f, 0f, wiggleAngle), wiggleDuration, Ease.InOutSine,
|
||||
cycles: wiggleCycles * 2, cycleMode: CycleMode.Yoyo)
|
||||
.ToUniTask(cancellationToken: ct);
|
||||
rt.localRotation = Quaternion.identity;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"GUID:c1c03c0e5b2f4412b9f2be1c20d6a9b1",
|
||||
"GUID:b4c9f7fbf1e144933a1797dc208ece5f",
|
||||
"GUID:b0214a6008ed146ff8f122a6a9c2f6cc",
|
||||
"GUID:f51ebe6a0ceec4240a699833d6309b23"
|
||||
"GUID:f51ebe6a0ceec4240a699833d6309b23",
|
||||
"GUID:6055be8ebefd69e48b49212b09b47b2f"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
|
||||
@@ -39,6 +39,8 @@ namespace Darkmatter.Features.DrawingCatalog
|
||||
_view.OnArtBookClicked += OnArtBookBtnClicked;
|
||||
_view.OnLeftPageClicked += OnLeftPageBtnClicked;
|
||||
_view.OnRightPageClicked += OnRightPageBtnClicked;
|
||||
_view.OnPageChangedByScroll += OnPageChangedByScroll;
|
||||
_view.OnPageNumberClicked += OnPageNumberClicked;
|
||||
|
||||
_controller.ListChanged += OnListChanged;
|
||||
|
||||
@@ -68,9 +70,21 @@ namespace Darkmatter.Features.DrawingCatalog
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPageChangedByScroll(int page)
|
||||
{
|
||||
_currentPage = page;
|
||||
}
|
||||
|
||||
private void OnPageNumberClicked(int page)
|
||||
{
|
||||
if (page < 0 || page >= _totalPages || page == _currentPage) return;
|
||||
_currentPage = page;
|
||||
_view.SetPagination(_currentPage, _totalPages);
|
||||
}
|
||||
|
||||
private void OnArtBookBtnClicked()
|
||||
{
|
||||
_eventBus.Publish(new OpenArtBookSignal());
|
||||
_eventBus.Publish(new OpenArtBookSignal(()=> _view.Show()));
|
||||
_view.Hide();
|
||||
}
|
||||
|
||||
@@ -116,6 +130,8 @@ namespace Darkmatter.Features.DrawingCatalog
|
||||
_view.OnArtBookClicked -= OnArtBookBtnClicked;
|
||||
_view.OnLeftPageClicked -= OnLeftPageBtnClicked;
|
||||
_view.OnRightPageClicked -= OnRightPageBtnClicked;
|
||||
_view.OnPageChangedByScroll -= OnPageChangedByScroll;
|
||||
_view.OnPageNumberClicked -= OnPageNumberClicked;
|
||||
_openSubscription?.Dispose();
|
||||
_cts.Cancel();
|
||||
_cts.Dispose();
|
||||
|
||||
@@ -20,15 +20,25 @@ namespace Darkmatter.Features.DrawingCatalog
|
||||
[SerializeField] private ScrollRect scrollRect;
|
||||
[SerializeField] private Button leftPageButton;
|
||||
[SerializeField] private Button rightPageButton;
|
||||
|
||||
[Header("Page Numbers")]
|
||||
[SerializeField] private RectTransform pageNumberContainer;
|
||||
[SerializeField] private PageNumberButton pageNumberPrefab;
|
||||
private readonly List<DrawingCatalogButton> _buttons = new();
|
||||
private readonly List<PageNumberButton> _pageButtons = new();
|
||||
|
||||
public event Action<string> OnItemClicked;
|
||||
public event Action OnBackClicked;
|
||||
public event Action OnArtBookClicked;
|
||||
public event Action OnLeftPageClicked;
|
||||
public event Action OnRightPageClicked;
|
||||
public event Action<int> OnPageChangedByScroll;
|
||||
public event Action<int> OnPageNumberClicked;
|
||||
|
||||
private Coroutine _sliderCoroutine;
|
||||
private int _currentPage;
|
||||
private int _totalPages = 1;
|
||||
private bool _suppressScrollEvents;
|
||||
|
||||
public void Start()
|
||||
{
|
||||
@@ -36,6 +46,7 @@ namespace Darkmatter.Features.DrawingCatalog
|
||||
artBookButton.onClick.AddListener(() => OnArtBookClicked?.Invoke());
|
||||
leftPageButton.onClick.AddListener(() => OnLeftPageClicked?.Invoke());
|
||||
rightPageButton.onClick.AddListener(() => OnRightPageClicked?.Invoke());
|
||||
scrollRect.onValueChanged.AddListener(OnScrollValueChanged);
|
||||
}
|
||||
public void Show()
|
||||
{
|
||||
@@ -69,8 +80,11 @@ namespace Darkmatter.Features.DrawingCatalog
|
||||
|
||||
public void SetPagination(int currentPage, int totalPages)
|
||||
{
|
||||
leftPageButton.interactable = currentPage > 0;
|
||||
rightPageButton.interactable = currentPage < totalPages - 1;
|
||||
_currentPage = currentPage;
|
||||
_totalPages = totalPages;
|
||||
BuildPageNumbers(totalPages);
|
||||
UpdateArrowButtons();
|
||||
UpdateActivePageNumber();
|
||||
if (totalPages <= 1) return;
|
||||
|
||||
float pageWidth = scrollRect.viewport.rect.width;
|
||||
@@ -84,8 +98,65 @@ namespace Darkmatter.Features.DrawingCatalog
|
||||
_sliderCoroutine = StartCoroutine(SmoothScrollTo(targetPosition));
|
||||
}
|
||||
|
||||
// Keeps the arrow buttons (and the presenter) in sync when the user scrolls manually.
|
||||
private void OnScrollValueChanged(Vector2 _)
|
||||
{
|
||||
if (_suppressScrollEvents || _totalPages <= 1) return;
|
||||
|
||||
int page = PageFromScrollPosition();
|
||||
if (page == _currentPage) return;
|
||||
|
||||
_currentPage = page;
|
||||
UpdateArrowButtons();
|
||||
UpdateActivePageNumber();
|
||||
OnPageChangedByScroll?.Invoke(page);
|
||||
}
|
||||
|
||||
private void BuildPageNumbers(int totalPages)
|
||||
{
|
||||
if (pageNumberPrefab == null || pageNumberContainer == null) return;
|
||||
|
||||
while (_pageButtons.Count < totalPages)
|
||||
{
|
||||
var button = Instantiate(pageNumberPrefab, pageNumberContainer);
|
||||
int pageIndex = _pageButtons.Count;
|
||||
button.Initialize(pageIndex, () => OnPageNumberClicked?.Invoke(pageIndex));
|
||||
_pageButtons.Add(button);
|
||||
}
|
||||
|
||||
while (_pageButtons.Count > totalPages)
|
||||
{
|
||||
var last = _pageButtons[^1];
|
||||
_pageButtons.RemoveAt(_pageButtons.Count - 1);
|
||||
Destroy(last.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateActivePageNumber()
|
||||
{
|
||||
for (int i = 0; i < _pageButtons.Count; i++)
|
||||
_pageButtons[i].SetHighlighted(i == _currentPage);
|
||||
}
|
||||
|
||||
private int PageFromScrollPosition()
|
||||
{
|
||||
float pageWidth = scrollRect.viewport.rect.width;
|
||||
float maxScroll = content.rect.width - pageWidth;
|
||||
if (maxScroll <= 0f) return 0;
|
||||
|
||||
int page = Mathf.RoundToInt(scrollRect.horizontalNormalizedPosition * maxScroll / pageWidth);
|
||||
return Mathf.Clamp(page, 0, _totalPages - 1);
|
||||
}
|
||||
|
||||
private void UpdateArrowButtons()
|
||||
{
|
||||
leftPageButton.interactable = _currentPage > 0;
|
||||
rightPageButton.interactable = _currentPage < _totalPages - 1;
|
||||
}
|
||||
|
||||
private System.Collections.IEnumerator SmoothScrollTo(float targetPosition)
|
||||
{
|
||||
_suppressScrollEvents = true;
|
||||
const float duration = 0.3f;
|
||||
float elapsed = 0f;
|
||||
float startPosition = scrollRect.horizontalNormalizedPosition;
|
||||
@@ -96,12 +167,15 @@ namespace Darkmatter.Features.DrawingCatalog
|
||||
yield return null;
|
||||
}
|
||||
scrollRect.horizontalNormalizedPosition = targetPosition;
|
||||
_suppressScrollEvents = false;
|
||||
}
|
||||
|
||||
|
||||
public void ResetScrollPosition()
|
||||
{
|
||||
_suppressScrollEvents = true;
|
||||
scrollRect.horizontalNormalizedPosition = 0f;
|
||||
_suppressScrollEvents = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Darkmatter.Features.DrawingCatalog
|
||||
{
|
||||
public class PageNumberButton : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private TMP_Text label;
|
||||
[SerializeField] private Button button;
|
||||
|
||||
[Header("Highlight")]
|
||||
[Tooltip("Image faded for unselected pages. Use the button's color block (below) to match the disabled look.")]
|
||||
[SerializeField] private Image image;
|
||||
[Tooltip("When on, unselected pages use the button's Disabled Color and selected use Normal Color. " +
|
||||
"When off, only the alpha below is applied.")]
|
||||
[SerializeField] private bool useButtonColors = true;
|
||||
[Range(0f, 1f)]
|
||||
[SerializeField] private float unselectedAlpha = 0.5f;
|
||||
|
||||
public int PageIndex { get; private set; }
|
||||
|
||||
public void Initialize(int pageIndex, UnityEngine.Events.UnityAction onClick)
|
||||
{
|
||||
PageIndex = pageIndex;
|
||||
if (label != null) label.text = (pageIndex + 1).ToString();
|
||||
button.onClick.RemoveAllListeners();
|
||||
button.onClick.AddListener(onClick);
|
||||
}
|
||||
|
||||
public void SetHighlighted(bool isActive)
|
||||
{
|
||||
if (image == null) return;
|
||||
|
||||
if (useButtonColors)
|
||||
{
|
||||
var colors = button.colors;
|
||||
image.color = isActive ? colors.normalColor : colors.disabledColor;
|
||||
return;
|
||||
}
|
||||
|
||||
var c = image.color;
|
||||
c.a = isActive ? 1f : unselectedAlpha;
|
||||
image.color = c;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 19495f2daba89ce44abe376a40ca313d
|
||||
@@ -9,13 +9,14 @@ using Darkmatter.Core.Contracts.Features.GameplayFlow;
|
||||
using Darkmatter.Core.Contracts.Features.Loading;
|
||||
using Darkmatter.Core.Contracts.Features.Progression;
|
||||
using Darkmatter.Core.Contracts.Features.ShapeBuilder;
|
||||
using Darkmatter.Core.Contracts.Services.Gallery;
|
||||
using Darkmatter.Core.Contracts.Services.Audio;
|
||||
using Darkmatter.Core.Contracts.Services.Scenes;
|
||||
using Darkmatter.Core.Data.Dynamic.Features.Progression;
|
||||
using Darkmatter.Core.Data.Signals.Features.Coloring;
|
||||
using Darkmatter.Core.Data.Signals.Features.Drawing;
|
||||
using Darkmatter.Core.Data.Signals.Features.ShapeBuilder;
|
||||
using Darkmatter.Core.Enums.Features.Progression;
|
||||
using Darkmatter.Core.Enums.Services.Audio;
|
||||
using Darkmatter.Core.Enums.Services.Scenes;
|
||||
using Darkmatter.Libs.Observer;
|
||||
using UnityEngine;
|
||||
@@ -35,6 +36,7 @@ namespace Darkmatter.Features.GameplayFlow.Systems
|
||||
private readonly ICaptureFeature _capture;
|
||||
private readonly ILoadingScreen _loadingScreen;
|
||||
private readonly IGameplaySceneRefs _refs;
|
||||
private readonly ISfxPlayer _sfx;
|
||||
private readonly IEventBus _bus;
|
||||
|
||||
private IDrawingTemplate _template;
|
||||
@@ -43,6 +45,7 @@ namespace Darkmatter.Features.GameplayFlow.Systems
|
||||
|
||||
private IDisposable _assembledSub;
|
||||
private IDisposable _colorAppliedSub;
|
||||
private IDisposable _drawingSelectedSub;
|
||||
private CancellationTokenSource _autosaveCts;
|
||||
private CancellationTokenSource _scopeCts;
|
||||
|
||||
@@ -55,6 +58,7 @@ namespace Darkmatter.Features.GameplayFlow.Systems
|
||||
ICaptureFeature capture,
|
||||
ILoadingScreen loadingScreen,
|
||||
IGameplaySceneRefs refs,
|
||||
ISfxPlayer sfx,
|
||||
IEventBus bus)
|
||||
{
|
||||
_progression = progression;
|
||||
@@ -65,6 +69,7 @@ namespace Darkmatter.Features.GameplayFlow.Systems
|
||||
_capture = capture;
|
||||
_loadingScreen = loadingScreen;
|
||||
_refs = refs;
|
||||
_sfx = sfx;
|
||||
_bus = bus;
|
||||
}
|
||||
|
||||
@@ -89,9 +94,7 @@ namespace Darkmatter.Features.GameplayFlow.Systems
|
||||
|
||||
_assembledSub = _bus.Subscribe<ShapeAssembledSignal>(OnShapeAssembled);
|
||||
_colorAppliedSub = _bus.Subscribe<ColorAppliedSignal>(OnColorApplied);
|
||||
|
||||
Application.quitting += OnAppQuitting;
|
||||
Application.focusChanged += OnAppFocusChanged;
|
||||
_drawingSelectedSub = _bus.Subscribe<DrawingSelectedSignal>(OnDrawingSelected);
|
||||
|
||||
_loadingScreen.SetProgress(1f);
|
||||
if (_phase == DrawingPhase.Coloring)
|
||||
@@ -127,6 +130,8 @@ namespace Darkmatter.Features.GameplayFlow.Systems
|
||||
{
|
||||
await SaveCurrentAsync(ct);
|
||||
_refs.Confetti.Play();
|
||||
_sfx.Play(SfxId.FireWorkLaunch);
|
||||
_sfx.Play(SfxId.LevelComplete);
|
||||
await _coloring.PlayCompletionAnimationAsync(ct);
|
||||
_progression.MarkCompleted(_templateId);
|
||||
|
||||
@@ -149,17 +154,6 @@ namespace Darkmatter.Features.GameplayFlow.Systems
|
||||
_bus.Publish(new DrawingSelectedSignal(nextId));
|
||||
}
|
||||
|
||||
public void OnApplicationPaused()
|
||||
{
|
||||
SaveCurrentAsync(CancellationToken.None).Forget();
|
||||
}
|
||||
|
||||
private void OnAppQuitting() => OnApplicationPaused();
|
||||
|
||||
private void OnAppFocusChanged(bool focused)
|
||||
{
|
||||
if (!focused) OnApplicationPaused();
|
||||
}
|
||||
|
||||
private void OnShapeAssembled(ShapeAssembledSignal signal)
|
||||
{
|
||||
@@ -188,6 +182,24 @@ namespace Darkmatter.Features.GameplayFlow.Systems
|
||||
DebouncedAutosaveAsync(_autosaveCts.Token).Forget();
|
||||
}
|
||||
|
||||
private void OnDrawingSelected(DrawingSelectedSignal signal)
|
||||
{
|
||||
if (string.IsNullOrEmpty(signal.TemplateId)) return;
|
||||
if (signal.TemplateId == _templateId) return;
|
||||
EditAsync(signal.TemplateId).Forget();
|
||||
}
|
||||
|
||||
private async UniTaskVoid EditAsync(string newTemplateId)
|
||||
{
|
||||
await SaveCurrentAsync(CancellationToken.None);
|
||||
_loadingScreen.Show();
|
||||
_shapeBuilder.Clear();
|
||||
_coloring.Clear();
|
||||
await _scenes.LoadSceneAsync(nameof(GameScene.Colorbook), progress: null, cancellationToken: default);
|
||||
await _scenes.UnloadSceneAsync(nameof(GameScene.Gameplay), progress: null, cancellationToken: default);
|
||||
_bus.Publish(new DrawingSelectedSignal(newTemplateId));
|
||||
}
|
||||
|
||||
private async UniTaskVoid DebouncedAutosaveAsync(CancellationToken ct)
|
||||
{
|
||||
try
|
||||
@@ -241,11 +253,10 @@ namespace Darkmatter.Features.GameplayFlow.Systems
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Application.quitting -= OnAppQuitting;
|
||||
Application.focusChanged -= OnAppFocusChanged;
|
||||
|
||||
_assembledSub?.Dispose();
|
||||
_colorAppliedSub?.Dispose();
|
||||
_drawingSelectedSub?.Dispose();
|
||||
_autosaveCts?.Cancel();
|
||||
_autosaveCts?.Dispose();
|
||||
_scopeCts?.Cancel();
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
using System;
|
||||
using Darkmatter.Core.Contracts.Services.Audio;
|
||||
using Darkmatter.Core.Data.Signals.Features.AppBoot;
|
||||
using Darkmatter.Core.Data.Signals.Features.MainMenu;
|
||||
using Darkmatter.Core.Enums.Services.Audio;
|
||||
using Darkmatter.Libs.Observer;
|
||||
using UnityEngine;
|
||||
using VContainer.Unity;
|
||||
|
||||
namespace Darkmatter.Features.Mainmenu
|
||||
@@ -11,17 +12,19 @@ namespace Darkmatter.Features.Mainmenu
|
||||
{
|
||||
private readonly IEventBus _eventBus;
|
||||
private readonly MainMenuView _view;
|
||||
private readonly ISfxPlayer _sfxPlayer;
|
||||
|
||||
|
||||
|
||||
public MainMenuPresenter(MainMenuView view, IEventBus eventBus)
|
||||
public MainMenuPresenter(MainMenuView view, IEventBus eventBus, ISfxPlayer sfxPlayer)
|
||||
{
|
||||
_view = view;
|
||||
_eventBus = eventBus;
|
||||
_sfxPlayer = sfxPlayer;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
_view.OnPlayBtnPressedEvent += OnPlayBtnPressed;
|
||||
_view.OnPlayBtnClickedEvent += OnPlayBtnClicked;
|
||||
_eventBus.Subscribe<IntroCompletedSignal>(OnIntroComplete);
|
||||
}
|
||||
@@ -31,6 +34,11 @@ namespace Darkmatter.Features.Mainmenu
|
||||
_view.PlayMascotIntro();
|
||||
}
|
||||
|
||||
private void OnPlayBtnPressed()
|
||||
{
|
||||
_sfxPlayer.Play(SfxId.PlayButtonTap);
|
||||
}
|
||||
|
||||
private void OnPlayBtnClicked()
|
||||
{
|
||||
_eventBus.Publish(new PlayBtnClickedSignal());
|
||||
@@ -38,6 +46,7 @@ namespace Darkmatter.Features.Mainmenu
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_view.OnPlayBtnPressedEvent -= OnPlayBtnPressed;
|
||||
_view.OnPlayBtnClickedEvent -= OnPlayBtnClicked;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ namespace Darkmatter.Features.Mainmenu
|
||||
public class MainMenuView : MonoBehaviour
|
||||
{
|
||||
[Header("UI Elements")]
|
||||
//[SerializeField] private Button playBtn;
|
||||
[SerializeField] private PlayButtonView playBtn;
|
||||
[SerializeField] private SkeletonGraphic mascotSkeletonGraphic;
|
||||
|
||||
@@ -20,12 +19,14 @@ namespace Darkmatter.Features.Mainmenu
|
||||
[SerializeField] private string helloAnimation = "hello";
|
||||
[SerializeField] private float helloInterval = 5f;
|
||||
|
||||
public event Action OnPlayBtnPressedEvent;
|
||||
public event Action OnPlayBtnClickedEvent;
|
||||
|
||||
private CancellationTokenSource helloCts;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
playBtn.OnPlayBtnPressedEvent += () => OnPlayBtnPressedEvent?.Invoke();
|
||||
playBtn.OnPlayBtnClickedEvent += () => OnPlayBtnClickedEvent?.Invoke();
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace Darkmatter.Features.Mainmenu
|
||||
private Animator playBtnAnimator;
|
||||
[SerializeField] private ParticleSystem playBtnParticle;
|
||||
|
||||
public event Action OnPlayBtnPressedEvent;
|
||||
public event Action OnPlayBtnClickedEvent;
|
||||
|
||||
private void Awake()
|
||||
@@ -29,6 +30,7 @@ namespace Darkmatter.Features.Mainmenu
|
||||
{
|
||||
playBtn.interactable = false;
|
||||
playBtnAnimator.enabled = false;
|
||||
OnPlayBtnPressedEvent?.Invoke();
|
||||
PlayBtnSequenceAsync(this.GetCancellationTokenOnDestroy()).Forget();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using Darkmatter.Core;
|
||||
using Darkmatter.Core.Data.Signals.Features.ShapeBuilder;
|
||||
using Darkmatter.Libs.Observer;
|
||||
using VContainer.Unity;
|
||||
@@ -24,12 +25,19 @@ namespace Darkmatter.Features.ShapeBuilder.UI
|
||||
_view.HideInstant();
|
||||
_startedSub = _bus.Subscribe<ShapeBuilderStartedSignal>(_ => _view.Show());
|
||||
_assembledSub = _bus.Subscribe<ShapeAssembledSignal>(_ => _view.Hide());
|
||||
_view.OnArtbookClicked += HandleArtbookClicked;
|
||||
}
|
||||
|
||||
private void HandleArtbookClicked()
|
||||
{
|
||||
_bus.Publish(new OpenArtBookSignal());
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_startedSub?.Dispose();
|
||||
_assembledSub?.Dispose();
|
||||
_view.OnArtbookClicked -= HandleArtbookClicked;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
using System;
|
||||
using Darkmatter.Features.ShapeBuilder.Systems;
|
||||
using PrimeTween;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Darkmatter.Features.ShapeBuilder.UI
|
||||
{
|
||||
@@ -8,6 +11,7 @@ namespace Darkmatter.Features.ShapeBuilder.UI
|
||||
{
|
||||
[SerializeField] private RectTransform spawnRoot;
|
||||
[SerializeField] private RectTransform animatedRoot;
|
||||
[SerializeField] private Button artbookButton;
|
||||
[SerializeField] private float showDuration = 0.35f;
|
||||
[SerializeField] private float hideDuration = 0.25f;
|
||||
[SerializeField] private Vector2 hiddenOffset = new(1500f, 0f);
|
||||
@@ -15,6 +19,7 @@ namespace Darkmatter.Features.ShapeBuilder.UI
|
||||
private CanvasGroup _canvasGroup;
|
||||
private Vector2 _shownAnchoredPos;
|
||||
private Sequence _activeSequence;
|
||||
public event Action OnArtbookClicked;
|
||||
|
||||
public RectTransform SpawnRoot => spawnRoot;
|
||||
public float SpawnWidth => spawnRoot.rect.width;
|
||||
@@ -24,6 +29,12 @@ namespace Darkmatter.Features.ShapeBuilder.UI
|
||||
_canvasGroup = GetComponent<CanvasGroup>();
|
||||
if (animatedRoot == null) animatedRoot = (RectTransform)transform;
|
||||
_shownAnchoredPos = animatedRoot.anchoredPosition;
|
||||
artbookButton.onClick.AddListener(HandleArtbookClicked);
|
||||
}
|
||||
|
||||
private void HandleArtbookClicked()
|
||||
{
|
||||
OnArtbookClicked?.Invoke();
|
||||
}
|
||||
|
||||
public Sequence Show()
|
||||
@@ -73,5 +84,10 @@ namespace Darkmatter.Features.ShapeBuilder.UI
|
||||
{
|
||||
KillActive();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
artbookButton.onClick.RemoveListener(HandleArtbookClicked);
|
||||
}
|
||||
}
|
||||
}
|
||||
8
Assets/Darkmatter/Code/Services/Ads.meta
Normal file
8
Assets/Darkmatter/Code/Services/Ads.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3a320ad839a14f578bb9ff149807ed84
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Darkmatter/Code/Services/Ads/Installers.meta
Normal file
8
Assets/Darkmatter/Code/Services/Ads/Installers.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fd05451b7a0c4e04ae9521e8052ee1e2
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,31 @@
|
||||
using Darkmatter.Core.Contracts.Services.Ads;
|
||||
using Darkmatter.Core.Data.Static.Services.Ads;
|
||||
using Darkmatter.Libs.Installers;
|
||||
using UnityEngine;
|
||||
using VContainer;
|
||||
using VContainer.Unity;
|
||||
|
||||
namespace Darkmatter.Services.Ads
|
||||
{
|
||||
public class AdServiceModule : MonoBehaviour, IModule
|
||||
{
|
||||
[SerializeField] private AdUnitCatalogSO adUnitCatalog;
|
||||
[SerializeField] private AdMobAdService adService;
|
||||
|
||||
public void Register(IContainerBuilder builder)
|
||||
{
|
||||
if (adUnitCatalog != null)
|
||||
builder.RegisterComponent(adUnitCatalog);
|
||||
|
||||
var resolved = adService;
|
||||
if (resolved == null) resolved = GetComponent<AdMobAdService>();
|
||||
if (resolved == null) resolved = GetComponentInChildren<AdMobAdService>(includeInactive: true);
|
||||
if (resolved == null) resolved = FindFirstObjectByType<AdMobAdService>(FindObjectsInactive.Include);
|
||||
|
||||
if (resolved != null)
|
||||
builder.RegisterComponent<IAdService>(resolved);
|
||||
else
|
||||
Debug.LogError("[AdServiceModule] No AdMobAdService component found. Assign 'adService' field or add AdMobAdService MonoBehaviour to scene.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3a17cb41935543a6b4f67be8f398d774
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
25
Assets/Darkmatter/Code/Services/Ads/Services.Ads.asmdef
Normal file
25
Assets/Darkmatter/Code/Services/Ads/Services.Ads.asmdef
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "Services.Ads",
|
||||
"rootNamespace": "Darkmatter.Services.Ads",
|
||||
"references": [
|
||||
"GUID:6a0a834eb41764f12ba55c3fb04a40cb",
|
||||
"GUID:f51ebe6a0ceec4240a699833d6309b23",
|
||||
"GUID:b0214a6008ed146ff8f122a6a9c2f6cc",
|
||||
"GUID:c1c03c0e5b2f4412b9f2be1c20d6a9b1"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [
|
||||
{
|
||||
"name": "com.google.ads.mobile",
|
||||
"expression": "1.0.0",
|
||||
"define": "GOOGLE_MOBILE_ADS"
|
||||
}
|
||||
],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 662a8e56a1724c84ad8c10211211f0d5
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Darkmatter/Code/Services/Ads/Systems.meta
Normal file
8
Assets/Darkmatter/Code/Services/Ads/Systems.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6fbb9ad075cb419095482b3d74c4311c
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
535
Assets/Darkmatter/Code/Services/Ads/Systems/AdMobAdService.cs
Normal file
535
Assets/Darkmatter/Code/Services/Ads/Systems/AdMobAdService.cs
Normal file
@@ -0,0 +1,535 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using Darkmatter.Core.Contracts.Services.Ads;
|
||||
using Darkmatter.Core.Data.Dynamic.Services.Ads;
|
||||
using Darkmatter.Core.Data.Static.Services.Ads;
|
||||
using Darkmatter.Core.Enums.Services.Ads;
|
||||
using UnityEngine;
|
||||
using AdFormat = Darkmatter.Core.Enums.Services.Ads.AdFormat;
|
||||
#if GOOGLE_MOBILE_ADS
|
||||
using GoogleMobileAds.Api;
|
||||
#endif
|
||||
|
||||
namespace Darkmatter.Services.Ads
|
||||
{
|
||||
public class AdMobAdService : MonoBehaviour, IAdService
|
||||
{
|
||||
[SerializeField] private AdUnitCatalogSO catalog;
|
||||
[Tooltip("Auto-reload after dismiss/failure for non-banner formats.")]
|
||||
[SerializeField] private bool autoReload = true;
|
||||
[Tooltip("Seconds between auto-reload retries on failure.")]
|
||||
[SerializeField, Min(1f)] private float reloadDelaySeconds = 5f;
|
||||
|
||||
public bool IsInitialized => _initialized;
|
||||
public event Action<AdFormat, AdLoadState> LoadStateChanged;
|
||||
|
||||
private bool _initialized;
|
||||
private bool _hasUserConsent = true;
|
||||
private bool _isChildDirected;
|
||||
private CancellationTokenSource _lifetimeCts;
|
||||
|
||||
private readonly Dictionary<AdFormat, AdLoadState> _states = new();
|
||||
|
||||
#if GOOGLE_MOBILE_ADS
|
||||
private InterstitialAd _interstitial;
|
||||
private RewardedAd _rewarded;
|
||||
private RewardedInterstitialAd _rewardedInterstitial;
|
||||
private AppOpenAd _appOpen;
|
||||
private BannerView _banner;
|
||||
#endif
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_lifetimeCts = new CancellationTokenSource();
|
||||
foreach (AdFormat fmt in Enum.GetValues(typeof(AdFormat)))
|
||||
_states[fmt] = AdLoadState.Idle;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
_lifetimeCts?.Cancel();
|
||||
_lifetimeCts?.Dispose();
|
||||
_lifetimeCts = null;
|
||||
|
||||
#if GOOGLE_MOBILE_ADS
|
||||
_interstitial?.Destroy();
|
||||
_rewarded?.Destroy();
|
||||
_rewardedInterstitial?.Destroy();
|
||||
_appOpen?.Destroy();
|
||||
_banner?.Destroy();
|
||||
#endif
|
||||
}
|
||||
|
||||
public async UniTask InitializeAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_initialized) return;
|
||||
if (catalog == null)
|
||||
{
|
||||
Debug.LogError("[AdMobAdService] No AdUnitCatalogSO assigned.");
|
||||
return;
|
||||
}
|
||||
|
||||
#if GOOGLE_MOBILE_ADS
|
||||
ApplyRequestConfiguration();
|
||||
|
||||
var tcs = new UniTaskCompletionSource<bool>();
|
||||
MobileAds.Initialize(_ => tcs.TrySetResult(true));
|
||||
|
||||
using (cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken)))
|
||||
{
|
||||
await tcs.Task;
|
||||
}
|
||||
|
||||
_initialized = true;
|
||||
Debug.Log("[AdMobAdService] Initialized.");
|
||||
#else
|
||||
await UniTask.CompletedTask;
|
||||
_initialized = true;
|
||||
Debug.LogWarning("[AdMobAdService] GOOGLE_MOBILE_ADS not defined. Service will no-op. Add scripting define after importing Google Mobile Ads SDK.");
|
||||
#endif
|
||||
}
|
||||
|
||||
public async UniTask<bool> LoadAsync(AdFormat format, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!_initialized)
|
||||
{
|
||||
Debug.LogWarning("[AdMobAdService] Load called before init.");
|
||||
return false;
|
||||
}
|
||||
|
||||
#if GOOGLE_MOBILE_ADS
|
||||
string unitId = catalog.GetUnitId(format, Application.platform);
|
||||
if (string.IsNullOrEmpty(unitId))
|
||||
{
|
||||
Debug.LogWarning($"[AdMobAdService] No unit ID for {format} on {Application.platform}.");
|
||||
return false;
|
||||
}
|
||||
|
||||
SetState(format, AdLoadState.Loading);
|
||||
var request = new AdRequest();
|
||||
|
||||
switch (format)
|
||||
{
|
||||
case AdFormat.Interstitial:
|
||||
return await LoadInterstitialAsync(unitId, request, cancellationToken);
|
||||
case AdFormat.Rewarded:
|
||||
return await LoadRewardedAsync(unitId, request, cancellationToken);
|
||||
case AdFormat.RewardedInterstitial:
|
||||
return await LoadRewardedInterstitialAsync(unitId, request, cancellationToken);
|
||||
case AdFormat.AppOpen:
|
||||
return await LoadAppOpenAsync(unitId, request, cancellationToken);
|
||||
case AdFormat.Banner:
|
||||
Debug.LogWarning("[AdMobAdService] Use ShowBannerAsync for banners.");
|
||||
SetState(format, AdLoadState.Failed);
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
#else
|
||||
await UniTask.CompletedTask;
|
||||
SetState(format, AdLoadState.Failed);
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
public bool IsReady(AdFormat format)
|
||||
{
|
||||
if (!_initialized) return false;
|
||||
#if GOOGLE_MOBILE_ADS
|
||||
return format switch
|
||||
{
|
||||
AdFormat.Interstitial => _interstitial != null && _interstitial.CanShowAd(),
|
||||
AdFormat.Rewarded => _rewarded != null && _rewarded.CanShowAd(),
|
||||
AdFormat.RewardedInterstitial => _rewardedInterstitial != null && _rewardedInterstitial.CanShowAd(),
|
||||
AdFormat.AppOpen => _appOpen != null && _appOpen.CanShowAd(),
|
||||
_ => false
|
||||
};
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
public async UniTask<AdShowResult> ShowAsync(AdFormat format, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!_initialized) return AdShowResult.Failure("Not initialized.");
|
||||
|
||||
#if GOOGLE_MOBILE_ADS
|
||||
if (!IsReady(format))
|
||||
{
|
||||
bool loaded = await LoadAsync(format, cancellationToken);
|
||||
if (!loaded) return AdShowResult.Failure($"{format} failed to load.");
|
||||
}
|
||||
|
||||
switch (format)
|
||||
{
|
||||
case AdFormat.Interstitial: return await ShowInterstitialAsync(cancellationToken);
|
||||
case AdFormat.Rewarded: return await ShowRewardedAsync(cancellationToken);
|
||||
case AdFormat.RewardedInterstitial: return await ShowRewardedInterstitialAsync(cancellationToken);
|
||||
case AdFormat.AppOpen: return await ShowAppOpenAsync(cancellationToken);
|
||||
}
|
||||
return AdShowResult.Failure($"{format} not supported by ShowAsync.");
|
||||
#else
|
||||
await UniTask.CompletedTask;
|
||||
return AdShowResult.Failure("GOOGLE_MOBILE_ADS not defined.");
|
||||
#endif
|
||||
}
|
||||
|
||||
public async UniTask<bool> ShowBannerAsync(BannerSize size, BannerPosition position, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!_initialized) return false;
|
||||
|
||||
#if GOOGLE_MOBILE_ADS
|
||||
string unitId = catalog.GetUnitId(AdFormat.Banner, Application.platform);
|
||||
if (string.IsNullOrEmpty(unitId)) return false;
|
||||
|
||||
_banner?.Destroy();
|
||||
_banner = new BannerView(unitId, MapBannerSize(size), MapBannerPosition(position));
|
||||
|
||||
var tcs = new UniTaskCompletionSource<bool>();
|
||||
_banner.OnBannerAdLoaded += () => tcs.TrySetResult(true);
|
||||
_banner.OnBannerAdLoadFailed += err =>
|
||||
{
|
||||
Debug.LogWarning($"[AdMobAdService] Banner load failed: {err}");
|
||||
tcs.TrySetResult(false);
|
||||
};
|
||||
|
||||
SetState(AdFormat.Banner, AdLoadState.Loading);
|
||||
_banner.LoadAd(new AdRequest());
|
||||
|
||||
using (cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken)))
|
||||
{
|
||||
bool ok = await tcs.Task;
|
||||
SetState(AdFormat.Banner, ok ? AdLoadState.Loaded : AdLoadState.Failed);
|
||||
return ok;
|
||||
}
|
||||
#else
|
||||
await UniTask.CompletedTask;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
public void HideBanner()
|
||||
{
|
||||
#if GOOGLE_MOBILE_ADS
|
||||
_banner?.Hide();
|
||||
#endif
|
||||
}
|
||||
|
||||
public void DestroyBanner()
|
||||
{
|
||||
#if GOOGLE_MOBILE_ADS
|
||||
_banner?.Destroy();
|
||||
_banner = null;
|
||||
SetState(AdFormat.Banner, AdLoadState.Idle);
|
||||
#endif
|
||||
}
|
||||
|
||||
public void SetConsent(bool hasUserConsent, bool isChildDirected)
|
||||
{
|
||||
_hasUserConsent = hasUserConsent;
|
||||
_isChildDirected = isChildDirected;
|
||||
#if GOOGLE_MOBILE_ADS
|
||||
if (_initialized) ApplyRequestConfiguration();
|
||||
#endif
|
||||
}
|
||||
|
||||
#if GOOGLE_MOBILE_ADS
|
||||
private void ApplyRequestConfiguration()
|
||||
{
|
||||
var config = new RequestConfiguration
|
||||
{
|
||||
TagForChildDirectedTreatment = _isChildDirected
|
||||
? TagForChildDirectedTreatment.True
|
||||
: TagForChildDirectedTreatment.Unspecified,
|
||||
TagForUnderAgeOfConsent = _hasUserConsent
|
||||
? TagForUnderAgeOfConsent.Unspecified
|
||||
: TagForUnderAgeOfConsent.True
|
||||
};
|
||||
|
||||
if (catalog.TestDeviceIds is { Count: > 0 })
|
||||
{
|
||||
config.TestDeviceIds = new List<string>(catalog.TestDeviceIds);
|
||||
}
|
||||
|
||||
MobileAds.SetRequestConfiguration(config);
|
||||
}
|
||||
|
||||
private async UniTask<bool> LoadInterstitialAsync(string unitId, AdRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var tcs = new UniTaskCompletionSource<bool>();
|
||||
InterstitialAd.Load(unitId, request, (ad, error) =>
|
||||
{
|
||||
if (error != null || ad == null)
|
||||
{
|
||||
Debug.LogWarning($"[AdMobAdService] Interstitial load failed: {error}");
|
||||
tcs.TrySetResult(false);
|
||||
return;
|
||||
}
|
||||
_interstitial?.Destroy();
|
||||
_interstitial = ad;
|
||||
WireFullScreenEvents(ad, AdFormat.Interstitial);
|
||||
tcs.TrySetResult(true);
|
||||
});
|
||||
|
||||
using (cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken)))
|
||||
{
|
||||
bool ok = await tcs.Task;
|
||||
SetState(AdFormat.Interstitial, ok ? AdLoadState.Loaded : AdLoadState.Failed);
|
||||
return ok;
|
||||
}
|
||||
}
|
||||
|
||||
private async UniTask<bool> LoadRewardedAsync(string unitId, AdRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var tcs = new UniTaskCompletionSource<bool>();
|
||||
RewardedAd.Load(unitId, request, (ad, error) =>
|
||||
{
|
||||
if (error != null || ad == null)
|
||||
{
|
||||
Debug.LogWarning($"[AdMobAdService] Rewarded load failed: {error}");
|
||||
tcs.TrySetResult(false);
|
||||
return;
|
||||
}
|
||||
_rewarded?.Destroy();
|
||||
_rewarded = ad;
|
||||
WireFullScreenEvents(ad, AdFormat.Rewarded);
|
||||
tcs.TrySetResult(true);
|
||||
});
|
||||
|
||||
using (cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken)))
|
||||
{
|
||||
bool ok = await tcs.Task;
|
||||
SetState(AdFormat.Rewarded, ok ? AdLoadState.Loaded : AdLoadState.Failed);
|
||||
return ok;
|
||||
}
|
||||
}
|
||||
|
||||
private async UniTask<bool> LoadRewardedInterstitialAsync(string unitId, AdRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var tcs = new UniTaskCompletionSource<bool>();
|
||||
RewardedInterstitialAd.Load(unitId, request, (ad, error) =>
|
||||
{
|
||||
if (error != null || ad == null)
|
||||
{
|
||||
Debug.LogWarning($"[AdMobAdService] RewardedInterstitial load failed: {error}");
|
||||
tcs.TrySetResult(false);
|
||||
return;
|
||||
}
|
||||
_rewardedInterstitial?.Destroy();
|
||||
_rewardedInterstitial = ad;
|
||||
WireFullScreenEvents(ad, AdFormat.RewardedInterstitial);
|
||||
tcs.TrySetResult(true);
|
||||
});
|
||||
|
||||
using (cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken)))
|
||||
{
|
||||
bool ok = await tcs.Task;
|
||||
SetState(AdFormat.RewardedInterstitial, ok ? AdLoadState.Loaded : AdLoadState.Failed);
|
||||
return ok;
|
||||
}
|
||||
}
|
||||
|
||||
private async UniTask<bool> LoadAppOpenAsync(string unitId, AdRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var tcs = new UniTaskCompletionSource<bool>();
|
||||
AppOpenAd.Load(unitId, request, (ad, error) =>
|
||||
{
|
||||
if (error != null || ad == null)
|
||||
{
|
||||
Debug.LogWarning($"[AdMobAdService] AppOpen load failed: {error}");
|
||||
tcs.TrySetResult(false);
|
||||
return;
|
||||
}
|
||||
_appOpen?.Destroy();
|
||||
_appOpen = ad;
|
||||
WireFullScreenEvents(ad, AdFormat.AppOpen);
|
||||
tcs.TrySetResult(true);
|
||||
});
|
||||
|
||||
using (cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken)))
|
||||
{
|
||||
bool ok = await tcs.Task;
|
||||
SetState(AdFormat.AppOpen, ok ? AdLoadState.Loaded : AdLoadState.Failed);
|
||||
return ok;
|
||||
}
|
||||
}
|
||||
|
||||
private async UniTask<AdShowResult> ShowInterstitialAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var tcs = new UniTaskCompletionSource<AdShowResult>();
|
||||
Action onClosed = () => tcs.TrySetResult(AdShowResult.Success());
|
||||
Action<AdError> onFailed = err => tcs.TrySetResult(AdShowResult.Failure(err?.GetMessage()));
|
||||
|
||||
_interstitial.OnAdFullScreenContentClosed += onClosed;
|
||||
_interstitial.OnAdFullScreenContentFailed += onFailed;
|
||||
_interstitial.Show();
|
||||
|
||||
try
|
||||
{
|
||||
using (cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken)))
|
||||
return await tcs.Task;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_interstitial != null)
|
||||
{
|
||||
_interstitial.OnAdFullScreenContentClosed -= onClosed;
|
||||
_interstitial.OnAdFullScreenContentFailed -= onFailed;
|
||||
}
|
||||
ScheduleReload(AdFormat.Interstitial);
|
||||
}
|
||||
}
|
||||
|
||||
private async UniTask<AdShowResult> ShowRewardedAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var tcs = new UniTaskCompletionSource<AdShowResult>();
|
||||
bool earned = false;
|
||||
AdReward reward = default;
|
||||
|
||||
Action onClosed = () => tcs.TrySetResult(earned ? AdShowResult.WithReward(reward) : AdShowResult.Success());
|
||||
Action<AdError> onFailed = err => tcs.TrySetResult(AdShowResult.Failure(err?.GetMessage()));
|
||||
|
||||
_rewarded.OnAdFullScreenContentClosed += onClosed;
|
||||
_rewarded.OnAdFullScreenContentFailed += onFailed;
|
||||
_rewarded.Show(r =>
|
||||
{
|
||||
earned = true;
|
||||
reward = new AdReward(r.Type, r.Amount);
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
using (cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken)))
|
||||
return await tcs.Task;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_rewarded != null)
|
||||
{
|
||||
_rewarded.OnAdFullScreenContentClosed -= onClosed;
|
||||
_rewarded.OnAdFullScreenContentFailed -= onFailed;
|
||||
}
|
||||
ScheduleReload(AdFormat.Rewarded);
|
||||
}
|
||||
}
|
||||
|
||||
private async UniTask<AdShowResult> ShowRewardedInterstitialAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var tcs = new UniTaskCompletionSource<AdShowResult>();
|
||||
bool earned = false;
|
||||
AdReward reward = default;
|
||||
|
||||
Action onClosed = () => tcs.TrySetResult(earned ? AdShowResult.WithReward(reward) : AdShowResult.Success());
|
||||
Action<AdError> onFailed = err => tcs.TrySetResult(AdShowResult.Failure(err?.GetMessage()));
|
||||
|
||||
_rewardedInterstitial.OnAdFullScreenContentClosed += onClosed;
|
||||
_rewardedInterstitial.OnAdFullScreenContentFailed += onFailed;
|
||||
_rewardedInterstitial.Show(r =>
|
||||
{
|
||||
earned = true;
|
||||
reward = new AdReward(r.Type, r.Amount);
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
using (cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken)))
|
||||
return await tcs.Task;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_rewardedInterstitial != null)
|
||||
{
|
||||
_rewardedInterstitial.OnAdFullScreenContentClosed -= onClosed;
|
||||
_rewardedInterstitial.OnAdFullScreenContentFailed -= onFailed;
|
||||
}
|
||||
ScheduleReload(AdFormat.RewardedInterstitial);
|
||||
}
|
||||
}
|
||||
|
||||
private async UniTask<AdShowResult> ShowAppOpenAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var tcs = new UniTaskCompletionSource<AdShowResult>();
|
||||
Action onClosed = () => tcs.TrySetResult(AdShowResult.Success());
|
||||
Action<AdError> onFailed = err => tcs.TrySetResult(AdShowResult.Failure(err?.GetMessage()));
|
||||
|
||||
_appOpen.OnAdFullScreenContentClosed += onClosed;
|
||||
_appOpen.OnAdFullScreenContentFailed += onFailed;
|
||||
_appOpen.Show();
|
||||
|
||||
try
|
||||
{
|
||||
using (cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken)))
|
||||
return await tcs.Task;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_appOpen != null)
|
||||
{
|
||||
_appOpen.OnAdFullScreenContentClosed -= onClosed;
|
||||
_appOpen.OnAdFullScreenContentFailed -= onFailed;
|
||||
}
|
||||
ScheduleReload(AdFormat.AppOpen);
|
||||
}
|
||||
}
|
||||
|
||||
private void WireFullScreenEvents(InterstitialAd ad, AdFormat format) =>
|
||||
ad.OnAdFullScreenContentClosed += () => SetState(format, AdLoadState.Idle);
|
||||
|
||||
private void WireFullScreenEvents(RewardedAd ad, AdFormat format) =>
|
||||
ad.OnAdFullScreenContentClosed += () => SetState(format, AdLoadState.Idle);
|
||||
|
||||
private void WireFullScreenEvents(RewardedInterstitialAd ad, AdFormat format) =>
|
||||
ad.OnAdFullScreenContentClosed += () => SetState(format, AdLoadState.Idle);
|
||||
|
||||
private void WireFullScreenEvents(AppOpenAd ad, AdFormat format) =>
|
||||
ad.OnAdFullScreenContentClosed += () => SetState(format, AdLoadState.Idle);
|
||||
|
||||
private void ScheduleReload(AdFormat format)
|
||||
{
|
||||
if (!autoReload || _lifetimeCts == null) return;
|
||||
ReloadAfterDelayAsync(format, _lifetimeCts.Token).Forget();
|
||||
}
|
||||
|
||||
private async UniTaskVoid ReloadAfterDelayAsync(AdFormat format, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await UniTask.Delay(TimeSpan.FromSeconds(reloadDelaySeconds), cancellationToken: cancellationToken);
|
||||
await LoadAsync(format, cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
}
|
||||
|
||||
private static AdSize MapBannerSize(BannerSize size) => size switch
|
||||
{
|
||||
BannerSize.Banner => AdSize.Banner,
|
||||
BannerSize.LargeBanner => AdSize.LargeBanner,
|
||||
BannerSize.MediumRectangle => AdSize.MediumRectangle,
|
||||
BannerSize.FullBanner => AdSize.IABBanner,
|
||||
BannerSize.Leaderboard => AdSize.Leaderboard,
|
||||
BannerSize.SmartBanner => AdSize.SmartBanner,
|
||||
BannerSize.AnchoredAdaptive => AdSize.GetCurrentOrientationAnchoredAdaptiveBannerAdSizeWithWidth(AdSize.FullWidth),
|
||||
_ => AdSize.Banner
|
||||
};
|
||||
|
||||
private static AdPosition MapBannerPosition(BannerPosition position) => position switch
|
||||
{
|
||||
BannerPosition.Top => AdPosition.Top,
|
||||
BannerPosition.Bottom => AdPosition.Bottom,
|
||||
BannerPosition.TopLeft => AdPosition.TopLeft,
|
||||
BannerPosition.TopRight => AdPosition.TopRight,
|
||||
BannerPosition.BottomLeft => AdPosition.BottomLeft,
|
||||
BannerPosition.BottomRight => AdPosition.BottomRight,
|
||||
BannerPosition.Center => AdPosition.Center,
|
||||
_ => AdPosition.Bottom
|
||||
};
|
||||
#endif
|
||||
|
||||
private void SetState(AdFormat format, AdLoadState state)
|
||||
{
|
||||
_states[format] = state;
|
||||
LoadStateChanged?.Invoke(format, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 018ba83ad51c4d9a89a904ba3d21ead2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -10,12 +10,24 @@ namespace Darkmatter.Services.Audio
|
||||
public class AudioServiceModule : MonoBehaviour, IModule
|
||||
{
|
||||
[SerializeField] private SfxCatalogSO sfxCatalog;
|
||||
[SerializeField] private AudioService audioService;
|
||||
|
||||
public void Register(IContainerBuilder builder)
|
||||
{
|
||||
if (sfxCatalog != null)
|
||||
builder.RegisterComponent(sfxCatalog);
|
||||
builder.Register<IAudioService, AudioService>(Lifetime.Singleton);
|
||||
if (sfxCatalog == null)
|
||||
{
|
||||
Debug.LogError("[AudioServiceModule] SfxCatalogSO not assigned.");
|
||||
return;
|
||||
}
|
||||
builder.RegisterComponent(sfxCatalog);
|
||||
|
||||
if (audioService == null)
|
||||
{
|
||||
Debug.LogError("[AudioServiceModule] AudioService not assigned. Assign the scene component to the 'audioService' field.");
|
||||
return;
|
||||
}
|
||||
builder.RegisterComponent<IAudioService>(audioService);
|
||||
|
||||
builder.Register<ISfxPlayer, SfxPlayer>(Lifetime.Singleton);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,8 +72,18 @@ namespace Darkmatter.Services.Audio
|
||||
private Dictionary<AudioChannel, AudioMixerGroup> _mixerGroups;
|
||||
private float[] _currentWeights;
|
||||
|
||||
private bool _initialized;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
InitializeAsync(default).Forget();
|
||||
}
|
||||
|
||||
public UniTask InitializeAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_initialized) return UniTask.CompletedTask;
|
||||
_initialized = true;
|
||||
|
||||
if (sourceRoot == null) sourceRoot = transform;
|
||||
_lifetimeCts = new CancellationTokenSource();
|
||||
|
||||
@@ -336,8 +346,11 @@ namespace Darkmatter.Services.Audio
|
||||
source.loop = request.PlayMode == AudioPlayMode.Loop;
|
||||
|
||||
source.transform.localPosition = Vector3.zero;
|
||||
source.enabled = true;
|
||||
if (!source.gameObject.activeInHierarchy) source.gameObject.SetActive(true);
|
||||
|
||||
source.Play();
|
||||
Debug.Log($"[AudioService] Play clip='{(request.Clip != null ? request.Clip.name : "null")}' ch={request.Channel} vol={source.volume:F2} mixer={(source.outputAudioMixerGroup != null ? source.outputAudioMixerGroup.name : "none")} listener={(AudioListener.volume)} muted={AudioListener.pause} isPlaying={source.isPlaying}");
|
||||
if (IsChannelPaused(request.Channel))
|
||||
{
|
||||
source.Pause();
|
||||
|
||||
@@ -1,73 +1,115 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using Darkmatter.Core.Contracts.Services.Camera;
|
||||
using Darkmatter.Core.Contracts.Services.Capture;
|
||||
using UnityEngine;
|
||||
using CameraType = Darkmatter.Core.Enums.Services.Camera.CameraType;
|
||||
|
||||
namespace Darkmatter.Services.Capture
|
||||
{
|
||||
public class CaptureService : ICaptureService
|
||||
{
|
||||
private readonly ICameraService _cameraService;
|
||||
|
||||
public CaptureService(ICameraService cameraService) => _cameraService = cameraService;
|
||||
|
||||
public async UniTask<byte[]> CapturePngAsync(GameObject captureObject, float scale,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (captureObject == null) return null;
|
||||
var paperRT = captureObject.transform as RectTransform;
|
||||
var paperCanvas = GetRootCanvas(captureObject);
|
||||
var cam = _cameraService.GetCamera(CameraType.CaptureCamera);
|
||||
if (paperCanvas == null || paperRT == null || cam == null) return null;
|
||||
|
||||
int sw = Screen.width;
|
||||
int sh = Screen.height;
|
||||
|
||||
var disabledCanvases = DisableOtherRootCanvases(paperCanvas);
|
||||
var cam = Camera.main;
|
||||
CameraClearFlags prevFlags = default;
|
||||
Color prevBg = default;
|
||||
if (cam != null)
|
||||
{
|
||||
prevFlags = cam.clearFlags;
|
||||
prevBg = cam.backgroundColor;
|
||||
cam.clearFlags = CameraClearFlags.SolidColor;
|
||||
cam.backgroundColor = new Color(0f, 0f, 0f, 0f);
|
||||
}
|
||||
var disabledGraphics = HideNonPaperGraphics(paperRT);
|
||||
|
||||
await UniTask.WaitForEndOfFrame(cancellationToken);
|
||||
Rect crop = ComputeCropRect(captureObject, sw, sh);
|
||||
int cropW = Mathf.Max(1, (int)crop.width);
|
||||
int cropH = Mathf.Max(1, (int)crop.height);
|
||||
|
||||
var prevFlags = cam.clearFlags;
|
||||
var prevBg = cam.backgroundColor;
|
||||
var prevTarget = cam.targetTexture;
|
||||
cam.clearFlags = CameraClearFlags.SolidColor;
|
||||
cam.backgroundColor = new Color(0f, 0f, 0f, 0f);
|
||||
|
||||
var rt = RenderTexture.GetTemporary(sw, sh, 24, RenderTextureFormat.ARGB32);
|
||||
cam.targetTexture = rt;
|
||||
|
||||
var prevMode = paperCanvas.renderMode;
|
||||
var prevWorldCam = paperCanvas.worldCamera;
|
||||
var prevPlaneDist = paperCanvas.planeDistance;
|
||||
paperCanvas.renderMode = RenderMode.ScreenSpaceCamera;
|
||||
paperCanvas.worldCamera = cam;
|
||||
paperCanvas.planeDistance = Mathf.Max(0.5f, (cam.nearClipPlane + cam.farClipPlane) * 0.5f);
|
||||
|
||||
var fullScreen = ScreenCapture.CaptureScreenshotAsTexture();
|
||||
try
|
||||
{
|
||||
Rect crop = ComputeCropRect(captureObject, fullScreen.width, fullScreen.height);
|
||||
int cropW = Mathf.Max(1, (int)crop.width);
|
||||
int cropH = Mathf.Max(1, (int)crop.height);
|
||||
await UniTask.WaitForEndOfFrame(cancellationToken);
|
||||
Canvas.ForceUpdateCanvases();
|
||||
cam.Render();
|
||||
|
||||
var prevActive = RenderTexture.active;
|
||||
RenderTexture.active = rt;
|
||||
var fullScreen = new Texture2D(sw, sh, TextureFormat.RGBA32, mipChain: false);
|
||||
fullScreen.ReadPixels(new Rect(0, 0, sw, sh), 0, 0);
|
||||
fullScreen.Apply();
|
||||
RenderTexture.active = prevActive;
|
||||
|
||||
var cropped = new Texture2D(cropW, cropH, TextureFormat.RGBA32, mipChain: false);
|
||||
try
|
||||
{
|
||||
cropped.SetPixels(fullScreen.GetPixels((int)crop.x, (int)crop.y, cropW, cropH));
|
||||
cropped.Apply();
|
||||
|
||||
int targetW = Mathf.Max(1, Mathf.RoundToInt(cropW * scale));
|
||||
int targetH = Mathf.Max(1, Mathf.RoundToInt(cropH * scale));
|
||||
Texture2D output = cropped;
|
||||
if (output.width != targetW || output.height != targetH)
|
||||
output = Resize(cropped, targetW, targetH);
|
||||
|
||||
var cropped = new Texture2D(cropW, cropH, TextureFormat.RGBA32, mipChain: false);
|
||||
try
|
||||
{
|
||||
return output.EncodeToPNG();
|
||||
cropped.SetPixels(fullScreen.GetPixels((int)crop.x, (int)crop.y, cropW, cropH));
|
||||
cropped.Apply();
|
||||
|
||||
int targetW = Mathf.Max(1, Mathf.RoundToInt(cropW * scale));
|
||||
int targetH = Mathf.Max(1, Mathf.RoundToInt(cropH * scale));
|
||||
Texture2D output = cropped;
|
||||
if (output.width != targetW || output.height != targetH)
|
||||
output = Resize(cropped, targetW, targetH);
|
||||
|
||||
try
|
||||
{
|
||||
return output.EncodeToPNG();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (output != cropped) Object.Destroy(output);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (output != cropped) Object.Destroy(output);
|
||||
Object.Destroy(cropped);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Object.Destroy(cropped);
|
||||
Object.Destroy(fullScreen);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Object.Destroy(fullScreen);
|
||||
foreach (var c in disabledCanvases) if (c != null) c.enabled = true;
|
||||
if (cam != null)
|
||||
{
|
||||
cam.clearFlags = prevFlags;
|
||||
cam.backgroundColor = prevBg;
|
||||
}
|
||||
paperCanvas.renderMode = prevMode;
|
||||
paperCanvas.worldCamera = prevWorldCam;
|
||||
paperCanvas.planeDistance = prevPlaneDist;
|
||||
cam.targetTexture = prevTarget;
|
||||
cam.clearFlags = prevFlags;
|
||||
cam.backgroundColor = prevBg;
|
||||
RenderTexture.ReleaseTemporary(rt);
|
||||
foreach (var g in disabledGraphics)
|
||||
if (g != null)
|
||||
g.enabled = true;
|
||||
foreach (var c in disabledCanvases)
|
||||
if (c != null)
|
||||
c.enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +130,23 @@ namespace Darkmatter.Services.Capture
|
||||
c.enabled = false;
|
||||
disabled.Add(c);
|
||||
}
|
||||
|
||||
return disabled;
|
||||
}
|
||||
|
||||
private static List<UnityEngine.UI.Graphic> HideNonPaperGraphics(Transform paper)
|
||||
{
|
||||
var disabled = new List<UnityEngine.UI.Graphic>();
|
||||
if (paper == null) return disabled;
|
||||
var all = Object.FindObjectsByType<UnityEngine.UI.Graphic>(FindObjectsSortMode.None);
|
||||
foreach (var g in all)
|
||||
{
|
||||
if (g == null || !g.enabled) continue;
|
||||
if (g.transform.IsChildOf(paper)) continue;
|
||||
g.enabled = false;
|
||||
disabled.Add(g);
|
||||
}
|
||||
|
||||
return disabled;
|
||||
}
|
||||
|
||||
@@ -98,7 +157,6 @@ namespace Darkmatter.Services.Capture
|
||||
|
||||
var corners = new Vector3[4];
|
||||
rt.GetWorldCorners(corners);
|
||||
// ScreenSpaceOverlay canvas: world corners are already in screen pixels.
|
||||
float minX = Mathf.Clamp(corners[0].x, 0, screenW);
|
||||
float minY = Mathf.Clamp(corners[0].y, 0, screenH);
|
||||
float maxX = Mathf.Clamp(corners[2].x, 0, screenW);
|
||||
|
||||
8
Assets/Darkmatter/Code/Services/Music.meta
Normal file
8
Assets/Darkmatter/Code/Services/Music.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 27c2e817c276c40b19f6284b7c87860c
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Darkmatter/Code/Services/Music/Installers.meta
Normal file
8
Assets/Darkmatter/Code/Services/Music/Installers.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e1d52cf6530064afd982535de65293bb
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,24 @@
|
||||
using Darkmatter.Core.Contracts.Services.Music;
|
||||
using Darkmatter.Libs.Installers;
|
||||
using Darkmatter.Services.Music.Systems;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Audio;
|
||||
using VContainer;
|
||||
using VContainer.Unity;
|
||||
|
||||
namespace Darkmatter.Services.Music.Installers
|
||||
{
|
||||
public class MusicServiceModule : MonoBehaviour, IModule
|
||||
{
|
||||
[SerializeField] private AudioClip defaultTrack;
|
||||
[SerializeField, Range(0f, 1f)] private float defaultVolume = 0.7f;
|
||||
[SerializeField, Min(0f)] private float crossFadeSeconds = 0.4f;
|
||||
[SerializeField] private AudioMixerGroup mixerGroup;
|
||||
|
||||
public void Register(IContainerBuilder builder)
|
||||
{
|
||||
builder.RegisterInstance(new MusicConfig(defaultTrack, defaultVolume, crossFadeSeconds, mixerGroup));
|
||||
builder.RegisterEntryPoint<MusicService>(Lifetime.Singleton).As<IMusicService>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 47cce31d8e31341a0ae46684e87cbd95
|
||||
8
Assets/Darkmatter/Code/Services/Music/Systems.meta
Normal file
8
Assets/Darkmatter/Code/Services/Music/Systems.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bec4745668fea4987809c8370d372048
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
23
Assets/Darkmatter/Code/Services/Music/Systems/MusicConfig.cs
Normal file
23
Assets/Darkmatter/Code/Services/Music/Systems/MusicConfig.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Audio;
|
||||
|
||||
namespace Darkmatter.Services.Music.Systems
|
||||
{
|
||||
[Serializable]
|
||||
public readonly struct MusicConfig
|
||||
{
|
||||
public AudioClip DefaultTrack { get; }
|
||||
public float DefaultVolume { get; }
|
||||
public float CrossFadeSeconds { get; }
|
||||
public AudioMixerGroup MixerGroup { get; }
|
||||
|
||||
public MusicConfig(AudioClip defaultTrack, float defaultVolume, float crossFadeSeconds, AudioMixerGroup mixerGroup)
|
||||
{
|
||||
DefaultTrack = defaultTrack;
|
||||
DefaultVolume = defaultVolume;
|
||||
CrossFadeSeconds = crossFadeSeconds;
|
||||
MixerGroup = mixerGroup;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4c15bf1ed26cf4c8f8339b22cb57efc6
|
||||
146
Assets/Darkmatter/Code/Services/Music/Systems/MusicService.cs
Normal file
146
Assets/Darkmatter/Code/Services/Music/Systems/MusicService.cs
Normal file
@@ -0,0 +1,146 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using Darkmatter.Core.Contracts.Services.Music;
|
||||
using Darkmatter.Core.Data.Signals.Features.AppBoot;
|
||||
using Darkmatter.Libs.Observer;
|
||||
using UnityEngine;
|
||||
using VContainer.Unity;
|
||||
|
||||
namespace Darkmatter.Services.Music.Systems
|
||||
{
|
||||
public class MusicService : IMusicService, IStartable, IDisposable
|
||||
{
|
||||
private readonly IEventBus _bus;
|
||||
private readonly MusicConfig _config;
|
||||
|
||||
private GameObject _host;
|
||||
private AudioSource _source;
|
||||
private IDisposable _introSub;
|
||||
private CancellationTokenSource _fadeCts;
|
||||
|
||||
public MusicService(IEventBus bus, MusicConfig config)
|
||||
{
|
||||
_bus = bus;
|
||||
_config = config;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
_host = new GameObject("[MusicService]");
|
||||
UnityEngine.Object.DontDestroyOnLoad(_host);
|
||||
_source = _host.AddComponent<AudioSource>();
|
||||
_source.playOnAwake = false;
|
||||
_source.loop = true;
|
||||
_source.spatialBlend = 0f;
|
||||
_source.volume = _config.DefaultVolume;
|
||||
if (_config.MixerGroup != null) _source.outputAudioMixerGroup = _config.MixerGroup;
|
||||
|
||||
_introSub = _bus.Subscribe<IntroCompletedSignal>(OnIntroCompleted);
|
||||
}
|
||||
|
||||
private void OnIntroCompleted(IntroCompletedSignal _)
|
||||
{
|
||||
if (_config.DefaultTrack != null && (_source == null || !_source.isPlaying))
|
||||
Play(_config.DefaultTrack, _config.DefaultVolume, loop: true);
|
||||
}
|
||||
|
||||
public void Play(AudioClip clip, float volume01 = 1f, bool loop = true)
|
||||
{
|
||||
if (clip == null || _source == null) return;
|
||||
CancelFade();
|
||||
|
||||
if (_source.isPlaying && _config.CrossFadeSeconds > 0f)
|
||||
{
|
||||
_fadeCts = new CancellationTokenSource();
|
||||
CrossFadeAsync(clip, Mathf.Clamp01(volume01), loop, _fadeCts.Token).Forget();
|
||||
return;
|
||||
}
|
||||
|
||||
_source.clip = clip;
|
||||
_source.loop = loop;
|
||||
_source.volume = Mathf.Clamp01(volume01);
|
||||
_source.Play();
|
||||
}
|
||||
|
||||
public void Stop(float fadeOutSeconds = 0f)
|
||||
{
|
||||
if (_source == null) return;
|
||||
CancelFade();
|
||||
if (fadeOutSeconds <= 0f) { _source.Stop(); return; }
|
||||
_fadeCts = new CancellationTokenSource();
|
||||
FadeOutAsync(fadeOutSeconds, _fadeCts.Token).Forget();
|
||||
}
|
||||
|
||||
public void SetVolume(float volume01)
|
||||
{
|
||||
if (_source == null) return;
|
||||
_source.volume = Mathf.Clamp01(volume01);
|
||||
}
|
||||
|
||||
private async UniTaskVoid CrossFadeAsync(AudioClip nextClip, float targetVolume, bool loop, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
float start = _source.volume;
|
||||
float t = 0f;
|
||||
float dur = _config.CrossFadeSeconds;
|
||||
while (t < dur)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
t += Time.unscaledDeltaTime;
|
||||
_source.volume = Mathf.Lerp(start, 0f, t / dur);
|
||||
await UniTask.Yield(PlayerLoopTiming.Update, ct);
|
||||
}
|
||||
_source.Stop();
|
||||
_source.clip = nextClip;
|
||||
_source.loop = loop;
|
||||
_source.volume = 0f;
|
||||
_source.Play();
|
||||
t = 0f;
|
||||
while (t < dur)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
t += Time.unscaledDeltaTime;
|
||||
_source.volume = Mathf.Lerp(0f, targetVolume, t / dur);
|
||||
await UniTask.Yield(PlayerLoopTiming.Update, ct);
|
||||
}
|
||||
_source.volume = targetVolume;
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
}
|
||||
|
||||
private async UniTaskVoid FadeOutAsync(float seconds, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
float start = _source.volume;
|
||||
float t = 0f;
|
||||
while (t < seconds)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
t += Time.unscaledDeltaTime;
|
||||
_source.volume = Mathf.Lerp(start, 0f, t / seconds);
|
||||
await UniTask.Yield(PlayerLoopTiming.Update, ct);
|
||||
}
|
||||
_source.Stop();
|
||||
_source.volume = start;
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
}
|
||||
|
||||
private void CancelFade()
|
||||
{
|
||||
_fadeCts?.Cancel();
|
||||
_fadeCts?.Dispose();
|
||||
_fadeCts = null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
CancelFade();
|
||||
_introSub?.Dispose();
|
||||
if (_host != null) UnityEngine.Object.Destroy(_host);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8b4c07c26f9a44409bd43d2777b9951d
|
||||
8
Assets/Darkmatter/Content/Audio.meta
Normal file
8
Assets/Darkmatter/Content/Audio.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 854202d005e754405862e01141384a81
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ed4a54b01b78fa1489c9baf442235e8a
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,47 @@
|
||||
Congrats! Now you have the FREE CASUAL GAME SFX
|
||||
This is the FREE SFX solucion for your casual games!
|
||||
*designed by Matheus Klein.
|
||||
|
||||
------>>>>> Do you want a GIFT? <<<<<------------
|
||||
|
||||
Subscribe here and download it:
|
||||
https://lp.labmdk.com/free-gift
|
||||
|
||||
------>>>>> <<<<<------------
|
||||
Leave for us your Review!
|
||||
|
||||
------>>>>> <<<<<------------
|
||||
Check It out:
|
||||
--> Casual-pack-SFX
|
||||
https://assetstore.unity.com/packages/audio/sound-fx/casual-pack-sfx-192848?_ga=2.42343461.1670194729.1622553216-1748727751.1620872230
|
||||
|
||||
--> Casual-pack- MUSIC
|
||||
https://assetstore.unity.com/packages/audio/music/casual-pack-music-193478?_ga=2.42343461.1670194729.1622553216-1748727751.1620872230
|
||||
|
||||
--> Casual-pack-COMPLETE
|
||||
https://assetstore.unity.com/packages/audio/music/casual-pack-complete-196775
|
||||
|
||||
------>>>>> <<<<<------------
|
||||
--> We created this product with enthusiasm and dedication. Enjoy it!
|
||||
|
||||
--> If you have any problem or if you are searching for customized assets
|
||||
feel free to contact us:
|
||||
|
||||
- www.labmdk.com
|
||||
- contato@labmdk.com
|
||||
|
||||
|
||||
-->For more, follow us:
|
||||
|
||||
@matheus_klein.mk
|
||||
@3dk_art
|
||||
|
||||
---> Looking for an original soundtrack for your custom project?
|
||||
What can I offer?
|
||||
|
||||
Composing exclusive music, soundtrack, filmscore for film, video game, trailers, apps and advertisement.
|
||||
Any music genre: cinematic, orchestral, Rock, metal, jazz, hip-hop, EDM, ambient, 8 bit etc.
|
||||
Realistic sound. I work with virtual instruments and some real instruments.
|
||||
|
||||
We're looking forward to working with you again.
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8dc69ef1d5b95dd48b0e5599b3de71d0
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 197054
|
||||
packageName: FREE CASUAL PACK SFX
|
||||
packageVersion: 2.0
|
||||
assetPath: Assets/1.1 - FREE CASUAL PACK SFX/(README) WELCOME_FREE CASUAL PACK
|
||||
SFX.txt
|
||||
uploadId: 550051
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9b5dca4b9b9435b468d27e1d658c8b7c
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,29 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cbd5ee476a488444aaeb06b7e1fb04e0
|
||||
AudioImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 6
|
||||
defaultSettings:
|
||||
loadType: 0
|
||||
sampleRateSetting: 0
|
||||
sampleRateOverride: 44100
|
||||
compressionFormat: 1
|
||||
quality: 1
|
||||
conversionMode: 0
|
||||
platformSettingOverrides: {}
|
||||
forceToMono: 0
|
||||
normalize: 1
|
||||
preloadAudioData: 1
|
||||
loadInBackground: 0
|
||||
ambisonic: 0
|
||||
3D: 1
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 197054
|
||||
packageName: FREE CASUAL PACK SFX
|
||||
packageVersion: 2.0
|
||||
assetPath: Assets/1.1 - FREE CASUAL PACK SFX/10 - BONUS/Casual loop 1.wav
|
||||
uploadId: 550051
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 003fad9ceeb31b44c9618baec1eed419
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,29 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 36a97d152a07e9f489c73b24e2870cf0
|
||||
AudioImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 6
|
||||
defaultSettings:
|
||||
loadType: 0
|
||||
sampleRateSetting: 0
|
||||
sampleRateOverride: 44100
|
||||
compressionFormat: 1
|
||||
quality: 1
|
||||
conversionMode: 0
|
||||
platformSettingOverrides: {}
|
||||
forceToMono: 0
|
||||
normalize: 1
|
||||
preloadAudioData: 1
|
||||
loadInBackground: 0
|
||||
ambisonic: 0
|
||||
3D: 1
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 197054
|
||||
packageName: FREE CASUAL PACK SFX
|
||||
packageVersion: 2.0
|
||||
assetPath: Assets/1.1 - FREE CASUAL PACK SFX/CAMERA CLICKS/CAMERA CLICK 1.wav
|
||||
uploadId: 550051
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 394f992e39409a246ad69e88caf01d6c
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,29 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4b5dbd3527221224caf2fde9d72cddb7
|
||||
AudioImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 6
|
||||
defaultSettings:
|
||||
loadType: 0
|
||||
sampleRateSetting: 0
|
||||
sampleRateOverride: 44100
|
||||
compressionFormat: 1
|
||||
quality: 1
|
||||
conversionMode: 0
|
||||
platformSettingOverrides: {}
|
||||
forceToMono: 0
|
||||
normalize: 1
|
||||
preloadAudioData: 1
|
||||
loadInBackground: 0
|
||||
ambisonic: 0
|
||||
3D: 1
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 197054
|
||||
packageName: FREE CASUAL PACK SFX
|
||||
packageVersion: 2.0
|
||||
assetPath: Assets/1.1 - FREE CASUAL PACK SFX/CARDS/CARDS 1.wav
|
||||
uploadId: 550051
|
||||
Binary file not shown.
@@ -0,0 +1,29 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4f3fe7a1f92f1e94db61865f48491057
|
||||
AudioImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 6
|
||||
defaultSettings:
|
||||
loadType: 0
|
||||
sampleRateSetting: 0
|
||||
sampleRateOverride: 44100
|
||||
compressionFormat: 1
|
||||
quality: 1
|
||||
conversionMode: 0
|
||||
platformSettingOverrides: {}
|
||||
forceToMono: 0
|
||||
normalize: 1
|
||||
preloadAudioData: 1
|
||||
loadInBackground: 0
|
||||
ambisonic: 0
|
||||
3D: 1
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 197054
|
||||
packageName: FREE CASUAL PACK SFX
|
||||
packageVersion: 2.0
|
||||
assetPath: Assets/1.1 - FREE CASUAL PACK SFX/CARDS/CARDS 2.wav
|
||||
uploadId: 550051
|
||||
Binary file not shown.
@@ -0,0 +1,14 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2a78ea6479568174996fe4ab6d489c16
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 197054
|
||||
packageName: FREE CASUAL PACK SFX
|
||||
packageVersion: 2.0
|
||||
assetPath: Assets/1.1 - FREE CASUAL PACK SFX/CASUAL GAME SFX FREE_2.0.pdf
|
||||
uploadId: 550051
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 77d40c2b43ea7704dad6d8c061256ca2
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user