第12章 构建跨平台星际漫游科普应用:从天文算法到AR/VR实现
跨平台星际漫游科普应用开发摘要 本项目基于Unity引擎开发跨平台天文科普应用,整合AR/VR技术提供沉浸式天文学习体验。核心功能包括: 实时星空模拟(支持时间控制和地理位置识别) 太阳系3D/AR交互模型 VR宇宙航行体验 采用NASA JPL DE430星历表作为天文数据基础,实现多精度计算模式和LOD系统优化性能。项目采用敏捷开发流程,分阶段实现核心功能、AR模块和VR模块。关键技术挑战包括
第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技术,从用户界面到外部设备集成,每个环节都考虑了商业应用的实际需求。
关键成功因素包括:
- 科学准确性:采用专业天文算法确保内容权威性
- 技术先进性:充分利用Unity最新特性实现最佳效果
- 用户体验:细致的交互设计和个性化设置
- 性能优化:多平台适配和动态性能调整
- 商业完整性:完整的分析、部署和运维支持
这个项目展示了如何将复杂的天文科学与现代游戏引擎技术相结合,创造出既有教育价值又有商业潜力的沉浸式应用。通过模块化设计和良好的架构,系统具备良好的可扩展性和可维护性,为后续功能迭代和平台扩展奠定了坚实基础。
这里是一个专注于游戏开发的社区,我们致力于为广大游戏爱好者提供一个良好的学习和交流平台。我们的专区包含了各大流行引擎的技术博文,涵盖了从入门到进阶的各个阶段,无论你是初学者还是资深开发者,都能在这里找到适合自己的内容。除此之外,我们还会不定期举办游戏开发相关的活动,让大家更好地交流互动。加入我们,一起探索游戏开发的奥秘吧!
更多推荐


所有评论(0)