music player makes menu now
This commit is contained in:
parent
d30d357f38
commit
d6fd187cea
3 changed files with 102 additions and 38 deletions
|
|
@ -12,11 +12,25 @@ using AnimatorAsCode.V1.VRC;
|
|||
using NUnit.Framework.Constraints;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using nadena.dev.modular_avatar.core;
|
||||
|
||||
[assembly: ExportsPlugin(typeof(MusicPlayerPlugin))]
|
||||
|
||||
namespace gay.lilyy.MusicPlayer.Editor
|
||||
{
|
||||
public static class ParameterNames
|
||||
{
|
||||
public const string SongIndex = "Music_SongIndex";
|
||||
public const string Volume = "Music_Volume";
|
||||
public const string IsLocal = "Music_IsLocal";
|
||||
}
|
||||
|
||||
public static class LayerNames
|
||||
{
|
||||
public const string Volume = "Music_Volume";
|
||||
public const string Player = "Music_Player";
|
||||
}
|
||||
|
||||
public class MusicPlayerPlugin : Plugin<MusicPlayerPlugin>
|
||||
{
|
||||
public override string QualifiedName => "gay.lilyy.MusicPlayer";
|
||||
|
|
@ -35,48 +49,31 @@ namespace gay.lilyy.MusicPlayer.Editor
|
|||
if (songs.Length == 0) return; // If there are none in the avatar, skip this entirely.
|
||||
|
||||
var songGo = songs[0].gameObject;
|
||||
List<int> usedIndexes = new List<int>();
|
||||
|
||||
int trackNumber = 0;
|
||||
foreach (var song in songs)
|
||||
{
|
||||
if (song.gameObject != songGo)
|
||||
{
|
||||
throw new System.Exception("All MusicPlayerSong components must be on the same GameObject.");
|
||||
}
|
||||
if (usedIndexes.Contains(song.TrackNumber))
|
||||
{
|
||||
throw new System.Exception($"${song.Name} is using a duplicate track number: ${song.TrackNumber}");
|
||||
}
|
||||
trackNumber++;
|
||||
song.TrackNumber = trackNumber;
|
||||
}
|
||||
|
||||
var audioSource = songGo.GetComponent<AudioSource>();
|
||||
// INSERT_YOUR_CODE
|
||||
// Ensure all track numbers from 1 to max are present
|
||||
int minTrackNumber = songs.Min(song => song.TrackNumber);
|
||||
if (minTrackNumber < 1) {
|
||||
throw new System.Exception("Track numbers are 1 and up");
|
||||
}
|
||||
int maxTrackNumber = songs.Max(song => song.TrackNumber);
|
||||
HashSet<int> trackNumbers = new HashSet<int>(songs.Select(song => song.TrackNumber));
|
||||
for (int i = 1; i <= maxTrackNumber; i++)
|
||||
{
|
||||
if (!trackNumbers.Contains(i))
|
||||
{
|
||||
throw new System.Exception($"Missing track number: {i}. All track numbers from 1 to {maxTrackNumber} must be filled.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Initialize Animator As Code.
|
||||
var modularAvatar = MaAc.Create(new GameObject(SystemName)
|
||||
{
|
||||
transform = { parent = ctx.AvatarRootTransform }
|
||||
});
|
||||
var modularAvatar = MaAc.Create(songGo);
|
||||
|
||||
|
||||
var controller = CreateController(ctx, modularAvatar, songs, audioSource);
|
||||
|
||||
foreach (var song in songs)
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(song);
|
||||
}
|
||||
|
||||
// Create a new object in the scene. We will add Modular Avatar components inside it.
|
||||
|
||||
|
|
@ -106,18 +103,75 @@ namespace gay.lilyy.MusicPlayer.Editor
|
|||
// This will be merged with the rest of the playable layer at the end of this function.
|
||||
var controller = aac.NewAnimatorController();
|
||||
|
||||
CreatePlayerLayer(controller, modularAvatar, songs, audioSource);
|
||||
CreateVolumeLayer(aac, controller, modularAvatar, audioSource);
|
||||
var songIndexParam = CreatePlayerLayer(controller, modularAvatar, songs, audioSource);
|
||||
var volumeParam = CreateVolumeLayer(aac, controller, modularAvatar, audioSource);
|
||||
|
||||
CreateMenu(songIndexParam, volumeParam, modularAvatar, songs);
|
||||
|
||||
return controller;
|
||||
}
|
||||
|
||||
private void CreatePlayerLayer(AacFlController controller, MaAc modularAvatar, MusicPlayerSong[] songs, AudioSource audioSource)
|
||||
private void CreateMenu(AacFlIntParameter songIndexParam, AacFlFloatParameter volumeParam, MaAc modularAvatar, MusicPlayerSong[] songs) {
|
||||
var menuGo = new GameObject("Menu");
|
||||
menuGo.transform.SetParent(songs[0].transform);
|
||||
var menuItem = menuGo.AddComponent<ModularAvatarMenuItem>();
|
||||
menuItem.label = "Music";
|
||||
menuItem.Control.type = VRCExpressionsMenu.Control.ControlType.SubMenu;
|
||||
menuItem.MenuSource = SubmenuSource.Children;
|
||||
menuGo.AddComponent<ModularAvatarMenuInstaller>();
|
||||
|
||||
// Dictionary to store folder hierarchy
|
||||
var folderHierarchy = new Dictionary<string, GameObject>();
|
||||
|
||||
foreach (var song in songs)
|
||||
{
|
||||
GameObject currentParent = menuGo;
|
||||
|
||||
// Create folder hierarchy if specified
|
||||
if (!string.IsNullOrEmpty(song.Folder))
|
||||
{
|
||||
string[] folderPath = song.Folder.Split('/');
|
||||
|
||||
// Create nested folder structure
|
||||
for (int i = 0; i < folderPath.Length; i++)
|
||||
{
|
||||
string folderName = folderPath[i];
|
||||
string fullPath = string.Join("/", folderPath, 0, i + 1);
|
||||
|
||||
if (!folderHierarchy.ContainsKey(fullPath))
|
||||
{
|
||||
var folderGo = new GameObject($"Folder_{folderName}");
|
||||
folderGo.transform.SetParent(currentParent.transform);
|
||||
var folderMenuItem = folderGo.AddComponent<ModularAvatarMenuItem>();
|
||||
folderMenuItem.label = folderName;
|
||||
folderMenuItem.Control.type = VRCExpressionsMenu.Control.ControlType.SubMenu;
|
||||
folderMenuItem.MenuSource = SubmenuSource.Children;
|
||||
if (song.icon != null) folderMenuItem.Control.icon = song.icon;
|
||||
|
||||
folderHierarchy[fullPath] = folderGo;
|
||||
}
|
||||
|
||||
currentParent = folderHierarchy[fullPath];
|
||||
}
|
||||
}
|
||||
|
||||
// Create the song toggle
|
||||
var go = new GameObject($"Song_{song.Name}");
|
||||
go.transform.SetParent(currentParent.transform);
|
||||
var toggle = modularAvatar.EditMenuItem(go).ToggleSets(songIndexParam, song.TrackNumber).Name(song.Name);
|
||||
if (song.icon != null) toggle.WithIcon(song.icon);
|
||||
}
|
||||
var volumeGo = new GameObject("Volume");
|
||||
volumeGo.transform.SetParent(menuGo.transform);
|
||||
modularAvatar.EditMenuItem(volumeGo).Radial(volumeParam).Name("Volume");
|
||||
}
|
||||
|
||||
private AacFlIntParameter CreatePlayerLayer(AacFlController controller, MaAc modularAvatar, MusicPlayerSong[] songs, AudioSource audioSource)
|
||||
{
|
||||
var playerLayer = controller.NewLayer("Player");
|
||||
var songIndexParam = playerLayer.IntParameter("SongIndex");
|
||||
var playerLayer = controller.NewLayer(LayerNames.Player);
|
||||
var songIndexParam = playerLayer.IntParameter(ParameterNames.SongIndex);
|
||||
modularAvatar.NewParameter(songIndexParam).NotSaved();
|
||||
var isLocal = playerLayer.BoolParameter("IsLocal");
|
||||
var isLocal = playerLayer.Av3().IsLocal;
|
||||
|
||||
var nonLocalState = playerLayer.NewState("NonLocal");
|
||||
var idleState = playerLayer.NewState("Idle");
|
||||
|
|
@ -125,6 +179,8 @@ namespace gay.lilyy.MusicPlayer.Editor
|
|||
nonLocalState.TransitionsTo(idleState).When(isLocal.IsEqualTo(true));
|
||||
// will never appear ingame, useful for testing
|
||||
idleState.TransitionsTo(nonLocalState).When(isLocal.IsEqualTo(false));
|
||||
|
||||
|
||||
|
||||
foreach (var song in songs)
|
||||
{
|
||||
|
|
@ -142,21 +198,24 @@ namespace gay.lilyy.MusicPlayer.Editor
|
|||
audio.StartsPlayingOnEnter();
|
||||
});
|
||||
}
|
||||
return songIndexParam;
|
||||
}
|
||||
private void CreateVolumeLayer(AacFlBase aac, AacFlController controller, MaAc modularAvatar, AudioSource audioSource) {
|
||||
var songVolumeLayer = controller.NewLayer("Volume");
|
||||
var volumeParam = songVolumeLayer.FloatParameter("volume");
|
||||
modularAvatar.NewParameter(volumeParam).WithDefaultValue(0.01f);
|
||||
private AacFlFloatParameter CreateVolumeLayer(AacFlBase aac, AacFlController controller, MaAc modularAvatar, AudioSource audioSource) {
|
||||
var songVolumeLayer = controller.NewLayer(LayerNames.Volume);
|
||||
var volumeParam = songVolumeLayer.FloatParameter(ParameterNames.Volume);
|
||||
modularAvatar.NewParameter(volumeParam).WithDefaultValue(0.2f);
|
||||
|
||||
var clip = aac.NewClip().Animating((action) => {
|
||||
var animate = action.Animates(audioSource, "m_Volume");
|
||||
animate.WithSecondsUnit((seconds) => {
|
||||
seconds.Linear(0, 0);
|
||||
seconds.Linear(1, 1);
|
||||
seconds.Linear(1, 0.3f);
|
||||
});
|
||||
});
|
||||
var state = songVolumeLayer.NewState("Volume");
|
||||
state.WithAnimation(clip).WithMotionTime(volumeParam);
|
||||
|
||||
return volumeParam;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@
|
|||
"AnimatorAsCode.V1.VRChat",
|
||||
"AnimatorAsCode.V1.ModularAvatar",
|
||||
"nadena.dev.ndmf",
|
||||
"nadena.dev.ndmf.vrchat"
|
||||
"nadena.dev.ndmf.vrchat",
|
||||
"nadena.dev.modular-avatar.core"
|
||||
],
|
||||
"includePlatforms": [
|
||||
"Editor"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using VRC.SDK3.Avatars.ScriptableObjects;
|
||||
|
|
@ -8,7 +9,10 @@ namespace gay.lilyy.MusicPlayer
|
|||
[RequireComponent(typeof(AudioSource))]
|
||||
public class MusicPlayerSong : MonoBehaviour, IEditorOnly {
|
||||
public string Name = "Song Name";
|
||||
public int TrackNumber = 1;
|
||||
[HideInInspector]
|
||||
public int TrackNumber = -1;
|
||||
public AudioClip song;
|
||||
public Texture2D icon;
|
||||
public string Folder;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue