diff --git a/Assets/Audio.meta b/Assets/Audio.meta new file mode 100644 index 0000000..5efda84 --- /dev/null +++ b/Assets/Audio.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3d988beb579a940d7bfbc1ad7e2e7e17 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Audio/SFX.meta b/Assets/Audio/SFX.meta new file mode 100644 index 0000000..ef07f18 --- /dev/null +++ b/Assets/Audio/SFX.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ddb76c89063d14d3eb74a6c0bbf5f8f2 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Audio/SFX/ButtonHit.wav b/Assets/Audio/SFX/ButtonHit.wav new file mode 100644 index 0000000..dd2c4de Binary files /dev/null and b/Assets/Audio/SFX/ButtonHit.wav differ diff --git a/Assets/Audio/SFX/ButtonHit.wav.meta b/Assets/Audio/SFX/ButtonHit.wav.meta new file mode 100644 index 0000000..86a9d81 --- /dev/null +++ b/Assets/Audio/SFX/ButtonHit.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 7bb487aa2617a4056a1b39a0273320d2 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Audio/SFX/CorrectHit.wav b/Assets/Audio/SFX/CorrectHit.wav new file mode 100644 index 0000000..1da576b Binary files /dev/null and b/Assets/Audio/SFX/CorrectHit.wav differ diff --git a/Assets/Audio/SFX/CorrectHit.wav.meta b/Assets/Audio/SFX/CorrectHit.wav.meta new file mode 100644 index 0000000..6fe47db --- /dev/null +++ b/Assets/Audio/SFX/CorrectHit.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 3933bfed4d8be4f9fabe55d5ade4c07e +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Audio/SFX/IncorrectHit.wav b/Assets/Audio/SFX/IncorrectHit.wav new file mode 100644 index 0000000..5db3f40 Binary files /dev/null and b/Assets/Audio/SFX/IncorrectHit.wav differ diff --git a/Assets/Audio/SFX/IncorrectHit.wav.meta b/Assets/Audio/SFX/IncorrectHit.wav.meta new file mode 100644 index 0000000..112fbcf --- /dev/null +++ b/Assets/Audio/SFX/IncorrectHit.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 530d8f9f532ea4f2e9098500121f2c57 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Audio/SFX/MissHit.wav b/Assets/Audio/SFX/MissHit.wav new file mode 100644 index 0000000..5645b18 Binary files /dev/null and b/Assets/Audio/SFX/MissHit.wav differ diff --git a/Assets/Audio/SFX/MissHit.wav.meta b/Assets/Audio/SFX/MissHit.wav.meta new file mode 100644 index 0000000..b3954ef --- /dev/null +++ b/Assets/Audio/SFX/MissHit.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 0ea1f6e4ffd644b628855576f686e3b4 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Audio/SFX/WrongDirection.wav b/Assets/Audio/SFX/WrongDirection.wav new file mode 100644 index 0000000..4f600f1 Binary files /dev/null and b/Assets/Audio/SFX/WrongDirection.wav differ diff --git a/Assets/Audio/SFX/WrongDirection.wav.meta b/Assets/Audio/SFX/WrongDirection.wav.meta new file mode 100644 index 0000000..2ec8079 --- /dev/null +++ b/Assets/Audio/SFX/WrongDirection.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 920c5009ea0384f22a22a9b366964a4a +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Integration.meta b/Assets/Integration.meta new file mode 100644 index 0000000..d56ae88 --- /dev/null +++ b/Assets/Integration.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3d326dd4d0d254842be556b977461446 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Integration/DebugPrototypeController.cs b/Assets/Integration/DebugPrototypeController.cs new file mode 100644 index 0000000..82ba4c9 --- /dev/null +++ b/Assets/Integration/DebugPrototypeController.cs @@ -0,0 +1,102 @@ +using TMPro; +using UnityEngine; +using UnityEngine.SceneManagement; +using UnityEngine.UI; + +/** + * Prototype debug panel. Wires sliders and a restart button at runtime; + * also forces 1920×1080 fullscreen on launch and locks the main camera's + * viewport to a 16:9 aspect ratio. + * + */ +public class DebugPrototypeController : MonoBehaviour +{ + #region Inspector Fields — Scriptable Objects + + [Header("Scriptable Objects")] + [Tooltip("Controls note appearance timing (note appearance beats slider).")] + [SerializeField] private RhythmVisualConfig _rhythmVisualConfig; + + #endregion + + #region Inspector Fields — Note Appearance Beats + + [Header("Note Appearance Beats")] + [Tooltip("Slider that sets RhythmVisualConfig.NoteAppearanceBeats. " + + "Higher value = notes appear further ahead = more reaction time.")] + [SerializeField] private Slider _noteBeatsSlider; + [Tooltip("Optional label displaying the current beats value.")] + [SerializeField] private TMP_Text _noteBeatsLabel; + [Tooltip("Minimum note appearance beats.")] + [SerializeField] private float _noteBeatsMin = 0.5f; + [Tooltip("Maximum note appearance beats.")] + [SerializeField] private float _noteBeatsMax = 6f; + + #endregion + + #region Inspector Fields — Controls + + [Header("Controls")] + [Tooltip("Reloads the scene at the specified build index.")] + [SerializeField] private Button _restartButton; + [Tooltip("Build index of the scene to load on restart.")] + [SerializeField] private int _gameSceneIndex; + + #endregion + + #region Lifecycle + + private void Awake() + { + Screen.SetResolution(1920, 1080, FullScreenMode.FullScreenWindow); + SetupNoteBeatsSlider(); + SetupRestartButton(); + } + + #endregion + + #region Setup + + private void SetupNoteBeatsSlider() + { + if (_noteBeatsSlider == null) return; + + _noteBeatsSlider.minValue = _noteBeatsMin; + _noteBeatsSlider.maxValue = _noteBeatsMax; + _noteBeatsSlider.value = _rhythmVisualConfig != null ? _rhythmVisualConfig.NoteAppearanceBeats : 2f; + + _noteBeatsSlider.onValueChanged.AddListener(OnNoteBeatsChanged); + RefreshNoteBeatsLabel(_noteBeatsSlider.value); + } + + private void SetupRestartButton() + { + if (_restartButton == null) return; + _restartButton.onClick.AddListener(OnRestartClicked); + } + + #endregion + + #region Callbacks + + private void OnNoteBeatsChanged(float value) + { + if (_rhythmVisualConfig != null) + _rhythmVisualConfig.NoteAppearanceBeats = value; + + RefreshNoteBeatsLabel(value); + } + + private void OnRestartClicked() + { + SceneManager.LoadScene(_gameSceneIndex); + } + + private void RefreshNoteBeatsLabel(float value) + { + if (_noteBeatsLabel != null) + _noteBeatsLabel.text = $"Note Speed: {value:F2} beats"; + } + + #endregion +} diff --git a/Assets/Integration/DebugPrototypeController.cs.meta b/Assets/Integration/DebugPrototypeController.cs.meta new file mode 100644 index 0000000..89500ec --- /dev/null +++ b/Assets/Integration/DebugPrototypeController.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a12a8f60640784ff6928d5c61ac956c9 \ No newline at end of file diff --git a/Assets/Integration/TreeInputRhythmAdapter.cs b/Assets/Integration/TreeInputRhythmAdapter.cs new file mode 100644 index 0000000..20947f0 --- /dev/null +++ b/Assets/Integration/TreeInputRhythmAdapter.cs @@ -0,0 +1,77 @@ +using System; + +/** + * Adapts to + * so the existing tree game inputs drive the rhythm system without any new + * InputActions or action maps. Each directional action is mapped to a configurable + * rhythm lane index that converts back to a + * after the beat window check passes. + * + */ +public class TreeInputRhythmAdapter : IRhythmInputProvider +{ + #region Private Events + + private event Action _onLanePressed; + private event Action _onLaneReleased; + + #endregion + + #region IRhythmInputProvider + + /** */ + event Action IRhythmInputProvider.OnLanePressed + { + add => _onLanePressed += value; + remove => _onLanePressed -= value; + } + + /** */ + event Action IRhythmInputProvider.OnLaneReleased + { + add => _onLaneReleased += value; + remove => _onLaneReleased -= value; + } + + /** */ + public void Enable() => _playerInput.Enable(); + + /** */ + public void Disable() => _playerInput.Disable(); + + #endregion + + #region Fields + + private readonly IPlayerInputProvider _playerInput; + private readonly int _leftLane; + private readonly int _submitLane; + private readonly int _rightLane; + + #endregion + + #region Constructor + + /** + * Wires events to lane-indexed rhythm events. + * + * The upstream tree game input source. + * Lane index emitted when NavLeft is pressed (default 0). + * Lane index emitted when Submit is pressed (default 1). + * Lane index emitted when NavRight is pressed (default 2). + */ + public TreeInputRhythmAdapter(IPlayerInputProvider playerInput, + int leftLane = 0, int submitLane = 1, int rightLane = 2) + { + _playerInput = playerInput; + _leftLane = leftLane; + _submitLane = submitLane; + _rightLane = rightLane; + + _playerInput.OnNavLeft += () => _onLanePressed?.Invoke(_leftLane); + _playerInput.OnNavRight += () => _onLanePressed?.Invoke(_rightLane); + _playerInput.OnSubmit += () => _onLanePressed?.Invoke(_submitLane); + } + + #endregion +} diff --git a/Assets/Integration/TreeInputRhythmAdapter.cs.meta b/Assets/Integration/TreeInputRhythmAdapter.cs.meta new file mode 100644 index 0000000..7d2d390 --- /dev/null +++ b/Assets/Integration/TreeInputRhythmAdapter.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a85633517b3bab345a98fe7e26743e9c \ No newline at end of file diff --git a/Assets/Integration/TreeRhythmController.cs b/Assets/Integration/TreeRhythmController.cs new file mode 100644 index 0000000..26ec6b2 --- /dev/null +++ b/Assets/Integration/TreeRhythmController.cs @@ -0,0 +1,393 @@ +using Cysharp.Threading.Tasks; +using TMPro; +using UnityEngine; + +/** + * Composition root for the combined tree and rhythm game mode. + * + */ +public class TreeRhythmController : MonoBehaviour, IRhythmGameAdapter +{ + #region Inspector Fields — Tree + + [Header("Tree Configuration")] + [Tooltip("Visual and layout parameters shared with the tree visualizer.")] + [SerializeField] private TreeVisualizerConfig _config; + + [Header("Tree Scene References")] + [Tooltip("Prefab instantiated for each BST node.")] + [SerializeField] private GameObject _nodePrefab; + [Tooltip("Drives camera panning to follow the carrier node.")] + [SerializeField] private CameraRig _cameraRig; + [Tooltip("Parent transform that holds edge line renderers.")] + [SerializeField] private Transform _edgeContainer; + + [Header("Tree UI")] + [Tooltip("Label displaying the running error count.")] + [SerializeField] private TMP_Text _errorCountLabel; + + [Header("Sample Tree")] + [Tooltip("Values pre-inserted into the tree before gameplay starts. Leave empty for a blank tree.")] + [SerializeField] private IntSampleTreeProvider _sampleTreeProvider; + + [Header("Animation")] + [Tooltip("Sound effects and animation parameters for node reactions.")] + [SerializeField] private TreeAnimationConfig _animationConfig; + + #endregion + + #region Inspector Fields — Rhythm + + [Header("Rhythm Configuration")] + [Tooltip("Beatmap asset containing song audio, BPM, notes, and key points.")] + [SerializeField] private BeatmapData _beatmap; + [Tooltip("Hit window and scoring parameters.")] + [SerializeField] private HitWindowConfig _hitWindows; + [Tooltip("Calibration offsets for audio and visual latency compensation.")] + [SerializeField] private RhythmCalibrationConfig _calibration; + + [Header("Rhythm Scene References")] + [Tooltip("AudioSource used to play the song. Must be on a persistent GameObject.")] + [SerializeField] private AudioSource _audioSource; + [Tooltip("Optional: drives note visuals. Null if running rhythm in headless/audio-only mode.")] + [SerializeField] private RhythmVisualizerController _visualizer; + + [Header("Lane → Action Mapping")] + [Tooltip("Maps each IRhythmInputProvider lane index to a NavigationAction. " + + "Default order: [0]=Left, [1]=Place, [2]=Right.")] + [SerializeField] private NavigationAction[] _laneToAction = + { + NavigationAction.Left, + NavigationAction.Place, + NavigationAction.Right + }; + + #endregion + + #region Private Systems + + private TreeNavigator _navigator; + private UnityInputProvider _rawInput; + private TreeInputRhythmAdapter _rhythmInput; + private TreeRhythmReactionPolicy _reactionPolicy; + private RhythmManager _rhythmManager; + private IntValueGenerator _generator; + private TreeLayoutManager _layout; + private int _lastPulseSubdivision = -1; + + #endregion + + #region Carrier State + + private TreeNodeView _carrierView; + private TreeNode _activeComparisonNode; + private bool _lastActionSucceeded; + + #endregion + + #region Lifecycle + + private void Awake() + { + _reactionPolicy = new TreeRhythmReactionPolicy(); + _rawInput = new UnityInputProvider(); + _rhythmInput = new TreeInputRhythmAdapter(_rawInput); + + float noteAppearanceBeats = _visualizer != null ? _visualizer.NoteAppearanceBeats : 2f; + _rhythmManager = new RhythmManager(_audioSource, _hitWindows, _calibration, _reactionPolicy, noteAppearanceBeats); + + var rules = new BSTInsertionRules(); + var hooks = new TreeAnimationHooks(_animationConfig, _audioSource, () => _carrierView); + _navigator = new TreeNavigator(rules, hooks); + _generator = new IntValueGenerator(_config.MinValue, _config.MaxValue); + _layout = new TreeLayoutManager(_nodePrefab, _config, _edgeContainer); + + SubscribeNavigatorEvents(); + SubscribeRhythmInput(); + + if (_beatmap != null) + _rhythmManager.LoadBeatmap(_beatmap); + + SubscribeRhythmEvents(); + + if (_visualizer != null) + _visualizer.Initialize(_rhythmManager); + + _cameraRig.Initialize(_config); + _rawInput.Enable(); + } + + private void Start() + { + PopulateSampleTree(); + if (_beatmap != null) + _rhythmManager.Play(); + } + + private void Update() + { + if (_visualizer == null) + _rhythmManager.Tick(); + + if (_visualizer != null) + _rhythmManager.NoteAppearanceBeats = _visualizer.NoteAppearanceBeats; + + TickBeatPulse(); + } + + private void OnDestroy() + { + _rawInput.Disable(); + _rhythmInput.Disable(); + } + + #endregion + + #region Wiring + + private void SubscribeNavigatorEvents() + { + _navigator.OnNavigationBegan += OnNavigationBegan; + _navigator.OnMovedLeft += OnMoved; + _navigator.OnMovedRight += OnMoved; + _navigator.OnMovedLeft += _ => _lastActionSucceeded = true; + _navigator.OnMovedRight += _ => _lastActionSucceeded = true; + _navigator.OnNodeInserted += _ => _lastActionSucceeded = true; + _navigator.OnInvalidActionAttempted += OnInvalidAction; + _navigator.OnReachedEmptySlot += context => OnReachedEmptySlotAsync(context).Forget(); + _navigator.OnNodeInserted += node => OnNodeInsertedAsync(node).Forget(); + _navigator.OnNavigationComplete += () => OnNavigationCompleteAsync().Forget(); + } + + private void SubscribeRhythmInput() + { + IRhythmInputProvider input = _rhythmInput; + input.OnLanePressed += lane => HandleLanePressed(lane).Forget(); + } + + private void SubscribeRhythmEvents() + { + _rhythmManager.OnNoteScheduled += OnNoteScheduled; + _rhythmManager.OnNoteHit += OnHit; + _rhythmManager.OnNoteMissed += OnMiss; + _rhythmManager.OnKeyPointReached += OnKeyPoint; + _rhythmManager.OnBeatPulse += OnBeatPulse; + _rhythmManager.OnSongComplete += OnSongComplete; + } + + #endregion + + #region Rhythm Input Gate + + private async UniTaskVoid HandleLanePressed(int lane) + { + NavigationAction action = LaneToAction(lane); + bool actionMatchesRequired = action == _navigator.GetRequiredAction(); + + HitResult result = _rhythmManager.TryConsumeHit(0, + actionMatchesRequired ? (HitRating?)null : HitRating.Miss); + + if (!result.IsHit) + { + if (_animationConfig != null && _animationConfig.OffBeatClip != null) + _audioSource.PlayOneShot(_animationConfig.OffBeatClip, _animationConfig.OffBeatVolume); + + _reactionPolicy.OnOffBeatInput(lane, + _rhythmManager.ScoreTracker, _rhythmManager.HealthSystem); + return; + } + + _lastActionSucceeded = false; + await _navigator.TryAction(action); + + _reactionPolicy.OnHit(result, _lastActionSucceeded, + _rhythmManager.ScoreTracker, _rhythmManager.HealthSystem); + } + + private NavigationAction LaneToAction(int lane) + { + if (_laneToAction == null || lane < 0 || lane >= _laneToAction.Length) + return NavigationAction.Left; + return _laneToAction[lane]; + } + + #endregion + + #region IRhythmGameAdapter + + /** */ + public void OnNoteScheduled(ScheduledNoteInfo info) { } + + /** */ + public void OnHit(HitResult result) { } + + /** */ + public void OnMiss(NoteData note) + { + if (_animationConfig != null && _animationConfig.MissClip != null) + _audioSource.PlayOneShot(_animationConfig.MissClip, _animationConfig.MissVolume); + } + + /** */ + public void OnKeyPoint(KeyPointData keyPoint) { } + + /** */ + public void OnBeatPulse(double beat) { } + + /** */ + public void OnSongComplete() { } + + #endregion + + #region Beat Pulse + + private void TickBeatPulse() + { + if (_animationConfig == null || _navigator.Root == null) return; + + int subdivision = Mathf.FloorToInt((float)_rhythmManager.CurrentBeat * _animationConfig.BeatPulseMultiplier); + if (subdivision == _lastPulseSubdivision) return; + + _lastPulseSubdivision = subdivision; + PulseTreeNodes(_navigator.Root); + } + + private void PulseTreeNodes(TreeNode node) + { + if (node == null) return; + + if (node.View != null && node.View != _carrierView) + node.View.PulseAsync( + _animationConfig.PulseScaleMultiplier, + _animationConfig.PulseDuration, + _animationConfig.PulseEase + ).Forget(); + + PulseTreeNodes(node.Left); + PulseTreeNodes(node.Right); + } + + #endregion + + #region Game Loop + + private void PopulateSampleTree() + { + if (_sampleTreeProvider != null && _sampleTreeProvider.Values.Count > 0) + { + foreach (int value in _sampleTreeProvider.Values) + { + TreeNode node = _navigator.InsertDirect(value); + node.View = _layout.CreateNodeView(value.ToString()); + node.View.SetState(NodeVisualState.Normal); + } + + _layout.SnapToLayout(_navigator.Root); + Vector3 rootPos = _navigator.Root.View.transform.position; + _cameraRig.SnapTo(new Vector2(rootPos.x, rootPos.y)); + } + + BeginNextInsertion(); + } + + private void BeginNextInsertion() + { + _generator.Min = _config.MinValue; + _generator.Max = _config.MaxValue; + int value = _generator.GenerateNext(); + _navigator.BeginInsertion(value).Forget(); + } + + #endregion + + #region Navigator Event Handlers + + private void OnNavigationBegan(NavigationContext context) + { + _carrierView = _layout.CreateNodeView(context.InsertValue.ToString()); + _carrierView.SetState(NodeVisualState.Carrier); + + Vector2 carrierPos = _layout.GetCarrierPosition(context); + _carrierView.SnapTo(carrierPos); + _cameraRig.SnapTo(carrierPos); + + if (!context.IsAtEmptySlot) + { + _activeComparisonNode = context.CurrentNode; + _layout.SetComparisonFocus(context.CurrentNode); + } + } + + private void OnMoved(NavigationContext context) + { + ClearActiveComparisonFocus(); + + if (context.IsAtEmptySlot) return; + + _activeComparisonNode = context.CurrentNode; + _layout.SetComparisonFocus(context.CurrentNode); + + MoveCarrierAndCamera(context); + } + + private async UniTask OnReachedEmptySlotAsync(NavigationContext context) + { + _carrierView.SetState(NodeVisualState.CarrierReady); + + Vector2 target = _layout.GetAnticipatedCarrierPosition(context); + + await UniTask.WhenAll( + _carrierView.MoveToAsync(target, _config.CarrierMoveDuration, _config.CarrierMoveEase), + _cameraRig.MoveToAsync(target), + _layout.PreviewLayout(_navigator.Root, context.Depth) + ); + } + + private void OnInvalidAction(NavigationContext context, NavigationAction attempted) + { + if (_errorCountLabel != null) + _errorCountLabel.text = $"Errors: {_navigator.ErrorCount}"; + } + + private async UniTask OnNodeInsertedAsync(TreeNode node) + { + ClearActiveComparisonFocus(); + + _carrierView.SetState(NodeVisualState.Normal); + node.View = _carrierView; + _carrierView = null; + + await UniTask.WhenAll( + _layout.RecalculateLayoutAsync(_navigator.Root), + _cameraRig.MoveToAsync(new Vector2(0f, _config.CarrierYOffset)) + ); + } + + private async UniTask OnNavigationCompleteAsync() + { + await UniTask.Delay(_config.PostPlacementDelayMs); + BeginNextInsertion(); + } + + #endregion + + #region Helpers + + private void MoveCarrierAndCamera(NavigationContext context) + { + Vector2 pos = _layout.GetCarrierPosition(context); + _carrierView.MoveToAsync(pos, _config.CarrierMoveDuration, _config.CarrierMoveEase).Forget(); + _cameraRig.MoveToAsync(pos).Forget(); + } + + private void ClearActiveComparisonFocus() + { + if (_activeComparisonNode != null) + { + _layout.ClearComparisonFocus(_activeComparisonNode); + _activeComparisonNode = null; + } + } + + #endregion +} diff --git a/Assets/Integration/TreeRhythmController.cs.meta b/Assets/Integration/TreeRhythmController.cs.meta new file mode 100644 index 0000000..d68e01c --- /dev/null +++ b/Assets/Integration/TreeRhythmController.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 0e36b33fed654fa4495553f5f9610203 \ No newline at end of file diff --git a/Assets/Integration/TreeRhythmReactionPolicy.cs b/Assets/Integration/TreeRhythmReactionPolicy.cs new file mode 100644 index 0000000..ff0935a --- /dev/null +++ b/Assets/Integration/TreeRhythmReactionPolicy.cs @@ -0,0 +1,35 @@ +/** + * for the tree-rhythm game. Combo and score + * only increase when the player is both on-beat and chose the correct BST + * direction. An on-beat wrong-direction press incurs no penalty. An expired note (miss) + * breaks combo and drains HP. + * + */ +public class TreeRhythmReactionPolicy : IRhythmReactionPolicy +{ + #region IRhythmReactionPolicy + + /** */ + public void OnHit(HitResult result, bool gameActionSucceeded, + RhythmScoreTracker tracker, RhythmHealthSystem health) + { + if (!gameActionSucceeded) return; + + tracker.RecordHit(result); + health.ApplyHit(result.Rating); + } + + /** */ + public void OnMiss(NoteData note, RhythmScoreTracker tracker, RhythmHealthSystem health) + { + tracker.RecordMiss(); + health.ApplyMiss(); + } + + /** */ + public void OnOffBeatInput(int lane, RhythmScoreTracker tracker, RhythmHealthSystem health) + { + } + + #endregion +} diff --git a/Assets/Integration/TreeRhythmReactionPolicy.cs.meta b/Assets/Integration/TreeRhythmReactionPolicy.cs.meta new file mode 100644 index 0000000..ca22e3e --- /dev/null +++ b/Assets/Integration/TreeRhythmReactionPolicy.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e1ed1cdda88c0d9409d2d58889917165 \ No newline at end of file diff --git a/Assets/Materials/Background.mat b/Assets/Materials/Background.mat new file mode 100644 index 0000000..7f7e8d0 --- /dev/null +++ b/Assets/Materials/Background.mat @@ -0,0 +1,136 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &-2578788880094833325 +MonoBehaviour: + m_ObjectHideFlags: 11 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3} + m_Name: + m_EditorClassIdentifier: + version: 10 +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Background + m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3} + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: [] + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: + RenderType: Opaque + disabledShaderPasses: + - MOTIONVECTORS + m_LockedProperties: + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BaseMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _SpecGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_Lightmaps: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_LightmapsInd: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_ShadowMasks: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Ints: [] + m_Floats: + - _AddPrecomputedVelocity: 0 + - _AlphaClip: 0 + - _AlphaToMask: 0 + - _Blend: 0 + - _BlendModePreserveSpecular: 1 + - _BumpScale: 1 + - _ClearCoatMask: 0 + - _ClearCoatSmoothness: 0 + - _Cull: 2 + - _Cutoff: 0.5 + - _DetailAlbedoMapScale: 1 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _DstBlendAlpha: 0 + - _EnvironmentReflections: 1 + - _GlossMapScale: 0 + - _Glossiness: 0 + - _GlossyReflections: 0 + - _Metallic: 0.555 + - _OcclusionStrength: 1 + - _Parallax: 0.005 + - _QueueOffset: 0 + - _ReceiveShadows: 1 + - _Smoothness: 0.104 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _SrcBlendAlpha: 1 + - _Surface: 0 + - _WorkflowMode: 1 + - _ZWrite: 1 + m_Colors: + - _BaseColor: {r: 0, g: 0.0030281835, b: 0.03773582, a: 1} + - _Color: {r: 0, g: 0.0030281835, b: 0.03773582, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1} + m_BuildTextureStacks: [] + m_AllowLocking: 1 diff --git a/Assets/Materials/Background.mat.meta b/Assets/Materials/Background.mat.meta new file mode 100644 index 0000000..bdd9e76 --- /dev/null +++ b/Assets/Materials/Background.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 9b6261c8e0b1e4ab89920f89d0bf8dc9 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/NoteHitSpot.mat b/Assets/Materials/NoteHitSpot.mat new file mode 100644 index 0000000..56781d3 --- /dev/null +++ b/Assets/Materials/NoteHitSpot.mat @@ -0,0 +1,136 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &-2578788880094833325 +MonoBehaviour: + m_ObjectHideFlags: 11 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3} + m_Name: + m_EditorClassIdentifier: + version: 10 +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: NoteHitSpot + m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3} + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: [] + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: + RenderType: Opaque + disabledShaderPasses: + - MOTIONVECTORS + m_LockedProperties: + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BaseMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _SpecGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_Lightmaps: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_LightmapsInd: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_ShadowMasks: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Ints: [] + m_Floats: + - _AddPrecomputedVelocity: 0 + - _AlphaClip: 0 + - _AlphaToMask: 0 + - _Blend: 0 + - _BlendModePreserveSpecular: 1 + - _BumpScale: 1 + - _ClearCoatMask: 0 + - _ClearCoatSmoothness: 0 + - _Cull: 2 + - _Cutoff: 0.5 + - _DetailAlbedoMapScale: 1 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _DstBlendAlpha: 0 + - _EnvironmentReflections: 1 + - _GlossMapScale: 0 + - _Glossiness: 0 + - _GlossyReflections: 0 + - _Metallic: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.005 + - _QueueOffset: 0 + - _ReceiveShadows: 1 + - _Smoothness: 0.5 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _SrcBlendAlpha: 1 + - _Surface: 0 + - _WorkflowMode: 1 + - _ZWrite: 1 + m_Colors: + - _BaseColor: {r: 1, g: 1, b: 1, a: 1} + - _Color: {r: 1, g: 1, b: 1, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1} + m_BuildTextureStacks: [] + m_AllowLocking: 1 diff --git a/Assets/Materials/NoteHitSpot.mat.meta b/Assets/Materials/NoteHitSpot.mat.meta new file mode 100644 index 0000000..4197ff1 --- /dev/null +++ b/Assets/Materials/NoteHitSpot.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 792cca3ba846a4217aef4c35be2fd6a8 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/RhythmNote.mat b/Assets/Materials/RhythmNote.mat new file mode 100644 index 0000000..63c66ae --- /dev/null +++ b/Assets/Materials/RhythmNote.mat @@ -0,0 +1,136 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &-2578788880094833325 +MonoBehaviour: + m_ObjectHideFlags: 11 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3} + m_Name: + m_EditorClassIdentifier: + version: 10 +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: RhythmNote + m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3} + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: [] + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: + RenderType: Opaque + disabledShaderPasses: + - MOTIONVECTORS + m_LockedProperties: + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BaseMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _SpecGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_Lightmaps: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_LightmapsInd: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_ShadowMasks: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Ints: [] + m_Floats: + - _AddPrecomputedVelocity: 0 + - _AlphaClip: 0 + - _AlphaToMask: 0 + - _Blend: 0 + - _BlendModePreserveSpecular: 1 + - _BumpScale: 1 + - _ClearCoatMask: 0 + - _ClearCoatSmoothness: 0 + - _Cull: 2 + - _Cutoff: 0.5 + - _DetailAlbedoMapScale: 1 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _DstBlendAlpha: 0 + - _EnvironmentReflections: 1 + - _GlossMapScale: 0 + - _Glossiness: 0 + - _GlossyReflections: 0 + - _Metallic: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.005 + - _QueueOffset: 0 + - _ReceiveShadows: 1 + - _Smoothness: 0.5 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _SrcBlendAlpha: 1 + - _Surface: 0 + - _WorkflowMode: 1 + - _ZWrite: 1 + m_Colors: + - _BaseColor: {r: 1, g: 1, b: 1, a: 1} + - _Color: {r: 1, g: 1, b: 1, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1} + m_BuildTextureStacks: [] + m_AllowLocking: 1 diff --git a/Assets/Materials/RhythmNote.mat.meta b/Assets/Materials/RhythmNote.mat.meta new file mode 100644 index 0000000..88b6ac5 --- /dev/null +++ b/Assets/Materials/RhythmNote.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4229ef19f9bfb91498e0385e8c4e9f1a +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Prefabs/Note.prefab b/Assets/Prefabs/Note.prefab new file mode 100644 index 0000000..83e247b --- /dev/null +++ b/Assets/Prefabs/Note.prefab @@ -0,0 +1,163 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &2132212214528617362 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 6036712751960075183} + - component: {fileID: 3126917581800003242} + m_Layer: 0 + m_Name: Note + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &6036712751960075183 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2132212214528617362} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 61309383839202681} + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &3126917581800003242 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2132212214528617362} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 913c914734a0019418f6d86bc7752916, type: 3} + m_Name: + m_EditorClassIdentifier: + _noteRenderer: {fileID: 7499671472802176758} + _holdTrail: {fileID: 0} + _hitParticles: {fileID: 0} + _hitPunchDuration: 0.12 + _hitPunchScale: {x: 1.5, y: 1.5, z: 1} + _missFadeDuration: 0.2 +--- !u!1 &6088083392788960613 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 61309383839202681} + - component: {fileID: 8097035401195675295} + - component: {fileID: 7499671472802176758} + - component: {fileID: 3984210916691481064} + m_Layer: 0 + m_Name: NodeBody + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &61309383839202681 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6088083392788960613} + serializedVersion: 2 + m_LocalRotation: {x: -0.7071068, y: 0, z: 0, w: 0.7071068} + m_LocalPosition: {x: 0, y: 0, z: -0.5} + m_LocalScale: {x: 0.5, y: 0.05, z: 0.5} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 6036712751960075183} + m_LocalEulerAnglesHint: {x: -90, y: 0, z: 0} +--- !u!33 &8097035401195675295 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6088083392788960613} + m_Mesh: {fileID: 10206, guid: 0000000000000000e000000000000000, type: 0} +--- !u!23 &7499671472802176758 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6088083392788960613} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 2 + m_RayTraceProcedural: 0 + m_RayTracingAccelStructBuildFlagsOverride: 0 + m_RayTracingAccelStructBuildFlags: 1 + m_SmallMeshCulling: 1 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: 4229ef19f9bfb91498e0385e8c4e9f1a, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 1 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} +--- !u!136 &3984210916691481064 +CapsuleCollider: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6088083392788960613} + m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 + m_IsTrigger: 0 + m_ProvidesContacts: 0 + m_Enabled: 0 + serializedVersion: 2 + m_Radius: 0.5000001 + m_Height: 2 + m_Direction: 1 + m_Center: {x: 0.000000059604645, y: 0, z: -0.00000008940697} diff --git a/Assets/Prefabs/Note.prefab.meta b/Assets/Prefabs/Note.prefab.meta new file mode 100644 index 0000000..fc3e107 --- /dev/null +++ b/Assets/Prefabs/Note.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: a4a524929e253024fb536d59b8c205e0 +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Rhythm.meta b/Assets/Rhythm.meta new file mode 100644 index 0000000..3fb116e --- /dev/null +++ b/Assets/Rhythm.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2dcfa4265a8c1bd478b8e0339ca249a3 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Rhythm/Core.meta b/Assets/Rhythm/Core.meta new file mode 100644 index 0000000..aa0b701 --- /dev/null +++ b/Assets/Rhythm/Core.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2b23d3bcb53bc2f45bacdf84e5a5697e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Rhythm/Core/BeatScheduler.cs b/Assets/Rhythm/Core/BeatScheduler.cs new file mode 100644 index 0000000..b1f6c2a --- /dev/null +++ b/Assets/Rhythm/Core/BeatScheduler.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; + +/** + * Scans an ordered list of and fires + * when a note's spawn threshold is crossed. + * + */ +public class BeatScheduler +{ + #region Dependencies + + /** + * How many beats before its target time a note is spawned. + * + */ + public float NoteAppearanceBeats { get; set; } + + #endregion + + #region State + + private IReadOnlyList _sortedNotes; + private int _nextScheduleIndex; + private RhythmManager _rhythmManager; + + #endregion + + #region Events + + /** + * Fired when a note has reached its spawn threshold. + * + */ + public event Action OnNoteScheduled; + + #endregion + + #region Constructor + + /** + * Creates a scheduler with the given look-ahead window. + * + * + * How many beats before its target time a note should be spawned. + * Sourced from . + * + */ + public BeatScheduler(float noteAppearanceBeats) + { + NoteAppearanceBeats = noteAppearanceBeats; + } + + #endregion + + #region Public API + + /** + * Loads a sorted note list and resets the schedule cursor to the beginning. + * + * Notes sorted ascending by beat. + * Used to convert beats to DSP arrival times. + */ + public void LoadNotes(IReadOnlyList notes, RhythmManager rhythmManager) + { + _sortedNotes = notes; + _rhythmManager = rhythmManager; + _nextScheduleIndex = 0; + } + + /** + * Moves the schedule cursor to the first note at or after the given beat. + * + * Beat position to seek to. + */ + public void SeekTo(double beat) + { + _nextScheduleIndex = 0; + if (_sortedNotes == null) return; + + double spawnThreshold = beat - NoteAppearanceBeats; + for (int i = 0; i < _sortedNotes.Count; i++) + { + if (_sortedNotes[i].Beat >= spawnThreshold) + { + _nextScheduleIndex = i; + return; + } + } + _nextScheduleIndex = _sortedNotes.Count; + } + + /** + * Advances the scheduler. Fires for every + * note whose spawn threshold is greater than . + * + * Current fractional beat position. + * DSP time when the song began playing. + */ + public void Tick(double currentBeat, double songStartDsp) + { + if (_sortedNotes == null || _rhythmManager == null) return; + + while (_nextScheduleIndex < _sortedNotes.Count) + { + NoteData note = _sortedNotes[_nextScheduleIndex]; + double spawnBeat = note.Beat - NoteAppearanceBeats; + + if (currentBeat < spawnBeat) break; + + float rate = _rhythmManager.PlaybackRate; + double spawnDsp = songStartDsp + _rhythmManager.BeatToSeconds(spawnBeat) / rate; + double hitDsp = songStartDsp + _rhythmManager.BeatToSeconds(note.Beat) / rate; + double holdEndDsp = note.NoteType == NoteType.Hold + ? songStartDsp + _rhythmManager.BeatToSeconds(note.Beat + note.HoldDuration) / rate + : 0.0; + + OnNoteScheduled?.Invoke(new ScheduledNoteInfo(note, spawnDsp, hitDsp, holdEndDsp)); + _nextScheduleIndex++; + } + } + + #endregion +} diff --git a/Assets/Rhythm/Core/BeatScheduler.cs.meta b/Assets/Rhythm/Core/BeatScheduler.cs.meta new file mode 100644 index 0000000..bff48a9 --- /dev/null +++ b/Assets/Rhythm/Core/BeatScheduler.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 08c31d875ad558e42968b8f4cf6b00a5 \ No newline at end of file diff --git a/Assets/Rhythm/Core/DefaultRhythmReactionPolicy.cs b/Assets/Rhythm/Core/DefaultRhythmReactionPolicy.cs new file mode 100644 index 0000000..69053e1 --- /dev/null +++ b/Assets/Rhythm/Core/DefaultRhythmReactionPolicy.cs @@ -0,0 +1,30 @@ +/** + * Default for standalone rhythm games. + * Increments combo on any on-beat hit regardless of game action outcome, + * breaks combo on miss, and ignores off-beat inputs. + * + */ +public class DefaultRhythmReactionPolicy : IRhythmReactionPolicy +{ + #region IRhythmReactionPolicy + + /** */ + public void OnHit(HitResult result, bool gameActionSucceeded, + RhythmScoreTracker tracker, RhythmHealthSystem health) + { + tracker.RecordHit(result); + health.ApplyHit(result.Rating); + } + + /** */ + public void OnMiss(NoteData note, RhythmScoreTracker tracker, RhythmHealthSystem health) + { + tracker.RecordMiss(); + health.ApplyMiss(); + } + + /** */ + public void OnOffBeatInput(int lane, RhythmScoreTracker tracker, RhythmHealthSystem health) { } + + #endregion +} diff --git a/Assets/Rhythm/Core/DefaultRhythmReactionPolicy.cs.meta b/Assets/Rhythm/Core/DefaultRhythmReactionPolicy.cs.meta new file mode 100644 index 0000000..56f434b --- /dev/null +++ b/Assets/Rhythm/Core/DefaultRhythmReactionPolicy.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e2a9072b8c860ea499ee9d63d461bb3c \ No newline at end of file diff --git a/Assets/Rhythm/Core/IRhythmReactionPolicy.cs b/Assets/Rhythm/Core/IRhythmReactionPolicy.cs new file mode 100644 index 0000000..f00e5b2 --- /dev/null +++ b/Assets/Rhythm/Core/IRhythmReactionPolicy.cs @@ -0,0 +1,45 @@ +/** + * Defines the consequences of rhythm events on score and health. Implement + * this interface to customise what happens when a note is hit, missed, or + * when the player inputs off-beat. Swap implementations at runtime via + * . + * + */ +public interface IRhythmReactionPolicy +{ + /** + * Called after a note window was consumed (on-beat input) and after the + * downstream game system has reported whether the game action succeeded. + * + * Timing evaluation of the hit. + * + * True when the game action that consumed this beat window produced the + * intended outcome (e.g. correct BST direction). False for valid timing + * but incorrect game action. + * + * Score tracker to update. + * Health system to update. + */ + void OnHit(HitResult result, bool gameActionSucceeded, + RhythmScoreTracker tracker, RhythmHealthSystem health); + + /** + * Called when a note passes the Ok window boundary without any input + * consuming it.. + * + * The note that was missed. + * Score tracker to update. + * Health system to update. + */ + void OnMiss(NoteData note, RhythmScoreTracker tracker, RhythmHealthSystem health); + + /** + * Called when the player inputs on a lane that has no open note window. + * Implementations may choose to penalise or ignore off-beat inputs. + * + * Lane the input was registered on. + * Score tracker to update. + * Health system to update. + */ + void OnOffBeatInput(int lane, RhythmScoreTracker tracker, RhythmHealthSystem health); +} diff --git a/Assets/Rhythm/Core/IRhythmReactionPolicy.cs.meta b/Assets/Rhythm/Core/IRhythmReactionPolicy.cs.meta new file mode 100644 index 0000000..21de8bc --- /dev/null +++ b/Assets/Rhythm/Core/IRhythmReactionPolicy.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 95fc8ae4da82610498a5b99fa3e312aa \ No newline at end of file diff --git a/Assets/Rhythm/Core/RhythmHealthSystem.cs b/Assets/Rhythm/Core/RhythmHealthSystem.cs new file mode 100644 index 0000000..12b24ad --- /dev/null +++ b/Assets/Rhythm/Core/RhythmHealthSystem.cs @@ -0,0 +1,114 @@ +using System; +using UnityEngine; + +/** + * Optional health-point system for a rhythm session. All methods are no-ops + * when is false, so disabling + * health requires zero changes at the call site. + * One instance per player for multiplayer support. + * + */ +public class RhythmHealthSystem +{ + #region Dependencies + + private readonly HitWindowConfig _config; + + #endregion + + #region Properties + + /** Current HP value clamped to [0, MaxHp]. */ + public float CurrentHp { get; private set; } + + /** Maximum HP value from configuration. */ + public float MaxHp => _config.MaxHp; + + /** True when CurrentHp has reached 0 and health is enabled. */ + public bool IsDead => _config.HealthEnabled && CurrentHp <= 0f; + + /** HP as a normalised fraction in [0, 1]. */ + public float HpPercent => _config.MaxHp > 0f ? CurrentHp / _config.MaxHp : 1f; + + #endregion + + #region Events + + /** + * Fired when HP reaches 0 for the first time in a session. + * Not re-fired on subsequent misses once dead. + * + */ + public event Action OnDeath; + + /** Fired whenever HP changes. Argument is the new HP value. */ + public event Action OnHpChanged; + + #endregion + + #region Constructor + + /** + * Creates a health system. HP starts at MaxHp. + * + * Configuration providing health parameters. + */ + public RhythmHealthSystem(HitWindowConfig config) + { + _config = config; + CurrentHp = config.HealthEnabled ? config.MaxHp : 0f; + } + + #endregion + + #region Public API + + /** + * Applies the HP delta for the given . No-op when + * health is disabled. + * + * Hit rating that determines the HP change. + */ + public void ApplyHit(HitRating rating) + { + if (!_config.HealthEnabled) return; + ChangeHp(_config.GetHpDelta(rating)); + } + + /** + * Applies the miss HP drain. No-op when health is disabled. + * + */ + public void ApplyMiss() + { + if (!_config.HealthEnabled) return; + ChangeHp(-_config.HpDrainMiss); + } + + /** + * Resets HP to MaxHp. No-op when health is disabled. + * + */ + public void Reset() + { + if (!_config.HealthEnabled) return; + CurrentHp = _config.MaxHp; + OnHpChanged?.Invoke(CurrentHp); + } + + #endregion + + #region Private Helpers + + private void ChangeHp(float delta) + { + bool wasDead = IsDead; + CurrentHp = Mathf.Clamp(CurrentHp + delta, 0f, _config.MaxHp); + OnHpChanged?.Invoke(CurrentHp); + + if (!wasDead && IsDead) + OnDeath?.Invoke(); + } + + #endregion +} diff --git a/Assets/Rhythm/Core/RhythmHealthSystem.cs.meta b/Assets/Rhythm/Core/RhythmHealthSystem.cs.meta new file mode 100644 index 0000000..7654f20 --- /dev/null +++ b/Assets/Rhythm/Core/RhythmHealthSystem.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 70b482f0b69c9ba4a8bc36cc3a658503 \ No newline at end of file diff --git a/Assets/Rhythm/Core/RhythmManager.cs b/Assets/Rhythm/Core/RhythmManager.cs new file mode 100644 index 0000000..d957147 --- /dev/null +++ b/Assets/Rhythm/Core/RhythmManager.cs @@ -0,0 +1,595 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +#region Enums + +/** + * Current playback state of the rhythm engine. + * + */ +public enum PlaybackState { Stopped, Playing, Paused } + +#endregion + +#region Active Hold + +/** + * Internal tracking record for a hold note currently being held by the player. + * + */ +internal struct ActiveHold +{ + public ScheduledNoteInfo Info; + public bool IsReleased; +} + +#endregion + +#region Rhythm Manager + +/** + * Central rhythm engine. Tracks DSP-based song time, drives + * on each frame, evaluates hit timing windows, + * and dispatches all rhythm events. + * + */ +public class RhythmManager +{ + #region Dependencies + + private readonly AudioSource _audioSource; + private readonly HitWindowConfig _hitWindows; + private readonly RhythmCalibrationConfig _calibration; + private readonly BeatScheduler _scheduler; + + #endregion + + #region State + + private BeatmapData _beatmap; + private IReadOnlyList _effectiveTimingPoints; + private double[] _segmentBoundarySecs; + + private double _songStartDsp; + private double _accumulatedPauseSec; + private double _pauseStartDsp; + + private float _playbackRate = 1f; + private PlaybackState _state; + private bool _autoplay; + + private double _lastTickBeat; + private int _nextBeatPulseIndex; + private int _nextKeyPointIndex; + + private readonly List _activeNotes = new List(); + private readonly Dictionary _activeHolds = new Dictionary(); + + private int _autoplayNoteIndex; + + #endregion + + #region Public Properties + + /** Current playback state. */ + public PlaybackState State => _state; + + /** Seconds elapsed since song start, accounting for pauses. */ + public double SongElapsedSeconds + { + get + { + if (_state != PlaybackState.Playing) return 0.0; + return (AudioSettings.dspTime - _songStartDsp - _accumulatedPauseSec) * _playbackRate; + } + } + + /** Current fractional beat position. */ + public double CurrentBeat => + _beatmap != null ? SecondsToBeat(SongElapsedSeconds) : 0.0; + + /** Per-player score tracker. */ + public RhythmScoreTracker ScoreTracker { get; private set; } + + /** Per-player health system. */ + public RhythmHealthSystem HealthSystem { get; private set; } + + /** + * How many beats ahead notes are spawned. Delegates to the internal + * so changes take effect on the next tick + * without restarting playback. Update this each frame from a live config + * reference to make runtime inspector or slider changes propagate instantly. + * + */ + public float NoteAppearanceBeats + { + get => _scheduler.NoteAppearanceBeats; + set => _scheduler.NoteAppearanceBeats = value; + } + + /** + * Playback speed multiplier. 1 = normal, 0.5 = half speed, 2 = double speed. + * Sets so the audio and beat timing remain in + * sync. When changed while playing, automatically seeks to the current beat + * so all in-flight notes are rescheduled with the correct DSP times for the + * new rate. + * + */ + public float PlaybackRate + { + get => _playbackRate; + set + { + value = Mathf.Max(0.1f, value); + if (Mathf.Approximately(_playbackRate, value)) return; + + double beatSnapshot = CurrentBeat; + _playbackRate = value; + _audioSource.pitch = _playbackRate; + + if (_state == PlaybackState.Playing) + Seek(beatSnapshot); + } + } + + #endregion + + #region Events + + /** + * Fired each time a whole-beat boundary is crossed. Argument is the beat + * index (0, 1, 2, …) that was just entered. + * + */ + public event Action OnBeatPulse; + + /** + * Fired when a note crosses its spawn threshold. + * + */ + public event Action OnNoteScheduled; + + /** + * Fired when a note expires past the Ok window without being hit. + * + */ + public event Action OnNoteMissed; + + /** + * Fired immediately after successfully consumes + * a note. The reaction policy has not yet been called at this point; the + * adapter is expected to call it after determining game action outcome. + * + */ + public event Action OnNoteHit; + + /** + * Fired when a key point beat is crossed. + * + */ + public event Action OnKeyPointReached; + + /** + * Fired when the song audio reaches its end (last note beat + buffer). + * + */ + public event Action OnSongComplete; + + /** + * Fired whenever transitions. + * + */ + public event Action OnPlaybackStateChanged; + + #endregion + + #region Constructor + + /** + * Creates a fully wired rhythm manager ready for beatmap loading. + * + * AudioSource used for DSP-scheduled playback. + * Timing window and scoring configuration. + * Per-device latency calibration offsets. + * Policy defining score/health consequences. + * How far ahead notes are spawned (beats). + */ + public RhythmManager(AudioSource audioSource, HitWindowConfig hitWindows, + RhythmCalibrationConfig calibration, IRhythmReactionPolicy reactionPolicy, + float noteAppearanceBeats = 2f) + { + _audioSource = audioSource; + _hitWindows = hitWindows; + _calibration = calibration; + _reactionPolicy = reactionPolicy; + _scheduler = new BeatScheduler(noteAppearanceBeats); + _scheduler.OnNoteScheduled += OnSchedulerNoteScheduled; + } + + #endregion + + #region Playback Control + + /** + * Loads a beatmap and prepares timing tables. Does not start playback. + * + * The beatmap to load. + */ + public void LoadBeatmap(BeatmapData beatmap) + { + _beatmap = beatmap; + _effectiveTimingPoints = BuildEffectiveTimingPoints(beatmap); + _segmentBoundarySecs = RhythmTimingMath.PrecomputeSegmentBoundaries( + _effectiveTimingPoints, beatmap.OffsetSeconds); + + int totalNotes = beatmap.Notes?.Count ?? 0; + ScoreTracker = new RhythmScoreTracker(_hitWindows, totalNotes); + HealthSystem = new RhythmHealthSystem(_hitWindows); + + _activeNotes.Clear(); + _activeHolds.Clear(); + _nextBeatPulseIndex = 0; + _nextKeyPointIndex = 0; + _autoplayNoteIndex = 0; + _lastTickBeat = 0.0; + + List sorted = new List(beatmap.Notes ?? new List()); + sorted.Sort((a, b) => a.Beat.CompareTo(b.Beat)); + _scheduler.LoadNotes(sorted, this); + } + + /** + * Starts playback from the beginning. Schedules audio via DSP for + * sample-accurate synchronisation. + * + */ + public void Play() + { + float delay = _beatmap != null ? _beatmap.StartDelaySeconds : 0f; + float buffer = _calibration != null ? _calibration.DspScheduleBufferSeconds : 0.1f; + Play(AudioSettings.dspTime + buffer + delay); + } + + /** + * Starts playback at an explicit DSP time (for synchronizing players). + * + * DSP time at which playback begins. + */ + public void Play(double scheduledDspTime) + { + if (_beatmap == null) return; + _audioSource.clip = _beatmap.SongClip; + _audioSource.PlayScheduled(scheduledDspTime); + _songStartDsp = scheduledDspTime; + _accumulatedPauseSec = 0.0; + SetState(PlaybackState.Playing); + } + + /** + * Pauses playback and preserves elapsed DSP time. + * + */ + public void Pause() + { + if (_state != PlaybackState.Playing) return; + _audioSource.Pause(); + _pauseStartDsp = AudioSettings.dspTime; + SetState(PlaybackState.Paused); + } + + /** + * Resumes from a paused state. + * + */ + public void Resume() + { + if (_state != PlaybackState.Paused) return; + _accumulatedPauseSec += AudioSettings.dspTime - _pauseStartDsp; + _audioSource.UnPause(); + SetState(PlaybackState.Playing); + } + + /** + * Stops playback and resets all runtime state. LoadBeatmap must be called + * again before the next Play. + * + */ + public void Stop() + { + _audioSource.Stop(); + _activeNotes.Clear(); + _activeHolds.Clear(); + SetState(PlaybackState.Stopped); + } + + /** + * Seeks to a beat position and restarts audio from the corresponding time. + * + * Beat position to seek to. + */ + public void Seek(double targetBeat) + { + if (_beatmap == null) return; + + double targetSec = BeatToSeconds(targetBeat); + _audioSource.time = (float)targetSec; + + double dspNow = AudioSettings.dspTime; + _songStartDsp = dspNow - targetSec / _playbackRate; + _accumulatedPauseSec = 0.0; + _lastTickBeat = targetBeat; + + _nextBeatPulseIndex = Mathf.FloorToInt((float)targetBeat); + _nextKeyPointIndex = FindKeyPointIndexAtBeat(targetBeat); + _autoplayNoteIndex = 0; + + _activeNotes.Clear(); + _scheduler.SeekTo(targetBeat); + } + + /** + * Enables or disables autoplay mode. In autoplay, notes are consumed + * at the Perfect window center automatically without player input. + * + * True to enable autoplay. + */ + public void SetAutoplay(bool enabled) + { + _autoplay = enabled; + } + + /** + * Replaces the active reaction policy at runtime. Useful for switching + * between game modes (e.g. practice vs. ranked). + * + * New policy to use. + */ + public void SetReactionPolicy(IRhythmReactionPolicy policy) + { + _reactionPolicy = policy; + } + + #endregion + + #region Tick + + /** + * Advances all rhythm state by one frame. Must be called from a + * MonoBehaviour.Update each frame while playing. + * + */ + public void Tick() + { + if (_state != PlaybackState.Playing) return; + + double currentBeat = CurrentBeat; + double dspNow = AudioSettings.dspTime; + + _scheduler.Tick(currentBeat, _songStartDsp); + + CheckAndFireBeatPulses(currentBeat); + CheckAndFireKeyPoints(currentBeat); + CheckForMissedNotes(dspNow); + + if (_autoplay) + RunAutoplay(dspNow); + + if (_beatmap != null && _beatmap.SongClip != null) + { + if (SongElapsedSeconds >= _beatmap.SongClip.length && _state == PlaybackState.Playing) + { + SetState(PlaybackState.Stopped); + OnSongComplete?.Invoke(); + } + } + + _lastTickBeat = currentBeat; + } + + #endregion + + #region Hit Evaluation + + /** + * Attempts to consume the nearest hittable note in the given lane. + * Returns a with IsHit = false when no + * note is within the Ok window (off-beat input). The reaction policy is + * NOT called here — adapters call it after determining game action outcome. + * + * Zero-based lane index from the input provider. + * Hit evaluation result. + */ + public HitResult TryConsumeHit(int lane, HitRating? ratingOverride = null) + { + double dspNow = AudioSettings.dspTime; + float calibOffset = _calibration != null ? _calibration.CalibrationOffsetSeconds : 0f; + + int bestIndex = -1; + float bestError = float.MaxValue; + + for (int i = 0; i < _activeNotes.Count; i++) + { + if (_activeNotes[i].Note.Lane != lane) continue; + + float error = (float)(dspNow - _activeNotes[i].HitDspTime + calibOffset); + float absError = Mathf.Abs(error); + + if (absError > _hitWindows.OkWindow) continue; + if (absError < bestError) + { + bestError = absError; + bestIndex = i; + } + } + + if (bestIndex < 0) + { + _reactionPolicy?.OnOffBeatInput(lane, ScoreTracker, HealthSystem); + return HitResult.Miss(lane); + } + + ScheduledNoteInfo consumed = _activeNotes[bestIndex]; + _activeNotes.RemoveAt(bestIndex); + + float rawError = (float)(dspNow - consumed.HitDspTime + calibOffset); + HitRating rating = ratingOverride ?? _hitWindows.EvaluateWindow(Mathf.Abs(rawError)); + bool isEarly = rawError < -_hitWindows.PerfectWindow; + bool isLate = rawError > _hitWindows.PerfectWindow; + + var result = new HitResult(true, rating, rawError, isEarly, isLate, + lane, consumed.Note); + OnNoteHit?.Invoke(result); + + if (consumed.Note.NoteType == NoteType.Hold) + _activeHolds[lane] = new ActiveHold { Info = consumed, IsReleased = false }; + + return result; + } + + /** + * Signals that the player released a hold note input on the given lane. + * Evaluates whether the hold was completed or broken early. + * + * Lane on which the key was released. + */ + public void NotifyHoldRelease(int lane) + { + if (!_activeHolds.TryGetValue(lane, out ActiveHold hold)) return; + + double dspNow = AudioSettings.dspTime; + bool completed = dspNow >= hold.Info.HoldEndDspTime - _hitWindows.OkWindow; + + if (!completed) + { + _reactionPolicy?.OnMiss(hold.Info.Note, ScoreTracker, HealthSystem); + OnNoteMissed?.Invoke(hold.Info.Note); + } + + _activeHolds.Remove(lane); + } + + #endregion + + #region Beat / Time Conversion + + /** + * Converts a fractional beat position to seconds from song start. + * + * Beat position to convert. + * Seconds from song start. + */ + public double BeatToSeconds(double beat) => + RhythmTimingMath.BeatToSeconds(beat, _effectiveTimingPoints, _beatmap.OffsetSeconds); + + /** + * Converts seconds from song start back to a fractional beat position. + * + * Seconds to convert. + * Fractional beat position. + */ + public double SecondsToBeat(double seconds) => + RhythmTimingMath.SecondsToBeat(seconds, _effectiveTimingPoints, + _beatmap.OffsetSeconds, _segmentBoundarySecs); + + #endregion + + #region Private Helpers + + private IRhythmReactionPolicy _reactionPolicy; + + private void SetState(PlaybackState newState) + { + _state = newState; + OnPlaybackStateChanged?.Invoke(newState); + } + + private void OnSchedulerNoteScheduled(ScheduledNoteInfo info) + { + _activeNotes.Add(info); + OnNoteScheduled?.Invoke(info); + } + + private void CheckAndFireBeatPulses(double currentBeat) + { + while (_nextBeatPulseIndex <= (int)currentBeat) + { + OnBeatPulse?.Invoke(_nextBeatPulseIndex); + _nextBeatPulseIndex++; + } + } + + private void CheckAndFireKeyPoints(double currentBeat) + { + if (_beatmap?.KeyPoints == null) return; + while (_nextKeyPointIndex < _beatmap.KeyPoints.Count && + _beatmap.KeyPoints[_nextKeyPointIndex].Beat <= currentBeat) + { + OnKeyPointReached?.Invoke(_beatmap.KeyPoints[_nextKeyPointIndex]); + _nextKeyPointIndex++; + } + } + + private void CheckForMissedNotes(double dspNow) + { + if (_hitWindows == null) return; + + float calibOffset = _calibration != null ? _calibration.CalibrationOffsetSeconds : 0f; + + for (int i = _activeNotes.Count - 1; i >= 0; i--) + { + float error = (float)(dspNow - _activeNotes[i].HitDspTime + calibOffset); + if (error > _hitWindows.OkWindow) + { + NoteData missed = _activeNotes[i].Note; + _activeNotes.RemoveAt(i); + _reactionPolicy?.OnMiss(missed, ScoreTracker, HealthSystem); + OnNoteMissed?.Invoke(missed); + } + } + } + + private void RunAutoplay(double dspNow) + { + if (_beatmap?.Notes == null) return; + for (int i = 0; i < _activeNotes.Count; i++) + { + double hitDsp = _activeNotes[i].HitDspTime; + if (dspNow >= hitDsp) + { + int lane = _activeNotes[i].Note.Lane; + HitResult result = TryConsumeHit(lane); + if (result.IsHit) + _reactionPolicy?.OnHit(result, true, ScoreTracker, HealthSystem); + break; + } + } + } + + private int FindKeyPointIndexAtBeat(double beat) + { + if (_beatmap?.KeyPoints == null) return 0; + for (int i = 0; i < _beatmap.KeyPoints.Count; i++) + { + if (_beatmap.KeyPoints[i].Beat >= beat) return i; + } + return _beatmap.KeyPoints.Count; + } + + private static IReadOnlyList BuildEffectiveTimingPoints(BeatmapData beatmap) + { + var list = new List(); + if (beatmap.TimingPoints == null || beatmap.TimingPoints.Count == 0 || + beatmap.TimingPoints[0].Beat > 0.0) + { + list.Add(new TimingPoint { Beat = 0.0, Bpm = beatmap.BaseBpm }); + } + if (beatmap.TimingPoints != null) + list.AddRange(beatmap.TimingPoints); + return list; + } + + #endregion +} + +#endregion diff --git a/Assets/Rhythm/Core/RhythmManager.cs.meta b/Assets/Rhythm/Core/RhythmManager.cs.meta new file mode 100644 index 0000000..0b54950 --- /dev/null +++ b/Assets/Rhythm/Core/RhythmManager.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 884b33d131820a74e89cc4c5980c9e2a \ No newline at end of file diff --git a/Assets/Rhythm/Core/RhythmScoreTracker.cs b/Assets/Rhythm/Core/RhythmScoreTracker.cs new file mode 100644 index 0000000..5025881 --- /dev/null +++ b/Assets/Rhythm/Core/RhythmScoreTracker.cs @@ -0,0 +1,192 @@ +using System; +using UnityEngine; + +#region Enums + +/** + * Letter grade awarded at the end of a song based on accuracy. + * Thresholds are configured in . + * + */ +public enum Grade { SS, S, A, B, C, D, F } + +#endregion + +#region Score Tracker + +/** + * Tracks score, combo, accuracy, and grade for a single player during a + * rhythm session. + * + */ +public class RhythmScoreTracker +{ + #region Dependencies + + private readonly HitWindowConfig _config; + + #endregion + + #region Score State + + /** Number of Perfect hits recorded. */ + public int PerfectCount { get; private set; } + + /** Number of Good hits recorded. */ + public int GoodCount { get; private set; } + + /** Number of Ok hits recorded. */ + public int OkCount { get; private set; } + + /** Number of Misses recorded (expired notes or off-beat inputs that break combo). */ + public int MissCount { get; private set; } + + /** Current consecutive hit streak. */ + public int Combo { get; private set; } + + /** Highest combo reached in this session. */ + public int MaxCombo { get; private set; } + + /** Total accumulated score including multiplier. */ + public long TotalScore { get; private set; } + + /** Accuracy as a value from 0 to 1 based on weighted hit counts. */ + public float Accuracy { get; private set; } + + /** Current letter grade derived from . */ + public Grade CurrentGrade { get; private set; } + + /** Total notes in the beatmap. Used for accuracy denominator. */ + public int TotalNotes { get; private set; } + + #endregion + + #region Events + + /** Fired after every or call. */ + public event Action OnScoreUpdated; + + /** Fired when a miss breaks the current combo. */ + public event Action OnComboBreak; + + #endregion + + #region Constructor + + /** + * Creates a score tracker configured for the given beatmap and windows. + * + * Scoring configuration. + * Total note count in the beatmap (for accuracy denominator). + */ + public RhythmScoreTracker(HitWindowConfig config, int totalNotes) + { + _config = config; + TotalNotes = totalNotes; + CurrentGrade = Grade.F; + } + + #endregion + + #region Public API + + /** + * Records a hit result and updates all derived statistics. + * + * The evaluated hit result. + */ + public void RecordHit(HitResult result) + { + switch (result.Rating) + { + case HitRating.Perfect: PerfectCount++; break; + case HitRating.Good: GoodCount++; break; + case HitRating.Ok: OkCount++; break; + } + + Combo++; + if (Combo > MaxCombo) MaxCombo = Combo; + + float multiplier = CalculateMultiplier(); + TotalScore += (long)(_config.GetBaseScore(result.Rating) * multiplier); + + RecalculateAccuracy(); + RecalculateGrade(); + OnScoreUpdated?.Invoke(result); + } + + /** + * Records a miss (note expired or combo-breaking input) and breaks the combo. + * + */ + public void RecordMiss() + { + MissCount++; + bool hadCombo = Combo > 0; + Combo = 0; + + RecalculateAccuracy(); + RecalculateGrade(); + + if (hadCombo) + OnComboBreak?.Invoke(); + + OnScoreUpdated?.Invoke(HitResult.Miss(0)); + } + + /** + * Resets all statistics to their initial state. + * + */ + public void Reset() + { + PerfectCount = GoodCount = OkCount = MissCount = 0; + Combo = MaxCombo = 0; + TotalScore = 0; + Accuracy = 0f; + CurrentGrade = Grade.F; + } + + #endregion + + #region Private Helpers + + private float CalculateMultiplier() + { + int steps = Combo / Mathf.Max(1, _config.ComboStepSize); + float multiplier = _config.BaseMultiplier + steps * 0.5f; + return Mathf.Min(multiplier, _config.MaxMultiplier); + } + + private void RecalculateAccuracy() + { + int judged = PerfectCount + GoodCount + OkCount + MissCount; + if (judged == 0 || TotalNotes == 0) + { + Accuracy = 0f; + return; + } + + float weightedSum = PerfectCount * _config.AccuracyWeightPerfect + + GoodCount * _config.AccuracyWeightGood + + OkCount * _config.AccuracyWeightOk + + MissCount * _config.AccuracyWeightMiss; + + Accuracy = weightedSum / TotalNotes; + } + + private void RecalculateGrade() + { + if (Accuracy >= _config.GradeSSThreshold) CurrentGrade = Grade.SS; + else if (Accuracy >= _config.GradeSThreshold) CurrentGrade = Grade.S; + else if (Accuracy >= _config.GradeAThreshold) CurrentGrade = Grade.A; + else if (Accuracy >= _config.GradeBThreshold) CurrentGrade = Grade.B; + else if (Accuracy >= _config.GradeCThreshold) CurrentGrade = Grade.C; + else if (Accuracy >= _config.GradeDThreshold) CurrentGrade = Grade.D; + else CurrentGrade = Grade.F; + } + + #endregion +} + +#endregion diff --git a/Assets/Rhythm/Core/RhythmScoreTracker.cs.meta b/Assets/Rhythm/Core/RhythmScoreTracker.cs.meta new file mode 100644 index 0000000..5ee2314 --- /dev/null +++ b/Assets/Rhythm/Core/RhythmScoreTracker.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 3f81dd90ca949be4e854dcd4bc3e9014 \ No newline at end of file diff --git a/Assets/Rhythm/Core/RhythmTimingMath.cs b/Assets/Rhythm/Core/RhythmTimingMath.cs new file mode 100644 index 0000000..de4d75c --- /dev/null +++ b/Assets/Rhythm/Core/RhythmTimingMath.cs @@ -0,0 +1,124 @@ +using System.Collections.Generic; + +/** + * Static utility class providing beat-to-seconds and seconds-to-beat + * conversions that account for multiple BPM sections. + * + */ +public static class RhythmTimingMath +{ + #region Public API + + /** + * Converts a fractional beat position to seconds elapsed from song start. + * Accumulates time across BPM segments defined by . + * OffsetSeconds is added once to the final result. + * + * + * Segment i spans [T[i].Beat, T[i+1].Beat). + * seconds += (min(queryBeat, T[i+1].Beat) − T[i].Beat) × (60.0 / T[i].Bpm) + * The last segment has no upper bound so T[n+1].Beat = queryBeat. + * OffsetSeconds shifts the entire timeline. + * + * Fractional beat position to convert. + * BPM sections sorted ascending by Beat. + * Must have at least one entry at Beat = 0. + * Silence/padding before beat 0. + * Seconds from song start corresponding to the beat. + */ + public static double BeatToSeconds(double beat, + IReadOnlyList timingPoints, float offsetSeconds) + { + double seconds = offsetSeconds; + + /* Beats before the first timing point extrapolate backward using that + point's BPM — this gives notes with spawn-beats < 0 a non-zero travel + duration instead of clamping them to offsetSeconds. */ + if (beat < timingPoints[0].Beat) + { + seconds += (beat - timingPoints[0].Beat) * (60.0 / timingPoints[0].Bpm); + return seconds; + } + + for (int i = 0; i < timingPoints.Count; i++) + { + double segStart = timingPoints[i].Beat; + if (beat <= segStart) break; + + double segEnd = (i + 1 < timingPoints.Count) + ? timingPoints[i + 1].Beat + : beat; + + double clampedEnd = (beat < segEnd) ? beat : segEnd; + seconds += (clampedEnd - segStart) * (60.0 / timingPoints[i].Bpm); + } + return seconds; + } + + /** + * Converts seconds elapsed from song start back to a fractional beat position. + * When is provided each call is O(1); + * otherwise segments are traversed linearly. + * + * Seconds from song start to convert. + * BPM sections sorted ascending by Beat. + * Must have at least one entry at Beat = 0. + * Same value passed to BeatToSeconds. + * Precomputed boundary seconds from + * . Pass null to compute on the fly. + * Fractional beat position corresponding to the seconds. + */ + public static double SecondsToBeat(double seconds, + IReadOnlyList timingPoints, float offsetSeconds, + IReadOnlyList segmentBoundarySecs = null) + { + double adjusted = seconds - offsetSeconds; + + for (int i = 0; i < timingPoints.Count; i++) + { + double segStartSec = segmentBoundarySecs != null + ? segmentBoundarySecs[i] - offsetSeconds + : BeatToSeconds(timingPoints[i].Beat, timingPoints, 0f); + + double segEndSec = (i + 1 < timingPoints.Count) + ? (segmentBoundarySecs != null + ? segmentBoundarySecs[i + 1] - offsetSeconds + : BeatToSeconds(timingPoints[i + 1].Beat, timingPoints, 0f)) + : double.PositiveInfinity; + + if (adjusted <= segEndSec) + { + double excessSec = adjusted - segStartSec; + return timingPoints[i].Beat + excessSec / (60.0 / timingPoints[i].Bpm); + } + } + + double lastSegStartSec = segmentBoundarySecs != null + ? segmentBoundarySecs[timingPoints.Count - 1] - offsetSeconds + : BeatToSeconds(timingPoints[timingPoints.Count - 1].Beat, timingPoints, 0f); + + return timingPoints[timingPoints.Count - 1].Beat + + (adjusted - lastSegStartSec) / (60.0 / timingPoints[timingPoints.Count - 1].Bpm); + } + + /** + * Precomputes the seconds value at each boundary. + * Store the returned array and pass it to to + * make that conversion O(1) at runtime. + * + * BPM sections sorted ascending by Beat. + * Same offset passed to BeatToSeconds. + * Array of length timingPoints.Count where index i is the seconds + * value at timingPoints[i].Beat. + */ + public static double[] PrecomputeSegmentBoundaries( + IReadOnlyList timingPoints, float offsetSeconds) + { + var boundaries = new double[timingPoints.Count]; + for (int i = 0; i < timingPoints.Count; i++) + boundaries[i] = BeatToSeconds(timingPoints[i].Beat, timingPoints, offsetSeconds); + return boundaries; + } + + #endregion +} diff --git a/Assets/Rhythm/Core/RhythmTimingMath.cs.meta b/Assets/Rhythm/Core/RhythmTimingMath.cs.meta new file mode 100644 index 0000000..aca150f --- /dev/null +++ b/Assets/Rhythm/Core/RhythmTimingMath.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 82e58281b4c97ca4b82fba821a87aa6f \ No newline at end of file diff --git a/Assets/Rhythm/Data.meta b/Assets/Rhythm/Data.meta new file mode 100644 index 0000000..0bcb25c --- /dev/null +++ b/Assets/Rhythm/Data.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: eaf07a0316e5b49469e4aa2d72d7d1a3 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Rhythm/Data/BeatmapData.cs b/Assets/Rhythm/Data/BeatmapData.cs new file mode 100644 index 0000000..00711e6 --- /dev/null +++ b/Assets/Rhythm/Data/BeatmapData.cs @@ -0,0 +1,166 @@ +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +/** + * ScriptableObject containing all data for a single song and its beatmap. + * Beat positions are stored as fractional beats so the data is BPM-agnostic. + * All beat-to-time conversions delegate to so + * the editor and runtime always agree. + * + */ +[CreateAssetMenu(menuName = "Rhythm/BeatmapData", fileName = "NewBeatmap")] +public class BeatmapData : ScriptableObject +{ + #region Metadata + + [Header("Metadata")] + + [Tooltip("Display name of the song shown in menus.")] + public string SongName; + + [Tooltip("Artist or composer name.")] + public string Author; + + [Tooltip("Album or source name (optional).")] + public string Album; + + [Tooltip("The AudioClip to play for this song.")] + public AudioClip SongClip; + + [Tooltip("Thumbnail image shown in the song selection screen.")] + public Sprite Thumbnail; + + [Tooltip("Start time in seconds for the song preview in selection screens.")] + public float PreviewStartTime; + + #endregion + + #region Timing + + [Header("Timing")] + + [Tooltip("Primary BPM of the song. Used for all beats when TimingPoints is empty.")] + public float BaseBpm = 120f; + + [Tooltip("Seconds of silence before beat 1. Positive values delay the first beat.")] + public float OffsetSeconds; + + [Tooltip("Extra seconds to wait after Play() is called before the audio and notes begin. " + + "Gives the player time to get ready. Does not affect note timing math.")] + public float StartDelaySeconds; + + [Tooltip("Additional BPM sections for tempo changes. Must be sorted ascending by Beat. " + + "Leave empty to use BaseBpm throughout.")] + public List TimingPoints = new List(); + + #endregion + + #region Difficulty + + [Header("Difficulty")] + + [Tooltip("Human-readable difficulty label (e.g. Easy, Normal, Hard, Expert).")] + public string DifficultyName; + + [Tooltip("Enum difficulty level. Used by SongManifest to identify this beatmap.")] + public Difficulty DifficultyLevel; + + [Tooltip("Numeric difficulty rating shown to the player (e.g. 1.0 – 10.0).")] + public float DifficultyRating; + + #endregion + + #region Notes + + [Header("Notes")] + + [Tooltip("All notes in this beatmap. The beatmap editor keeps these sorted by Beat.")] + public List Notes = new List(); + + #endregion + + #region Key Points + + [Header("Key Points")] + + [Tooltip("Tagged non-note events used for visual effects, tutorial triggers, etc.")] + public List KeyPoints = new List(); + + #endregion + + #region Public API + + /** + * Converts a fractional beat position to seconds elapsed from song start, + * accounting for all and . + * + * Fractional beat position. + * Seconds from song start. + */ + public double BeatToSeconds(double beat) => + RhythmTimingMath.BeatToSeconds(beat, GetEffectiveTimingPoints(), OffsetSeconds); + + /** + * Converts seconds elapsed from song start back to a fractional beat position. + * + * Seconds elapsed from song start. + * Fractional beat position. + */ + public double SecondsToBeat(double seconds) => + RhythmTimingMath.SecondsToBeat(seconds, GetEffectiveTimingPoints(), OffsetSeconds); + + /** + * Returns all notes whose matches the given index, + * in ascending beat order. + * + * Zero-based lane index to filter by. + */ + public IEnumerable GetNotesInLane(int lane) => + Notes.Where(n => n.Lane == lane).OrderBy(n => n.Beat); + + /** + * Returns all key points whose array contains + * the specified tag (case-sensitive), in ascending beat order. + * + * Tag to search for. + */ + public IEnumerable GetKeyPointsByTag(string tag) => + KeyPoints.Where(kp => kp.Tags != null && System.Array.IndexOf(kp.Tags, tag) >= 0) + .OrderBy(kp => kp.Beat); + + /** + * Validates that are sorted ascending by Beat. + * + * True if valid or the list is empty. + */ + public bool ValidateTimingPoints() + { + for (int i = 1; i < TimingPoints.Count; i++) + { + if (TimingPoints[i].Beat <= TimingPoints[i - 1].Beat) + return false; + } + return true; + } + + #endregion + + #region Private Helpers + + private IReadOnlyList GetEffectiveTimingPoints() + { + if (TimingPoints == null || TimingPoints.Count == 0 || + TimingPoints[0].Beat > 0.0) + { + var list = new List(); + list.Add(new TimingPoint { Beat = 0.0, Bpm = BaseBpm }); + if (TimingPoints != null) + list.AddRange(TimingPoints); + return list; + } + return TimingPoints; + } + + #endregion +} diff --git a/Assets/Rhythm/Data/BeatmapData.cs.meta b/Assets/Rhythm/Data/BeatmapData.cs.meta new file mode 100644 index 0000000..37f58a9 --- /dev/null +++ b/Assets/Rhythm/Data/BeatmapData.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 4ce5ecd1e95d4474f8092d79730c87bd \ No newline at end of file diff --git a/Assets/Rhythm/Data/Difficulty.cs b/Assets/Rhythm/Data/Difficulty.cs new file mode 100644 index 0000000..e0dfac7 --- /dev/null +++ b/Assets/Rhythm/Data/Difficulty.cs @@ -0,0 +1,13 @@ +/** + * Difficulty levels for a beatmap. Used by to + * group multiple assets under one song. + * + */ +public enum Difficulty +{ + Easy, + Normal, + Hard, + Expert, + ExpertPlus +} diff --git a/Assets/Rhythm/Data/Difficulty.cs.meta b/Assets/Rhythm/Data/Difficulty.cs.meta new file mode 100644 index 0000000..1780d1c --- /dev/null +++ b/Assets/Rhythm/Data/Difficulty.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 69ca55fb103411444a9f8b6c5d883279 \ No newline at end of file diff --git a/Assets/Rhythm/Data/HitResult.cs b/Assets/Rhythm/Data/HitResult.cs new file mode 100644 index 0000000..d44c94c --- /dev/null +++ b/Assets/Rhythm/Data/HitResult.cs @@ -0,0 +1,87 @@ +#region Enums + +/** + * The quality rating of a single note hit. + * + */ +public enum HitRating +{ + Perfect, + Good, + Ok, + Miss +} + +#endregion + +#region Hit Result + +/** + * The result of evaluating a player input against an active note + * window. + * + */ +public readonly struct HitResult +{ + #region Fields + + /** Whether the input landed within any hit window (Ok or better). */ + public readonly bool IsHit; + + /** Quality rating of the hit. */ + public readonly HitRating Rating; + + /** + * Signed timing error in seconds. Negative = hit early, positive = hit late. + * Zero for misses. + * + */ + public readonly float TimingError; + + /** True when TimingError is meaningfully negative (early hit). */ + public readonly bool IsEarly; + + /** True when TimingError is meaningfully positive (late hit). */ + public readonly bool IsLate; + + /** Zero-based lane index from which the hit was recorded. */ + public readonly int Lane; + + /** The note that was consumed by this hit. */ + public readonly NoteData Note; + + #endregion + + #region Constructor + + /** + * Creates a fully populated hit result. + * + */ + public HitResult(bool isHit, HitRating rating, float timingError, bool isEarly, bool isLate, int lane, NoteData note) + { + IsHit = isHit; + Rating = rating; + TimingError = timingError; + IsEarly = isEarly; + IsLate = isLate; + Lane = lane; + Note = note; + } + + #endregion + + #region Factories + + /** + * Returns a Miss result for an off-beat input with no note consumed. + * + * Lane the input was registered on. + */ + public static HitResult Miss(int lane) => + new HitResult(false, HitRating.Miss, 0f, false, false, lane, default); + + #endregion +} + +#endregion diff --git a/Assets/Rhythm/Data/HitResult.cs.meta b/Assets/Rhythm/Data/HitResult.cs.meta new file mode 100644 index 0000000..f84dcf2 --- /dev/null +++ b/Assets/Rhythm/Data/HitResult.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 2a6076bf4015bba48843f49b90d2db80 \ No newline at end of file diff --git a/Assets/Rhythm/Data/HitWindowConfig.cs b/Assets/Rhythm/Data/HitWindowConfig.cs new file mode 100644 index 0000000..a52fa88 --- /dev/null +++ b/Assets/Rhythm/Data/HitWindowConfig.cs @@ -0,0 +1,190 @@ +using UnityEngine; + +/** + * ScriptableObject that defines all timing windows, scoring values, grade + * thresholds, health parameters, and accuracy weights. Every judgment-related + * constant lives here so they can be tuned without recompiling. + * + */ +[CreateAssetMenu(menuName = "Rhythm/HitWindowConfig", fileName = "HitWindowConfig")] +public class HitWindowConfig : ScriptableObject +{ + #region Timing Windows + + [Header("Timing Windows (± seconds)")] + + [Tooltip("Half-width of the Perfect window in seconds. Input within ±PerfectWindow of the note's target time earns Perfect.")] + public float PerfectWindow = 0.022f; + + [Tooltip("Half-width of the Good window in seconds. Input within ±GoodWindow (outside Perfect) earns Good.")] + public float GoodWindow = 0.060f; + + [Tooltip("Half-width of the Ok window in seconds. Input within ±OkWindow (outside Good) earns Ok. Beyond this is a Miss.")] + public float OkWindow = 0.120f; + + #endregion + + #region Score Values + + [Header("Score Values")] + + [Tooltip("Base score awarded for a Perfect hit before combo multiplier.")] + public int PerfectScore = 300; + + [Tooltip("Base score awarded for a Good hit before combo multiplier.")] + public int GoodScore = 200; + + [Tooltip("Base score awarded for an Ok hit before combo multiplier.")] + public int OkScore = 100; + + [Tooltip("Base score awarded for a Miss. Typically 0.")] + public int MissScore = 0; + + #endregion + + #region Combo Multiplier + + [Header("Combo Multiplier")] + + [Tooltip("Score multiplier applied when combo is 0. Typically 1.")] + public float BaseMultiplier = 1.0f; + + [Tooltip("Maximum score multiplier attainable through combo.")] + public float MaxMultiplier = 8.0f; + + [Tooltip("Number of consecutive hits required to increment the multiplier by one step.")] + public int ComboStepSize = 10; + + #endregion + + #region Grade Thresholds + + [Header("Grade Thresholds (accuracy 0–1)")] + + [Tooltip("Minimum accuracy for SS grade (typically 1.0 = 100% perfect).")] + public float GradeSSThreshold = 1.00f; + + [Tooltip("Minimum accuracy for S grade.")] + public float GradeSThreshold = 0.97f; + + [Tooltip("Minimum accuracy for A grade.")] + public float GradeAThreshold = 0.93f; + + [Tooltip("Minimum accuracy for B grade.")] + public float GradeBThreshold = 0.85f; + + [Tooltip("Minimum accuracy for C grade.")] + public float GradeCThreshold = 0.75f; + + [Tooltip("Minimum accuracy for D grade. Below this threshold earns F.")] + public float GradeDThreshold = 0.60f; + + #endregion + + #region Health System + + [Header("Health System")] + + [Tooltip("When false all health methods are no-ops and the player cannot fail.")] + public bool HealthEnabled = false; + + [Tooltip("Maximum HP value. Current HP starts at this value.")] + public float MaxHp = 100f; + + [Tooltip("HP restored on a Perfect hit.")] + public float HpGainPerfect = 2f; + + [Tooltip("HP restored on a Good hit.")] + public float HpGainGood = 1f; + + [Tooltip("HP restored on an Ok hit.")] + public float HpGainOk = 0f; + + [Tooltip("HP lost on a Miss.")] + public float HpDrainMiss = 10f; + + #endregion + + #region Accuracy Weights + + [Header("Accuracy Weights")] + + [Tooltip("Weight of a Perfect hit in the accuracy formula (0–1). Default 1.0.")] + public float AccuracyWeightPerfect = 1.00f; + + [Tooltip("Weight of a Good hit in the accuracy formula. Default 0.67.")] + public float AccuracyWeightGood = 0.67f; + + [Tooltip("Weight of an Ok hit in the accuracy formula. Default 0.33.")] + public float AccuracyWeightOk = 0.33f; + + [Tooltip("Weight of a Miss in the accuracy formula. Default 0.")] + public float AccuracyWeightMiss = 0.00f; + + #endregion + + #region Public API + + /** + * Returns the for a given absolute timing error. + * + * Absolute value of timing error in seconds. + */ + public HitRating EvaluateWindow(float absErrorSeconds) + { + if (absErrorSeconds <= PerfectWindow) return HitRating.Perfect; + if (absErrorSeconds <= GoodWindow) return HitRating.Good; + if (absErrorSeconds <= OkWindow) return HitRating.Ok; + return HitRating.Miss; + } + + /** + * Returns the base score value for a given . + * + */ + public int GetBaseScore(HitRating rating) + { + return rating switch + { + HitRating.Perfect => PerfectScore, + HitRating.Good => GoodScore, + HitRating.Ok => OkScore, + _ => MissScore + }; + } + + /** + * Returns the accuracy weight for a given . + * + */ + public float GetAccuracyWeight(HitRating rating) + { + return rating switch + { + HitRating.Perfect => AccuracyWeightPerfect, + HitRating.Good => AccuracyWeightGood, + HitRating.Ok => AccuracyWeightOk, + _ => AccuracyWeightMiss + }; + } + + /** + * Returns the HP delta (positive = gain, negative = drain) for a given + * . Always returns 0 when + * is false. + * + */ + public float GetHpDelta(HitRating rating) + { + if (!HealthEnabled) return 0f; + return rating switch + { + HitRating.Perfect => HpGainPerfect, + HitRating.Good => HpGainGood, + HitRating.Ok => HpGainOk, + _ => -HpDrainMiss + }; + } + + #endregion +} diff --git a/Assets/Rhythm/Data/HitWindowConfig.cs.meta b/Assets/Rhythm/Data/HitWindowConfig.cs.meta new file mode 100644 index 0000000..f713f24 --- /dev/null +++ b/Assets/Rhythm/Data/HitWindowConfig.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7e494470476ae5847b3b0d9bbc5fff7e \ No newline at end of file diff --git a/Assets/Rhythm/Data/KeyPointData.cs b/Assets/Rhythm/Data/KeyPointData.cs new file mode 100644 index 0000000..d0f1fac --- /dev/null +++ b/Assets/Rhythm/Data/KeyPointData.cs @@ -0,0 +1,33 @@ +using System; +using UnityEngine; + +/** + * A tagged event at a specific beat position used for visual effects, tutorial + * triggers, or any other non-note beat-aligned event. Key points are fired + * by via OnKeyPointReached. + * + */ +[Serializable] +public struct KeyPointData +{ + /** Fractional beat position at which this event fires. */ + [Tooltip("Beat position at which this key point fires. Fractional values supported.")] + public double Beat; + + /** + * Semantic tags for filtering (e.g. "drop", "chorus", "tutorial_arrow"). + * Query via . + * + */ + [Tooltip("Semantic tags for filtering in adapters (e.g. \"drop\", \"chorus\", \"camera_shake\").")] + public string[] Tags; + + /** + * Optional freeform payload. Accepts plain text or serialized JSON for + * structured data consumed by adapters. + * + */ + [Tooltip("Optional freeform or JSON payload consumed by IRhythmGameAdapter implementations.")] + [TextArea] + public string Data; +} diff --git a/Assets/Rhythm/Data/KeyPointData.cs.meta b/Assets/Rhythm/Data/KeyPointData.cs.meta new file mode 100644 index 0000000..e64940c --- /dev/null +++ b/Assets/Rhythm/Data/KeyPointData.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 24980be3ff989554d8cb0a8b33a2016f \ No newline at end of file diff --git a/Assets/Rhythm/Data/NoteData.cs b/Assets/Rhythm/Data/NoteData.cs new file mode 100644 index 0000000..e2b0c42 --- /dev/null +++ b/Assets/Rhythm/Data/NoteData.cs @@ -0,0 +1,60 @@ +using System; +using UnityEngine; + +#region Enums + +/** + * The type of note in a beatmap. + * + */ +public enum NoteType +{ + Normal, + Hold +} + +#endregion + +#region Note Data + +/** + * A single note entry in a beatmap. Beat positions are stored as fractional + * beats (e.g. 4.5 = beat four plus a half beat) so the data is BPM-agnostic + * and survives BPM changes correctly. + * + */ +[Serializable] +public struct NoteData +{ + /** + * Beat position at which this note must be hit. Fractional values are valid. + * + */ + [Tooltip("Beat position of this note. Fractional values supported (e.g. 4.5 = beat 4 + half beat).")] + public double Beat; + + /** Zero-based lane index this note belongs to. */ + [Tooltip("Zero-based lane index. Must match a valid index in RhythmVisualConfig.LanePaths.")] + public int Lane; + + /** Type of note: Normal or Hold. */ + [Tooltip("Normal = tap; Hold = sustain for HoldDuration beats.")] + public NoteType NoteType; + + /** + * Duration of a hold note in beats. Ignored for Normal notes. + * + */ + [Tooltip("Duration in beats for Hold notes. Ignored for Normal notes.")] + public double HoldDuration; + + /** + * Stable editor-assigned GUID used for undo/redo operations in the + * beatmap editor. Not used at runtime. + * + */ + [HideInInspector] + public string EditorId; +} + +#endregion diff --git a/Assets/Rhythm/Data/NoteData.cs.meta b/Assets/Rhythm/Data/NoteData.cs.meta new file mode 100644 index 0000000..fe19708 --- /dev/null +++ b/Assets/Rhythm/Data/NoteData.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e83d686ba366dc844ade2c9617370fb8 \ No newline at end of file diff --git a/Assets/Rhythm/Data/RhythmCalibrationConfig.cs b/Assets/Rhythm/Data/RhythmCalibrationConfig.cs new file mode 100644 index 0000000..02c4e9c --- /dev/null +++ b/Assets/Rhythm/Data/RhythmCalibrationConfig.cs @@ -0,0 +1,33 @@ +using UnityEngine; + +/** + * ScriptableObject storing per-device audio and visual latency calibration + * offsets. + * + */ +[CreateAssetMenu(menuName = "Rhythm/RhythmCalibrationConfig", fileName = "RhythmCalibrationConfig")] +public class RhythmCalibrationConfig : ScriptableObject +{ + #region Offsets + + [Header("Scheduling")] + + [Tooltip("How many seconds in advance PlayScheduled is called relative to AudioSettings.dspTime. " + + "Increase if notes and audio fall out of sync on slow devices.")] + public float DspScheduleBufferSeconds = 0.1f; + + [Header("Audio Offset")] + + [Tooltip("Shift applied to all note hit window evaluations in seconds. " + + "Positive = notes arrive earlier from the player's perspective (compensates for late audio). " + + "Negative = notes arrive later.")] + public float CalibrationOffsetSeconds = 0.0f; + + [Header("Visual Offset")] + + [Tooltip("Shifts note visual position along the path independently of hit window timing. " + + "Use to compensate for display latency without affecting judgment accuracy.")] + public float VisualOffsetSeconds = 0.0f; + + #endregion +} diff --git a/Assets/Rhythm/Data/RhythmCalibrationConfig.cs.meta b/Assets/Rhythm/Data/RhythmCalibrationConfig.cs.meta new file mode 100644 index 0000000..1b6e115 --- /dev/null +++ b/Assets/Rhythm/Data/RhythmCalibrationConfig.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 3b666d4b58929be4ba0e72378442a4cc \ No newline at end of file diff --git a/Assets/Rhythm/Data/RhythmVisualConfig.cs b/Assets/Rhythm/Data/RhythmVisualConfig.cs new file mode 100644 index 0000000..c58c0c3 --- /dev/null +++ b/Assets/Rhythm/Data/RhythmVisualConfig.cs @@ -0,0 +1,134 @@ +using UnityEngine; + +/** + * ScriptableObject containing all non-scene visual settings for the rhythm + * system. + * + */ +[CreateAssetMenu(menuName = "Rhythm/RhythmVisualConfig", fileName = "RhythmVisualConfig")] +public class RhythmVisualConfig : ScriptableObject +{ + #region Spawning + + [Header("Note Spawning")] + + [Tooltip("How many beats before its target time a note should be spawned and become visible. Controls apparent note speed.")] + public float NoteAppearanceBeats = 2.0f; + + [Tooltip("Seconds after a note's hit time before the visualizer force-despawns it as a safety net. " + + "Covers edge cases where OnNoteMissed fires late or not at all.")] + public float MissCleanupGraceSeconds = 0.5f; + + [Header("Object Pooling")] + + [Tooltip("Number of Normal note views pre-allocated in the pool at startup.")] + public int NormalPoolSize = 16; + + [Tooltip("Number of Hold note views pre-allocated in the pool at startup.")] + public int HoldPoolSize = 8; + + #endregion + + #region Colors + + [Header("Note Colors")] + + [Tooltip("Default tint for Normal notes.")] + public Color NormalNoteColor = Color.white; + + [Tooltip("Default tint for Hold notes.")] + public Color HoldNoteColor = new Color(0.4f, 0.8f, 1.0f, 1f); + + [Header("Hit Flash Colors")] + + [Tooltip("Color flash shown on a Perfect hit.")] + public Color HitFlashPerfect = Color.yellow; + + [Tooltip("Color flash shown on a Good hit.")] + public Color HitFlashGood = Color.green; + + [Tooltip("Color flash shown on an Ok hit.")] + public Color HitFlashOk = Color.cyan; + + [Tooltip("Color flash shown on a Miss.")] + public Color HitFlashMiss = Color.red; + + #endregion + + #region Hit Zone + + [Header("Hit Zone")] + + [Tooltip("World-space radius of each lane's hit zone indicator sphere.")] + public float HitZoneRadius = 0.5f; + + [Tooltip("World-space center positions for each lane's hit zone. Index matches lane index.")] + public Vector3[] LaneHitZonePositions = new Vector3[0]; + + #endregion + + #region Prefabs + + [Header("Prefabs")] + + [Tooltip("Prefab instantiated for Normal notes. Must have a RhythmNoteView component.")] + public GameObject NormalNotePrefab; + + [Tooltip("Prefab instantiated for Hold notes. Must have a RhythmNoteView component.")] + public GameObject HoldNotePrefab; + + #endregion + + #region Feedback Labels + + [Header("Feedback UI Prefabs")] + + [Tooltip("Prefab for the 'Perfect!' feedback label.")] + public GameObject FeedbackPerfectPrefab; + + [Tooltip("Prefab for the 'Good' feedback label.")] + public GameObject FeedbackGoodPrefab; + + [Tooltip("Prefab for the 'Ok' feedback label.")] + public GameObject FeedbackOkPrefab; + + [Tooltip("Prefab for the 'Miss' feedback label.")] + public GameObject FeedbackMissPrefab; + + #endregion + + #region Public API + + /** + * Returns the hit flash color for the given . + * + */ + public Color GetHitFlashColor(HitRating rating) + { + return rating switch + { + HitRating.Perfect => HitFlashPerfect, + HitRating.Good => HitFlashGood, + HitRating.Ok => HitFlashOk, + _ => HitFlashMiss + }; + } + + /** + * Returns the feedback prefab for the given . + * May return null if the prefab is not assigned. + * + */ + public GameObject GetFeedbackPrefab(HitRating rating) + { + return rating switch + { + HitRating.Perfect => FeedbackPerfectPrefab, + HitRating.Good => FeedbackGoodPrefab, + HitRating.Ok => FeedbackOkPrefab, + _ => FeedbackMissPrefab + }; + } + + #endregion +} diff --git a/Assets/Rhythm/Data/RhythmVisualConfig.cs.meta b/Assets/Rhythm/Data/RhythmVisualConfig.cs.meta new file mode 100644 index 0000000..20b47bf --- /dev/null +++ b/Assets/Rhythm/Data/RhythmVisualConfig.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: fb02592c77f9e1f42a56dc9c4989ab30 \ No newline at end of file diff --git a/Assets/Rhythm/Data/ScheduledNoteInfo.cs b/Assets/Rhythm/Data/ScheduledNoteInfo.cs new file mode 100644 index 0000000..1143fa8 --- /dev/null +++ b/Assets/Rhythm/Data/ScheduledNoteInfo.cs @@ -0,0 +1,51 @@ +/** + * All data the visualizer needs to spawn and drive a note view. Produced by + * and forwarded via . + * + */ +public readonly struct ScheduledNoteInfo +{ + #region Fields + + /** The beatmap note this info was generated for. */ + public readonly NoteData Note; + + /** + * DSP time at which the note view should first become visible (spawn position). + * Computed as songStartDsp + BeatToSeconds(note.Beat - noteAppearanceBeats). + * + */ + public readonly double SpawnDspTime; + + /** + * DSP time at which the note reaches the hit zone. This is the target for + * hit window evaluation and visual arrival. + * + */ + public readonly double HitDspTime; + + /** + * DSP time at which a hold note's tail ends. Zero for Normal notes. + * Computed as songStartDsp + BeatToSeconds(note.Beat + note.HoldDuration). + * + */ + public readonly double HoldEndDspTime; + + #endregion + + #region Constructor + + /** + * Creates a scheduled note info with all timing pre-computed in DSP time. + * + */ + public ScheduledNoteInfo(NoteData note, double spawnDspTime, double hitDspTime, double holdEndDspTime) + { + Note = note; + SpawnDspTime = spawnDspTime; + HitDspTime = hitDspTime; + HoldEndDspTime = holdEndDspTime; + } + + #endregion +} diff --git a/Assets/Rhythm/Data/ScheduledNoteInfo.cs.meta b/Assets/Rhythm/Data/ScheduledNoteInfo.cs.meta new file mode 100644 index 0000000..c9e57dc --- /dev/null +++ b/Assets/Rhythm/Data/ScheduledNoteInfo.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f2efcb900017ef04aa05c179af9c17b7 \ No newline at end of file diff --git a/Assets/Rhythm/Data/SongManifest.cs b/Assets/Rhythm/Data/SongManifest.cs new file mode 100644 index 0000000..bc7f354 --- /dev/null +++ b/Assets/Rhythm/Data/SongManifest.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using UnityEngine; + +/** + * Groups multiple assets under one song, one per + * level. + * + */ +[CreateAssetMenu(menuName = "Rhythm/SongManifest", fileName = "NewSongManifest")] +public class SongManifest : ScriptableObject +{ + [Tooltip("Display name of this song shown in menus and the Beatmap Editor title.")] + public string SongName; + + [Tooltip("One entry per difficulty level. Order determines tab order in the Beatmap Editor.")] + public List Difficulties = new List(); + + /** + * Returns the for the given difficulty, + * or null if no entry with that level exists. + * + */ + public BeatmapData GetBeatmap(Difficulty level) + { + foreach (DifficultyEntry entry in Difficulties) + { + if (entry.Level == level) + return entry.Beatmap; + } + return null; + } +} + +/** + * One row in a : a difficulty level paired with + * the asset that implements it. + * + */ +[System.Serializable] +public struct DifficultyEntry +{ + [Tooltip("Difficulty level for this entry.")] + public Difficulty Level; + + [Tooltip("BeatmapData asset for this difficulty. Assign an existing asset or use " + + "Duplicate in the Beatmap Editor to generate one from another difficulty.")] + public BeatmapData Beatmap; +} diff --git a/Assets/Rhythm/Data/SongManifest.cs.meta b/Assets/Rhythm/Data/SongManifest.cs.meta new file mode 100644 index 0000000..edae86a --- /dev/null +++ b/Assets/Rhythm/Data/SongManifest.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 318b60a35bddb77409b7ec4125f99b64 \ No newline at end of file diff --git a/Assets/Rhythm/Data/TimingPoint.cs b/Assets/Rhythm/Data/TimingPoint.cs new file mode 100644 index 0000000..4eeaa93 --- /dev/null +++ b/Assets/Rhythm/Data/TimingPoint.cs @@ -0,0 +1,23 @@ +using System; +using UnityEngine; + +/** + * Defines a BPM section within a song. Timing points must be sorted ascending + * by Beat. The section from TimingPoint[i].Beat to TimingPoint[i+1].Beat uses + * TimingPoint[i].Bpm for all beat-to-seconds conversions. + * + */ +[Serializable] +public struct TimingPoint +{ + /** Beat at which this BPM section begins. */ + [Tooltip("Beat at which this BPM section begins. Must be sorted ascending across the list.")] + public double Beat; + + /** + * Beats per minute from this point forward until the next TimingPoint. + * + */ + [Tooltip("Beats per minute for this section.")] + public float Bpm; +} diff --git a/Assets/Rhythm/Data/TimingPoint.cs.meta b/Assets/Rhythm/Data/TimingPoint.cs.meta new file mode 100644 index 0000000..299fba7 --- /dev/null +++ b/Assets/Rhythm/Data/TimingPoint.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a0b5b521639ce334eb9b04a8ce58a7c0 \ No newline at end of file diff --git a/Assets/Rhythm/Editor.meta b/Assets/Rhythm/Editor.meta new file mode 100644 index 0000000..53befec --- /dev/null +++ b/Assets/Rhythm/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6355b9f25866f6c4f81dc3404bb1f635 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Rhythm/Editor/BeatmapDataEditor.cs b/Assets/Rhythm/Editor/BeatmapDataEditor.cs new file mode 100644 index 0000000..ded9e2e --- /dev/null +++ b/Assets/Rhythm/Editor/BeatmapDataEditor.cs @@ -0,0 +1,22 @@ +#if UNITY_EDITOR +using UnityEditor; +using UnityEngine; + +/** + * Custom inspector for . Adds a prominent + * "Open in Beatmap Editor" button above the default fields. + * + */ +[CustomEditor(typeof(BeatmapData))] +public class BeatmapDataEditor : Editor +{ + public override void OnInspectorGUI() + { + if (GUILayout.Button("Open in Beatmap Editor", GUILayout.Height(28f))) + BeatmapEditorWindow.OpenWithBeatmap((BeatmapData)target); + + EditorGUILayout.Space(4f); + DrawDefaultInspector(); + } +} +#endif diff --git a/Assets/Rhythm/Editor/BeatmapDataEditor.cs.meta b/Assets/Rhythm/Editor/BeatmapDataEditor.cs.meta new file mode 100644 index 0000000..1f884eb --- /dev/null +++ b/Assets/Rhythm/Editor/BeatmapDataEditor.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: fad61c040e35d7d43b1320cbc4c7ffc1 \ No newline at end of file diff --git a/Assets/Rhythm/Editor/BeatmapEditorWindow.cs b/Assets/Rhythm/Editor/BeatmapEditorWindow.cs new file mode 100644 index 0000000..4cb625c --- /dev/null +++ b/Assets/Rhythm/Editor/BeatmapEditorWindow.cs @@ -0,0 +1,892 @@ +#if UNITY_EDITOR +using System; +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; + +/** + * Unity EditorWindow for authoring assets. + * Provides a beat-based timeline, audio preview, note placement, key point + * editing, difficulty management via , and a BPM + * tap calculator. All edits go through so they + * participate in Undo/Redo. + * Open via menu: Rhythm / Beatmap Editor, or via the "Open in Beatmap Editor" + * button on any BeatmapData or SongManifest inspector. + * + */ +public class BeatmapEditorWindow : EditorWindow +{ + #region Menu Item / Static Entry Points + + [MenuItem("Rhythm/Beatmap Editor")] + public static void OpenWindow() + { + var window = GetWindow("Beatmap Editor"); + window.minSize = new Vector2(700f, 400f); + window.Show(); + } + + /** + * Opens the editor and loads the given beatmap directly, without requiring + * a . Called by the BeatmapData custom inspector. + * + */ + public static void OpenWithBeatmap(BeatmapData beatmap) + { + var window = GetWindow("Beatmap Editor"); + window.minSize = new Vector2(700f, 400f); + window._beatmap = beatmap; + window._serializedBeatmap = beatmap != null ? new SerializedObject(beatmap) : null; + window._editingKeyPointIndex = -1; + window.StopPlayback(); + window.Show(); + window.Focus(); + } + + /** + * Opens the editor with a loaded. Automatically + * activates the first difficulty tab. Called by the SongManifest custom inspector. + * + */ + public static void OpenWithManifest(SongManifest manifest) + { + var window = GetWindow("Beatmap Editor"); + window.minSize = new Vector2(700f, 400f); + window._manifest = manifest; + window._activeDifficultyIndex = -1; + if (manifest != null && manifest.Difficulties.Count > 0) + { + window._activeDifficultyIndex = 0; + BeatmapData bm = manifest.Difficulties[0].Beatmap; + window._beatmap = bm; + window._serializedBeatmap = bm != null ? new SerializedObject(bm) : null; + } + window._editingKeyPointIndex = -1; + window.StopPlayback(); + window.Show(); + window.Focus(); + } + + #endregion + + #region Constants + + private const float HeaderHeight = 80f; + private const float LaneHeight = 48f; + private const float LaneHeaderWidth = 60f; + private const float KeyPointRowHeight = 24f; + private const float PlayheadWidth = 2f; + private const float NoteWidth = 12f; + private const float MinZoom = 0.25f; + private const float MaxZoom = 8f; + + #endregion + + #region State + + private BeatmapData _beatmap; + private SerializedObject _serializedBeatmap; + private SongManifest _manifest; + private int _activeDifficultyIndex = -1; + private int _duplicateDifficultyIndex = 0; + + private bool _isPlaying; + private double _playbackStartEditorTime; + private double _scrubBeat; + + private float _zoom = 1f; + private float _scrollOffset = 0f; + private Vector2 _scrollPos; + + private bool _snapToGrid = true; + private float _snapResolution = 0.25f; + + private int _selectedLane = 0; + private bool _showKeyPoints = true; + + private readonly List _tapTimes = new List(); + private double _tappedBpm; + + private int _editingKeyPointIndex = -1; + + private static readonly float[] SnapOptions = { 1f, 0.5f, 0.25f, 0.125f, 0.0625f }; + private static readonly string[] SnapLabels = { "1/1", "1/2", "1/4", "1/8", "1/16" }; + + #endregion + + #region EditorWindow Lifecycle + + private void OnEnable() + { + EditorApplication.update += OnEditorUpdate; + titleContent = new GUIContent("Beatmap Editor"); + } + + private void OnDisable() + { + EditorApplication.update -= OnEditorUpdate; + StopPlayback(); + } + + private void OnGUI() + { + DrawToolbar(); + DrawDifficultyTabs(); + DrawMetadataAndTap(); + EditorGUILayout.Space(4f); + DrawTimeline(); + DrawKeyPointInspector(); + } + + private void OnEditorUpdate() + { + if (!_isPlaying) return; + double elapsed = EditorApplication.timeSinceStartup - _playbackStartEditorTime; + if (_beatmap != null) + _scrubBeat = _beatmap.SecondsToBeat(elapsed); + Repaint(); + } + + #endregion + + #region Toolbar + + private void DrawToolbar() + { + EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); + + EditorGUILayout.LabelField("Beatmap:", GUILayout.Width(60f)); + var newBeatmap = (BeatmapData)EditorGUILayout.ObjectField( + _beatmap, typeof(BeatmapData), false, GUILayout.Width(180f)); + if (newBeatmap != _beatmap) + { + _beatmap = newBeatmap; + _serializedBeatmap = _beatmap != null ? new SerializedObject(_beatmap) : null; + _activeDifficultyIndex = -1; + if (_manifest != null && _beatmap != null) + { + for (int i = 0; i < _manifest.Difficulties.Count; i++) + { + if (_manifest.Difficulties[i].Beatmap == _beatmap) + { + _activeDifficultyIndex = i; + break; + } + } + } + _editingKeyPointIndex = -1; + StopPlayback(); + } + + GUILayout.Space(6f); + + EditorGUILayout.LabelField("Manifest:", GUILayout.Width(58f)); + var newManifest = (SongManifest)EditorGUILayout.ObjectField( + _manifest, typeof(SongManifest), false, GUILayout.Width(150f)); + if (newManifest != _manifest) + { + _manifest = newManifest; + _activeDifficultyIndex = -1; + if (_manifest != null) + { + bool found = false; + for (int i = 0; i < _manifest.Difficulties.Count; i++) + { + if (_manifest.Difficulties[i].Beatmap == _beatmap) + { + _activeDifficultyIndex = i; + found = true; + break; + } + } + if (!found && _beatmap == null && _manifest.Difficulties.Count > 0) + { + _activeDifficultyIndex = 0; + BeatmapData bm = _manifest.Difficulties[0].Beatmap; + _beatmap = bm; + _serializedBeatmap = bm != null ? new SerializedObject(bm) : null; + } + } + } + + GUILayout.Space(8f); + + GUI.enabled = _beatmap != null; + if (GUILayout.Button(_isPlaying ? "■ Stop" : "▶ Play", EditorStyles.toolbarButton, GUILayout.Width(60f))) + { + if (_isPlaying) StopPlayback(); + else StartPlayback(); + } + GUI.enabled = true; + + GUILayout.Space(8f); + + _snapToGrid = GUILayout.Toggle(_snapToGrid, "Snap", EditorStyles.toolbarButton); + GUILayout.Space(4f); + + int currentSnapIndex = Array.IndexOf(SnapOptions, _snapResolution); + if (currentSnapIndex < 0) currentSnapIndex = 2; + int newSnapIndex = EditorGUILayout.Popup(currentSnapIndex, SnapLabels, + EditorStyles.toolbarPopup, GUILayout.Width(40f)); + _snapResolution = SnapOptions[newSnapIndex]; + + GUILayout.Space(8f); + EditorGUILayout.LabelField("Zoom:", GUILayout.Width(40f)); + _zoom = EditorGUILayout.Slider(_zoom, MinZoom, MaxZoom, GUILayout.Width(100f)); + + GUILayout.FlexibleSpace(); + + _showKeyPoints = GUILayout.Toggle(_showKeyPoints, "Key Points", EditorStyles.toolbarButton); + + EditorGUILayout.EndHorizontal(); + } + + #endregion + + #region Difficulty Tabs + + private void DrawDifficultyTabs() + { + if (_manifest == null) return; + + EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); + + for (int i = 0; i < _manifest.Difficulties.Count; i++) + { + bool isActive = i == _activeDifficultyIndex; + if (isActive) + GUI.backgroundColor = new Color(0.6f, 0.85f, 1f); + + if (GUILayout.Button(_manifest.Difficulties[i].Level.ToString(), + EditorStyles.toolbarButton, GUILayout.Width(72f))) + { + if (!isActive) + SwitchToDifficulty(i); + } + + GUI.backgroundColor = Color.white; + } + + GUILayout.Space(8f); + + var usedLevels = BuildUsedLevels(); + var available = BuildAvailableLevels(usedLevels); + + if (available.Count > 0) + { + _duplicateDifficultyIndex = Mathf.Clamp(_duplicateDifficultyIndex, 0, available.Count - 1); + string[] availNames = available.ConvertAll(d => d.ToString()).ToArray(); + + if (_beatmap != null && _activeDifficultyIndex >= 0) + { + EditorGUILayout.LabelField("Duplicate as:", GUILayout.Width(84f)); + _duplicateDifficultyIndex = EditorGUILayout.Popup( + _duplicateDifficultyIndex, availNames, + EditorStyles.toolbarPopup, GUILayout.Width(84f)); + if (GUILayout.Button("Create", EditorStyles.toolbarButton, GUILayout.Width(52f))) + DuplicateBeatmapAs(available[_duplicateDifficultyIndex]); + } + else if (_beatmap != null && _activeDifficultyIndex < 0) + { + EditorGUILayout.LabelField("Add as:", GUILayout.Width(48f)); + _duplicateDifficultyIndex = EditorGUILayout.Popup( + _duplicateDifficultyIndex, availNames, + EditorStyles.toolbarPopup, GUILayout.Width(84f)); + if (GUILayout.Button("Add", EditorStyles.toolbarButton, GUILayout.Width(40f))) + AddCurrentBeatmapToManifest(available[_duplicateDifficultyIndex]); + } + } + + GUILayout.FlexibleSpace(); + EditorGUILayout.EndHorizontal(); + } + + private HashSet BuildUsedLevels() + { + var set = new HashSet(); + if (_manifest == null) return set; + foreach (DifficultyEntry e in _manifest.Difficulties) + set.Add(e.Level); + return set; + } + + private List BuildAvailableLevels(HashSet used) + { + var list = new List(); + foreach (Difficulty d in Enum.GetValues(typeof(Difficulty))) + if (!used.Contains(d)) list.Add(d); + return list; + } + + private void SwitchToDifficulty(int index) + { + if (_manifest == null || index < 0 || index >= _manifest.Difficulties.Count) return; + _activeDifficultyIndex = index; + BeatmapData bm = _manifest.Difficulties[index].Beatmap; + _beatmap = bm; + _serializedBeatmap = bm != null ? new SerializedObject(bm) : null; + _editingKeyPointIndex = -1; + StopPlayback(); + } + + private void DuplicateBeatmapAs(Difficulty targetLevel) + { + if (_beatmap == null || _manifest == null) return; + + string sourcePath = AssetDatabase.GetAssetPath(_beatmap); + if (string.IsNullOrEmpty(sourcePath)) return; + + int lastSlash = sourcePath.LastIndexOf('/'); + string dir = lastSlash >= 0 ? sourcePath.Substring(0, lastSlash) : "Assets"; + string songName = !string.IsNullOrEmpty(_manifest.SongName) ? _manifest.SongName : "Song"; + string newPath = AssetDatabase.GenerateUniqueAssetPath( + $"{dir}/{songName}_{targetLevel}.asset"); + + AssetDatabase.CopyAsset(sourcePath, newPath); + BeatmapData copy = AssetDatabase.LoadAssetAtPath(newPath); + if (copy == null) return; + + copy.DifficultyLevel = targetLevel; + copy.DifficultyName = targetLevel.ToString(); + EditorUtility.SetDirty(copy); + + Undo.RecordObject(_manifest, "Duplicate Beatmap"); + _manifest.Difficulties.Add(new DifficultyEntry { Level = targetLevel, Beatmap = copy }); + EditorUtility.SetDirty(_manifest); + AssetDatabase.SaveAssets(); + + _activeDifficultyIndex = _manifest.Difficulties.Count - 1; + _beatmap = copy; + _serializedBeatmap = new SerializedObject(copy); + _editingKeyPointIndex = -1; + } + + private void AddCurrentBeatmapToManifest(Difficulty level) + { + if (_beatmap == null || _manifest == null) return; + + Undo.RecordObject(_manifest, "Add Beatmap to Manifest"); + _manifest.Difficulties.Add(new DifficultyEntry { Level = level, Beatmap = _beatmap }); + EditorUtility.SetDirty(_manifest); + AssetDatabase.SaveAssets(); + + _activeDifficultyIndex = _manifest.Difficulties.Count - 1; + + _beatmap.DifficultyLevel = level; + _beatmap.DifficultyName = level.ToString(); + EditorUtility.SetDirty(_beatmap); + } + + #endregion + + #region Metadata and BPM Tap + + private void DrawMetadataAndTap() + { + if (_beatmap == null || _serializedBeatmap == null) return; + + _serializedBeatmap.Update(); + + EditorGUILayout.BeginHorizontal(); + + EditorGUILayout.BeginVertical(GUILayout.Width(300f)); + EditorGUILayout.LabelField("Song Metadata", EditorStyles.boldLabel); + EditorGUILayout.PropertyField(_serializedBeatmap.FindProperty("SongName")); + EditorGUILayout.PropertyField(_serializedBeatmap.FindProperty("Author")); + EditorGUILayout.PropertyField(_serializedBeatmap.FindProperty("BaseBpm")); + EditorGUILayout.PropertyField(_serializedBeatmap.FindProperty("OffsetSeconds")); + EditorGUILayout.PropertyField(_serializedBeatmap.FindProperty("DifficultyName")); + EditorGUILayout.PropertyField(_serializedBeatmap.FindProperty("DifficultyLevel")); + EditorGUILayout.EndVertical(); + + GUILayout.Space(16f); + + EditorGUILayout.BeginVertical(GUILayout.Width(220f)); + EditorGUILayout.LabelField("BPM Tap Calculator", EditorStyles.boldLabel); + if (GUILayout.Button("Tap", GUILayout.Height(32f))) + RecordTap(); + + if (_tappedBpm > 0) + { + EditorGUILayout.LabelField($"Detected BPM: {_tappedBpm:F2}"); + if (GUILayout.Button("Apply to BaseBpm")) + ApplyTappedBpm(); + } + + if (_tapTimes.Count > 0 && GUILayout.Button("Clear Taps")) + _tapTimes.Clear(); + + EditorGUILayout.EndVertical(); + + EditorGUILayout.BeginVertical(GUILayout.Width(180f)); + EditorGUILayout.LabelField("Lanes", EditorStyles.boldLabel); + int laneCount = GetLaneCount(); + EditorGUILayout.LabelField($"Detected lanes: {laneCount}"); + EditorGUILayout.LabelField("Selected lane:"); + _selectedLane = EditorGUILayout.IntSlider(_selectedLane, 0, Mathf.Max(0, laneCount - 1)); + EditorGUILayout.EndVertical(); + + EditorGUILayout.EndHorizontal(); + + _serializedBeatmap.ApplyModifiedProperties(); + } + + #endregion + + #region Timeline + + private void DrawTimeline() + { + if (_beatmap == null) return; + + int laneCount = Mathf.Max(1, GetLaneCount()); + float timelineHeight = KeyPointRowHeight + laneCount * LaneHeight + 24f; + + Rect timelineRect = GUILayoutUtility.GetRect( + GUIContent.none, GUIStyle.none, + GUILayout.ExpandWidth(true), GUILayout.Height(timelineHeight)); + + if (timelineRect.width < 10f) return; + + HandleTimelineEvents(timelineRect); + + DrawTimelineBackground(timelineRect, laneCount); + DrawBeatGrid(timelineRect); + + if (_showKeyPoints) + DrawKeyPoints(timelineRect); + + DrawNotes(timelineRect, laneCount); + DrawLaneHeaders(timelineRect, laneCount); + DrawPlayhead(timelineRect); + } + + private void DrawTimelineBackground(Rect rect, int laneCount) + { + EditorGUI.DrawRect(rect, new Color(0.15f, 0.15f, 0.15f)); + + float keyPointRowY = rect.y; + EditorGUI.DrawRect(new Rect(rect.x, keyPointRowY, rect.width, KeyPointRowHeight), + new Color(0.12f, 0.12f, 0.18f)); + + for (int i = 0; i < laneCount; i++) + { + float y = rect.y + KeyPointRowHeight + i * LaneHeight; + Color laneColor = i % 2 == 0 + ? new Color(0.18f, 0.18f, 0.18f) + : new Color(0.20f, 0.20f, 0.20f); + EditorGUI.DrawRect(new Rect(rect.x + LaneHeaderWidth, y, rect.width - LaneHeaderWidth, LaneHeight), + laneColor); + } + } + + private void DrawBeatGrid(Rect rect) + { + if (_beatmap == null) return; + + float beatsVisible = (rect.width - LaneHeaderWidth) / PixelsPerBeat(); + double startBeat = _scrollOffset; + double endBeat = startBeat + beatsVisible + 1.0; + + double subDiv = _snapResolution; + + for (double b = Math.Floor(startBeat / subDiv) * subDiv; b <= endBeat; b += subDiv) + { + float x = BeatToPixel(b, rect); + if (x < rect.x + LaneHeaderWidth || x > rect.xMax) continue; + + bool isWholeBeat = Math.Abs(b - Math.Round(b)) < 0.001; + Color lineColor = isWholeBeat + ? new Color(0.5f, 0.5f, 0.5f, 0.9f) + : new Color(0.3f, 0.3f, 0.3f, 0.5f); + + Handles.color = lineColor; + Handles.DrawLine( + new Vector3(x, rect.y + KeyPointRowHeight), + new Vector3(x, rect.yMax)); + + if (isWholeBeat) + { + GUI.Label(new Rect(x + 2f, rect.y + KeyPointRowHeight, 40f, 14f), + $"{(int)Math.Round(b)}", EditorStyles.miniLabel); + } + } + } + + private void DrawNotes(Rect rect, int laneCount) + { + if (_beatmap?.Notes == null) return; + + for (int i = 0; i < _beatmap.Notes.Count; i++) + { + NoteData note = _beatmap.Notes[i]; + if (note.Lane >= laneCount) continue; + + float x = BeatToPixel(note.Beat, rect); + float y = rect.y + KeyPointRowHeight + note.Lane * LaneHeight + LaneHeight * 0.25f; + float h = LaneHeight * 0.5f; + + if (x < rect.x + LaneHeaderWidth || x > rect.xMax) continue; + + Color noteColor = note.NoteType == NoteType.Hold + ? new Color(0.4f, 0.8f, 1.0f) + : new Color(1f, 0.85f, 0.2f); + + Rect noteRect = new Rect(x - NoteWidth * 0.5f, y, NoteWidth, h); + + if (note.NoteType == NoteType.Hold && note.HoldDuration > 0) + { + float endX = BeatToPixel(note.Beat + note.HoldDuration, rect); + EditorGUI.DrawRect(new Rect(x, y + h * 0.35f, endX - x, h * 0.3f), + new Color(noteColor.r, noteColor.g, noteColor.b, 0.5f)); + } + + EditorGUI.DrawRect(noteRect, noteColor); + + Rect hitRect = new Rect(x - 8f, y - 4f, 16f, h + 8f); + if (Event.current.type == EventType.MouseDown && + Event.current.button == 1 && + hitRect.Contains(Event.current.mousePosition)) + { + int capturedIndex = i; + var menu = new GenericMenu(); + menu.AddItem(new GUIContent("Delete Note"), false, () => DeleteNote(capturedIndex)); + menu.ShowAsContext(); + Event.current.Use(); + } + } + } + + private void DrawKeyPoints(Rect rect) + { + if (_beatmap?.KeyPoints == null) return; + + float rowY = rect.y + 4f; + float rowH = KeyPointRowHeight - 8f; + + for (int i = 0; i < _beatmap.KeyPoints.Count; i++) + { + KeyPointData kp = _beatmap.KeyPoints[i]; + float x = BeatToPixel(kp.Beat, rect); + if (x < rect.x + LaneHeaderWidth || x > rect.xMax) continue; + + Color kpColor = _editingKeyPointIndex == i + ? new Color(1f, 0.5f, 0.1f) + : new Color(0.7f, 0.4f, 1.0f); + + Vector3[] triangle = + { + new Vector3(x, rowY + rowH), + new Vector3(x - 6f, rowY), + new Vector3(x + 6f, rowY) + }; + Handles.color = kpColor; + Handles.DrawAAConvexPolygon(triangle); + + string label = kp.Tags != null && kp.Tags.Length > 0 ? kp.Tags[0] : "kp"; + GUI.Label(new Rect(x + 7f, rowY, 80f, rowH), label, EditorStyles.miniLabel); + + Rect hitRect = new Rect(x - 8f, rowY, 16f, rowH); + if (Event.current.type == EventType.MouseDown && + Event.current.button == 0 && + hitRect.Contains(Event.current.mousePosition)) + { + _editingKeyPointIndex = i; + Event.current.Use(); + } + + if (Event.current.type == EventType.MouseDown && + Event.current.button == 1 && + hitRect.Contains(Event.current.mousePosition)) + { + int capturedIndex = i; + var menu = new GenericMenu(); + menu.AddItem(new GUIContent("Delete Key Point"), false, () => DeleteKeyPoint(capturedIndex)); + menu.ShowAsContext(); + Event.current.Use(); + } + } + } + + private void DrawLaneHeaders(Rect rect, int laneCount) + { + EditorGUI.DrawRect(new Rect(rect.x, rect.y, LaneHeaderWidth, rect.height), + new Color(0.1f, 0.1f, 0.1f)); + + for (int i = 0; i < laneCount; i++) + { + float y = rect.y + KeyPointRowHeight + i * LaneHeight; + GUI.Label(new Rect(rect.x + 4f, y + LaneHeight * 0.35f, LaneHeaderWidth - 4f, 18f), + $"Lane {i}", EditorStyles.miniLabel); + } + } + + private void DrawPlayhead(Rect rect) + { + float x = BeatToPixel(_scrubBeat, rect); + if (x >= rect.x + LaneHeaderWidth && x <= rect.xMax) + { + Handles.color = new Color(1f, 0.3f, 0.3f, 0.9f); + Handles.DrawLine( + new Vector3(x, rect.y), + new Vector3(x, rect.yMax)); + } + } + + #endregion + + #region Key Point Inspector + + private void DrawKeyPointInspector() + { + if (_editingKeyPointIndex < 0 || _beatmap == null) return; + if (_editingKeyPointIndex >= _beatmap.KeyPoints.Count) + { + _editingKeyPointIndex = -1; + return; + } + + EditorGUILayout.Space(4f); + EditorGUILayout.LabelField( + $"Key Point at beat {_beatmap.KeyPoints[_editingKeyPointIndex].Beat:F3}", + EditorStyles.boldLabel); + + _serializedBeatmap.Update(); + SerializedProperty kpList = _serializedBeatmap.FindProperty("KeyPoints"); + SerializedProperty kpProp = kpList.GetArrayElementAtIndex(_editingKeyPointIndex); + SerializedProperty tagsArr = kpProp.FindPropertyRelative("Tags"); + + if (tagsArr.arraySize == 0) + tagsArr.InsertArrayElementAtIndex(0); + + EditorGUILayout.LabelField("Event Name", EditorStyles.boldLabel); + SerializedProperty firstTag = tagsArr.GetArrayElementAtIndex(0); + firstTag.stringValue = EditorGUILayout.TextField("Name", firstTag.stringValue); + + if (tagsArr.arraySize > 1) + { + EditorGUILayout.LabelField("Additional Tags"); + for (int t = 1; t < tagsArr.arraySize; t++) + { + EditorGUILayout.BeginHorizontal(); + SerializedProperty tagProp = tagsArr.GetArrayElementAtIndex(t); + tagProp.stringValue = EditorGUILayout.TextField(tagProp.stringValue); + if (GUILayout.Button("✕", GUILayout.Width(24f))) + { + tagsArr.DeleteArrayElementAtIndex(t); + break; + } + EditorGUILayout.EndHorizontal(); + } + } + + if (GUILayout.Button("+ Tag", GUILayout.Width(60f))) + tagsArr.InsertArrayElementAtIndex(tagsArr.arraySize); + + EditorGUILayout.Space(4f); + EditorGUILayout.PropertyField(kpProp.FindPropertyRelative("Data")); + + _serializedBeatmap.ApplyModifiedProperties(); + + if (GUILayout.Button("Close Inspector", GUILayout.Width(130f))) + _editingKeyPointIndex = -1; + } + + #endregion + + #region Timeline Interaction + + private void HandleTimelineEvents(Rect timelineRect) + { + Event evt = Event.current; + + if (evt.type == EventType.ScrollWheel && timelineRect.Contains(evt.mousePosition)) + { + /* Ctrl+scroll = zoom; plain scroll = horizontal pan (0.5 beats per notch) */ + if (evt.control) + _zoom = Mathf.Clamp(_zoom - evt.delta.y * 0.1f, MinZoom, MaxZoom); + else + _scrollOffset = Mathf.Max(0f, _scrollOffset + evt.delta.y * 0.5f); + + evt.Use(); + Repaint(); + } + + if (evt.type == EventType.MouseDown && evt.button == 0 && + timelineRect.Contains(evt.mousePosition)) + { + float relX = evt.mousePosition.x - (timelineRect.x + LaneHeaderWidth); + if (relX < 0f) return; + + float clickedY = evt.mousePosition.y - (timelineRect.y + KeyPointRowHeight); + + if (evt.mousePosition.y < timelineRect.y + KeyPointRowHeight) + { + double beat = SnapBeat(PixelToBeat(evt.mousePosition.x, timelineRect)); + AddKeyPoint(beat); + evt.Use(); + return; + } + + if (clickedY < 0f) return; + + int lane = (int)(clickedY / LaneHeight); + double noteBeat = SnapBeat(PixelToBeat(evt.mousePosition.x, timelineRect)); + AddNote(noteBeat, lane); + evt.Use(); + } + + if (evt.type == EventType.MouseDrag && evt.button == 2 && + timelineRect.Contains(evt.mousePosition)) + { + _scrollOffset -= evt.delta.x / PixelsPerBeat(); + _scrollOffset = Mathf.Max(0f, _scrollOffset); + evt.Use(); + Repaint(); + } + } + + private double SnapBeat(double beat) + { + if (!_snapToGrid) return beat; + return Math.Round(beat / _snapResolution) * _snapResolution; + } + + private double PixelToBeat(float pixelX, Rect timelineRect) + { + float relX = pixelX - (timelineRect.x + LaneHeaderWidth); + return _scrollOffset + relX / PixelsPerBeat(); + } + + private float BeatToPixel(double beat, Rect timelineRect) + { + return timelineRect.x + LaneHeaderWidth + (float)((beat - _scrollOffset) * PixelsPerBeat()); + } + + private float PixelsPerBeat() => 80f * _zoom; + + private int GetLaneCount() + { + if (_beatmap?.Notes == null || _beatmap.Notes.Count == 0) return 1; + int max = 0; + foreach (var n in _beatmap.Notes) + if (n.Lane > max) max = n.Lane; + return max + 1; + } + + #endregion + + #region Note Operations + + private void AddNote(double beat, int lane) + { + if (_beatmap == null || beat < 0.0) return; + + Undo.RecordObject(_beatmap, "Add Note"); + var note = new NoteData + { + Beat = beat, + Lane = lane, + NoteType = NoteType.Normal, + HoldDuration = 0.0, + EditorId = Guid.NewGuid().ToString() + }; + _beatmap.Notes.Add(note); + _beatmap.Notes.Sort((a, b) => a.Beat.CompareTo(b.Beat)); + EditorUtility.SetDirty(_beatmap); + Repaint(); + } + + private void DeleteNote(int index) + { + if (_beatmap == null || index < 0 || index >= _beatmap.Notes.Count) return; + Undo.RecordObject(_beatmap, "Delete Note"); + _beatmap.Notes.RemoveAt(index); + EditorUtility.SetDirty(_beatmap); + Repaint(); + } + + private void AddKeyPoint(double beat) + { + if (_beatmap == null || beat < 0.0) return; + + Undo.RecordObject(_beatmap, "Add Key Point"); + var kp = new KeyPointData + { + Beat = beat, + Tags = new string[] { "event" }, + Data = "" + }; + _beatmap.KeyPoints.Add(kp); + _beatmap.KeyPoints.Sort((a, b) => a.Beat.CompareTo(b.Beat)); + EditorUtility.SetDirty(_beatmap); + Repaint(); + } + + private void DeleteKeyPoint(int index) + { + if (_beatmap == null || index < 0 || index >= _beatmap.KeyPoints.Count) return; + Undo.RecordObject(_beatmap, "Delete Key Point"); + _beatmap.KeyPoints.RemoveAt(index); + if (_editingKeyPointIndex == index) _editingKeyPointIndex = -1; + EditorUtility.SetDirty(_beatmap); + Repaint(); + } + + #endregion + + #region Audio Playback + + private void StartPlayback() + { + if (_beatmap?.SongClip == null) return; + _isPlaying = true; + _playbackStartEditorTime = EditorApplication.timeSinceStartup + - (_beatmap.BeatToSeconds(_scrubBeat)); + } + + private void StopPlayback() + { + _isPlaying = false; + } + + #endregion + + #region BPM Tap + + private void RecordTap() + { + double now = EditorApplication.timeSinceStartup; + if (_tapTimes.Count > 0 && now - _tapTimes[_tapTimes.Count - 1] > 3.0) + _tapTimes.Clear(); + + _tapTimes.Add(now); + CalculateTappedBpm(); + } + + private void CalculateTappedBpm() + { + if (_tapTimes.Count < 2) + { + _tappedBpm = 0; + return; + } + + double totalInterval = _tapTimes[_tapTimes.Count - 1] - _tapTimes[0]; + double avgInterval = totalInterval / (_tapTimes.Count - 1); + _tappedBpm = 60.0 / avgInterval; + } + + private void ApplyTappedBpm() + { + if (_beatmap == null || _tappedBpm <= 0) return; + Undo.RecordObject(_beatmap, "Apply Tapped BPM"); + _beatmap.BaseBpm = (float)Math.Round(_tappedBpm, 2); + EditorUtility.SetDirty(_beatmap); + _tapTimes.Clear(); + _tappedBpm = 0; + } + + #endregion +} +#endif diff --git a/Assets/Rhythm/Editor/BeatmapEditorWindow.cs.meta b/Assets/Rhythm/Editor/BeatmapEditorWindow.cs.meta new file mode 100644 index 0000000..9d28400 --- /dev/null +++ b/Assets/Rhythm/Editor/BeatmapEditorWindow.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 6d5f65be5a4a44f498782102b44b1638 \ No newline at end of file diff --git a/Assets/Rhythm/Editor/NotePathBehaviourEditor.cs b/Assets/Rhythm/Editor/NotePathBehaviourEditor.cs new file mode 100644 index 0000000..c116205 --- /dev/null +++ b/Assets/Rhythm/Editor/NotePathBehaviourEditor.cs @@ -0,0 +1,146 @@ +#if UNITY_EDITOR +using UnityEditor; +using UnityEngine; + +/** + * Custom inspector and SceneView editor for . + * Draws the Bezier path curve, shows draggable handles on each waypoint, and + * labels the spawn and hit zone endpoints. + * + */ +[CustomEditor(typeof(NotePathBehaviour))] +public class NotePathBehaviourEditor : Editor +{ + #region Constants + + private const int CurveSampleCount = 32; + private static readonly Color PathColor = new Color(0.2f, 0.9f, 1.0f, 1f); + private static readonly Color EasedPathColor = new Color(1.0f, 0.6f, 0.1f, 0.7f); + private static readonly Color SpawnColor = new Color(0.2f, 1.0f, 0.3f, 1f); + private static readonly Color HitZoneColor = new Color(1.0f, 0.2f, 0.2f, 1f); + private static readonly Color WaypointColor = new Color(1.0f, 1.0f, 0.3f, 1f); + private const float WaypointHandleSize = 0.15f; + private const float EndpointSphereSize = 0.12f; + + #endregion + + #region Unity Editor Lifecycle + + /** + * Draws waypoint handles and the Bezier path in the SceneView. + * + */ + public void OnSceneGUI() + { + var behaviour = (NotePathBehaviour)target; + Transform[] waypoints = behaviour.Waypoints; + + if (waypoints == null || waypoints.Length < 2) return; + + DrawWaypointHandles(behaviour, waypoints); + DrawBezierCurve(behaviour, waypoints); + DrawEasedPathSamples(behaviour); + DrawEndpointLabels(behaviour); + } + + /** + * Draws a helper button in the Inspector to collect children. + * + */ + public override void OnInspectorGUI() + { + DrawDefaultInspector(); + + EditorGUILayout.Space(); + if (GUILayout.Button("Collect Children As Waypoints")) + { + var behaviour = (NotePathBehaviour)target; + Undo.RecordObject(behaviour, "Collect Children As Waypoints"); + behaviour.CollectChildrenAsWaypoints(); + EditorUtility.SetDirty(behaviour); + } + } + + #endregion + + #region Private Helpers + + private void DrawWaypointHandles(NotePathBehaviour behaviour, Transform[] waypoints) + { + for (int i = 0; i < waypoints.Length; i++) + { + if (waypoints[i] == null) continue; + + Handles.color = WaypointColor; + float size = HandleUtility.GetHandleSize(waypoints[i].position) * WaypointHandleSize; + + EditorGUI.BeginChangeCheck(); + Vector3 newPos = Handles.FreeMoveHandle( + waypoints[i].position, + size, + Vector3.zero, + Handles.SphereHandleCap); + + if (EditorGUI.EndChangeCheck()) + { + Undo.RecordObject(waypoints[i], "Move Waypoint"); + waypoints[i].position = newPos; + } + } + } + + private void DrawBezierCurve(NotePathBehaviour behaviour, Transform[] waypoints) + { + Handles.color = PathColor; + Vector3 prev = waypoints[0].position; + for (int i = 1; i <= CurveSampleCount; i++) + { + float t = i / (float)CurveSampleCount; + Vector3 pos = behaviour.EvaluatePosition(t); + Handles.DrawLine(prev, pos); + prev = pos; + } + } + + private void DrawEasedPathSamples(NotePathBehaviour behaviour) + { + Handles.color = EasedPathColor; + const int dotCount = 16; + for (int i = 0; i <= dotCount; i++) + { + float t = i / (float)dotCount; + Vector3 pos = behaviour.EvaluatePosition(t); + float size = HandleUtility.GetHandleSize(pos) * 0.06f; + Handles.DrawSolidDisc(pos, Camera.current != null ? -Camera.current.transform.forward : Vector3.forward, size); + } + } + + private void DrawEndpointLabels(NotePathBehaviour behaviour) + { + GUIStyle spawnStyle = new GUIStyle(); + spawnStyle.normal.textColor = SpawnColor; + spawnStyle.fontStyle = FontStyle.Bold; + + GUIStyle hitStyle = new GUIStyle(); + hitStyle.normal.textColor = HitZoneColor; + hitStyle.fontStyle = FontStyle.Bold; + + Handles.Label(behaviour.SpawnPosition + Vector3.up * 0.3f, "Spawn", spawnStyle); + Handles.Label(behaviour.HitZonePosition + Vector3.up * 0.3f, "Hit Zone", hitStyle); + + Handles.color = SpawnColor; + float spawnSize = HandleUtility.GetHandleSize(behaviour.SpawnPosition) * EndpointSphereSize; + Handles.DrawSolidDisc(behaviour.SpawnPosition, + Camera.current != null ? -Camera.current.transform.forward : Vector3.forward, + spawnSize); + + Handles.color = HitZoneColor; + float hitSize = HandleUtility.GetHandleSize(behaviour.HitZonePosition) * EndpointSphereSize; + Handles.DrawSolidDisc(behaviour.HitZonePosition, + Camera.current != null ? -Camera.current.transform.forward : Vector3.forward, + hitSize); + } + + #endregion +} +#endif diff --git a/Assets/Rhythm/Editor/NotePathBehaviourEditor.cs.meta b/Assets/Rhythm/Editor/NotePathBehaviourEditor.cs.meta new file mode 100644 index 0000000..65d566f --- /dev/null +++ b/Assets/Rhythm/Editor/NotePathBehaviourEditor.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: dcf7c727336299f43b249b15434cb289 \ No newline at end of file diff --git a/Assets/Rhythm/Editor/SongManifestEditor.cs b/Assets/Rhythm/Editor/SongManifestEditor.cs new file mode 100644 index 0000000..6ed9582 --- /dev/null +++ b/Assets/Rhythm/Editor/SongManifestEditor.cs @@ -0,0 +1,23 @@ +#if UNITY_EDITOR +using UnityEditor; +using UnityEngine; + +/** + * Custom inspector for . Adds a prominent + * "Open in Beatmap Editor" button that loads the manifest and auto-activates + * its first difficulty tab. + * + */ +[CustomEditor(typeof(SongManifest))] +public class SongManifestEditor : Editor +{ + public override void OnInspectorGUI() + { + if (GUILayout.Button("Open in Beatmap Editor", GUILayout.Height(28f))) + BeatmapEditorWindow.OpenWithManifest((SongManifest)target); + + EditorGUILayout.Space(4f); + DrawDefaultInspector(); + } +} +#endif diff --git a/Assets/Rhythm/Editor/SongManifestEditor.cs.meta b/Assets/Rhythm/Editor/SongManifestEditor.cs.meta new file mode 100644 index 0000000..6a56522 --- /dev/null +++ b/Assets/Rhythm/Editor/SongManifestEditor.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 66c34fa5a92532e42ab380c35d77b9c7 \ No newline at end of file diff --git a/Assets/Rhythm/Input.meta b/Assets/Rhythm/Input.meta new file mode 100644 index 0000000..7d83f7f --- /dev/null +++ b/Assets/Rhythm/Input.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e1a0f952eed56ec4097e7d8a257fa359 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Rhythm/Input/IRhythmInputProvider.cs b/Assets/Rhythm/Input/IRhythmInputProvider.cs new file mode 100644 index 0000000..497db71 --- /dev/null +++ b/Assets/Rhythm/Input/IRhythmInputProvider.cs @@ -0,0 +1,29 @@ +using System; + +/** + * Input abstraction for lane-based rhythm input. Lane indices correspond to + * indices. + * + */ +public interface IRhythmInputProvider +{ + /** + * Fired when a player presses a lane key. + * Argument is the zero-based lane index. + * + */ + event Action OnLanePressed; + + /** + * Fired when a player releases a lane key. Used for hold note tracking. + * Argument is the zero-based lane index. + * + */ + event Action OnLaneReleased; + + /** Enables input event forwarding. */ + void Enable(); + + /** Disables input event forwarding without destroying the provider. */ + void Disable(); +} diff --git a/Assets/Rhythm/Input/IRhythmInputProvider.cs.meta b/Assets/Rhythm/Input/IRhythmInputProvider.cs.meta new file mode 100644 index 0000000..2bb24c9 --- /dev/null +++ b/Assets/Rhythm/Input/IRhythmInputProvider.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: be102ba49b6449e4bb655bec938526f4 \ No newline at end of file diff --git a/Assets/Rhythm/Input/UnityRhythmInputProvider.cs b/Assets/Rhythm/Input/UnityRhythmInputProvider.cs new file mode 100644 index 0000000..41a8805 --- /dev/null +++ b/Assets/Rhythm/Input/UnityRhythmInputProvider.cs @@ -0,0 +1,96 @@ +using System; +using UnityEngine.InputSystem; + +/** + * implementation for standalone lane-based + * rhythm games. Creates one per lane using the Unity + * Input System. Default keyboard bindings are D / F / J / K for lanes 0–3. + * Pass custom binding paths to the constructor to override. + * + */ +public class UnityRhythmInputProvider : IRhythmInputProvider +{ + #region Private Events + + private event Action _onLanePressed; + private event Action _onLaneReleased; + + #endregion + + #region IRhythmInputProvider + + /** */ + event Action IRhythmInputProvider.OnLanePressed + { + add => _onLanePressed += value; + remove => _onLanePressed -= value; + } + + /** */ + event Action IRhythmInputProvider.OnLaneReleased + { + add => _onLaneReleased += value; + remove => _onLaneReleased -= value; + } + + /** */ + public void Enable() + { + foreach (var action in _laneActions) + action.Enable(); + } + + /** */ + public void Disable() + { + foreach (var action in _laneActions) + action.Disable(); + } + + #endregion + + #region Fields + + private readonly InputAction[] _laneActions; + + private static readonly string[] DefaultBindings = + { + "/d", + "/f", + "/j", + "/k" + }; + + #endregion + + #region Constructor + + /** + * Creates input actions for each lane with default or custom key bindings. + * + * Number of lanes to create actions for. + * + * Optional per-lane Unity InputSystem binding path strings + * (e.g. "<Keyboard>/space"). Indices beyond this array fall back to + * the defaults (D / F / J / K). + * + */ + public UnityRhythmInputProvider(int laneCount = 4, string[] bindingPaths = null) + { + _laneActions = new InputAction[laneCount]; + for (int i = 0; i < laneCount; i++) + { + string binding = bindingPaths != null && i < bindingPaths.Length + ? bindingPaths[i] + : (i < DefaultBindings.Length ? DefaultBindings[i] : null); + + _laneActions[i] = new InputAction($"Lane{i}", InputActionType.Button, binding); + + int laneIndex = i; + _laneActions[i].performed += _ => _onLanePressed?.Invoke(laneIndex); + _laneActions[i].canceled += _ => _onLaneReleased?.Invoke(laneIndex); + } + } + + #endregion +} diff --git a/Assets/Rhythm/Input/UnityRhythmInputProvider.cs.meta b/Assets/Rhythm/Input/UnityRhythmInputProvider.cs.meta new file mode 100644 index 0000000..0bbaffa --- /dev/null +++ b/Assets/Rhythm/Input/UnityRhythmInputProvider.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 1815caa93d35a0744878495403912889 \ No newline at end of file diff --git a/Assets/Rhythm/Integration.meta b/Assets/Rhythm/Integration.meta new file mode 100644 index 0000000..2275b38 --- /dev/null +++ b/Assets/Rhythm/Integration.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 39972456b2068d445b71dccc940591d1 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Rhythm/Integration/IRhythmGameAdapter.cs b/Assets/Rhythm/Integration/IRhythmGameAdapter.cs new file mode 100644 index 0000000..9fe9995 --- /dev/null +++ b/Assets/Rhythm/Integration/IRhythmGameAdapter.cs @@ -0,0 +1,54 @@ +/** + * Portability contract for any game system that wants to receive rhythm + * events without depending directly on . + * + */ +public interface IRhythmGameAdapter +{ + /** + * A note has crossed its spawn threshold and is heading toward the hit zone. + * Use this to prepare game-side state before the player must act. + * + * DSP timing and note data for the spawned note. + */ + void OnNoteScheduled(ScheduledNoteInfo info); + + /** + * A note was hit by the player. Rating and timing error are in + * . + * + * Full hit evaluation result. + */ + void OnHit(HitResult result); + + /** + * A note expired past the Ok window without being hit. + * + * The note that was missed. + */ + void OnMiss(NoteData note); + + /** + * A tagged key point beat was crossed. Inspect + * to determine the event type and + * for structured payload. + * + * The key point that was reached. + */ + void OnKeyPoint(KeyPointData keyPoint); + + /** + * A whole beat boundary was crossed. Useful for visual pulses, + * environment beat-sync effects, and metronome feedback. + * + * Beat index (0, 1, 2, …) that was just entered. + */ + void OnBeatPulse(double beat); + + /** + * Song playback reached its end. Perform result screen transitions or + * cleanup here. + * + */ + void OnSongComplete(); +} diff --git a/Assets/Rhythm/Integration/IRhythmGameAdapter.cs.meta b/Assets/Rhythm/Integration/IRhythmGameAdapter.cs.meta new file mode 100644 index 0000000..30eb7a7 --- /dev/null +++ b/Assets/Rhythm/Integration/IRhythmGameAdapter.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 4d35d623502a1b74aaca48b8ade77d2a \ No newline at end of file diff --git a/Assets/Rhythm/Visual.meta b/Assets/Rhythm/Visual.meta new file mode 100644 index 0000000..8adc506 --- /dev/null +++ b/Assets/Rhythm/Visual.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 80805a8056a52d040b759c09349844bb +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Rhythm/Visual/DefaultVisualRhythmHooks.cs b/Assets/Rhythm/Visual/DefaultVisualRhythmHooks.cs new file mode 100644 index 0000000..a4106a6 --- /dev/null +++ b/Assets/Rhythm/Visual/DefaultVisualRhythmHooks.cs @@ -0,0 +1,27 @@ +using Cysharp.Threading.Tasks; + +/** + * No-op base implementation of . Every method + * returns immediately. + * + */ +public class DefaultVisualRhythmHooks : IVisualRhythmHooks +{ + /** */ + public virtual UniTask OnNoteSpawned(IRhythmNoteView view) => UniTask.CompletedTask; + + /** */ + public virtual UniTask OnNoteUpdated(IRhythmNoteView view, float headT) => UniTask.CompletedTask; + + /** */ + public virtual UniTask OnNoteHit(IRhythmNoteView view, HitResult result) => UniTask.CompletedTask; + + /** */ + public virtual UniTask OnNoteMissed(IRhythmNoteView view, NoteData note) => UniTask.CompletedTask; + + /** */ + public virtual UniTask OnBeatPulse(double beat) => UniTask.CompletedTask; + + /** */ + public virtual UniTask OnKeyPointReached(KeyPointData keyPoint) => UniTask.CompletedTask; +} diff --git a/Assets/Rhythm/Visual/DefaultVisualRhythmHooks.cs.meta b/Assets/Rhythm/Visual/DefaultVisualRhythmHooks.cs.meta new file mode 100644 index 0000000..210c1c5 --- /dev/null +++ b/Assets/Rhythm/Visual/DefaultVisualRhythmHooks.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 3588d4b877963e54da8874cada6f68a6 \ No newline at end of file diff --git a/Assets/Rhythm/Visual/IRhythmNoteView.cs b/Assets/Rhythm/Visual/IRhythmNoteView.cs new file mode 100644 index 0000000..41771fe --- /dev/null +++ b/Assets/Rhythm/Visual/IRhythmNoteView.cs @@ -0,0 +1,57 @@ +using Cysharp.Threading.Tasks; + +/** + * Contract for any MonoBehaviour that represents a note on screen. Swap + * implementations to change note visual style without touching + * . + * + */ +public interface IRhythmNoteView +{ + /** Zero-based lane index this note belongs to. */ + int Lane { get; } + + /** The scheduled info this view was initialised with. */ + ScheduledNoteInfo Info { get; } + + /** + * Called once immediately after the view is taken from the pool. Sets up + * the path, colours, and initial state. + * + * DSP timing and note data. + * Path behaviour for this lane. + * Visual configuration asset. + */ + void Initialize(ScheduledNoteInfo info, NotePathBehaviour path, RhythmVisualConfig config); + + /** + * Called every frame while the note is active. Move the note head to + * path.EvaluatePosition(headT) and stretch hold tail if applicable. + * + * Normalised progress of the note head [0, 1]. 0 = spawn, 1 = hit zone. + * Normalised progress of the hold tail end. Equal to headT for Normal notes. + */ + void UpdateProgress(float headT, float tailT); + + /** + * Plays the hit feedback animation for the given rating. The note view is + * typically returned to the pool after this UniTask completes. + * + * Rating that determines the flash colour. + */ + UniTask PlayHitEffect(HitRating rating); + + /** + * Plays the miss animation (fade out, fall off screen, etc.) then calls + * automatically when complete. + * + */ + UniTask PlayMissEffect(); + + /** + * Returns this view to the pool or destroys it. Called by the visualizer + * controller when a note is finished. + * + */ + void Despawn(); +} diff --git a/Assets/Rhythm/Visual/IRhythmNoteView.cs.meta b/Assets/Rhythm/Visual/IRhythmNoteView.cs.meta new file mode 100644 index 0000000..7caf0d2 --- /dev/null +++ b/Assets/Rhythm/Visual/IRhythmNoteView.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 66d4729ad3f454a45b18e3c1bef1c7a3 \ No newline at end of file diff --git a/Assets/Rhythm/Visual/IVisualRhythmHooks.cs b/Assets/Rhythm/Visual/IVisualRhythmHooks.cs new file mode 100644 index 0000000..5c08aa4 --- /dev/null +++ b/Assets/Rhythm/Visual/IVisualRhythmHooks.cs @@ -0,0 +1,59 @@ +using Cysharp.Threading.Tasks; + +/** + * Async hook interface for injecting visual and audio effects into the rhythm + * visualizer flow. + * + */ +public interface IVisualRhythmHooks +{ + /** + * Called when a note view is spawned and positioned at its spawn point. + * Await to delay further processing (e.g. play a spawn animation). + * + * The note view that was spawned. + */ + UniTask OnNoteSpawned(IRhythmNoteView view); + + /** + * Called every frame while a note is active, before + * is called. + * Keep implementations lightweight — this fires every frame per active note. + * + * The active note view. + * Current normalised progress [0, 1] of the note head. + */ + UniTask OnNoteUpdated(IRhythmNoteView view, float headT); + + /** + * Called when a note is hit by the player, before the note view's own hit + * effect runs. Await to sequence effects. + * + * The note view that was hit. + * The full hit result including rating and timing error. + */ + UniTask OnNoteHit(IRhythmNoteView view, HitResult result); + + /** + * Called when a note expires without being hit. + * + * The note view that was missed. + * The note data of the missed note. + */ + UniTask OnNoteMissed(IRhythmNoteView view, NoteData note); + + /** + * Called each time a whole-beat boundary is crossed. + * + * The beat index that was just entered. + */ + UniTask OnBeatPulse(double beat); + + /** + * Called when a tagged key point beat is crossed. + * Inspect to determine the event type. + * + * The key point that was reached. + */ + UniTask OnKeyPointReached(KeyPointData keyPoint); +} diff --git a/Assets/Rhythm/Visual/IVisualRhythmHooks.cs.meta b/Assets/Rhythm/Visual/IVisualRhythmHooks.cs.meta new file mode 100644 index 0000000..c72ee35 --- /dev/null +++ b/Assets/Rhythm/Visual/IVisualRhythmHooks.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 6348a894b5398e549b444dfc2519a634 \ No newline at end of file diff --git a/Assets/Rhythm/Visual/NotePathBehaviour.cs b/Assets/Rhythm/Visual/NotePathBehaviour.cs new file mode 100644 index 0000000..dfdfdb5 --- /dev/null +++ b/Assets/Rhythm/Visual/NotePathBehaviour.cs @@ -0,0 +1,145 @@ +using UnityEngine; + +/** + * Defines the 3D path a note travels from its spawn position to the hit zone. + * Control points are child objects so they can be + * dragged directly in the Scene view. The path is evaluated via De Casteljau + * cubic Bezier with an remapping progress before + * sampling, decoupling path shape from movement velocity profile. + * + */ +public class NotePathBehaviour : MonoBehaviour +{ + #region Inspector Fields + + [Tooltip("Control point transforms. First = spawn position, last = hit zone. " + + "Use 4 points for a cubic Bezier. Drag these in the Scene view to adjust the path.")] + [SerializeField] private Transform[] _waypoints; + + [Tooltip("Remaps linear progress t (0–1) before Bezier sampling. " + + "X axis = linear t, Y axis = remapped t. Default is linear (no remap).")] + [SerializeField] private AnimationCurve _easeCurve = AnimationCurve.Linear(0f, 0f, 1f, 1f); + + [Tooltip("Draw the path curve in play mode for debugging.")] + [SerializeField] private bool _drawGizmosInPlayMode; + + #endregion + + #region Public Properties + + /** World position of the spawn point (t = 0). */ + public Vector3 SpawnPosition => EvaluatePosition(0f); + + /** World position of the hit zone (t = 1). */ + public Vector3 HitZonePosition => EvaluatePosition(1f); + + /** The configured waypoint transforms. */ + public Transform[] Waypoints => _waypoints; + + #endregion + + #region Public API + + /** + * Evaluates the world-space position at normalised progress . + * The ease curve is applied to before sampling the + * Bezier, so path shape and velocity profile are independently controllable. + * + * Linear progress in [0, 1]. 0 = spawn, 1 = hit zone. + * World-space Vector3 position at the given progress. + */ + public Vector3 EvaluatePosition(float t) + { + if (_waypoints == null || _waypoints.Length == 0) return transform.position; + + float easedT = _easeCurve.Evaluate(Mathf.Clamp01(t)); + Vector3[] positions = GetWaypointPositions(); + return EvaluateBezier(positions, easedT); + } + + #endregion + + #region Context Menu Helpers + + /** + * Populates from all direct child transforms in + * hierarchy order. Invoke from the component context menu in the Inspector. + * + */ + [ContextMenu("Collect Children As Waypoints")] + public void CollectChildrenAsWaypoints() + { + _waypoints = new Transform[transform.childCount]; + for (int i = 0; i < transform.childCount; i++) + _waypoints[i] = transform.GetChild(i); + } + + #endregion + + #region Gizmos + + private void OnDrawGizmosSelected() + { + DrawPathGizmo(); + } + + private void OnDrawGizmos() + { + if (_drawGizmosInPlayMode && Application.isPlaying) + DrawPathGizmo(); + } + + private void DrawPathGizmo() + { + if (_waypoints == null || _waypoints.Length < 2) return; + + Vector3[] positions = GetWaypointPositions(); + + Gizmos.color = Color.cyan; + const int segments = 24; + Vector3 prev = EvaluateBezier(positions, 0f); + for (int i = 1; i <= segments; i++) + { + float t = i / (float)segments; + Vector3 next = EvaluateBezier(positions, t); + Gizmos.DrawLine(prev, next); + prev = next; + } + + Gizmos.color = Color.green; + Gizmos.DrawSphere(positions[0], 0.1f); + + Gizmos.color = Color.red; + Gizmos.DrawSphere(positions[positions.Length - 1], 0.1f); + } + + #endregion + + #region Private Helpers + + private Vector3[] GetWaypointPositions() + { + var positions = new Vector3[_waypoints.Length]; + for (int i = 0; i < _waypoints.Length; i++) + positions[i] = _waypoints[i] != null ? _waypoints[i].position : transform.position; + return positions; + } + + /** + * De Casteljau algorithm: repeatedly lerps between adjacent points until + * one point remains. Works for any number of control points (linear, + * quadratic, cubic, higher-order). + * + */ + private static Vector3 EvaluateBezier(Vector3[] pts, float t) + { + Vector3[] work = (Vector3[])pts.Clone(); + int n = work.Length - 1; + for (int r = 1; r <= n; r++) + for (int i = 0; i <= n - r; i++) + work[i] = Vector3.LerpUnclamped(work[i], work[i + 1], t); + return work[0]; + } + + #endregion +} diff --git a/Assets/Rhythm/Visual/NotePathBehaviour.cs.meta b/Assets/Rhythm/Visual/NotePathBehaviour.cs.meta new file mode 100644 index 0000000..ca46d9a --- /dev/null +++ b/Assets/Rhythm/Visual/NotePathBehaviour.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 84f4dc4f60fba7e4db3098b36887c32b \ No newline at end of file diff --git a/Assets/Rhythm/Visual/RhythmNoteView.cs b/Assets/Rhythm/Visual/RhythmNoteView.cs new file mode 100644 index 0000000..23c3cdc --- /dev/null +++ b/Assets/Rhythm/Visual/RhythmNoteView.cs @@ -0,0 +1,158 @@ +using Cysharp.Threading.Tasks; +using PrimeTween; +using UnityEngine; + +/** + * Concrete MonoBehaviour implementation of . + * + */ +public class RhythmNoteView : MonoBehaviour, IRhythmNoteView +{ + #region Inspector Fields + + [Tooltip("Renderer for the note head (MeshRenderer, SkinnedMeshRenderer, etc.). " + + "Leave null if color is handled by child objects or materials directly.")] + [SerializeField] private Renderer _noteRenderer; + + [Tooltip("LineRenderer that draws the hold trail between head and tail positions. " + + "Leave null for Normal note prefabs or if no trail is desired.")] + [SerializeField] private LineRenderer _holdTrail; + + [Tooltip("Particle system burst played on hit. Leave null to skip.")] + [SerializeField] private ParticleSystem _hitParticles; + + [Tooltip("Duration of the hit scale punch animation in seconds.")] + [SerializeField] private float _hitPunchDuration = 0.12f; + + [Tooltip("Peak scale during the hit punch animation.")] + [SerializeField] private Vector3 _hitPunchScale = new Vector3(1.5f, 1.5f, 1.5f); + + [Tooltip("Duration of the miss shrink-to-zero animation in seconds.")] + [SerializeField] private float _missFadeDuration = 0.2f; + + #endregion + + #region Private State + + private ScheduledNoteInfo _info; + private NotePathBehaviour _path; + private RhythmVisualConfig _config; + private System.Action _despawnCallback; + + private static readonly int BaseColorId = Shader.PropertyToID("_BaseColor"); + private MaterialPropertyBlock _propBlock; + + #endregion + + #region IRhythmNoteView + + /** */ + public int Lane => _info.Note.Lane; + + /** */ + public ScheduledNoteInfo Info => _info; + + /** */ + public void Initialize(ScheduledNoteInfo info, NotePathBehaviour path, RhythmVisualConfig config) + { + _info = info; + _path = path; + _config = config; + + _propBlock ??= new MaterialPropertyBlock(); + + Color baseColor = info.Note.NoteType == NoteType.Hold + ? (config != null ? config.HoldNoteColor : Color.white) + : (config != null ? config.NormalNoteColor : Color.white); + + SetRendererColor(baseColor); + + if (_holdTrail != null) + { + bool isHold = info.Note.NoteType == NoteType.Hold; + _holdTrail.enabled = isHold; + if (isHold) + { + _holdTrail.positionCount = 2; + Vector3 spawnPos = path != null ? path.EvaluatePosition(0f) : transform.position; + _holdTrail.SetPosition(0, spawnPos); + _holdTrail.SetPosition(1, spawnPos); + } + } + + transform.position = path != null ? path.EvaluatePosition(0f) : transform.position; + transform.localScale = Vector3.one; + } + + /** */ + public void UpdateProgress(float headT, float tailT) + { + if (_path == null) return; + + transform.position = _path.EvaluatePosition(headT); + + if (_holdTrail != null && _holdTrail.enabled) + { + _holdTrail.SetPosition(0, transform.position); + _holdTrail.SetPosition(1, _path.EvaluatePosition(tailT)); + } + } + + /** */ + public async UniTask PlayHitEffect(HitRating rating) + { + Color flashColor = _config != null ? _config.GetHitFlashColor(rating) : Color.white; + SetRendererColor(flashColor); + + if (_hitParticles != null) _hitParticles.Play(); + + await Tween.Scale(transform, _hitPunchScale, _hitPunchDuration * 0.5f, Ease.OutBack); + await Tween.Scale(transform, Vector3.zero, _hitPunchDuration * 0.5f, Ease.InCubic); + + Despawn(); + } + + /** */ + public async UniTask PlayMissEffect() + { + Color missColor = _config != null ? _config.GetHitFlashColor(HitRating.Miss) : Color.red; + SetRendererColor(missColor); + await Tween.Scale(transform, Vector3.zero, _missFadeDuration, Ease.InCubic); + Despawn(); + } + + /** */ + public void Despawn() + { + Tween.StopAll(gameObject); + gameObject.SetActive(false); + _despawnCallback?.Invoke(); + } + + #endregion + + #region Public Helpers + + /** + * Registers the callback invoked when is called. + * The visualizer controller uses this to return the view to its pool. + * + */ + public void SetDespawnCallback(System.Action callback) + { + _despawnCallback = callback; + } + + #endregion + + #region Private Helpers + + private void SetRendererColor(Color color) + { + if (_noteRenderer == null) return; + _propBlock.SetColor(BaseColorId, color); + _noteRenderer.SetPropertyBlock(_propBlock); + } + + #endregion +} diff --git a/Assets/Rhythm/Visual/RhythmNoteView.cs.meta b/Assets/Rhythm/Visual/RhythmNoteView.cs.meta new file mode 100644 index 0000000..582dc06 --- /dev/null +++ b/Assets/Rhythm/Visual/RhythmNoteView.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 913c914734a0019418f6d86bc7752916 \ No newline at end of file diff --git a/Assets/Rhythm/Visual/RhythmVisualizerController.cs b/Assets/Rhythm/Visual/RhythmVisualizerController.cs new file mode 100644 index 0000000..55a079a --- /dev/null +++ b/Assets/Rhythm/Visual/RhythmVisualizerController.cs @@ -0,0 +1,384 @@ +using System.Collections.Generic; +using Cysharp.Threading.Tasks; +using UnityEngine; + +/** + * MonoBehaviour bridge between events and + * objects. Manages two object pools (Normal and + * Hold), updates all active note positions every frame using live DSP time, + * and forwards events to an implementation + * for additional visual effects. + * + */ +public class RhythmVisualizerController : MonoBehaviour +{ + #region Inspector Fields + + [Tooltip("Visual configuration asset (colours, prefabs, hit zone settings).")] + [SerializeField] private RhythmVisualConfig _visualConfig; + + [Tooltip("One NotePathBehaviour per lane. Index must match the lane index in NoteData.")] + [SerializeField] private NotePathBehaviour[] _lanePaths; + + [Tooltip("Parent transform under which note view GameObjects are instantiated.")] + [SerializeField] private Transform _noteContainer; + + [Header("Mirror Paths")] + [Tooltip("Optional mirror path per lane (parallel array to Lane Paths). " + + "When assigned for a lane, every note on that lane spawns a second " + + "visual-only note on the mirror path — no rhythm logic involved.")] + [SerializeField] private NotePathBehaviour[] _mirrorPaths; + + #endregion + + #region Private State + + private RhythmManager _rhythmManager; + private IVisualRhythmHooks _hooks; + + private readonly List _activeNotes = new List(); + private readonly Dictionary _mirrorViews = new Dictionary(); + + private readonly Queue _normalPool = new Queue(); + private readonly Queue _holdPool = new Queue(); + private readonly Queue _mirrorNormalPool = new Queue(); + private readonly Queue _mirrorHoldPool = new Queue(); + + #endregion + + #region Public API + + /** + * The look-ahead window (in beats) read from the visual config. Consumed by + * the owner (e.g. ) when constructing + * so both systems use the same value. + * + */ + public float NoteAppearanceBeats => _visualConfig != null ? _visualConfig.NoteAppearanceBeats : 2f; + + #endregion + + #region Initialisation + + /** + * Wires the visualizer to a . Must be called + * once after the manager is constructed. Subscribes to all events and + * pre-warms the note pools. + * + * The rhythm manager to visualize. + * Optional animation hooks. Defaults to no-op if null. + */ + public void Initialize(RhythmManager manager, IVisualRhythmHooks hooks = null) + { + _rhythmManager = manager; + _hooks = hooks ?? new DefaultVisualRhythmHooks(); + + int normalSize = _visualConfig != null ? _visualConfig.NormalPoolSize : 16; + int holdSize = _visualConfig != null ? _visualConfig.HoldPoolSize : 8; + + PrewarmPool(_visualConfig.NormalNotePrefab, _normalPool, normalSize); + PrewarmPool(_visualConfig.HoldNotePrefab, _holdPool, holdSize); + + if (HasAnyMirrorPath()) + { + PrewarmPool(_visualConfig.NormalNotePrefab, _mirrorNormalPool, normalSize); + PrewarmPool(_visualConfig.HoldNotePrefab, _mirrorHoldPool, holdSize); + } + + manager.OnNoteScheduled += OnNoteScheduled; + manager.OnNoteHit += OnNoteHit; + manager.OnNoteMissed += OnNoteMissed; + manager.OnBeatPulse += OnBeatPulse; + manager.OnKeyPointReached += OnKeyPointReached; + manager.OnSongComplete += OnSongComplete; + } + + #endregion + + #region Unity Lifecycle + + private void Update() + { + _rhythmManager?.Tick(); + UpdateAllNotes(); + } + + private void OnDestroy() + { + if (_rhythmManager == null) return; + _rhythmManager.OnNoteScheduled -= OnNoteScheduled; + _rhythmManager.OnNoteHit -= OnNoteHit; + _rhythmManager.OnNoteMissed -= OnNoteMissed; + _rhythmManager.OnBeatPulse -= OnBeatPulse; + _rhythmManager.OnKeyPointReached -= OnKeyPointReached; + _rhythmManager.OnSongComplete -= OnSongComplete; + } + + #endregion + + #region Event Handlers + + private void OnNoteScheduled(ScheduledNoteInfo info) + { + IRhythmNoteView view = SpawnNote(info); + if (view == null) return; + + _activeNotes.Add(view); + + RhythmNoteView mirror = SpawnMirrorNote(info); + if (mirror != null) + _mirrorViews[view] = mirror; + + _hooks.OnNoteSpawned(view).Forget(); + } + + private void OnNoteHit(HitResult result) + { + IRhythmNoteView view = FindActiveView(result.Note); + if (view == null) return; + + _activeNotes.Remove(view); + RhythmNoteView mirror = ConsumeMirror(view); + PlayHitEffectChain(view, result, mirror).Forget(); + } + + private void OnNoteMissed(NoteData note) + { + IRhythmNoteView view = FindActiveView(note); + if (view == null) return; + + _activeNotes.Remove(view); + RhythmNoteView mirror = ConsumeMirror(view); + PlayMissEffectChain(view, note, mirror).Forget(); + } + + private async UniTaskVoid PlayHitEffectChain(IRhythmNoteView view, HitResult result, RhythmNoteView mirror) + { + await _hooks.OnNoteHit(view, result); + view.PlayHitEffect(result.Rating).Forget(); + mirror?.PlayHitEffect(result.Rating).Forget(); + SpawnFeedback(result.Note.Lane, _visualConfig.GetFeedbackPrefab(result.Rating)); + } + + private async UniTaskVoid PlayMissEffectChain(IRhythmNoteView view, NoteData note, RhythmNoteView mirror) + { + await _hooks.OnNoteMissed(view, note); + view.PlayMissEffect().Forget(); + mirror?.PlayMissEffect().Forget(); + SpawnFeedback(note.Lane, _visualConfig != null ? _visualConfig.FeedbackMissPrefab : null); + } + + private void OnBeatPulse(double beat) + { + _hooks.OnBeatPulse(beat).Forget(); + } + + private void OnKeyPointReached(KeyPointData keyPoint) + { + _hooks.OnKeyPointReached(keyPoint).Forget(); + } + + private void OnSongComplete() { } + + #endregion + + #region Note Management + + private void UpdateAllNotes() + { + double dspNow = AudioSettings.dspTime; + + for (int i = _activeNotes.Count - 1; i >= 0; i--) + { + IRhythmNoteView view = _activeNotes[i]; + ScheduledNoteInfo info = view.Info; + + double travelDuration = info.HitDspTime - info.SpawnDspTime; + if (travelDuration <= 0.0) continue; + + /* Safety net: OnNoteMissed may not have fired or FindActiveView may have + failed to match — force-despawn anything past the grace window. */ + double cleanupGrace = _visualConfig != null ? _visualConfig.MissCleanupGraceSeconds : 0.5f; + if (dspNow > info.HitDspTime + cleanupGrace) + { + _activeNotes.RemoveAt(i); + RhythmNoteView staleMirror = ConsumeMirror(view); + view.PlayMissEffect().Forget(); + staleMirror?.PlayMissEffect().Forget(); + continue; + } + + /* headT: 0 at spawn, 1 when note reaches hit zone */ + float headT = Mathf.Clamp01((float)((dspNow - info.SpawnDspTime) / travelDuration)); + + float tailT = headT; + if (info.Note.NoteType == NoteType.Hold && info.HoldEndDspTime > info.HitDspTime) + { + double holdDuration = info.HoldEndDspTime - info.SpawnDspTime; + tailT = Mathf.Clamp01((float)((dspNow - info.SpawnDspTime) / holdDuration)); + } + + _hooks.OnNoteUpdated(view, headT).Forget(); + view.UpdateProgress(headT, tailT); + + if (_mirrorViews.TryGetValue(view, out RhythmNoteView mirror)) + mirror.UpdateProgress(headT, tailT); + } + } + + private IRhythmNoteView SpawnNote(ScheduledNoteInfo info) + { + bool isHold = info.Note.NoteType == NoteType.Hold; + RhythmNoteView view = BorrowFromPool(isHold ? _holdPool : _normalPool, + isHold ? _visualConfig.HoldNotePrefab + : _visualConfig.NormalNotePrefab); + if (view == null) return null; + + NotePathBehaviour path = GetLanePath(info.Note.Lane); + view.Initialize(info, path, _visualConfig); + view.SetDespawnCallback(() => ReturnToPool(view)); + return view; + } + + private IRhythmNoteView FindActiveView(NoteData note) + { + for (int i = 0; i < _activeNotes.Count; i++) + { + if (_activeNotes[i].Info.Note.EditorId == note.EditorId && + _activeNotes[i].Info.Note.Beat == note.Beat && + _activeNotes[i].Info.Note.Lane == note.Lane) + return _activeNotes[i]; + } + return null; + } + + private RhythmNoteView SpawnMirrorNote(ScheduledNoteInfo info) + { + NotePathBehaviour mirrorPath = GetMirrorPath(info.Note.Lane); + if (mirrorPath == null) return null; + + bool isHold = info.Note.NoteType == NoteType.Hold; + RhythmNoteView mirror = BorrowFromPool(isHold ? _mirrorHoldPool : _mirrorNormalPool, + isHold ? _visualConfig.HoldNotePrefab + : _visualConfig.NormalNotePrefab); + if (mirror == null) return null; + + mirror.Initialize(info, mirrorPath, _visualConfig); + mirror.SetDespawnCallback(() => ReturnToMirrorPool(mirror)); + return mirror; + } + + private RhythmNoteView ConsumeMirror(IRhythmNoteView primaryView) + { + if (!_mirrorViews.TryGetValue(primaryView, out RhythmNoteView mirror)) return null; + _mirrorViews.Remove(primaryView); + return mirror; + } + + private NotePathBehaviour GetLanePath(int lane) + { + if (_lanePaths != null && lane >= 0 && lane < _lanePaths.Length) + return _lanePaths[lane]; + return null; + } + + private NotePathBehaviour GetMirrorPath(int lane) + { + if (_mirrorPaths != null && lane >= 0 && lane < _mirrorPaths.Length) + return _mirrorPaths[lane]; + return null; + } + + private bool HasAnyMirrorPath() + { + if (_mirrorPaths == null) return false; + foreach (var p in _mirrorPaths) + if (p != null) return true; + return false; + } + + #endregion + + #region Pooling + + private void PrewarmPool(GameObject prefab, Queue pool, int count) + { + if (prefab == null) return; + for (int i = 0; i < count; i++) + { + RhythmNoteView view = CreateView(prefab); + view.gameObject.SetActive(false); + pool.Enqueue(view); + } + } + + private RhythmNoteView BorrowFromPool(Queue pool, GameObject prefab) + { + RhythmNoteView view = null; + + while (pool.Count > 0) + { + view = pool.Dequeue(); + if (view != null) break; + } + + if (view == null) + { + if (prefab == null) return null; + view = CreateView(prefab); + } + + view.gameObject.SetActive(true); + return view; + } + + private void ReturnToPool(RhythmNoteView view) + { + bool isHold = view.Info.Note.NoteType == NoteType.Hold; + Queue pool = isHold ? _holdPool : _normalPool; + pool.Enqueue(view); + } + + private void ReturnToMirrorPool(RhythmNoteView view) + { + bool isHold = view.Info.Note.NoteType == NoteType.Hold; + Queue pool = isHold ? _mirrorHoldPool : _mirrorNormalPool; + pool.Enqueue(view); + } + + private RhythmNoteView CreateView(GameObject prefab) + { + Transform parent = _noteContainer != null ? _noteContainer : transform; + GameObject go = Instantiate(prefab, parent); + return go.GetComponent(); + } + + #endregion + + #region Feedback Labels + + private void SpawnFeedback(int lane, GameObject prefab) + { + if (prefab == null || _visualConfig == null) return; + if (_visualConfig.LaneHitZonePositions == null || lane < 0 || lane >= _visualConfig.LaneHitZonePositions.Length) return; + + Transform parent = _noteContainer != null ? _noteContainer : transform; + GameObject go = Instantiate(prefab, _visualConfig.LaneHitZonePositions[lane], Quaternion.identity, parent); + Destroy(go, 1f); + } + + #endregion + + #region Editor Gizmos + + private void OnDrawGizmos() + { + if (_visualConfig == null || _visualConfig.LaneHitZonePositions == null) return; + + Gizmos.color = new Color(1f, 0.5f, 0f, 0.5f); + foreach (Vector3 pos in _visualConfig.LaneHitZonePositions) + Gizmos.DrawWireSphere(pos, _visualConfig.HitZoneRadius); + } + + #endregion +} diff --git a/Assets/Rhythm/Visual/RhythmVisualizerController.cs.meta b/Assets/Rhythm/Visual/RhythmVisualizerController.cs.meta new file mode 100644 index 0000000..90fb289 --- /dev/null +++ b/Assets/Rhythm/Visual/RhythmVisualizerController.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7f15a2ff39e3f3c4db49fb4f8409f9e6 \ No newline at end of file diff --git a/Assets/Scenes/GameScene.unity b/Assets/Scenes/GameScene.unity index 4ab57a5..e1303f9 100644 --- a/Assets/Scenes/GameScene.unity +++ b/Assets/Scenes/GameScene.unity @@ -210,7 +210,10 @@ Transform: m_LocalPosition: {x: 0, y: 1, z: -10} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 - m_Children: [] + m_Children: + - {fileID: 1192756032} + - {fileID: 552294209} + - {fileID: 1148278824} m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!114 &68930445 @@ -301,7 +304,7 @@ Transform: m_Children: [] m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} ---- !u!1 &689797283 +--- !u!1 &195719195 GameObject: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} @@ -309,78 +312,1384 @@ GameObject: m_PrefabAsset: {fileID: 0} serializedVersion: 6 m_Component: - - component: {fileID: 689797286} - - component: {fileID: 689797285} - - component: {fileID: 689797284} + - component: {fileID: 195719196} + - component: {fileID: 195719199} + - component: {fileID: 195719198} + - component: {fileID: 195719197} + m_Layer: 5 + m_Name: Restart + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &195719196 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 195719195} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1.9, y: 1.9, z: 1.9} + m_ConstrainProportionsScale: 1 + m_Children: + - {fileID: 473532651} + m_Father: {fileID: 2104414999} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 1, y: 1} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 737.7, y: 330.7} + m_SizeDelta: {x: 160, y: 30} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &195719197 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 195719195} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Selected + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 195719198} + m_OnClick: + m_PersistentCalls: + m_Calls: [] +--- !u!114 &195719198 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 195719195} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &195719199 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 195719195} + m_CullTransparentMesh: 1 +--- !u!1 &222589560 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 222589561} + - component: {fileID: 222589563} + - component: {fileID: 222589562} + m_Layer: 5 + m_Name: Handle + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &222589561 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 222589560} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 546759842} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 20, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &222589562 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 222589560} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10913, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &222589563 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 222589560} + m_CullTransparentMesh: 1 +--- !u!1 &451490097 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 451490098} + - component: {fileID: 451490101} + - component: {fileID: 451490100} + - component: {fileID: 451490099} m_Layer: 0 - m_Name: EventSystem + m_Name: NoteHitSpot m_TagString: Untagged m_Icon: {fileID: 0} m_NavMeshLayer: 0 m_StaticEditorFlags: 0 m_IsActive: 1 ---- !u!114 &689797284 +--- !u!4 &451490098 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 451490097} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 0.1, z: 0.2} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1595041900} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!65 &451490099 +BoxCollider: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 451490097} + m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 + m_IsTrigger: 0 + m_ProvidesContacts: 0 + m_Enabled: 1 + serializedVersion: 3 + m_Size: {x: 1, y: 1, z: 1} + m_Center: {x: 0, y: 0, z: 0} +--- !u!23 &451490100 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 451490097} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 2 + m_RayTraceProcedural: 0 + m_RayTracingAccelStructBuildFlagsOverride: 0 + m_RayTracingAccelStructBuildFlags: 1 + m_SmallMeshCulling: 1 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: 792cca3ba846a4217aef4c35be2fd6a8, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 1 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} +--- !u!33 &451490101 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 451490097} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!1 &473532650 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 473532651} + - component: {fileID: 473532653} + - component: {fileID: 473532652} + m_Layer: 5 + m_Name: Text (TMP) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &473532651 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 473532650} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 195719196} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &473532652 MonoBehaviour: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 689797283} + m_GameObject: {fileID: 473532650} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 01614664b831546d2ae94a42149d80ac, type: 3} + m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} m_Name: m_EditorClassIdentifier: - m_SendPointerHoverToParent: 1 - m_MoveRepeatDelay: 0.5 - m_MoveRepeatRate: 0.1 - m_XRTrackingOrigin: {fileID: 0} - m_ActionsAsset: {fileID: -944628639613478452, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} - m_PointAction: {fileID: -1654692200621890270, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} - m_MoveAction: {fileID: -8784545083839296357, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} - m_SubmitAction: {fileID: 392368643174621059, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} - m_CancelAction: {fileID: 7727032971491509709, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} - m_LeftClickAction: {fileID: 3001919216989983466, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} - m_MiddleClickAction: {fileID: -2185481485913320682, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} - m_RightClickAction: {fileID: -4090225696740746782, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} - m_ScrollWheelAction: {fileID: 6240969308177333660, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} - m_TrackedDevicePositionAction: {fileID: 6564999863303420839, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} - m_TrackedDeviceOrientationAction: {fileID: 7970375526676320489, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} - m_DeselectOnBackgroundClick: 1 - m_PointerBehavior: 0 - m_CursorLockBehavior: 0 - m_ScrollDeltaPerTick: 6 ---- !u!114 &689797285 + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_text: 'Restart + +' + m_isRightToLeft: 0 + m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2} + m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2} + m_fontSharedMaterials: [] + m_fontMaterial: {fileID: 0} + m_fontMaterials: [] + m_fontColor32: + serializedVersion: 2 + rgba: 4281479730 + m_fontColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_enableVertexGradient: 0 + m_colorMode: 3 + m_fontColorGradient: + topLeft: {r: 1, g: 1, b: 1, a: 1} + topRight: {r: 1, g: 1, b: 1, a: 1} + bottomLeft: {r: 1, g: 1, b: 1, a: 1} + bottomRight: {r: 1, g: 1, b: 1, a: 1} + m_fontColorGradientPreset: {fileID: 0} + m_spriteAsset: {fileID: 0} + m_tintAllSprites: 0 + m_StyleSheet: {fileID: 0} + m_TextStyleHashCode: -1183493901 + m_overrideHtmlColors: 0 + m_faceColor: + serializedVersion: 2 + rgba: 4294967295 + m_fontSize: 24 + m_fontSizeBase: 24 + m_fontWeight: 400 + m_enableAutoSizing: 0 + m_fontSizeMin: 18 + m_fontSizeMax: 72 + m_fontStyle: 0 + m_HorizontalAlignment: 2 + m_VerticalAlignment: 512 + m_textAlignment: 65535 + m_characterSpacing: 0 + m_characterHorizontalScale: 1 + m_wordSpacing: 0 + m_lineSpacing: 0 + m_lineSpacingMax: 0 + m_paragraphSpacing: 0 + m_charWidthMaxAdj: 0 + m_TextWrappingMode: 1 + m_wordWrappingRatios: 0.4 + m_overflowMode: 0 + m_linkedTextComponent: {fileID: 0} + parentLinkedComponent: {fileID: 0} + m_enableKerning: 0 + m_ActiveFontFeatures: 6e72656b + m_enableExtraPadding: 0 + checkPaddingRequired: 0 + m_isRichText: 1 + m_EmojiFallbackSupport: 1 + m_parseCtrlCharacters: 1 + m_isOrthographic: 1 + m_isCullingEnabled: 0 + m_horizontalMapping: 0 + m_verticalMapping: 0 + m_uvLineOffset: 0 + m_geometrySortingOrder: 0 + m_IsTextObjectScaleStatic: 0 + m_VertexBufferAutoSizeReduction: 0 + m_useMaxVisibleDescender: 1 + m_pageToDisplay: 1 + m_margin: {x: 0, y: 0, z: 0, w: 0} + m_isUsingLegacyAnimationComponent: 0 + m_isVolumetricText: 0 + m_hasFontAssetChanged: 0 + m_baseMaterial: {fileID: 0} + m_maskOffset: {x: 0, y: 0, z: 0, w: 0} +--- !u!222 &473532653 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 473532650} + m_CullTransparentMesh: 1 +--- !u!1 &515217940 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 515217941} + m_Layer: 5 + m_Name: Fill Area + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &515217941 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 515217940} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 2014446948} + m_Father: {fileID: 661169485} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0.25} + m_AnchorMax: {x: 1, y: 0.75} + m_AnchoredPosition: {x: 5, y: 0} + m_SizeDelta: {x: -20, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &546759841 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 546759842} + m_Layer: 5 + m_Name: Handle Slide Area + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &546759842 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 546759841} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 222589561} + m_Father: {fileID: 661169485} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -20, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &552294208 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 552294209} + m_Layer: 0 + m_Name: NoteContainer + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &552294209 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 552294208} + serializedVersion: 2 + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: -1, z: 10} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 68930444} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &566812980 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 566812981} + - component: {fileID: 566812983} + - component: {fileID: 566812982} + m_Layer: 5 + m_Name: Label + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &566812981 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 566812980} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1329169888} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: 195.7, y: 61.8} + m_SizeDelta: {x: 276, y: 50} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &566812982 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 566812980} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_text: 'Beat Speed: 2' + m_isRightToLeft: 0 + m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2} + m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2} + m_fontSharedMaterials: [] + m_fontMaterial: {fileID: 0} + m_fontMaterials: [] + m_fontColor32: + serializedVersion: 2 + rgba: 4294967295 + m_fontColor: {r: 1, g: 1, b: 1, a: 1} + m_enableVertexGradient: 0 + m_colorMode: 3 + m_fontColorGradient: + topLeft: {r: 1, g: 1, b: 1, a: 1} + topRight: {r: 1, g: 1, b: 1, a: 1} + bottomLeft: {r: 1, g: 1, b: 1, a: 1} + bottomRight: {r: 1, g: 1, b: 1, a: 1} + m_fontColorGradientPreset: {fileID: 0} + m_spriteAsset: {fileID: 0} + m_tintAllSprites: 0 + m_StyleSheet: {fileID: 0} + m_TextStyleHashCode: -1183493901 + m_overrideHtmlColors: 0 + m_faceColor: + serializedVersion: 2 + rgba: 4294967295 + m_fontSize: 36 + m_fontSizeBase: 36 + m_fontWeight: 400 + m_enableAutoSizing: 0 + m_fontSizeMin: 18 + m_fontSizeMax: 72 + m_fontStyle: 0 + m_HorizontalAlignment: 4 + m_VerticalAlignment: 256 + m_textAlignment: 65535 + m_characterSpacing: 0 + m_characterHorizontalScale: 1 + m_wordSpacing: 0 + m_lineSpacing: 0 + m_lineSpacingMax: 0 + m_paragraphSpacing: 0 + m_charWidthMaxAdj: 0 + m_TextWrappingMode: 1 + m_wordWrappingRatios: 0.4 + m_overflowMode: 0 + m_linkedTextComponent: {fileID: 0} + parentLinkedComponent: {fileID: 0} + m_enableKerning: 0 + m_ActiveFontFeatures: 6e72656b + m_enableExtraPadding: 0 + checkPaddingRequired: 0 + m_isRichText: 1 + m_EmojiFallbackSupport: 1 + m_parseCtrlCharacters: 1 + m_isOrthographic: 1 + m_isCullingEnabled: 0 + m_horizontalMapping: 0 + m_verticalMapping: 0 + m_uvLineOffset: 0 + m_geometrySortingOrder: 0 + m_IsTextObjectScaleStatic: 0 + m_VertexBufferAutoSizeReduction: 0 + m_useMaxVisibleDescender: 1 + m_pageToDisplay: 1 + m_margin: {x: -223.27539, y: 0, z: 0, w: 0} + m_isUsingLegacyAnimationComponent: 0 + m_isVolumetricText: 0 + m_hasFontAssetChanged: 0 + m_baseMaterial: {fileID: 0} + m_maskOffset: {x: 0, y: 0, z: 0, w: 0} +--- !u!222 &566812983 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 566812980} + m_CullTransparentMesh: 1 +--- !u!1 &661169484 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 661169485} + - component: {fileID: 661169486} + m_Layer: 5 + m_Name: Slider + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &661169485 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 661169484} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 2.2, y: 2.2, z: 2.2} + m_ConstrainProportionsScale: 1 + m_Children: + - {fileID: 810347529} + - {fileID: 515217941} + - {fileID: 546759842} + m_Father: {fileID: 1329169888} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: 176, y: 22} + m_SizeDelta: {x: 160, y: 20} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &661169486 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 661169484} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 67db9e8f0e2ae9c40bc1e2b64352a6b4, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 1, g: 1, b: 1, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Selected + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 222589562} + m_FillRect: {fileID: 2014446948} + m_HandleRect: {fileID: 222589561} + m_Direction: 1 + m_MinValue: 0 + m_MaxValue: 1 + m_WholeNumbers: 0 + m_Value: 0.669 + m_OnValueChanged: + m_PersistentCalls: + m_Calls: [] +--- !u!1 &689797283 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 689797286} + - component: {fileID: 689797285} + - component: {fileID: 689797284} + m_Layer: 0 + m_Name: EventSystem + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &689797284 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 689797283} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 01614664b831546d2ae94a42149d80ac, type: 3} + m_Name: + m_EditorClassIdentifier: + m_SendPointerHoverToParent: 1 + m_MoveRepeatDelay: 0.5 + m_MoveRepeatRate: 0.1 + m_XRTrackingOrigin: {fileID: 0} + m_ActionsAsset: {fileID: -944628639613478452, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_PointAction: {fileID: -1654692200621890270, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_MoveAction: {fileID: -8784545083839296357, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_SubmitAction: {fileID: 392368643174621059, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_CancelAction: {fileID: 7727032971491509709, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_LeftClickAction: {fileID: 3001919216989983466, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_MiddleClickAction: {fileID: -2185481485913320682, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_RightClickAction: {fileID: -4090225696740746782, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_ScrollWheelAction: {fileID: 6240969308177333660, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_TrackedDevicePositionAction: {fileID: 6564999863303420839, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_TrackedDeviceOrientationAction: {fileID: 7970375526676320489, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_DeselectOnBackgroundClick: 1 + m_PointerBehavior: 0 + m_CursorLockBehavior: 0 + m_ScrollDeltaPerTick: 6 +--- !u!114 &689797285 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 689797283} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 76c392e42b5098c458856cdf6ecaaaa1, type: 3} + m_Name: + m_EditorClassIdentifier: + m_FirstSelected: {fileID: 0} + m_sendNavigationEvents: 1 + m_DragThreshold: 10 +--- !u!4 &689797286 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 689797283} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &805334466 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 805334468} + - component: {fileID: 805334467} + m_Layer: 0 + m_Name: Audio + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!82 &805334467 +AudioSource: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 805334466} + m_Enabled: 1 + serializedVersion: 4 + OutputAudioMixerGroup: {fileID: 0} + m_audioClip: {fileID: 0} + m_Resource: {fileID: 0} + m_PlayOnAwake: 1 + m_Volume: 1 + m_Pitch: 1 + Loop: 0 + Mute: 0 + Spatialize: 0 + SpatializePostEffects: 0 + Priority: 128 + DopplerLevel: 1 + MinDistance: 1 + MaxDistance: 500 + Pan2D: 0 + rolloffMode: 0 + BypassEffects: 0 + BypassListenerEffects: 0 + BypassReverbZones: 0 + rolloffCustomCurve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 1 + inSlope: 0 + outSlope: 0 + tangentMode: 0 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 1 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 0 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + panLevelCustomCurve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 0 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + spreadCustomCurve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 0 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + reverbZoneMixCustomCurve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 1 + inSlope: 0 + outSlope: 0 + tangentMode: 0 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 +--- !u!4 &805334468 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 805334466} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &810347528 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 810347529} + - component: {fileID: 810347531} + - component: {fileID: 810347530} + m_Layer: 5 + m_Name: Background + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &810347529 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 810347528} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 661169485} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0.25} + m_AnchorMax: {x: 1, y: 0.75} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &810347530 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 810347528} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 0.22745098} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10907, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &810347531 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 810347528} + m_CullTransparentMesh: 1 +--- !u!1 &837510802 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 837510803} + m_Layer: 0 + m_Name: Point (2) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &837510803 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 837510802} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 4, z: 11} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 1978219628} + m_Father: {fileID: 1192756032} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &934141671 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 934141673} + - component: {fileID: 934141675} + - component: {fileID: 934141674} + m_Layer: 0 + m_Name: GameController + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &934141673 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 934141671} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &934141674 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 934141671} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 7f15a2ff39e3f3c4db49fb4f8409f9e6, type: 3} + m_Name: + m_EditorClassIdentifier: + _visualConfig: {fileID: 11400000, guid: cecb790105d80e648867f3a87090d970, type: 2} + _lanePaths: + - {fileID: 1192756033} + _noteContainer: {fileID: 552294209} + _mirrorPaths: [] +--- !u!114 &934141675 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 934141671} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 0e36b33fed654fa4495553f5f9610203, type: 3} + m_Name: + m_EditorClassIdentifier: + _config: {fileID: 11400000, guid: 5f86e1357cf9047feb3a73b01384300a, type: 2} + _nodePrefab: {fileID: 2132212214528617362, guid: 47434b2d937064155aca3329574f5c2c, type: 3} + _cameraRig: {fileID: 68930445} + _edgeContainer: {fileID: 141288317} + _errorCountLabel: {fileID: 1625478451} + _sampleTreeProvider: {fileID: 11400000, guid: 5e742b1b425de4606ad22578dcb5452d, type: 2} + _animationConfig: {fileID: 11400000, guid: cd1dd569b0aee4eb89ba696fe4f3164d, type: 2} + _beatmap: {fileID: 11400000, guid: 9d66e8de59dee1246b742c0a1d1fdcdd, type: 2} + _hitWindows: {fileID: 11400000, guid: 2d35008a557ccca439a1278a088cb8e8, type: 2} + _calibration: {fileID: 11400000, guid: 7835c71bd9045204dae701ad89457734, type: 2} + _audioSource: {fileID: 805334467} + _visualizer: {fileID: 934141674} + _laneToAction: 000000000200000001000000 +--- !u!1 &1148278823 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1148278824} + - component: {fileID: 1148278827} + - component: {fileID: 1148278826} + - component: {fileID: 1148278825} + m_Layer: 0 + m_Name: Background + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1148278824 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1148278823} + serializedVersion: 2 + m_LocalRotation: {x: -0.7071068, y: 0, z: 0, w: 0.7071068} + m_LocalPosition: {x: 0, y: 0, z: 30} + m_LocalScale: {x: 20, y: 20, z: 20} + m_ConstrainProportionsScale: 1 + m_Children: [] + m_Father: {fileID: 68930444} + m_LocalEulerAnglesHint: {x: -90, y: 0, z: 0} +--- !u!64 &1148278825 +MeshCollider: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1148278823} + m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 + m_IsTrigger: 0 + m_ProvidesContacts: 0 + m_Enabled: 1 + serializedVersion: 5 + m_Convex: 0 + m_CookingOptions: 30 + m_Mesh: {fileID: 10209, guid: 0000000000000000e000000000000000, type: 0} +--- !u!23 &1148278826 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1148278823} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 2 + m_RayTraceProcedural: 0 + m_RayTracingAccelStructBuildFlagsOverride: 0 + m_RayTracingAccelStructBuildFlags: 1 + m_SmallMeshCulling: 1 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: 9b6261c8e0b1e4ab89920f89d0bf8dc9, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 1 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} +--- !u!33 &1148278827 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1148278823} + m_Mesh: {fileID: 10209, guid: 0000000000000000e000000000000000, type: 0} +--- !u!1 &1192756031 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1192756032} + - component: {fileID: 1192756033} + m_Layer: 0 + m_Name: LeftNotePath + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1192756032 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1192756031} + serializedVersion: 2 + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 1450648614} + - {fileID: 837510803} + - {fileID: 1595041900} + m_Father: {fileID: 68930444} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &1192756033 MonoBehaviour: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 689797283} + m_GameObject: {fileID: 1192756031} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 76c392e42b5098c458856cdf6ecaaaa1, type: 3} + m_Script: {fileID: 11500000, guid: 84f4dc4f60fba7e4db3098b36887c32b, type: 3} m_Name: m_EditorClassIdentifier: - m_FirstSelected: {fileID: 0} - m_sendNavigationEvents: 1 - m_DragThreshold: 10 ---- !u!4 &689797286 -Transform: + _waypoints: + - {fileID: 1450648614} + - {fileID: 837510803} + - {fileID: 1595041900} + _easeCurve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 0 + inSlope: 0 + outSlope: 1 + tangentMode: 0 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 1 + value: 1 + inSlope: 1 + outSlope: 0 + tangentMode: 0 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + _drawGizmosInPlayMode: 0 +--- !u!1 &1329169887 +GameObject: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 689797283} - serializedVersion: 2 + serializedVersion: 6 + m_Component: + - component: {fileID: 1329169888} + m_Layer: 5 + m_Name: BeatSpeed + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1329169888 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1329169887} m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 - m_Children: [] - m_Father: {fileID: 0} + m_Children: + - {fileID: 661169485} + - {fileID: 566812981} + m_Father: {fileID: 2104414999} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} ---- !u!1 &934141671 + m_AnchorMin: {x: 1, y: 1} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 544, y: 380.5} + m_SizeDelta: {x: 352, y: 44} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &1450648613 GameObject: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} @@ -388,47 +1697,29 @@ GameObject: m_PrefabAsset: {fileID: 0} serializedVersion: 6 m_Component: - - component: {fileID: 934141673} - - component: {fileID: 934141672} + - component: {fileID: 1450648614} m_Layer: 0 - m_Name: GameController + m_Name: Point (1) m_TagString: Untagged m_Icon: {fileID: 0} m_NavMeshLayer: 0 m_StaticEditorFlags: 0 m_IsActive: 1 ---- !u!114 &934141672 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 934141671} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 51e93b1393d71450f89ef7d43b0b2a26, type: 3} - m_Name: - m_EditorClassIdentifier: - _config: {fileID: 11400000, guid: 5f86e1357cf9047feb3a73b01384300a, type: 2} - _nodePrefab: {fileID: 2132212214528617362, guid: 47434b2d937064155aca3329574f5c2c, type: 3} - _cameraRig: {fileID: 68930445} - _edgeContainer: {fileID: 141288317} - _errorCountLabel: {fileID: 1625478451} - _sampleTreeProvider: {fileID: 11400000, guid: 5e742b1b425de4606ad22578dcb5452d, type: 2} ---- !u!4 &934141673 +--- !u!4 &1450648614 Transform: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 934141671} + m_GameObject: {fileID: 1450648613} serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalPosition: {x: 0, y: 8, z: 11} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 - m_Children: [] - m_Father: {fileID: 0} + m_Children: + - {fileID: 1649977640} + m_Father: {fileID: 1192756032} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &1482817658 GameObject: @@ -551,6 +1842,38 @@ Transform: m_Children: [] m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 50, y: -30, z: 0} +--- !u!1 &1595041899 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1595041900} + m_Layer: 0 + m_Name: Point (3) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1595041900 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1595041899} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 1.25, z: 11} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 451490098} + m_Father: {fileID: 1192756032} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &1625478449 GameObject: m_ObjectHideFlags: 0 @@ -608,7 +1931,7 @@ MonoBehaviour: m_OnCullStateChanged: m_PersistentCalls: m_Calls: [] - m_text: 'Errors: ' + m_text: 'Errors: 0' m_isRightToLeft: 0 m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2} m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2} @@ -688,6 +2011,297 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1625478449} m_CullTransparentMesh: 1 +--- !u!1 &1649977639 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1649977640} + - component: {fileID: 1649977643} + - component: {fileID: 1649977642} + - component: {fileID: 1649977641} + m_Layer: 0 + m_Name: Cube + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 0 +--- !u!4 &1649977640 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1649977639} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1450648614} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!65 &1649977641 +BoxCollider: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1649977639} + m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 + m_IsTrigger: 0 + m_ProvidesContacts: 0 + m_Enabled: 1 + serializedVersion: 3 + m_Size: {x: 1, y: 1, z: 1} + m_Center: {x: 0, y: 0, z: 0} +--- !u!23 &1649977642 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1649977639} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 2 + m_RayTraceProcedural: 0 + m_RayTracingAccelStructBuildFlagsOverride: 0 + m_RayTracingAccelStructBuildFlags: 1 + m_SmallMeshCulling: 1 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: 31321ba15b8f8eb4c954353edc038b1d, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 1 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} +--- !u!33 &1649977643 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1649977639} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!1 &1978219627 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1978219628} + - component: {fileID: 1978219631} + - component: {fileID: 1978219630} + - component: {fileID: 1978219629} + m_Layer: 0 + m_Name: Cube + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 0 +--- !u!4 &1978219628 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1978219627} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 837510803} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!65 &1978219629 +BoxCollider: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1978219627} + m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 + m_IsTrigger: 0 + m_ProvidesContacts: 0 + m_Enabled: 1 + serializedVersion: 3 + m_Size: {x: 1, y: 1, z: 1} + m_Center: {x: 0, y: 0, z: 0} +--- !u!23 &1978219630 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1978219627} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 2 + m_RayTraceProcedural: 0 + m_RayTracingAccelStructBuildFlagsOverride: 0 + m_RayTracingAccelStructBuildFlags: 1 + m_SmallMeshCulling: 1 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: 31321ba15b8f8eb4c954353edc038b1d, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 1 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} +--- !u!33 &1978219631 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1978219627} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!1 &2014446947 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2014446948} + - component: {fileID: 2014446950} + - component: {fileID: 2014446949} + m_Layer: 5 + m_Name: Fill + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2014446948 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2014446947} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 515217941} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 10, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &2014446949 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2014446947} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.5518868, g: 1, b: 0.6274163, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &2014446950 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2014446947} + m_CullTransparentMesh: 1 --- !u!1 &2100820698 GameObject: m_ObjectHideFlags: 0 @@ -783,6 +2397,7 @@ RectTransform: m_ConstrainProportionsScale: 0 m_Children: - {fileID: 1625478450} + - {fileID: 2104414999} m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0} @@ -790,6 +2405,63 @@ RectTransform: m_AnchoredPosition: {x: 0, y: 0} m_SizeDelta: {x: 0, y: 0} m_Pivot: {x: 0, y: 0} +--- !u!1 &2104414998 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2104414999} + - component: {fileID: 2104415000} + m_Layer: 5 + m_Name: DebugUI + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2104414999 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2104414998} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 1329169888} + - {fileID: 195719196} + m_Father: {fileID: 2100820702} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 100, y: 100} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &2104415000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2104414998} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: a12a8f60640784ff6928d5c61ac956c9, type: 3} + m_Name: + m_EditorClassIdentifier: + _rhythmVisualConfig: {fileID: 11400000, guid: cecb790105d80e648867f3a87090d970, type: 2} + _noteBeatsSlider: {fileID: 661169486} + _noteBeatsLabel: {fileID: 566812982} + _noteBeatsMin: 0.5 + _noteBeatsMax: 6 + _restartButton: {fileID: 195719197} + _gameSceneIndex: 0 --- !u!1660057539 &9223372036854775807 SceneRoots: m_ObjectHideFlags: 0 @@ -800,3 +2472,4 @@ SceneRoots: - {fileID: 934141673} - {fileID: 141288317} - {fileID: 2100820702} + - {fileID: 805334468} diff --git a/Assets/ScriptableObjects/Configs.meta b/Assets/ScriptableObjects/Configs.meta new file mode 100644 index 0000000..7dc9646 --- /dev/null +++ b/Assets/ScriptableObjects/Configs.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5a134c11c1556f44580516f36b324fe5 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ScriptableObjects/Configs/HitWindowConfig.asset b/Assets/ScriptableObjects/Configs/HitWindowConfig.asset new file mode 100644 index 0000000..53b165e --- /dev/null +++ b/Assets/ScriptableObjects/Configs/HitWindowConfig.asset @@ -0,0 +1,40 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 7e494470476ae5847b3b0d9bbc5fff7e, type: 3} + m_Name: HitWindowConfig + m_EditorClassIdentifier: + PerfectWindow: 0.03 + GoodWindow: 0.07 + OkWindow: 0.12 + PerfectScore: 300 + GoodScore: 200 + OkScore: 100 + MissScore: 0 + BaseMultiplier: 1 + MaxMultiplier: 8 + ComboStepSize: 10 + GradeSSThreshold: 1 + GradeSThreshold: 0.97 + GradeAThreshold: 0.93 + GradeBThreshold: 0.85 + GradeCThreshold: 0.75 + GradeDThreshold: 0.6 + HealthEnabled: 0 + MaxHp: 100 + HpGainPerfect: 2 + HpGainGood: 1 + HpGainOk: 0 + HpDrainMiss: 10 + AccuracyWeightPerfect: 1 + AccuracyWeightGood: 0.67 + AccuracyWeightOk: 0.33 + AccuracyWeightMiss: 0 diff --git a/Assets/ScriptableObjects/Configs/HitWindowConfig.asset.meta b/Assets/ScriptableObjects/Configs/HitWindowConfig.asset.meta new file mode 100644 index 0000000..1e19110 --- /dev/null +++ b/Assets/ScriptableObjects/Configs/HitWindowConfig.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2d35008a557ccca439a1278a088cb8e8 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ScriptableObjects/Configs/RhythmCalibrationConfig.asset b/Assets/ScriptableObjects/Configs/RhythmCalibrationConfig.asset new file mode 100644 index 0000000..b560d73 --- /dev/null +++ b/Assets/ScriptableObjects/Configs/RhythmCalibrationConfig.asset @@ -0,0 +1,16 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 3b666d4b58929be4ba0e72378442a4cc, type: 3} + m_Name: RhythmCalibrationConfig + m_EditorClassIdentifier: + CalibrationOffsetSeconds: 0 + VisualOffsetSeconds: 0 diff --git a/Assets/ScriptableObjects/Configs/RhythmCalibrationConfig.asset.meta b/Assets/ScriptableObjects/Configs/RhythmCalibrationConfig.asset.meta new file mode 100644 index 0000000..f7d1f40 --- /dev/null +++ b/Assets/ScriptableObjects/Configs/RhythmCalibrationConfig.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7835c71bd9045204dae701ad89457734 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ScriptableObjects/Configs/RhythmVisualConfig.asset b/Assets/ScriptableObjects/Configs/RhythmVisualConfig.asset new file mode 100644 index 0000000..d4cf152 --- /dev/null +++ b/Assets/ScriptableObjects/Configs/RhythmVisualConfig.asset @@ -0,0 +1,32 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fb02592c77f9e1f42a56dc9c4989ab30, type: 3} + m_Name: RhythmVisualConfig + m_EditorClassIdentifier: + NoteAppearanceBeats: 1.503571 + MissCleanupGraceSeconds: 0.5 + NormalPoolSize: 16 + HoldPoolSize: 8 + NormalNoteColor: {r: 0.99986243, g: 0.8915094, b: 1, a: 1} + HoldNoteColor: {r: 0.4, g: 0.8, b: 1, a: 1} + HitFlashPerfect: {r: 1, g: 0.8243091, b: 0.3915094, a: 1} + HitFlashGood: {r: 0.5424528, g: 1, b: 0.5424528, a: 1} + HitFlashOk: {r: 1, g: 0.3915094, b: 0.8627911, a: 1} + HitFlashMiss: {r: 1, g: 0.21226418, b: 0.21226418, a: 1} + HitZoneRadius: 0.5 + LaneHitZonePositions: [] + NormalNotePrefab: {fileID: 2132212214528617362, guid: a4a524929e253024fb536d59b8c205e0, type: 3} + HoldNotePrefab: {fileID: 0} + FeedbackPerfectPrefab: {fileID: 0} + FeedbackGoodPrefab: {fileID: 0} + FeedbackOkPrefab: {fileID: 0} + FeedbackMissPrefab: {fileID: 0} diff --git a/Assets/ScriptableObjects/Configs/RhythmVisualConfig.asset.meta b/Assets/ScriptableObjects/Configs/RhythmVisualConfig.asset.meta new file mode 100644 index 0000000..86994b5 --- /dev/null +++ b/Assets/ScriptableObjects/Configs/RhythmVisualConfig.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: cecb790105d80e648867f3a87090d970 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ScriptableObjects/Configs/TreeAnimationConfig.asset b/Assets/ScriptableObjects/Configs/TreeAnimationConfig.asset new file mode 100644 index 0000000..8fb04e1 --- /dev/null +++ b/Assets/ScriptableObjects/Configs/TreeAnimationConfig.asset @@ -0,0 +1,31 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 08d88fdfd5135499187cd39a1423b188, type: 3} + m_Name: TreeAnimationConfig + m_EditorClassIdentifier: + _hitClip: {fileID: 8300000, guid: 3933bfed4d8be4f9fabe55d5ade4c07e, type: 3} + _wrongInputClip: {fileID: 8300000, guid: 920c5009ea0384f22a22a9b366964a4a, type: 3} + _missClip: {fileID: 0} + _offBeatClip: {fileID: 8300000, guid: 7bb487aa2617a4056a1b39a0273320d2, type: 3} + _hitVolume: 0.8 + _wrongInputVolume: 0.8 + _missVolume: 0.8 + _offBeatVolume: 0.6 + _pulseScaleMultiplier: 1.15 + _pulseDuration: 0.12 + _pulseEase: 6 + _beatPulseMultiplier: 2 + _bounceOffset: 0.3 + _bounceDuration: 0.25 + _bounceOutEase: 6 + _bounceReturnEase: 24 + _wrongInputLockoutDuration: 0.15 diff --git a/Assets/ScriptableObjects/Configs/TreeAnimationConfig.asset.meta b/Assets/ScriptableObjects/Configs/TreeAnimationConfig.asset.meta new file mode 100644 index 0000000..180b611 --- /dev/null +++ b/Assets/ScriptableObjects/Configs/TreeAnimationConfig.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: cd1dd569b0aee4eb89ba696fe4f3164d +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ScriptableObjects/TreeVisualizerConfig.asset b/Assets/ScriptableObjects/Configs/TreeVisualizerConfig.asset similarity index 67% rename from Assets/ScriptableObjects/TreeVisualizerConfig.asset rename to Assets/ScriptableObjects/Configs/TreeVisualizerConfig.asset index e0fa0cf..52b7960 100644 --- a/Assets/ScriptableObjects/TreeVisualizerConfig.asset +++ b/Assets/ScriptableObjects/Configs/TreeVisualizerConfig.asset @@ -18,21 +18,21 @@ MonoBehaviour: CameraMoveDuration: 0.2 CameraEase: 9 CameraRotation: {x: 0, y: 0, z: 0} - CameraPositionOffset: {x: 0, y: 0, z: -8} - CameraOrthographicSize: 10 + CameraPositionOffset: {x: 0, y: 0, z: -6} + CameraOrthographicSize: 5 CameraFieldOfView: 60 NodeRepositionDuration: 0.2 RepositionEase: 27 CarrierMoveDuration: 0.2 CarrierMoveEase: 9 - PostPlacementDelayMs: 500 + PostPlacementDelayMs: 100 CarrierReadyAlpha: 0.65 - NormalNodeColor: {r: 1, g: 1, b: 1, a: 1} - FocusedNodeColor: {r: 1, g: 0.92156863, b: 0.015686275, a: 1} - CarrierColor: {r: 0, g: 1, b: 1, a: 1} - CarrierReadyColor: {r: 0.2, g: 1, b: 0.4, a: 1} + NormalNodeColor: {r: 0.8911494, g: 0.7080812, b: 0.9811321, a: 1} + FocusedNodeColor: {r: 0.64480627, g: 0.2971698, b: 1, a: 1} + CarrierColor: {r: 0.43867922, g: 0.53349704, b: 1, a: 1} + CarrierReadyColor: {r: 0.41037738, g: 1, b: 0.56451243, a: 1} EdgeMaterial: {fileID: 10306, guid: 0000000000000000f000000000000000, type: 0} - EdgeColor: {r: 0.7, g: 0.7, b: 0.7, a: 1} + EdgeColor: {r: 1, g: 0.36320752, b: 0.99139464, a: 1} EdgeWidth: 0.06 MinValue: 1 MaxValue: 99 diff --git a/Assets/ScriptableObjects/TreeVisualizerConfig.asset.meta b/Assets/ScriptableObjects/Configs/TreeVisualizerConfig.asset.meta similarity index 100% rename from Assets/ScriptableObjects/TreeVisualizerConfig.asset.meta rename to Assets/ScriptableObjects/Configs/TreeVisualizerConfig.asset.meta diff --git a/Assets/ScriptableObjects/SampleTrees.meta b/Assets/ScriptableObjects/SampleTrees.meta new file mode 100644 index 0000000..25d50c6 --- /dev/null +++ b/Assets/ScriptableObjects/SampleTrees.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 94145dd846f3f364c8a7f04e3aa54525 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ScriptableObjects/IntSampleTree.asset b/Assets/ScriptableObjects/SampleTrees/IntSampleTree.asset similarity index 100% rename from Assets/ScriptableObjects/IntSampleTree.asset rename to Assets/ScriptableObjects/SampleTrees/IntSampleTree.asset diff --git a/Assets/ScriptableObjects/IntSampleTree.asset.meta b/Assets/ScriptableObjects/SampleTrees/IntSampleTree.asset.meta similarity index 100% rename from Assets/ScriptableObjects/IntSampleTree.asset.meta rename to Assets/ScriptableObjects/SampleTrees/IntSampleTree.asset.meta diff --git a/Assets/Songs.meta b/Assets/Songs.meta new file mode 100644 index 0000000..c2c3916 --- /dev/null +++ b/Assets/Songs.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6feeb535242ea4444bdbc48b38c4798c +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Songs/HailPurdue.meta b/Assets/Songs/HailPurdue.meta new file mode 100644 index 0000000..7b9b9ad --- /dev/null +++ b/Assets/Songs/HailPurdue.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 85cb1e49b4435984e920da8dc93cc764 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Songs/HailPurdue/Hail-Purdue.wav b/Assets/Songs/HailPurdue/Hail-Purdue.wav new file mode 100644 index 0000000..e2f3c94 Binary files /dev/null and b/Assets/Songs/HailPurdue/Hail-Purdue.wav differ diff --git a/Assets/Songs/HailPurdue/Hail-Purdue.wav.meta b/Assets/Songs/HailPurdue/Hail-Purdue.wav.meta new file mode 100644 index 0000000..bf98a9b --- /dev/null +++ b/Assets/Songs/HailPurdue/Hail-Purdue.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 8359952b42d6e514a92a4fcbdc0ad344 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Songs/TestSong.meta b/Assets/Songs/TestSong.meta new file mode 100644 index 0000000..bb1f103 --- /dev/null +++ b/Assets/Songs/TestSong.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ee602f77a558b4ef4b7b27cba574c6f1 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Songs/TestSong/TestManifest.asset b/Assets/Songs/TestSong/TestManifest.asset new file mode 100644 index 0000000..c0593f5 --- /dev/null +++ b/Assets/Songs/TestSong/TestManifest.asset @@ -0,0 +1,18 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 318b60a35bddb77409b7ec4125f99b64, type: 3} + m_Name: TestManifest + m_EditorClassIdentifier: + SongName: + Difficulties: + - Level: 1 + Beatmap: {fileID: 11400000, guid: 9d66e8de59dee1246b742c0a1d1fdcdd, type: 2} diff --git a/Assets/Songs/TestSong/TestManifest.asset.meta b/Assets/Songs/TestSong/TestManifest.asset.meta new file mode 100644 index 0000000..6fa6d7c --- /dev/null +++ b/Assets/Songs/TestSong/TestManifest.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5ca5a381fbfa5de4f8f2680b32dfd1d8 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Songs/TestSong/TestSong.asset b/Assets/Songs/TestSong/TestSong.asset new file mode 100644 index 0000000..fd75085 --- /dev/null +++ b/Assets/Songs/TestSong/TestSong.asset @@ -0,0 +1,334 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4ce5ecd1e95d4474f8092d79730c87bd, type: 3} + m_Name: TestSong + m_EditorClassIdentifier: + SongName: + Author: + Album: + SongClip: {fileID: 8300000, guid: e78d73311ea7e42639f730aca264fbe8, type: 3} + Thumbnail: {fileID: 0} + PreviewStartTime: 0 + BaseBpm: 60 + OffsetSeconds: 0 + StartDelaySeconds: 2 + TimingPoints: [] + DifficultyName: + DifficultyLevel: 1 + DifficultyRating: 0 + Notes: + - Beat: 0 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: 2ea6a103-f0bc-4273-98fc-ec27007b4897 + - Beat: 1 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: 71f3b0d6-bb58-4bbf-8396-45088b05943b + - Beat: 2 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: 123523d2-ef7f-4542-ad07-23ed63b25255 + - Beat: 3 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: 42f0badf-bc50-4bd2-873c-79b97b300118 + - Beat: 3.5 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: defb15ed-d5a8-48ae-838d-6d4072dc5ecc + - Beat: 4 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: 981d33b3-c098-4193-a1e6-6047ef51ccff + - Beat: 5 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: 16ad27c9-b58b-4e1c-ab30-0709a3f1a99a + - Beat: 6 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: 7b4cfc8c-0bec-4b1b-a4f3-53df8fbde46b + - Beat: 7 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: 01f707d5-c224-417c-9bd7-13d67506bf38 + - Beat: 7.5 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: 84f83db2-850b-4d5a-836c-3f130e0e9a5b + - Beat: 8 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: 63c67e61-09da-47d8-8210-71ec7ca1b29f + - Beat: 9 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: 801af6d2-6550-41a3-9af7-0bf168d3ef2c + - Beat: 10 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: ee0d6700-c509-4e87-a8d0-04f2560cacba + - Beat: 11 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: fde61dd2-0d82-4e9b-b3c9-4ed768a49dc1 + - Beat: 11.5 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: d8766c0c-6910-4ee0-9fe9-4ace3c935f94 + - Beat: 12 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: 0c8e4bcb-a877-4ca5-9a86-921a4d81a574 + - Beat: 13 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: 3627c561-082d-4e50-81b3-11abb6d18307 + - Beat: 14 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: 7277bb2d-239e-4be3-80e4-df17ebd7afd1 + - Beat: 15 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: 4c009917-395b-4017-9e69-a3185580bf9b + - Beat: 15.5 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: 9fb9597a-4ade-49c6-a358-9cd730d1d21f + - Beat: 16 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: 0dbb509f-d199-498f-92ce-da1bd95a8932 + - Beat: 17 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: cc0c2a55-16cf-46ba-9e69-17eaed1bbbf1 + - Beat: 18 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: 14a1999b-44a0-4a51-9dfb-8e9d5c6b3cde + - Beat: 19 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: c7be1f26-bfd7-4deb-a281-b3c917e3a722 + - Beat: 19.5 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: 4fb9512a-5a4b-46f5-bc2b-2e1825671406 + - Beat: 20 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: 5a90cf0e-f9b0-42ee-9999-0dd228ee1e4e + - Beat: 21 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: f385187f-2b3d-499a-b3a1-404e43e9e460 + - Beat: 22 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: 8330c327-7e4b-4bab-82d0-584d87d923fe + - Beat: 23 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: 73867a6d-7802-4551-8afa-77f8499d0184 + - Beat: 23.5 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: d8485cf9-4042-43ba-b730-1cb4813203da + - Beat: 24 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: 2a441a69-8bf1-4722-9561-c9c0dfecd9b5 + - Beat: 25 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: 851c4a5c-dd00-4efd-8a75-fcc3a3a13f44 + - Beat: 26 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: 824c2d12-ff42-43ed-9846-23ddf8bb57c4 + - Beat: 27 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: 612a1ac6-88bc-4df7-adff-1c5dbb6ea2df + - Beat: 27.5 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: 577915b3-ff04-44b3-bb2a-51aada78cb1d + - Beat: 28 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: 3f9a65c1-d557-402f-a0a1-d61376a90792 + - Beat: 29 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: 9f32905c-cf00-4fe5-8109-b30dd10e6673 + - Beat: 30 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: 40f3cb9c-f2e6-44f3-bb5d-245c7b9e0cf1 + - Beat: 31 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: d5fa7319-5384-4a2f-bb96-150e69d58122 + - Beat: 31.5 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: ce1129b2-1003-4f6a-9a0f-2de668e05989 + - Beat: 32 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: 3b355cd2-59d3-4aef-806b-f7440a097124 + - Beat: 33 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: 088efe9f-af73-4e5d-81ab-2df58779d730 + - Beat: 34 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: 9fdfdefd-3707-4c62-bb02-1b1b6f8f3f05 + - Beat: 35 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: 8a5429d0-7fb6-40c1-a7f2-76c1a79c3b29 + - Beat: 35.5 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: fe5fa10c-ffc0-4080-a1c3-209ebb925d57 + - Beat: 36 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: 08c5e53e-0079-479b-abbf-597098b78caa + - Beat: 37 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: b7ebca3d-0d8e-4bc1-a780-bf879320a3b7 + - Beat: 38 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: 2b45a89c-7b3e-4491-a138-67b152b1a214 + - Beat: 39 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: d458a1fa-aba7-4e18-b213-d0c58253b7f3 + - Beat: 39.5 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: 1d0a2877-f875-4478-9bf5-2608984a6109 + - Beat: 40 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: 47687dc7-ded6-41a0-9ac3-370d64f823cf + - Beat: 41 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: 5236bcd9-215c-4ca1-8d83-cb4bc04508f6 + - Beat: 42 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: 5ba98406-3af6-4155-ac43-c8d284ec2aac + - Beat: 43 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: 25620c3b-9407-41d6-ab8a-738c9e5a580d + - Beat: 43.5 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: 7d3d14ed-7de9-4855-8e31-cd86c6f41e22 + - Beat: 44 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: 83a96792-68d9-4d4b-8d42-ad51933ce6ee + - Beat: 45 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: 404d509b-60b7-47cf-9dd9-0956a9d2c5ce + - Beat: 46 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: e0f7bc41-bd2c-4fcc-94d8-1014e173f418 + - Beat: 47 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: 47132679-3435-46bd-af5d-c0f356acbc02 + - Beat: 47.5 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: 3ae025cf-5315-4908-9286-db0e4f605824 + - Beat: 48 + Lane: 0 + NoteType: 0 + HoldDuration: 0 + EditorId: 35a862a7-bc51-4c82-87f7-f405a75dc950 + KeyPoints: [] diff --git a/Assets/Songs/TestSong/TestSong.asset.meta b/Assets/Songs/TestSong/TestSong.asset.meta new file mode 100644 index 0000000..bcd37d3 --- /dev/null +++ b/Assets/Songs/TestSong/TestSong.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 9d66e8de59dee1246b742c0a1d1fdcdd +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Songs/TestSong/TestSong.mp3 b/Assets/Songs/TestSong/TestSong.mp3 new file mode 100644 index 0000000..4ea4911 Binary files /dev/null and b/Assets/Songs/TestSong/TestSong.mp3 differ diff --git a/Assets/Songs/TestSong/TestSong.mp3.meta b/Assets/Songs/TestSong/TestSong.mp3.meta new file mode 100644 index 0000000..c21e411 --- /dev/null +++ b/Assets/Songs/TestSong/TestSong.mp3.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: e78d73311ea7e42639f730aca264fbe8 +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tree/Controller/TreeGameController.cs b/Assets/Tree/Controller/TreeGameController.cs index a1da826..6065442 100644 --- a/Assets/Tree/Controller/TreeGameController.cs +++ b/Assets/Tree/Controller/TreeGameController.cs @@ -14,6 +14,12 @@ public class TreeGameController : MonoBehaviour [Header("Configuration")] [SerializeField] private TreeVisualizerConfig _config; + [Header("Animation")] + [Tooltip("Sound effects and animation parameters for node reactions.")] + [SerializeField] private TreeAnimationConfig _animationConfig; + [Tooltip("AudioSource used to play one-shot sound effects.")] + [SerializeField] private AudioSource _audioSource; + [Header("Scene References")] [SerializeField] private GameObject _nodePrefab; [SerializeField] private CameraRig _cameraRig; @@ -49,7 +55,7 @@ public class TreeGameController : MonoBehaviour private void Awake() { var rules = new BSTInsertionRules(); - var hooks = new DefaultNavigationHooks(); + var hooks = new TreeAnimationHooks(_animationConfig, _audioSource, () => _carrierView); _navigator = new TreeNavigator(rules, hooks); _generator = new IntValueGenerator(_config.MinValue, _config.MaxValue); @@ -122,6 +128,8 @@ private void PopulateSampleTree() private void BeginNextInsertion() { + _generator.Min = _config.MinValue; + _generator.Max = _config.MaxValue; int value = _generator.GenerateNext(); _navigator.BeginInsertion(value).Forget(); } diff --git a/Assets/Tree/Generation/IntValueGenerator.cs b/Assets/Tree/Generation/IntValueGenerator.cs index dbbec0f..3df2211 100644 --- a/Assets/Tree/Generation/IntValueGenerator.cs +++ b/Assets/Tree/Generation/IntValueGenerator.cs @@ -11,10 +11,14 @@ public class IntValueGenerator : IValueGenerator { #region Fields - private readonly int _min; - private readonly int _max; private readonly Random _random; + /** Inclusive lower bound. Settable so live config changes propagate without recreating the generator. */ + public int Min { get; set; } + + /** Inclusive upper bound. Settable so live config changes propagate without recreating the generator. */ + public int Max { get; set; } + #endregion #region Constructor @@ -27,8 +31,8 @@ public class IntValueGenerator : IValueGenerator */ public IntValueGenerator(int min = 1, int max = 99) { - _min = min; - _max = max; + Min = min; + Max = max; _random = new Random(); } @@ -36,8 +40,8 @@ public IntValueGenerator(int min = 1, int max = 99) #region IValueGenerator Implementation - /** Returns a random integer in [min, max]. */ - public int GenerateNext() => _random.Next(_min, _max + 1); + /** Returns a random integer in [Min, Max]. */ + public int GenerateNext() => _random.Next(Min, Max + 1); #endregion } diff --git a/Assets/Tree/Hooks/TreeAnimationHooks.cs b/Assets/Tree/Hooks/TreeAnimationHooks.cs new file mode 100644 index 0000000..d6636e2 --- /dev/null +++ b/Assets/Tree/Hooks/TreeAnimationHooks.cs @@ -0,0 +1,162 @@ +using System; +using Cysharp.Threading.Tasks; +using PrimeTween; +using UnityEngine; + +/** + * Navigation hooks that drive sound effects, node pulse, and wrong-input carrier + * bounce animations. + * + * The comparable value type stored in the tree. + */ +public class TreeAnimationHooks : DefaultNavigationHooks +{ + #region Dependencies + + private readonly TreeAnimationConfig _config; + private readonly AudioSource _audioSource; + private readonly Func _getCarrierView; + + #endregion + + #region Bounce State + + private TreeNodeView _bounceCarrier; + private Vector3 _bounceOrigin; + + #endregion + + #region Constructor + + /** + * Creates the hooks with the required collaborators. + * + * Animation parameters ScriptableObject. + * AudioSource used for all one-shot clips. + * + * Delegate that returns the current carrier view. Called at hook time rather + * than constructor time because the carrier is recreated each insertion round. + * + */ + public TreeAnimationHooks(TreeAnimationConfig config, AudioSource audioSource, Func getCarrierView) + { + _config = config; + _audioSource = audioSource; + _getCarrierView = getCarrierView; + } + + #endregion + + #region Bounce Cancellation + + /** */ + public override UniTask OnBeforeNavigateLeft(NavigationContext ctx) + { + CancelBounce(); + return UniTask.CompletedTask; + } + + /** */ + public override UniTask OnBeforeNavigateRight(NavigationContext ctx) + { + CancelBounce(); + return UniTask.CompletedTask; + } + + /** */ + public override UniTask OnBeforePlace(NavigationContext ctx) + { + CancelBounce(); + return UniTask.CompletedTask; + } + + #endregion + + #region Hit Sound + + /** */ + public override UniTask OnAfterNavigateLeft(NavigationContext ctx) + { + PlayHit(); + return UniTask.CompletedTask; + } + + /** */ + public override UniTask OnAfterNavigateRight(NavigationContext ctx) + { + PlayHit(); + return UniTask.CompletedTask; + } + + /** */ + public override UniTask OnAfterPlace(NavigationContext ctx) + { + PlayHit(); + return UniTask.CompletedTask; + } + + #endregion + + #region Wrong Input + + /** */ + public override async UniTask OnInvalidAction(NavigationContext ctx, NavigationAction attempted) + { + if (_config.WrongInputClip != null) + _audioSource.PlayOneShot(_config.WrongInputClip, _config.WrongInputVolume); + + TreeNodeView carrier = _getCarrierView(); + if (carrier == null) return; + + _bounceCarrier = carrier; + _bounceOrigin = carrier.transform.position; + + RunBounceAsync(carrier, attempted).Forget(); + + if (_config.WrongInputLockoutDuration > 0f) + await UniTask.Delay(TimeSpan.FromSeconds(_config.WrongInputLockoutDuration)); + } + + #endregion + + #region Private Helpers + + private async UniTaskVoid RunBounceAsync(TreeNodeView carrier, NavigationAction attempted) + { + Vector3 origin = _bounceOrigin; + Vector3 delta = attempted switch + { + NavigationAction.Left => Vector3.left * _config.BounceOffset, + NavigationAction.Right => Vector3.right * _config.BounceOffset, + NavigationAction.Place => Vector3.down * _config.BounceOffset, + _ => Vector3.zero + }; + + float outTime = _config.BounceDuration * 0.35f; + float returnTime = _config.BounceDuration * 0.65f; + + await Tween.Position(carrier.transform, origin + delta, outTime, _config.BounceOutEase); + + if (_bounceCarrier == carrier) + await Tween.Position(carrier.transform, origin, returnTime, _config.BounceReturnEase); + + if (_bounceCarrier == carrier) + _bounceCarrier = null; + } + + private void CancelBounce() + { + if (_bounceCarrier == null) return; + Tween.StopAll(_bounceCarrier.gameObject); + _bounceCarrier.SnapTo(new Vector2(_bounceOrigin.x, _bounceOrigin.y)); + _bounceCarrier = null; + } + + private void PlayHit() + { + if (_config.HitClip != null) + _audioSource.PlayOneShot(_config.HitClip, _config.HitVolume); + } + + #endregion +} diff --git a/Assets/Tree/Hooks/TreeAnimationHooks.cs.meta b/Assets/Tree/Hooks/TreeAnimationHooks.cs.meta new file mode 100644 index 0000000..c36e145 --- /dev/null +++ b/Assets/Tree/Hooks/TreeAnimationHooks.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a469602f674d64a46b1f160acc7644f4 \ No newline at end of file diff --git a/Assets/Tree/Navigation/TreeNavigator.cs b/Assets/Tree/Navigation/TreeNavigator.cs index b8c6078..9305f46 100644 --- a/Assets/Tree/Navigation/TreeNavigator.cs +++ b/Assets/Tree/Navigation/TreeNavigator.cs @@ -28,6 +28,9 @@ public class TreeNavigator /** Running count of invalid actions the player has taken across all sessions. */ public int ErrorCount { get; private set; } + /** Returns the action required at the current cursor position. */ + public NavigationAction GetRequiredAction() => _rules.GetRequiredAction(CurrentContext); + #endregion #region Events diff --git a/Assets/Tree/Visual/TreeAnimationConfig.cs b/Assets/Tree/Visual/TreeAnimationConfig.cs new file mode 100644 index 0000000..1628b7b --- /dev/null +++ b/Assets/Tree/Visual/TreeAnimationConfig.cs @@ -0,0 +1,135 @@ +using PrimeTween; +using UnityEngine; + +/** + * ScriptableObject controlling all animation parameters for tree node reactions: + * sound effects, BPM-driven node pulsing, and wrong-input carrier bounce. + * + */ +[CreateAssetMenu(menuName = "Treeformance/TreeAnimationConfig", fileName = "TreeAnimationConfig")] +public class TreeAnimationConfig : ScriptableObject +{ + #region Sound Effects + + [Header("Sound Effects")] + [Tooltip("Played when the player navigates correctly (left, right, or place).")] + [SerializeField] private AudioClip _hitClip; + + [Tooltip("Played when the player inputs an incorrect action.")] + [SerializeField] private AudioClip _wrongInputClip; + + [Tooltip("Played when a rhythm note is missed entirely.")] + [SerializeField] private AudioClip _missClip; + + [Tooltip("Played when a button is pressed outside the hit window (off-beat click with no note present).")] + [SerializeField] private AudioClip _offBeatClip; + + [Tooltip("Volume for the hit sound.")] + [SerializeField] [Range(0f, 1f)] private float _hitVolume = 0.8f; + + [Tooltip("Volume for the wrong-input sound.")] + [SerializeField] [Range(0f, 1f)] private float _wrongInputVolume = 0.8f; + + [Tooltip("Volume for the miss sound.")] + [SerializeField] [Range(0f, 1f)] private float _missVolume = 0.8f; + + [Tooltip("Volume for the off-beat click sound.")] + [SerializeField] [Range(0f, 1f)] private float _offBeatVolume = 0.6f; + + #endregion + + #region Node Pulse + + [Header("Node Pulse")] + [Tooltip("Peak scale multiplier during a pulse. 1.15 = 15% larger at peak.")] + [SerializeField] private float _pulseScaleMultiplier = 1.15f; + + [Tooltip("Total duration of one pulse cycle (expand + contract), in seconds.")] + [SerializeField] private float _pulseDuration = 0.12f; + + [Tooltip("Easing applied to each half of the pulse.")] + [SerializeField] private Ease _pulseEase = Ease.OutQuad; + + [Tooltip("Beat multiplier for node pulsing. " + + "1 = every beat (full-time), 2 = every half-beat (double-time), 0.5 = every 2 beats (half-time).")] + [SerializeField] private float _beatPulseMultiplier = 1f; + + #endregion + + #region Wrong Input Bounce + + [Header("Wrong Input Bounce")] + [Tooltip("How far the carrier slides in world units before springing back.")] + [SerializeField] private float _bounceOffset = 0.3f; + + [Tooltip("Total duration of the bounce animation (slide out + spring back), in seconds.")] + [SerializeField] private float _bounceDuration = 0.25f; + + [Tooltip("Easing for the slide-away portion of the bounce.")] + [SerializeField] private Ease _bounceOutEase = Ease.OutQuad; + + [Tooltip("Easing for the spring-return portion of the bounce.")] + [SerializeField] private Ease _bounceReturnEase = Ease.OutElastic; + + [Tooltip("How long correct input is blocked after a wrong action, in seconds. " + + "Set to 0 to allow immediate re-input while the bounce plays visually. " + + "Values equal to or greater than BounceDuration lock the player out for the full animation.")] + [SerializeField] private float _wrongInputLockoutDuration = 0.15f; + + #endregion + + #region Properties + + /** Clip played on correct navigation. */ + public AudioClip HitClip => _hitClip; + + /** Clip played on incorrect input. */ + public AudioClip WrongInputClip => _wrongInputClip; + + /** Clip played when a rhythm note is missed. */ + public AudioClip MissClip => _missClip; + + /** Clip played on an off-beat button press. */ + public AudioClip OffBeatClip => _offBeatClip; + + /** Volume for hit sounds. */ + public float HitVolume => _hitVolume; + + /** Volume for wrong-input sounds. */ + public float WrongInputVolume => _wrongInputVolume; + + /** Volume for miss sounds. */ + public float MissVolume => _missVolume; + + /** Volume for off-beat click sounds. */ + public float OffBeatVolume => _offBeatVolume; + + /** Peak scale factor during a node pulse. */ + public float PulseScaleMultiplier => _pulseScaleMultiplier; + + /** Total duration of a single pulse cycle. */ + public float PulseDuration => _pulseDuration; + + /** Easing for each pulse half. */ + public Ease PulseEase => _pulseEase; + + /** Multiplier applied to the beat counter when determining pulse frequency. */ + public float BeatPulseMultiplier => _beatPulseMultiplier; + + /** World-unit slide distance for the wrong-input bounce. */ + public float BounceOffset => _bounceOffset; + + /** Total bounce animation duration. */ + public float BounceDuration => _bounceDuration; + + /** Easing for the slide-out phase of the bounce. */ + public Ease BounceOutEase => _bounceOutEase; + + /** Easing for the spring-back phase of the bounce. */ + public Ease BounceReturnEase => _bounceReturnEase; + + /** Seconds correct input is blocked after a wrong action. Zero means no lockout. */ + public float WrongInputLockoutDuration => _wrongInputLockoutDuration; + + #endregion +} diff --git a/Assets/Tree/Visual/TreeAnimationConfig.cs.meta b/Assets/Tree/Visual/TreeAnimationConfig.cs.meta new file mode 100644 index 0000000..1260b70 --- /dev/null +++ b/Assets/Tree/Visual/TreeAnimationConfig.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 08d88fdfd5135499187cd39a1423b188 \ No newline at end of file diff --git a/Assets/Tree/Visual/TreeNodeView.cs b/Assets/Tree/Visual/TreeNodeView.cs index df08b6b..368c551 100644 --- a/Assets/Tree/Visual/TreeNodeView.cs +++ b/Assets/Tree/Visual/TreeNodeView.cs @@ -24,6 +24,8 @@ public class TreeNodeView : MonoBehaviour private MaterialPropertyBlock _nodeBlock; private MaterialPropertyBlock _ringBlock; + private Vector3 _baseScale; + private static readonly int BaseColorId = Shader.PropertyToID("_BaseColor"); #endregion @@ -42,6 +44,7 @@ public void Initialize(TreeVisualizerConfig config) _config = config; _nodeBlock = new MaterialPropertyBlock(); _ringBlock = new MaterialPropertyBlock(); + _baseScale = transform.localScale; SetState(NodeVisualState.Normal); } @@ -107,6 +110,23 @@ public async UniTask MoveToAsync(Vector2 position, float duration, Ease ease) await Tween.Position(transform, target, duration, ease); } + /** + * Scales the node to a peak then returns it to its base scale. + * Safe to fire-and-forget; a subsequent call while one is already running + * will be overridden by PrimeTween on the same transform property. + * + * Peak scale relative to base scale. + * Total duration of the expand + contract cycle. + * Easing applied to each half of the cycle. + */ + public async UniTask PulseAsync(float scaleMultiplier, float duration, Ease ease) + { + Vector3 peak = _baseScale * scaleMultiplier; + float half = duration * 0.5f; + await Tween.Scale(transform, peak, half, ease); + await Tween.Scale(transform, _baseScale, half, ease); + } + /** * Instantly repositions the node to the target world-space XY position. * Z is preserved.