第12章 构建跨平台星际漫游科普应用:从天文算法到AR/VR实现

12.1 项目背景与核心功能解析

12.1.1 市场背景与技术选型分析

在当前的数字科普教育领域,天文学内容始终占据着重要地位。传统天文教学受限于时间、天气和地理条件,而AR/VR技术为天文科普带来了革命性突破。根据国际天文教育协会2023年的调查报告,采用沉浸式技术的天文学习应用能够提升学习者73%的知识留存率和68%的学习兴趣。

我们选择Unity作为开发平台,主要基于以下商业考量:Unity的跨平台特性允许我们一次性开发即可部署到iOS、Android、Windows及主流VR设备;其成熟的ARFoundation框架和XR Interaction Toolkit提供了稳定的AR/VR开发基础;庞大的资源商店生态能够降低开发成本。本项目定位为B2C科普应用,同时开放B2B接口供教育机构定制使用。

从技术架构角度,我们需要处理几个核心挑战:实时天文计算的高性能需求、跨平台渲染的一致性、AR环境下的稳定跟踪、VR模式下的舒适性体验。商业应用中还需考虑应用商店的审核标准、不同设备性能的适配、用户数据的隐私保护等问题。

12.1.2 功能需求与用户体验设计

在商业项目中,功能设计必须紧密围绕用户需求和商业模式展开。我们通过用户调研确定了三个核心使用场景:家庭亲子学习、学校课堂教学、天文爱好者户外观测辅助。基于这些场景,设计了以下功能模块:

星空实时模拟模块需要实现基于真实天文数据的星空渲染,支持时间流动控制(加速、减速、暂停)、地理位置识别和星座高亮提示。商业版本特别加入了星座神话故事讲解和天文事件预告(如流星雨、日食等)。

太阳系模拟模块分为普通3D模式和AR增强模式。普通模式用于课堂教学,展示精确比例的行星轨道和运行规律;AR模式允许用户在任何平面(如书桌、地板)上放置太阳系模型,通过手势进行缩放、旋转和选择查看详细信息。

VR沉浸模块针对高端用户和天文馆场景设计,提供第一人称宇宙航行体验。用户可以在虚拟空间中自由移动,近距离观察行星表面细节,体验从地球到冥王星的完整旅行。

商业应用还需包含社交功能:观测记录分享、天文摄影社区、专家在线问答。这些功能通过后端服务器实现,本文重点讨论客户端实现技术。

12.2 项目前期的策划与资源准备

12.2.1 商业项目策划方法论

在商业项目开发中,科学的策划流程是成功的关键。我们采用敏捷开发与阶段式交付相结合的模式。第一阶段(MVP)包含核心天文计算和基础渲染;第二阶段加入AR功能;第三阶段完善VR体验和社交功能。

项目时间线规划如下:天文算法开发(4周)、基础渲染引擎(3周)、AR模块(3周)、VR模块(2周)、测试与优化(2周)。采用两周为一个迭代周期,每个迭代都有可演示的版本产出。

风险评估方面,主要识别了以下几个关键点:天文计算的精度与性能平衡、AR在不同光照条件下的稳定性、低端移动设备的性能适配。针对这些风险,我们制定了应对策略:采用多精度计算模式(高精度用于专业模式,低精度用于实时渲染)、实现AR跟踪状态降级机制、开发可配置的图形质量等级。

12.2.2 资源管理与优化策略

商业项目的资源管理需要专业化的流程。我们建立了以下资源目录结构:

Assets/
├── Scripts/                 # 脚本目录
│   ├── Astronomy/          # 天文算法核心
│   ├── Rendering/          # 渲染控制
│   ├── ARVR/              # AR/VR专用脚本
│   └── UI/                # 用户界面
├── Models/                 # 3D模型
│   ├── Planets/           # 行星模型(多精度版本)
│   ├── Constellations/    # 星座连线
│   └── UI/               # UI相关模型
├── Textures/              # 纹理贴图
│   ├── PlanetSurfaces/    # 行星表面(4K/2K/1K)
│   ├── StarMaps/         # 星图
│   └── UI/               # UI纹理
├── Shaders/               # 自定义着色器
├── Prefabs/               # 预制体
├── Scenes/                # 场景文件
└── StreamingAssets/       # 流式资源(天文数据)

天文数据采用NASA的JPL DE430星历表作为基础,通过预处理转换为应用内可用的二进制格式。行星纹理来自NASA官方发布的数据集,通过Photoscan技术生成高精度法线贴图和高度图。

性能优化方面,我们实现了以下策略:使用Texture2DArray管理行星纹理,减少Draw Call;实现LOD(细节层次)系统,根据距离切换模型精度;使用Compute Shader进行天文计算的GPU加速;采用对象池管理频繁创建销毁的天体标签。

以下代码展示了资源加载管理器的基础实现,采用异步加载避免卡顿:

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;

public class AssetLoaderManager : MonoBehaviour
{
    private static AssetLoaderManager instance;
    public static AssetLoaderManager Instance
    {
        get
        {
            if (instance == null)
            {
                GameObject go = new GameObject("AssetLoaderManager");
                instance = go.AddComponent<AssetLoaderManager>();
                DontDestroyOnLoad(go);
            }
            return instance;
        }
    }

    private Dictionary<string, AsyncOperationHandle> loadedAssets = new Dictionary<string, AsyncOperationHandle>();
    private Dictionary<string, List<System.Action<UnityEngine.Object>>> pendingCallbacks = new Dictionary<string, List<System.Action<UnityEngine.Object>>>();

    public async Task<T> LoadAssetAsync<T>(string addressablePath) where T : UnityEngine.Object
    {
        if (loadedAssets.ContainsKey(addressablePath))
        {
            return loadedAssets[addressablePath].Result as T;
        }

        var handle = Addressables.LoadAssetAsync<T>(addressablePath);
        await handle.Task;

        if (handle.Status == AsyncOperationStatus.Succeeded)
        {
            loadedAssets[addressablePath] = handle;
            return handle.Result;
        }
        else
        {
            Debug.LogError($"Failed to load asset: {addressablePath}");
            return null;
        }
    }

    public void LoadAssetWithCallback<T>(string addressablePath, System.Action<T> callback) where T : UnityEngine.Object
    {
        if (loadedAssets.ContainsKey(addressablePath))
        {
            callback?.Invoke(loadedAssets[addressablePath].Result as T);
            return;
        }

        if (!pendingCallbacks.ContainsKey(addressablePath))
        {
            pendingCallbacks[addressablePath] = new List<System.Action<UnityEngine.Object>>();
        }

        pendingCallbacks[addressablePath].Add((obj) => callback?.Invoke(obj as T));

        if (pendingCallbacks[addressablePath].Count == 1)
        {
            StartLoadAsset<T>(addressablePath);
        }
    }

    private async void StartLoadAsset<T>(string addressablePath) where T : UnityEngine.Object
    {
        var handle = Addressables.LoadAssetAsync<T>(addressablePath);
        await handle.Task;

        if (handle.Status == AsyncOperationStatus.Succeeded)
        {
            loadedAssets[addressablePath] = handle;

            if (pendingCallbacks.ContainsKey(addressablePath))
            {
                foreach (var callback in pendingCallbacks[addressablePath])
                {
                    callback?.Invoke(handle.Result);
                }
                pendingCallbacks.Remove(addressablePath);
            }
        }
    }

    public void ReleaseAsset(string addressablePath)
    {
        if (loadedAssets.ContainsKey(addressablePath))
        {
            Addressables.Release(loadedAssets[addressablePath]);
            loadedAssets.Remove(addressablePath);
        }
    }

    public void ClearAllAssets()
    {
        foreach (var handle in loadedAssets.Values)
        {
            Addressables.Release(handle);
        }
        loadedAssets.Clear();
        pendingCallbacks.Clear();
    }
}

12.3 应用架构设计与核心系统

12.3.1 模块化架构设计

在商业级应用中,清晰的架构设计至关重要。我们采用分层架构,将系统分为数据层、逻辑层、表现层和AR/VR适配层。这种设计有利于团队协作、代码维护和功能扩展。

数据层负责天文数据的存储、读取和计算。我们设计了CelestialDataManager作为数据中枢,采用单例模式确保全局访问一致性。数据分为静态数据(恒星目录、星座信息)和动态数据(行星位置、时间参数),分别采用不同的更新策略。

逻辑层包含天文计算引擎、用户输入处理、状态管理和业务逻辑。天文计算引擎进一步分为太阳系计算、恒星计算和坐标系转换三个子模块,每个子模块都可以独立优化和替换。

表现层采用MVC模式,将数据模型、界面控制和视图渲染分离。Unity的GameObject作为视图载体,通过专门的RendererController组件与逻辑层通信。

AR/VR适配层作为抽象层,隔离平台差异。我们定义了IARModule和IVRModule接口,为不同平台提供统一的操作接口。

12.3.2 核心脚本系统详解

以下是主要脚本类的职责和关系设计:

using System;
using System.Collections.Generic;
using UnityEngine;

public class CelestialDataManager : MonoBehaviour
{
    private static CelestialDataManager instance;
    public static CelestialDataManager Instance
    {
        get
        {
            if (instance == null)
            {
                instance = FindObjectOfType<CelestialDataManager>();
                if (instance == null)
                {
                    GameObject go = new GameObject("CelestialDataManager");
                    instance = go.AddComponent<CelestialDataManager>();
                }
            }
            return instance;
        }
    }

    private StarCatalog starCatalog;
    private PlanetPositionCalculator planetCalculator;
    private LunarPositionCalculator lunarCalculator;
    private DateTime currentSimulationTime;
    private double julianDate;

    public DateTime CurrentSimulationTime
    {
        get { return currentSimulationTime; }
        set
        {
            currentSimulationTime = value;
            julianDate = TimeUtils.ConvertToJulianDate(value);
            UpdateAllCelestialPositions();
        }
    }

    private void Awake()
    {
        if (instance != null && instance != this)
        {
            Destroy(gameObject);
            return;
        }
        instance = this;
        DontDestroyOnLoad(gameObject);

        InitializeComponents();
        LoadStaticData();
    }

    private void InitializeComponents()
    {
        starCatalog = new StarCatalog();
        planetCalculator = new PlanetPositionCalculator();
        lunarCalculator = new LunarPositionCalculator();
        
        currentSimulationTime = DateTime.UtcNow;
        julianDate = TimeUtils.ConvertToJulianDate(currentSimulationTime);
    }

    private void LoadStaticData()
    {
        starCatalog.LoadFromFile(Application.streamingAssetsPath + "/StarData/stars.bin");
    }

    private void UpdateAllCelestialPositions()
    {
        planetCalculator.UpdatePositions(julianDate);
        lunarCalculator.UpdatePosition(julianDate);
    }

    public Vector3 GetPlanetPosition(CelestialBodyType bodyType)
    {
        return planetCalculator.GetPosition(bodyType);
    }

    public Quaternion GetPlanetRotation(CelestialBodyType bodyType)
    {
        return planetCalculator.GetRotation(bodyType);
    }

    public StarData[] GetVisibleStars(Vector3 observerPosition, float fov, float magnitudeLimit)
    {
        return starCatalog.GetVisibleStars(observerPosition, fov, magnitudeLimit, julianDate);
    }
}

public enum CelestialBodyType
{
    Sun,
    Mercury,
    Venus,
    Earth,
    Moon,
    Mars,
    Jupiter,
    Saturn,
    Uranus,
    Neptune
}

public struct StarData
{
    public float rightAscension;
    public float declination;
    public float magnitude;
    public Color color;
    public string name;
    public string constellation;
}

12.4 天文计算核心算法实现

12.4.1 天文坐标系转换系统

商业天文应用需要处理多种坐标系之间的转换:赤道坐标系(用于星表)、黄道坐标系(用于太阳系)、地平坐标系(用于观察者视角)。我们实现了完整的转换链,支持任意坐标系间的转换。

赤道坐标系是基础坐标系,以地球赤道面为基准平面。黄道坐标系以地球公转轨道面(黄道面)为基准,特别适合太阳系天体计算。地平坐标系以观察者所在位置为基准,直接对应观察者看到的天空。

以下是坐标系转换的核心实现:

using System;
using UnityEngine;

public static class CoordinateConverter
{
    private const float OBLIQUITY_2000 = 23.4392911f; // 2000年黄赤交角

    public static Vector3 EquatorialToHorizontal(Vector3 equatorial, float latitude, float longitude, double julianDate)
    {
        double localSiderealTime = CalculateLocalSiderealTime(longitude, julianDate);
        
        float hourAngle = (float)(localSiderealTime * 15f - equatorial.x); // 赤经转时角
        hourAngle = Mathf.Repeat(hourAngle + 180f, 360f) - 180f;
        
        float decRad = equatorial.y * Mathf.Deg2Rad;
        float haRad = hourAngle * Mathf.Deg2Rad;
        float latRad = latitude * Mathf.Deg2Rad;
        
        float sinAlt = Mathf.Sin(decRad) * Mathf.Sin(latRad) + 
                      Mathf.Cos(decRad) * Mathf.Cos(latRad) * Mathf.Cos(haRad);
        float altitude = Mathf.Asin(sinAlt) * Mathf.Rad2Deg;
        
        float cosAz = (Mathf.Sin(decRad) - Mathf.Sin(altitude * Mathf.Deg2Rad) * Mathf.Sin(latRad)) / 
                     (Mathf.Cos(altitude * Mathf.Deg2Rad) * Mathf.Cos(latRad));
        cosAz = Mathf.Clamp(cosAz, -1f, 1f);
        
        float azimuth = Mathf.Acos(cosAz) * Mathf.Rad2Deg;
        if (Mathf.Sin(haRad) > 0)
        {
            azimuth = 360f - azimuth;
        }
        
        return new Vector3(azimuth, altitude, equatorial.z);
    }

    public static Vector3 EclipticToEquatorial(Vector3 ecliptic)
    {
        float eclipticLonRad = ecliptic.x * Mathf.Deg2Rad;
        float eclipticLatRad = ecliptic.y * Mathf.Deg2Rad;
        float obliquityRad = OBLIQUITY_2000 * Mathf.Deg2Rad;
        
        float sinDec = Mathf.Sin(eclipticLatRad) * Mathf.Cos(obliquityRad) + 
                      Mathf.Cos(eclipticLatRad) * Mathf.Sin(obliquityRad) * Mathf.Sin(eclipticLonRad);
        float declination = Mathf.Asin(sinDec) * Mathf.Rad2Deg;
        
        float y = Mathf.Sin(eclipticLonRad) * Mathf.Cos(obliquityRad) - 
                 Mathf.Tan(eclipticLatRad) * Mathf.Sin(obliquityRad);
        float x = Mathf.Cos(eclipticLonRad);
        
        float rightAscension = Mathf.Atan2(y, x) * Mathf.Rad2Deg;
        if (rightAscension < 0)
        {
            rightAscension += 360f;
        }
        
        return new Vector3(rightAscension / 15f, declination, ecliptic.z);
    }

    private static double CalculateLocalSiderealTime(float longitude, double julianDate)
    {
        double t = (julianDate - 2451545.0) / 36525.0;
        
        double gmst = 280.46061837 + 360.98564736629 * (julianDate - 2451545.0) +
                     0.000387933 * t * t - t * t * t / 38710000.0;
        
        gmst = gmst % 360.0;
        if (gmst < 0)
        {
            gmst += 360.0;
        }
        
        double lst = gmst + longitude;
        lst = lst % 360.0;
        if (lst < 0)
        {
            lst += 360.0;
        }
        
        return lst / 15.0;
    }
}

12.4.2 太阳系天体位置计算

行星位置计算采用VSOP87(Variations Séculaires des Orbites Planétaires)理论的简化版本,平衡精度和性能。对于商业应用,我们实现了两种精度模式:高精度模式用于专业天文计算,低精度模式用于实时渲染。

using System;
using System.Collections.Generic;
using UnityEngine;

public class PlanetPositionCalculator
{
    private Dictionary<CelestialBodyType, PlanetOrbitalElements> orbitalElements;
    private Dictionary<CelestialBodyType, Vector3> currentPositions;
    private Dictionary<CelestialBodyType, Quaternion> currentRotations;

    public PlanetPositionCalculator()
    {
        orbitalElements = new Dictionary<CelestialBodyType, PlanetOrbitalElements>();
        currentPositions = new Dictionary<CelestialBodyType, Vector3>();
        currentRotations = new Dictionary<CelestialBodyType, Quaternion>();
        
        InitializeOrbitalElements();
    }

    private void InitializeOrbitalElements()
    {
        orbitalElements[CelestialBodyType.Mercury] = new PlanetOrbitalElements
        {
            semiMajorAxis = 0.38709893f,
            eccentricity = 0.20563069f,
            inclination = 7.00487f,
            meanLongitude = 252.25084f,
            perihelionLongitude = 77.45645f,
            ascendingNodeLongitude = 48.33167f
        };
        
        orbitalElements[CelestialBodyType.Venus] = new PlanetOrbitalElements
        {
            semiMajorAxis = 0.72333199f,
            eccentricity = 0.00677323f,
            inclination = 3.39471f,
            meanLongitude = 181.97973f,
            perihelionLongitude = 131.53298f,
            ascendingNodeLongitude = 76.68069f
        };
        
        orbitalElements[CelestialBodyType.Earth] = new PlanetOrbitalElements
        {
            semiMajorAxis = 1.00000011f,
            eccentricity = 0.01671022f,
            inclination = 0.00005f,
            meanLongitude = 100.46435f,
            perihelionLongitude = 102.94719f,
            ascendingNodeLongitude = -11.26064f
        };
    }

    public void UpdatePositions(double julianDate)
    {
        double t = (julianDate - 2451545.0) / 36525.0;
        
        foreach (var kvp in orbitalElements)
        {
            CelestialBodyType bodyType = kvp.Key;
            PlanetOrbitalElements elements = kvp.Value;
            
            Vector3 position = CalculateHeliocentricPosition(elements, t);
            Quaternion rotation = CalculateAxialRotation(bodyType, julianDate);
            
            currentPositions[bodyType] = position;
            currentRotations[bodyType] = rotation;
        }
    }

    private Vector3 CalculateHeliocentricPosition(PlanetOrbitalElements elements, double t)
    {
        float meanAnomaly = CalculateMeanAnomaly(elements, t);
        float eccentricAnomaly = SolveKeplerEquation(meanAnomaly, elements.eccentricity);
        
        float trueAnomaly = 2f * Mathf.Atan(
            Mathf.Sqrt((1f + elements.eccentricity) / (1f - elements.eccentricity)) * 
            Mathf.Tan(eccentricAnomaly / 2f));
        
        float distance = elements.semiMajorAxis * (1f - elements.eccentricity * Mathf.Cos(eccentricAnomaly));
        
        float x = distance * (Mathf.Cos(elements.ascendingNodeLongitude * Mathf.Deg2Rad) * 
                             Mathf.Cos(trueAnomaly + elements.perihelionLongitude * Mathf.Deg2Rad - 
                                      elements.ascendingNodeLongitude * Mathf.Deg2Rad) - 
                             Mathf.Sin(elements.ascendingNodeLongitude * Mathf.Deg2Rad) * 
                             Mathf.Sin(trueAnomaly + elements.perihelionLongitude * Mathf.Deg2Rad - 
                                      elements.ascendingNodeLongitude * Mathf.Deg2Rad) * 
                             Mathf.Cos(elements.inclination * Mathf.Deg2Rad));
        
        float y = distance * (Mathf.Sin(trueAnomaly + elements.perihelionLongitude * Mathf.Deg2Rad - 
                                      elements.ascendingNodeLongitude * Mathf.Deg2Rad) * 
                             Mathf.Sin(elements.inclination * Mathf.Deg2Rad));
        
        float z = distance * (Mathf.Sin(elements.ascendingNodeLongitude * Mathf.Deg2Rad) * 
                             Mathf.Cos(trueAnomaly + elements.perihelionLongitude * Mathf.Deg2Rad - 
                                      elements.ascendingNodeLongitude * Mathf.Deg2Rad) + 
                             Mathf.Cos(elements.ascendingNodeLongitude * Mathf.Deg2Rad) * 
                             Mathf.Sin(trueAnomaly + elements.perihelionLongitude * Mathf.Deg2Rad - 
                                      elements.ascendingNodeLongitude * Mathf.Deg2Rad) * 
                             Mathf.Cos(elements.inclination * Mathf.Deg2Rad));
        
        return new Vector3(x, y, z);
    }

    private float CalculateMeanAnomaly(PlanetOrbitalElements elements, double t)
    {
        float meanLongitude = elements.meanLongitude + 0.9856074f * (float)t;
        float perihelionLongitude = elements.perihelionLongitude + 0.000025f * (float)t;
        
        float meanAnomaly = meanLongitude - perihelionLongitude;
        meanAnomaly = Mathf.Repeat(meanAnomaly + 180f, 360f) - 180f;
        
        return meanAnomaly * Mathf.Deg2Rad;
    }

    private float SolveKeplerEquation(float meanAnomaly, float eccentricity, int maxIterations = 50, float tolerance = 1e-8f)
    {
        float eccentricAnomaly = meanAnomaly;
        
        for (int i = 0; i < maxIterations; i++)
        {
            float f = eccentricAnomaly - eccentricity * Mathf.Sin(eccentricAnomaly) - meanAnomaly;
            float fPrime = 1f - eccentricity * Mathf.Cos(eccentricAnomaly);
            
            float delta = f / fPrime;
            eccentricAnomaly -= delta;
            
            if (Mathf.Abs(delta) < tolerance)
            {
                break;
            }
        }
        
        return eccentricAnomaly;
    }

    private Quaternion CalculateAxialRotation(CelestialBodyType bodyType, double julianDate)
    {
        float rotationAngle = 0f;
        
        switch (bodyType)
        {
            case CelestialBodyType.Earth:
                double daysSinceJ2000 = julianDate - 2451545.0;
                rotationAngle = (float)(280.46061837 + 360.98564736629 * daysSinceJ2000);
                break;
                
            case CelestialBodyType.Mars:
                rotationAngle = (float)((julianDate - 2451545.0) * 350.89198226);
                break;
                
            default:
                rotationAngle = (float)((julianDate - 2451545.0) * 360f / GetRotationPeriod(bodyType));
                break;
        }
        
        rotationAngle = Mathf.Repeat(rotationAngle, 360f);
        
        return Quaternion.Euler(0f, rotationAngle, 0f);
    }

    private float GetRotationPeriod(CelestialBodyType bodyType)
    {
        switch (bodyType)
        {
            case CelestialBodyType.Mercury: return 58.6462f;
            case CelestialBodyType.Venus: return -243.0185f;
            case CelestialBodyType.Earth: return 0.99726968f;
            case CelestialBodyType.Mars: return 1.02595675f;
            case CelestialBodyType.Jupiter: return 0.41354f;
            case CelestialBodyType.Saturn: return 0.44401f;
            case CelestialBodyType.Uranus: return -0.71833f;
            case CelestialBodyType.Neptune: return 0.67125f;
            default: return 1f;
        }
    }

    public Vector3 GetPosition(CelestialBodyType bodyType)
    {
        if (currentPositions.ContainsKey(bodyType))
        {
            return currentPositions[bodyType];
        }
        return Vector3.zero;
    }

    public Quaternion GetRotation(CelestialBodyType bodyType)
    {
        if (currentRotations.ContainsKey(bodyType))
        {
            return currentRotations[bodyType];
        }
        return Quaternion.identity;
    }
}

public struct PlanetOrbitalElements
{
    public float semiMajorAxis;
    public float eccentricity;
    public float inclination;
    public float meanLongitude;
    public float perihelionLongitude;
    public float ascendingNodeLongitude;
}

12.5 星空可视化渲染系统

12.5.1 大规模星点渲染优化

渲染数千甚至数万颗星星是性能敏感任务。我们采用GPU实例化技术,结合四叉树空间分割,实现高效的大规模星点渲染。

using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;

public class StarFieldRenderer : MonoBehaviour
{
    public Mesh starMesh;
    public Material starMaterial;
    public int maxStarsPerBatch = 1023;
    public float magnitudeCutoff = 6.0f;
    
    private List<Matrix4x4[]> starMatricesBatches;
    private List<int> starCountPerBatch;
    private StarData[] visibleStars;
    private MaterialPropertyBlock materialPropertyBlock;
    private ComputeBuffer starDataBuffer;
    
    private void Start()
    {
        starMatricesBatches = new List<Matrix4x4[]>();
        starCountPerBatch = new List<int>();
        materialPropertyBlock = new MaterialPropertyBlock();
        
        InitializeStarDataBuffer();
    }
    
    private void InitializeStarDataBuffer()
    {
        starDataBuffer = new ComputeBuffer(1000, sizeof(float) * 8, ComputeBufferType.Structured);
        
        Vector4[] starData = new Vector4[1000];
        for (int i = 0; i < 1000; i++)
        {
            starData[i] = new Vector4(
                UnityEngine.Random.Range(0f, 1f),
                UnityEngine.Random.Range(0f, 1f),
                UnityEngine.Random.Range(0f, 1f),
                UnityEngine.Random.Range(0.5f, 2f)
            );
        }
        
        starDataBuffer.SetData(starData);
        materialPropertyBlock.SetBuffer("_StarData", starDataBuffer);
    }
    
    public void UpdateStarPositions(StarData[] stars, Transform observerTransform)
    {
        if (stars == null || stars.Length == 0)
        {
            starMatricesBatches.Clear();
            starCountPerBatch.Clear();
            return;
        }
        
        starMatricesBatches.Clear();
        starCountPerBatch.Clear();
        
        List<Matrix4x4> currentBatch = new List<Matrix4x4>();
        
        for (int i = 0; i < stars.Length; i++)
        {
            if (stars[i].magnitude > magnitudeCutoff)
            {
                continue;
            }
            
            Vector3 equatorialPosition = new Vector3(
                stars[i].rightAscension,
                stars[i].declination,
                100f
            );
            
            Vector3 horizontalPosition = CoordinateConverter.EquatorialToHorizontal(
                equatorialPosition,
                observerTransform.position.y,
                observerTransform.position.x,
                CelestialDataManager.Instance.CurrentSimulationTime
            );
            
            Vector3 starWorldPosition = CalculateStarWorldPosition(horizontalPosition);
            float starSize = CalculateStarSize(stars[i].magnitude);
            
            Matrix4x4 matrix = Matrix4x4.TRS(
                starWorldPosition,
                Quaternion.LookRotation(starWorldPosition - observerTransform.position),
                Vector3.one * starSize
            );
            
            currentBatch.Add(matrix);
            
            if (currentBatch.Count >= maxStarsPerBatch)
            {
                starMatricesBatches.Add(currentBatch.ToArray());
                starCountPerBatch.Add(currentBatch.Count);
                currentBatch.Clear();
            }
        }
        
        if (currentBatch.Count > 0)
        {
            starMatricesBatches.Add(currentBatch.ToArray());
            starCountPerBatch.Add(currentBatch.Count);
        }
        
        visibleStars = stars;
    }
    
    private Vector3 CalculateStarWorldPosition(Vector3 horizontalPosition)
    {
        float distance = 1000f;
        
        float azimuthRad = horizontalPosition.x * Mathf.Deg2Rad;
        float altitudeRad = horizontalPosition.y * Mathf.Deg2Rad;
        
        float x = distance * Mathf.Cos(altitudeRad) * Mathf.Sin(azimuthRad);
        float y = distance * Mathf.Sin(altitudeRad);
        float z = distance * Mathf.Cos(altitudeRad) * Mathf.Cos(azimuthRad);
        
        return new Vector3(x, y, z);
    }
    
    private float CalculateStarSize(float magnitude)
    {
        float baseSize = 0.1f;
        float brightnessFactor = Mathf.Pow(2.512f, -magnitude + 2.0f);
        
        return baseSize * brightnessFactor;
    }
    
    private void Update()
    {
        if (starMatricesBatches == null || starMatricesBatches.Count == 0)
        {
            return;
        }
        
        for (int i = 0; i < starMatricesBatches.Count; i++)
        {
            Graphics.DrawMeshInstanced(
                starMesh,
                0,
                starMaterial,
                starMatricesBatches[i],
                starCountPerBatch[i],
                materialPropertyBlock,
                ShadowCastingMode.Off,
                false,
                0,
                null,
                LightProbeUsage.Off,
                null
            );
        }
    }
    
    private void OnDestroy()
    {
        if (starDataBuffer != null)
        {
            starDataBuffer.Release();
            starDataBuffer = null;
        }
    }
}

12.5.2 星座连线与特效系统

星座连线需要特殊处理,我们采用LineRenderer与自定义着色器结合的方式,实现流畅的星座连线效果。

using System;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(LineRenderer))]
public class ConstellationLineRenderer : MonoBehaviour
{
    public ConstellationData constellationData;
    public float lineWidth = 0.02f;
    public Color lineColor = Color.cyan;
    public float fadeDistance = 50f;
    public float maxDrawDistance = 100f;
    
    private LineRenderer lineRenderer;
    private Transform observerTransform;
    private List<Vector3> starPositions;
    private Material lineMaterial;
    
    private void Awake()
    {
        lineRenderer = GetComponent<LineRenderer>();
        lineRenderer.positionCount = 0;
        lineRenderer.startWidth = lineWidth;
        lineRenderer.endWidth = lineWidth;
        lineRenderer.startColor = lineColor;
        lineRenderer.endColor = lineColor;
        
        lineMaterial = new Material(Shader.Find("Custom/ConstellationLine"));
        lineRenderer.material = lineMaterial;
        
        starPositions = new List<Vector3>();
    }
    
    private void Start()
    {
        observerTransform = Camera.main.transform;
        InitializeConstellationLines();
    }
    
    private void InitializeConstellationLines()
    {
        if (constellationData == null || constellationData.starConnections.Count == 0)
        {
            return;
        }
        
        starPositions.Clear();
        
        foreach (var connection in constellationData.starConnections)
        {
            Vector3 fromPosition = GetStarWorldPosition(connection.fromStarIndex);
            Vector3 toPosition = GetStarWorldPosition(connection.toStarIndex);
            
            if (fromPosition != Vector3.zero && toPosition != Vector3.zero)
            {
                starPositions.Add(fromPosition);
                starPositions.Add(toPosition);
            }
        }
        
        UpdateLineRenderer();
    }
    
    private Vector3 GetStarWorldPosition(int starIndex)
    {
        StarCatalog starCatalog = CelestialDataManager.Instance.StarCatalog;
        StarData starData = starCatalog.GetStarByIndex(starIndex);
        
        if (starData == null)
        {
            return Vector3.zero;
        }
        
        Vector3 equatorialPosition = new Vector3(
            starData.rightAscension,
            starData.declination,
            100f
        );
        
        Vector3 horizontalPosition = CoordinateConverter.EquatorialToHorizontal(
            equatorialPosition,
            observerTransform.position.y,
            observerTransform.position.x,
            DateTime.UtcNow
        );
        
        return CalculateStarWorldPosition(horizontalPosition);
    }
    
    private Vector3 CalculateStarWorldPosition(Vector3 horizontalPosition)
    {
        float distance = 800f;
        
        float azimuthRad = horizontalPosition.x * Mathf.Deg2Rad;
        float altitudeRad = horizontalPosition.y * Mathf.Deg2Rad;
        
        float x = distance * Mathf.Cos(altitudeRad) * Mathf.Sin(azimuthRad);
        float y = distance * Mathf.Sin(altitudeRad);
        float z = distance * Mathf.Cos(altitudeRad) * Mathf.Cos(azimuthRad);
        
        return new Vector3(x, y, z);
    }
    
    private void UpdateLineRenderer()
    {
        if (starPositions.Count == 0)
        {
            lineRenderer.positionCount = 0;
            return;
        }
        
        lineRenderer.positionCount = starPositions.Count;
        lineRenderer.SetPositions(starPositions.ToArray());
    }
    
    private void Update()
    {
        if (observerTransform == null || starPositions.Count == 0)
        {
            return;
        }
        
        float distanceToObserver = Vector3.Distance(transform.position, observerTransform.position);
        
        if (distanceToObserver > maxDrawDistance)
        {
            lineRenderer.enabled = false;
            return;
        }
        
        lineRenderer.enabled = true;
        
        float alpha = Mathf.Clamp01(1f - (distanceToObserver / fadeDistance));
        Color currentColor = lineColor;
        currentColor.a = alpha;
        
        lineRenderer.startColor = currentColor;
        lineRenderer.endColor = currentColor;
        
        UpdateLineMaterialProperties();
    }
    
    private void UpdateLineMaterialProperties()
    {
        if (lineMaterial == null)
        {
            return;
        }
        
        lineMaterial.SetFloat("_CurrentTime", Time.time);
        lineMaterial.SetVector("_ObserverPosition", observerTransform.position);
        
        if (Camera.main != null)
        {
            lineMaterial.SetMatrix("_InvViewMatrix", Camera.main.worldToCameraMatrix.inverse);
        }
    }
    
    public void HighlightConstellation(bool highlight)
    {
        if (lineMaterial == null)
        {
            return;
        }
        
        lineMaterial.SetFloat("_Highlight", highlight ? 1f : 0f);
    }
}

[System.Serializable]
public class ConstellationData
{
    public string constellationName;
    public string abbreviation;
    public List<StarConnection> starConnections;
    public Vector3 centerPosition;
    public string mythologyStory;
}

[System.Serializable]
public class StarConnection
{
    public int fromStarIndex;
    public int toStarIndex;
}

星座连线着色器实现:

Shader "Custom/ConstellationLine"
{
    Properties
    {
        _Color ("Color", Color) = (0, 1, 1, 1)
        _Highlight ("Highlight", Range(0, 1)) = 0
        _PulseSpeed ("Pulse Speed", Float) = 1.0
        _LineWidth ("Line Width", Float) = 0.02
    }
    
    SubShader
    {
        Tags { "RenderType"="Transparent" "Queue"="Transparent" }
        LOD 100
        
        Blend SrcAlpha OneMinusSrcAlpha
        ZWrite Off
        Cull Off
        
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma geometry geom
            #pragma target 4.0
            
            #include "UnityCG.cginc"
            
            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };
            
            struct v2g
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };
            
            struct g2f
            {
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
                float distance : TEXCOORD1;
            };
            
            float4 _Color;
            float _Highlight;
            float _PulseSpeed;
            float _LineWidth;
            float _CurrentTime;
            float4 _ObserverPosition;
            
            v2g vert (appdata v)
            {
                v2g o;
                o.vertex = v.vertex;
                o.uv = v.uv;
                return o;
            }
            
            [maxvertexcount(4)]
            void geom(line v2g input[2], inout TriangleStream<g2f> output)
            {
                float3 p0 = input[0].vertex.xyz;
                float3 p1 = input[1].vertex.xyz;
                
                float3 viewDir = normalize(_ObserverPosition.xyz - (p0 + p1) * 0.5);
                float3 lineDir = normalize(p1 - p0);
                float3 right = normalize(cross(viewDir, lineDir));
                
                float halfWidth = _LineWidth * 0.5;
                
                g2f v;
                
                v.vertex = UnityWorldToClipPos(float4(p0 - right * halfWidth, 1.0));
                v.uv = float2(0, 0);
                v.distance = distance(p0, _ObserverPosition);
                output.Append(v);
                
                v.vertex = UnityWorldToClipPos(float4(p0 + right * halfWidth, 1.0));
                v.uv = float2(1, 0);
                v.distance = distance(p0, _ObserverPosition);
                output.Append(v);
                
                v.vertex = UnityWorldToClipPos(float4(p1 - right * halfWidth, 1.0));
                v.uv = float2(0, 1);
                v.distance = distance(p1, _ObserverPosition);
                output.Append(v);
                
                v.vertex = UnityWorldToClipPos(float4(p1 + right * halfWidth, 1.0));
                v.uv = float2(1, 1);
                v.distance = distance(p1, _ObserverPosition);
                output.Append(v);
            }
            
            fixed4 frag (g2f i) : SV_Target
            {
                float pulse = sin(_CurrentTime * _PulseSpeed) * 0.5 + 0.5;
                float distanceFade = 1.0 - saturate(i.distance / 100.0);
                
                float4 color = _Color;
                color.rgb = lerp(color.rgb, float3(1, 1, 0.5), _Highlight);
                color.a *= pulse * distanceFade;
                
                return color;
            }
            ENDCG
        }
    }
    FallBack "Diffuse"
}

12.6 太阳系动态模拟系统

12.6.1 物理精确的轨道模拟

在商业天文应用中,太阳系模拟需要在视觉吸引力和科学准确性之间找到平衡。我们实现了基于开普勒定律的简化物理模拟,同时提供了教育模式(精确比例)和观赏模式(视觉优化)两种选项。

using System;
using System.Collections.Generic;
using UnityEngine;

public class SolarSystemSimulator : MonoBehaviour
{
    public enum SimulationMode
    {
        Educational,
        Spectacular,
        RealTime
    }
    
    [Header("Simulation Settings")]
    public SimulationMode currentMode = SimulationMode.Educational;
    public float timeScale = 1.0f;
    public bool pauseSimulation = false;
    
    [Header("Planetary Settings")]
    public float planetScaleMultiplier = 1000f;
    public float orbitScaleMultiplier = 0.1f;
    public bool showOrbitPaths = true;
    public bool showPlanetLabels = true;
    
    [Header("References")]
    public Transform sunTransform;
    public GameObject planetPrefab;
    public GameObject orbitPathPrefab;
    public Transform planetContainer;
    public Transform orbitContainer;
    
    private Dictionary<CelestialBodyType, PlanetController> planets;
    private Dictionary<CelestialBodyType, LineRenderer> orbitPaths;
    private DateTime simulationDateTime;
    private double simulationJulianDate;
    private float accumulatedDeltaTime;
    
    private const float SECONDS_PER_DAY = 86400f;
    private const float AU_TO_UNITY_UNITS = 100f;
    
    private void Awake()
    {
        planets = new Dictionary<CelestialBodyType, PlanetController>();
        orbitPaths = new Dictionary<CelestialBodyType, LineRenderer>();
        simulationDateTime = DateTime.UtcNow;
        simulationJulianDate = TimeUtils.ConvertToJulianDate(simulationDateTime);
    }
    
    private void Start()
    {
        InitializeSolarSystem();
        CreateOrbitPaths();
    }
    
    private void InitializeSolarSystem()
    {
        CelestialBodyType[] planetTypes = {
            CelestialBodyType.Mercury,
            CelestialBodyType.Venus,
            CelestialBodyType.Earth,
            CelestialBodyType.Mars,
            CelestialBodyType.Jupiter,
            CelestialBodyType.Saturn,
            CelestialBodyType.Uranus,
            CelestialBodyType.Neptune
        };
        
        foreach (CelestialBodyType planetType in planetTypes)
        {
            CreatePlanet(planetType);
        }
    }
    
    private void CreatePlanet(CelestialBodyType planetType)
    {
        GameObject planetGO = Instantiate(planetPrefab, planetContainer);
        planetGO.name = planetType.ToString();
        
        PlanetController controller = planetGO.GetComponent<PlanetController>();
        controller.Initialize(planetType, currentMode);
        
        planets[planetType] = controller;
        
        SetPlanetScale(controller, planetType);
        SetPlanetPosition(controller, planetType);
    }
    
    private void SetPlanetScale(PlanetController controller, CelestialBodyType planetType)
    {
        float scale = GetPlanetScale(planetType);
        
        switch (currentMode)
        {
            case SimulationMode.Educational:
                controller.transform.localScale = Vector3.one * scale * planetScaleMultiplier;
                break;
                
            case SimulationMode.Spectacular:
                float spectacularScale = Mathf.Pow(scale, 0.5f) * planetScaleMultiplier * 2f;
                controller.transform.localScale = Vector3.one * spectacularScale;
                break;
                
            case SimulationMode.RealTime:
                controller.transform.localScale = Vector3.one * scale * planetScaleMultiplier;
                break;
        }
    }
    
    private float GetPlanetScale(CelestialBodyType planetType)
    {
        switch (planetType)
        {
            case CelestialBodyType.Mercury: return 0.383f;
            case CelestialBodyType.Venus: return 0.949f;
            case CelestialBodyType.Earth: return 1.0f;
            case CelestialBodyType.Mars: return 0.532f;
            case CelestialBodyType.Jupiter: return 11.21f;
            case CelestialBodyType.Saturn: return 9.45f;
            case CelestialBodyType.Uranus: return 4.01f;
            case CelestialBodyType.Neptune: return 3.88f;
            default: return 1.0f;
        }
    }
    
    private void SetPlanetPosition(PlanetController controller, CelestialBodyType planetType)
    {
        Vector3 position = CelestialDataManager.Instance.GetPlanetPosition(planetType);
        
        switch (currentMode)
        {
            case SimulationMode.Educational:
                controller.transform.position = position * AU_TO_UNITY_UNITS * orbitScaleMultiplier;
                break;
                
            case SimulationMode.Spectacular:
                float distanceScale = Mathf.Log(Vector3.Distance(Vector3.zero, position) + 1f);
                controller.transform.position = position.normalized * distanceScale * 10f;
                break;
                
            case SimulationMode.RealTime:
                controller.transform.position = position * AU_TO_UNITY_UNITS;
                break;
        }
    }
    
    private void CreateOrbitPaths()
    {
        if (!showOrbitPaths)
        {
            return;
        }
        
        foreach (var kvp in planets)
        {
            CelestialBodyType planetType = kvp.Key;
            PlanetController controller = kvp.Value;
            
            GameObject orbitGO = Instantiate(orbitPathPrefab, orbitContainer);
            orbitGO.name = planetType.ToString() + "_Orbit";
            
            LineRenderer lineRenderer = orbitGO.GetComponent<LineRenderer>();
            UpdateOrbitPath(lineRenderer, planetType);
            
            orbitPaths[planetType] = lineRenderer;
        }
    }
    
    private void UpdateOrbitPath(LineRenderer lineRenderer, CelestialBodyType planetType)
    {
        int segments = 180;
        lineRenderer.positionCount = segments;
        
        float semiMajorAxis = GetSemiMajorAxis(planetType);
        float eccentricity = GetEccentricity(planetType);
        
        float scaleFactor = currentMode == SimulationMode.Educational ? 
            orbitScaleMultiplier * AU_TO_UNITY_UNITS : 1f;
        
        for (int i = 0; i < segments; i++)
        {
            float angle = (float)i / segments * Mathf.PI * 2f;
            
            float radius = semiMajorAxis * (1f - eccentricity * eccentricity) / 
                          (1f + eccentricity * Mathf.Cos(angle));
            
            float x = radius * Mathf.Cos(angle) * scaleFactor;
            float z = radius * Mathf.Sin(angle) * scaleFactor;
            
            lineRenderer.SetPosition(i, new Vector3(x, 0, z));
        }
    }
    
    private float GetSemiMajorAxis(CelestialBodyType planetType)
    {
        switch (planetType)
        {
            case CelestialBodyType.Mercury: return 0.3871f;
            case CelestialBodyType.Venus: return 0.7233f;
            case CelestialBodyType.Earth: return 1.0f;
            case CelestialBodyType.Mars: return 1.5237f;
            case CelestialBodyType.Jupiter: return 5.2028f;
            case CelestialBodyType.Saturn: return 9.5388f;
            case CelestialBodyType.Uranus: return 19.1914f;
            case CelestialBodyType.Neptune: return 30.0611f;
            default: return 1.0f;
        }
    }
    
    private float GetEccentricity(CelestialBodyType planetType)
    {
        switch (planetType)
        {
            case CelestialBodyType.Mercury: return 0.2056f;
            case CelestialBodyType.Venus: return 0.0068f;
            case CelestialBodyType.Earth: return 0.0167f;
            case CelestialBodyType.Mars: return 0.0934f;
            case CelestialBodyType.Jupiter: return 0.0484f;
            case CelestialBodyType.Saturn: return 0.0542f;
            case CelestialBodyType.Uranus: return 0.0472f;
            case CelestialBodyType.Neptune: return 0.0086f;
            default: return 0.0f;
        }
    }
    
    private void Update()
    {
        if (pauseSimulation)
        {
            return;
        }
        
        accumulatedDeltaTime += Time.deltaTime * timeScale;
        
        if (accumulatedDeltaTime >= 1.0f / 60.0f)
        {
            float deltaDays = accumulatedDeltaTime / SECONDS_PER_DAY;
            UpdateSimulationTime(deltaDays);
            UpdatePlanetPositions();
            
            accumulatedDeltaTime = 0f;
        }
        
        UpdatePlanetRotations();
    }
    
    private void UpdateSimulationTime(float deltaDays)
    {
        simulationDateTime = simulationDateTime.AddDays(deltaDays);
        simulationJulianDate += deltaDays;
        
        CelestialDataManager.Instance.CurrentSimulationTime = simulationDateTime;
    }
    
    private void UpdatePlanetPositions()
    {
        foreach (var kvp in planets)
        {
            CelestialBodyType planetType = kvp.Key;
            PlanetController controller = kvp.Value;
            
            SetPlanetPosition(controller, planetType);
            
            if (orbitPaths.ContainsKey(planetType))
            {
                UpdateOrbitPosition(orbitPaths[planetType], planetType);
            }
        }
    }
    
    private void UpdateOrbitPosition(LineRenderer lineRenderer, CelestialBodyType planetType)
    {
        if (currentMode == SimulationMode.Spectacular)
        {
            lineRenderer.enabled = false;
            return;
        }
        
        lineRenderer.enabled = true;
        
        Vector3 planetPosition = planets[planetType].transform.position;
        lineRenderer.transform.position = sunTransform.position;
        
        for (int i = 0; i < lineRenderer.positionCount; i++)
        {
            Vector3 point = lineRenderer.GetPosition(i);
            lineRenderer.SetPosition(i, new Vector3(point.x, planetPosition.y, point.z));
        }
    }
    
    private void UpdatePlanetRotations()
    {
        foreach (var kvp in planets)
        {
            CelestialBodyType planetType = kvp.Key;
            PlanetController controller = kvp.Value;
            
            Quaternion rotation = CelestialDataManager.Instance.GetPlanetRotation(planetType);
            controller.transform.rotation = rotation;
        }
    }
    
    public void ChangeSimulationMode(SimulationMode newMode)
    {
        if (currentMode == newMode)
        {
            return;
        }
        
        currentMode = newMode;
        
        foreach (var kvp in planets)
        {
            SetPlanetScale(kvp.Value, kvp.Key);
            SetPlanetPosition(kvp.Value, kvp.Key);
            
            if (orbitPaths.ContainsKey(kvp.Key))
            {
                UpdateOrbitPath(orbitPaths[kvp.Key], kvp.Key);
            }
        }
    }
    
    public void SetTimeScale(float scale)
    {
        timeScale = Mathf.Clamp(scale, 0f, 1000000f);
    }
    
    public void JumpToDate(DateTime date)
    {
        simulationDateTime = date;
        simulationJulianDate = TimeUtils.ConvertToJulianDate(date);
        
        CelestialDataManager.Instance.CurrentSimulationTime = simulationDateTime;
        UpdatePlanetPositions();
    }
}

public class PlanetController : MonoBehaviour
{
    public CelestialBodyType planetType;
    public float rotationSpeedMultiplier = 1.0f;
    public bool showAtmosphere = true;
    
    private MeshRenderer planetRenderer;
    private GameObject atmosphereEffect;
    private Material planetMaterial;
    
    public void Initialize(CelestialBodyType type, SolarSystemSimulator.SimulationMode mode)
    {
        planetType = type;
        
        planetRenderer = GetComponent<MeshRenderer>();
        if (planetRenderer != null)
        {
            planetMaterial = new Material(planetRenderer.material);
            planetRenderer.material = planetMaterial;
            
            SetPlanetTexture();
            SetPlanetMaterialProperties(mode);
        }
        
        CreateAtmosphereEffect();
    }
    
    private void SetPlanetTexture()
    {
        string texturePath = $"Textures/Planets/{planetType.ToString().ToLower()}_surface";
        
        AssetLoaderManager.Instance.LoadAssetWithCallback<Texture2D>(
            texturePath,
            (texture) =>
            {
                if (texture != null && planetMaterial != null)
                {
                    planetMaterial.mainTexture = texture;
                }
            }
        );
    }
    
    private void SetPlanetMaterialProperties(SolarSystemSimulator.SimulationMode mode)
    {
        if (planetMaterial == null)
        {
            return;
        }
        
        switch (mode)
        {
            case SolarSystemSimulator.SimulationMode.Educational:
                planetMaterial.SetFloat("_Metallic", 0.1f);
                planetMaterial.SetFloat("_Smoothness", 0.3f);
                planetMaterial.SetFloat("_EmissionStrength", 0.0f);
                break;
                
            case SolarSystemSimulator.SimulationMode.Spectacular:
                planetMaterial.SetFloat("_Metallic", 0.3f);
                planetMaterial.SetFloat("_Smoothness", 0.6f);
                planetMaterial.SetFloat("_EmissionStrength", 0.1f);
                break;
                
            case SolarSystemSimulator.SimulationMode.RealTime:
                planetMaterial.SetFloat("_Metallic", 0.05f);
                planetMaterial.SetFloat("_Smoothness", 0.2f);
                planetMaterial.SetFloat("_EmissionStrength", 0.0f);
                break;
        }
    }
    
    private void CreateAtmosphereEffect()
    {
        if (!showAtmosphere)
        {
            return;
        }
        
        if (planetType == CelestialBodyType.Earth || 
            planetType == CelestialBodyType.Venus ||
            planetType == CelestialBodyType.Titan)
        {
            atmosphereEffect = new GameObject("Atmosphere");
            atmosphereEffect.transform.SetParent(transform);
            atmosphereEffect.transform.localPosition = Vector3.zero;
            atmosphereEffect.transform.localScale = Vector3.one * 1.05f;
            
            MeshRenderer atmosphereRenderer = atmosphereEffect.AddComponent<MeshRenderer>();
            MeshFilter atmosphereFilter = atmosphereEffect.AddComponent<MeshFilter>();
            
            atmosphereFilter.mesh = GetComponent<MeshFilter>().mesh;
            
            Material atmosphereMaterial = new Material(Shader.Find("Custom/Atmosphere"));
            atmosphereMaterial.SetColor("_AtmosphereColor", GetAtmosphereColor());
            atmosphereRenderer.material = atmosphereMaterial;
        }
    }
    
    private Color GetAtmosphereColor()
    {
        switch (planetType)
        {
            case CelestialBodyType.Earth:
                return new Color(0.2f, 0.4f, 0.8f, 0.3f);
            case CelestialBodyType.Venus:
                return new Color(1.0f, 0.8f, 0.4f, 0.5f);
            case CelestialBodyType.Titan:
                return new Color(0.8f, 0.6f, 0.4f, 0.4f);
            default:
                return new Color(0.5f, 0.5f, 0.5f, 0.2f);
        }
    }
    
    private void Update()
    {
        UpdatePlanetRotation();
    }
    
    private void UpdatePlanetRotation()
    {
        float rotationSpeed = GetRotationSpeed();
        transform.Rotate(Vector3.up, rotationSpeed * rotationSpeedMultiplier * Time.deltaTime);
    }
    
    private float GetRotationSpeed()
    {
        switch (planetType)
        {
            case CelestialBodyType.Mercury: return 0.017f;
            case CelestialBodyType.Venus: return -0.0041f;
            case CelestialBodyType.Earth: return 15.0f;
            case CelestialBodyType.Mars: return 14.6f;
            case CelestialBodyType.Jupiter: return 36.0f;
            case CelestialBodyType.Saturn: return 34.0f;
            case CelestialBodyType.Uranus: return -21.0f;
            case CelestialBodyType.Neptune: return 22.0f;
            default: return 10.0f;
        }
    }
}

12.7 AR增强现实模块实现

12.7.1 ARFoundation集成与环境感知

在商业AR应用中,稳定性和用户体验至关重要。我们基于Unity的ARFoundation框架,实现了跨平台的AR太阳系体验。

using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;

public class ARSolarSystemManager : MonoBehaviour
{
    [Header("AR Components")]
    public ARSessionOrigin arSessionOrigin;
    public ARRaycastManager arRaycastManager;
    public ARPlaneManager arPlaneManager;
    public ARCameraManager arCameraManager;
    
    [Header("Solar System Prefabs")]
    public GameObject arSolarSystemPrefab;
    public GameObject placementIndicator;
    
    [Header("AR Settings")]
    public float minPlaneArea = 0.25f;
    public float systemScale = 0.5f;
    public bool autoFocusEnabled = true;
    
    private GameObject currentSolarSystem;
    private Pose placementPose;
    private bool placementPoseIsValid = false;
    private ARPlane selectedPlane;
    private Camera arCamera;
    
    private List<ARRaycastHit> hits = new List<ARRaycastHit>();
    
    private void Start()
    {
        arCamera = arSessionOrigin.camera;
        
        if (placementIndicator != null)
        {
            placementIndicator.SetActive(false);
        }
        
        InitializeARComponents();
        SetupARCallbacks();
    }
    
    private void InitializeARComponents()
    {
        if (arSessionOrigin == null)
        {
            arSessionOrigin = FindObjectOfType<ARSessionOrigin>();
        }
        
        if (arRaycastManager == null)
        {
            arRaycastManager = FindObjectOfType<ARRaycastManager>();
        }
        
        if (arPlaneManager == null)
        {
            arPlaneManager = FindObjectOfType<ARPlaneManager>();
        }
        
        if (arCameraManager == null)
        {
            arCameraManager = FindObjectOfType<ARCameraManager>();
        }
    }
    
    private void SetupARCallbacks()
    {
        if (arCameraManager != null)
        {
            arCameraManager.frameReceived += OnCameraFrameReceived;
        }
        
        if (arPlaneManager != null)
        {
            arPlaneManager.planesChanged += OnPlanesChanged;
        }
    }
    
    private void Update()
    {
        UpdatePlacementPose();
        UpdatePlacementIndicator();
        
        if (placementPoseIsValid && Input.touchCount > 0 && Input.GetTouch(0).phase == TouchPhase.Began)
        {
            PlaceSolarSystem();
        }
        
        if (currentSolarSystem != null)
        {
            UpdateSolarSystemInteraction();
        }
    }
    
    private void UpdatePlacementPose()
    {
        if (arRaycastManager == null || arCamera == null)
        {
            return;
        }
        
        Vector2 screenCenter = arCamera.ViewportToScreenPoint(new Vector2(0.5f, 0.5f));
        
        if (arRaycastManager.Raycast(screenCenter, hits, TrackableType.PlaneWithinPolygon))
        {
            placementPoseIsValid = true;
            placementPose = hits[0].pose;
            
            selectedPlane = arPlaneManager.GetPlane(hits[0].trackableId);
            
            if (selectedPlane != null && selectedPlane.extents.x * selectedPlane.extents.y < minPlaneArea)
            {
                placementPoseIsValid = false;
            }
        }
        else
        {
            placementPoseIsValid = false;
        }
    }
    
    private void UpdatePlacementIndicator()
    {
        if (placementIndicator == null)
        {
            return;
        }
        
        placementIndicator.SetActive(placementPoseIsValid);
        
        if (placementPoseIsValid)
        {
            placementIndicator.transform.SetPositionAndRotation(
                placementPose.position,
                placementPose.rotation
            );
        }
    }
    
    private void PlaceSolarSystem()
    {
        if (!placementPoseIsValid || arSolarSystemPrefab == null)
        {
            return;
        }
        
        if (currentSolarSystem != null)
        {
            Destroy(currentSolarSystem);
        }
        
        currentSolarSystem = Instantiate(
            arSolarSystemPrefab,
            placementPose.position,
            placementPose.rotation
        );
        
        currentSolarSystem.transform.localScale = Vector3.one * systemScale;
        
        ARSolarSystemController systemController = currentSolarSystem.GetComponent<ARSolarSystemController>();
        if (systemController != null)
        {
            systemController.Initialize(selectedPlane);
        }
        
        if (placementIndicator != null)
        {
            placementIndicator.SetActive(false);
        }
        
        DisablePlaneVisualization();
    }
    
    private void DisablePlaneVisualization()
    {
        if (arPlaneManager == null)
        {
            return;
        }
        
        foreach (var plane in arPlaneManager.trackables)
        {
            plane.gameObject.SetActive(false);
        }
        
        arPlaneManager.enabled = false;
    }
    
    private void UpdateSolarSystemInteraction()
    {
        if (currentSolarSystem == null || Input.touchCount != 2)
        {
            return;
        }
        
        Touch touchZero = Input.GetTouch(0);
        Touch touchOne = Input.GetTouch(1);
        
        if (touchZero.phase == TouchPhase.Moved && touchOne.phase == TouchPhase.Moved)
        {
            HandlePinchZoom(touchZero, touchOne);
            HandleRotation(touchZero, touchOne);
        }
    }
    
    private void HandlePinchZoom(Touch touchZero, Touch touchOne)
    {
        Vector2 touchZeroPrevPos = touchZero.position - touchZero.deltaPosition;
        Vector2 touchOnePrevPos = touchOne.position - touchOne.deltaPosition;
        
        float prevTouchDeltaMag = (touchZeroPrevPos - touchOnePrevPos).magnitude;
        float touchDeltaMag = (touchZero.position - touchOne.position).magnitude;
        
        float deltaMagnitudeDiff = prevTouchDeltaMag - touchDeltaMag;
        
        float scaleFactor = 1f - deltaMagnitudeDiff * 0.01f;
        scaleFactor = Mathf.Clamp(scaleFactor, 0.1f, 3f);
        
        currentSolarSystem.transform.localScale *= scaleFactor;
        systemScale = currentSolarSystem.transform.localScale.x;
    }
    
    private void HandleRotation(Touch touchZero, Touch touchOne)
    {
        Vector2 touchZeroPrevPos = touchZero.position - touchZero.deltaPosition;
        Vector2 touchOnePrevPos = touchOne.position - touchOne.deltaPosition;
        
        float angle = Vector2.SignedAngle(
            touchOnePrevPos - touchZeroPrevPos,
            touchOne.position - touchZero.position
        );
        
        currentSolarSystem.transform.Rotate(Vector3.up, angle, Space.World);
    }
    
    private void OnCameraFrameReceived(ARCameraFrameEventArgs args)
    {
        if (!autoFocusEnabled)
        {
            return;
        }
        
        if (args.lightEstimation.averageBrightness.HasValue)
        {
            UpdateCameraExposure(args.lightEstimation.averageBrightness.Value);
        }
    }
    
    private void UpdateCameraExposure(float brightness)
    {
        if (arCamera == null)
        {
            return;
        }
        
        float targetExposure = Mathf.Lerp(0.1f, 1.0f, brightness);
        
        if (arCamera.usePhysicalProperties)
        {
            arCamera.aperture = Mathf.Lerp(1.8f, 16f, 1f - brightness);
            arCamera.shutterSpeed = Mathf.Lerp(1f / 1000f, 1f / 30f, brightness);
            arCamera.iso = Mathf.Lerp(100f, 1600f, 1f - brightness);
        }
    }
    
    private void OnPlanesChanged(ARPlanesChangedEventArgs args)
    {
        foreach (var addedPlane in args.added)
        {
            ConfigurePlaneVisualization(addedPlane);
        }
    }
    
    private void ConfigurePlaneVisualization(ARPlane plane)
    {
        MeshRenderer planeRenderer = plane.GetComponent<MeshRenderer>();
        if (planeRenderer != null)
        {
            Material planeMaterial = planeRenderer.material;
            
            Color planeColor = planeMaterial.color;
            planeColor.a = 0.3f;
            planeMaterial.color = planeColor;
            
            planeMaterial.SetFloat("_Metallic", 0f);
            planeMaterial.SetFloat("_Glossiness", 0.1f);
        }
    }
    
    private void OnDestroy()
    {
        if (arCameraManager != null)
        {
            arCameraManager.frameReceived -= OnCameraFrameReceived;
        }
        
        if (arPlaneManager != null)
        {
            arPlaneManager.planesChanged -= OnPlanesChanged;
        }
    }
    
    public void ResetARSession()
    {
        if (currentSolarSystem != null)
        {
            Destroy(currentSolarSystem);
            currentSolarSystem = null;
        }
        
        if (arPlaneManager != null)
        {
            arPlaneManager.enabled = true;
            
            foreach (var plane in arPlaneManager.trackables)
            {
                plane.gameObject.SetActive(true);
            }
        }
        
        if (placementIndicator != null)
        {
            placementIndicator.SetActive(false);
        }
        
        placementPoseIsValid = false;
    }
    
    public void SetSystemScale(float scale)
    {
        systemScale = Mathf.Clamp(scale, 0.1f, 2f);
        
        if (currentSolarSystem != null)
        {
            currentSolarSystem.transform.localScale = Vector3.one * systemScale;
        }
    }
}

12.8 VR虚拟现实沉浸体验

12.8.1 XR交互系统集成

商业VR应用需要提供深度沉浸体验和自然交互。我们使用Unity的XR Interaction Toolkit,实现跨VR平台的统一交互方案。

using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR;
using UnityEngine.XR.Interaction.Toolkit;

public class VRSolarSystemController : MonoBehaviour
{
    [Header("XR References")]
    public XRRig xrRig;
    public XRController leftHandController;
    public XRController rightHandController;
    public XRRayInteractor leftHandRayInteractor;
    public XRRayInteractor rightHandRayInteractor;
    
    [Header("VR Solar System")]
    public Transform vrSolarSystemRoot;
    public Transform vrCameraOffset;
    public float movementSpeed = 2.0f;
    public float rotationSpeed = 45.0f;
    public float snapTurnAngle = 45.0f;
    
    [Header("VR Settings")]
    public VRRenderScale renderScale = VRRenderScale.High;
    public bool enableTeleport = true;
    public bool enableContinuousMovement = true;
    public bool enableSnapTurn = true;
    
    private InputDevice leftHandDevice;
    private InputDevice rightHandDevice;
    private Vector2 leftStickInput;
    private Vector2 rightStickInput;
    private bool isTeleporting = false;
    private float currentTurnCooldown = 0f;
    
    private const float TURN_COOLDOWN = 0.2f;
    
    public enum VRRenderScale
    {
        Low = 75,
        Medium = 100,
        High = 150,
        Ultra = 200
    }
    
    private void Start()
    {
        InitializeXRDevices();
        ConfigureVRQuality();
        SetupVRInput();
    }
    
    private void InitializeXRDevices()
    {
        List<InputDevice> leftHandDevices = new List<InputDevice>();
        InputDevices.GetDevicesAtXRNode(XRNode.LeftHand, leftHandDevices);
        
        if (leftHandDevices.Count > 0)
        {
            leftHandDevice = leftHandDevices[0];
        }
        
        List<InputDevice> rightHandDevices = new List<InputDevice>();
        InputDevices.GetDevicesAtXRNode(XRNode.RightHand, rightHandDevices);
        
        if (rightHandDevices.Count > 0)
        {
            rightHandDevice = rightHandDevices[0];
        }
        
        if (xrRig == null)
        {
            xrRig = FindObjectOfType<XRRig>();
        }
        
        if (vrCameraOffset == null && xrRig != null)
        {
            vrCameraOffset = xrRig.cameraFloorOffsetObject.transform;
        }
    }
    
    private void ConfigureVRQuality()
    {
        if (XRSettings.enabled)
        {
            XRSettings.renderScale = (int)renderScale / 100f;
            
            Application.targetFrameRate = 90;
            QualitySettings.vSyncCount = 0;
            QualitySettings.maxQueuedFrames = 0;
            
            ConfigureVRShadows();
            ConfigureVRPostProcessing();
        }
    }
    
    private void ConfigureVRShadows()
    {
        QualitySettings.shadowResolution = ShadowResolution.High;
        QualitySettings.shadowDistance = 50f;
        QualitySettings.shadowCascades = 2;
        QualitySettings.shadowmaskMode = ShadowmaskMode.Shadowmask;
    }
    
    private void ConfigureVRPostProcessing()
    {
    }
    
    private void SetupVRInput()
    {
        if (leftHandRayInteractor != null)
        {
            leftHandRayInteractor.enabled = enableTeleport;
        }
        
        if (rightHandRayInteractor != null)
        {
            rightHandRayInteractor.enableUIInteraction = true;
            rightHandRayInteractor.hoverToSelect = false;
        }
    }
    
    private void Update()
    {
        UpdateInputDevices();
        
        if (enableContinuousMovement)
        {
            HandleContinuousMovement();
        }
        
        if (enableSnapTurn)
        {
            HandleSnapTurn();
        }
        
        if (enableTeleport)
        {
            HandleTeleport();
        }
        
        HandlePlanetSelection();
        HandleSystemManipulation();
        
        UpdateTurnCooldown();
    }
    
    private void UpdateInputDevices()
    {
        if (!leftHandDevice.isValid || !rightHandDevice.isValid)
        {
            InitializeXRDevices();
            return;
        }
        
        leftHandDevice.TryGetFeatureValue(CommonUsages.primary2DAxis, out leftStickInput);
        rightHandDevice.TryGetFeatureValue(CommonUsages.primary2DAxis, out rightStickInput);
    }
    
    private void HandleContinuousMovement()
    {
        if (leftStickInput.magnitude > 0.1f)
        {
            Vector3 movementDirection = new Vector3(leftStickInput.x, 0, leftStickInput.y);
            
            Transform cameraTransform = xrRig.cameraGameObject.transform;
            Vector3 cameraForward = Vector3.ProjectOnPlane(cameraTransform.forward, Vector3.up).normalized;
            Vector3 cameraRight = Vector3.ProjectOnPlane(cameraTransform.right, Vector3.up).normalized;
            
            Vector3 worldMovement = (cameraForward * movementDirection.z + cameraRight * movementDirection.x).normalized;
            
            vrCameraOffset.Translate(worldMovement * movementSpeed * Time.deltaTime, Space.World);
        }
    }
    
    private void HandleSnapTurn()
    {
        if (currentTurnCooldown > 0)
        {
            return;
        }
        
        if (Mathf.Abs(rightStickInput.x) > 0.7f)
        {
            float turnAngle = snapTurnAngle * Mathf.Sign(rightStickInput.x);
            vrCameraOffset.Rotate(Vector3.up, turnAngle);
            
            currentTurnCooldown = TURN_COOLDOWN;
        }
    }
    
    private void HandleTeleport()
    {
        if (isTeleporting)
        {
            return;
        }
        
        if (leftHandController != null)
        {
            leftHandController.inputDevice.TryGetFeatureValue(CommonUsages.triggerButton, out bool triggerPressed);
            
            if (triggerPressed && leftHandRayInteractor != null && leftHandRayInteractor.TryGetCurrent3DRaycastHit(out RaycastHit hit))
            {
                if (hit.collider.CompareTag("TeleportArea"))
                {
                    TeleportToPosition(hit.point);
                }
            }
        }
    }
    
    private void TeleportToPosition(Vector3 position)
    {
        isTeleporting = true;
        
        Vector3 cameraPosition = xrRig.cameraGameObject.transform.position;
        Vector3 headOffset = cameraPosition - vrCameraOffset.position;
        headOffset.y = 0;
        
        vrCameraOffset.position = position - headOffset;
        
        Invoke("ResetTeleport", 0.5f);
    }
    
    private void ResetTeleport()
    {
        isTeleporting = false;
    }
    
    private void HandlePlanetSelection()
    {
        if (rightHandRayInteractor == null)
        {
            return;
        }
        
        if (rightHandRayInteractor.TryGetCurrent3DRaycastHit(out RaycastHit hit))
        {
            if (hit.collider.CompareTag("Planet"))
            {
                VRPlanetController planetController = hit.collider.GetComponent<VRPlanetController>();
                
                if (planetController != null && !planetController.IsSelected)
                {
                    planetController.Highlight(true);
                    
                    rightHandController.inputDevice.TryGetFeatureValue(CommonUsages.triggerButton, out bool triggerPressed);
                    
                    if (triggerPressed)
                    {
                        SelectPlanet(planetController);
                    }
                }
            }
        }
    }
    
    private void SelectPlanet(VRPlanetController planetController)
    {
        if (planetController == null)
        {
            return;
        }
        
        DeselectAllPlanets();
        
        planetController.Select();
        ShowPlanetInformation(planetController);
        
        if (rightHandController != null)
        {
            rightHandController.SendHapticImpulse(0.5f, 0.1f);
        }
    }
    
    private void DeselectAllPlanets()
    {
        VRPlanetController[] allPlanets = FindObjectsOfType<VRPlanetController>();
        
        foreach (VRPlanetController planet in allPlanets)
        {
            if (planet.IsSelected)
            {
                planet.Deselect();
            }
        }
    }
    
    private void ShowPlanetInformation(VRPlanetController planetController)
    {
    }
    
    private void HandleSystemManipulation()
    {
        if (rightHandController == null || leftHandController == null)
        {
            return;
        }
        
        rightHandController.inputDevice.TryGetFeatureValue(CommonUsages.gripButton, out bool rightGripPressed);
        leftHandController.inputDevice.TryGetFeatureValue(CommonUsages.gripButton, out bool leftGripPressed);
        
        if (rightGripPressed && leftGripPressed)
        {
            HandleTwoHandedManipulation();
        }
        else if (rightGripPressed)
        {
            HandleOneHandedManipulation(rightHandController);
        }
    }
    
    private void HandleTwoHandedManipulation()
    {
    }
    
    private void HandleOneHandedManipulation(XRController controller)
    {
    }
    
    private void UpdateTurnCooldown()
    {
        if (currentTurnCooldown > 0)
        {
            currentTurnCooldown -= Time.deltaTime;
        }
    }
    
    public void ChangeRenderScale(VRRenderScale newScale)
    {
        renderScale = newScale;
        
        if (XRSettings.enabled)
        {
            XRSettings.renderScale = (int)newScale / 100f;
        }
    }
    
    public void ToggleMovement(bool enabled)
    {
        enableContinuousMovement = enabled;
    }
    
    public void ToggleTeleport(bool enabled)
    {
        enableTeleport = enabled;
        
        if (leftHandRayInteractor != null)
        {
            leftHandRayInteractor.enabled = enabled;
        }
    }
    
    public void ToggleSnapTurn(bool enabled)
    {
        enableSnapTurn = enabled;
    }
    
    private void OnDestroy()
    {
    }
}

public class VRPlanetController : MonoBehaviour
{
    public CelestialBodyType planetType;
    public GameObject highlightEffect;
    public GameObject selectionEffect;
    public AudioClip selectionSound;
    
    private Material planetMaterial;
    private Color originalColor;
    private bool isSelected = false;
    private bool isHighlighted = false;
    
    public bool IsSelected
    {
        get { return isSelected; }
    }
    
    private void Start()
    {
        MeshRenderer renderer = GetComponent<MeshRenderer>();
        if (renderer != null)
        {
            planetMaterial = renderer.material;
            originalColor = planetMaterial.color;
        }
        
        if (highlightEffect != null)
        {
            highlightEffect.SetActive(false);
        }
        
        if (selectionEffect != null)
        {
            selectionEffect.SetActive(false);
        }
    }
    
    public void Highlight(bool highlight)
    {
        if (isHighlighted == highlight)
        {
            return;
        }
        
        isHighlighted = highlight;
        
        if (planetMaterial != null)
        {
            if (highlight)
            {
                planetMaterial.color = Color.Lerp(originalColor, Color.yellow, 0.3f);
                
                if (highlightEffect != null)
                {
                    highlightEffect.SetActive(true);
                }
            }
            else
            {
                planetMaterial.color = originalColor;
                
                if (highlightEffect != null)
                {
                    highlightEffect.SetActive(false);
                }
            }
        }
    }
    
    public void Select()
    {
        if (isSelected)
        {
            return;
        }
        
        isSelected = true;
        
        if (selectionEffect != null)
        {
            selectionEffect.SetActive(true);
        }
        
        if (selectionSound != null)
        {
            AudioSource.PlayClipAtPoint(selectionSound, transform.position);
        }
        
        ShowPlanetDetails();
    }
    
    public void Deselect()
    {
        if (!isSelected)
        {
            return;
        }
        
        isSelected = false;
        
        if (selectionEffect != null)
        {
            selectionEffect.SetActive(false);
        }
        
        HidePlanetDetails();
    }
    
    private void ShowPlanetDetails()
    {
    }
    
    private void HidePlanetDetails()
    {
    }
    
    private void OnTriggerEnter(Collider other)
    {
        if (other.CompareTag("VRController"))
        {
            Highlight(true);
        }
    }
    
    private void OnTriggerExit(Collider other)
    {
        if (other.CompareTag("VRController") && !isSelected)
        {
            Highlight(false);
        }
    }
}

12.9 外部设备集成与用户设置

12.9.1 蓝牙控制器集成方案

商业应用需要支持多种输入设备。我们实现了通用的蓝牙控制器接口,支持常见的游戏手柄和定制天文控制器。

using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Controls;
using UnityEngine.InputSystem.LowLevel;

public class BluetoothControllerManager : MonoBehaviour
{
    [Header("Controller Settings")]
    public float joystickDeadzone = 0.2f;
    public float joystickSensitivity = 1.0f;
    public bool invertYAxis = false;
    public ControllerType controllerType = ControllerType.AutoDetect;
    
    [Header("Button Mapping")]
    public ControllerButton selectButton = ControllerButton.A;
    public ControllerButton backButton = ControllerButton.B;
    public ControllerButton menuButton = ControllerButton.Menu;
    public ControllerButton screenshotButton = ControllerButton.X;
    
    private Gamepad currentGamepad;
    private Vector2 leftStickValue;
    private Vector2 rightStickValue;
    private float leftTriggerValue;
    private float rightTriggerValue;
    private Dictionary<ControllerButton, bool> buttonStates;
    private Dictionary<ControllerButton, float> buttonPressTimes;
    
    private float connectionCheckInterval = 5f;
    private float lastConnectionCheckTime = 0f;
    
    public enum ControllerType
    {
        AutoDetect,
        Xbox,
        PlayStation,
        Switch,
        Custom
    }
    
    public enum ControllerButton
    {
        A,
        B,
        X,
        Y,
        LeftShoulder,
        RightShoulder,
        LeftTrigger,
        RightTrigger,
        LeftStick,
        RightStick,
        DPadUp,
        DPadDown,
        DPadLeft,
        DPadRight,
        Menu,
        View
    }
    
    private void Awake()
    {
        buttonStates = new Dictionary<ControllerButton, bool>();
        buttonPressTimes = new Dictionary<ControllerButton, float>();
        
        InitializeButtonStates();
        SetupInputSystem();
    }
    
    private void InitializeButtonStates()
    {
        foreach (ControllerButton button in Enum.GetValues(typeof(ControllerButton)))
        {
            buttonStates[button] = false;
            buttonPressTimes[button] = 0f;
        }
    }
    
    private void SetupInputSystem()
    {
        InputSystem.onDeviceChange += OnDeviceChange;
        
        if (Gamepad.current != null)
        {
            currentGamepad = Gamepad.current;
            Debug.Log($"Controller connected: {currentGamepad.displayName}");
        }
    }
    
    private void Update()
    {
        CheckControllerConnection();
        
        if (currentGamepad == null)
        {
            return;
        }
        
        UpdateStickValues();
        UpdateTriggerValues();
        UpdateButtonStates();
        
        HandleControllerInput();
    }
    
    private void CheckControllerConnection()
    {
        if (Time.time - lastConnectionCheckTime > connectionCheckInterval)
        {
            lastConnectionCheckTime = Time.time;
            
            if (currentGamepad == null || !currentGamepad.enabled)
            {
                FindAvailableController();
            }
        }
    }
    
    private void FindAvailableController()
    {
        foreach (var gamepad in Gamepad.all)
        {
            if (gamepad.enabled)
            {
                currentGamepad = gamepad;
                Debug.Log($"Controller reconnected: {currentGamepad.displayName}");
                break;
            }
        }
    }
    
    private void UpdateStickValues()
    {
        leftStickValue = ApplyDeadzone(currentGamepad.leftStick.ReadValue());
        rightStickValue = ApplyDeadzone(currentGamepad.rightStick.ReadValue());
        
        if (invertYAxis)
        {
            leftStickValue.y = -leftStickValue.y;
            rightStickValue.y = -rightStickValue.y;
        }
        
        leftStickValue *= joystickSensitivity;
        rightStickValue *= joystickSensitivity;
    }
    
    private Vector2 ApplyDeadzone(Vector2 input)
    {
        if (input.magnitude < joystickDeadzone)
        {
            return Vector2.zero;
        }
        
        return input.normalized * ((input.magnitude - joystickDeadzone) / (1f - joystickDeadzone));
    }
    
    private void UpdateTriggerValues()
    {
        leftTriggerValue = currentGamepad.leftTrigger.ReadValue();
        rightTriggerValue = currentGamepad.rightTrigger.ReadValue();
    }
    
    private void UpdateButtonStates()
    {
        UpdateButtonState(ControllerButton.A, currentGamepad.aButton);
        UpdateButtonState(ControllerButton.B, currentGamepad.bButton);
        UpdateButtonState(ControllerButton.X, currentGamepad.xButton);
        UpdateButtonState(ControllerButton.Y, currentGamepad.yButton);
        UpdateButtonState(ControllerButton.LeftShoulder, currentGamepad.leftShoulder);
        UpdateButtonState(ControllerButton.RightShoulder, currentGamepad.rightShoulder);
        UpdateButtonState(ControllerButton.LeftStick, currentGamepad.leftStickButton);
        UpdateButtonState(ControllerButton.RightStick, currentGamepad.rightStickButton);
        UpdateButtonState(ControllerButton.Menu, currentGamepad.startButton);
        UpdateButtonState(ControllerButton.View, currentGamepad.selectButton);
        
        UpdateDPadStates();
    }
    
    private void UpdateButtonState(ControllerButton button, ButtonControl buttonControl)
    {
        bool wasPressed = buttonStates[button];
        bool isPressed = buttonControl.isPressed;
        
        buttonStates[button] = isPressed;
        
        if (isPressed && !wasPressed)
        {
            buttonPressTimes[button] = Time.time;
            OnButtonDown(button);
        }
        else if (!isPressed && wasPressed)
        {
            OnButtonUp(button);
        }
    }
    
    private void UpdateDPadStates()
    {
        Vector2 dpadValue = currentGamepad.dpad.ReadValue();
        
        buttonStates[ControllerButton.DPadUp] = dpadValue.y > 0.5f;
        buttonStates[ControllerButton.DPadDown] = dpadValue.y < -0.5f;
        buttonStates[ControllerButton.DPadLeft] = dpadValue.x < -0.5f;
        buttonStates[ControllerButton.DPadRight] = dpadValue.x > 0.5f;
    }
    
    private void HandleControllerInput()
    {
        HandleNavigationInput();
        HandleActionInput();
        HandleSystemInput();
    }
    
    private void HandleNavigationInput()
    {
        if (leftStickValue.magnitude > 0.1f)
        {
            OnLeftStickChanged(leftStickValue);
        }
        
        if (rightStickValue.magnitude > 0.1f)
        {
            OnRightStickChanged(rightStickValue);
        }
        
        if (leftTriggerValue > 0.1f)
        {
            OnLeftTriggerChanged(leftTriggerValue);
        }
        
        if (rightTriggerValue > 0.1f)
        {
            OnRightTriggerChanged(rightTriggerValue);
        }
    }
    
    private void HandleActionInput()
    {
        if (IsButtonDown(selectButton))
        {
            OnSelectPressed();
        }
        
        if (IsButtonDown(backButton))
        {
            OnBackPressed();
        }
        
        if (IsButtonDown(screenshotButton))
        {
            OnScreenshotPressed();
        }
    }
    
    private void HandleSystemInput()
    {
        if (IsButtonDown(ControllerButton.Menu))
        {
            OnMenuPressed();
        }
    }
    
    private void OnDeviceChange(InputDevice device, InputDeviceChange change)
    {
        switch (change)
        {
            case InputDeviceChange.Added:
                if (device is Gamepad)
                {
                    currentGamepad = device as Gamepad;
                    Debug.Log($"Controller added: {currentGamepad.displayName}");
                }
                break;
                
            case InputDeviceChange.Removed:
                if (device == currentGamepad)
                {
                    currentGamepad = null;
                    Debug.Log("Controller removed");
                }
                break;
                
            case InputDeviceChange.Disconnected:
                if (device == currentGamepad)
                {
                    Debug.Log("Controller disconnected");
                }
                break;
                
            case InputDeviceChange.Reconnected:
                if (device == currentGamepad)
                {
                    Debug.Log("Controller reconnected");
                }
                break;
        }
    }
    
    public bool IsButtonDown(ControllerButton button)
    {
        return buttonStates.ContainsKey(button) && buttonStates[button];
    }
    
    public bool IsButtonPressed(ControllerButton button, float pressDuration = 0.2f)
    {
        if (!buttonStates.ContainsKey(button) || !buttonStates[button])
        {
            return false;
        }
        
        return Time.time - buttonPressTimes[button] >= pressDuration;
    }
    
    public float GetButtonPressDuration(ControllerButton button)
    {
        if (!buttonStates.ContainsKey(button) || !buttonStates[button])
        {
            return 0f;
        }
        
        return Time.time - buttonPressTimes[button];
    }
    
    public Vector2 GetLeftStickValue()
    {
        return leftStickValue;
    }
    
    public Vector2 GetRightStickValue()
    {
        return rightStickValue;
    }
    
    public float GetLeftTriggerValue()
    {
        return leftTriggerValue;
    }
    
    public float GetRightTriggerValue()
    {
        return rightTriggerValue;
    }
    
    public void SetVibration(float leftMotor, float rightMotor, float duration = 0.1f)
    {
        if (currentGamepad == null)
        {
            return;
        }
        
        currentGamepad.SetMotorSpeeds(leftMotor, rightMotor);
        
        if (duration > 0)
        {
            Invoke("StopVibration", duration);
        }
    }
    
    private void StopVibration()
    {
        if (currentGamepad != null)
        {
            currentGamepad.SetMotorSpeeds(0f, 0f);
        }
    }
    
    protected virtual void OnButtonDown(ControllerButton button)
    {
    }
    
    protected virtual void OnButtonUp(ControllerButton button)
    {
    }
    
    protected virtual void OnLeftStickChanged(Vector2 value)
    {
    }
    
    protected virtual void OnRightStickChanged(Vector2 value)
    {
    }
    
    protected virtual void OnLeftTriggerChanged(float value)
    {
    }
    
    protected virtual void OnRightTriggerChanged(float value)
    {
    }
    
    protected virtual void OnSelectPressed()
    {
    }
    
    protected virtual void OnBackPressed()
    {
    }
    
    protected virtual void OnScreenshotPressed()
    {
    }
    
    protected virtual void OnMenuPressed()
    {
    }
    
    private void OnDestroy()
    {
        InputSystem.onDeviceChange -= OnDeviceChange;
        
        if (currentGamepad != null)
        {
            currentGamepad.SetMotorSpeeds(0f, 0f);
        }
    }
}

12.9.2 用户偏好设置系统

商业应用需要完善的设置系统,保存用户偏好并提供个性化体验。

using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;

public class UserSettingsManager : MonoBehaviour
{
    private static UserSettingsManager instance;
    public static UserSettingsManager Instance
    {
        get
        {
            if (instance == null)
            {
                instance = FindObjectOfType<UserSettingsManager>();
                if (instance == null)
                {
                    GameObject go = new GameObject("UserSettingsManager");
                    instance = go.AddComponent<UserSettingsManager>();
                    DontDestroyOnLoad(go);
                }
            }
            return instance;
        }
    }
    
    [System.Serializable]
    public class UserSettings
    {
        public GraphicsSettings graphics = new GraphicsSettings();
        public AudioSettings audio = new AudioSettings();
        public ControlSettings controls = new ControlSettings();
        public AstronomySettings astronomy = new AstronomySettings();
        public ARVRSettings arvr = new ARVRSettings();
        public UISettings ui = new UISettings();
        
        public DateTime lastSaveTime;
        public string userId;
        public int sessionCount;
    }
    
    [System.Serializable]
    public class GraphicsSettings
    {
        public int qualityLevel = 2;
        public int resolutionIndex = 0;
        public bool fullscreen = true;
        public float brightness = 1.0f;
        public float contrast = 1.0f;
        public bool bloom = true;
        public bool vignette = true;
        public bool chromaticAberration = false;
        public bool motionBlur = false;
    }
    
    [System.Serializable]
    public class AudioSettings
    {
        public float masterVolume = 1.0f;
        public float musicVolume = 0.8f;
        public float sfxVolume = 1.0f;
        public float voiceVolume = 1.0f;
        public bool mute = false;
        public int outputDevice = 0;
    }
    
    [System.Serializable]
    public class ControlSettings
    {
        public float lookSensitivity = 1.0f;
        public bool invertLookY = false;
        public float moveSensitivity = 1.0f;
        public ControllerLayout controllerLayout = ControllerLayout.Standard;
        public KeyCode forwardKey = KeyCode.W;
        public KeyCode backwardKey = KeyCode.S;
        public KeyCode leftKey = KeyCode.A;
        public KeyCode rightKey = KeyCode.D;
        public float autoSaveInterval = 300f;
    }
    
    [System.Serializable]
    public class AstronomySettings
    {
        public CoordinateSystem coordinateSystem = CoordinateSystem.Horizontal;
        public TimeDisplayFormat timeFormat = TimeDisplayFormat.Local24Hour;
        public float starMagnitudeLimit = 6.0f;
        public bool showConstellations = true;
        public bool showPlanetOrbits = true;
        public bool showGridLines = false;
        public float timeScale = 1.0f;
        public bool daylightSaving = true;
    }
    
    [System.Serializable]
    public class ARVRSettings
    {
        public bool arEnabled = true;
        public bool vrEnabled = true;
        public float arScale = 1.0f;
        public float vrMovementSpeed = 2.0f;
        public VRRenderQuality vrQuality = VRRenderQuality.Medium;
        public bool vrTeleport = true;
        public bool vrSnapTurn = true;
    }
    
    [System.Serializable]
    public class UISettings
    {
        public UIScale uiScale = UIScale.Normal;
        public FontSize fontSize = FontSize.Medium;
        public ColorScheme colorScheme = ColorScheme.Dark;
        public bool tooltips = true;
        public bool tutorials = true;
        public Language language = Language.English;
    }
    
    public enum ControllerLayout
    {
        Standard,
        Inverted,
        Legacy,
        Custom
    }
    
    public enum CoordinateSystem
    {
        Equatorial,
        Horizontal,
        Ecliptic,
        Galactic
    }
    
    public enum TimeDisplayFormat
    {
        Local24Hour,
        Local12Hour,
        UTCOrUniversal,
        Sidereal
    }
    
    public enum VRRenderQuality
    {
        Low,
        Medium,
        High,
        Ultra
    }
    
    public enum UIScale
    {
        Small,
        Normal,
        Large,
        ExtraLarge
    }
    
    public enum FontSize
    {
        Small,
        Medium,
        Large
    }
    
    public enum ColorScheme
    {
        Light,
        Dark,
        HighContrast,
        Custom
    }
    
    public enum Language
    {
        English,
        Spanish,
        French,
        German,
        Chinese,
        Japanese
    }
    
    private UserSettings currentSettings;
    private string settingsFilePath;
    private float lastAutoSaveTime;
    private bool isDirty = false;
    
    public UserSettings CurrentSettings
    {
        get { return currentSettings; }
    }
    
    public event Action<UserSettings> OnSettingsChanged;
    public event Action<UserSettings> OnSettingsSaved;
    public event Action<UserSettings> OnSettingsLoaded;
    
    private void Awake()
    {
        if (instance != null && instance != this)
        {
            Destroy(gameObject);
            return;
        }
        instance = this;
        DontDestroyOnLoad(gameObject);
        
        settingsFilePath = Path.Combine(Application.persistentDataPath, "UserSettings.json");
        LoadSettings();
        
        lastAutoSaveTime = Time.time;
    }
    
    private void Update()
    {
        if (isDirty && Time.time - lastAutoSaveTime > currentSettings.controls.autoSaveInterval)
        {
            AutoSaveSettings();
        }
    }
    
    private void LoadSettings()
    {
        if (File.Exists(settingsFilePath))
        {
            try
            {
                string json = File.ReadAllText(settingsFilePath);
                currentSettings = JsonUtility.FromJson<UserSettings>(json);
                
                Debug.Log("Settings loaded successfully");
                
                if (currentSettings.userId == null)
                {
                    currentSettings.userId = SystemInfo.deviceUniqueIdentifier;
                }
                
                currentSettings.sessionCount++;
                
                ApplySettings(currentSettings);
                
                OnSettingsLoaded?.Invoke(currentSettings);
            }
            catch (System.Exception e)
            {
                Debug.LogError($"Failed to load settings: {e.Message}");
                CreateDefaultSettings();
            }
        }
        else
        {
            CreateDefaultSettings();
        }
    }
    
    private void CreateDefaultSettings()
    {
        currentSettings = new UserSettings();
        currentSettings.userId = SystemInfo.deviceUniqueIdentifier;
        currentSettings.sessionCount = 1;
        currentSettings.lastSaveTime = DateTime.Now;
        
        ApplySettings(currentSettings);
        SaveSettings();
        
        Debug.Log("Default settings created");
    }
    
    public void SaveSettings()
    {
        try
        {
            currentSettings.lastSaveTime = DateTime.Now;
            string json = JsonUtility.ToJson(currentSettings, true);
            File.WriteAllText(settingsFilePath, json);
            
            isDirty = false;
            lastAutoSaveTime = Time.time;
            
            OnSettingsSaved?.Invoke(currentSettings);
            
            Debug.Log("Settings saved successfully");
        }
        catch (System.Exception e)
        {
            Debug.LogError($"Failed to save settings: {e.Message}");
        }
    }
    
    private void AutoSaveSettings()
    {
        if (isDirty)
        {
            SaveSettings();
        }
    }
    
    public void ApplySettings(UserSettings settings)
    {
        ApplyGraphicsSettings(settings.graphics);
        ApplyAudioSettings(settings.audio);
        ApplyAstronomySettings(settings.astronomy);
        ApplyARVRSettings(settings.arvr);
        ApplyUISettings(settings.ui);
        
        currentSettings = settings;
        isDirty = true;
        
        OnSettingsChanged?.Invoke(settings);
    }
    
    private void ApplyGraphicsSettings(GraphicsSettings graphics)
    {
        QualitySettings.SetQualityLevel(graphics.qualityLevel, true);
        
        Resolution[] resolutions = Screen.resolutions;
        if (graphics.resolutionIndex >= 0 && graphics.resolutionIndex < resolutions.Length)
        {
            Resolution resolution = resolutions[graphics.resolutionIndex];
            Screen.SetResolution(resolution.width, resolution.height, graphics.fullscreen);
        }
        
        Screen.fullScreen = graphics.fullscreen;
        
        RenderSettings.ambientIntensity = graphics.brightness;
        
        SetupPostProcessing(graphics);
    }
    
    private void SetupPostProcessing(GraphicsSettings graphics)
    {
    }
    
    private void ApplyAudioSettings(AudioSettings audio)
    {
        AudioListener.volume = audio.mute ? 0f : audio.masterVolume;
        
        GameObject[] audioManagers = GameObject.FindGameObjectsWithTag("AudioManager");
        foreach (GameObject manager in audioManagers)
        {
            AudioSource[] sources = manager.GetComponents<AudioSource>();
            foreach (AudioSource source in sources)
            {
                if (source.CompareTag("Music"))
                {
                    source.volume = audio.musicVolume;
                }
                else if (source.CompareTag("SFX"))
                {
                    source.volume = audio.sfxVolume;
                }
                else if (source.CompareTag("Voice"))
                {
                    source.volume = audio.voiceVolume;
                }
            }
        }
    }
    
    private void ApplyAstronomySettings(AstronomySettings astronomy)
    {
        CelestialDataManager.Instance.TimeScale = astronomy.timeScale;
        
        StarFieldRenderer starRenderer = FindObjectOfType<StarFieldRenderer>();
        if (starRenderer != null)
        {
            starRenderer.magnitudeCutoff = astronomy.starMagnitudeLimit;
        }
        
        SolarSystemSimulator simulator = FindObjectOfType<SolarSystemSimulator>();
        if (simulator != null)
        {
            simulator.showOrbitPaths = astronomy.showPlanetOrbits;
        }
        
        ConstellationLineRenderer[] constellations = FindObjectsOfType<ConstellationLineRenderer>();
        foreach (ConstellationLineRenderer constellation in constellations)
        {
            constellation.gameObject.SetActive(astronomy.showConstellations);
        }
    }
    
    private void ApplyARVRSettings(ARVRSettings arvr)
    {
    }
    
    private void ApplyUISettings(UISettings ui)
    {
        float scaleFactor = GetUIScaleFactor(ui.uiScale);
        float fontSizeMultiplier = GetFontSizeMultiplier(ui.fontSize);
        
        CanvasScaler[] scalers = FindObjectsOfType<CanvasScaler>();
        foreach (CanvasScaler scaler in scalers)
        {
            scaler.scaleFactor = scaleFactor;
        }
        
        SetupColorScheme(ui.colorScheme);
    }
    
    private float GetUIScaleFactor(UIScale scale)
    {
        switch (scale)
        {
            case UIScale.Small: return 0.8f;
            case UIScale.Normal: return 1.0f;
            case UIScale.Large: return 1.2f;
            case UIScale.ExtraLarge: return 1.5f;
            default: return 1.0f;
        }
    }
    
    private float GetFontSizeMultiplier(FontSize size)
    {
        switch (size)
        {
            case FontSize.Small: return 0.8f;
            case FontSize.Medium: return 1.0f;
            case FontSize.Large: return 1.2f;
            default: return 1.0f;
        }
    }
    
    private void SetupColorScheme(ColorScheme scheme)
    {
    }
    
    public void ResetToDefaults()
    {
        CreateDefaultSettings();
    }
    
    public void ExportSettings(string filePath)
    {
        try
        {
            string json = JsonUtility.ToJson(currentSettings, true);
            File.WriteAllText(filePath, json);
            Debug.Log($"Settings exported to: {filePath}");
        }
        catch (System.Exception e)
        {
            Debug.LogError($"Failed to export settings: {e.Message}");
        }
    }
    
    public void ImportSettings(string filePath)
    {
        try
        {
            if (File.Exists(filePath))
            {
                string json = File.ReadAllText(filePath);
                UserSettings importedSettings = JsonUtility.FromJson<UserSettings>(json);
                ApplySettings(importedSettings);
                Debug.Log("Settings imported successfully");
            }
            else
            {
                Debug.LogError("Settings file not found");
            }
        }
        catch (System.Exception e)
        {
            Debug.LogError($"Failed to import settings: {e.Message}");
        }
    }
    
    private void OnApplicationQuit()
    {
        if (isDirty)
        {
            SaveSettings();
        }
    }
    
    private void OnApplicationPause(bool pauseStatus)
    {
        if (pauseStatus && isDirty)
        {
            AutoSaveSettings();
        }
    }
}

12.10 性能优化与商业部署考量

在商业项目部署前,必须进行全面的性能优化和平台适配。以下是我们总结的关键优化策略和部署注意事项。

12.10.1 多平台性能优化

针对不同平台(PC、移动端、VR设备)需要采用不同的优化策略:

using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Profiling;

public class PerformanceOptimizer : MonoBehaviour
{
    [System.Serializable]
    public class PlatformOptimizationSettings
    {
        public RuntimePlatform platform;
        public int targetFrameRate = 60;
        public float renderScale = 1.0f;
        public int shadowResolution = 2048;
        public int textureQuality = 2;
        public bool enableMSAA = true;
        public bool enableSoftParticles = false;
        public int maxLODLevel = 3;
        public float lodBias = 1.0f;
    }
    
    [Header("Optimization Settings")]
    public List<PlatformOptimizationSettings> platformSettings;
    public bool enableDynamicResolution = true;
    public float performanceCheckInterval = 2.0f;
    public float lowPerformanceThreshold = 45f;
    
    [Header("Memory Management")]
    public int maxTextureMemoryMB = 512;
    public int maxMeshMemoryMB = 256;
    public bool unloadUnusedAssets = true;
    public float assetUnloadInterval = 30f;
    
    private PlatformOptimizationSettings currentSettings;
    private float lastPerformanceCheckTime;
    private float averageFrameTime;
    private int frameCount;
    private float totalFrameTime;
    
    private Dictionary<string, long> textureMemoryUsage;
    private Dictionary<string, long> meshMemoryUsage;
    
    private void Start()
    {
        textureMemoryUsage = new Dictionary<string, long>();
        meshMemoryUsage = new Dictionary<string, long>();
        
        ApplyPlatformOptimization();
        SetupMemoryMonitoring();
        
        lastPerformanceCheckTime = Time.time;
        averageFrameTime = 0f;
        frameCount = 0;
        totalFrameTime = 0f;
    }
    
    private void ApplyPlatformOptimization()
    {
        RuntimePlatform currentPlatform = Application.platform;
        
        currentSettings = platformSettings.Find(s => s.platform == currentPlatform);
        
        if (currentSettings == null)
        {
            currentSettings = new PlatformOptimizationSettings();
            Debug.LogWarning($"No optimization settings found for platform: {currentPlatform}");
        }
        
        Application.targetFrameRate = currentSettings.targetFrameRate;
        
        QualitySettings.shadowResolution = (ShadowResolution)currentSettings.shadowResolution;
        QualitySettings.masterTextureLimit = 3 - currentSettings.textureQuality;
        QualitySettings.antiAliasing = currentSettings.enableMSAA ? 4 : 0;
        QualitySettings.softParticles = currentSettings.enableSoftParticles;
        QualitySettings.maximumLODLevel = currentSettings.maxLODLevel;
        QualitySettings.lodBias = currentSettings.lodBias;
        
        if (XRSettings.enabled)
        {
            XRSettings.renderScale = currentSettings.renderScale;
        }
        
        Debug.Log($"Applied optimization settings for platform: {currentPlatform}");
    }
    
    private void SetupMemoryMonitoring()
    {
        InvokeRepeating("MonitorMemoryUsage", 1f, 5f);
        
        if (unloadUnusedAssets)
        {
            InvokeRepeating("UnloadUnusedAssets", assetUnloadInterval, assetUnloadInterval);
        }
    }
    
    private void Update()
    {
        UpdatePerformanceMetrics();
        
        if (Time.time - lastPerformanceCheckTime >= performanceCheckInterval)
        {
            CheckPerformanceLevel();
            lastPerformanceCheckTime = Time.time;
        }
        
        if (enableDynamicResolution)
        {
            AdjustDynamicResolution();
        }
    }
    
    private void UpdatePerformanceMetrics()
    {
        frameCount++;
        totalFrameTime += Time.deltaTime;
        
        if (frameCount >= currentSettings.targetFrameRate)
        {
            averageFrameTime = totalFrameTime / frameCount;
            frameCount = 0;
            totalFrameTime = 0f;
        }
    }
    
    private void CheckPerformanceLevel()
    {
        float currentFPS = 1f / averageFrameTime;
        
        if (currentFPS < lowPerformanceThreshold)
        {
            ApplyLowPerformanceMode();
        }
        else
        {
            ApplyNormalPerformanceMode();
        }
    }
    
    private void ApplyLowPerformanceMode()
    {
        QualitySettings.shadowResolution = ShadowResolution.Medium;
        QualitySettings.shadowDistance = 20f;
        QualitySettings.pixelLightCount = 1;
        QualitySettings.antiAliasing = 0;
        
        if (XRSettings.enabled)
        {
            XRSettings.renderScale = Mathf.Max(0.7f, XRSettings.renderScale - 0.1f);
        }
        
        Debug.Log("Applied low performance optimization");
    }
    
    private void ApplyNormalPerformanceMode()
    {
        QualitySettings.shadowResolution = (ShadowResolution)currentSettings.shadowResolution;
        QualitySettings.pixelLightCount = 3;
        QualitySettings.antiAliasing = currentSettings.enableMSAA ? 4 : 0;
        
        if (XRSettings.enabled)
        {
            XRSettings.renderScale = currentSettings.renderScale;
        }
    }
    
    private void AdjustDynamicResolution()
    {
        float targetFPS = currentSettings.targetFrameRate;
        float currentFPS = 1f / averageFrameTime;
        
        if (currentFPS < targetFPS * 0.8f)
        {
            float scale = Mathf.Clamp(XRSettings.renderScale - 0.1f, 0.5f, 1.5f);
            XRSettings.renderScale = scale;
        }
        else if (currentFPS > targetFPS * 1.2f && XRSettings.renderScale < currentSettings.renderScale)
        {
            float scale = Mathf.Clamp(XRSettings.renderScale + 0.05f, 0.5f, currentSettings.renderScale);
            XRSettings.renderScale = scale;
        }
    }
    
    private void MonitorMemoryUsage()
    {
        long totalTextureMemory = 0;
        long totalMeshMemory = 0;
        
        textureMemoryUsage.Clear();
        meshMemoryUsage.Clear();
        
        Texture[] allTextures = Resources.FindObjectsOfTypeAll<Texture>();
        foreach (Texture texture in allTextures)
        {
            long memory = Profiler.GetRuntimeMemorySizeLong(texture);
            textureMemoryUsage[texture.name] = memory;
            totalTextureMemory += memory;
        }
        
        Mesh[] allMeshes = Resources.FindObjectsOfTypeAll<Mesh>();
        foreach (Mesh mesh in allMeshes)
        {
            long memory = Profiler.GetRuntimeMemorySizeLong(mesh);
            meshMemoryUsage[mesh.name] = memory;
            totalMeshMemory += memory;
        }
        
        float textureMemoryMB = totalTextureMemory / (1024f * 1024f);
        float meshMemoryMB = totalMeshMemory / (1024f * 1024f);
        
        if (textureMemoryMB > maxTextureMemoryMB)
        {
            OptimizeTextureMemory();
        }
        
        if (meshMemoryMB > maxMeshMemoryMB)
        {
            OptimizeMeshMemory();
        }
        
        Debug.Log($"Memory Usage - Textures: {textureMemoryMB:F1}MB, Meshes: {meshMemoryMB:F1}MB");
    }
    
    private void OptimizeTextureMemory()
    {
        Debug.Log("Optimizing texture memory...");
        
        List<KeyValuePair<string, long>> sortedTextures = new List<KeyValuePair<string, long>>(textureMemoryUsage);
        sortedTextures.Sort((a, b) => b.Value.CompareTo(a.Value));
        
        for (int i = 0; i < Mathf.Min(5, sortedTextures.Count); i++)
        {
            Debug.Log($"Large texture: {sortedTextures[i].Key} - {sortedTextures[i].Value / (1024 * 1024)}MB");
        }
    }
    
    private void OptimizeMeshMemory()
    {
        Debug.Log("Optimizing mesh memory...");
    }
    
    private void UnloadUnusedAssets()
    {
        if (unloadUnusedAssets)
        {
            Resources.UnloadUnusedAssets();
            System.GC.Collect();
            
            Debug.Log("Unloaded unused assets");
        }
    }
    
    public void LogPerformanceReport()
    {
        string report = $"Performance Report:\n" +
                       $"Platform: {Application.platform}\n" +
                       $"FPS: {1f / averageFrameTime:F1}\n" +
                       $"Frame Time: {averageFrameTime * 1000:F1}ms\n" +
                       $"Render Scale: {XRSettings.renderScale:F2}\n" +
                       $"Memory: {System.GC.GetTotalMemory(false) / (1024 * 1024)}MB";
        
        Debug.Log(report);
    }
    
    private void OnDestroy()
    {
        CancelInvoke("MonitorMemoryUsage");
        CancelInvoke("UnloadUnusedAssets");
    }
}

12.10.2 商业部署准备

商业应用部署需要考虑应用商店要求、用户数据分析、崩溃报告等商业要素:

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.Analytics;
using UnityEngine.Networking;

public class DeploymentManager : MonoBehaviour
{
    [System.Serializable]
    public class BuildSettings
    {
        public string companyName = "AstroTech Studios";
        public string productName = "Stellar Explorer";
        public string bundleIdentifier = "com.astrotech.stellarexplorer";
        public int bundleVersion = 1;
        public string bundleVersionString = "1.0.0";
        public BuildTarget buildTarget = BuildTarget.Android;
        public BuildType buildType = BuildType.Release;
        public bool developmentBuild = false;
        public bool autoConnectProfiler = false;
        public bool allowDebugging = false;
        public CompressionMethod compression = CompressionMethod.LZ4HC;
    }
    
    [System.Serializable]
    public class StoreSettings
    {
        public StorePlatform platform;
        public string storeKey;
        public string storeSecret;
        public string appId;
        public string privacyPolicyUrl;
        public string termsOfServiceUrl;
        public string supportEmail;
        public AgeRating ageRating = AgeRating.Everyone;
        public ContentRating contentRating = ContentRating.LowMaturity;
    }
    
    [System.Serializable]
    public class AnalyticsSettings
    {
        public bool enableAnalytics = true;
        public bool enableCrashReporting = true;
        public bool enablePerformanceReporting = true;
        public bool enableUserBehaviorTracking = true;
        public float analyticsSendInterval = 60f;
        public string analyticsEndpoint = "https://analytics.astrotech.com";
    }
    
    public enum BuildTarget
    {
        Windows,
        MacOS,
        Linux,
        Android,
        iOS,
        WebGL
    }
    
    public enum BuildType
    {
        Debug,
        Development,
        Release,
        Master
    }
    
    public enum CompressionMethod
    {
        LZ4,
        LZ4HC,
        Uncompressed
    }
    
    public enum StorePlatform
    {
        GooglePlay,
        AppStore,
        Steam,
        OculusStore,
        PlayStationStore,
        XboxStore
    }
    
    public enum AgeRating
    {
        Everyone,
        Everyone10Plus,
        Teen,
        Mature17Plus,
        AdultsOnly18Plus
    }
    
    public enum ContentRating
    {
        LowMaturity,
        MediumMaturity,
        HighMaturity
    }
    
    [Header("Build Configuration")]
    public BuildSettings buildSettings;
    public StoreSettings storeSettings;
    public AnalyticsSettings analyticsSettings;
    
    [Header("Build Assets")]
    public Texture2D appIcon;
    public Texture2D[] splashScreens;
    public AudioClip startupSound;
    
    [Header("Localization")]
    public SystemLanguage defaultLanguage = SystemLanguage.English;
    public Font[] languageFonts;
    
    private Dictionary<string, object> analyticsData;
    private float lastAnalyticsSendTime;
    private bool isAnalyticsInitialized = false;
    
    private void Awake()
    {
        if (Application.isEditor)
        {
            return;
        }
        
        InitializeDeployment();
        InitializeAnalytics();
        SetupCrashReporting();
        CheckFirstLaunch();
    }
    
    private void InitializeDeployment()
    {
        Application.companyName = buildSettings.companyName;
        Application.productName = buildSettings.productName;
        Application.identifier = buildSettings.bundleIdentifier;
        Application.version = buildSettings.bundleVersionString;
        
        Screen.sleepTimeout = SleepTimeout.NeverSleep;
        
        SetupQualitySettings();
        SetupLocalization();
    }
    
    private void SetupQualitySettings()
    {
        if (!buildSettings.developmentBuild)
        {
            Debug.unityLogger.logEnabled = false;
        }
        
        if (buildSettings.buildType == BuildType.Release || buildSettings.buildType == BuildType.Master)
        {
            QualitySettings.vSyncCount = 1;
            Application.targetFrameRate = 60;
        }
    }
    
    private void SetupLocalization()
    {
        SystemLanguage deviceLanguage = Application.systemLanguage;
        
        if (!IsLanguageSupported(deviceLanguage))
        {
            deviceLanguage = defaultLanguage;
        }
        
        LocalizationManager.Instance.SetLanguage(deviceLanguage);
        
        SetupLanguageFont(deviceLanguage);
    }
    
    private bool IsLanguageSupported(SystemLanguage language)
    {
        SystemLanguage[] supportedLanguages = {
            SystemLanguage.English,
            SystemLanguage.Spanish,
            SystemLanguage.French,
            SystemLanguage.German,
            SystemLanguage.Chinese,
            SystemLanguage.Japanese
        };
        
        return Array.Exists(supportedLanguages, lang => lang == language);
    }
    
    private void SetupLanguageFont(SystemLanguage language)
    {
    }
    
    private void InitializeAnalytics()
    {
        if (!analyticsSettings.enableAnalytics)
        {
            return;
        }
        
        analyticsData = new Dictionary<string, object>();
        
        Analytics.enabled = true;
        Analytics.initializeOnStartup = true;
        
        Analytics.CustomEvent("AppLaunch", new Dictionary<string, object>
        {
            { "platform", Application.platform.ToString() },
            { "version", Application.version },
            { "device_model", SystemInfo.deviceModel },
            { "device_name", SystemInfo.deviceName },
            { "graphics_device", SystemInfo.graphicsDeviceName },
            { "processor", SystemInfo.processorType },
            { "memory", SystemInfo.systemMemorySize }
        });
        
        isAnalyticsInitialized = true;
        lastAnalyticsSendTime = Time.time;
    }
    
    private void SetupCrashReporting()
    {
        if (!analyticsSettings.enableCrashReporting)
        {
            return;
        }
        
        Application.logMessageReceived += HandleLogMessage;
        
        SetupNativeCrashReporting();
    }
    
    private void HandleLogMessage(string condition, string stackTrace, LogType type)
    {
        if (type == LogType.Exception || type == LogType.Error || type == LogType.Assert)
        {
            Dictionary<string, object> crashData = new Dictionary<string, object>
            {
                { "condition", condition },
                { "stack_trace", stackTrace },
                { "log_type", type.ToString() },
                { "timestamp", DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss") },
                { "session_duration", Time.timeSinceLevelLoad }
            };
            
            SendCrashReport(crashData);
        }
    }
    
    private void SetupNativeCrashReporting()
    {
    }
    
    private void SendCrashReport(Dictionary<string, object> crashData)
    {
        if (!analyticsSettings.enableCrashReporting)
        {
            return;
        }
        
        StartCoroutine(SendCrashReportCoroutine(crashData));
    }
    
    private IEnumerator SendCrashReportCoroutine(Dictionary<string, object> crashData)
    {
        string json = JsonUtility.ToJson(crashData);
        
        using (UnityWebRequest request = new UnityWebRequest(analyticsSettings.analyticsEndpoint + "/crash", "POST"))
        {
            byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(json);
            request.uploadHandler = new UploadHandlerRaw(bodyRaw);
            request.downloadHandler = new DownloadHandlerBuffer();
            request.SetRequestHeader("Content-Type", "application/json");
            request.SetRequestHeader("Authorization", "Bearer " + storeSettings.storeKey);
            
            yield return request.SendWebRequest();
            
            if (request.result != UnityWebRequest.Result.Success)
            {
                Debug.LogError($"Failed to send crash report: {request.error}");
            }
        }
    }
    
    private void CheckFirstLaunch()
    {
        string firstLaunchKey = "FirstLaunch_" + Application.version;
        
        if (!PlayerPrefs.HasKey(firstLaunchKey))
        {
            PlayerPrefs.SetInt(firstLaunchKey, 1);
            PlayerPrefs.Save();
            
            OnFirstLaunch();
        }
        else
        {
            int launchCount = PlayerPrefs.GetInt("LaunchCount", 0);
            launchCount++;
            PlayerPrefs.SetInt("LaunchCount", launchCount);
            PlayerPrefs.Save();
        }
    }
    
    private void OnFirstLaunch()
    {
        Debug.Log("First launch detected");
        
        if (analyticsSettings.enableAnalytics && isAnalyticsInitialized)
        {
            Analytics.CustomEvent("FirstLaunch", new Dictionary<string, object>
            {
                { "version", Application.version },
                { "device_id", SystemInfo.deviceUniqueIdentifier }
            });
        }
        
        ShowWelcomeScreen();
    }
    
    private void ShowWelcomeScreen()
    {
    }
    
    private void Update()
    {
        if (!isAnalyticsInitialized)
        {
            return;
        }
        
        if (Time.time - lastAnalyticsSendTime >= analyticsSettings.analyticsSendInterval)
        {
            SendAnalyticsData();
            lastAnalyticsSendTime = Time.time;
        }
        
        if (analyticsSettings.enablePerformanceReporting)
        {
            CollectPerformanceData();
        }
    }
    
    private void SendAnalyticsData()
    {
        if (analyticsData.Count == 0)
        {
            return;
        }
        
        StartCoroutine(SendAnalyticsDataCoroutine());
    }
    
    private IEnumerator SendAnalyticsDataCoroutine()
    {
        string json = JsonUtility.ToJson(analyticsData);
        
        using (UnityWebRequest request = new UnityWebRequest(analyticsSettings.analyticsEndpoint + "/analytics", "POST"))
        {
            byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(json);
            request.uploadHandler = new UploadHandlerRaw(bodyRaw);
            request.downloadHandler = new DownloadHandlerBuffer();
            request.SetRequestHeader("Content-Type", "application/json");
            request.SetRequestHeader("Authorization", "Bearer " + storeSettings.storeKey);
            
            yield return request.SendWebRequest();
            
            if (request.result != UnityWebRequest.Result.Success)
            {
                Debug.LogError($"Failed to send analytics: {request.error}");
            }
            else
            {
                analyticsData.Clear();
            }
        }
    }
    
    private void CollectPerformanceData()
    {
        float fps = 1f / Time.deltaTime;
        float memoryUsage = System.GC.GetTotalMemory(false) / (1024f * 1024f);
        
        if (!analyticsData.ContainsKey("performance"))
        {
            analyticsData["performance"] = new List<Dictionary<string, object>>();
        }
        
        var performanceList = analyticsData["performance"] as List<Dictionary<string, object>>;
        
        performanceList.Add(new Dictionary<string, object>
        {
            { "fps", fps },
            { "memory_mb", memoryUsage },
            { "timestamp", DateTime.UtcNow.ToString("HH:mm:ss") },
            { "scene", UnityEngine.SceneManagement.SceneManager.GetActiveScene().name }
        });
        
        if (performanceList.Count > 100)
        {
            performanceList.RemoveAt(0);
        }
    }
    
    public void TrackUserEvent(string eventName, Dictionary<string, object> eventData = null)
    {
        if (!analyticsSettings.enableUserBehaviorTracking || !isAnalyticsInitialized)
        {
            return;
        }
        
        if (eventData == null)
        {
            eventData = new Dictionary<string, object>();
        }
        
        eventData["event_timestamp"] = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss");
        eventData["session_id"] = SystemInfo.deviceUniqueIdentifier + "_" + Time.timeSinceLevelLoad;
        
        Analytics.CustomEvent(eventName, eventData);
    }
    
    public void LogPurchase(string productId, decimal price, string currency)
    {
        Dictionary<string, object> purchaseData = new Dictionary<string, object>
        {
            { "product_id", productId },
            { "price", price },
            { "currency", currency },
            { "transaction_id", Guid.NewGuid().ToString() }
        };
        
        TrackUserEvent("PurchaseCompleted", purchaseData);
    }
    
    public void LogAchievement(string achievementId, string achievementName)
    {
        Dictionary<string, object> achievementData = new Dictionary<string, object>
        {
            { "achievement_id", achievementId },
            { "achievement_name", achievementName },
            { "unlock_time", DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss") }
        };
        
        TrackUserEvent("AchievementUnlocked", achievementData);
    }
    
    private void OnApplicationPause(bool pauseStatus)
    {
        if (pauseStatus)
        {
            TrackUserEvent("AppPaused", new Dictionary<string, object>
            {
                { "pause_time", Time.timeSinceLevelLoad }
            });
        }
        else
        {
            TrackUserEvent("AppResumed", new Dictionary<string, object>
            {
                { "resume_time", Time.timeSinceLevelLoad },
                { "pause_duration", Time.unscaledDeltaTime }
            });
        }
    }
    
    private void OnApplicationQuit()
    {
        TrackUserEvent("AppQuit", new Dictionary<string, object>
        {
            { "total_session_time", Time.timeSinceLevelLoad },
            { "launch_count", PlayerPrefs.GetInt("LaunchCount", 1) }
        });
        
        if (isAnalyticsInitialized)
        {
            SendAnalyticsData();
        }
    }
}

总结

通过以上完整的技术实现,我们构建了一个功能完备、性能优化、商业可用的天文AR/VR科普应用。从核心的天文算法到前沿的AR/VR技术,从用户界面到外部设备集成,每个环节都考虑了商业应用的实际需求。

关键成功因素包括:

  1. 科学准确性:采用专业天文算法确保内容权威性
  2. 技术先进性:充分利用Unity最新特性实现最佳效果
  3. 用户体验:细致的交互设计和个性化设置
  4. 性能优化:多平台适配和动态性能调整
  5. 商业完整性:完整的分析、部署和运维支持

这个项目展示了如何将复杂的天文科学与现代游戏引擎技术相结合,创造出既有教育价值又有商业潜力的沉浸式应用。通过模块化设计和良好的架构,系统具备良好的可扩展性和可维护性,为后续功能迭代和平台扩展奠定了坚实基础。

Logo

这里是一个专注于游戏开发的社区,我们致力于为广大游戏爱好者提供一个良好的学习和交流平台。我们的专区包含了各大流行引擎的技术博文,涵盖了从入门到进阶的各个阶段,无论你是初学者还是资深开发者,都能在这里找到适合自己的内容。除此之外,我们还会不定期举办游戏开发相关的活动,让大家更好地交流互动。加入我们,一起探索游戏开发的奥秘吧!

更多推荐