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