diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Packaging/PackageReader.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Packaging/PackageReader.cs
index b791cffa0..3d6220de2 100644
--- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Packaging/PackageReader.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Packaging/PackageReader.cs
@@ -139,6 +139,7 @@ private async Task ReadAssets()
{
_packageFiles.UnpackAssets(_assetPath);
await _packageFiles.UnpackMeshes(_assetPath);
+ _packageFiles.UnpackSounds(_assetPath);
}
///
diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Packaging/PackageWriter.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Packaging/PackageWriter.cs
index 13240de05..01ffc45d6 100644
--- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Packaging/PackageWriter.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Packaging/PackageWriter.cs
@@ -91,7 +91,8 @@ public async Task WritePackage(string path)
// write assets & co
sw1 = Stopwatch.StartNew();
_files.PackAssets();
- Logger.Info($"Assets written in {sw1.ElapsedMilliseconds}ms.");
+ _files.PackSoundMetas();
+ Logger.Info($"Assets and files written in {sw1.ElapsedMilliseconds}ms.");
storage.Close();
sw.Stop();
@@ -180,7 +181,7 @@ private async Task WriteColliderMeshes()
Format = GltfFormat.Binary,
};
var export = new GameObjectExport(exportSettings, logger: logger);
- export.AddScene(meshGos.ToArray(), _table.transform.worldToLocalMatrix, "VPE Table");
+ export.AddScene(meshGos.ToArray(), _table.transform.worldToLocalMatrix, "Colliders");
await export.SaveToStreamAndDispose(glbFile.AsStream());
var glbMeta = _metaFolder.AddFile(PackageApi.ColliderMeshesMeta, PackageApi.Packer.FileExtension);
diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/SoundAssetInspector.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/SoundAssetInspector.cs
index 6d9e219da..c2719719a 100644
--- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/SoundAssetInspector.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/SoundAssetInspector.cs
@@ -17,10 +17,9 @@
using System;
using System.Threading;
using UnityEditor;
-using UnityEngine.UIElements;
-using UnityEngine;
using UnityEditor.UIElements;
-using System.Collections.Generic;
+using UnityEngine;
+using UnityEngine.UIElements;
namespace VisualPinball.Unity.Editor
{
@@ -70,7 +69,7 @@ private void OnDisable()
private void RemoveNullClips()
{
- var clipsProp = serializedObject.FindProperty("_clips");
+ var clipsProp = serializedObject.FindProperty(nameof(SoundAsset.Clips));
for (var i = clipsProp.arraySize -1; i >= 0; i--) {
if (clipsProp.GetArrayElementAtIndex(i).objectReferenceValue == null)
clipsProp.DeleteArrayElementAtIndex(i);
@@ -112,4 +111,3 @@ private void OnStopForrealButtonClicked()
}
}
}
-
diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/SoundAssetInspector.uxml b/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/SoundAssetInspector.uxml
index 4b7569fb3..ab69a35e7 100644
--- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/SoundAssetInspector.uxml
+++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/SoundAssetInspector.uxml
@@ -1,15 +1,15 @@
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/SoundComponentInspector.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/SoundComponentInspector.cs
index 99e03495e..eb99058cc 100644
--- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/SoundComponentInspector.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/SoundComponentInspector.cs
@@ -48,7 +48,7 @@ protected void InvalidSoundAssetHelpBox(VisualElement container)
"The selected sound asset is invalid. Make sure it has at least one audio clip.",
HelpBoxMessageType.Warning);
container.Add(helpBox);
- var soundAssetProp = serializedObject.FindProperty(nameof(SoundComponent._soundAsset));
+ var soundAssetProp = serializedObject.FindProperty(nameof(SoundComponent.SoundAsset));
UpdateVisibility(soundAssetProp);
helpBox.TrackPropertyValue(soundAssetProp, UpdateVisibility);
@@ -106,7 +106,7 @@ protected void InfiniteLoopHelpBox(VisualElement container)
void UpdateVisbility(SerializedObject obj)
{
- var prop = obj.FindProperty(nameof(SoundComponent._soundAsset));
+ var prop = obj.FindProperty(nameof(SoundComponent.SoundAsset));
var soundAsset = prop.objectReferenceValue as SoundAsset;
if (soundAsset && soundAsset.Loop && !AllTargetsSupportLoopingSoundAssets()) {
helpBox.style.display = DisplayStyle.Flex;
diff --git a/VisualPinball.Unity/VisualPinball.Unity/Packaging/JsonPacker.cs b/VisualPinball.Unity/VisualPinball.Unity/Packaging/JsonPacker.cs
index c04b4b7d5..2b42a53b1 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/Packaging/JsonPacker.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/Packaging/JsonPacker.cs
@@ -26,7 +26,9 @@ public class JsonPacker : IDataPacker
public byte[] Pack(T obj)
{
try {
- return Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(obj, Formatting.Indented));
+ return Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(obj, Formatting.Indented, new JsonSerializerSettings {
+ ReferenceLoopHandling = ReferenceLoopHandling.Ignore
+ }));
} catch (Exception e) {
Debug.LogError(e);
throw e;
diff --git a/VisualPinball.Unity/VisualPinball.Unity/Packaging/PackageApi.cs b/VisualPinball.Unity/VisualPinball.Unity/Packaging/PackageApi.cs
index 4c0996c18..fd3cdae07 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/Packaging/PackageApi.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/Packaging/PackageApi.cs
@@ -39,6 +39,7 @@ public static class PackageApi
public const string WiresFile = "wires";
public const string LampsFile = "lamps";
public const string AssetFolder = "assets";
+ public const string SoundFolder = "sounds";
public static readonly IStorageManager StorageManager = new SharpZipStorageManager();
// public static IStorageManager StorageManager => new OpenMcdfStorageManager();
diff --git a/VisualPinball.Unity/VisualPinball.Unity/Packaging/PackagedFiles.cs b/VisualPinball.Unity/VisualPinball.Unity/Packaging/PackagedFiles.cs
index b5043a893..7b6d60fef 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/Packaging/PackagedFiles.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/Packaging/PackagedFiles.cs
@@ -27,13 +27,6 @@ namespace VisualPinball.Unity
{
public class PackagedFiles
{
- public readonly Dictionary ColliderMeshInstanceIdToGuid = new();
-
- private readonly Dictionary _colliderMeshes = new();
- private Dictionary _colliderMeshMeta;
-
- private readonly HashSet _scriptableObjects = new();
- private readonly Dictionary _deserializedObjects = new();
private readonly IPackageFolder _tableFolder;
private readonly PackNameLookup _typeLookup;
@@ -45,12 +38,115 @@ public PackagedFiles(IPackageFolder tableFolder, PackNameLookup typeLookup)
_typeLookup = typeLookup;
}
+ private string UniqueName(IPackageFolder folder, string name, string fileExtension = null)
+ {
+ var baseName = name;
+ var i = 1;
+ while (true) {
+ if (folder.TryGetFile(name, out _, fileExtension ?? PackageApi.Packer.FileExtension)) {
+ name = $"{baseName} ({++i})";
+ } else {
+ return name;
+ }
+ }
+ }
+
+ #region Collider Meshes
+
+ public readonly Dictionary ColliderMeshInstanceIdToGuid = new();
+ private readonly Dictionary _colliderMeshes = new();
+ private Dictionary _colliderMeshMeta;
+
public string GetColliderMeshGuid(IColliderMesh cm)
{
var instanceId = (cm as Component)!.GetInstanceID();
return ColliderMeshInstanceIdToGuid.GetValueOrDefault(instanceId);
}
+ public async Task UnpackMeshes(string assetPath)
+ {
+ if (!_tableFolder.TryGetFolder(PackageApi.MetaFolder, out var metaFolder)) {
+ return;
+ }
+ if (!_tableFolder.TryGetFile(PackageApi.ColliderMeshesFile, out var colliderMeshes)) {
+ return;
+ }
+ if (!metaFolder.TryGetFile(PackageApi.ColliderMeshesMeta, out var colliderMeta, PackageApi.Packer.FileExtension)) {
+ return;
+ }
+ var glbPath = Path.Combine(assetPath, "colliders.glb");
+ try {
+ AssetDatabase.StartAssetEditing();
+
+ // dump glb
+ await using var glbFileStream = new FileStream(glbPath, FileMode.Create, FileAccess.Write);
+ await colliderMeshes.AsStream().CopyToAsync(glbFileStream);
+
+ } finally {
+ // resume asset database refreshing
+ AssetDatabase.StopAssetEditing();
+ AssetDatabase.Refresh();
+ }
+
+ var glbRelativePath = Path.GetRelativePath(Path.Combine(Application.dataPath, ".."), glbPath);
+ var glbPrefab = AssetDatabase.LoadAssetAtPath(glbRelativePath);
+ if (glbPrefab == null) {
+ throw new Exception($"Could not load colliders.glb at path: {glbRelativePath}");
+ }
+
+ _colliderMeshMeta = ColliderMeshMetaPackable.Unpack(colliderMeta.GetData());
+ var n = glbPrefab.transform.childCount;
+ _colliderMeshes.Clear();
+ for (var i = 0; i < n; i++) {
+ var collider = glbPrefab.transform.GetChild(i);
+ var guid = collider.name;
+ if (_colliderMeshMeta.TryGetValue(guid, out var meta)) {
+ if (meta.PrefabGuid != null) {
+ var prefabPath = AssetDatabase.GUIDToAssetPath(meta.PrefabGuid);
+ if (prefabPath != null) {
+ var prefab = AssetDatabase.LoadAssetAtPath(prefabPath);
+ if (prefab != null) {
+ // this is only half-tested (with empty string), since we currently don't have prefabs with a deeper hierarchy
+ var meshGo = prefab.transform.Find(meta.PathWithinPrefab);
+ if (meshGo != null) {
+ var meshFilter = meshGo.GetComponent();
+ if (meshFilter != null) {
+ var mesh = meshGo.GetComponent().sharedMesh;
+ if (mesh != null) {
+ collider.GetComponent().sharedMesh = mesh;
+ }
+ } else {
+ Logger.Warn($"Cannot find mesh at path {meta.PathWithinPrefab} in prefab {prefabPath}.");
+ }
+ } else {
+ Logger.Warn($"Cannot find mesh at path {meta.PathWithinPrefab} in prefab {prefabPath}.");
+ }
+ } else {
+ Logger.Warn($"Cannot load prefab for collider mesh at {prefabPath}.");
+ }
+ } else {
+ Logger.Warn($"Cannot find prefab for collider mesh {guid}.");
+ }
+ }
+ } else {
+ Logger.Warn($"Cannot fine meta data for collider mesh {guid}.");
+ }
+ _colliderMeshes.Add(guid, collider.GetComponent().sharedMesh);
+ }
+ }
+
+ public Mesh GetColliderMesh(string guid)
+ {
+ return _colliderMeshes[guid];
+ }
+
+ #endregion
+
+ #region Assets
+
+ private readonly HashSet _scriptableObjects = new();
+ private readonly Dictionary _deserializedAssets = new();
+
public int AddAsset(ScriptableObject scriptableObject)
{
if (scriptableObject == null) {
@@ -67,7 +163,7 @@ public int AddAsset(ScriptableObject scriptableObject)
public T GetAsset(int instanceId) where T : ScriptableObject
{
- if (_deserializedObjects.TryGetValue(instanceId, out var asset)) {
+ if (_deserializedAssets.TryGetValue(instanceId, out var asset)) {
return asset as T;
}
return null;
@@ -104,8 +200,6 @@ public void UnpackAssets(string assetPath)
assetFolder.VisitFolders(assetTypeFolder => {
assetTypeFolder.VisitFiles(assetFile => {
- var folder = Path.Combine(assetPath, assetTypeFolder.Name);
-
if (assetFile.Name.Contains(".meta")) {
return;
}
@@ -124,48 +218,109 @@ public void UnpackAssets(string assetPath)
throw new Exception($"Failed to unpack asset {assetFile.Name}");
}
+ var folder = Path.Combine(assetPath, assetTypeFolder.Name);
if (!Directory.Exists(folder)) {
Directory.CreateDirectory(folder);
}
var relativePath = Path.GetRelativePath(Path.Combine(Application.dataPath, ".."), folder);
AssetDatabase.CreateAsset(asset, Path.Combine(relativePath, Path.GetFileNameWithoutExtension(assetFile.Name) + ".asset"));
- _deserializedObjects.Add(meta.InstanceId, asset);
+ _deserializedAssets.Add(meta.InstanceId, asset);
});
});
}
- private string UniqueName(IPackageFolder folder, string name)
+ #endregion
+
+ #region Sounds
+
+ private readonly Dictionary _soundMeta = new();
+ private readonly Dictionary _audioClips = new();
+
+ public string Add(AudioClip clip)
{
- var baseName = name;
- var i = 1;
- while (true) {
- if (folder.TryGetFile(name, out _, PackageApi.Packer.FileExtension)) {
- name = $"{baseName} ({++i})";
- } else {
- return name;
- }
+ if (!clip) {
+ return null;
+ }
+ if (!_tableFolder.TryGetFolder(PackageApi.SoundFolder, out var soundFolder)) {
+ soundFolder = _tableFolder.AddFolder(PackageApi.SoundFolder);
}
+ var path = AssetDatabase.GetAssetPath(clip);
+ var guid = AssetDatabase.AssetPathToGUID(path);
+ var filename = UniqueName(soundFolder, Path.GetFileName(path), Path.GetExtension(path));
+ using var writeStream = soundFolder.AddFile(filename).AsStream();
+ using var readStream = File.OpenRead(path);
+ readStream.CopyTo(writeStream);
+
+ _soundMeta.Add(filename, new SoundMetaPackable {
+ Guid = guid
+ });
+
+ return guid;
}
- public async Task UnpackMeshes(string assetPath)
+ public AudioClip GetAudioClip(string guid)
{
+ if (_audioClips.TryGetValue(guid, out var clip)) {
+ return clip;
+ }
+ Logger.Error($"Could not find loaded AudioClip with GUID {guid}");
+ return null;
+ }
+
+ public void PackSoundMetas()
+ {
+ if (_soundMeta.Count == 0) {
+ return;
+ }
if (!_tableFolder.TryGetFolder(PackageApi.MetaFolder, out var metaFolder)) {
+ metaFolder = _tableFolder.AddFolder(PackageApi.MetaFolder);
+ }
+ var soundMeta = metaFolder.AddFile(PackageApi.SoundFolder, PackageApi.Packer.FileExtension);
+ soundMeta.SetData(PackageApi.Packer.Pack(_soundMeta));
+ }
+
+ public void UnpackSounds(string assetPath)
+ {
+ if (!_tableFolder.TryGetFolder(PackageApi.SoundFolder, out var soundFolder)) {
return;
}
- if (!_tableFolder.TryGetFile(PackageApi.ColliderMeshesFile, out var colliderMeshes)) {
+ if (!_tableFolder.TryGetFolder(PackageApi.MetaFolder, out var metaFolder)) {
return;
}
- if (!metaFolder.TryGetFile(PackageApi.ColliderMeshesMeta, out var colliderMeta, PackageApi.Packer.FileExtension)) {
+ if (!metaFolder.TryGetFile(PackageApi.SoundFolder, out var soundMeta, PackageApi.Packer.FileExtension)) {
return;
}
- var glbPath = Path.Combine(assetPath, "colliders.glb");
+ var soundMetas = PackageApi.Packer.Unpack>(soundMeta.GetData());
+ var dumpedSounds = new Dictionary();
+ var folder = Path.Combine(assetPath, "Sounds");
+
try {
+ // dump sounds in batch and load them afterwards
AssetDatabase.StartAssetEditing();
+ soundFolder.VisitFiles(soundFile => {
+ if (soundMetas.TryGetValue(soundFile.Name, out var meta)) {
- // dump glb
- await using var glbFileStream = new FileStream(glbPath, FileMode.Create, FileAccess.Write);
- await colliderMeshes.AsStream().CopyToAsync(glbFileStream);
+ // check if we don't already have this file
+ var path = AssetDatabase.GUIDToAssetPath(meta.Guid);
+ if (!string.IsNullOrEmpty(path)) {
+ _audioClips.Add(meta.Guid, AssetDatabase.LoadAssetAtPath(path));
+ Logger.Info($"Matched sound file {soundFile.Name} with existing asset at {path}, skipping.");
+ return;
+ }
+
+ if (!Directory.Exists(folder)) {
+ Directory.CreateDirectory(folder);
+ }
+ path = Path.Combine(folder, soundFile.Name);
+ using var readStream = soundFile.AsStream();
+ using var writeStream = new FileStream(path, FileMode.Create, FileAccess.Write);
+ readStream.CopyTo(writeStream);
+ dumpedSounds.Add(meta.Guid, Path.GetRelativePath(Path.Combine(Application.dataPath, ".."), path));
+ } else {
+ Logger.Error($"Cannot find meta data for sound file {soundFile.Name}");
+ }
+ });
} finally {
// resume asset database refreshing
@@ -173,56 +328,12 @@ public async Task UnpackMeshes(string assetPath)
AssetDatabase.Refresh();
}
- var glbRelativePath = Path.GetRelativePath(Path.Combine(Application.dataPath, ".."), glbPath);
- var glbPrefab = AssetDatabase.LoadAssetAtPath(glbRelativePath);
- if (glbPrefab == null) {
- throw new Exception($"Could not load colliders.glb at path: {glbRelativePath}");
- }
-
- _colliderMeshMeta = ColliderMeshMetaPackable.Unpack(colliderMeta.GetData());
- var n = glbPrefab.transform.childCount;
- _colliderMeshes.Clear();
- for (var i = 0; i < n; i++) {
- var collider = glbPrefab.transform.GetChild(i);
- var guid = collider.name;
- if (_colliderMeshMeta.TryGetValue(guid, out var meta)) {
- if (meta.PrefabGuid != null) {
- var prefabPath = AssetDatabase.GUIDToAssetPath(meta.PrefabGuid);
- if (prefabPath != null) {
- var prefab = AssetDatabase.LoadAssetAtPath(prefabPath);
- if (prefab != null) {
- // this is only half-tested (with empty string), since we currently don't have prefabs with a deeper hierarchy
- var meshGo = prefab.transform.Find(meta.PathWithinPrefab);
- if (meshGo != null) {
- var meshFilter = meshGo.GetComponent();
- if (meshFilter != null) {
- var mesh = meshGo.GetComponent().sharedMesh;
- if (mesh != null) {
- collider.GetComponent().sharedMesh = mesh;
- }
- } else {
- Logger.Warn($"Cannot find mesh at path {meta.PathWithinPrefab} in prefab {prefabPath}.");
- }
- } else {
- Logger.Warn($"Cannot find mesh at path {meta.PathWithinPrefab} in prefab {prefabPath}.");
- }
- } else {
- Logger.Warn($"Cannot load prefab for collider mesh at {prefabPath}.");
- }
- } else {
- Logger.Warn($"Cannot find prefab for collider mesh {guid}.");
- }
- }
- } else {
- Logger.Warn($"Cannot fine meta data for collider mesh {guid}.");
- }
- _colliderMeshes.Add(guid, collider.GetComponent().sharedMesh);
+ // load dumped sounds
+ foreach (var (guid, path) in dumpedSounds) {
+ _audioClips.Add(guid, AssetDatabase.LoadAssetAtPath(path));
}
}
- public Mesh GetColliderMesh(string guid)
- {
- return _colliderMeshes[guid];
- }
+ #endregion
}
}
diff --git a/VisualPinball.Unity/VisualPinball.Unity/Packaging/README.md b/VisualPinball.Unity/VisualPinball.Unity/Packaging/README.md
index e01aa4b88..32b3dac9c 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/Packaging/README.md
+++ b/VisualPinball.Unity/VisualPinball.Unity/Packaging/README.md
@@ -36,7 +36,8 @@ If you extract a `.vpe` file, you'll see the following structure:
β ββ π BumperCollider
β ββ π 0.json
ββ π meta
- β ββ π colliders.json
+ β ββ π colliders.json
+ β ββ π sounds.json
ββ π refs
β ββ π 0
β ββ π 0.1
@@ -45,6 +46,8 @@ If you extract a `.vpe` file, you'll see the following structure:
β ββ π BumperCollider
β ββ π BumperSound
β ββ π 0.json
+ ββ π sounds
+ β ββ π Flipper 1.wav
ββ π table.glb
ββ π colliders.glb
```
@@ -92,6 +95,8 @@ Next, we serialize the GameObject and component data into `π items` and `π
Each component determines for itself which data is written to `π items` and which to `π refs`. The purpose of these two folders is that data is read in two passes during import: the first pass creates the components, and the second pass updates cross-references between them.
+Components might add include other files. For example, sound components add the actual sound files, which are written to `π sounds`.
+
### Globals
Global data is then written to `π global`. Currently, this folder contains mappings for switches, coils, lamps, and wires.
@@ -102,9 +107,11 @@ In this context, assets are instances of `ScriptableObject`, usually serialized
Assets are grouped into folders based on their type (again determined by the `[PackAs]` attribute). Because they are deserialized as-is, we need an easy way to reference them, which is the purpose of their `*.meta.json` counterparts. The goal of these meta files is to link each asset to an identifier, which is then used by the component data.
+This step also writes other metadata such as `π sounds.json` in the `π meta` folder, which maps the GUID of the sound assets to their name.
+
### More to come
-Future additions will include sounds, shaders, external dependencies such as PinMAME, MPF, and Visual Scripting, and more.
+Future additions will include shaders, external dependencies such as PinMAME, MPF, and Visual Scripting, and more.
## Import
@@ -117,9 +124,10 @@ One important point is that loading a `.vpe` file during runtime is fundamentall
- The order in which data is imported is important for both runtime and edit time, because some steps depend on others:
1. Load `π table.glb`, which gives us the scene hierarchy.
2. Unpack `π assets` and `π colliders.glb`
- 3. Loop through `π items` and do, in this order:
+ 3. Unpack sounds to `π sounds`. Since the GUIDs used for referencing the sound files are the original asset GUIDs, we only write sound files that aren't already in the asset database.
+ 4. Loop through `π items` and do, in this order:
1. Instantiate and apply components
2. Link them to their prefab (if the prefab exists in the editor).
3. Apply component data.
- 4. Loop through `π refs` and restore cross-references between components.
- 5. Import data from `π global`.
+ 5. Loop through `π refs` and restore cross-references between components.
+ 6. Import data from `π global`.
diff --git a/VisualPinball.Unity/VisualPinball.Unity/Sound/CoilSoundComponent.cs b/VisualPinball.Unity/VisualPinball.Unity/Sound/CoilSoundComponent.cs
index 84c479406..00859f121 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/Sound/CoilSoundComponent.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/Sound/CoilSoundComponent.cs
@@ -24,6 +24,7 @@ namespace VisualPinball.Unity
///
/// Start and or stop a sound when a coil is energized or deenergized.
///
+ [PackAs("CoilSound")]
[AddComponentMenu("Visual Pinball/Sound/Coil Sound")]
public class CoilSoundComponent : BinaryEventSoundComponent
{
diff --git a/VisualPinball.Unity/VisualPinball.Unity/Sound/SoundAsset.cs b/VisualPinball.Unity/VisualPinball.Unity/Sound/SoundAsset.cs
index b4861768c..c4192e938 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/Sound/SoundAsset.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/Sound/SoundAsset.cs
@@ -17,9 +17,10 @@
// ReSharper disable InconsistentNaming
using System;
-using System.Linq;
+using Newtonsoft.Json;
using UnityEngine;
using UnityEngine.Audio;
+using UnityEngine.Serialization;
using Random = UnityEngine.Random;
namespace VisualPinball.Unity
@@ -30,43 +31,46 @@ namespace VisualPinball.Unity
/// for frequently used sounds. Instances of this class can be stored in the project files or
/// in an asset library.
///
+ [PackAs("SoundAsset")]
[CreateAssetMenu(fileName = "Sound", menuName = "Visual Pinball/Sound", order = 102)]
public class SoundAsset : ScriptableObject
{
- private enum SelectionMethod
+ public enum SelectionMethod
{
RoundRobin,
Random
}
- [SerializeField]
- private string _description;
+ [FormerlySerializedAs("_description")]
+ public string Description;
- [SerializeField]
- private AudioClip[] _clips;
+ [JsonIgnore]
+ [FormerlySerializedAs("_clips")]
+ public AudioClip[] Clips;
- [SerializeField]
- private SelectionMethod _clipSelectionMethod;
+ [FormerlySerializedAs("_clipSelectionMethod")]
+ public SelectionMethod ClipSelectionMethod;
- [SerializeField]
- private Vector2 _volumeRange = new(1f, 1f);
+ [FormerlySerializedAs("_volumeRange")]
+ public Vector2 VolumeRange = new(1f, 1f);
- [SerializeField]
- private Vector2 _pitchRange = new(1f, 1f);
+ [FormerlySerializedAs("_pitchRange")]
+ public Vector2 PitchRange = new(1f, 1f);
- [SerializeField]
- private bool _loop;
- public bool Loop => _loop;
+ [FormerlySerializedAs("_loop")]
+ public bool Loop;
- [SerializeField, Range(0, 10f)] private float _fadeInTime;
- public float FadeInTime => _fadeInTime;
+ [FormerlySerializedAs("_fadeInTime")]
+ [SerializeField, Range(0, 10f)]
+ public float FadeInTime;
- [SerializeField, Range(0, 10f)] private float _fadeOutTime;
- public float FadeOutTime => _fadeOutTime;
+ [FormerlySerializedAs("_fadeOutTime")]
+ [SerializeField, Range(0, 10f)]
+ public float FadeOutTime;
+ [FormerlySerializedAs("_isSpatial")]
[Tooltip("Should the sound appear to come from the position of the emitter?")]
- [SerializeField]
- private bool _isSpatial = true;
+ public bool IsSpatial = true;
[SerializeField]
private AudioMixerGroup _audioMixerGroup;
@@ -75,21 +79,21 @@ private enum SelectionMethod
public void ConfigureAudioSource(AudioSource audioSource, float volume = 1)
{
- audioSource.volume = volume * Random.Range(_volumeRange.x, _volumeRange.y);
- audioSource.pitch = Random.Range(_pitchRange.x, _pitchRange.y);
- audioSource.loop = _loop;
+ audioSource.volume = volume * Random.Range(VolumeRange.x, VolumeRange.y);
+ audioSource.pitch = Random.Range(PitchRange.x, PitchRange.y);
+ audioSource.loop = Loop;
audioSource.clip = GetClip();
- audioSource.spatialBlend = _isSpatial ? 0f : 1f;
+ audioSource.spatialBlend = IsSpatial ? 0f : 1f;
audioSource.outputAudioMixerGroup = _audioMixerGroup;
}
public bool IsValid()
{
- if (_clips == null) {
+ if (Clips == null) {
return false;
}
- foreach (var clip in _clips) {
+ foreach (var clip in Clips) {
if (clip != null) {
return true;
}
@@ -100,20 +104,20 @@ public bool IsValid()
private AudioClip GetClip()
{
- if (_clips.Length == 0) {
+ if (Clips.Length == 0) {
throw new InvalidOperationException($"The sound asset '{name}' has no audio clips to play.");
}
- switch (_clipSelectionMethod) {
+ switch (ClipSelectionMethod) {
case SelectionMethod.RoundRobin:
- _roundRobinIndex %= _clips.Length;
- var clip = _clips[_roundRobinIndex];
+ _roundRobinIndex %= Clips.Length;
+ var clip = Clips[_roundRobinIndex];
_roundRobinIndex++;
return clip;
case SelectionMethod.Random:
- return _clips[Random.Range(0, _clips.Length)];
+ return Clips[Random.Range(0, Clips.Length)];
default:
throw new NotImplementedException("Selection method not implemented.");
diff --git a/VisualPinball.Unity/VisualPinball.Unity/Sound/SoundComponent.cs b/VisualPinball.Unity/VisualPinball.Unity/Sound/SoundComponent.cs
index 036337534..c74389114 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/Sound/SoundComponent.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/Sound/SoundComponent.cs
@@ -22,30 +22,48 @@
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
+using UnityEngine.Serialization;
namespace VisualPinball.Unity
{
///
/// Base component for playing a SoundAsset using the public methods Play and Stop.
///
+ [PackAs("Sound")]
[AddComponentMenu("Visual Pinball/Sound/Sound")]
- public class SoundComponent : EnableAfterAwakeComponent
+ public class SoundComponent : EnableAfterAwakeComponent, IPackable
{
- [SerializeReference]
- public SoundAsset _soundAsset;
+ [FormerlySerializedAs("_soundAsset")]
+ public SoundAsset SoundAsset;
- [SerializeField]
+ [FormerlySerializedAs("_interrupt")]
[Tooltip("Should the sound be interrupted if it is triggered again while already playing?")]
- protected bool _interrupt;
+ public bool Interrupt;
+ [FormerlySerializedAs("_volume")]
[SerializeField, Range(0f, 1f)]
- private float _volume = 1f;
+ public float Volume = 1f;
private CancellationTokenSource _instantCts;
private CancellationTokenSource _allowFadeCts;
private float _lastPlayStartTime = float.NegativeInfinity;
protected static readonly Logger Logger = LogManager.GetCurrentClassLogger();
+ #region Packaging
+
+ public byte[] Pack() => SoundPackable.Pack(this);
+
+ public byte[] PackReferences(Transform root, PackNameLookup lookup, PackagedFiles files) =>
+ SoundReferencesPackable.PackReferences(this, files);
+
+ public void Unpack(byte[] bytes) => SoundPackable.Unpack(bytes, this);
+
+ public void UnpackReferences(byte[] data, Transform root, PackNameLookup lookup, PackagedFiles files)
+ => SoundReferencesPackable.Unpack(data, this, files);
+
+ #endregion
+
+
protected override void OnEnableAfterAfterAwake()
{
base.OnEnableAfterAfterAwake();
@@ -68,7 +86,7 @@ public async Task Play(float volume = 1f)
Logger.Warn("Cannot play a disabled sound component.");
return;
}
- if (_soundAsset == null) {
+ if (SoundAsset == null) {
Logger.Warn("Cannot play without sound asset. Assign it in the inspector.");
return;
}
@@ -80,13 +98,13 @@ public async Task Play(float volume = 1f)
return;
}
- if (_interrupt) {
+ if (Interrupt) {
Stop(allowFade: true);
}
try {
- var combinedVol = _volume * volume;
+ var combinedVol = Volume * volume;
_lastPlayStartTime = Time.unscaledTime;
- await SoundUtils.Play(_soundAsset, gameObject, _allowFadeCts.Token, _instantCts.Token, combinedVol);
+ await SoundUtils.Play(SoundAsset, gameObject, _allowFadeCts.Token, _instantCts.Token, combinedVol);
} catch (OperationCanceledException) { }
}
diff --git a/VisualPinball.Unity/VisualPinball.Unity/Sound/SoundPackable.cs b/VisualPinball.Unity/VisualPinball.Unity/Sound/SoundPackable.cs
new file mode 100644
index 000000000..13b7c39d2
--- /dev/null
+++ b/VisualPinball.Unity/VisualPinball.Unity/Sound/SoundPackable.cs
@@ -0,0 +1,80 @@
+ο»Ώ// Visual Pinball Engine
+// Copyright (C) 2023 freezy and VPE Team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+// ReSharper disable MemberCanBePrivate.Global
+
+using System;
+using System.Linq;
+
+namespace VisualPinball.Unity
+{
+ public struct SoundPackable {
+
+ public bool Interrupt;
+ public float Volume;
+
+ public static byte[] Pack(SoundComponent comp) => PackageApi.Packer.Pack(new SoundPackable {
+ Interrupt = comp.Interrupt,
+ Volume = comp.Volume,
+ });
+
+ public static void Unpack(byte[] bytes, SoundComponent comp)
+ {
+ var data = PackageApi.Packer.Unpack(bytes);
+ comp.Interrupt = data.Interrupt;
+ comp.Volume = data.Volume;
+ }
+ }
+
+ public struct SoundReferencesPackable {
+
+ public int SoundAssetRef;
+ public string[] ClipRefs;
+
+ public static byte[] PackReferences(SoundComponent comp, PackagedFiles files)
+ {
+ if (!comp.SoundAsset) {
+ return Array.Empty();
+ }
+
+ // pack asset
+ var assetRef = files.AddAsset(comp.SoundAsset);
+
+ var clipRefs = comp.SoundAsset.Clips != null
+ // pack sound files
+ ? comp.SoundAsset.Clips.Select(files.Add).ToArray()
+ : Array.Empty();
+
+ return PackageApi.Packer.Pack(new SoundReferencesPackable {
+ SoundAssetRef = assetRef,
+ ClipRefs = clipRefs
+ });
+ }
+
+ public static void Unpack(byte[] bytes, SoundComponent comp, PackagedFiles files)
+ {
+ var data = PackageApi.Packer.Unpack(bytes);
+ comp.SoundAsset = files.GetAsset(data.SoundAssetRef);
+ comp.SoundAsset.Clips = data.ClipRefs.Select(files.GetAudioClip).ToArray();
+ }
+ }
+
+ public struct SoundMetaPackable
+ {
+ public string Guid;
+ // will probably get more data in here
+ }
+}
diff --git a/VisualPinball.Unity/VisualPinball.Unity/Sound/SoundPackable.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/Sound/SoundPackable.cs.meta
new file mode 100644
index 000000000..b948886d6
--- /dev/null
+++ b/VisualPinball.Unity/VisualPinball.Unity/Sound/SoundPackable.cs.meta
@@ -0,0 +1,3 @@
+ο»ΏfileFormatVersion: 2
+guid: 58bb87c682844d98b546a4ecb6573cd3
+timeCreated: 1739045662
\ No newline at end of file
diff --git a/VisualPinball.Unity/VisualPinball.Unity/Sound/SwitchSoundComponent.cs b/VisualPinball.Unity/VisualPinball.Unity/Sound/SwitchSoundComponent.cs
index c7374e64e..4bab79f3d 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/Sound/SwitchSoundComponent.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/Sound/SwitchSoundComponent.cs
@@ -24,6 +24,7 @@ namespace VisualPinball.Unity
///
/// Start and or stop a sound when a coil is energized or deenergized.
///
+ [PackAs("SwitchSound")]
[AddComponentMenu("Visual Pinball/Sound/Switch Sound")]
public class SwitchSoundComponent : BinaryEventSoundComponent
{
diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Bumper/BumperColliderComponent.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Bumper/BumperColliderComponent.cs
index 5c6fd577e..fbb2d7aab 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Bumper/BumperColliderComponent.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Bumper/BumperColliderComponent.cs
@@ -59,6 +59,7 @@ public void UnpackReferences(byte[] data, Transform root, PackNameLookup lookup,
Scatter = mat.Scatter;
PhysicsMaterial = files.GetAsset(mat.AssetRef);
}
+
#endregion
#region Physics Material
diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperColliderComponent.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperColliderComponent.cs
index f49bfed86..011b0f1d3 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperColliderComponent.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperColliderComponent.cs
@@ -22,9 +22,10 @@
namespace VisualPinball.Unity
{
+ [PackAs("FlipperCollider")]
[AddComponentMenu("Visual Pinball/Collision/Flipper Collider")]
[HelpURL("https://docs.visualpinball.org/creators-guide/manual/mechanisms/flippers.html")]
- public class FlipperColliderComponent : ColliderComponent
+ public class FlipperColliderComponent : ColliderComponent, IPackable
{
#region Data
@@ -76,6 +77,20 @@ public class FlipperColliderComponent : ColliderComponent FlipperColliderPackable.Pack(this);
+
+ public byte[] PackReferences(Transform root, PackNameLookup lookup, PackagedFiles files)
+ => FlipperColliderReferencesPackable.PackReferences(this, files);
+
+ public void Unpack(byte[] bytes) => FlipperColliderPackable.Unpack(bytes, this);
+
+ public void UnpackReferences(byte[] data, Transform root, PackNameLookup lookup, PackagedFiles files)
+ => FlipperColliderReferencesPackable.Unpack(data, this, files);
+
+ #endregion
+
#region Physics Material
protected override float PhysicsElasticity => Elasticity;
diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperCorrectionAsset.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperCorrectionAsset.cs
index 5d32e350a..1ef665390 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperCorrectionAsset.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperCorrectionAsset.cs
@@ -21,6 +21,7 @@ namespace VisualPinball.Unity
///
/// An asset containing the flipper correction parameters (aka nFozzy).
///
+ [PackAs("FlipperCorrection")]
[CreateAssetMenu(fileName = "Flipper Correction", menuName = "Visual Pinball/Flipper Correction", order = 101)]
public class FlipperCorrectionAsset : ScriptableObject
{
diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperPackable.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperPackable.cs
index 25bc32aca..721564d5c 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperPackable.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperPackable.cs
@@ -65,4 +65,69 @@ public static void Unpack(byte[] bytes, FlipperComponent comp)
comp._rubberWidth = data.RubberWidth;
}
}
+
+ public struct FlipperColliderPackable
+ {
+ public float Mass;
+ public float Strength;
+ public float Return;
+ public float RampUp;
+ public float TorqueDamping;
+ public float TorqueDampingAngle;
+
+ public static byte[] Pack(FlipperColliderComponent comp)
+ {
+ return PackageApi.Packer.Pack(new FlipperColliderPackable {
+ Mass = comp.Mass,
+ Strength = comp.Strength,
+ Return = comp.Return,
+ RampUp = comp.RampUp,
+ TorqueDamping = comp.TorqueDamping,
+ TorqueDampingAngle = comp.TorqueDampingAngle,
+ });
+ }
+
+ public static void Unpack(byte[] bytes, FlipperColliderComponent comp)
+ {
+ var data = PackageApi.Packer.Unpack(bytes);
+ comp.Mass = data.Mass;
+ comp.Strength = data.Strength;
+ comp.Return = data.Return;
+ comp.RampUp = data.RampUp;
+ comp.TorqueDamping = data.TorqueDamping;
+ comp.TorqueDampingAngle = data.TorqueDampingAngle;
+ }
+ }
+
+ public struct FlipperColliderReferencesPackable
+ {
+ public PhysicalMaterialPackable PhysicalMaterial;
+ public int FlipperCorrectionRef;
+
+ public static byte[] PackReferences(FlipperColliderComponent comp, PackagedFiles files)
+ {
+ return PackageApi.Packer.Pack(new FlipperColliderReferencesPackable {
+ PhysicalMaterial = new PhysicalMaterialPackable {
+ Elasticity = comp.Elasticity,
+ ElasticityFalloff = comp.ElasticityFalloff,
+ Friction = comp.Friction,
+ Scatter = comp.Scatter,
+ Overwrite = true,
+ AssetRef = files.AddAsset(comp.PhysicsMaterial),
+ },
+ FlipperCorrectionRef = files.AddAsset(comp.FlipperCorrection)
+ });
+ }
+
+ public static void Unpack(byte[] bytes, FlipperColliderComponent comp, PackagedFiles files)
+ {
+ var data = PackageApi.Packer.Unpack(bytes);
+ comp.Elasticity = data.PhysicalMaterial.Elasticity;
+ comp.ElasticityFalloff = data.PhysicalMaterial.ElasticityFalloff;
+ comp.Friction = data.PhysicalMaterial.Friction;
+ comp.Scatter = data.PhysicalMaterial.Scatter;
+ comp.PhysicsMaterial = files.GetAsset(data.PhysicalMaterial.AssetRef);
+ comp.FlipperCorrection = files.GetAsset(data.FlipperCorrectionRef);
+ }
+ }
}