Merge remote-tracking branch 'origin/savya' into work_branch
This commit is contained in:
BIN
Assets/.DS_Store
vendored
BIN
Assets/.DS_Store
vendored
Binary file not shown.
@@ -15,7 +15,7 @@ MonoBehaviour:
|
||||
m_DefaultGroup: 0e030d5498bfe4ffd8443c796618c539
|
||||
m_currentHash:
|
||||
serializedVersion: 2
|
||||
Hash: 00000000000000000000000000000000
|
||||
Hash: f172661451d53007cd560d2db7f013f5
|
||||
m_OptimizeCatalogSize: 0
|
||||
m_BuildRemoteCatalog: 0
|
||||
m_CatalogRequestsTimeout: 0
|
||||
|
||||
BIN
Assets/Darkmatter/.DS_Store
vendored
BIN
Assets/Darkmatter/.DS_Store
vendored
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 224a4c0e39279476aa91487a2572bbdd
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,23 @@
|
||||
using System.Threading;
|
||||
using Cysharp.Threading.Tasks;
|
||||
|
||||
namespace Darkmatter.Core.Contracts.Features.SaveGate
|
||||
{
|
||||
/// <summary>
|
||||
/// Cross-scene gate in front of "save to gallery": shows a kid-friendly prompt and, on confirm,
|
||||
/// a rewarded ad. Root-scoped so any feature in any scene (capture, art book, …) can reuse it.
|
||||
/// </summary>
|
||||
public interface IRewardedSaveGate
|
||||
{
|
||||
/// <summary>
|
||||
/// Shows the prompt and (on Watch) a rewarded ad. Returns true if the caller should save.
|
||||
/// </summary>
|
||||
UniTask<bool> RequestSaveAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Briefly shows the shared "Saved to gallery!" confirmation. For callers without their own
|
||||
/// success popup (e.g. the art book). No-op if the overlay has no success panel wired.
|
||||
/// </summary>
|
||||
UniTask ShowSavedAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c8ca87e84abf64a0eb0c32de824318b6
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Threading;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using Darkmatter.Core.Enums.Features.Tutorial;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Darkmatter.Core.Contracts.Features.Tutorial
|
||||
@@ -18,19 +19,23 @@ namespace Darkmatter.Core.Contracts.Features.Tutorial
|
||||
/// Spotlight a single tap target. When <paramref name="target"/> is null the dim/hole are
|
||||
/// skipped and only a centered bubble is shown (a non-blocking hint).
|
||||
/// When <paramref name="blockInput"/> is true every touch outside the hole is swallowed.
|
||||
/// <paramref name="placement"/> overrides where the instruction bubble sits; <paramref name="offset"/>
|
||||
/// nudges it from that slot (px, +x right / +y up).
|
||||
/// </summary>
|
||||
void ShowTap(RectTransform target, string message, bool blockInput);
|
||||
void ShowTap(RectTransform target, string message, bool blockInput, BubblePlacement placement = BubblePlacement.Auto, Vector2 offset = default);
|
||||
|
||||
/// <summary>
|
||||
/// Spotlight a drag gesture from <paramref name="from"/> to <paramref name="to"/>. Input is
|
||||
/// never blocked here, so the dragged piece can render above the dim.
|
||||
/// <paramref name="placement"/> overrides where the instruction bubble sits; <paramref name="offset"/>
|
||||
/// nudges it from that slot (px, +x right / +y up).
|
||||
/// </summary>
|
||||
void ShowDrag(RectTransform from, RectTransform to, string message);
|
||||
void ShowDrag(RectTransform from, RectTransform to, string message, BubblePlacement placement = BubblePlacement.Auto, Vector2 offset = default);
|
||||
|
||||
/// <summary>Hide everything immediately (used across scene swaps).</summary>
|
||||
void HideInstant();
|
||||
|
||||
/// <summary>Show a centered celebratory bubble for a short beat, then hide.</summary>
|
||||
UniTask ShowToastAsync(string message, CancellationToken ct);
|
||||
UniTask ShowToastAsync(string message, CancellationToken ct, BubblePlacement placement = BubblePlacement.Auto, Vector2 offset = default);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
namespace Darkmatter.Core.Contracts.Services.Analytics
|
||||
{
|
||||
/// <summary>
|
||||
/// Canonical analytics event names. Use these constants everywhere — never raw strings — so the
|
||||
/// Firebase and Facebook sinks stay identical and typo-free. GA4 limit: 500 distinct event names.
|
||||
/// Names are snake_case and <= 40 chars (Facebook App Events limit). Lives in Core so any feature
|
||||
/// or service can reference them alongside <see cref="IAnalyticsService"/>.
|
||||
/// </summary>
|
||||
public static class AnalyticsEvents
|
||||
{
|
||||
// Onboarding / activation funnel
|
||||
public const string IntroStarted = "intro_started";
|
||||
public const string IntroCompleted = "intro_completed";
|
||||
public const string TutorialStarted = "tutorial_started";
|
||||
public const string TutorialStepCompleted = "tutorial_step_completed";
|
||||
public const string TutorialCompleted = "tutorial_completed";
|
||||
public const string PlayClicked = "play_clicked";
|
||||
public const string FirstDrawingStarted = "first_drawing_started";
|
||||
|
||||
// Navigation
|
||||
public const string ColorbookOpened = "colorbook_opened";
|
||||
public const string ArtbookOpened = "artbook_opened";
|
||||
public const string MainMenuReturned = "main_menu_returned";
|
||||
|
||||
// Core gameplay loop
|
||||
public const string DrawingSelected = "drawing_selected";
|
||||
public const string DrawingStarted = "drawing_started";
|
||||
public const string DrawingCompleted = "drawing_completed";
|
||||
public const string DrawingAbandoned = "drawing_abandoned";
|
||||
public const string DrawingSaved = "drawing_saved";
|
||||
public const string ShapeBuilderStarted = "shape_builder_started";
|
||||
public const string ShapeAssembled = "shape_assembled";
|
||||
public const string ColorApplied = "color_applied";
|
||||
|
||||
// Progression & content (GA4 recommended level_start / level_complete)
|
||||
public const string LevelStart = "level_start";
|
||||
public const string LevelComplete = "level_complete";
|
||||
public const string AllContentCompleted = "all_content_completed";
|
||||
|
||||
// Gallery capture (pre-existing)
|
||||
public const string GallerySaveStarted = "gallery_save_started";
|
||||
public const string GallerySaveCompleted = "gallery_save_completed";
|
||||
|
||||
// Monetization (GA4 recommended ad_impression)
|
||||
public const string AdImpression = "ad_impression";
|
||||
public const string AdClicked = "ad_clicked";
|
||||
|
||||
// Friction
|
||||
public const string ErrorShown = "error_shown";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 44ad2e947d64f40a38febad220fa92a1
|
||||
@@ -0,0 +1,39 @@
|
||||
namespace Darkmatter.Core.Contracts.Services.Analytics
|
||||
{
|
||||
/// <summary>
|
||||
/// Canonical analytics parameter keys. Pass a consistent set on every gameplay event so reports can
|
||||
/// answer "which drawing did people quit on", not just "how many quit". Each key used in reports must
|
||||
/// be registered as a Custom Dimension in the Firebase console. GA4 limit: 25 params per event.
|
||||
/// GA4-recommended events (level_start/level_complete/ad_impression) reuse GA4's reserved param names
|
||||
/// (level_name, success, value, currency, ad_platform, ad_format, ad_unit_name) so they're recognised.
|
||||
/// </summary>
|
||||
public static class AnalyticsParams
|
||||
{
|
||||
// Content identity
|
||||
public const string DrawingId = "drawing_id";
|
||||
public const string LevelName = "level_name"; // GA4 reserved (level_start/level_complete)
|
||||
|
||||
// Tutorial
|
||||
public const string StepId = "step_id";
|
||||
public const string StepIndex = "step_index";
|
||||
|
||||
// Gameplay metrics
|
||||
public const string DurationSeconds = "duration_seconds";
|
||||
public const string ColorsUsed = "colors_used";
|
||||
public const string Attempts = "attempts";
|
||||
public const string ApplyIndex = "apply_index";
|
||||
public const string CompletionCount = "completion_count";
|
||||
public const string Success = "success"; // GA4 reserved (level_complete)
|
||||
|
||||
// Monetization (GA4 ad_impression reserved names)
|
||||
public const string Value = "value";
|
||||
public const string Currency = "currency";
|
||||
public const string Precision = "precision";
|
||||
public const string AdPlatform = "ad_platform";
|
||||
public const string AdFormat = "ad_format";
|
||||
public const string AdUnitName = "ad_unit_name";
|
||||
|
||||
// Friction
|
||||
public const string ErrorType = "error_type";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3a7d4e7d6ad7e4ba39f557ddc970890c
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Darkmatter.Core.Data.Signals.Features.AppBoot;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the intro/logo video begins playing at app boot. Paired with
|
||||
/// <see cref="IntroCompletedSignal"/> to bound the top of the activation funnel.
|
||||
/// </summary>
|
||||
public record struct IntroStartedSignal();
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ebc4c1a0dab8d4b4fb95428c30f57f35
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Darkmatter.Core.Data.Signals.Features.Coloring
|
||||
{
|
||||
/// <summary>
|
||||
/// Raised the moment every region of the current drawing has been painted away from its authored
|
||||
/// (uncoloured) default — i.e. the picture is fully coloured. Published right after the
|
||||
/// <see cref="ColorAppliedSignal"/> that completes it. Used by the onboarding tutorial to hold the
|
||||
/// "Tap Next" prompt until the child has coloured the whole picture.
|
||||
/// </summary>
|
||||
public record struct AllRegionsColoredSignal;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 381ea33520a140d8b5639279c5390d2c
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Darkmatter.Core.Data.Signals.Features.GameplayFlow
|
||||
{
|
||||
/// <summary>
|
||||
/// Raised once the player has completed every drawing in the catalog (no content left). Published by
|
||||
/// the gameplay flow, which owns the catalog + progression; analytics dedupes it to once-ever.
|
||||
/// </summary>
|
||||
public record struct AllContentCompletedSignal(int CompletedCount);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3dc3c138deba5452ebb0d84ad3d4bf44
|
||||
@@ -6,9 +6,13 @@ namespace Darkmatter.Core.Data.Static.Features.ShapeBuilder
|
||||
menuName = "Darkmatter/ShapeBuilder/Config")]
|
||||
public sealed class ShapeBuilderConfig : ScriptableObject
|
||||
{
|
||||
[Header("Radii (canvas units; reference resolution 2048x2048)")]
|
||||
[SerializeField] private float snapRadius = 100f;
|
||||
[SerializeField] private float previewRadius = 200f;
|
||||
// Single catch radius for both preview and snap, expressed as a multiple of
|
||||
// EACH slot's own half-diagonal — so a big slot gets a big catch and a small
|
||||
// slot a small one, instead of one flat distance for all. 1 = exactly the
|
||||
// slot's circumscribed circle; larger = more forgiving. Computed in canvas
|
||||
// reference units, so it feels the same on every screen resolution/aspect.
|
||||
[Header("Catch radius (multiple of slot half-diagonal)")]
|
||||
[SerializeField, Range(0.25f, 2.5f)] private float catchRadiusScale = 1.1f;
|
||||
|
||||
[Header("Tween durations (seconds)")]
|
||||
[SerializeField] private float snapDuration = 0.25f;
|
||||
@@ -18,16 +22,11 @@ namespace Darkmatter.Core.Data.Static.Features.ShapeBuilder
|
||||
[SerializeField, Range(1f, 2f)] private float dragScale = 1.15f;
|
||||
[SerializeField, Range(0f, 1f)] private float dragAlpha = 0.7f;
|
||||
|
||||
[Header("Preview easing")]
|
||||
[SerializeField] private AnimationCurve previewCurve = AnimationCurve.EaseInOut(0, 0, 1, 1);
|
||||
|
||||
public float SnapRadius => snapRadius;
|
||||
public float PreviewRadius => previewRadius;
|
||||
public float CatchRadiusScale => catchRadiusScale;
|
||||
public float SnapDuration => snapDuration;
|
||||
public float ReturnDuration => returnDuration;
|
||||
public float DragScale => dragScale;
|
||||
public float DragAlpha => dragAlpha;
|
||||
public AnimationCurve PreviewCurve => previewCurve;
|
||||
|
||||
public Vector2 DragSizeDelta(ShapeSO shape) =>
|
||||
shape != null ? shape.DefaultSizeDelta : new Vector2(256, 256);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Darkmatter.Core.Enums.Features.Tutorial;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Darkmatter.Core.Data.Static.Features.Tutorial
|
||||
@@ -17,9 +18,28 @@ namespace Darkmatter.Core.Data.Static.Features.Tutorial
|
||||
[SerializeField] private string finishText = "Now finish the puzzle!";
|
||||
[SerializeField] private string colorText = "Choose a color!";
|
||||
[SerializeField] private string paintText = "Tap the picture to color it!";
|
||||
[SerializeField] private string finishColoringText = "Color the whole picture!";
|
||||
[SerializeField] private string nextText = "Tap Next when you're done!";
|
||||
[SerializeField] private string doneText = "Yay! You did it!";
|
||||
|
||||
[Header("Bubble placement + offset per step (Auto = smart; offset nudges from the slot in px, +x right / +y up)")]
|
||||
[SerializeField] private BubblePlacement pickBubble = BubblePlacement.Auto;
|
||||
[SerializeField] private Vector2 pickBubbleOffset;
|
||||
[SerializeField] private BubblePlacement dragBubble = BubblePlacement.Auto;
|
||||
[SerializeField] private Vector2 dragBubbleOffset;
|
||||
[SerializeField] private BubblePlacement finishBubble = BubblePlacement.Auto;
|
||||
[SerializeField] private Vector2 finishBubbleOffset;
|
||||
[SerializeField] private BubblePlacement colorBubble = BubblePlacement.Auto;
|
||||
[SerializeField] private Vector2 colorBubbleOffset;
|
||||
[SerializeField] private BubblePlacement paintBubble = BubblePlacement.Auto;
|
||||
[SerializeField] private Vector2 paintBubbleOffset;
|
||||
[SerializeField] private BubblePlacement finishColoringBubble = BubblePlacement.Auto;
|
||||
[SerializeField] private Vector2 finishColoringBubbleOffset;
|
||||
[SerializeField] private BubblePlacement nextBubble = BubblePlacement.Auto;
|
||||
[SerializeField] private Vector2 nextBubbleOffset;
|
||||
[SerializeField] private BubblePlacement doneBubble = BubblePlacement.Auto;
|
||||
[SerializeField] private Vector2 doneBubbleOffset;
|
||||
|
||||
[Header("Watchdog")]
|
||||
[Tooltip("Timeout (s) while waiting for a SYSTEM precondition (scene/catalog/regions ready). If it " +
|
||||
"doesn't arrive the tutorial fails open so the player is never trapped.")]
|
||||
@@ -34,9 +54,28 @@ namespace Darkmatter.Core.Data.Static.Features.Tutorial
|
||||
public string FinishText => finishText;
|
||||
public string ColorText => colorText;
|
||||
public string PaintText => paintText;
|
||||
public string FinishColoringText => finishColoringText;
|
||||
public string NextText => nextText;
|
||||
public string DoneText => doneText;
|
||||
|
||||
public BubblePlacement PickBubble => pickBubble;
|
||||
public BubblePlacement DragBubble => dragBubble;
|
||||
public BubblePlacement FinishBubble => finishBubble;
|
||||
public BubblePlacement ColorBubble => colorBubble;
|
||||
public BubblePlacement PaintBubble => paintBubble;
|
||||
public BubblePlacement FinishColoringBubble => finishColoringBubble;
|
||||
public BubblePlacement NextBubble => nextBubble;
|
||||
public BubblePlacement DoneBubble => doneBubble;
|
||||
|
||||
public Vector2 PickBubbleOffset => pickBubbleOffset;
|
||||
public Vector2 DragBubbleOffset => dragBubbleOffset;
|
||||
public Vector2 FinishBubbleOffset => finishBubbleOffset;
|
||||
public Vector2 ColorBubbleOffset => colorBubbleOffset;
|
||||
public Vector2 PaintBubbleOffset => paintBubbleOffset;
|
||||
public Vector2 FinishColoringBubbleOffset => finishColoringBubbleOffset;
|
||||
public Vector2 NextBubbleOffset => nextBubbleOffset;
|
||||
public Vector2 DoneBubbleOffset => doneBubbleOffset;
|
||||
|
||||
public float StepTimeoutSeconds => stepTimeoutSeconds;
|
||||
public float ActionTimeoutSeconds => actionTimeoutSeconds;
|
||||
}
|
||||
|
||||
8
Assets/Darkmatter/Code/Core/Enums/Features/Tutorial.meta
Normal file
8
Assets/Darkmatter/Code/Core/Enums/Features/Tutorial.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4b28ea43ea1b942cab1264b52ec34a60
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace Darkmatter.Core.Enums.Features.Tutorial
|
||||
{
|
||||
/// <summary>
|
||||
/// Where the tutorial instruction bubble sits for a given step. <see cref="Auto"/> keeps the
|
||||
/// smart default (sits opposite the spotlighted target so it never covers it; pinned to a screen
|
||||
/// edge for the drag step; upper-centre for target-less hints). The other values override that
|
||||
/// with a fixed vertical slot, horizontally centred.
|
||||
/// </summary>
|
||||
public enum BubblePlacement
|
||||
{
|
||||
Auto,
|
||||
Top,
|
||||
Center,
|
||||
Bottom,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 61e68e58d45cf4314886e2a414e3856d
|
||||
@@ -44,6 +44,7 @@ namespace Darkmatter.Features.AppBoot.Flow
|
||||
|
||||
player.loopPointReached += OnDone;
|
||||
player.Play();
|
||||
_eventBus.Publish(new IntroStartedSignal());
|
||||
await _sceneService.LoadSceneAsync(nameof(GameScene.MainMenu), null, cancellation);
|
||||
|
||||
await tcs.Task.AttachExternalCancellation(cancellation);
|
||||
|
||||
@@ -5,6 +5,7 @@ using Cysharp.Threading.Tasks;
|
||||
using Darkmatter.Core;
|
||||
using Darkmatter.Core.Contracts.Features.DrawingCatalog;
|
||||
using Darkmatter.Core.Contracts.Features.Progression;
|
||||
using Darkmatter.Core.Contracts.Features.SaveGate;
|
||||
using Darkmatter.Core.Contracts.Services.Gallery;
|
||||
using Darkmatter.Core.Data.Signals.Features.Drawing;
|
||||
using Darkmatter.Libs.Observer;
|
||||
@@ -22,6 +23,7 @@ namespace Darkmatter.Features.Artbook
|
||||
private readonly IProgressionSystem _progression;
|
||||
private readonly IDrawingTemplateCatalog _catalog;
|
||||
private readonly IGalleryService _gallery;
|
||||
private readonly IRewardedSaveGate _saveGate;
|
||||
|
||||
private readonly List<ArtbookEntry> _entries = new();
|
||||
private readonly List<Sprite> _ownedSprites = new();
|
||||
@@ -36,13 +38,15 @@ namespace Darkmatter.Features.Artbook
|
||||
IEventBus eventBus,
|
||||
IProgressionSystem progression,
|
||||
IDrawingTemplateCatalog catalog,
|
||||
IGalleryService gallery)
|
||||
IGalleryService gallery,
|
||||
IRewardedSaveGate saveGate)
|
||||
{
|
||||
_view = view;
|
||||
_eventBus = eventBus;
|
||||
_progression = progression;
|
||||
_catalog = catalog;
|
||||
_gallery = gallery;
|
||||
_saveGate = saveGate;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
@@ -151,7 +155,11 @@ namespace Darkmatter.Features.Artbook
|
||||
{
|
||||
if (!entry.HasValue || entry.Value.Thumbnail == null) return;
|
||||
var ct = _cts?.Token ?? CancellationToken.None;
|
||||
// Same kid-friendly prompt + rewarded ad as the gameplay save button.
|
||||
if (!await _saveGate.RequestSaveAsync(ct)) return;
|
||||
await _gallery.SaveImageAsync(entry.Value.Thumbnail, entry.Value.Name, ct);
|
||||
// The art book has no success popup of its own, so use the shared toast.
|
||||
await _saveGate.ShowSavedAsync(ct);
|
||||
}
|
||||
|
||||
private void HandleLeftEditClicked() => OpenForEdit(GetLeftEntry());
|
||||
|
||||
@@ -18,6 +18,7 @@ namespace Darkmatter.Features.Capture
|
||||
builder.RegisterInstance(new CaptureConfig(captureScale));
|
||||
builder.Register<ICaptureFeature, CaptureSystem>(Lifetime.Singleton);
|
||||
|
||||
// IRewardedSaveGate is resolved from the root scope (SaveGateModule in Boot).
|
||||
if (captureButtonView != null)
|
||||
builder.RegisterEntryPoint<CaptureButtonPresenter>().WithParameter(captureButtonView);
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using Darkmatter.Core.Contracts.Features.Capture;
|
||||
using Darkmatter.Core.Contracts.Features.SaveGate;
|
||||
using VContainer.Unity;
|
||||
|
||||
namespace Darkmatter.Features.Capture.UI
|
||||
@@ -9,15 +11,20 @@ namespace Darkmatter.Features.Capture.UI
|
||||
{
|
||||
private readonly CaptureButtonView _view;
|
||||
private readonly ICaptureFeature _capture;
|
||||
private readonly IRewardedSaveGate _saveGate;
|
||||
|
||||
public CaptureButtonPresenter(CaptureButtonView view, ICaptureFeature capture)
|
||||
private CancellationTokenSource _cts;
|
||||
|
||||
public CaptureButtonPresenter(CaptureButtonView view, ICaptureFeature capture, IRewardedSaveGate saveGate)
|
||||
{
|
||||
_view = view;
|
||||
_capture = capture;
|
||||
_saveGate = saveGate;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
_cts = new CancellationTokenSource();
|
||||
_view.OnCaptureClicked += HandleCaptureClicked;
|
||||
}
|
||||
|
||||
@@ -25,11 +32,16 @@ namespace Darkmatter.Features.Capture.UI
|
||||
|
||||
private async UniTaskVoid CaptureAsync()
|
||||
{
|
||||
var ct = _cts?.Token ?? CancellationToken.None;
|
||||
_view.SetInteractable(false);
|
||||
try
|
||||
{
|
||||
await _capture.CapturePngAsync(saveToGallery: true);
|
||||
// Kid-friendly prompt + rewarded ad (shared, cross-scene gate). The save then
|
||||
// publishes the existing signals that drive the "Saved to gallery!" popup.
|
||||
if (await _saveGate.RequestSaveAsync(ct))
|
||||
await _capture.CapturePngAsync(saveToGallery: true, ct);
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
finally
|
||||
{
|
||||
_view.SetInteractable(true);
|
||||
@@ -39,6 +51,9 @@ namespace Darkmatter.Features.Capture.UI
|
||||
public void Dispose()
|
||||
{
|
||||
_view.OnCaptureClicked -= HandleCaptureClicked;
|
||||
_cts?.Cancel();
|
||||
_cts?.Dispose();
|
||||
_cts = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ public class ColorbookFlowController : IAsyncStartable, IDisposable
|
||||
private IDisposable _selectedSub;
|
||||
private IDisposable _returnToMainMenuSubscription;
|
||||
private bool _navigatingToGameplay;
|
||||
private int _selectCount;
|
||||
private CancellationTokenSource _scopeCts;
|
||||
|
||||
public ColorbookFlowController(
|
||||
@@ -110,9 +111,14 @@ public class ColorbookFlowController : IAsyncStartable, IDisposable
|
||||
_loadingScreen.Show();
|
||||
_loadingScreen.SetProgress(0f);
|
||||
|
||||
// Fire the interstitial but never await it: the ad overlays the transition while the level
|
||||
// loads underneath, so a missed/dropped ad callback can't stall the flow at 0% anymore.
|
||||
ShowInterstitialAdAsync(ct).Forget();
|
||||
// Frequency cap: show an interstitial on every 2nd level open (2nd, 4th, ...). On skip turns
|
||||
// just keep one prewarmed so the next show has an ad ready. Fire-and-forget either way — the
|
||||
// ad overlays the transition while the level loads underneath, so a missed/dropped ad callback
|
||||
// can't stall the flow at 0% anymore.
|
||||
if (++_selectCount % 2 == 0)
|
||||
ShowInterstitialAdAsync(ct).Forget();
|
||||
else
|
||||
PrewarmInterstitialAdAsync(ct).Forget();
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
@@ -79,6 +79,21 @@ public class ColoringController : IColoringController, IDisposable
|
||||
if (from != color)
|
||||
_history.Push(new ColorRegionCommand(region, from, color));
|
||||
_bus.Publish(new ColorAppliedSignal(regionId, color));
|
||||
if (AllRegionsColored())
|
||||
_bus.Publish(new AllRegionsColoredSignal());
|
||||
}
|
||||
|
||||
// True once every region has been painted away from its authored (uncoloured) default.
|
||||
private bool AllRegionsColored()
|
||||
{
|
||||
if (_regions.Count == 0) return false;
|
||||
foreach (var region in _regions)
|
||||
{
|
||||
if (region == null) continue;
|
||||
if (_authoredColors.TryGetValue(region.RegionId, out var authored) && region.Color == authored)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<string, Color> GetCurrentColors()
|
||||
|
||||
@@ -25,10 +25,16 @@ namespace Darkmatter.Features.Coloring.UI
|
||||
rt.localScale = startScale;
|
||||
rt.localRotation = Quaternion.identity;
|
||||
await Tween.Scale(rt, endScale, duration, ease).ToUniTask(cancellationToken: ct);
|
||||
// The view can be destroyed mid-animation (flow advances, Back/Clear, scene teardown).
|
||||
// PrimeTween auto-stops the tween on target death and ToUniTask resolves normally, so
|
||||
// re-check the (Unity fake-null) transform before touching it again — otherwise the
|
||||
// localRotation write below hits a freed native object and throws NullReferenceException.
|
||||
if (rt == null) return;
|
||||
|
||||
await Tween.LocalRotation(rt, new Vector3(0f, 0f, wiggleAngle), wiggleDuration, Ease.InOutSine,
|
||||
cycles: wiggleCycles * 2, cycleMode: CycleMode.Yoyo)
|
||||
.ToUniTask(cancellationToken: ct);
|
||||
if (rt == null) return;
|
||||
rt.localRotation = Quaternion.identity;
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ namespace Darkmatter.Features.GameplayFlow.Systems
|
||||
private IDrawingTemplate _template;
|
||||
private string _templateId;
|
||||
private DrawingPhase _phase = DrawingPhase.ShapeBuilding;
|
||||
private bool _allContentReported;
|
||||
|
||||
private IDisposable _assembledSub;
|
||||
private IDisposable _colorAppliedSub;
|
||||
@@ -151,6 +152,16 @@ namespace Darkmatter.Features.GameplayFlow.Systems
|
||||
var progressAfter = _progression.GetProgress(_templateId);
|
||||
_bus.Publish(new DrawingCompletedSignal(_templateId, progressAfter?.completionCount ?? 1));
|
||||
|
||||
// Player has cleared the whole catalog → surface the "ran out of content" milestone. Guarded
|
||||
// per session here; analytics dedupes it to once-ever.
|
||||
if (!_allContentReported &&
|
||||
_catalog.AllTemplateIds.Count > 0 &&
|
||||
_progression.CompletedTemplateIds.Count >= _catalog.AllTemplateIds.Count)
|
||||
{
|
||||
_allContentReported = true;
|
||||
_bus.Publish(new AllContentCompletedSignal(_progression.CompletedTemplateIds.Count));
|
||||
}
|
||||
|
||||
var nextId = _catalog.GetNextTemplate(_templateId);
|
||||
if (string.IsNullOrEmpty(nextId))
|
||||
{
|
||||
|
||||
8
Assets/Darkmatter/Code/Features/SaveGate.meta
Normal file
8
Assets/Darkmatter/Code/Features/SaveGate.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e6434fcc29f4e4949be4be8115fda2b9
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "Features.SaveGate",
|
||||
"rootNamespace": "Darkmatter.Features.SaveGate",
|
||||
"references": [
|
||||
"GUID:6a0a834eb41764f12ba55c3fb04a40cb",
|
||||
"GUID:c1c03c0e5b2f4412b9f2be1c20d6a9b1",
|
||||
"GUID:b0214a6008ed146ff8f122a6a9c2f6cc",
|
||||
"GUID:f51ebe6a0ceec4240a699833d6309b23",
|
||||
"GUID:6055be8ebefd69e48b49212b09b47b2f"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 48cf1e0dac50c4952b2f27e17e8a065a
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Darkmatter/Code/Features/SaveGate/Installers.meta
Normal file
8
Assets/Darkmatter/Code/Features/SaveGate/Installers.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8f38ec6a6d3544172b3fdf20e8d31800
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,31 @@
|
||||
using Darkmatter.Features.SaveGate.Systems;
|
||||
using Darkmatter.Features.SaveGate.UI;
|
||||
using Darkmatter.Libs.Installers;
|
||||
using UnityEngine;
|
||||
using VContainer;
|
||||
using VContainer.Unity;
|
||||
|
||||
namespace Darkmatter.Features.SaveGate
|
||||
{
|
||||
/// <summary>
|
||||
/// Root module (add to Boot's RootLifetimeScope service modules) so the gate + overlay are
|
||||
/// resolvable and persistent across every scene. If no overlay is assigned it spawns a
|
||||
/// self-building one at runtime — no scene/prefab setup required.
|
||||
/// </summary>
|
||||
public class SaveGateModule : MonoBehaviour, IModule
|
||||
{
|
||||
[SerializeField] private SaveGalleryOverlayView overlayView;
|
||||
[SerializeField, Tooltip("Save anyway when the rewarded ad has no fill or fails to show.")]
|
||||
private bool saveWithoutAdOnFailure = true;
|
||||
|
||||
public void Register(IContainerBuilder builder)
|
||||
{
|
||||
|
||||
// Explicit type so a null overlay still resolves; the gate then lets saving proceed
|
||||
// without a prompt.
|
||||
builder.RegisterEntryPoint<RewardedSaveGate>()
|
||||
.WithParameter(typeof(SaveGalleryOverlayView), overlayView)
|
||||
.WithParameter(new SaveGateConfig(saveWithoutAdOnFailure));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 46ff2e3f5fac64fc9a40fe00a37bd9c8
|
||||
61
Assets/Darkmatter/Code/Features/SaveGate/SETUP.md
Normal file
61
Assets/Darkmatter/Code/Features/SaveGate/SETUP.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# SaveGate — cross-scene rewarded-ad save gate (manual Unity wiring)
|
||||
|
||||
Code is complete and compiles. The gate does **not** show its prompt until the overlay is built in
|
||||
**Boot** and assigned to `SaveGateModule`. Until then both callers fall back to saving directly
|
||||
(no prompt), so nothing breaks pre-wiring.
|
||||
|
||||
## What it does
|
||||
|
||||
A single root-scoped gate (`IRewardedSaveGate` → `RewardedSaveGate`) used by **every** save-to-gallery
|
||||
button, in any scene:
|
||||
|
||||
1. Tap save → **kid-friendly prompt** ("Watch a short video to save your picture to the gallery!")
|
||||
with **Watch** / **Cancel**.
|
||||
2. **Cancel** → nothing saved.
|
||||
3. **Watch** → **rewarded ad** (`AdFormat.Rewarded`). Reward earned → caller saves.
|
||||
4. Ad closed early → not saved. No fill / load-or-show error → saved anyway iff
|
||||
**Save Without Ad On Failure** (default ON).
|
||||
|
||||
Callers today:
|
||||
- **Gameplay capture/save button** (`CaptureButtonPresenter`) — gate, then its existing
|
||||
**GallerySaveView** "Saved to gallery!" popup (unchanged).
|
||||
- **Art book** page save buttons (`ArtbookPresenter`) — gate, then the gate's **shared success toast**
|
||||
(the art book has no popup of its own).
|
||||
|
||||
It lives in **Boot** (never unloaded), so the same overlay shows over MainMenu / Colorbook / Gameplay
|
||||
— the loading-screen trick.
|
||||
|
||||
## The overlay prefab (ready-made)
|
||||
|
||||
A complete, wired overlay prefab already exists:
|
||||
|
||||
`Assets/Darkmatter/Content/Colorbook UI/Prefabs/UI/SaveGalleryOverlay.prefab`
|
||||
|
||||
It has its own screen-space Canvas (sort order 5100, 1080×1920 scaler — matches `TutorialOverlayCanvas`),
|
||||
the `SaveGalleryOverlayView` on the root with all six fields wired, and:
|
||||
- **PromptPanel** → dim backdrop + cream Card → Fredoka message, green **Watch video**, grey **Not now**.
|
||||
- **SuccessPanel** (starts inactive) → dark toast → "Saved to gallery!".
|
||||
|
||||
Edit the copy/colors freely; it uses plain colored rects (no sprite art) so a designer can drop in
|
||||
sprites later. To wire it: just drag the prefab into **Boot** (it's a self-contained Canvas, so it
|
||||
works as-is) and assign it to `SaveGateModule` below. Boot is never unloaded, so it persists across
|
||||
every scene. (Building your own overlay also works — put `SaveGalleryOverlayView` on an always-active
|
||||
object and wire promptPanel / watchButton / cancelButton / promptLabel / successPanel / successLabel.)
|
||||
|
||||
## Register in DI (Boot RootLifetimeScope)
|
||||
|
||||
1. Add a **`SaveGateModule`** component in Boot (it's an `IModule`).
|
||||
2. Add it to the **RootLifetimeScope**'s `serviceModules` list (same list that holds the Ad / Loading
|
||||
modules) so it registers at the root.
|
||||
3. On `SaveGateModule`: assign **Overlay View** → the `SaveGalleryOverlayView`; tick
|
||||
**Save Without Ad On Failure** (recommended).
|
||||
|
||||
`IAdService` is already root-scoped (AdServiceModule in Boot); the gate resolves it automatically, and
|
||||
`CaptureButtonPresenter` / `ArtbookPresenter` resolve `IRewardedSaveGate` from the root.
|
||||
|
||||
## Before release
|
||||
|
||||
- `AdUnitCatalogSO` holds AdMob **test** unit IDs (incl. test Rewarded). Swap in the real Rewarded ID.
|
||||
- Child-directed / COPPA app (IDFA off, `TagForChildDirectedTreatment.True`, `MaxAdContentRating.G`
|
||||
already set). Confirm rewarded ads are permitted under the relevant kids / family policies before
|
||||
shipping rewarded-gated saving.
|
||||
7
Assets/Darkmatter/Code/Features/SaveGate/SETUP.md.meta
Normal file
7
Assets/Darkmatter/Code/Features/SaveGate/SETUP.md.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9db44e977bbde446daa19aa928ee1230
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Darkmatter/Code/Features/SaveGate/Systems.meta
Normal file
8
Assets/Darkmatter/Code/Features/SaveGate/Systems.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 010c08604fc7e44ceb9d069874618ce8
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,145 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using Darkmatter.Core.Contracts.Features.SaveGate;
|
||||
using Darkmatter.Core.Contracts.Services.Ads;
|
||||
using Darkmatter.Core.Enums.Services.Ads;
|
||||
using Darkmatter.Features.SaveGate.UI;
|
||||
using VContainer.Unity;
|
||||
|
||||
namespace Darkmatter.Features.SaveGate.Systems
|
||||
{
|
||||
public class RewardedSaveGate : IRewardedSaveGate, IStartable, IDisposable
|
||||
{
|
||||
private readonly SaveGalleryOverlayView _overlay;
|
||||
private readonly IAdService _ads;
|
||||
private readonly SaveGateConfig _config;
|
||||
|
||||
private CancellationTokenSource _cts;
|
||||
private bool _busy;
|
||||
|
||||
public RewardedSaveGate(SaveGalleryOverlayView overlay, IAdService ads, SaveGateConfig config)
|
||||
{
|
||||
_overlay = overlay;
|
||||
_ads = ads;
|
||||
_config = config;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
_cts = new CancellationTokenSource();
|
||||
// Warm a rewarded ad so the gate is snappy on first save.
|
||||
PrewarmRewardedAsync(_cts.Token).Forget();
|
||||
}
|
||||
|
||||
public async UniTask<bool> RequestSaveAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Overlay not built yet, or no ad service: never block saving.
|
||||
if (_overlay == null || _ads == null) return true;
|
||||
if (_busy) return false; // a prompt is already up; ignore the extra tap
|
||||
|
||||
_busy = true;
|
||||
using var linked = CancellationTokenSource.CreateLinkedTokenSource(
|
||||
_cts?.Token ?? CancellationToken.None, cancellationToken);
|
||||
var ct = linked.Token;
|
||||
try
|
||||
{
|
||||
if (!await ShowPromptAsync(ct)) return false; // child tapped Cancel
|
||||
return await TryShowRewardedAsync(ct); // ad closed early / unavailable
|
||||
}
|
||||
catch (OperationCanceledException) { return false; }
|
||||
finally
|
||||
{
|
||||
_busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
public async UniTask ShowSavedAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_overlay == null || !_overlay.HasSuccessPanel) return;
|
||||
using var linked = CancellationTokenSource.CreateLinkedTokenSource(
|
||||
_cts?.Token ?? CancellationToken.None, cancellationToken);
|
||||
_overlay.ShowSuccess();
|
||||
try
|
||||
{
|
||||
await UniTask.Delay(TimeSpan.FromSeconds(_overlay.SuccessAutoHideSeconds),
|
||||
cancellationToken: linked.Token);
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
_overlay.HideSuccess();
|
||||
}
|
||||
|
||||
// Shows the prompt and resolves true on Watch, false on Cancel (or cancellation).
|
||||
private UniTask<bool> ShowPromptAsync(CancellationToken ct)
|
||||
{
|
||||
var tcs = new UniTaskCompletionSource<bool>();
|
||||
CancellationTokenRegistration reg = default;
|
||||
var done = false;
|
||||
|
||||
void Finish(bool result, bool canceled)
|
||||
{
|
||||
if (done) return;
|
||||
done = true;
|
||||
_overlay.OnWatch -= OnWatch;
|
||||
_overlay.OnCancel -= OnCancel;
|
||||
reg.Dispose();
|
||||
_overlay.HidePrompt();
|
||||
if (canceled) tcs.TrySetCanceled(ct);
|
||||
else tcs.TrySetResult(result);
|
||||
}
|
||||
|
||||
void OnWatch() => Finish(true, false);
|
||||
void OnCancel() => Finish(false, false);
|
||||
|
||||
_overlay.OnWatch += OnWatch;
|
||||
_overlay.OnCancel += OnCancel;
|
||||
_overlay.ShowPrompt();
|
||||
reg = ct.Register(() => Finish(false, true));
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
// Returns true only when the child earned the reward. Falls back to SaveWithoutAdOnFailure
|
||||
// when the ad can't be shown at all; an ad closed early counts as not rewarded -> no save.
|
||||
private async UniTask<bool> TryShowRewardedAsync(CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!_ads.IsInitialized) await _ads.InitializeAsync(ct);
|
||||
if (!_ads.IsReady(AdFormat.Rewarded) && !await _ads.LoadAsync(AdFormat.Rewarded, ct))
|
||||
return _config.SaveWithoutAdOnFailure;
|
||||
|
||||
var result = await _ads.ShowAsync(AdFormat.Rewarded, ct);
|
||||
if (result.Rewarded) return true;
|
||||
return result.Shown ? false : _config.SaveWithoutAdOnFailure;
|
||||
}
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
UnityEngine.Debug.LogWarning($"[SaveGate] Rewarded ad gate failed: {ex.Message}");
|
||||
return _config.SaveWithoutAdOnFailure;
|
||||
}
|
||||
}
|
||||
|
||||
private async UniTaskVoid PrewarmRewardedAsync(CancellationToken ct)
|
||||
{
|
||||
if (_ads == null) return;
|
||||
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($"[SaveGate] Rewarded prewarm failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cts?.Cancel();
|
||||
_cts?.Dispose();
|
||||
_cts = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bcff7c06c2d77499db81865208f807e7
|
||||
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
|
||||
namespace Darkmatter.Features.SaveGate
|
||||
{
|
||||
[Serializable]
|
||||
public struct SaveGateConfig
|
||||
{
|
||||
// When the rewarded ad can't be shown (no fill / load or show error), save anyway instead of
|
||||
// blocking the child. A user who opens the ad and closes it early is never rewarded and does
|
||||
// not trigger this fallback.
|
||||
public bool SaveWithoutAdOnFailure { get; }
|
||||
|
||||
public SaveGateConfig(bool saveWithoutAdOnFailure)
|
||||
{
|
||||
SaveWithoutAdOnFailure = saveWithoutAdOnFailure;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c7343ebeefe9e4db48c41c617347adae
|
||||
8
Assets/Darkmatter/Code/Features/SaveGate/UI.meta
Normal file
8
Assets/Darkmatter/Code/Features/SaveGate/UI.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1356ea11d3edf4c1293bc8dec5ecdb3a
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,77 @@
|
||||
using System;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Darkmatter.Features.SaveGate.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Root-scoped, cross-scene overlay for the save-to-gallery flow: a kid-friendly "watch a video"
|
||||
/// prompt and a "Saved!" toast. Dumb view — it shows/hides panels and raises button events;
|
||||
/// <c>RewardedSaveGate</c> drives the flow. Lives in Boot so it persists over every scene.
|
||||
///
|
||||
/// If the panels aren't wired in the inspector it builds the whole UI in code (see
|
||||
/// <see cref="buildUiAtRuntime"/>), so the feature works with no manual prefab setup. A designer
|
||||
/// can still wire custom panels and the code build is skipped.
|
||||
/// </summary>
|
||||
public class SaveGalleryOverlayView : MonoBehaviour
|
||||
{
|
||||
[Header("Watch-ad prompt")] [SerializeField]
|
||||
private GameObject promptPanel;
|
||||
|
||||
[SerializeField] private Button watchButton;
|
||||
[SerializeField] private Button cancelButton;
|
||||
[SerializeField] private TMP_Text promptLabel;
|
||||
|
||||
[Header("Saved toast")] [SerializeField]
|
||||
private GameObject successPanel;
|
||||
|
||||
[SerializeField] private TMP_Text successLabel;
|
||||
[SerializeField, Min(0f)] private float successAutoHideSeconds = 1.5f;
|
||||
|
||||
public event Action OnWatch;
|
||||
public event Action OnCancel;
|
||||
|
||||
public float SuccessAutoHideSeconds => successAutoHideSeconds;
|
||||
public bool HasSuccessPanel => successPanel != null;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (promptPanel != null) promptPanel.SetActive(false);
|
||||
if (successPanel != null) successPanel.SetActive(false);
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (watchButton != null) watchButton.onClick.AddListener(() => OnWatch?.Invoke());
|
||||
if (cancelButton != null) cancelButton.onClick.AddListener(() => OnCancel?.Invoke());
|
||||
}
|
||||
|
||||
public void ShowPrompt()
|
||||
{
|
||||
if (successPanel != null) successPanel.SetActive(false);
|
||||
if (promptPanel != null) promptPanel.SetActive(true);
|
||||
}
|
||||
|
||||
public void HidePrompt()
|
||||
{
|
||||
if (promptPanel != null) promptPanel.SetActive(false);
|
||||
}
|
||||
|
||||
public void ShowSuccess()
|
||||
{
|
||||
if (successPanel != null) successPanel.SetActive(true);
|
||||
}
|
||||
|
||||
public void HideSuccess()
|
||||
{
|
||||
if (successPanel != null) successPanel.SetActive(false);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (watchButton != null) watchButton.onClick.RemoveAllListeners();
|
||||
if (cancelButton != null) cancelButton.onClick.RemoveAllListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e2688ce9b9d9848719e2ee33cf6eb155
|
||||
@@ -97,7 +97,10 @@ namespace Darkmatter.Features.ShapeBuilder.UI
|
||||
_bus = bus;
|
||||
_undo = undo;
|
||||
_trayPos = trayPos;
|
||||
_traySize = shape.DefaultSizeDelta;
|
||||
// Keep the piece at the prefab Image's authored size. Sourcing this from
|
||||
// shape.DefaultSizeDelta let each ShapeSO override the image's default size;
|
||||
// the prefab (with preserveAspect) is the single source of truth now.
|
||||
_traySize = RectTransform.sizeDelta;
|
||||
_dragRoot = dragRoot;
|
||||
|
||||
_homeParent = RectTransform.parent;
|
||||
@@ -151,7 +154,7 @@ namespace Darkmatter.Features.ShapeBuilder.UI
|
||||
if (_locked) return;
|
||||
|
||||
var pointerLocal = ScreenToLocal(e.position) + _grabOffset;
|
||||
var hovered = FindSlotUnder(e.position);
|
||||
var hovered = FindSlotForCatch(e.position);
|
||||
bool insidePreview = hovered != null;
|
||||
|
||||
if (insidePreview && !_inPreview)
|
||||
@@ -187,7 +190,11 @@ namespace Darkmatter.Features.ShapeBuilder.UI
|
||||
|
||||
SetAlpha(_dragOrigAlpha);
|
||||
|
||||
var target = FindSlotUnder(e.position);
|
||||
// If a slot is already previewing, releasing commits to it. Otherwise catch
|
||||
// a quick drop with no prior preview using the same per-slot radius.
|
||||
var target = _inPreview && _activeSlot != null
|
||||
? _activeSlot
|
||||
: FindSlotForCatch(e.position);
|
||||
if (target != null)
|
||||
{
|
||||
_activeSlot = target;
|
||||
@@ -200,17 +207,45 @@ namespace Darkmatter.Features.ShapeBuilder.UI
|
||||
}
|
||||
}
|
||||
|
||||
private SlotMarker FindSlotUnder(Vector2 screenPos)
|
||||
// Nearest slot whose per-slot catch circle contains the pointer. Each slot's
|
||||
// radius is derived from its own size (see SlotCatchRadius), so big slots catch
|
||||
// from farther than small ones. All distances are in PaperRoot local space —
|
||||
// ScreenPointToLocalPointInRectangle and InverseTransformVector both strip the
|
||||
// CanvasScaler factor, so the catch feels identical on every screen resolution.
|
||||
private SlotMarker FindSlotForCatch(Vector2 screenPos)
|
||||
{
|
||||
if (_candidateSlots == null) return null;
|
||||
Vector2 pointerLocal = ScreenToLocal(screenPos);
|
||||
SlotMarker best = null;
|
||||
float bestSqr = float.MaxValue;
|
||||
foreach (var s in _candidateSlots)
|
||||
{
|
||||
if (s == null) continue;
|
||||
if (s.IsOccupied && s != _activeSlot) continue;
|
||||
if (RectTransformUtility.RectangleContainsScreenPoint(s.RectTransform, screenPos, _eventCam))
|
||||
return s;
|
||||
var srt = s.RectTransform;
|
||||
Vector2 slotLocal = _parentRect.InverseTransformPoint(srt.position);
|
||||
float sqr = (slotLocal - pointerLocal).sqrMagnitude;
|
||||
float radius = SlotCatchRadius(srt);
|
||||
if (sqr <= radius * radius && sqr < bestSqr)
|
||||
{
|
||||
bestSqr = sqr;
|
||||
best = s;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return best;
|
||||
}
|
||||
|
||||
// Catch radius for one slot, in PaperRoot local units: half the slot's diagonal
|
||||
// (its size mapped into parent space, so any slot rotation/mirror/scale is
|
||||
// accounted for) times the config multiplier. Scales per slot — not one flat
|
||||
// distance for all.
|
||||
private float SlotCatchRadius(RectTransform slot)
|
||||
{
|
||||
Vector2 size = slot.rect.size;
|
||||
Vector2 prX = _parentRect.InverseTransformVector(slot.TransformVector(new Vector3(size.x, 0f, 0f)));
|
||||
Vector2 prY = _parentRect.InverseTransformVector(slot.TransformVector(new Vector3(0f, size.y, 0f)));
|
||||
float halfDiagonal = 0.5f * new Vector2(prX.magnitude, prY.magnitude).magnitude;
|
||||
return halfDiagonal * _cfg.CatchRadiusScale;
|
||||
}
|
||||
|
||||
private void AnimatePreviewPose(bool toSlot)
|
||||
|
||||
@@ -11,6 +11,7 @@ using Darkmatter.Core.Data.Signals.Features.GameplayFlow; // DrawingCompletedSig
|
||||
using Darkmatter.Core.Data.Signals.Features.ShapeBuilder; // ShapeBuilderStarted/PieceSnapped/ShapeAssembled
|
||||
using Darkmatter.Core.Data.Signals.Features.Tutorial;
|
||||
using Darkmatter.Core.Data.Static.Features.Tutorial;
|
||||
using Darkmatter.Core.Enums.Features.Tutorial; // BubblePlacement
|
||||
using Darkmatter.Features.Coloring.UI; // ColorButton, ColorRegionView
|
||||
using Darkmatter.Features.DrawingCatalog; // DrawingCatalogButton
|
||||
using Darkmatter.Features.GameplayFlow.UI; // NextButtonView
|
||||
@@ -44,6 +45,8 @@ namespace Darkmatter.Features.Tutorial.Systems
|
||||
private CancellationTokenSource _runCts;
|
||||
private CancellationToken _ct;
|
||||
private bool _completed;
|
||||
private bool _drawingCompleted;
|
||||
private bool _hasColored;
|
||||
private bool _suspended;
|
||||
private Action _reshow;
|
||||
private int _stepIndex;
|
||||
@@ -74,6 +77,13 @@ namespace Darkmatter.Features.Tutorial.Systems
|
||||
_navSubs.Add(_bus.Subscribe<ReturnToMainMenuSignal>(_ => Suspend()));
|
||||
_navSubs.Add(_bus.Subscribe<OpenColorBookSignal>(_ => Resume()));
|
||||
_navSubs.Add(_bus.Subscribe<DrawingCatalogReadySignal>(OnCatalogReadyGlobal));
|
||||
// The drawing being completed (Next pressed) is the run's true end — even if the child
|
||||
// finds Next early during free-paint. Flag it so the catalog reload that follows isn't
|
||||
// mistaken for a Back-navigation and doesn't restart the tutorial.
|
||||
_navSubs.Add(_bus.Subscribe<DrawingCompletedSignal>(_ => _drawingCompleted = true));
|
||||
// Painting at least one region means the child has grasped the core colour gesture. If
|
||||
// they then go Back, we finish the tutorial instead of restarting it (OnCatalogReadyGlobal).
|
||||
_navSubs.Add(_bus.Subscribe<ColorAppliedSignal>(_ => _hasColored = true));
|
||||
|
||||
StartRun(skipCatalogWait: false);
|
||||
}
|
||||
@@ -86,6 +96,8 @@ namespace Darkmatter.Features.Tutorial.Systems
|
||||
_ct = _runCts.Token;
|
||||
_gen++;
|
||||
_suspended = false;
|
||||
_drawingCompleted = false;
|
||||
_hasColored = false;
|
||||
_reshow = null;
|
||||
_stepIndex = 0;
|
||||
_overlay.HideInstant();
|
||||
@@ -108,12 +120,23 @@ namespace Darkmatter.Features.Tutorial.Systems
|
||||
}
|
||||
|
||||
// The catalog re-appeared while we were mid-gameplay -> the player went Back. Restart from
|
||||
// step 1 (the catalog is already on screen, so skip the wait). Excludes step 6+, whose own
|
||||
// completion loads the catalog.
|
||||
// step 1 (the catalog is already on screen, so skip the wait). Excludes step 7+ (the Next
|
||||
// press), whose own completion loads the catalog.
|
||||
private void OnCatalogReadyGlobal(DrawingCatalogReadySignal _)
|
||||
{
|
||||
if (!_completed && _stepIndex >= 2 && _stepIndex <= 5)
|
||||
StartRun(skipCatalogWait: true);
|
||||
if (_completed || _drawingCompleted || _stepIndex < 2 || _stepIndex > 6) return;
|
||||
|
||||
// Already coloured at least one region before backing out: the child has the core gesture,
|
||||
// so finish the tutorial rather than trapping them in a full restart.
|
||||
if (_hasColored)
|
||||
{
|
||||
_runCts?.Cancel();
|
||||
Complete();
|
||||
_overlay.HideInstant();
|
||||
return;
|
||||
}
|
||||
|
||||
StartRun(skipCatalogWait: true);
|
||||
}
|
||||
|
||||
private void ShowStep(Action show)
|
||||
@@ -154,7 +177,7 @@ namespace Darkmatter.Features.Tutorial.Systems
|
||||
if (!await WaitForSignalAsync<DrawingCatalogReadySignal>()) return;
|
||||
}
|
||||
await UniTask.NextFrame(_ct);
|
||||
ShowStep(() => ShowBlockingTap(FindFirstCatalogCell(), _config.PickText));
|
||||
ShowStep(() => ShowBlockingTap(FindFirstCatalogCell(), _config.PickText, _config.PickBubble, _config.PickBubbleOffset));
|
||||
if (!await WaitForActionAsync<DrawingSelectedSignal>()) return;
|
||||
EndStep("pick", 1);
|
||||
|
||||
@@ -165,7 +188,7 @@ namespace Darkmatter.Features.Tutorial.Systems
|
||||
ShowStep(() =>
|
||||
{
|
||||
var (pieceRect, slotRect) = FindFirstPieceAndSlot();
|
||||
if (pieceRect != null) _overlay.ShowDrag(pieceRect, slotRect, _config.DragText);
|
||||
if (pieceRect != null) _overlay.ShowDrag(pieceRect, slotRect, _config.DragText, _config.DragBubble, _config.DragBubbleOffset);
|
||||
else Debug.LogWarning("[Tutorial] No draggable piece found for the drag step.");
|
||||
});
|
||||
if (!await WaitForActionAsync<PieceSnappedSignal>()) return; // any snap teaches the gesture
|
||||
@@ -173,7 +196,7 @@ namespace Darkmatter.Features.Tutorial.Systems
|
||||
|
||||
// Step 3 — finish the rest of the puzzle freely (non-blocking hint).
|
||||
_stepIndex = 3;
|
||||
ShowStep(() => _overlay.ShowTap(null, _config.FinishText, blockInput: false));
|
||||
ShowStep(() => _overlay.ShowTap(null, _config.FinishText, blockInput: false, _config.FinishBubble, _config.FinishBubbleOffset));
|
||||
if (!await WaitForActionAsync<ShapeAssembledSignal>()) return;
|
||||
EndStep("finish", 3);
|
||||
|
||||
@@ -181,32 +204,55 @@ namespace Darkmatter.Features.Tutorial.Systems
|
||||
_stepIndex = 4;
|
||||
if (!await WaitForSignalAsync<RegionsInitializedSignal>()) return;
|
||||
await UniTask.NextFrame(_ct);
|
||||
ShowStep(() => ShowBlockingTap(FindFirstColorButton(), _config.ColorText));
|
||||
ShowStep(() => ShowBlockingTap(FindFirstColorButton(), _config.ColorText, _config.ColorBubble, _config.ColorBubbleOffset));
|
||||
if (!await WaitForActionAsync<ColorSelectedSignal>()) return;
|
||||
EndStep("color", 4);
|
||||
|
||||
// Step 5 — paint a region.
|
||||
// Step 5 — paint the first region (teaches the tap by spotlighting one region).
|
||||
_stepIndex = 5;
|
||||
ShowStep(() => ShowBlockingTap(FindLargestRegion(), _config.PaintText));
|
||||
// Watch full-colour completion across both this taught tap and the free-paint step that
|
||||
// follows. A one-region drawing is finished by this very tap, so its signal would fire
|
||||
// before step 6 could subscribe — the flag lets us skip the free-paint step instead of
|
||||
// stranding the child on a hint with nothing left to colour.
|
||||
var fullyColored = false;
|
||||
using var fullColorSub = _bus.Subscribe<AllRegionsColoredSignal>(_ => fullyColored = true);
|
||||
ShowStep(() => ShowBlockingTap(FindLargestRegion(), _config.PaintText, _config.PaintBubble, _config.PaintBubbleOffset));
|
||||
if (!await WaitForActionAsync<ColorAppliedSignal>()) return;
|
||||
EndStep("paint", 5);
|
||||
|
||||
// Step 6 — press Next.
|
||||
// Step 6 — colour the rest of the picture freely (non-blocking hint). Hold here until
|
||||
// every region is filled, so the "Tap Next" prompt only appears once colouring is done.
|
||||
// The child can still reach the live Next button during this open step; if they finish
|
||||
// the drawing early that way, take it as done rather than waiting on a fill that can't come.
|
||||
_stepIndex = 6;
|
||||
await UniTask.NextFrame(_ct);
|
||||
ShowStep(() => ShowBlockingTap(FindNextButton(), _config.NextText));
|
||||
if (!await WaitForActionAsync<DrawingCompletedSignal>()) return;
|
||||
EndStep("next", 6);
|
||||
if (!fullyColored)
|
||||
{
|
||||
ShowStep(() => _overlay.ShowTap(null, _config.FinishColoringText, blockInput: false, _config.FinishColoringBubble, _config.FinishColoringBubbleOffset));
|
||||
await UniTask.WhenAny(
|
||||
WaitForActionAsync<AllRegionsColoredSignal>(),
|
||||
WaitForActionAsync<DrawingCompletedSignal>());
|
||||
}
|
||||
EndStep("finishColoring", 6);
|
||||
|
||||
// Step 7 — celebrate.
|
||||
_stepIndex = 7;
|
||||
await _overlay.ShowToastAsync(_config.DoneText, _ct);
|
||||
// Step 7 — press Next (skipped if the drawing was already completed early).
|
||||
if (!_drawingCompleted)
|
||||
{
|
||||
_stepIndex = 7;
|
||||
await UniTask.NextFrame(_ct);
|
||||
ShowStep(() => ShowBlockingTap(FindNextButton(), _config.NextText, _config.NextBubble, _config.NextBubbleOffset));
|
||||
if (!await WaitForActionAsync<DrawingCompletedSignal>()) return;
|
||||
EndStep("next", 7);
|
||||
}
|
||||
|
||||
// Step 8 — celebrate.
|
||||
_stepIndex = 8;
|
||||
await _overlay.ShowToastAsync(_config.DoneText, _ct, _config.DoneBubble, _config.DoneBubbleOffset);
|
||||
}
|
||||
|
||||
private void ShowBlockingTap(RectTransform target, string message)
|
||||
private void ShowBlockingTap(RectTransform target, string message, BubblePlacement placement, Vector2 offset)
|
||||
{
|
||||
if (target == null) Debug.LogWarning($"[Tutorial] No target found for step: \"{message}\"");
|
||||
_overlay.ShowTap(target, message, blockInput: target != null);
|
||||
_overlay.ShowTap(target, message, blockInput: target != null, placement, offset);
|
||||
}
|
||||
|
||||
private void EndStep(string id, int index)
|
||||
@@ -283,13 +329,16 @@ namespace Darkmatter.Features.Tutorial.Systems
|
||||
|
||||
private static (RectTransform piece, RectTransform slot) FindFirstPieceAndSlot()
|
||||
{
|
||||
// Pieces spawn in data order under one SpawnRoot, so sibling 0 is the top of the tray.
|
||||
// FindObjectsByType returns undefined order — pick the lowest sibling index, not [0].
|
||||
var pieces = UnityEngine.Object.FindObjectsByType<ShapePiece>(FindObjectsSortMode.None);
|
||||
ShapePiece piece = null;
|
||||
var bestIndex = int.MaxValue;
|
||||
foreach (var p in pieces)
|
||||
{
|
||||
if (p == null || p.IsLocked || !p.gameObject.activeInHierarchy) continue;
|
||||
piece = p;
|
||||
break;
|
||||
var index = p.transform.GetSiblingIndex();
|
||||
if (index < bestIndex) { bestIndex = index; piece = p; }
|
||||
}
|
||||
if (piece == null) return (null, null);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Threading;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using Darkmatter.Core.Contracts.Features.Tutorial;
|
||||
using Darkmatter.Core.Enums.Features.Tutorial;
|
||||
using PrimeTween;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
@@ -45,6 +46,8 @@ namespace Darkmatter.Features.Tutorial.UI
|
||||
[SerializeField] private float fadeDuration = 0.25f;
|
||||
[SerializeField] private float toastSeconds = 1.6f;
|
||||
[SerializeField] private float bubbleGap = 60f;
|
||||
[Tooltip("Margin (px) from the top/bottom edge for the Top/Bottom bubble placement presets.")]
|
||||
[SerializeField] private float bubbleEdgeMargin = 150f;
|
||||
[SerializeField] private float haloPulseScale = 1.18f;
|
||||
[SerializeField] private float pulseDuration = 0.6f;
|
||||
[SerializeField] private float dragHandDuration = 1.1f;
|
||||
@@ -60,6 +63,8 @@ namespace Darkmatter.Features.Tutorial.UI
|
||||
private RectTransform _area;
|
||||
private Camera _overlayCam;
|
||||
private Mode _mode = Mode.Hidden;
|
||||
private BubblePlacement _placement = BubblePlacement.Auto;
|
||||
private Vector2 _bubbleOffset;
|
||||
private RectTransform _target; // tap target
|
||||
private RectTransform _dragFrom; // the piece (drag start)
|
||||
private RectTransform _dragTo; // the slot (drag destination)
|
||||
@@ -91,11 +96,13 @@ namespace Darkmatter.Features.Tutorial.UI
|
||||
|
||||
// ── ITutorialOverlay ─────────────────────────────────────────────────
|
||||
|
||||
public void ShowTap(RectTransform target, string message, bool blockInput)
|
||||
public void ShowTap(RectTransform target, string message, bool blockInput, BubblePlacement placement = BubblePlacement.Auto, Vector2 offset = default)
|
||||
{
|
||||
CacheRefs();
|
||||
KillAnims();
|
||||
SetText(message);
|
||||
_placement = placement;
|
||||
_bubbleOffset = offset;
|
||||
_dragFrom = null;
|
||||
_dragTo = null;
|
||||
|
||||
@@ -122,11 +129,13 @@ namespace Darkmatter.Features.Tutorial.UI
|
||||
FadeInQuick();
|
||||
}
|
||||
|
||||
public void ShowDrag(RectTransform from, RectTransform to, string message)
|
||||
public void ShowDrag(RectTransform from, RectTransform to, string message, BubblePlacement placement = BubblePlacement.Auto, Vector2 offset = default)
|
||||
{
|
||||
CacheRefs();
|
||||
KillAnims();
|
||||
SetText(message);
|
||||
_placement = placement;
|
||||
_bubbleOffset = offset;
|
||||
|
||||
// No dim and no blocking — the piece lives under the overlay and must stay visible and
|
||||
// draggable. Halo + hand travel together along the piece -> slot path, recomputed live.
|
||||
@@ -153,11 +162,13 @@ namespace Darkmatter.Features.Tutorial.UI
|
||||
FadeInQuick();
|
||||
}
|
||||
|
||||
public async UniTask ShowToastAsync(string message, CancellationToken ct)
|
||||
public async UniTask ShowToastAsync(string message, CancellationToken ct, BubblePlacement placement = BubblePlacement.Auto, Vector2 offset = default)
|
||||
{
|
||||
CacheRefs();
|
||||
KillAnims();
|
||||
SetText(message);
|
||||
_placement = placement;
|
||||
_bubbleOffset = offset;
|
||||
|
||||
_mode = Mode.Centered;
|
||||
_target = null;
|
||||
@@ -238,8 +249,10 @@ namespace Darkmatter.Features.Tutorial.UI
|
||||
if (hand != null)
|
||||
hand.anchoredPosition = centerLocal + new Vector2(0f, -radiusLocal * 0.5f);
|
||||
|
||||
PositionBubble(centerLocal.y, centerLocal.y + radiusLocal, centerLocal.y - radiusLocal,
|
||||
_area.rect.height * 0.5f);
|
||||
if (_placement == BubblePlacement.Auto)
|
||||
PositionBubble(centerLocal.y, centerLocal.y + radiusLocal, centerLocal.y - radiusLocal);
|
||||
else
|
||||
PositionBubblePreset(_placement);
|
||||
}
|
||||
|
||||
// Drag: halo + hand glide piece -> slot, recomputed live so it tracks the real positions.
|
||||
@@ -283,27 +296,59 @@ namespace Darkmatter.Features.Tutorial.UI
|
||||
|
||||
// The drag path spans the play area, so pin the bubble to a screen edge instead of hugging
|
||||
// the piece — keeps it off the shapes.
|
||||
PositionBubbleAtEdge(dragBubbleAtTop, dragBubbleEdgeMargin);
|
||||
if (_placement == BubblePlacement.Auto)
|
||||
PositionBubbleAtEdge(dragBubbleAtTop, dragBubbleEdgeMargin);
|
||||
else
|
||||
PositionBubblePreset(_placement);
|
||||
}
|
||||
|
||||
private void LayoutCentered()
|
||||
{
|
||||
if (bubbleRoot == null) return;
|
||||
if (_placement != BubblePlacement.Auto) { PositionBubblePreset(_placement); return; }
|
||||
float halfH = _area.rect.height * 0.5f;
|
||||
float bubbleHalf = bubbleRoot.rect.height * 0.5f;
|
||||
bubbleRoot.anchoredPosition =
|
||||
new Vector2(0f, Mathf.Clamp(halfH * 0.45f, -halfH + bubbleHalf, halfH - bubbleHalf));
|
||||
SetBubbleAnchored(new Vector2(0f, halfH * 0.45f));
|
||||
}
|
||||
|
||||
private void PositionBubble(float holeCenterY, float holeTopY, float holeBottomY, float halfH)
|
||||
// Applies the per-step offset to a computed base position and clamps so the bubble always
|
||||
// stays fully on-screen, then commits it. Every bubble-positioning path routes through here.
|
||||
private void SetBubbleAnchored(Vector2 baseLocal)
|
||||
{
|
||||
if (bubbleRoot == null) return;
|
||||
float halfW = _area.rect.width * 0.5f;
|
||||
float halfH = _area.rect.height * 0.5f;
|
||||
float bubbleHalfW = bubbleRoot.rect.width * 0.5f;
|
||||
float bubbleHalfH = bubbleRoot.rect.height * 0.5f;
|
||||
Vector2 p = baseLocal + _bubbleOffset;
|
||||
p.x = Mathf.Clamp(p.x, -halfW + bubbleHalfW, halfW - bubbleHalfW);
|
||||
p.y = Mathf.Clamp(p.y, -halfH + bubbleHalfH, halfH - bubbleHalfH);
|
||||
bubbleRoot.anchoredPosition = p;
|
||||
}
|
||||
|
||||
private void PositionBubble(float holeCenterY, float holeTopY, float holeBottomY)
|
||||
{
|
||||
if (bubbleRoot == null) return;
|
||||
float bubbleHalf = bubbleRoot.rect.height * 0.5f;
|
||||
float y = holeCenterY <= 0f
|
||||
? holeTopY + bubbleGap + bubbleHalf
|
||||
: holeBottomY - bubbleGap - bubbleHalf;
|
||||
y = Mathf.Clamp(y, -halfH + bubbleHalf, halfH - bubbleHalf);
|
||||
bubbleRoot.anchoredPosition = new Vector2(0f, y);
|
||||
SetBubbleAnchored(new Vector2(0f, y));
|
||||
}
|
||||
|
||||
// Fixed-slot placement chosen per step (Top/Center/Bottom). Horizontally centred; clamped so
|
||||
// the bubble always stays fully on-screen.
|
||||
private void PositionBubblePreset(BubblePlacement placement)
|
||||
{
|
||||
if (bubbleRoot == null) return;
|
||||
float halfH = _area.rect.height * 0.5f;
|
||||
float bubbleHalf = bubbleRoot.rect.height * 0.5f;
|
||||
float y = placement switch
|
||||
{
|
||||
BubblePlacement.Top => halfH - bubbleHalf - bubbleEdgeMargin,
|
||||
BubblePlacement.Bottom => -halfH + bubbleHalf + bubbleEdgeMargin,
|
||||
_ => 0f, // Center
|
||||
};
|
||||
SetBubbleAnchored(new Vector2(0f, y));
|
||||
}
|
||||
|
||||
// Pins the bubble to the top or bottom edge (used for the drag step, whose target spans the
|
||||
@@ -314,8 +359,7 @@ namespace Darkmatter.Features.Tutorial.UI
|
||||
float halfH = _area.rect.height * 0.5f;
|
||||
float bubbleHalf = bubbleRoot.rect.height * 0.5f;
|
||||
float y = top ? halfH - bubbleHalf - margin : -halfH + bubbleHalf + margin;
|
||||
y = Mathf.Clamp(y, -halfH + bubbleHalf, halfH - bubbleHalf);
|
||||
bubbleRoot.anchoredPosition = new Vector2(0f, y);
|
||||
SetBubbleAnchored(new Vector2(0f, y));
|
||||
}
|
||||
|
||||
// ── Coordinate conversion (camera-agnostic) ──────────────────────────
|
||||
@@ -405,6 +449,8 @@ namespace Darkmatter.Features.Tutorial.UI
|
||||
private void ApplyHiddenState()
|
||||
{
|
||||
_mode = Mode.Hidden;
|
||||
_placement = BubblePlacement.Auto;
|
||||
_bubbleOffset = Vector2.zero;
|
||||
_target = null;
|
||||
_dragFrom = null;
|
||||
_dragTo = null;
|
||||
|
||||
@@ -3,10 +3,12 @@ using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using Darkmatter.Core.Contracts.Services.Ads;
|
||||
using Darkmatter.Core.Contracts.Services.Analytics;
|
||||
using Darkmatter.Core.Data.Dynamic.Services.Ads;
|
||||
using Darkmatter.Core.Data.Static.Services.Ads;
|
||||
using Darkmatter.Core.Enums.Services.Ads;
|
||||
using UnityEngine;
|
||||
using VContainer;
|
||||
using AdFormat = Darkmatter.Core.Enums.Services.Ads.AdFormat;
|
||||
#if GOOGLE_MOBILE_ADS
|
||||
using GoogleMobileAds.Api;
|
||||
@@ -29,9 +31,12 @@ namespace Darkmatter.Services.Ads
|
||||
public bool IsInitialized => _initialized;
|
||||
public event Action<AdFormat, AdLoadState> LoadStateChanged;
|
||||
|
||||
private IAnalyticsService _analytics;
|
||||
private bool _initialized;
|
||||
private bool _hasUserConsent = true;
|
||||
private bool _isChildDirected;
|
||||
// Coloring book is a child-directed app, so default to true. SetConsent can still
|
||||
// override if a consent flow later supplies a different value.
|
||||
private bool _isChildDirected = true;
|
||||
private CancellationTokenSource _lifetimeCts;
|
||||
|
||||
// App interruption state, fed by the Unity lifecycle messages below. A full-screen ad
|
||||
@@ -51,6 +56,12 @@ namespace Darkmatter.Services.Ads
|
||||
private BannerView _banner;
|
||||
#endif
|
||||
|
||||
// Method injection: AdMobAdService is a scene MonoBehaviour registered via RegisterComponent, so
|
||||
// VContainer injects here rather than through a constructor. IAnalyticsService is the Root-scoped
|
||||
// composite (Firebase + Facebook).
|
||||
[Inject]
|
||||
public void Construct(IAnalyticsService analytics) => _analytics = analytics;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_lifetimeCts = new CancellationTokenSource();
|
||||
@@ -208,6 +219,8 @@ namespace Darkmatter.Services.Ads
|
||||
|
||||
_banner?.Destroy();
|
||||
_banner = new BannerView(unitId, MapBannerSize(size), MapBannerPosition(position));
|
||||
_banner.OnAdPaid += v => LogAdImpression(AdFormat.Banner, v);
|
||||
_banner.OnAdClicked += () => LogAdClicked(AdFormat.Banner);
|
||||
|
||||
var tcs = new UniTaskCompletionSource<bool>();
|
||||
_banner.OnBannerAdLoaded += () => tcs.TrySetResult(true);
|
||||
@@ -267,7 +280,9 @@ namespace Darkmatter.Services.Ads
|
||||
: TagForChildDirectedTreatment.Unspecified,
|
||||
TagForUnderAgeOfConsent = _hasUserConsent
|
||||
? TagForUnderAgeOfConsent.Unspecified
|
||||
: TagForUnderAgeOfConsent.True
|
||||
: TagForUnderAgeOfConsent.True,
|
||||
// Child-directed app: cap served ads at G-rated content.
|
||||
MaxAdContentRating = MaxAdContentRating.G
|
||||
};
|
||||
|
||||
if (catalog.TestDeviceIds is { Count: > 0 })
|
||||
@@ -556,17 +571,59 @@ namespace Darkmatter.Services.Ads
|
||||
catch (OperationCanceledException) { }
|
||||
}
|
||||
|
||||
private void WireFullScreenEvents(InterstitialAd ad, AdFormat format) =>
|
||||
private void WireFullScreenEvents(InterstitialAd ad, AdFormat format)
|
||||
{
|
||||
ad.OnAdFullScreenContentClosed += () => SetState(format, AdLoadState.Idle);
|
||||
ad.OnAdPaid += v => LogAdImpression(format, v);
|
||||
ad.OnAdClicked += () => LogAdClicked(format);
|
||||
}
|
||||
|
||||
private void WireFullScreenEvents(RewardedAd ad, AdFormat format) =>
|
||||
private void WireFullScreenEvents(RewardedAd ad, AdFormat format)
|
||||
{
|
||||
ad.OnAdFullScreenContentClosed += () => SetState(format, AdLoadState.Idle);
|
||||
ad.OnAdPaid += v => LogAdImpression(format, v);
|
||||
ad.OnAdClicked += () => LogAdClicked(format);
|
||||
}
|
||||
|
||||
private void WireFullScreenEvents(RewardedInterstitialAd ad, AdFormat format) =>
|
||||
private void WireFullScreenEvents(RewardedInterstitialAd ad, AdFormat format)
|
||||
{
|
||||
ad.OnAdFullScreenContentClosed += () => SetState(format, AdLoadState.Idle);
|
||||
ad.OnAdPaid += v => LogAdImpression(format, v);
|
||||
ad.OnAdClicked += () => LogAdClicked(format);
|
||||
}
|
||||
|
||||
private void WireFullScreenEvents(AppOpenAd ad, AdFormat format) =>
|
||||
private void WireFullScreenEvents(AppOpenAd ad, AdFormat format)
|
||||
{
|
||||
ad.OnAdFullScreenContentClosed += () => SetState(format, AdLoadState.Idle);
|
||||
ad.OnAdPaid += v => LogAdImpression(format, v);
|
||||
ad.OnAdClicked += () => LogAdClicked(format);
|
||||
}
|
||||
|
||||
// Ad revenue: GA4-recommended ad_impression carries value+currency from AdMob's paid callback —
|
||||
// this is what surfaces ad revenue in Firebase/GA4. Ad callbacks fire on the Unity main thread
|
||||
// (RaiseAdEventsOnUnityMainThread = true), so logging straight to analytics is safe.
|
||||
private void LogAdImpression(AdFormat format, AdValue value)
|
||||
{
|
||||
_analytics?.LogEvent(AnalyticsEvents.AdImpression, new Dictionary<string, object>
|
||||
{
|
||||
[AnalyticsParams.AdPlatform] = "AdMob",
|
||||
[AnalyticsParams.AdFormat] = format.ToString(),
|
||||
[AnalyticsParams.AdUnitName] = catalog.GetUnitId(format, Application.platform) ?? string.Empty,
|
||||
[AnalyticsParams.Value] = value.Value / 1_000_000.0, // AdValue.Value is micros
|
||||
[AnalyticsParams.Currency] = value.CurrencyCode ?? string.Empty,
|
||||
[AnalyticsParams.Precision] = value.Precision.ToString(),
|
||||
});
|
||||
}
|
||||
|
||||
private void LogAdClicked(AdFormat format)
|
||||
{
|
||||
_analytics?.LogEvent(AnalyticsEvents.AdClicked, new Dictionary<string, object>
|
||||
{
|
||||
[AnalyticsParams.AdPlatform] = "AdMob",
|
||||
[AnalyticsParams.AdFormat] = format.ToString(),
|
||||
[AnalyticsParams.AdUnitName] = catalog.GetUnitId(format, Application.platform) ?? string.Empty,
|
||||
});
|
||||
}
|
||||
|
||||
private void ScheduleReload(AdFormat format)
|
||||
{
|
||||
|
||||
@@ -10,8 +10,17 @@ namespace Darkmatter.Services.Analytics
|
||||
{
|
||||
public void Register(IContainerBuilder builder)
|
||||
{
|
||||
builder.RegisterEntryPoint<FirebaseAnalyticsSystem>().As<IAnalyticsService>();
|
||||
// Firebase + Facebook are sinks (registered AsSelf so the composite can inject them);
|
||||
// CompositeAnalyticsService is the single IAnalyticsService the rest of the app consumes.
|
||||
builder.RegisterEntryPoint<FirebaseAnalyticsSystem>().AsSelf();
|
||||
builder.RegisterEntryPoint<FacebookAnalyticsSystem>().AsSelf();
|
||||
builder.Register<CompositeAnalyticsService>(Lifetime.Singleton).As<IAnalyticsService>();
|
||||
|
||||
builder.RegisterEntryPoint<AnalyticsTracker>();
|
||||
builder.RegisterEntryPoint<ErrorAnalyticsTracker>();
|
||||
|
||||
// Feeds the FCM token to AppsFlyer for uninstall measurement (Android-only).
|
||||
builder.RegisterEntryPoint<AppsFlyerUninstallTracker>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,12 @@
|
||||
"name": "Services.Analytics",
|
||||
"rootNamespace": "Darkmatter.Services.Analytics",
|
||||
"references": [
|
||||
"GUID:bd7ea2d41bfe64d229c22616f66e20f7",
|
||||
"GUID:c1c03c0e5b2f4412b9f2be1c20d6a9b1",
|
||||
"GUID:f51ebe6a0ceec4240a699833d6309b23",
|
||||
"GUID:b0214a6008ed146ff8f122a6a9c2f6cc",
|
||||
"GUID:f8c64bb88d959406689053ae3f31183d",
|
||||
"GUID:a0b1547602fc44f6da0a5e755ab3a7ef",
|
||||
"GUID:6a0a834eb41764f12ba55c3fb04a40cb",
|
||||
"GUID:b4c9f7fbf1e144933a1797dc208ece5f"
|
||||
"GUID:b4c9f7fbf1e144933a1797dc208ece5f",
|
||||
"GUID:2a37df438292d4903b4e5159c5de3bf9"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
|
||||
@@ -4,22 +4,43 @@ using Darkmatter.Core;
|
||||
using Darkmatter.Core.Contracts.Services.Analytics;
|
||||
using Darkmatter.Core.Data.Signals.Features.AppBoot;
|
||||
using Darkmatter.Core.Data.Signals.Features.Capture;
|
||||
using Darkmatter.Core.Data.Signals.Features.Coloring;
|
||||
using Darkmatter.Core.Data.Signals.Features.Drawing;
|
||||
using Darkmatter.Core.Data.Signals.Features.GameplayFlow;
|
||||
using Darkmatter.Core.Data.Signals.Features.MainMenu;
|
||||
using Darkmatter.Core.Data.Signals.Features.ShapeBuilder;
|
||||
using Darkmatter.Core.Data.Signals.Features.Tutorial;
|
||||
using Darkmatter.Libs.Observer;
|
||||
using UnityEngine;
|
||||
using VContainer.Unity;
|
||||
|
||||
namespace Darkmatter.Services.Analytics
|
||||
{
|
||||
/// <summary>
|
||||
/// Bridges gameplay signals to analytics events. Holds light per-drawing state (start time, distinct
|
||||
/// colors, shape attempts) so completion/abandon events carry duration_seconds, colors_used and
|
||||
/// attempts — the params that turn "they quit" into "they quit on drawing X after N seconds".
|
||||
///
|
||||
/// Logs once to <see cref="IAnalyticsService"/> (the composite), which fans out to Firebase + Facebook.
|
||||
/// </summary>
|
||||
public sealed class AnalyticsTracker : IStartable, IDisposable
|
||||
{
|
||||
private const int ColorApplySampleEvery = 25; // color_applied is high-frequency → sample
|
||||
private const string FirstDrawingKey = "analytics_first_drawing_started";
|
||||
private const string AllContentKey = "analytics_all_content_completed";
|
||||
|
||||
private readonly IEventBus _bus;
|
||||
private readonly IAnalyticsService _analytics;
|
||||
private readonly List<IDisposable> _subs = new();
|
||||
|
||||
// Per-drawing aggregation state.
|
||||
private string _activeDrawingId;
|
||||
private float _drawingStartTime = -1f;
|
||||
private readonly HashSet<int> _colors = new();
|
||||
private int _colorApplyCount;
|
||||
private int _shapeAttempts;
|
||||
private bool _firstDrawingTracked;
|
||||
|
||||
public AnalyticsTracker(IEventBus bus, IAnalyticsService analytics)
|
||||
{
|
||||
_bus = bus;
|
||||
@@ -28,40 +49,180 @@ namespace Darkmatter.Services.Analytics
|
||||
|
||||
public void Start()
|
||||
{
|
||||
_subs.Add(_bus.Subscribe<IntroCompletedSignal>(_ => _analytics.LogEvent("intro_completed")));
|
||||
_subs.Add(_bus.Subscribe<PlayBtnClickedSignal>(_ => _analytics.LogEvent("play_clicked")));
|
||||
_subs.Add(_bus.Subscribe<OpenColorBookSignal>(_ => _analytics.LogEvent("colorbook_opened")));
|
||||
_subs.Add(_bus.Subscribe<OpenArtBookSignal>(_ => _analytics.LogEvent("artbook_opened")));
|
||||
_subs.Add(_bus.Subscribe<ReturnToMainMenuSignal>(_ => _analytics.LogEvent("main_menu_returned")));
|
||||
_firstDrawingTracked = PlayerPrefs.GetInt(FirstDrawingKey, 0) == 1;
|
||||
|
||||
_subs.Add(_bus.Subscribe<DrawingSelectedSignal>(s =>
|
||||
_analytics.LogEvent("drawing_selected", "template_id", s.TemplateId)));
|
||||
// Onboarding / activation funnel
|
||||
_subs.Add(_bus.Subscribe<IntroStartedSignal>(_ => _analytics.LogEvent(AnalyticsEvents.IntroStarted)));
|
||||
_subs.Add(_bus.Subscribe<IntroCompletedSignal>(_ => _analytics.LogEvent(AnalyticsEvents.IntroCompleted)));
|
||||
_subs.Add(_bus.Subscribe<PlayBtnClickedSignal>(_ => _analytics.LogEvent(AnalyticsEvents.PlayClicked)));
|
||||
|
||||
_subs.Add(_bus.Subscribe<ShapeBuilderStartedSignal>(s =>
|
||||
_analytics.LogEvent("shape_builder_started", "template_id", s.TemplateId)));
|
||||
// Navigation
|
||||
_subs.Add(_bus.Subscribe<OpenColorBookSignal>(_ => _analytics.LogEvent(AnalyticsEvents.ColorbookOpened)));
|
||||
_subs.Add(_bus.Subscribe<OpenArtBookSignal>(_ => _analytics.LogEvent(AnalyticsEvents.ArtbookOpened)));
|
||||
_subs.Add(_bus.Subscribe<ReturnToMainMenuSignal>(_ => OnReturnToMainMenu()));
|
||||
|
||||
_subs.Add(_bus.Subscribe<ShapeAssembledSignal>(s =>
|
||||
_analytics.LogEvent("shape_assembled", "template_id", s.TemplateId)));
|
||||
// Core gameplay loop
|
||||
_subs.Add(_bus.Subscribe<DrawingSelectedSignal>(s => OnDrawingSelected(s.TemplateId)));
|
||||
_subs.Add(_bus.Subscribe<ColorAppliedSignal>(OnColorApplied));
|
||||
_subs.Add(_bus.Subscribe<ShapeBuilderStartedSignal>(s => OnShapeBuilderStarted(s.TemplateId)));
|
||||
_subs.Add(_bus.Subscribe<PieceSnappedSignal>(_ => _shapeAttempts++));
|
||||
_subs.Add(_bus.Subscribe<PieceUnsnappedSignal>(_ => _shapeAttempts++));
|
||||
_subs.Add(_bus.Subscribe<ShapeAssembledSignal>(s => OnShapeAssembled(s.TemplateId)));
|
||||
_subs.Add(_bus.Subscribe<DrawingCompletedSignal>(OnDrawingCompleted));
|
||||
|
||||
_subs.Add(_bus.Subscribe<DrawingCompletedSignal>(s => _analytics.LogEvent("drawing_completed",
|
||||
// Progression & content
|
||||
_subs.Add(_bus.Subscribe<AllContentCompletedSignal>(OnAllContentCompleted));
|
||||
|
||||
// Capture / save
|
||||
_subs.Add(_bus.Subscribe<GallerySaveStartedSignal>(_ => _analytics.LogEvent(AnalyticsEvents.GallerySaveStarted)));
|
||||
_subs.Add(_bus.Subscribe<GallerySaveCompletedSignal>(OnGallerySaveCompleted));
|
||||
|
||||
// Tutorial
|
||||
_subs.Add(_bus.Subscribe<TutorialStartedSignal>(_ => _analytics.LogEvent(AnalyticsEvents.TutorialStarted)));
|
||||
_subs.Add(_bus.Subscribe<TutorialStepCompletedSignal>(s => _analytics.LogEvent(AnalyticsEvents.TutorialStepCompleted,
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
["template_id"] = s.TemplateId,
|
||||
["completion_count"] = s.CompletionCount,
|
||||
[AnalyticsParams.StepId] = s.StepId,
|
||||
[AnalyticsParams.StepIndex] = s.StepIndex,
|
||||
})));
|
||||
_subs.Add(_bus.Subscribe<TutorialCompletedSignal>(_ => _analytics.LogEvent(AnalyticsEvents.TutorialCompleted)));
|
||||
}
|
||||
|
||||
_subs.Add(_bus.Subscribe<GallerySaveStartedSignal>(_ => _analytics.LogEvent("gallery_save_started")));
|
||||
_subs.Add(_bus.Subscribe<GallerySaveCompletedSignal>(s =>
|
||||
_analytics.LogEvent("gallery_save_completed", "success", s.Success ? "true" : "false")));
|
||||
private void OnDrawingSelected(string drawingId)
|
||||
{
|
||||
// A new selection while a drawing is still open (uncompleted) = the previous one was abandoned.
|
||||
EndActiveDrawingIfAbandoned();
|
||||
|
||||
_subs.Add(_bus.Subscribe<TutorialStartedSignal>(_ => _analytics.LogEvent("tutorial_started")));
|
||||
_subs.Add(_bus.Subscribe<TutorialStepCompletedSignal>(s => _analytics.LogEvent("tutorial_step_completed",
|
||||
new Dictionary<string, object>
|
||||
_activeDrawingId = drawingId;
|
||||
_drawingStartTime = Time.realtimeSinceStartup;
|
||||
_colors.Clear();
|
||||
_colorApplyCount = 0;
|
||||
_shapeAttempts = 0;
|
||||
|
||||
_analytics.LogEvent(AnalyticsEvents.DrawingSelected, AnalyticsParams.DrawingId, drawingId);
|
||||
_analytics.LogEvent(AnalyticsEvents.DrawingStarted, AnalyticsParams.DrawingId, drawingId);
|
||||
_analytics.LogEvent(AnalyticsEvents.LevelStart, AnalyticsParams.LevelName, drawingId);
|
||||
|
||||
if (!_firstDrawingTracked)
|
||||
{
|
||||
_firstDrawingTracked = true;
|
||||
PlayerPrefs.SetInt(FirstDrawingKey, 1);
|
||||
PlayerPrefs.Save();
|
||||
_analytics.LogEvent(AnalyticsEvents.FirstDrawingStarted, AnalyticsParams.DrawingId, drawingId);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnColorApplied(ColorAppliedSignal s)
|
||||
{
|
||||
_colorApplyCount++;
|
||||
Color32 c = s.Color;
|
||||
_colors.Add((c.r << 24) | (c.g << 16) | (c.b << 8) | c.a);
|
||||
|
||||
// High-frequency event: emit only the first apply and every Nth thereafter.
|
||||
if (_colorApplyCount == 1 || _colorApplyCount % ColorApplySampleEvery == 0)
|
||||
{
|
||||
_analytics.LogEvent(AnalyticsEvents.ColorApplied, new Dictionary<string, object>
|
||||
{
|
||||
["step_id"] = s.StepId,
|
||||
["step_index"] = s.StepIndex,
|
||||
})));
|
||||
_subs.Add(_bus.Subscribe<TutorialCompletedSignal>(_ => _analytics.LogEvent("tutorial_completed")));
|
||||
[AnalyticsParams.DrawingId] = _activeDrawingId ?? string.Empty,
|
||||
[AnalyticsParams.ApplyIndex] = _colorApplyCount,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void OnShapeBuilderStarted(string drawingId)
|
||||
{
|
||||
_shapeAttempts = 0;
|
||||
_analytics.LogEvent(AnalyticsEvents.ShapeBuilderStarted, AnalyticsParams.DrawingId, drawingId);
|
||||
}
|
||||
|
||||
private void OnShapeAssembled(string drawingId)
|
||||
{
|
||||
_analytics.LogEvent(AnalyticsEvents.ShapeAssembled, new Dictionary<string, object>
|
||||
{
|
||||
[AnalyticsParams.DrawingId] = drawingId,
|
||||
[AnalyticsParams.Attempts] = _shapeAttempts,
|
||||
});
|
||||
}
|
||||
|
||||
private void OnDrawingCompleted(DrawingCompletedSignal s)
|
||||
{
|
||||
var p = new Dictionary<string, object>
|
||||
{
|
||||
[AnalyticsParams.DrawingId] = s.TemplateId,
|
||||
[AnalyticsParams.CompletionCount] = s.CompletionCount,
|
||||
[AnalyticsParams.ColorsUsed] = _colors.Count,
|
||||
};
|
||||
float dur = ElapsedDrawingSeconds();
|
||||
if (dur >= 0f) p[AnalyticsParams.DurationSeconds] = dur;
|
||||
_analytics.LogEvent(AnalyticsEvents.DrawingCompleted, p);
|
||||
|
||||
// GA4 recommended games event (lights up built-in level funnels).
|
||||
_analytics.LogEvent(AnalyticsEvents.LevelComplete, new Dictionary<string, object>
|
||||
{
|
||||
[AnalyticsParams.LevelName] = s.TemplateId,
|
||||
[AnalyticsParams.Success] = 1,
|
||||
});
|
||||
|
||||
ClearActiveDrawing();
|
||||
}
|
||||
|
||||
private void OnAllContentCompleted(AllContentCompletedSignal s)
|
||||
{
|
||||
if (PlayerPrefs.GetInt(AllContentKey, 0) == 1) return; // once ever
|
||||
PlayerPrefs.SetInt(AllContentKey, 1);
|
||||
PlayerPrefs.Save();
|
||||
_analytics.LogEvent(AnalyticsEvents.AllContentCompleted,
|
||||
AnalyticsParams.CompletionCount, s.CompletedCount.ToString());
|
||||
}
|
||||
|
||||
private void OnGallerySaveCompleted(GallerySaveCompletedSignal s)
|
||||
{
|
||||
_analytics.LogEvent(AnalyticsEvents.GallerySaveCompleted, AnalyticsParams.Success, s.Success ? "true" : "false");
|
||||
_analytics.LogEvent(AnalyticsEvents.DrawingSaved, new Dictionary<string, object>
|
||||
{
|
||||
[AnalyticsParams.DrawingId] = _activeDrawingId ?? string.Empty,
|
||||
[AnalyticsParams.Success] = s.Success ? 1 : 0,
|
||||
});
|
||||
}
|
||||
|
||||
private void OnReturnToMainMenu()
|
||||
{
|
||||
EndActiveDrawingIfAbandoned();
|
||||
_analytics.LogEvent(AnalyticsEvents.MainMenuReturned);
|
||||
}
|
||||
|
||||
// Emits drawing_abandoned for an open, uncompleted drawing. Completion clears _activeDrawingId, so
|
||||
// reaching here with a non-null id means the drawing was left without finishing — the leak signal.
|
||||
private void EndActiveDrawingIfAbandoned()
|
||||
{
|
||||
if (_activeDrawingId == null) return;
|
||||
|
||||
var p = new Dictionary<string, object>
|
||||
{
|
||||
[AnalyticsParams.DrawingId] = _activeDrawingId,
|
||||
[AnalyticsParams.ColorsUsed] = _colors.Count,
|
||||
};
|
||||
float dur = ElapsedDrawingSeconds();
|
||||
if (dur >= 0f) p[AnalyticsParams.DurationSeconds] = dur;
|
||||
_analytics.LogEvent(AnalyticsEvents.DrawingAbandoned, p);
|
||||
|
||||
ClearActiveDrawing();
|
||||
}
|
||||
|
||||
private float ElapsedDrawingSeconds()
|
||||
{
|
||||
if (_drawingStartTime < 0f) return -1f;
|
||||
float d = Time.realtimeSinceStartup - _drawingStartTime;
|
||||
return d >= 0f ? d : -1f; // guard against realtime baseline reset across an app relaunch
|
||||
}
|
||||
|
||||
private void ClearActiveDrawing()
|
||||
{
|
||||
_activeDrawingId = null;
|
||||
_drawingStartTime = -1f;
|
||||
_colors.Clear();
|
||||
_colorApplyCount = 0;
|
||||
_shapeAttempts = 0;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using VContainer.Unity;
|
||||
#if UNITY_ANDROID
|
||||
using AppsFlyerSDK;
|
||||
using Firebase;
|
||||
using Firebase.Messaging;
|
||||
#endif
|
||||
|
||||
namespace Darkmatter.Services.Analytics
|
||||
{
|
||||
/// <summary>
|
||||
/// Forwards the Firebase Cloud Messaging registration token to AppsFlyer so it can measure uninstalls.
|
||||
/// Android-only: AppsFlyer's server-uninstall measurement keys off the FCM token. iOS uninstall uses the
|
||||
/// APNs device token via a different AppsFlyer API and is intentionally not wired here, so FCM is not
|
||||
/// initialised on iOS.
|
||||
///
|
||||
/// AppsFlyer itself is started in the Boot scene (AppsFlyerObjectScript); this only feeds it the token.
|
||||
/// </summary>
|
||||
public class AppsFlyerUninstallTracker : IAsyncStartable, IDisposable
|
||||
{
|
||||
public async UniTask StartAsync(CancellationToken cancellation = default)
|
||||
{
|
||||
#if UNITY_ANDROID
|
||||
DependencyStatus status;
|
||||
try
|
||||
{
|
||||
status = await FirebaseApp.CheckAndFixDependenciesAsync().AsUniTask();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError($"[AppsFlyerUninstall] Firebase init failed: {e}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (status != DependencyStatus.Available)
|
||||
{
|
||||
Debug.LogError($"[AppsFlyerUninstall] Firebase deps unavailable: {status}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Subscribing triggers FCM registration; the current token is delivered here on every refresh.
|
||||
FirebaseMessaging.TokenReceived += OnTokenReceived;
|
||||
#else
|
||||
await UniTask.CompletedTask;
|
||||
#endif
|
||||
}
|
||||
|
||||
#if UNITY_ANDROID
|
||||
private static void OnTokenReceived(object sender, TokenReceivedEventArgs e)
|
||||
{
|
||||
try { AppsFlyer.updateServerUninstallToken(e.Token); }
|
||||
catch (Exception ex) { Debug.LogError($"[AppsFlyerUninstall] updateServerUninstallToken failed: {ex}"); }
|
||||
}
|
||||
#endif
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
#if UNITY_ANDROID
|
||||
FirebaseMessaging.TokenReceived -= OnTokenReceived;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7a08ecb787f14b11a4f8ce163f223a53
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Darkmatter.Core.Contracts.Services.Analytics;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Darkmatter.Services.Analytics
|
||||
{
|
||||
/// <summary>
|
||||
/// The single <see cref="IAnalyticsService"/> the app consumes. Fans every call out to all sinks
|
||||
/// (Firebase + Facebook) so call sites log once and both backends receive it. A throwing sink is
|
||||
/// isolated so it can't break the others. To add a sink: add a constructor arg and include it below.
|
||||
/// </summary>
|
||||
public sealed class CompositeAnalyticsService : IAnalyticsService
|
||||
{
|
||||
private readonly IAnalyticsService[] _sinks;
|
||||
|
||||
public CompositeAnalyticsService(FirebaseAnalyticsSystem firebase, FacebookAnalyticsSystem facebook)
|
||||
{
|
||||
_sinks = new IAnalyticsService[] { firebase, facebook };
|
||||
}
|
||||
|
||||
public void LogEvent(string name) =>
|
||||
ForEach(s => s.LogEvent(name));
|
||||
|
||||
public void LogEvent(string name, string paramName, string paramValue) =>
|
||||
ForEach(s => s.LogEvent(name, paramName, paramValue));
|
||||
|
||||
public void LogEvent(string name, IReadOnlyDictionary<string, object> parameters) =>
|
||||
ForEach(s => s.LogEvent(name, parameters));
|
||||
|
||||
public void SetUserProperty(string name, string value) =>
|
||||
ForEach(s => s.SetUserProperty(name, value));
|
||||
|
||||
private void ForEach(Action<IAnalyticsService> action)
|
||||
{
|
||||
foreach (var sink in _sinks)
|
||||
{
|
||||
try { action(sink); }
|
||||
catch (Exception e) { Debug.LogError($"[Analytics] Sink {sink.GetType().Name} failed: {e}"); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fbc94bef99b574c83a85b6886ff7fd03
|
||||
@@ -0,0 +1,51 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Darkmatter.Core.Contracts.Services.Analytics;
|
||||
using UnityEngine;
|
||||
using VContainer.Unity;
|
||||
|
||||
namespace Darkmatter.Services.Analytics
|
||||
{
|
||||
/// <summary>
|
||||
/// Forwards Unity error/exception logs to analytics as a sampled <c>error_shown</c> event so
|
||||
/// crash-adjacent friction surfaces in funnels. Capped per session and deduped by error_type to avoid
|
||||
/// event floods / GA4 quota burn. No message text or PII is sent — only the leading token of the
|
||||
/// condition (e.g. "NullReferenceException"). Distinct from Crashlytics (which logs full crashes).
|
||||
/// </summary>
|
||||
public sealed class ErrorAnalyticsTracker : IStartable, IDisposable
|
||||
{
|
||||
private const int MaxPerSession = 25;
|
||||
|
||||
private readonly IAnalyticsService _analytics;
|
||||
private readonly HashSet<string> _seen = new();
|
||||
private int _count;
|
||||
|
||||
public ErrorAnalyticsTracker(IAnalyticsService analytics) => _analytics = analytics;
|
||||
|
||||
public void Start() => Application.logMessageReceived += OnLog;
|
||||
public void Dispose() => Application.logMessageReceived -= OnLog;
|
||||
|
||||
private void OnLog(string condition, string stackTrace, LogType type)
|
||||
{
|
||||
if (type != LogType.Error && type != LogType.Exception) return;
|
||||
if (_count >= MaxPerSession) return;
|
||||
|
||||
string errorType = ExtractType(condition);
|
||||
if (!_seen.Add(errorType)) return; // one per distinct type per session
|
||||
|
||||
_count++;
|
||||
_analytics.LogEvent(AnalyticsEvents.ErrorShown, AnalyticsParams.ErrorType, errorType);
|
||||
}
|
||||
|
||||
// Leading token only — keeps cardinality low and avoids leaking message contents / PII.
|
||||
private static string ExtractType(string condition)
|
||||
{
|
||||
if (string.IsNullOrEmpty(condition)) return "unknown";
|
||||
int colon = condition.IndexOf(':');
|
||||
string head = (colon > 0 ? condition.Substring(0, colon) : condition).Trim();
|
||||
int space = head.IndexOf(' ');
|
||||
if (space > 0) head = head.Substring(0, space);
|
||||
return head.Length > 40 ? head.Substring(0, 40) : head;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 05e0b4bc8382a40c0a78b122ac37a9fa
|
||||
@@ -0,0 +1,108 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Darkmatter.Core.Contracts.Services.Analytics;
|
||||
using Facebook.Unity;
|
||||
using UnityEngine;
|
||||
using VContainer.Unity;
|
||||
|
||||
namespace Darkmatter.Services.Analytics
|
||||
{
|
||||
/// <summary>
|
||||
/// Facebook App Events sink. Mirrors <see cref="FirebaseAnalyticsSystem"/>'s readiness queue: events
|
||||
/// logged before FB.Init completes are buffered and flushed on init.
|
||||
///
|
||||
/// Compliance: child-directed app — advertiser-ID collection is OFF (FacebookSettings), so App Events
|
||||
/// flow without IDFA. Do NOT enable advertiser tracking / ATT here. See memory child-directed-coppa.
|
||||
/// </summary>
|
||||
public class FacebookAnalyticsSystem : IAnalyticsService, IStartable
|
||||
{
|
||||
private bool _ready;
|
||||
private readonly Queue<Action> _pending = new();
|
||||
|
||||
public void Start()
|
||||
{
|
||||
if (FB.IsInitialized) OnInitialized();
|
||||
else FB.Init(OnInitialized);
|
||||
}
|
||||
|
||||
private void OnInitialized()
|
||||
{
|
||||
FB.ActivateApp();
|
||||
_ready = true;
|
||||
while (_pending.Count > 0)
|
||||
{
|
||||
try { _pending.Dequeue().Invoke(); }
|
||||
catch (Exception e) { Debug.LogError($"[FacebookAnalytics] Queued event failed: {e}"); }
|
||||
}
|
||||
}
|
||||
|
||||
public void LogEvent(string name) =>
|
||||
Run(() => FB.LogAppEvent(MapName(name)));
|
||||
|
||||
public void LogEvent(string name, string paramName, string paramValue) =>
|
||||
Run(() => FB.LogAppEvent(MapName(name), null,
|
||||
new Dictionary<string, object> { [paramName] = paramValue ?? string.Empty }));
|
||||
|
||||
public void LogEvent(string name, IReadOnlyDictionary<string, object> parameters)
|
||||
{
|
||||
if (parameters == null || parameters.Count == 0)
|
||||
{
|
||||
LogEvent(name);
|
||||
return;
|
||||
}
|
||||
|
||||
var dict = new Dictionary<string, object>(parameters.Count);
|
||||
float? valueToSum = null;
|
||||
foreach (var kv in parameters)
|
||||
{
|
||||
// GA4 ad_impression carries revenue in "value"; surface it to FB as the summable value.
|
||||
if (kv.Key == AnalyticsParams.Value && TryToFloat(kv.Value, out var v)) valueToSum = v;
|
||||
dict[kv.Key] = ToFbValue(kv.Value);
|
||||
}
|
||||
Run(() => FB.LogAppEvent(MapName(name), valueToSum, dict));
|
||||
}
|
||||
|
||||
// Facebook App Events has no user-property concept.
|
||||
public void SetUserProperty(string name, string value) { }
|
||||
|
||||
private void Run(Action action)
|
||||
{
|
||||
if (_ready)
|
||||
{
|
||||
try { action(); }
|
||||
catch (Exception e) { Debug.LogError($"[FacebookAnalytics] Event failed: {e}"); }
|
||||
}
|
||||
else _pending.Enqueue(action);
|
||||
}
|
||||
|
||||
// Map our snake_case names to Facebook standard events where one exists; else log as custom.
|
||||
// (This SDK's AppEventName has no Ad* constants, so ad_impression / ad_clicked stay custom.)
|
||||
private static string MapName(string name) => name switch
|
||||
{
|
||||
AnalyticsEvents.TutorialCompleted => AppEventName.CompletedTutorial,
|
||||
AnalyticsEvents.LevelComplete => AppEventName.AchievedLevel,
|
||||
_ => name
|
||||
};
|
||||
|
||||
private static object ToFbValue(object value) => value switch
|
||||
{
|
||||
null => string.Empty,
|
||||
bool b => b ? 1 : 0,
|
||||
string s => s,
|
||||
int or long or float or double => value,
|
||||
_ => value.ToString()
|
||||
};
|
||||
|
||||
private static bool TryToFloat(object value, out float result)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case float f: result = f; return true;
|
||||
case double d: result = (float)d; return true;
|
||||
case int i: result = i; return true;
|
||||
case long l: result = l; return true;
|
||||
default: result = 0f; return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 196c5d190005d45f885236d8765813d1
|
||||
BIN
Assets/Darkmatter/Content/Colorbook UI/Prefabs/.DS_Store
vendored
Normal file
BIN
Assets/Darkmatter/Content/Colorbook UI/Prefabs/.DS_Store
vendored
Normal file
Binary file not shown.
@@ -330,15 +330,15 @@ PrefabInstance:
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3}
|
||||
propertyPath: m_LocalScale.x
|
||||
value: 0.24355799
|
||||
value: 0.4092992
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3}
|
||||
propertyPath: m_LocalScale.y
|
||||
value: 0.24355799
|
||||
value: 0.4092992
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3}
|
||||
propertyPath: m_LocalScale.z
|
||||
value: 0.24355799
|
||||
value: 0.4092992
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3}
|
||||
propertyPath: m_LocalPosition.x
|
||||
@@ -370,11 +370,11 @@ PrefabInstance:
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3}
|
||||
propertyPath: m_AnchoredPosition.x
|
||||
value: -381.463
|
||||
value: -404.24997
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3}
|
||||
propertyPath: m_AnchoredPosition.y
|
||||
value: 20.077026
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3}
|
||||
propertyPath: m_LocalEulerAnglesHint.x
|
||||
@@ -562,15 +562,15 @@ PrefabInstance:
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3}
|
||||
propertyPath: m_LocalScale.x
|
||||
value: 0.24355799
|
||||
value: 0.4092992
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3}
|
||||
propertyPath: m_LocalScale.y
|
||||
value: 0.24355799
|
||||
value: 0.4092992
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3}
|
||||
propertyPath: m_LocalScale.z
|
||||
value: 0.24355799
|
||||
value: 0.4092992
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3}
|
||||
propertyPath: m_LocalPosition.x
|
||||
@@ -602,11 +602,11 @@ PrefabInstance:
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3}
|
||||
propertyPath: m_AnchoredPosition.x
|
||||
value: -204.58456
|
||||
value: -107
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3}
|
||||
propertyPath: m_AnchoredPosition.y
|
||||
value: 20.077026
|
||||
value: -0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3}
|
||||
propertyPath: m_LocalEulerAnglesHint.x
|
||||
|
||||
@@ -12,6 +12,7 @@ GameObject:
|
||||
- component: {fileID: 501846013816502432}
|
||||
- component: {fileID: 8665832532240036901}
|
||||
- component: {fileID: 8513459380562180911}
|
||||
- component: {fileID: 7638529631096518376}
|
||||
m_Layer: 5
|
||||
m_Name: Recatngle
|
||||
m_TagString: Untagged
|
||||
@@ -90,3 +91,18 @@ MonoBehaviour:
|
||||
m_EditorClassIdentifier: Features.ShapeBuilder::Darkmatter.Features.ShapeBuilder.UI.SlotMarker
|
||||
shape: {fileID: 11400000, guid: 4a67406eb6fe043628d2b6a4e0c970ba, type: 2}
|
||||
outline: {fileID: 0}
|
||||
--- !u!114 &7638529631096518376
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 4279528263583881541}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: e19747de3f5aca642ab2be37e372fb86, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Outline
|
||||
m_EffectColor: {r: 0, g: 0, b: 0, a: 0.5}
|
||||
m_EffectDistance: {x: 2.5, y: -2.5}
|
||||
m_UseGraphicAlpha: 1
|
||||
|
||||
BIN
Assets/Darkmatter/Content/Colorbook UI/Prefabs/UI/.DS_Store
vendored
Normal file
BIN
Assets/Darkmatter/Content/Colorbook UI/Prefabs/UI/.DS_Store
vendored
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f9a026758de8463bb1416c9a09c38c11
|
||||
PrefabImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -198,6 +198,9 @@ MonoBehaviour:
|
||||
haloPulseScale: 1.18
|
||||
pulseDuration: 0.6
|
||||
dragHandDuration: 1.1
|
||||
dragHandTipOffset: {x: 0, y: 70}
|
||||
dragBubbleAtTop: 1
|
||||
dragBubbleEdgeMargin: 150
|
||||
--- !u!225 &6008915593776814208
|
||||
CanvasGroup:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -352,7 +355,7 @@ MonoBehaviour:
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 100600}
|
||||
m_Enabled: 1
|
||||
m_Enabled: 0
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
|
||||
m_Name:
|
||||
@@ -410,7 +413,7 @@ RectTransform:
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 130, y: 130}
|
||||
m_SizeDelta: {x: 200, y: 200}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &100702
|
||||
CanvasRenderer:
|
||||
@@ -465,7 +468,7 @@ GameObject:
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
m_IsActive: 0
|
||||
--- !u!224 &100801
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -524,7 +527,7 @@ RectTransform:
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 91}
|
||||
m_SizeDelta: {x: -235, y: 91}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &100902
|
||||
CanvasRenderer:
|
||||
@@ -639,8 +642,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 463.445, y: -95.50001}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 207.45999, y: -95.50001}
|
||||
m_SizeDelta: {x: 214.92, y: 72.61}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &101002
|
||||
CanvasRenderer:
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
BIN
Assets/Darkmatter/Content/Sprites/World/.DS_Store
vendored
Normal file
BIN
Assets/Darkmatter/Content/Sprites/World/.DS_Store
vendored
Normal file
Binary file not shown.
BIN
Assets/Darkmatter/Content/Sprites/World/Ask Coins Sprites/.DS_Store
vendored
Normal file
BIN
Assets/Darkmatter/Content/Sprites/World/Ask Coins Sprites/.DS_Store
vendored
Normal file
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 22 KiB |
@@ -49,7 +49,7 @@ TextureImporter:
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 270, y: 191, z: 255, w: 110}
|
||||
spriteBorder: {x: 741, y: 322, z: 925, w: 337}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 1
|
||||
|
||||
@@ -12,8 +12,14 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: e6352f0162df4adf99a5945e25c6bf40, type: 3}
|
||||
m_Name: AdUnitCatalog
|
||||
m_EditorClassIdentifier: Core::Darkmatter.Core.Data.Static.Services.Ads.AdUnitCatalogSO
|
||||
androidAppId: ca-app-pub-3940256099942544~3347511713
|
||||
iosAppId: ca-app-pub-3940256099942544~1458002511
|
||||
androidAppId: ca-app-pub-3810264200292720~3342731243
|
||||
iosAppId: ca-app-pub-3810264200292720~5597889728
|
||||
useTestUnits: 0
|
||||
testDeviceIds: []
|
||||
entries: []
|
||||
entries:
|
||||
- Format: 1
|
||||
AndroidUnitId: ca-app-pub-3810264200292720/8753612666
|
||||
IosUnitId: ca-app-pub-3810264200292720/8064711900
|
||||
- Format: 2
|
||||
AndroidUnitId: ca-app-pub-3810264200292720/9189954299
|
||||
IosUnitId: ca-app-pub-3810264200292720/3366781979
|
||||
|
||||
@@ -12,32 +12,8 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: 52d6fdba64cc3491880636e34ed593d0, type: 3}
|
||||
m_Name: ShapeBuilderConfig
|
||||
m_EditorClassIdentifier: Core::Darkmatter.Core.Data.Static.Features.ShapeBuilder.ShapeBuilderConfig
|
||||
snapRadius: 200
|
||||
previewRadius: 300
|
||||
catchRadiusScale: 1.5
|
||||
snapDuration: 0.25
|
||||
returnDuration: 0.25
|
||||
dragScale: 1.15
|
||||
previewCurve:
|
||||
serializedVersion: 2
|
||||
m_Curve:
|
||||
- serializedVersion: 3
|
||||
time: 0
|
||||
value: 0
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
- serializedVersion: 3
|
||||
time: 1
|
||||
value: 1
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
weightedMode: 0
|
||||
inWeight: 0
|
||||
outWeight: 0
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
dragAlpha: 0.7
|
||||
|
||||
@@ -17,6 +17,24 @@ MonoBehaviour:
|
||||
finishText: Now finish the puzzle!
|
||||
colorText: Choose a color!
|
||||
paintText: Tap the picture to color it!
|
||||
finishColoringText: Color the whole picture!
|
||||
nextText: Tap Next when you're done!
|
||||
doneText: Yay! You did it!
|
||||
pickBubble: 0
|
||||
pickBubbleOffset: {x: 0, y: 0}
|
||||
dragBubble: 1
|
||||
dragBubbleOffset: {x: 200, y: 0}
|
||||
finishBubble: 1
|
||||
finishBubbleOffset: {x: 200, y: 0}
|
||||
colorBubble: 0
|
||||
colorBubbleOffset: {x: 0, y: 0}
|
||||
paintBubble: 0
|
||||
paintBubbleOffset: {x: 0, y: 0}
|
||||
finishColoringBubble: 1
|
||||
finishColoringBubbleOffset: {x: 200, y: 0}
|
||||
nextBubble: 0
|
||||
nextBubbleOffset: {x: 0, y: 0}
|
||||
doneBubble: 0
|
||||
doneBubbleOffset: {x: 0, y: 0}
|
||||
stepTimeoutSeconds: 45
|
||||
actionTimeoutSeconds: 0
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2067,7 +2067,7 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: -0.0006763566}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0, y: 1}
|
||||
--- !u!114 &1155451093
|
||||
@@ -2098,11 +2098,11 @@ MonoBehaviour:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.VerticalLayoutGroup
|
||||
m_Padding:
|
||||
m_Left: 0
|
||||
m_Right: 0
|
||||
m_Right: 41
|
||||
m_Top: 0
|
||||
m_Bottom: 0
|
||||
m_ChildAlignment: 1
|
||||
m_Spacing: 30
|
||||
m_Spacing: 120
|
||||
m_ChildForceExpandWidth: 0
|
||||
m_ChildForceExpandHeight: 0
|
||||
m_ChildControlWidth: 0
|
||||
@@ -2339,8 +2339,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: -82.37799}
|
||||
m_SizeDelta: {x: -191.90698, y: -574.56104}
|
||||
m_AnchoredPosition: {x: 0, y: -115.30393}
|
||||
m_SizeDelta: {x: -191.907, y: -626.817}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1281354569
|
||||
MonoBehaviour:
|
||||
|
||||
62
Assets/Editor/DisableBitcode.cs
Normal file
62
Assets/Editor/DisableBitcode.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
|
||||
*
|
||||
* You are hereby granted a non-exclusive, worldwide, royalty-free license to use,
|
||||
* copy, modify, and distribute this software in source code or binary form for use
|
||||
* in connection with the web services and APIs provided by Facebook.
|
||||
*
|
||||
* As with any software that integrates with the Facebook platform, your use of
|
||||
* this software is subject to the Facebook Developer Principles and Policies
|
||||
* [http://developers.facebook.com/policy/]. This copyright notice shall be
|
||||
* included in all copies or substantial portions of the software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using System.IO;
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
#if UNITY_IOS
|
||||
using UnityEditor.Build.Reporting;
|
||||
using UnityEditor.iOS.Xcode;
|
||||
#endif
|
||||
using UnityEditor.Callbacks;
|
||||
|
||||
|
||||
namespace Facebook.Unity.PostProcess
|
||||
{
|
||||
/// <summary>
|
||||
/// Automatically disables Bitcode on iOS builds
|
||||
/// </summary>
|
||||
public static class DisableBitcode
|
||||
{
|
||||
[PostProcessBuildAttribute(999)]
|
||||
public static void OnPostProcessBuild(BuildTarget buildTarget, string pathToBuildProject)
|
||||
{
|
||||
#if UNITY_IOS
|
||||
if (buildTarget != BuildTarget.iOS) return;
|
||||
string projectPath = pathToBuildProject + "/Unity-iPhone.xcodeproj/project.pbxproj";
|
||||
PBXProject pbxProject = new PBXProject();
|
||||
pbxProject.ReadFromFile(projectPath);
|
||||
|
||||
//Disabling Bitcode on all targets
|
||||
//Main
|
||||
string target = pbxProject.GetUnityMainTargetGuid();
|
||||
pbxProject.SetBuildProperty(target, "ENABLE_BITCODE", "NO");
|
||||
//Unity Tests
|
||||
target = pbxProject.TargetGuidByName(PBXProject.GetUnityTestTargetName());
|
||||
pbxProject.SetBuildProperty(target, "ENABLE_BITCODE", "NO");
|
||||
//Unity Framework
|
||||
target = pbxProject.GetUnityFrameworkTargetGuid();
|
||||
pbxProject.SetBuildProperty(target, "ENABLE_BITCODE", "NO");
|
||||
|
||||
pbxProject.WriteToFile(projectPath);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Editor/DisableBitcode.cs.meta
Normal file
11
Assets/Editor/DisableBitcode.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 201fa01d552b84a74917c9cc8a4c297c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
107
Assets/Editor/EmbedFacebookFrameworks.cs
Normal file
107
Assets/Editor/EmbedFacebookFrameworks.cs
Normal file
@@ -0,0 +1,107 @@
|
||||
using System.IO;
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
#if UNITY_IOS
|
||||
using UnityEditor.iOS.Xcode;
|
||||
using UnityEditor.iOS.Xcode.Extensions;
|
||||
#endif
|
||||
using UnityEditor.Callbacks;
|
||||
|
||||
namespace Darkmatter.Editor.PostProcess
|
||||
{
|
||||
/// <summary>
|
||||
/// Embeds the Facebook iOS dynamic frameworks (FBSDKCoreKit, FBAEMKit, ...) into the
|
||||
/// main app target on every iOS export.
|
||||
///
|
||||
/// Why this exists: EDM4U is configured with "Link frameworks statically"
|
||||
/// (Google.IOSResolver.PodfileStaticLinkFrameworks = 1) — needed for Firebase /
|
||||
/// AppsFlyer. With static linkage CocoaPods does NOT generate the
|
||||
/// "[CP] Embed Pods Frameworks" phase, so Facebook's prebuilt *dynamic* xcframeworks
|
||||
/// get linked but never copied into the app bundle. At launch dyld then fails with:
|
||||
/// Library not loaded: @rpath/FBAEMKit.framework/FBAEMKit
|
||||
/// This reproduces the manual "Embed & Sign" step automatically, and leaves the
|
||||
/// static linkage of every other SDK untouched.
|
||||
/// </summary>
|
||||
public static class EmbedFacebookFrameworks
|
||||
{
|
||||
// FB pods from Assets/FacebookSDK/Plugins/Editor/Dependencies.xml, plus FBAEMKit
|
||||
// which CocoaPods pulls in transitively via FBSDKCoreKit.
|
||||
static readonly string[] Frameworks =
|
||||
{
|
||||
"FBAEMKit",
|
||||
"FBSDKCoreKit",
|
||||
"FBSDKCoreKit_Basics",
|
||||
"FBSDKGamingServicesKit",
|
||||
"FBSDKLoginKit",
|
||||
"FBSDKShareKit",
|
||||
};
|
||||
|
||||
// Order 100: after EDM4U's pod install (low order) so Pods/ exists, before
|
||||
// DisableBitcode (999). Exact timing is not critical — paths are project-relative
|
||||
// and resolved by Xcode at build time.
|
||||
[PostProcessBuild(100)]
|
||||
public static void OnPostProcessBuild(BuildTarget buildTarget, string pathToBuiltProject)
|
||||
{
|
||||
#if UNITY_IOS
|
||||
if (buildTarget != BuildTarget.iOS) return;
|
||||
|
||||
string projectPath = PBXProject.GetPBXProjectPath(pathToBuiltProject);
|
||||
var project = new PBXProject();
|
||||
project.ReadFromFile(projectPath);
|
||||
|
||||
string mainTarget = project.GetUnityMainTargetGuid();
|
||||
|
||||
int embedded = 0;
|
||||
foreach (string fw in Frameworks)
|
||||
{
|
||||
string relPath = ResolveFrameworkPath(pathToBuiltProject, fw);
|
||||
|
||||
// Idempotent: a fresh project is exported each build, but guard anyway.
|
||||
if (project.ContainsFileByProjectPath(relPath))
|
||||
continue;
|
||||
|
||||
string fileGuid = project.AddFile(relPath, relPath);
|
||||
project.AddFileToBuild(mainTarget, fileGuid); // link
|
||||
project.AddFileToEmbedFrameworks(mainTarget, fileGuid); // Embed & Sign
|
||||
embedded++;
|
||||
}
|
||||
|
||||
// FB frameworks bundle their own Swift runtime, and the FB SDK ships Swift
|
||||
// sources compiled into UnityFramework. Embedding the Swift std libs in BOTH
|
||||
// the app target and UnityFramework triggers duplicate-dylib signing failures
|
||||
// on archive. Only the main target should embed them. Remove this line if your
|
||||
// setup reports "Swift runtime not found".
|
||||
string unityFramework = project.GetUnityFrameworkTargetGuid();
|
||||
project.SetBuildProperty(unityFramework, "ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES", "NO");
|
||||
|
||||
project.WriteToFile(projectPath);
|
||||
Debug.Log($"[EmbedFacebookFrameworks] Embedded {embedded}/{Frameworks.Length} Facebook frameworks into the main iOS target.");
|
||||
#endif
|
||||
}
|
||||
|
||||
#if UNITY_IOS
|
||||
// Current FB SDK vendors the binary at Pods/<fw>/XCFrameworks/<fw>.xcframework;
|
||||
// older layouts used Frameworks/. Probe both. If Pods/ isn't installed yet at
|
||||
// post-process time, fall back to the XCFrameworks path (resolves at build time).
|
||||
static string ResolveFrameworkPath(string buildRoot, string fw)
|
||||
{
|
||||
string name = fw + ".xcframework";
|
||||
string[] candidates =
|
||||
{
|
||||
Path.Combine("Pods", fw, "XCFrameworks", name),
|
||||
Path.Combine("Pods", fw, "Frameworks", name),
|
||||
};
|
||||
|
||||
foreach (string rel in candidates)
|
||||
{
|
||||
if (Directory.Exists(Path.Combine(buildRoot, rel)))
|
||||
return rel;
|
||||
}
|
||||
|
||||
Debug.LogWarning($"[EmbedFacebookFrameworks] {name} not found under Pods/ yet; " +
|
||||
$"referencing {candidates[0]} (valid once pod install has run).");
|
||||
return candidates[0];
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
2
Assets/Editor/EmbedFacebookFrameworks.cs.meta
Normal file
2
Assets/Editor/EmbedFacebookFrameworks.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 71252a86bda034fb8aee21f1fc29836a
|
||||
8
Assets/ExternalDependencyManager.meta
Normal file
8
Assets/ExternalDependencyManager.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6c086f9271605419c9a7c97154160bda
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/ExternalDependencyManager/Editor.meta
Normal file
8
Assets/ExternalDependencyManager/Editor.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c4a270ade2b3046fe925705bbfa612a6
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
1435
Assets/ExternalDependencyManager/Editor/CHANGELOG.md
Executable file
1435
Assets/ExternalDependencyManager/Editor/CHANGELOG.md
Executable file
File diff suppressed because it is too large
Load Diff
11
Assets/ExternalDependencyManager/Editor/CHANGELOG.md.meta
Normal file
11
Assets/ExternalDependencyManager/Editor/CHANGELOG.md.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ff38baa7337e404f8a2bb7852f8f731a
|
||||
labels:
|
||||
- gvh
|
||||
- gvh_version-13.11.0
|
||||
- gvhp_exportpath-ExternalDependencyManager/Editor/CHANGELOG.md
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
245
Assets/ExternalDependencyManager/Editor/LICENSE
Executable file
245
Assets/ExternalDependencyManager/Editor/LICENSE
Executable file
@@ -0,0 +1,245 @@
|
||||
Copyright (C) 2014 Google Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
====================================================================================================
|
||||
This package uses MiniJSON
|
||||
|
||||
Copyright (c) 2013 Calvin Rien
|
||||
|
||||
Based on the JSON parser by Patrick van Bergen
|
||||
http://techblog.procurios.nl/k/618/news/view/14605/14863/How-do-I-write-my-own-parser-for-JSON.html
|
||||
|
||||
Simplified it so that it doesn't throw exceptions
|
||||
and can be used in Unity iPhone with maximum code stripping.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
11
Assets/ExternalDependencyManager/Editor/LICENSE.meta
Normal file
11
Assets/ExternalDependencyManager/Editor/LICENSE.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: eca58a95d98c4fc297d3ad17b7bf24c6
|
||||
labels:
|
||||
- gvh
|
||||
- gvh_version-13.11.0
|
||||
- gvhp_exportpath-ExternalDependencyManager/Editor/LICENSE
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
936
Assets/ExternalDependencyManager/Editor/README.md
Executable file
936
Assets/ExternalDependencyManager/Editor/README.md
Executable file
@@ -0,0 +1,936 @@
|
||||
# External Dependency Manager for Unity
|
||||
|
||||
[](https://openupm.com/packages/com.google.external-dependency-manager/)
|
||||
[](https://openupm.com/packages/com.google.external-dependency-manager/)
|
||||
|
||||
## Overview
|
||||
|
||||
The External Dependency Manager for Unity (EDM4U) (formerly Play Services
|
||||
Resolver/Jar Resolver) is intended to be used by any Unity package or user that
|
||||
requires:
|
||||
|
||||
* Android specific libraries (e.g
|
||||
[AARs](https://developer.android.com/studio/projects/android-library.html))
|
||||
|
||||
* iOS [CocoaPods](https://cocoapods.org/)
|
||||
|
||||
* Version management of transitive dependencies
|
||||
|
||||
* Management of Package Manager (PM) Registries
|
||||
|
||||
If you want to add and use iOS/Android dependencies directly in your project,
|
||||
then you should to install EDM4U in your project.
|
||||
|
||||
If you are a package user and the plugin you are using depends on EDM4U, *and*
|
||||
the package does not include EDM4U as a package dependency already, then you
|
||||
should to install EDM4U in your project.
|
||||
|
||||
If you are a UPM package maintainer and your package requires EDM4U, then you
|
||||
should add EDM4U as a
|
||||
[package dependency](https://docs.unity3d.com/2019.3/Documentation/Manual/upm-dependencies.html)
|
||||
in your package manifest (`package.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"com.google.external-dependency-manager": "1.2.178"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You should still install EDM4U to test out the package during development.
|
||||
|
||||
If you are a legacy `.unitypackage` package maintainer and your package requires
|
||||
EDM4U, please ask the user to install EDM4U separately. You should install EDM4U
|
||||
to test out the package during development.
|
||||
|
||||
Updated releases are available on
|
||||
[GitHub](https://github.com/googlesamples/unity-jar-resolver)
|
||||
|
||||
## Requirements
|
||||
|
||||
The *Android Resolver* and *iOS Resolver* components of the plugin only work
|
||||
with Unity version 4.6.8 or higher.
|
||||
|
||||
The *Version Handler* component only works with Unity 5.x or higher as it
|
||||
depends upon the `PluginImporter` UnityEditor API.
|
||||
|
||||
The *Package Manager Resolver* component only works with Unity 2018.4 or above,
|
||||
when [scoped registry](https://docs.unity3d.com/Manual/upm-scoped.html) support
|
||||
was added to the Package Manager.
|
||||
|
||||
## Getting Started
|
||||
|
||||
Check out [troubleshooting](troubleshooting-faq.md) if you need help.
|
||||
|
||||
### Install via OpenUPM
|
||||
|
||||
EDM4U is available on
|
||||
[OpenUPM](https://openupm.com/packages/com.google.external-dependency-manager/):
|
||||
|
||||
```shell
|
||||
openupm add com.google.external-dependency-manager
|
||||
```
|
||||
|
||||
### Install via git URL
|
||||
1. Open Package Manager
|
||||
2. Click on the + icon on the top left corner of the "Package Manager" screen
|
||||
3. Click on "Install package from git url..."
|
||||
4. Paste: https://github.com/googlesamples/unity-jar-resolver.git?path=upm
|
||||
|
||||
### Install via Google APIs for Unity
|
||||
|
||||
EDM4U is available both in UPM and legacy `.unitypackage` formats on
|
||||
[Google APIs for Unity](https://developers.google.com/unity/archive#external_dependency_manager_for_unity).
|
||||
|
||||
You may install the UPM version (.tgz) as a
|
||||
[local UPM package](https://docs.unity3d.com/Manual/upm-ui-local.html).
|
||||
|
||||
You can also install EDM4U in your project as a `.unitypackage`. This is not
|
||||
recommended due to potential conflicts.
|
||||
|
||||
### Conflict Resolution
|
||||
|
||||
For historical reasons, a package maintainer may choose to embed EDM4U in their
|
||||
package for ease of installation. This will create a conflict when you try to
|
||||
install EDM4U with the steps above, or with another package with embedded EDM4U.
|
||||
If your project imported a `.unitypackage` that has a copy of EDM4U embedded in
|
||||
it, you may safely delete it from your Assets folder. If your project depends on
|
||||
another UPM package with EDM4U, please reach out to the package maintainer and
|
||||
ask them to replace it with a dependency to this package. In the meantime, you
|
||||
can workaround the issue by copying the package to your Packages folder (to
|
||||
create an
|
||||
[embedded package](https://docs.unity3d.com/Manual/upm-concepts.html#Embedded))
|
||||
and perform the steps yourself to avoid a dependency conflict.
|
||||
|
||||
### Config file
|
||||
|
||||
To start adding dependencies to your project, copy and rename the
|
||||
[SampleDependencies.xml](https://github.com/googlesamples/unity-jar-resolver/blob/master/sample/Assets/ExternalDependencyManager/Editor/SampleDependencies.xml)
|
||||
file into your plugin and add the dependencies your project requires.
|
||||
|
||||
The XML file needs to be under an `Editor` directory and match the name
|
||||
`*Dependencies.xml`. For example, `MyPlugin/Editor/MyPluginDependencies.xml`.
|
||||
|
||||
## Usages
|
||||
|
||||
### Android Resolver
|
||||
|
||||
The Android Resolver copies specified dependencies from local or remote Maven
|
||||
repositories into the Unity project when a user selects Android as the build
|
||||
target in the Unity editor.
|
||||
|
||||
For example, to add the Google Play Games library
|
||||
(`com.google.android.gms:play-services-games` package) at version `9.8.0` to the
|
||||
set of a plugin's Android dependencies:
|
||||
|
||||
```xml
|
||||
<dependencies>
|
||||
<androidPackages>
|
||||
<androidPackage spec="com.google.android.gms:play-services-games:9.8.0">
|
||||
<androidSdkPackageIds>
|
||||
<androidSdkPackageId>extra-google-m2repository</androidSdkPackageId>
|
||||
</androidSdkPackageIds>
|
||||
</androidPackage>
|
||||
</androidPackages>
|
||||
</dependencies>
|
||||
```
|
||||
|
||||
The version specification (last component) supports:
|
||||
|
||||
* Specific versions e.g `9.8.0`
|
||||
|
||||
* Partial matches e.g `9.8.+` would match 9.8.0, 9.8.1 etc. choosing the most
|
||||
recent version
|
||||
|
||||
* Latest version using `LATEST` or `+`. We do *not* recommend using this
|
||||
unless you're 100% sure the library you depend upon will not break your
|
||||
Unity plugin in future
|
||||
|
||||
The above example specifies the dependency as a component of the Android SDK
|
||||
manager such that the Android SDK manager will be executed to install the
|
||||
package if it's not found. If your Android dependency is located on Maven
|
||||
central it's possible to specify the package simply using the `androidPackage`
|
||||
element:
|
||||
|
||||
```xml
|
||||
<dependencies>
|
||||
<androidPackages>
|
||||
<androidPackage spec="com.google.api-client:google-api-client-android:1.22.0" />
|
||||
</androidPackages>
|
||||
</dependencies>
|
||||
```
|
||||
|
||||
#### Auto-resolution
|
||||
|
||||
By default the Android Resolver automatically monitors the dependencies you have
|
||||
specified and the `Plugins/Android` folder of your Unity project. The resolution
|
||||
process runs when the specified dependencies are not present in your project.
|
||||
|
||||
The *auto-resolution* process can be disabled via the `Assets > External
|
||||
Dependency Manager > Android Resolver > Settings` menu.
|
||||
|
||||
Manual resolution can be performed using the following menu options:
|
||||
|
||||
* `Assets > External Dependency Manager > Android Resolver > Resolve`
|
||||
|
||||
* `Assets > External Dependency Manager > Android Resolver > Force Resolve`
|
||||
|
||||
#### Deleting libraries
|
||||
|
||||
Resolved packages are tracked via asset labels by the Android Resolver. They can
|
||||
easily be deleted using the `Assets > External Dependency Manager > Android
|
||||
Resolver > Delete Resolved Libraries` menu item.
|
||||
|
||||
#### Android Manifest Variable Processing
|
||||
|
||||
Some AAR files (for example play-services-measurement) contain variables that
|
||||
are processed by the Android Gradle plugin. Unfortunately, Unity does not
|
||||
perform the same processing when using Unity's Internal Build System, so the
|
||||
Android Resolver plugin handles known cases of this variable substitution by
|
||||
exploding the AAR into a folder and replacing `${applicationId}` with the
|
||||
`bundleID`.
|
||||
|
||||
Disabling AAR explosion and therefore Android manifest processing can be done
|
||||
via the `Assets > External Dependency Manager > Android Resolver > Settings`
|
||||
menu. You may want to disable explosion of AARs if you're exporting a project to
|
||||
be built with Gradle/Android Studio.
|
||||
|
||||
#### ABI Stripping
|
||||
|
||||
Some AAR files contain native libraries (.so files) for each ABI supported by
|
||||
Android. Unfortunately, when targeting a single ABI (e.g x86), Unity does not
|
||||
strip native libraries for unused ABIs. To strip unused ABIs, the Android
|
||||
Resolver plugin explodes an AAR into a folder and removes unused ABIs to reduce
|
||||
the built APK size. Furthermore, if native libraries are not stripped from an
|
||||
APK (e.g you have a mix of Unity's x86 library and some armeabi-v7a libraries)
|
||||
Android may attempt to load the wrong library for the current runtime ABI
|
||||
completely breaking your plugin when targeting some architectures.
|
||||
|
||||
AAR explosion and therefore ABI stripping can be disabled via the `Assets >
|
||||
External Dependency Manager > Android Resolver > Settings` menu. You may want to
|
||||
disable explosion of AARs if you're exporting a project to be built with
|
||||
Gradle/Android Studio.
|
||||
|
||||
#### Resolution Strategies
|
||||
|
||||
By default the Android Resolver will use Gradle to download dependencies prior
|
||||
to integrating them into a Unity project. This works with Unity's internal build
|
||||
system and Gradle/Android Studio project export.
|
||||
|
||||
It's possible to change the resolution strategy via the `Assets > External
|
||||
Dependency Manager > Android Resolver > Settings` menu.
|
||||
|
||||
##### Download Artifacts with Gradle
|
||||
|
||||
Using the default resolution strategy, the Android resolver executes the
|
||||
following operations:
|
||||
|
||||
- Remove the result of previous Android resolutions. E.g Delete all files and
|
||||
directories labeled with "gpsr" under `Plugins/Android` from the project.
|
||||
|
||||
- Collect the set of Android dependencies (libraries) specified by a project's
|
||||
`*Dependencies.xml` files.
|
||||
|
||||
- Run `download_artifacts.gradle` with Gradle to resolve conflicts and, if
|
||||
successful, download the set of resolved Android libraries (AARs, JARs).
|
||||
|
||||
- Process each AAR/JAR so that it can be used with the currently selected
|
||||
Unity build system (e.g Internal vs. Gradle, Export vs. No Export). This
|
||||
involves patching each reference to `applicationId` in the
|
||||
`AndroidManifest.xml` with the project's bundle ID. This means resolution
|
||||
must be run again if the bundle ID has changed.
|
||||
|
||||
- Move the processed AARs to `Plugins/Android` so they will be included when
|
||||
Unity invokes the Android build.
|
||||
|
||||
##### Integrate into mainTemplate.gradle
|
||||
|
||||
Unity 5.6 introduced support for customizing the `build.gradle` used to build
|
||||
Unity projects with Gradle. When the *Patch mainTemplate.gradle* setting is
|
||||
enabled, rather than downloading artifacts before the build, Android resolution
|
||||
results in the execution of the following operations:
|
||||
|
||||
- Remove the result of previous Android resolutions. E.g Delete all files and
|
||||
directories labeled with "gpsr" under `Plugins/Android` from the project and
|
||||
remove sections delimited with `// Android Resolver * Start` and `// Android
|
||||
Resolver * End` lines.
|
||||
|
||||
- Collect the set of Android dependencies (libraries) specified by a project's
|
||||
`*Dependencies.xml` files.
|
||||
|
||||
- Rename any `.srcaar` files in the build to `.aar` and exclude them from
|
||||
being included directly by Unity in the Android build as
|
||||
`mainTemplate.gradle` will be patched to include them instead from their
|
||||
local maven repositories.
|
||||
|
||||
- Inject the required Gradle repositories into `mainTemplate.gradle` at the
|
||||
line matching the pattern `.*apply plugin:
|
||||
'com\.android\.(application|library)'.*` or the section starting at the line
|
||||
`// Android Resolver Repos Start`. If you want to control the injection
|
||||
point in the file, the section delimited by the lines `// Android Resolver
|
||||
Repos Start` and `// Android Resolver Repos End` should be placed in the
|
||||
global scope before the `dependencies` section.
|
||||
|
||||
- Inject the required Android dependencies (libraries) into
|
||||
`mainTemplate.gradle` at the line matching the pattern `***DEPS***` or the
|
||||
section starting at the line `// Android Resolver Dependencies Start`. If
|
||||
you want to control the injection point in the file, the section delimited
|
||||
by the lines `// Android Resolver Dependencies Start` and `// Android
|
||||
Resolver Dependencies End` should be placed in the `dependencies` section.
|
||||
|
||||
- Inject the packaging options logic, which excludes architecture specific
|
||||
libraries based upon the selected build target, into `mainTemplate.gradle`
|
||||
at the line matching the pattern `android +{` or the section starting at the
|
||||
line `// Android Resolver Exclusions Start`. If you want to control the
|
||||
injection point in the file, the section delimited by the lines `// Android
|
||||
Resolver Exclusions Start` and `// Android Resolver Exclusions End` should
|
||||
be placed in the global scope before the `android` section.
|
||||
|
||||
#### Dependency Tracking
|
||||
|
||||
The Android Resolver creates the
|
||||
`ProjectSettings/AndroidResolverDependencies.xml` to quickly determine the set
|
||||
of resolved dependencies in a project. This is used by the auto-resolution
|
||||
process to only run the expensive resolution process when necessary.
|
||||
|
||||
#### Displaying Dependencies
|
||||
|
||||
It's possible to display the set of dependencies the Android Resolver would
|
||||
download and process in your project via the `Assets > External Dependency
|
||||
Manager > Android Resolver > Display Libraries` menu item.
|
||||
|
||||
### iOS Resolver
|
||||
|
||||
The iOS resolver component of this plugin supports both [Swift Packages](https://www.swift.org/packages/) and
|
||||
[CocoaPods](https://cocoapods.org/).
|
||||
|
||||
#### Swift Package Manager Support
|
||||
Swift Packages are a newer way to add dependencies on iOS+ platforms. EDM4U uses Unity's built in ability to add Packages to the generated Xcode project, parsing from the library's Dependencies xml file.
|
||||
|
||||
For example, to add the Firebase Analytics package:
|
||||
|
||||
```xml
|
||||
<dependencies>
|
||||
<remoteSwiftPackage url="https://github.com/firebase/firebase-ios-sdk.git"
|
||||
version="12.0.0">
|
||||
<swiftPackage name="FirebaseAnalytics"/>
|
||||
</remoteSwiftPackage>
|
||||
</dependencies>
|
||||
```
|
||||
|
||||
##### Replacing Cocoapods
|
||||
|
||||
In the Dependencies xml files, libraries can reference both Swift Packages, and Cocoapods. This is useful to give developers the option to fallback to the previous Cocoapods behavior if they want to. To specify which Pods the Package is meant to be replacing in the xml file, so that when the resolution runs, it knows not to add both.
|
||||
|
||||
For example, to add the AdMob package to replace the Pod example provided below:
|
||||
|
||||
```xml
|
||||
<dependencies>
|
||||
<remoteSwiftPackage url="https://github.com/googleads/swift-package-manager-google-mobile-ads.git"
|
||||
version="12.12.0"
|
||||
upToNextMajor="true">
|
||||
<swiftPackage name="GoogleMobileAds" replacesPod="Google-Mobiles-Ads-SDK"/>
|
||||
</remoteSwiftPackage>
|
||||
</dependencies>
|
||||
```
|
||||
|
||||
#### CocoaPods Support
|
||||
A CocoaPods `Podfile` is generated and the
|
||||
`pod` tool is executed as a post build process step to add dependencies to the
|
||||
Xcode project exported by Unity.
|
||||
|
||||
Dependencies for iOS are added by referring to CocoaPods.
|
||||
|
||||
For example, to add the AdMob pod, version 7.0 or greater with bitcode enabled:
|
||||
|
||||
```xml
|
||||
<dependencies>
|
||||
<iosPods>
|
||||
<iosPod name="Google-Mobile-Ads-SDK" version="~> 7.0" bitcodeEnabled="true"
|
||||
minTargetSdk="6.0" addToAllTargets="false" />
|
||||
</iosPods>
|
||||
</dependencies>
|
||||
```
|
||||
|
||||
##### Integration Strategies
|
||||
|
||||
The `CocoaPods` are either:
|
||||
|
||||
* Downloaded and injected into the Xcode project file directly, rather than
|
||||
creating a separate xcworkspace. We call this `Xcode project` integration.
|
||||
|
||||
* If the Unity version supports opening a xcworkspace file, the `pod` tool is
|
||||
used as intended to generate a xcworkspace which references the CocoaPods.
|
||||
We call this `Xcode workspace` integration.
|
||||
|
||||
The resolution strategy can be changed via the `Assets > External Dependency
|
||||
Manager > iOS Resolver > Settings` menu.
|
||||
|
||||
##### Appending text to generated Podfile
|
||||
|
||||
In order to modify the generated Podfile you can create a script like this:
|
||||
|
||||
```csharp
|
||||
using System.IO;
|
||||
|
||||
using UnityEditor;
|
||||
using UnityEditor.Callbacks;
|
||||
using UnityEngine;
|
||||
|
||||
public class PostProcessIOS
|
||||
{
|
||||
// Must be between 40 and 50 to ensure that it's not overriden by Podfile generation (40) and
|
||||
// that it's added before "pod install" (50).
|
||||
[PostProcessBuildAttribute(45)]
|
||||
private static void PostProcessBuild_iOS(BuildTarget target, string buildPath)
|
||||
{
|
||||
if (target == BuildTarget.iOS)
|
||||
{
|
||||
using (StreamWriter sw = File.AppendText(buildPath + "/Podfile"))
|
||||
{
|
||||
// E.g. add an app extension
|
||||
sw.WriteLine("\ntarget 'NSExtension' do\n pod 'Firebase/Messaging', '6.6.0'\nend");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Package Manager Resolver
|
||||
|
||||
Adding registries to the
|
||||
[Package Manager](https://docs.unity3d.com/Manual/Packages.html) (PM) is a
|
||||
manual process. The Package Manager Resolver (PMR) component of this plugin
|
||||
makes it easy for plugin maintainers to distribute new PM registry servers and
|
||||
easy for plugin users to manage PM registry servers.
|
||||
|
||||
#### Adding Registries
|
||||
|
||||
For example, to add a registry for plugins in the scope `com.coolstuff`:
|
||||
|
||||
```xml
|
||||
<registries>
|
||||
<registry name="Cool Stuff"
|
||||
url="https://unityregistry.coolstuff.com"
|
||||
termsOfService="https://coolstuff.com/unityregistry/terms"
|
||||
privacyPolicy="https://coolstuff.com/unityregistry/privacy">
|
||||
<scopes>
|
||||
<scope>com.coolstuff</scope>
|
||||
</scopes>
|
||||
</registry>
|
||||
</registries>
|
||||
```
|
||||
|
||||
When PMR is loaded it will prompt the developer to add the registry to their
|
||||
project if it isn't already present in the `Packages/manifest.json` file.
|
||||
|
||||
For more information, see Unity's documentation on
|
||||
[scoped package registries](https://docs.unity3d.com/Manual/upm-scoped.html).
|
||||
|
||||
#### Managing Registries
|
||||
|
||||
It's possible to add and remove registries that are specified via PMR XML
|
||||
configuration files via the following menu options:
|
||||
|
||||
* `Assets > External Dependency Manager > Package Manager Resolver > Add
|
||||
Registries` will prompt the user with a window which allows them to add
|
||||
registries discovered in the project to the Package Manager.
|
||||
|
||||
* `Assets > External Dependency Manager > Package Manager Resolver > Remove
|
||||
Registries` will prompt the user with a window which allows them to remove
|
||||
registries discovered in the project from the Package Manager.
|
||||
|
||||
* `Assets > External Dependency Manager > Package Manager Resolver > Modify
|
||||
Registries` will prompt the user with a window which allows them to add or
|
||||
remove registries discovered in the project.
|
||||
|
||||
#### Migration
|
||||
|
||||
PMR can migrate Version Handler packages installed in the `Assets` folder to PM
|
||||
packages. This requires the plugins to implement the following:
|
||||
|
||||
* `.unitypackage` must include a Version Handler manifests that describes the
|
||||
components of the plugin. If the plugin has no dependencies the manifest
|
||||
would just include the files in the plugin.
|
||||
|
||||
* The PM package JSON provided by the registry must include a keyword (in the
|
||||
`versions.VERSION.keyword` list) that maps the PM package to a Version
|
||||
Handler package using the format `vh-name:VERSION_HANDLER_MANIFEST_NAME`
|
||||
where `VERSION_HANDLER_MANIFEST_NAME` is the name of the manifest defined in
|
||||
the `.unitypackage`. For more information see the description of the
|
||||
`gvhp_manifestname` asset label in the [Version Handler](#version-handler)
|
||||
section.
|
||||
|
||||
When using the `Assets > External Dependency Manager > Package Manager
|
||||
Resolver > Migrate Packages` menu option, PMR then will:
|
||||
|
||||
* List all Version Handler manager packages in the project.
|
||||
|
||||
* Search all available packages in the PM registries and fetch keywords
|
||||
associated with each package parsing the Version Handler manifest names for
|
||||
each package.
|
||||
|
||||
* Map each installed Version Handler package to a PM package.
|
||||
|
||||
* Prompt the user to migrate the discovered packages.
|
||||
|
||||
* Perform package migration for all selected packages if the user clicks the
|
||||
`Apply` button.
|
||||
|
||||
#### Configuration
|
||||
|
||||
PMR can be configured via the `Assets > External Dependency Manager > Package
|
||||
Manager Resolver > Settings` menu option:
|
||||
|
||||
* `Add package registries` when enabled, when the plugin loads or registry
|
||||
configuration files change, this will prompt the user to add registries that
|
||||
are not present in the Package Manager.
|
||||
|
||||
* `Prompt to add package registries` will cause a developer to be prompted
|
||||
with a window that will ask for confirmation before adding registries. When
|
||||
this is disabled registries are added silently to the project.
|
||||
|
||||
* `Prompt to migrate packages` will cause a developer to be prompted with a
|
||||
window that will ask for confirmation before migrating packages installed in
|
||||
the `Assets` directory to PM packages.
|
||||
|
||||
* `Enable Analytics Reporting` when enabled, reports the use of the plugin to
|
||||
the developers so they can make imrpovements.
|
||||
|
||||
* `Verbose logging` when enabled prints debug information to the console which
|
||||
can be useful when filing bug reports.
|
||||
|
||||
### Version Handler
|
||||
|
||||
The Version Handler component of this plugin manages:
|
||||
|
||||
* Shared Unity plugin dependencies.
|
||||
|
||||
* Upgrading Unity plugins by cleaning up old files from previous versions.
|
||||
|
||||
* Uninstallation of plugins that are distributed with manifest files.
|
||||
|
||||
* Restoration of plugin assets to their original install locations if assets
|
||||
are tagged with the `exportpath` label.
|
||||
|
||||
Since the Version Handler needs to modify Unity asset metadata (`.meta` files),
|
||||
to enable/disable components, rename and delete asset files it does not work
|
||||
with Package Manager installed packages. It's still possible to include EDM4U in
|
||||
Package Manager packages, the Version Handler component simply won't do anything
|
||||
to PM plugins in this case.
|
||||
|
||||
#### Using Version Handler Managed Plugins
|
||||
|
||||
If a plugin is imported at multiple different versions into a project, if the
|
||||
Version Handler is enabled, it will automatically check all managed assets to
|
||||
determine the set of assets that are out of date and assets that should be
|
||||
removed. To disable automatic checking managed assets disable the `Enable
|
||||
version management` option in the `Assets > External Dependency Manager >
|
||||
Version Handler > Settings` menu.
|
||||
|
||||
If version management is disabled, it's possible to check managed assets
|
||||
manually using the `Assets > External Dependency Manager > Version Handler >
|
||||
Update` menu option.
|
||||
|
||||
##### Listing Managed Plugins
|
||||
|
||||
Plugins managed by the Version Handler, those that ship with manifest files, can
|
||||
displayed using the `Assets > External Dependency Manager > Version Handler >
|
||||
Display Managed Packages` menu option. The list of plugins are written to the
|
||||
console window along with the set of files used by each plugin.
|
||||
|
||||
##### Uninstalling Managed Plugins
|
||||
|
||||
Plugins managed by the Version Handler, those that ship with manifest files, can
|
||||
be removed using the `Assets > External Dependency Manager > Version Handler >
|
||||
Uninstall Managed Packages` menu option. This operation will display a window
|
||||
that allows a developer to select a set of plugins to remove which will remove
|
||||
all files owned by each plugin excluding those that are in use by other
|
||||
installed plugins.
|
||||
|
||||
Files managed by the Version Handler, those labeled with the `gvh` asset label,
|
||||
can be checked to see whether anything needs to be upgraded, disabled or removed
|
||||
using the `Assets > External Dependency Manager > Version Handler > Update` menu
|
||||
option.
|
||||
|
||||
##### Restore Install Paths
|
||||
|
||||
Some developers move assets around in their project which can make it harder for
|
||||
plugin maintainers to debug issues if this breaks Unity's
|
||||
[special folders](https://docs.unity3d.com/Manual/SpecialFolders.html) rules. If
|
||||
assets are labeled with their original install/export path (see
|
||||
`gvhp_exportpath` below), Version Handler can restore assets to their original
|
||||
locations when using the `Assets > External Dependency Manager > Version
|
||||
Handler > Move Files To Install Locations` menu option.
|
||||
|
||||
##### Settings
|
||||
|
||||
Some behavior of the Version Handler can be configured via the `Assets >
|
||||
External Dependency Manager > Version Handler > Settings` menu option.
|
||||
|
||||
* `Enable version management` controls whether the plugin should automatically
|
||||
check asset versions and apply changes. If this is disabled the process
|
||||
should be run manually when installing or upgrading managed plugins using
|
||||
`Assets > External Dependency Manager > Version Handler > Update`.
|
||||
|
||||
* `Rename to canonical filenames` is a legacy option that will rename files to
|
||||
remove version numbers and other labels from filenames.
|
||||
|
||||
* `Prompt for obsolete file deletion` enables the display of a window when
|
||||
obsolete files are deleted allowing the developer to select which files to
|
||||
delete and those to keep.
|
||||
|
||||
* `Allow disabling files via renaming` controls whether obsolete or disabled
|
||||
files should be disabled by renaming them to `myfilename_DISABLED`. Renaming
|
||||
to disable files is required in some scenarios where Unity doesn't support
|
||||
removing files from the build via the PluginImporter.
|
||||
|
||||
* `Enable Analytics Reporting` enables/disables usage reporting to plugin
|
||||
developers to improve the product.
|
||||
|
||||
* `Verbose logging` enables *very* noisy log output that is useful for
|
||||
debugging while filing a bug report or building a new managed plugin.
|
||||
|
||||
* `Use project settings` saves settings for the plugin in the project rather
|
||||
than system-wide.
|
||||
|
||||
#### Redistributing a Managed Plugin
|
||||
|
||||
The Version Handler employs a couple of methods for managing version selection,
|
||||
upgrade and removal of plugins.
|
||||
|
||||
* Each plugin can ship with a manifest file that lists the files it includes.
|
||||
This makes it possible for Version Handler to calculate the difference in
|
||||
assets between the most recent release of a plugin and the previous release
|
||||
installed in a project. If a files are removed the Version Handler will
|
||||
prompt the user to clean up obsolete files.
|
||||
|
||||
* Plugins can ship using assets with unique names, unique GUIDs and version
|
||||
number labels. Version numbers can be attached to assets using labels or
|
||||
added to the filename (e.g `myfile.txt` would be `myfile_version-x.y.z.txt).
|
||||
This allows the Version Handler to determine which set of files are the same
|
||||
file at different versions, select the most recent version and prompt the
|
||||
developer to clean up old versions.
|
||||
|
||||
Unity plugins can be managed by the Version Handler using the following steps:
|
||||
|
||||
1. Add the `gvh` asset label to each asset (file) you want Version Handler to
|
||||
manage.
|
||||
|
||||
1. Add the `gvh_version-VERSION` label to each asset where `VERSION` is the
|
||||
version of the plugin you're releasing (e.g 1.2.3).
|
||||
|
||||
1. Add the `gvhp_exportpath-PATH` label to each asset where `PATH` is the
|
||||
export path of the file when the `.unitypackage` is created. This is used to
|
||||
track files if they're moved around in a project by developers.
|
||||
|
||||
1. Optional: Add `gvh_targets-editor` label to each editor DLL in your plugin
|
||||
and disable `editor` as a target platform for the DLL. The Version Handler
|
||||
will enable the most recent version of this DLL when the plugin is imported.
|
||||
|
||||
1. Optional: If your plugin is included in other Unity plugins, you should add
|
||||
the version number to each filename and change the GUID of each asset. This
|
||||
allows multiple versions of your plugin to be imported into a Unity project,
|
||||
with the Version Handler component activating only the most recent version.
|
||||
|
||||
1. Create a manifest text file named `MY_UNIQUE_PLUGIN_NAME_VERSION.txt` that
|
||||
lists all the files in your plugin relative to the project root. Then add
|
||||
the `gvh_manifest` label to the asset to indicate this file is a plugin
|
||||
manifest.
|
||||
|
||||
1. Optional: Add a `gvhp_manifestname-NAME` label to your manifest file to
|
||||
provide a human readable name for your package. If this isn't provided the
|
||||
name of the manifest file will be used as the package name. NAME can match
|
||||
the pattern `[0-9]+[a-zA-Z -]` where a leading integer will set the priority
|
||||
of the name where `0` is the highest priority and preferably used as the
|
||||
display name. The lowest value (i.e highest priority name) will be used as
|
||||
the display name and all other specified names will be aliases of the
|
||||
display name. Aliases can refer to previous names of the package allowing
|
||||
renaming across published versions.
|
||||
|
||||
1. Redistribute EDM4U Unity plugin with your plugin. See the
|
||||
[Plugin Redistribution](#plugin-redistribution) section for details.
|
||||
|
||||
If you follow these steps:
|
||||
|
||||
* When users import a newer version of your plugin, files referenced by the
|
||||
older version's manifest are cleaned up.
|
||||
|
||||
* The latest version of the plugin will be selected when users import multiple
|
||||
packages that include your plugin, assuming the steps in
|
||||
[Plugin Redistribution](#plugin-redistribution) are followed.
|
||||
|
||||
## Background
|
||||
|
||||
Many Unity plugins have dependencies upon Android specific libraries, iOS
|
||||
CocoaPods, and sometimes have transitive dependencies upon other Unity plugins.
|
||||
This causes the following problems:
|
||||
|
||||
* Integrating platform specific (e.g Android and iOS) libraries within a Unity
|
||||
project can be complex and a burden on a Unity plugin maintainer.
|
||||
* The process of resolving conflicting dependencies on platform specific
|
||||
libraries is pushed to the developer attempting to use a Unity plugin. The
|
||||
developer trying to use your plugin is very likely to give up when faced
|
||||
with Android or iOS specific build errors.
|
||||
* The process of resolving conflicting Unity plugins (due to shared Unity
|
||||
plugin components) is pushed to the developer attempting to use your Unity
|
||||
plugin. In an effort to resolve conflicts, the developer will very likely
|
||||
attempt to resolve problems by deleting random files in your plugin, report
|
||||
bugs when that doesn't work and finally give up.
|
||||
|
||||
EDM4U provides solutions for each of these problems.
|
||||
|
||||
### Android Dependency Management
|
||||
|
||||
The *Android Resolver* component of this plugin will download and integrate
|
||||
Android library dependencies and handle any conflicts between plugins that share
|
||||
the same dependencies.
|
||||
|
||||
Without the Android Resolver, typically Unity plugins bundle their AAR and JAR
|
||||
dependencies, e.g. a Unity plugin `SomePlugin` that requires the Google Play
|
||||
Games Android library would redistribute the library and its transitive
|
||||
dependencies in the folder `SomePlugin/Android/`. When a user imports
|
||||
`SomeOtherPlugin` that includes the same libraries (potentially at different
|
||||
versions) in `SomeOtherPlugin/Android/`, the developer using `SomePlugin` and
|
||||
`SomeOtherPlugin` will see an error when building for Android that can be hard
|
||||
to interpret.
|
||||
|
||||
Using the Android Resolver to manage Android library dependencies:
|
||||
|
||||
* Solves Android library conflicts between plugins.
|
||||
* Handles all of the various processing steps required to use Android
|
||||
libraries (AARs, JARs) in Unity 4.x and above projects. Almost all versions
|
||||
of Unity have - at best - partial support for AARs.
|
||||
* (Experimental) Supports minification of included Java components without
|
||||
exporting a project.
|
||||
|
||||
### iOS Dependency Management
|
||||
|
||||
The *iOS Resolver* component of this plugin integrates with
|
||||
[CocoaPods](https://cocoapods.org/) to download and integrate iOS libraries and
|
||||
frameworks into the Xcode project Unity generates when building for iOS. Using
|
||||
CocoaPods allows multiple plugins to utilize shared components without forcing
|
||||
developers to fix either duplicate or incompatible versions of libraries
|
||||
included through multiple Unity plugins in their project.
|
||||
|
||||
### Package Manager Registry Setup
|
||||
|
||||
The [Package Manager](https://docs.unity3d.com/Manual/Packages.html) (PM) makes
|
||||
use of [NPM](https://www.npmjs.com/) registry servers for package hosting and
|
||||
provides ways to discover, install, upgrade and uninstall packages. This makes
|
||||
it easier for developers to manage plugins within their projects.
|
||||
|
||||
However, installing additional package registries requires a few manual steps
|
||||
that can potentially be error prone. The *Package Manager Resolver* component of
|
||||
this plugin integrates with [PM](https://docs.unity3d.com/Manual/Packages.html)
|
||||
to provide a way to auto-install PM package registries when a `.unitypackage` is
|
||||
installed which allows plugin maintainers to ship a `.unitypackage` that can
|
||||
provide access to their own PM registry server to make it easier for developers
|
||||
to manage their plugins.
|
||||
|
||||
### Unity Plugin Version Management
|
||||
|
||||
Finally, the *Version Handler* component of this plugin simplifies the process
|
||||
of managing transitive dependencies of Unity plugins and each plugin's upgrade
|
||||
process.
|
||||
|
||||
For example, without the Version Handler plugin, if:
|
||||
|
||||
* Unity plugin `SomePlugin` includes `EDM4U` plugin at version 1.1.
|
||||
* Unity plugin `SomeOtherPlugin` includes `EDM4U` plugin at version 1.2.
|
||||
|
||||
The version of `EDM4U` included in the developer's project depends upon the
|
||||
order the developer imports `SomePlugin` or `SomeOtherPlugin`.
|
||||
|
||||
This results in:
|
||||
|
||||
* `EDM4U` at version 1.2, if `SomePlugin` is imported then `SomeOtherPlugin`
|
||||
is imported.
|
||||
* `EDM4U` at version 1.1, if `SomeOtherPlugin` is imported then `SomePlugin`
|
||||
is imported.
|
||||
|
||||
The Version Handler solves the problem of managing transitive dependencies by:
|
||||
|
||||
* Specifying a set of packaging requirements that enable a plugin at different
|
||||
versions to be imported into a Unity project.
|
||||
* Providing activation logic that selects the latest version of a plugin
|
||||
within a project.
|
||||
|
||||
When using the Version Handler to manage `EDM4U` included in `SomePlugin` and
|
||||
`SomeOtherPlugin`, from the prior example, version 1.2 will always be the
|
||||
version activated in a developer's Unity project.
|
||||
|
||||
Plugin creators are encouraged to adopt this library to ease integration for
|
||||
their customers. For more information about integrating EDM4U into your own
|
||||
plugin, see the [Plugin Redistribution](#plugin-redistribution) section of this
|
||||
document.
|
||||
|
||||
## Analytics
|
||||
|
||||
The External Dependency Manager for Unity plugin by default logs usage to Google
|
||||
Analytics. The purpose of the logging is to quantitatively measure the usage of
|
||||
functionality, to gather reports on integration failures and to inform future
|
||||
improvements to the developer experience of the External Dependency Manager
|
||||
plugin. Note that the analytics collected are limited to the scope of the EDM4U
|
||||
plugin’s usage.
|
||||
|
||||
For details of what is logged, please refer to the usage of
|
||||
`EditorMeasurement.Report()` in the source code.
|
||||
|
||||
## Plugin Redistribution
|
||||
|
||||
If you are a package maintainer and your package depends on EDM4U, it is highly
|
||||
recommended to use the UPM format and add EDM4U as a dependency. If you must
|
||||
include it in your `.unitypackage`, redistributing `EDM4U` inside your own
|
||||
plugin might ease the integration process for your users.
|
||||
|
||||
If you wish to redistribute `EDM4U` inside your plugin, you **must** follow
|
||||
these steps when importing the `external-dependency-manager-*.unitypackage`, and
|
||||
when exporting your own plugin package:
|
||||
|
||||
1. Import the `external-dependency-manager-*.unitypackage` into your plugin
|
||||
project by
|
||||
[running Unity from the command line](https://docs.unity3d.com/Manual/CommandLineArguments.html),
|
||||
ensuring that you add the `-gvh_disable` option.
|
||||
1. Export your plugin by
|
||||
[running Unity from the command line](https://docs.unity3d.com/Manual/CommandLineArguments.html),
|
||||
ensuring that you:
|
||||
- Include the contents of the `Assets/PlayServicesResolver` and
|
||||
`Assets/ExternalDependencyManager` directory.
|
||||
- Add the `-gvh_disable` option.
|
||||
|
||||
You **must** specify the `-gvh_disable` option in order for the Version Handler
|
||||
to work correctly!
|
||||
|
||||
For example, the following command will import the
|
||||
`external-dependency-manager-1.2.46.0.unitypackage` into the project
|
||||
`MyPluginProject` and export the entire Assets folder to
|
||||
`MyPlugin.unitypackage`:
|
||||
|
||||
```shell
|
||||
Unity -gvh_disable \
|
||||
-batchmode \
|
||||
-importPackage external-dependency-manager-1.2.46.0.unitypackage \
|
||||
-projectPath MyPluginProject \
|
||||
-exportPackage Assets MyPlugin.unitypackage \
|
||||
-quit
|
||||
```
|
||||
|
||||
### Background
|
||||
|
||||
The *Version Handler* component relies upon deferring the load of editor DLLs so
|
||||
that it can run first and determine the latest version of a plugin component to
|
||||
activate. The build of `EDM4U` plugin has Unity asset metadata that is
|
||||
configured so that the editor components are not initially enabled when it's
|
||||
imported into a Unity project. To maintain this configuration when importing the
|
||||
`external-dependency-manager.unitypackage` into a Unity plugin project, you
|
||||
*must* specify the command line option `-gvh_disable` which will prevent the
|
||||
Version Handler component from running and changing the Unity asset metadata.
|
||||
|
||||
## Building from Source
|
||||
|
||||
To build this plugin from source you need the following tools installed: * Unity
|
||||
2021 and below (with iOS and Android modules installed) * Java 11
|
||||
|
||||
You can build the plugin by running the following from your shell (Linux / OSX):
|
||||
|
||||
```shell
|
||||
./gradlew build
|
||||
|
||||
```
|
||||
|
||||
or Windows:
|
||||
|
||||
```shell
|
||||
./gradlew.bat build
|
||||
```
|
||||
|
||||
If Java 11 is not your default Java command, add
|
||||
`-Dorg.gradle.java.home=<PATH_TO_JAVA_HOME>` to the command above.
|
||||
|
||||
## Testing
|
||||
|
||||
You can run the tests by running the following from your shell (Linux / OSX):
|
||||
|
||||
```shell
|
||||
./gradlew test
|
||||
```
|
||||
|
||||
or Windows:
|
||||
|
||||
```shell
|
||||
./gradlew.bat test
|
||||
```
|
||||
|
||||
The following properties can be set to narrow down the tests to run or change
|
||||
the test run behavior.
|
||||
|
||||
* `INTERACTIVE_MODE_TESTS_ENABLED` - Default to `1`. Set to `1` to enable
|
||||
interactive mode tests, which requires GPU on the machine. Otherwise, only
|
||||
run tests in the batch mode.
|
||||
* `INCLUDE_TEST_TYPES` - Default to empty string, which means to include every
|
||||
type of the test. To narrow down the types of test to run, set this
|
||||
properties with a list of case-insensitive type strings separated by comma.
|
||||
For instance, `-PINCLUDE_TEST_TYPES="Python,NUnit"` means to include only
|
||||
Python tests and NUnit tests. See `TestTypeEnum` in `build.gradle` for
|
||||
available options.
|
||||
* `EXCLUDE_TEST_TYPES` - Default to empty string, which means to exclude none.
|
||||
To add types of tests to exclude, set this properties with a list of
|
||||
case-insensitive type strings separated by comma. For instance,
|
||||
`-PEXCLUDE_TEST_TYPES="Python,NUnit"` means to exclude Python tests and
|
||||
NUnit tests. See `TestTypeEnum` in `build.gradle` for available options.
|
||||
* `INCLUDE_TEST_MODULES` - Default to empty string, which means to include the
|
||||
tests for every modules. To narrow down modules to test, set this properties
|
||||
with a list of case-insensitive module strings separated by comma. For
|
||||
instance, `-PINCLUDE_TEST_MODULES="Tool,AndroidResolver"` means to run tests
|
||||
for tools and Android Resolver only. See `TestModuleEnum` in `build.gradle`
|
||||
for available options.
|
||||
* `EXCLUDE_TEST_MODULES` - Default to empty string, which means to exclude
|
||||
none. To add modules to exclude, set this properties with a list of
|
||||
case-insensitive module strings separated by comma. For instance,
|
||||
`-PEXCLUDE_TEST_MODULES="Tool,AndroidResolver"` means to run tests for any
|
||||
modules other than tools and Android Resolver. See `TestModuleEnum` in
|
||||
`build.gradle` for available options.
|
||||
* `EXCLUDE_TESTS` - Default to empty string, which means to exclude none. To
|
||||
add tests to exclude, set this properties with a list of case-insensitive
|
||||
test names separated by comma. For instance,
|
||||
`-PEXCLUDE_TESTS="testGenGuids,testDownloadArtifacts"` means to run tests
|
||||
except the tests with name of `testGenGuids` and `testDownloadArtifacts`.
|
||||
* `CONTINUE_ON_FAIL_FOR_TESTS_ENABLED` - Default to `1`. Set to `1` to
|
||||
continue running the next test when the current one fails. Otherwise, the
|
||||
build script stops whenever any test fails.
|
||||
|
||||
For instance, by running the following command, it only runs the Unity
|
||||
integration tests that does not requires GPU, but exclude tests for Android
|
||||
Resolver module and iOS Resolver module.
|
||||
|
||||
```shell
|
||||
./gradlew test \
|
||||
-PINTERACTIVE_MODE_TESTS_ENABLED=0 \
|
||||
-PINCLUDE_TEST_TYPES="Integration" \
|
||||
-PEXCLUDE_TEST_MODULES="AndroidResolver,iOSResolver"
|
||||
```
|
||||
|
||||
## Releasing
|
||||
|
||||
Each time a new build of this plugin is checked into the source tree you need to
|
||||
do the following:
|
||||
|
||||
* Bump the plugin version variable `pluginVersion` in `build.gradle`
|
||||
* Update `CHANGELOG.md` with the new version number and changes included in
|
||||
the release.
|
||||
* Build the release using `./gradlew release` which performs the following:
|
||||
* Updates `external-dependency-manager-*.unitypackage`
|
||||
* Copies the unpacked plugin to the `exploded` directory.
|
||||
* Updates template metadata files in the `plugin` directory. The GUIDs of
|
||||
all asset metadata is modified due to the version number change. Each
|
||||
file within the plugin is versioned to allow multiple versions of the
|
||||
plugin to be imported into a Unity project which allows the most recent
|
||||
version to be activated by the Version Handler component.
|
||||
* Create release commit using `./gradlew gitCreateReleaseCommit` which
|
||||
performs `git commit -a -m "description from CHANGELOG.md"`
|
||||
* Once the release commit is merge, tag the release using `./gradlew
|
||||
gitTagRelease` which performs the following:
|
||||
* `git tag -a pluginVersion -m "version RELEASE"` to tag the release.
|
||||
* Update tags on remote branch using `git push --tag REMOTE HEAD:master`
|
||||
11
Assets/ExternalDependencyManager/Editor/README.md.meta
Normal file
11
Assets/ExternalDependencyManager/Editor/README.md.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: db858ca0dc4e44d79fe00e9d1a428eba
|
||||
labels:
|
||||
- gvh
|
||||
- gvh_version-13.11.0
|
||||
- gvhp_exportpath-ExternalDependencyManager/Editor/README.md
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/FacebookSDK.meta
Normal file
8
Assets/FacebookSDK.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 48350942acaef4975a95d60a8f8b34d3
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/FacebookSDK/Plugins.meta
Normal file
8
Assets/FacebookSDK/Plugins.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b0fa1e02c18a7477192d40dbde1fbb94
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Assets/FacebookSDK/Plugins/.DS_Store
vendored
Normal file
BIN
Assets/FacebookSDK/Plugins/.DS_Store
vendored
Normal file
Binary file not shown.
8
Assets/FacebookSDK/Plugins/Android.meta
Normal file
8
Assets/FacebookSDK/Plugins/Android.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6c1e341b7555e4e68bca6fe9ec10b0d0
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Assets/FacebookSDK/Plugins/Android/Facebook.Unity.Android.dll
Normal file
BIN
Assets/FacebookSDK/Plugins/Android/Facebook.Unity.Android.dll
Normal file
Binary file not shown.
@@ -0,0 +1,26 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fb286cd0f33ce4f5e81baf10dab8d865
|
||||
PluginImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 3
|
||||
iconMap: {}
|
||||
executionOrder: {}
|
||||
defineConstraints: []
|
||||
isPreloaded: 0
|
||||
isOverridable: 0
|
||||
isExplicitlyReferenced: 0
|
||||
validateReferences: 1
|
||||
platformData:
|
||||
Android:
|
||||
enabled: 1
|
||||
settings: {}
|
||||
Any:
|
||||
enabled: 0
|
||||
settings: {}
|
||||
Editor:
|
||||
enabled: 1
|
||||
settings:
|
||||
DefaultValueInitialized: true
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/FacebookSDK/Plugins/Android/libs.meta
Normal file
8
Assets/FacebookSDK/Plugins/Android/libs.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ac0fbda9ff1a44a2084461c42e8dd911
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user