using System; using System.IO; using System.Collections.Generic; using UnityEngine; using Newtonsoft.Json; using BaseGames.Core; namespace BaseGames.Support.Analytics { /// /// 游戏玩法数据分析收集器(架构 16_SupportingModules §7)。 /// 在本地记录结构化事件日志,供后续分析或上传。 /// 注:不收集任何个人身份信息(PII)。 /// public class AnalyticsManager : MonoBehaviour, IAnalyticsService { [Header("配置")] [Tooltip("是否启用分析收集")] [SerializeField] private bool _enabled = true; [Tooltip("超过此条数时自动将缓存刷写到磁盘")] [SerializeField] private int _flushThreshold = 50; private readonly List _buffer = new(); private string _logPath; private void Awake() { #if !UNITY_EDITOR && !DEVELOPMENT_BUILD if (!_enabled) { enabled = false; return; } #endif if (ServiceLocator.GetOrDefault() != null) { Destroy(gameObject); return; } ServiceLocator.Register(this); _logPath = Path.Combine(Application.persistentDataPath, "analytics.json"); } private void OnDestroy() { Flush(); ServiceLocator.Unregister(this); } private void OnApplicationQuit() => Flush(); // ── 实例方法(IAnalyticsService 实现)──────────────────────────────────── /// 记录一个自定义分析事件。 public void Track(string eventName, Dictionary parameters = null) { if (!_enabled) return; Enqueue(eventName, parameters); } // ── 预定义事件 ──────────────────────────────────────────────────────────── public void TrackBossKill(string bossId, float duration, int deathCount) => Track("boss_kill", new Dictionary { { "boss_id", bossId }, { "duration", duration }, { "death_count", deathCount }, }); public void TrackDeath(string cause, string sceneId, Vector2 position) => Track("player_death", new Dictionary { { "cause", cause }, { "scene", sceneId }, { "pos_x", Mathf.RoundToInt(position.x) }, { "pos_y", Mathf.RoundToInt(position.y) }, }); public void TrackAbilityUnlock(string abilityId) => Track("ability_unlock", new Dictionary { { "ability", abilityId } }); public void TrackSessionStart(int slotIndex) => Track("session_start", new Dictionary { { "slot", slotIndex }, }); public void TrackSessionEnd(float playtimeSeconds) => Track("session_end", new Dictionary { { "playtime", playtimeSeconds }, }); // ── 内部实现 ────────────────────────────────────────────────────────────── private void Enqueue(string name, Dictionary parms) { _buffer.Add(new AnalyticsEvent { EventName = name, Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), Parameters = parms ?? new Dictionary(), }); if (_buffer.Count >= _flushThreshold) Flush(); } private void Flush() { if (_buffer.Count == 0) return; try { var existing = new List(); if (File.Exists(_logPath)) { string json = File.ReadAllText(_logPath); var loaded = JsonConvert.DeserializeObject>(json); if (loaded != null) existing.AddRange(loaded); } existing.AddRange(_buffer); File.WriteAllText(_logPath, JsonConvert.SerializeObject(existing, Formatting.Indented)); _buffer.Clear(); } catch (Exception ex) { Debug.LogWarning($"[Analytics] 写入失败: {ex.Message}"); } } [Serializable] private class AnalyticsEvent { public string EventName; public long Timestamp; public Dictionary Parameters; } } } // namespace BaseGames.Support.Analytics