From c85a8871e7d5654e121e2428750ce4b265bc2215 Mon Sep 17 00:00:00 2001 From: Joe Anderson Date: Sun, 14 Dec 2025 18:15:04 +0000 Subject: [PATCH 01/12] Implement scene with basic arrow movement --- Assets/Unity/Scenes/Test Rhythm Game.unity | 1146 +++++++++++++++++ .../Unity/Scenes/Test Rhythm Game.unity.meta | 7 + Assets/src/RhythmGameInputTracks.cs | 74 ++ Assets/src/RhythmGameInputTracks.cs.meta | 11 + 4 files changed, 1238 insertions(+) create mode 100644 Assets/Unity/Scenes/Test Rhythm Game.unity create mode 100644 Assets/Unity/Scenes/Test Rhythm Game.unity.meta create mode 100644 Assets/src/RhythmGameInputTracks.cs create mode 100644 Assets/src/RhythmGameInputTracks.cs.meta diff --git a/Assets/Unity/Scenes/Test Rhythm Game.unity b/Assets/Unity/Scenes/Test Rhythm Game.unity new file mode 100644 index 00000000..e672e99e --- /dev/null +++ b/Assets/Unity/Scenes/Test Rhythm Game.unity @@ -0,0 +1,1146 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 00000000000000000000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 9 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 3 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 12 + m_GIWorkflowMode: 1 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 0 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 12 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_FinalGather: 0 + m_FinalGatherFiltering: 1 + m_FinalGatherRayCount: 256 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 1 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 500 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 500 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 2 + m_PVRDenoiserTypeDirect: 0 + m_PVRDenoiserTypeIndirect: 0 + m_PVRDenoiserTypeAO: 0 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 0 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 5 + m_PVRFilteringGaussRadiusAO: 2 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 + m_LightingDataAsset: {fileID: 0} + m_LightingSettings: {fileID: 4890085278179872738, guid: d2d0db6e4370340da8f48ad4806cf49b, + type: 2} +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 3 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + buildHeightMesh: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} +--- !u!1 &67349635 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 67349636} + - component: {fileID: 67349638} + - component: {fileID: 67349637} + - component: {fileID: 67349639} + m_Layer: 5 + m_Name: Pop Pixie + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &67349636 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 67349635} + 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: 488498665} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 384.00006, y: 0} + m_SizeDelta: {x: -767.9999, y: 720} + m_Pivot: {x: 0.5, y: 0} +--- !u!114 &67349637 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 67349635} + 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: 21300000, guid: a1102afe6498b4cd49da0d9cff4a20b0, type: 3} + m_Type: 0 + m_PreserveAspect: 1 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &67349638 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 67349635} + m_CullTransparentMesh: 1 +--- !u!114 &67349639 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 67349635} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 68a817e8c6bf94c38be7b0ea3a19cc6a, type: 3} + m_Name: + m_EditorClassIdentifier: + Transform: {fileID: 67349636} + BaseScale: 1 + OscillationSpeed: 12.566371 + OscillationScale: 0.025 + XAxis: 1 + YAxis: 1 +--- !u!1 &250930356 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 250930357} + - component: {fileID: 250930359} + - component: {fileID: 250930358} + m_Layer: 5 + m_Name: Down + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &250930357 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 250930356} + m_LocalRotation: {x: 0, y: 0, z: 0.7071068, w: 0.7071068} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 452012667} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 90} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 60, y: 60} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &250930358 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 250930356} + 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: 21300000, guid: 7e57f5a3a009146d4966646e4752502c, type: 3} + m_Type: 0 + m_PreserveAspect: 1 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &250930359 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 250930356} + m_CullTransparentMesh: 1 +--- !u!1 &327090774 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 327090775} + - component: {fileID: 327090777} + - component: {fileID: 327090776} + m_Layer: 5 + m_Name: Right + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &327090775 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 327090774} + m_LocalRotation: {x: 0, y: 0, z: 1, w: 0} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 452012667} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 180} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 60, y: 60} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &327090776 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 327090774} + 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: 21300000, guid: 7e57f5a3a009146d4966646e4752502c, type: 3} + m_Type: 0 + m_PreserveAspect: 1 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &327090777 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 327090774} + m_CullTransparentMesh: 1 +--- !u!1 &452012666 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 452012667} + - component: {fileID: 452012669} + - component: {fileID: 452012668} + m_Layer: 5 + m_Name: Input Targets + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &452012667 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 452012666} + 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: 787737352} + - {fileID: 250930357} + - {fileID: 2014153219} + - {fileID: 327090775} + m_Father: {fileID: 1489695913} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 1} + m_AnchorMax: {x: 0.5, y: 1} + m_AnchoredPosition: {x: 0, y: -100} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 1} +--- !u!114 &452012668 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 452012666} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 3245ec927659c4140ac4f8d17403cc18, type: 3} + m_Name: + m_EditorClassIdentifier: + m_HorizontalFit: 1 + m_VerticalFit: 1 +--- !u!114 &452012669 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 452012666} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 30649d3a9faa99c48a7b1166b86bf2a0, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Padding: + m_Left: 0 + m_Right: 0 + m_Top: 0 + m_Bottom: 0 + m_ChildAlignment: 0 + m_Spacing: 40 + m_ChildForceExpandWidth: 1 + m_ChildForceExpandHeight: 1 + m_ChildControlWidth: 0 + m_ChildControlHeight: 0 + m_ChildScaleWidth: 0 + m_ChildScaleHeight: 0 + m_ReverseArrangement: 0 +--- !u!1 &488498664 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 488498665} + - component: {fileID: 488498669} + - component: {fileID: 488498668} + - component: {fileID: 488498667} + - component: {fileID: 488498666} + m_Layer: 5 + m_Name: Viewport + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &488498665 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 488498664} + 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: 1489695913} + - {fileID: 67349636} + m_Father: {fileID: 740930168} + 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: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &488498666 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 488498664} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 3312d7739989d2b4e91e6319e9a96d76, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Padding: {x: 0, y: 0, z: 0, w: 0} + m_Softness: {x: 0, y: 0} +--- !u!114 &488498667 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 488498664} + 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.101960786} + 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: 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 &488498668 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 488498664} + m_CullTransparentMesh: 1 +--- !u!114 &488498669 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 488498664} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 86710e43de46f6f4bac7c8e50813a599, type: 3} + m_Name: + m_EditorClassIdentifier: + m_AspectMode: 3 + m_AspectRatio: 1.7777778 +--- !u!1 &740930167 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 740930168} + - component: {fileID: 740930171} + - component: {fileID: 740930170} + - component: {fileID: 740930169} + m_Layer: 5 + m_Name: Canvas + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &740930168 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 740930167} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 0, y: 0, z: 0} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 488498665} + m_Father: {fileID: 0} + 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: 0, y: 0} + m_Pivot: {x: 0, y: 0} +--- !u!114 &740930169 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 740930167} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: dc42784cf147c0c48a680349fa168899, type: 3} + m_Name: + m_EditorClassIdentifier: + m_IgnoreReversedGraphics: 1 + m_BlockingObjects: 0 + m_BlockingMask: + serializedVersion: 2 + m_Bits: 4294967295 +--- !u!114 &740930170 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 740930167} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 0cd44c1031e13a943bb63640046fad76, type: 3} + m_Name: + m_EditorClassIdentifier: + m_UiScaleMode: 1 + m_ReferencePixelsPerUnit: 100 + m_ScaleFactor: 1 + m_ReferenceResolution: {x: 1920, y: 1080} + m_ScreenMatchMode: 1 + m_MatchWidthOrHeight: 0 + m_PhysicalUnit: 3 + m_FallbackScreenDPI: 96 + m_DefaultSpriteDPI: 96 + m_DynamicPixelsPerUnit: 1 + m_PresetInfoIsWorld: 0 +--- !u!223 &740930171 +Canvas: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 740930167} + m_Enabled: 1 + serializedVersion: 3 + m_RenderMode: 1 + m_Camera: {fileID: 898746628} + m_PlaneDistance: 100 + m_PixelPerfect: 0 + m_ReceivesEvents: 1 + m_OverrideSorting: 0 + m_OverridePixelPerfect: 0 + m_SortingBucketNormalizedSize: 0 + m_VertexColorAlwaysGammaSpace: 0 + m_AdditionalShaderChannelsFlag: 25 + m_UpdateRectTransformForStandalone: 0 + m_SortingLayerID: 0 + m_SortingOrder: 0 + m_TargetDisplay: 0 +--- !u!1 &787737351 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 787737352} + - component: {fileID: 787737354} + - component: {fileID: 787737353} + m_Layer: 5 + m_Name: Left + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &787737352 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 787737351} + 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: 452012667} + 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: 60, y: 60} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &787737353 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 787737351} + 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: 21300000, guid: 7e57f5a3a009146d4966646e4752502c, type: 3} + m_Type: 0 + m_PreserveAspect: 1 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &787737354 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 787737351} + m_CullTransparentMesh: 1 +--- !u!1001 &898746627 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + serializedVersion: 3 + m_TransformParent: {fileID: 0} + m_Modifications: + - target: {fileID: 1665900140710026, guid: db7b40b7e04a74afa95cc6e82a722496, type: 3} + propertyPath: m_Name + value: Main Camera + objectReference: {fileID: 0} + - target: {fileID: 4198617257405980, guid: db7b40b7e04a74afa95cc6e82a722496, type: 3} + propertyPath: m_LocalPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4198617257405980, guid: db7b40b7e04a74afa95cc6e82a722496, type: 3} + propertyPath: m_LocalPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4198617257405980, guid: db7b40b7e04a74afa95cc6e82a722496, type: 3} + propertyPath: m_LocalPosition.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4198617257405980, guid: db7b40b7e04a74afa95cc6e82a722496, type: 3} + propertyPath: m_LocalRotation.w + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 4198617257405980, guid: db7b40b7e04a74afa95cc6e82a722496, type: 3} + propertyPath: m_LocalRotation.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4198617257405980, guid: db7b40b7e04a74afa95cc6e82a722496, type: 3} + propertyPath: m_LocalRotation.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4198617257405980, guid: db7b40b7e04a74afa95cc6e82a722496, type: 3} + propertyPath: m_LocalRotation.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4198617257405980, guid: db7b40b7e04a74afa95cc6e82a722496, type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4198617257405980, guid: db7b40b7e04a74afa95cc6e82a722496, type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4198617257405980, guid: db7b40b7e04a74afa95cc6e82a722496, type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 114253518789369406, guid: db7b40b7e04a74afa95cc6e82a722496, + type: 3} + propertyPath: m_Enabled + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 374538616986019307, guid: db7b40b7e04a74afa95cc6e82a722496, + type: 3} + propertyPath: m_Enabled + value: 0 + objectReference: {fileID: 0} + m_RemovedComponents: + - {fileID: 374538616986019307, guid: db7b40b7e04a74afa95cc6e82a722496, type: 3} + - {fileID: 114253518789369406, guid: db7b40b7e04a74afa95cc6e82a722496, type: 3} + m_RemovedGameObjects: [] + m_AddedGameObjects: [] + m_AddedComponents: [] + m_SourcePrefab: {fileID: 100100000, guid: db7b40b7e04a74afa95cc6e82a722496, type: 3} +--- !u!20 &898746628 stripped +Camera: + m_CorrespondingSourceObject: {fileID: 20682073832733502, guid: db7b40b7e04a74afa95cc6e82a722496, + type: 3} + m_PrefabInstance: {fileID: 898746627} + m_PrefabAsset: {fileID: 0} +--- !u!1 &959757541 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 959757542} + - component: {fileID: 959757544} + - component: {fileID: 959757543} + m_Layer: 5 + m_Name: Model Input + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 0 +--- !u!224 &959757542 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 959757541} + 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: 1489695913} + 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: 60, y: 60} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &959757543 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 959757541} + 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: 21300000, guid: 7e57f5a3a009146d4966646e4752502c, type: 3} + m_Type: 0 + m_PreserveAspect: 1 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &959757544 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 959757541} + m_CullTransparentMesh: 1 +--- !u!1 &1489695912 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1489695913} + - component: {fileID: 1489695914} + - component: {fileID: 1489695916} + - component: {fileID: 1489695915} + - component: {fileID: 1489695917} + m_Layer: 5 + m_Name: Input Tracks + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1489695913 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1489695912} + 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: 452012667} + - {fileID: 959757542} + m_Father: {fileID: 488498665} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0} + m_AnchorMax: {x: 0.5, y: 1} + m_AnchoredPosition: {x: -422, y: 0} + m_SizeDelta: {x: 460, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &1489695914 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1489695912} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 8112514d52eaa43adb5d0c47e84a79ed, type: 3} + m_Name: + m_EditorClassIdentifier: + SpawnInputInterval: 0.5 + InputSpeed: 540 + LeftTarget: {fileID: 787737352} + DownTarget: {fileID: 250930357} + UpTarget: {fileID: 2014153219} + RightTarget: {fileID: 327090775} + ModelInput: {fileID: 959757541} +--- !u!114 &1489695915 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1489695912} + 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, g: 0, b: 0, a: 0.5019608} + 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: 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 &1489695916 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1489695912} + m_CullTransparentMesh: 1 +--- !u!114 &1489695917 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1489695912} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 3245ec927659c4140ac4f8d17403cc18, type: 3} + m_Name: + m_EditorClassIdentifier: + m_HorizontalFit: 0 + m_VerticalFit: 0 +--- !u!1001 &1945062054 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + serializedVersion: 3 + m_TransformParent: {fileID: 0} + m_Modifications: + - target: {fileID: 2254461912267086629, guid: 310bd337419114b4595e5056c7463838, + type: 3} + propertyPath: m_Name + value: Scene Essentials + objectReference: {fileID: 0} + - target: {fileID: 5636461190564298426, guid: 310bd337419114b4595e5056c7463838, + type: 3} + propertyPath: m_LocalPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 5636461190564298426, guid: 310bd337419114b4595e5056c7463838, + type: 3} + propertyPath: m_LocalPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 5636461190564298426, guid: 310bd337419114b4595e5056c7463838, + type: 3} + propertyPath: m_LocalPosition.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 5636461190564298426, guid: 310bd337419114b4595e5056c7463838, + type: 3} + propertyPath: m_LocalRotation.w + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 5636461190564298426, guid: 310bd337419114b4595e5056c7463838, + type: 3} + propertyPath: m_LocalRotation.x + value: -0 + objectReference: {fileID: 0} + - target: {fileID: 5636461190564298426, guid: 310bd337419114b4595e5056c7463838, + type: 3} + propertyPath: m_LocalRotation.y + value: -0 + objectReference: {fileID: 0} + - target: {fileID: 5636461190564298426, guid: 310bd337419114b4595e5056c7463838, + type: 3} + propertyPath: m_LocalRotation.z + value: -0 + objectReference: {fileID: 0} + - target: {fileID: 5636461190564298426, guid: 310bd337419114b4595e5056c7463838, + type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 5636461190564298426, guid: 310bd337419114b4595e5056c7463838, + type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 5636461190564298426, guid: 310bd337419114b4595e5056c7463838, + type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_RemovedGameObjects: [] + m_AddedGameObjects: [] + m_AddedComponents: [] + m_SourcePrefab: {fileID: 100100000, guid: 310bd337419114b4595e5056c7463838, type: 3} +--- !u!1 &2014153218 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2014153219} + - component: {fileID: 2014153221} + - component: {fileID: 2014153220} + m_Layer: 5 + m_Name: Up + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2014153219 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2014153218} + m_LocalRotation: {x: 0, y: 0, z: -0.7071068, w: 0.7071068} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 452012667} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: -90} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 60, y: 60} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &2014153220 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2014153218} + 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: 21300000, guid: 7e57f5a3a009146d4966646e4752502c, type: 3} + m_Type: 0 + m_PreserveAspect: 1 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &2014153221 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2014153218} + m_CullTransparentMesh: 1 +--- !u!1660057539 &9223372036854775807 +SceneRoots: + m_ObjectHideFlags: 0 + m_Roots: + - {fileID: 1945062054} + - {fileID: 898746627} + - {fileID: 740930168} diff --git a/Assets/Unity/Scenes/Test Rhythm Game.unity.meta b/Assets/Unity/Scenes/Test Rhythm Game.unity.meta new file mode 100644 index 00000000..1483d87b --- /dev/null +++ b/Assets/Unity/Scenes/Test Rhythm Game.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 78020175a1b404edd96e91f00bb0bcfb +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/src/RhythmGameInputTracks.cs b/Assets/src/RhythmGameInputTracks.cs new file mode 100644 index 00000000..6c927a67 --- /dev/null +++ b/Assets/src/RhythmGameInputTracks.cs @@ -0,0 +1,74 @@ +using System.Collections.Generic; +using UnityEngine; + +public enum RhythmGameInputType { + Left = 0, + Down = 1, + Up = 2, + Right = 3, +}; + +public class RhythmGameInputTracks : MonoBehaviour { + public float SpawnInputInterval, + InputSpeed; + public Transform LeftTarget, + DownTarget, + UpTarget, + RightTarget; + public GameObject ModelInput; + + private List SpawnedInputs = new(); + + /** + * The Y coordinate relative to this GameObject at which new inputs are + * spawned. + */ + private float SpawnY; + + void Start() { + // Spawn inputs just below the bottom of the screen + float inputSize = ModelInput.GetComponent().rect.height; + SpawnY = GetComponent().rect.yMin - inputSize / 2f; + + AsyncTimer.BaseTime.SetInterval(SpawnInput, SpawnInputInterval, bindToGameObject: gameObject); + } + + void Update() { + Vector3 displacement = Vector2.up * InputSpeed * Time.deltaTime; + + SpawnedInputs.ForEach(input => { + input.transform.localPosition += displacement; + }); + } + + private void SpawnInput() { + GameObject input = Instantiate(ModelInput, transform); + input.SetActive(true); + SpawnedInputs.Add(input); + + RhythmGameInputType inputType = (RhythmGameInputType)Random.Range(0, 4); + Transform target = TargetForInputType(inputType); + float spawnX = target.localPosition.x; + + input.transform.localPosition = new Vector2(spawnX, SpawnY); + input.transform.localRotation = target.localRotation; + } + + private Transform TargetForInputType(RhythmGameInputType inputType) { + switch (inputType) { + case RhythmGameInputType.Left: + return LeftTarget; + + case RhythmGameInputType.Down: + return DownTarget; + + case RhythmGameInputType.Up: + return UpTarget; + + case RhythmGameInputType.Right: + return RightTarget; + } + + throw new System.ArgumentException("Unexpected input type"); + } +} diff --git a/Assets/src/RhythmGameInputTracks.cs.meta b/Assets/src/RhythmGameInputTracks.cs.meta new file mode 100644 index 00000000..08fda7d9 --- /dev/null +++ b/Assets/src/RhythmGameInputTracks.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8112514d52eaa43adb5d0c47e84a79ed +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From 9441c57db2f04efdfaad83e5db9e428f7e1fb1ae Mon Sep 17 00:00:00 2001 From: Joe Anderson Date: Mon, 15 Dec 2025 14:37:15 +0000 Subject: [PATCH 02/12] Add arrow sprites --- Assets/Unity/Scenes/Test Rhythm Game.unity | 106 ++++++++++++++----- Assets/images/Rhythm Game Arrow.png | Bin 0 -> 2110 bytes Assets/images/Rhythm Game Arrow.png.meta | 114 +++++++++++++++++++++ Assets/src/RhythmGameInputTracks.cs | 14 +-- 4 files changed, 201 insertions(+), 33 deletions(-) create mode 100644 Assets/images/Rhythm Game Arrow.png create mode 100644 Assets/images/Rhythm Game Arrow.png.meta diff --git a/Assets/Unity/Scenes/Test Rhythm Game.unity b/Assets/Unity/Scenes/Test Rhythm Game.unity index e672e99e..49ca28ea 100644 --- a/Assets/Unity/Scenes/Test Rhythm Game.unity +++ b/Assets/Unity/Scenes/Test Rhythm Game.unity @@ -228,6 +228,7 @@ GameObject: - component: {fileID: 250930357} - component: {fileID: 250930359} - component: {fileID: 250930358} + - component: {fileID: 250930360} m_Layer: 5 m_Name: Down m_TagString: Untagged @@ -242,17 +243,17 @@ RectTransform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 250930356} - m_LocalRotation: {x: 0, y: 0, z: 0.7071068, w: 0.7071068} + m_LocalRotation: {x: 0, y: 0, z: -0.7071068, w: 0.7071068} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 452012667} - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 90} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: -90} m_AnchorMin: {x: 0, y: 0} m_AnchorMax: {x: 0, y: 0} m_AnchoredPosition: {x: 0, y: 0} - m_SizeDelta: {x: 60, y: 60} + m_SizeDelta: {x: 120, y: 80} m_Pivot: {x: 0.5, y: 0.5} --- !u!114 &250930358 MonoBehaviour: @@ -267,14 +268,14 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: m_Material: {fileID: 0} - m_Color: {r: 1, g: 1, b: 1, a: 1} + m_Color: {r: 0.39999998, 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: 21300000, guid: 7e57f5a3a009146d4966646e4752502c, type: 3} + m_Sprite: {fileID: 21300000, guid: bfb48dca50c3049d4af953850214ffd9, type: 3} m_Type: 0 m_PreserveAspect: 1 m_FillCenter: 1 @@ -292,6 +293,18 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 250930356} m_CullTransparentMesh: 1 +--- !u!225 &250930360 +CanvasGroup: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 250930356} + m_Enabled: 1 + m_Alpha: 0.6 + m_Interactable: 1 + m_BlocksRaycasts: 1 + m_IgnoreParentGroups: 0 --- !u!1 &327090774 GameObject: m_ObjectHideFlags: 0 @@ -303,6 +316,7 @@ GameObject: - component: {fileID: 327090775} - component: {fileID: 327090777} - component: {fileID: 327090776} + - component: {fileID: 327090778} m_Layer: 5 m_Name: Right m_TagString: Untagged @@ -317,17 +331,17 @@ RectTransform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 327090774} - m_LocalRotation: {x: 0, y: 0, z: 1, w: 0} + 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: 452012667} - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 180} + 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: 60, y: 60} + m_SizeDelta: {x: 120, y: 80} m_Pivot: {x: 0.5, y: 0.5} --- !u!114 &327090776 MonoBehaviour: @@ -342,14 +356,14 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: m_Material: {fileID: 0} - m_Color: {r: 1, g: 1, b: 1, a: 1} + m_Color: {r: 1, g: 0.39999998, b: 0.39999998, 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: 21300000, guid: 7e57f5a3a009146d4966646e4752502c, type: 3} + m_Sprite: {fileID: 21300000, guid: bfb48dca50c3049d4af953850214ffd9, type: 3} m_Type: 0 m_PreserveAspect: 1 m_FillCenter: 1 @@ -367,6 +381,18 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 327090774} m_CullTransparentMesh: 1 +--- !u!225 &327090778 +CanvasGroup: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 327090774} + m_Enabled: 1 + m_Alpha: 0.6 + m_Interactable: 1 + m_BlocksRaycasts: 1 + m_IgnoreParentGroups: 0 --- !u!1 &452012666 GameObject: m_ObjectHideFlags: 0 @@ -405,7 +431,7 @@ RectTransform: m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0.5, y: 1} m_AnchorMax: {x: 0.5, y: 1} - m_AnchoredPosition: {x: 0, y: -100} + m_AnchoredPosition: {x: 0, y: -40} m_SizeDelta: {x: 0, y: 0} m_Pivot: {x: 0.5, y: 1} --- !u!114 &452012668 @@ -440,7 +466,7 @@ MonoBehaviour: m_Top: 0 m_Bottom: 0 m_ChildAlignment: 0 - m_Spacing: 40 + m_Spacing: 20 m_ChildForceExpandWidth: 1 m_ChildForceExpandHeight: 1 m_ChildControlWidth: 0 @@ -668,6 +694,7 @@ GameObject: - component: {fileID: 787737352} - component: {fileID: 787737354} - component: {fileID: 787737353} + - component: {fileID: 787737355} m_Layer: 5 m_Name: Left m_TagString: Untagged @@ -682,17 +709,17 @@ RectTransform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 787737351} - m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalRotation: {x: 0, y: 0, z: 1, w: 0} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 452012667} - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 180} m_AnchorMin: {x: 0, y: 0} m_AnchorMax: {x: 0, y: 0} m_AnchoredPosition: {x: 0, y: 0} - m_SizeDelta: {x: 60, y: 60} + m_SizeDelta: {x: 120, y: 80} m_Pivot: {x: 0.5, y: 0.5} --- !u!114 &787737353 MonoBehaviour: @@ -707,14 +734,14 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: m_Material: {fileID: 0} - m_Color: {r: 1, g: 1, b: 1, a: 1} + m_Color: {r: 0.7, g: 0.39999998, 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: 21300000, guid: 7e57f5a3a009146d4966646e4752502c, type: 3} + m_Sprite: {fileID: 21300000, guid: bfb48dca50c3049d4af953850214ffd9, type: 3} m_Type: 0 m_PreserveAspect: 1 m_FillCenter: 1 @@ -732,6 +759,18 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 787737351} m_CullTransparentMesh: 1 +--- !u!225 &787737355 +CanvasGroup: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 787737351} + m_Enabled: 1 + m_Alpha: 0.6 + m_Interactable: 1 + m_BlocksRaycasts: 1 + m_IgnoreParentGroups: 0 --- !u!1001 &898746627 PrefabInstance: m_ObjectHideFlags: 0 @@ -842,7 +881,7 @@ RectTransform: 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: 60, y: 60} + m_SizeDelta: {x: 120, y: 80} m_Pivot: {x: 0.5, y: 0.5} --- !u!114 &959757543 MonoBehaviour: @@ -864,7 +903,7 @@ MonoBehaviour: m_OnCullStateChanged: m_PersistentCalls: m_Calls: [] - m_Sprite: {fileID: 21300000, guid: 7e57f5a3a009146d4966646e4752502c, type: 3} + m_Sprite: {fileID: 21300000, guid: bfb48dca50c3049d4af953850214ffd9, type: 3} m_Type: 0 m_PreserveAspect: 1 m_FillCenter: 1 @@ -920,8 +959,8 @@ RectTransform: m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0.5, y: 0} m_AnchorMax: {x: 0.5, y: 1} - m_AnchoredPosition: {x: -422, y: 0} - m_SizeDelta: {x: 460, y: 0} + m_AnchoredPosition: {x: -482, y: 0} + m_SizeDelta: {x: 580, y: 0} m_Pivot: {x: 0.5, y: 0.5} --- !u!114 &1489695914 MonoBehaviour: @@ -936,7 +975,7 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: SpawnInputInterval: 0.5 - InputSpeed: 540 + InputSpeed: 360 LeftTarget: {fileID: 787737352} DownTarget: {fileID: 250930357} UpTarget: {fileID: 2014153219} @@ -1073,6 +1112,7 @@ GameObject: - component: {fileID: 2014153219} - component: {fileID: 2014153221} - component: {fileID: 2014153220} + - component: {fileID: 2014153222} m_Layer: 5 m_Name: Up m_TagString: Untagged @@ -1087,17 +1127,17 @@ RectTransform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 2014153218} - m_LocalRotation: {x: 0, y: 0, z: -0.7071068, w: 0.7071068} + m_LocalRotation: {x: 0, y: 0, z: 0.7071068, w: 0.7071068} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 452012667} - m_LocalEulerAnglesHint: {x: 0, y: 0, z: -90} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 90} m_AnchorMin: {x: 0, y: 0} m_AnchorMax: {x: 0, y: 0} m_AnchoredPosition: {x: 0, y: 0} - m_SizeDelta: {x: 60, y: 60} + m_SizeDelta: {x: 120, y: 80} m_Pivot: {x: 0.5, y: 0.5} --- !u!114 &2014153220 MonoBehaviour: @@ -1112,14 +1152,14 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: m_Material: {fileID: 0} - m_Color: {r: 1, g: 1, b: 1, a: 1} + m_Color: {r: 0.7, g: 1, b: 0.39999998, 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: 21300000, guid: 7e57f5a3a009146d4966646e4752502c, type: 3} + m_Sprite: {fileID: 21300000, guid: bfb48dca50c3049d4af953850214ffd9, type: 3} m_Type: 0 m_PreserveAspect: 1 m_FillCenter: 1 @@ -1137,6 +1177,18 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 2014153218} m_CullTransparentMesh: 1 +--- !u!225 &2014153222 +CanvasGroup: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2014153218} + m_Enabled: 1 + m_Alpha: 0.6 + m_Interactable: 1 + m_BlocksRaycasts: 1 + m_IgnoreParentGroups: 0 --- !u!1660057539 &9223372036854775807 SceneRoots: m_ObjectHideFlags: 0 diff --git a/Assets/images/Rhythm Game Arrow.png b/Assets/images/Rhythm Game Arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..d5900e46fcd9a122915ace0afc32a8c958930adc GIT binary patch literal 2110 zcmaJ?4K!3~7`|f&V>uhz<4DdRp_;ifBWiAAs_{D&Y5WcHGmXI#r)*T>Tk zXO05^z>w+X#zJNS((UvXAmguDO_Rukk+3`&K-Eo)A>^f*$7V)*djrc6TMt-(F$S8lI6NLt6GufuEH@7| z9eH!Hj)ma_2n3UplZnZWM3FcKBvYwWkVF9~6as=ENK%9_M@kS%Y^Fs%>$veGTrocZ z=8J@QRF|_|lnA?6TcaDzwCOlue)Q~4LWw#pBta1MfMg;GoR7wn^1netJ>R4uXEc-6 z7n?~r3DYFiVNAzJLnDBKSv-j-J`v4@zM4Wp8c0S*nkv9A+_ULTQ`!Gd)pXCL&N2gp ze3(vA?>1+7E^CXJ&qKb5Dps<;wmRi zqJ~3*h@3s-joid24$N`mz&tv7{Rw0Wf$Zc@qC#Y6Xq96K4g3l=N5WOiL&#qVsOv{9 zpaGhXE)x5TMDg_bYI!;U6<>`~3(!FHCTra6>9SF+B6UN@AT#xu-9O_4=PaJw~3$$n&r|a^y%s zV`$cbiF@~g1Y_ND%*UyzFoWL%VlzjR8d{Z=u2|WheL-5EOge#e7DgwUJ|s-kg`9xj zCyzA^fgrX$f21mEH{8*dUCDe2MeIp2O7HrxgJyZBDKg=m&fTzu=k}eh46ayoI@4og z{k~=6T+`p4Z7){EN0um5&bJ=iOj$;8GZ5T^}8N6`z7V?F(`~S3N237?>XvPWCpc96YDej zDv*@wX!?Uaw( zd`l0SlyDNQjBVPwhoqek=v{)Kic2OIR<$>^qM>Ij@AmAJbm)B6fm7ksvEek~K<+!D zyVpc~F0r?x)kPW1aK~udMtk4dI~B-1_&_AI$U3`>8;^G|>V9?ny78kfKjjtN*&mX} z1wVVp(%dMAST_!4bUgA{N{@f3a4^2(H@Jn+yB3pMsVfK;PZZve zK}lSL+3NbY*hanBa>Mdl#Vdku$0JDF^43ziv^)1G%TasGCmS)!Wv^VIkFCE8X!zcCL+RB!ozd)wDk6RTZGW$PY Ctn_;T literal 0 HcmV?d00001 diff --git a/Assets/images/Rhythm Game Arrow.png.meta b/Assets/images/Rhythm Game Arrow.png.meta new file mode 100644 index 00000000..c144e41d --- /dev/null +++ b/Assets/images/Rhythm Game Arrow.png.meta @@ -0,0 +1,114 @@ +fileFormatVersion: 2 +guid: bfb48dca50c3049d4af953850214ffd9 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 0 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/src/RhythmGameInputTracks.cs b/Assets/src/RhythmGameInputTracks.cs index 6c927a67..ffa13101 100644 --- a/Assets/src/RhythmGameInputTracks.cs +++ b/Assets/src/RhythmGameInputTracks.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using UnityEngine; +using UnityEngine.UI; public enum RhythmGameInputType { Left = 0, @@ -9,12 +10,12 @@ public enum RhythmGameInputType { }; public class RhythmGameInputTracks : MonoBehaviour { - public float SpawnInputInterval, - InputSpeed; - public Transform LeftTarget, - DownTarget, - UpTarget, - RightTarget; + public float SpawnInputInterval; + public float InputSpeed; + public Transform LeftTarget; + public Transform DownTarget; + public Transform UpTarget; + public Transform RightTarget; public GameObject ModelInput; private List SpawnedInputs = new(); @@ -52,6 +53,7 @@ private void SpawnInput() { input.transform.localPosition = new Vector2(spawnX, SpawnY); input.transform.localRotation = target.localRotation; + input.GetComponent().color = target.GetComponent().color; } private Transform TargetForInputType(RhythmGameInputType inputType) { From a9c4d208c04dacfba635c5ca643c92219b813bcb Mon Sep 17 00:00:00 2001 From: Joe Anderson Date: Mon, 15 Dec 2025 16:15:45 +0000 Subject: [PATCH 03/12] Spawn and despawn inputs based on data --- Assets/Unity/Scenes/Test Rhythm Game.unity | 120 +++++++++++++++++++- Assets/src/RhythmGame.cs | 32 ++++++ Assets/src/RhythmGame.cs.meta | 11 ++ Assets/src/RhythmGameInputTracks.cs | 121 +++++++++++++++------ 4 files changed, 249 insertions(+), 35 deletions(-) create mode 100644 Assets/src/RhythmGame.cs create mode 100644 Assets/src/RhythmGame.cs.meta diff --git a/Assets/Unity/Scenes/Test Rhythm Game.unity b/Assets/Unity/Scenes/Test Rhythm Game.unity index 49ca28ea..e8b718f1 100644 --- a/Assets/Unity/Scenes/Test Rhythm Game.unity +++ b/Assets/Unity/Scenes/Test Rhythm Game.unity @@ -613,7 +613,7 @@ RectTransform: m_ConstrainProportionsScale: 0 m_Children: - {fileID: 488498665} - m_Father: {fileID: 0} + m_Father: {fileID: 1090514947} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0} m_AnchorMax: {x: 0, y: 0} @@ -921,6 +921,119 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 959757541} m_CullTransparentMesh: 1 +--- !u!1 &1090514945 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1090514947} + - component: {fileID: 1090514946} + m_Layer: 0 + m_Name: Rhythm Game + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1090514946 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1090514945} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: da791d72e319d492e9eb6f6d293b66a6, type: 3} + m_Name: + m_EditorClassIdentifier: + InputTracks: {fileID: 1489695914} + Song: + BeatsPerMinute: 120 + Inputs: + - Type: 2 + Time: 2 + - Type: 2 + Time: 2.5 + - Type: 1 + Time: 3 + - Type: 1 + Time: 3.5 + - Type: 0 + Time: 4 + - Type: 3 + Time: 4.5 + - Type: 0 + Time: 5 + - Type: 3 + Time: 5.5 + - Type: 2 + Time: 6 + - Type: 2 + Time: 6.5 + - Type: 1 + Time: 7 + - Type: 1 + Time: 7.5 + - Type: 0 + Time: 8 + - Type: 3 + Time: 8.5 + - Type: 0 + Time: 9 + - Type: 3 + Time: 9.5 + - Type: 2 + Time: 10 + - Type: 2 + Time: 10.5 + - Type: 1 + Time: 11 + - Type: 1 + Time: 11.5 + - Type: 0 + Time: 12 + - Type: 3 + Time: 12.5 + - Type: 0 + Time: 13 + - Type: 3 + Time: 13.5 + - Type: 2 + Time: 14 + - Type: 2 + Time: 14.5 + - Type: 1 + Time: 15 + - Type: 1 + Time: 15.5 + - Type: 0 + Time: 16 + - Type: 3 + Time: 16.5 + - Type: 0 + Time: 17 + - Type: 3 + Time: 17.5 +--- !u!4 &1090514947 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1090514945} + 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: 740930168} + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &1489695912 GameObject: m_ObjectHideFlags: 0 @@ -974,8 +1087,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8112514d52eaa43adb5d0c47e84a79ed, type: 3} m_Name: m_EditorClassIdentifier: - SpawnInputInterval: 0.5 - InputSpeed: 360 + PixelsBetweenEachBeat: 270 LeftTarget: {fileID: 787737352} DownTarget: {fileID: 250930357} UpTarget: {fileID: 2014153219} @@ -1195,4 +1307,4 @@ SceneRoots: m_Roots: - {fileID: 1945062054} - {fileID: 898746627} - - {fileID: 740930168} + - {fileID: 1090514947} diff --git a/Assets/src/RhythmGame.cs b/Assets/src/RhythmGame.cs new file mode 100644 index 00000000..97c25d0a --- /dev/null +++ b/Assets/src/RhythmGame.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using UnityEngine; + +public enum RhythmGameInputType { + Left = 0, + Down = 1, + Up = 2, + Right = 3, +}; + +[System.Serializable] +public class RhythmGameInput { + public RhythmGameInputType Type; + public float Time; +} + +[System.Serializable] +public class RhythmGameSong { + public int BeatsPerMinute; + public List Inputs; +} + +public class RhythmGame : MonoBehaviour { + public RhythmGameInputTracks InputTracks; + public RhythmGameSong Song; + + public float CurrentTime => Time.time; + + void Start() { + InputTracks.Init(this); + } +} diff --git a/Assets/src/RhythmGame.cs.meta b/Assets/src/RhythmGame.cs.meta new file mode 100644 index 00000000..eec13056 --- /dev/null +++ b/Assets/src/RhythmGame.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: da791d72e319d492e9eb6f6d293b66a6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/src/RhythmGameInputTracks.cs b/Assets/src/RhythmGameInputTracks.cs index ffa13101..1434d560 100644 --- a/Assets/src/RhythmGameInputTracks.cs +++ b/Assets/src/RhythmGameInputTracks.cs @@ -2,58 +2,108 @@ using UnityEngine; using UnityEngine.UI; -public enum RhythmGameInputType { - Left = 0, - Down = 1, - Up = 2, - Right = 3, -}; - public class RhythmGameInputTracks : MonoBehaviour { - public float SpawnInputInterval; - public float InputSpeed; + public float PixelsBetweenEachBeat; public Transform LeftTarget; public Transform DownTarget; public Transform UpTarget; public Transform RightTarget; public GameObject ModelInput; - private List SpawnedInputs = new(); - /** - * The Y coordinate relative to this GameObject at which new inputs are - * spawned. + * The amount of leeway added to SpawnY to ensure that inputs are spawned well + * before they're due to appear on the screen. */ + private const float SPAWN_Y_LEEWAY = -100f; + + private RhythmGame RhythmGame; + private bool AfterFirstUpdate = false; + private List SpawnedInputs = new(); + private float TargetY; private float SpawnY; + private float DespawnY; + private float InputSpeed; + private int LastSpawnedIndex = -1; + + public void Init(RhythmGame rhythmGame) { + RhythmGame = rhythmGame; + + TargetY = transform.InverseTransformPoint(LeftTarget.position).y; - void Start() { - // Spawn inputs just below the bottom of the screen float inputSize = ModelInput.GetComponent().rect.height; - SpawnY = GetComponent().rect.yMin - inputSize / 2f; + float bottomOfScreen = GetComponent().rect.yMin; + float topOfScreen = GetComponent().rect.yMax; + SpawnY = bottomOfScreen - inputSize / 2f + SPAWN_Y_LEEWAY; + DespawnY = topOfScreen + inputSize / 2f; - AsyncTimer.BaseTime.SetInterval(SpawnInput, SpawnInputInterval, bindToGameObject: gameObject); + float secondsPerBeat = 60f / Song.BeatsPerMinute; + InputSpeed = PixelsBetweenEachBeat / secondsPerBeat; } void Update() { - Vector3 displacement = Vector2.up * InputSpeed * Time.deltaTime; + /** + * Do not spawn any inputs during the first update, since the positions can + * be incorrect. + */ + if (!AfterFirstUpdate) { + AfterFirstUpdate = true; + return; + } - SpawnedInputs.ForEach(input => { - input.transform.localPosition += displacement; - }); + List toDespawn = new(); + + // Update spawned inputs + foreach (SpawnedInput spawnedInput in SpawnedInputs) { + float y = YPositionForInput(spawnedInput.Input); + + if (y >= DespawnY) { + toDespawn.Add(spawnedInput); + } else { + Transform inputTransform = spawnedInput.GameObject.transform; + inputTransform.localPosition = new Vector2(inputTransform.localPosition.x, y); + } + } + + // Despawn off-screen inputs + foreach (SpawnedInput spawnedInput in toDespawn) { + Destroy(spawnedInput.GameObject); + SpawnedInputs.Remove(spawnedInput); + } + + // Spawn new inputs + while (LastSpawnedIndex < Inputs.Count - 1) { + RhythmGameInput nextInput = Inputs[LastSpawnedIndex + 1]; + + if (DueToSpawnInput(nextInput)) { + SpawnInput(nextInput); + LastSpawnedIndex++; + } else { + break; + } + } + } + + private float YPositionForInput(RhythmGameInput input) { + float relativeTime = input.Time - CurrentTime; + float distanceToTarget = relativeTime * InputSpeed; + return TargetY - distanceToTarget; } - private void SpawnInput() { - GameObject input = Instantiate(ModelInput, transform); - input.SetActive(true); - SpawnedInputs.Add(input); + private bool DueToSpawnInput(RhythmGameInput input) => YPositionForInput(input) >= SpawnY; - RhythmGameInputType inputType = (RhythmGameInputType)Random.Range(0, 4); - Transform target = TargetForInputType(inputType); - float spawnX = target.localPosition.x; + private void SpawnInput(RhythmGameInput input) { + GameObject inputGameObject = Instantiate(ModelInput, transform); + inputGameObject.SetActive(true); - input.transform.localPosition = new Vector2(spawnX, SpawnY); - input.transform.localRotation = target.localRotation; - input.GetComponent().color = target.GetComponent().color; + Transform target = TargetForInputType(input.Type); + float spawnX = transform.InverseTransformPoint(target.position).x; + + inputGameObject.transform.localPosition = new Vector2(spawnX, SpawnY); + inputGameObject.transform.localRotation = target.localRotation; + inputGameObject.GetComponent().color = target.GetComponent().color; + + SpawnedInput spawnedInput = new SpawnedInput { Input = input, GameObject = inputGameObject }; + SpawnedInputs.Add(spawnedInput); } private Transform TargetForInputType(RhythmGameInputType inputType) { @@ -73,4 +123,13 @@ private Transform TargetForInputType(RhythmGameInputType inputType) { throw new System.ArgumentException("Unexpected input type"); } + + private RhythmGameSong Song => RhythmGame.Song; + private List Inputs => Song.Inputs; + private float CurrentTime => RhythmGame.CurrentTime; + + class SpawnedInput { + public RhythmGameInput Input; + public GameObject GameObject; + } } From 1f4e7a2b170343d81131da1e2d647a95a902e422 Mon Sep 17 00:00:00 2001 From: Joe Anderson Date: Mon, 15 Dec 2025 20:14:38 +0000 Subject: [PATCH 04/12] Add keyboard and controller maps --- .../Prefabs/Rewired Input Manager.prefab | 915 ++++++------------ 1 file changed, 298 insertions(+), 617 deletions(-) diff --git a/Assets/Unity/Prefabs/Rewired Input Manager.prefab b/Assets/Unity/Prefabs/Rewired Input Manager.prefab index f30397dd..b890ff3d 100644 --- a/Assets/Unity/Prefabs/Rewired Input Manager.prefab +++ b/Assets/Unity/Prefabs/Rewired Input Manager.prefab @@ -430,6 +430,42 @@ MonoBehaviour: _behaviorId: 0 _userAssignable: 1 _categoryId: 0 + - _id: 28 + _name: Rhythm Game Left + _type: 1 + _descriptiveName: Rhythm Game Left + _positiveDescriptiveName: + _negativeDescriptiveName: + _behaviorId: 0 + _userAssignable: 1 + _categoryId: 0 + - _id: 31 + _name: Rhythm Game Down + _type: 1 + _descriptiveName: Rhythm Game Down + _positiveDescriptiveName: + _negativeDescriptiveName: + _behaviorId: 0 + _userAssignable: 1 + _categoryId: 0 + - _id: 30 + _name: Rhythm Game Up + _type: 1 + _descriptiveName: Rhythm Game Up + _positiveDescriptiveName: + _negativeDescriptiveName: + _behaviorId: 0 + _userAssignable: 1 + _categoryId: 0 + - _id: 29 + _name: Rhythm Game Right + _type: 1 + _descriptiveName: Rhythm Game Right + _positiveDescriptiveName: + _negativeDescriptiveName: + _behaviorId: 0 + _userAssignable: 1 + _categoryId: 0 actionCategories: - _name: Default _descriptiveName: Default @@ -439,7 +475,7 @@ MonoBehaviour: actionCategoryMap: list: - categoryId: 0 - actionIds: 0000000001000000100000000f000000030000000400000005000000060000000700000008000000090000001b0000000a0000000b0000001300000014000000150000001700000018000000190000001a000000 + actionIds: 0000000001000000100000000f000000030000000400000005000000060000000700000008000000090000001b0000000a0000000b0000001300000014000000150000001700000018000000190000001a0000001c0000001f0000001e0000001d000000 inputBehaviors: - _id: 0 _name: Default @@ -542,10 +578,10 @@ MonoBehaviour: _modifierKey2: 0 _modifierKey3: 0 - _actionCategoryId: 0 - _actionId: 20 + _actionId: 29 _elementType: 0 _elementIdentifierId: 0 - _axisRange: 2 + _axisRange: 1 _invert: 0 _axisContribution: 0 _keyboardKeyCode: 0 @@ -553,10 +589,10 @@ MonoBehaviour: _modifierKey2: 0 _modifierKey3: 0 - _actionCategoryId: 0 - _actionId: 1 + _actionId: 20 _elementType: 0 - _elementIdentifierId: 1 - _axisRange: 0 + _elementIdentifierId: 0 + _axisRange: 2 _invert: 0 _axisContribution: 0 _keyboardKeyCode: 0 @@ -564,10 +600,10 @@ MonoBehaviour: _modifierKey2: 0 _modifierKey3: 0 - _actionCategoryId: 0 - _actionId: 16 + _actionId: 28 _elementType: 0 - _elementIdentifierId: 2 - _axisRange: 0 + _elementIdentifierId: 0 + _axisRange: 2 _invert: 0 _axisContribution: 0 _keyboardKeyCode: 0 @@ -575,10 +611,10 @@ MonoBehaviour: _modifierKey2: 0 _modifierKey3: 0 - _actionCategoryId: 0 - _actionId: 15 + _actionId: 30 _elementType: 0 - _elementIdentifierId: 3 - _axisRange: 0 + _elementIdentifierId: 1 + _axisRange: 1 _invert: 0 _axisContribution: 0 _keyboardKeyCode: 0 @@ -586,98 +622,10 @@ MonoBehaviour: _modifierKey2: 0 _modifierKey3: 0 - _actionCategoryId: 0 - _actionId: 23 + _actionId: 31 _elementType: 0 - _elementIdentifierId: 3 - _axisRange: 0 - _invert: 0 - _axisContribution: 0 - _keyboardKeyCode: 0 - _modifierKey1: 0 - _modifierKey2: 0 - _modifierKey3: 0 - - _actionCategoryId: 0 - _actionId: 7 - _elementType: 1 - _elementIdentifierId: 4 - _axisRange: 0 - _invert: 0 - _axisContribution: 0 - _keyboardKeyCode: 0 - _modifierKey1: 0 - _modifierKey2: 0 - _modifierKey3: 0 - - _actionCategoryId: 0 - _actionId: 6 - _elementType: 1 - _elementIdentifierId: 4 - _axisRange: 0 - _invert: 0 - _axisContribution: 0 - _keyboardKeyCode: 0 - _modifierKey1: 0 - _modifierKey2: 0 - _modifierKey3: 0 - - _actionCategoryId: 0 - _actionId: 27 - _elementType: 1 - _elementIdentifierId: 5 - _axisRange: 0 - _invert: 0 - _axisContribution: 0 - _keyboardKeyCode: 0 - _modifierKey1: 0 - _modifierKey2: 0 - _modifierKey3: 0 - - _actionCategoryId: 0 - _actionId: 8 - _elementType: 1 - _elementIdentifierId: 5 - _axisRange: 0 - _invert: 0 - _axisContribution: 0 - _keyboardKeyCode: 0 - _modifierKey1: 0 - _modifierKey2: 0 - _modifierKey3: 0 - - _actionCategoryId: 0 - _actionId: -1 - _elementType: 1 - _elementIdentifierId: 6 - _axisRange: 0 - _invert: 0 - _axisContribution: 0 - _keyboardKeyCode: 0 - _modifierKey1: 0 - _modifierKey2: 0 - _modifierKey3: 0 - - _actionCategoryId: 0 - _actionId: 4 - _elementType: 1 - _elementIdentifierId: 7 - _axisRange: 0 - _invert: 0 - _axisContribution: 0 - _keyboardKeyCode: 0 - _modifierKey1: 0 - _modifierKey2: 0 - _modifierKey3: 0 - - _actionCategoryId: 0 - _actionId: 11 - _elementType: 1 - _elementIdentifierId: 8 - _axisRange: 0 - _invert: 0 - _axisContribution: 0 - _keyboardKeyCode: 0 - _modifierKey1: 0 - _modifierKey2: 0 - _modifierKey3: 0 - - _actionCategoryId: 0 - _actionId: -1 - _elementType: 1 - _elementIdentifierId: 9 - _axisRange: 0 + _elementIdentifierId: 1 + _axisRange: 2 _invert: 0 _axisContribution: 0 _keyboardKeyCode: 0 @@ -685,9 +633,9 @@ MonoBehaviour: _modifierKey2: 0 _modifierKey3: 0 - _actionCategoryId: 0 - _actionId: 9 - _elementType: 1 - _elementIdentifierId: 12 + _actionId: 1 + _elementType: 0 + _elementIdentifierId: 1 _axisRange: 0 _invert: 0 _axisContribution: 0 @@ -696,20 +644,9 @@ MonoBehaviour: _modifierKey2: 0 _modifierKey3: 0 - _actionCategoryId: 0 - _actionId: 23 - _elementType: 1 - _elementIdentifierId: 12 - _axisRange: 0 - _invert: 0 - _axisContribution: 1 - _keyboardKeyCode: 0 - _modifierKey1: 0 - _modifierKey2: 0 - _modifierKey3: 0 - - _actionCategoryId: 0 - _actionId: 3 + _actionId: 16 _elementType: 0 - _elementIdentifierId: 13 + _elementIdentifierId: 2 _axisRange: 0 _invert: 0 _axisContribution: 0 @@ -718,9 +655,9 @@ MonoBehaviour: _modifierKey2: 0 _modifierKey3: 0 - _actionCategoryId: 0 - _actionId: 23 + _actionId: 29 _elementType: 0 - _elementIdentifierId: 13 + _elementIdentifierId: 2 _axisRange: 1 _invert: 0 _axisContribution: 0 @@ -729,65 +666,10 @@ MonoBehaviour: _modifierKey2: 0 _modifierKey3: 0 - _actionCategoryId: 0 - _actionId: 10 - _elementType: 1 - _elementIdentifierId: 14 - _axisRange: 0 - _invert: 0 - _axisContribution: 0 - _keyboardKeyCode: 0 - _modifierKey1: 0 - _modifierKey2: 0 - _modifierKey3: 0 - - _actionCategoryId: 0 - _actionId: 10 - _elementType: 1 - _elementIdentifierId: 15 - _axisRange: 0 - _invert: 0 - _axisContribution: 0 - _keyboardKeyCode: 0 - _modifierKey1: 0 - _modifierKey2: 0 - _modifierKey3: 0 - - _actionCategoryId: 0 - _actionId: 10 - _elementType: 1 - _elementIdentifierId: 16 - _axisRange: 0 - _invert: 0 - _axisContribution: 0 - _keyboardKeyCode: 0 - _modifierKey1: 0 - _modifierKey2: 0 - _modifierKey3: 0 - - _actionCategoryId: 0 - _actionId: -1 - _elementType: 1 - _elementIdentifierId: 17 - _axisRange: 0 - _invert: 0 - _axisContribution: 0 - _keyboardKeyCode: 0 - _modifierKey1: 0 - _modifierKey2: 0 - _modifierKey3: 0 - - _actionCategoryId: 0 - _actionId: -1 - _elementType: 1 - _elementIdentifierId: 18 - _axisRange: 0 - _invert: 0 - _axisContribution: 0 - _keyboardKeyCode: 0 - _modifierKey1: 0 - _modifierKey2: 0 - _modifierKey3: 0 - - _actionCategoryId: 0 - _actionId: 1 - _elementType: 1 - _elementIdentifierId: 19 - _axisRange: 0 + _actionId: 28 + _elementType: 0 + _elementIdentifierId: 2 + _axisRange: 2 _invert: 0 _axisContribution: 0 _keyboardKeyCode: 0 @@ -795,9 +677,9 @@ MonoBehaviour: _modifierKey2: 0 _modifierKey3: 0 - _actionCategoryId: 0 - _actionId: 0 - _elementType: 1 - _elementIdentifierId: 20 + _actionId: 15 + _elementType: 0 + _elementIdentifierId: 3 _axisRange: 0 _invert: 0 _axisContribution: 0 @@ -806,10 +688,10 @@ MonoBehaviour: _modifierKey2: 0 _modifierKey3: 0 - _actionCategoryId: 0 - _actionId: 21 - _elementType: 1 - _elementIdentifierId: 20 - _axisRange: 0 + _actionId: 30 + _elementType: 0 + _elementIdentifierId: 3 + _axisRange: 1 _invert: 0 _axisContribution: 0 _keyboardKeyCode: 0 @@ -817,397 +699,20 @@ MonoBehaviour: _modifierKey2: 0 _modifierKey3: 0 - _actionCategoryId: 0 - _actionId: 1 - _elementType: 1 - _elementIdentifierId: 21 - _axisRange: 0 - _invert: 0 - _axisContribution: 1 - _keyboardKeyCode: 0 - _modifierKey1: 0 - _modifierKey2: 0 - _modifierKey3: 0 - - _actionCategoryId: 0 - _actionId: 0 - _elementType: 1 - _elementIdentifierId: 22 - _axisRange: 0 - _invert: 0 - _axisContribution: 1 - _keyboardKeyCode: 0 - _modifierKey1: 0 - _modifierKey2: 0 - _modifierKey3: 0 - - _actionCategoryId: 0 - _actionId: 20 - _elementType: 1 - _elementIdentifierId: 22 - _axisRange: 0 - _invert: 0 - _axisContribution: 0 - _keyboardKeyCode: 0 - _modifierKey1: 0 - _modifierKey2: 0 - _modifierKey3: 0 - - _actionCategoryId: 0 - _actionId: 27 - _elementType: 1 - _elementIdentifierId: 18 - _axisRange: 0 - _invert: 0 - _axisContribution: 0 - _keyboardKeyCode: 0 - _modifierKey1: 0 - _modifierKey2: 0 - _modifierKey3: 0 - - id: 2 - categoryId: 0 - layoutId: 0 - name: - hardwareGuidString: cd9718bf-a87a-44bc-8716-60a0def28a9f - customControllerUid: 0 - actionElementMaps: - - _actionCategoryId: 0 - _actionId: 0 - _elementType: 0 - _elementIdentifierId: 0 - _axisRange: 0 - _invert: 0 - _axisContribution: 0 - _keyboardKeyCode: 0 - _modifierKey1: 0 - _modifierKey2: 0 - _modifierKey3: 0 - - _actionCategoryId: 0 - _actionId: 21 + _actionId: 31 _elementType: 0 - _elementIdentifierId: 0 - _axisRange: 1 - _invert: 0 - _axisContribution: 0 - _keyboardKeyCode: 0 - _modifierKey1: 0 - _modifierKey2: 0 - _modifierKey3: 0 - - _actionCategoryId: 0 - _actionId: 20 - _elementType: 0 - _elementIdentifierId: 0 - _axisRange: 2 - _invert: 0 - _axisContribution: 0 - _keyboardKeyCode: 0 - _modifierKey1: 0 - _modifierKey2: 0 - _modifierKey3: 0 - - _actionCategoryId: 0 - _actionId: 1 - _elementType: 1 - _elementIdentifierId: 1 - _axisRange: 0 - _invert: 0 - _axisContribution: 0 - _keyboardKeyCode: 0 - _modifierKey1: 0 - _modifierKey2: 0 - _modifierKey3: 0 - - _actionCategoryId: 0 - _actionId: 16 - _elementType: 1 - _elementIdentifierId: 2 - _axisRange: 0 - _invert: 0 - _axisContribution: 0 - _keyboardKeyCode: 0 - _modifierKey1: 0 - _modifierKey2: 0 - _modifierKey3: 0 - - _actionCategoryId: 0 - _actionId: 15 - _elementType: 1 - _elementIdentifierId: 3 - _axisRange: 0 - _invert: 0 - _axisContribution: 0 - _keyboardKeyCode: 0 - _modifierKey1: 0 - _modifierKey2: 0 - _modifierKey3: 0 - - _actionCategoryId: 0 - _actionId: 23 - _elementType: 1 _elementIdentifierId: 3 - _axisRange: 0 - _invert: 0 - _axisContribution: 0 - _keyboardKeyCode: 0 - _modifierKey1: 0 - _modifierKey2: 0 - _modifierKey3: 0 - - _actionCategoryId: 0 - _actionId: 7 - _elementType: 1 - _elementIdentifierId: 6 - _axisRange: 0 - _invert: 0 - _axisContribution: 0 - _keyboardKeyCode: 0 - _modifierKey1: 0 - _modifierKey2: 0 - _modifierKey3: 0 - - _actionCategoryId: 0 - _actionId: 6 - _elementType: 1 - _elementIdentifierId: 6 - _axisRange: 0 - _invert: 0 - _axisContribution: 0 - _keyboardKeyCode: 0 - _modifierKey1: 0 - _modifierKey2: 0 - _modifierKey3: 0 - - _actionCategoryId: 0 - _actionId: 27 - _elementType: 1 - _elementIdentifierId: 7 - _axisRange: 0 - _invert: 0 - _axisContribution: 0 - _keyboardKeyCode: 0 - _modifierKey1: 0 - _modifierKey2: 0 - _modifierKey3: 0 - - _actionCategoryId: 0 - _actionId: 8 - _elementType: 1 - _elementIdentifierId: 7 - _axisRange: 0 - _invert: 0 - _axisContribution: 0 - _keyboardKeyCode: 0 - _modifierKey1: 0 - _modifierKey2: 0 - _modifierKey3: 0 - - _actionCategoryId: 0 - _actionId: 4 - _elementType: 1 - _elementIdentifierId: 8 - _axisRange: 0 - _invert: 0 - _axisContribution: 0 - _keyboardKeyCode: 0 - _modifierKey1: 0 - _modifierKey2: 0 - _modifierKey3: 0 - - _actionCategoryId: 0 - _actionId: 11 - _elementType: 1 - _elementIdentifierId: 9 - _axisRange: 0 - _invert: 0 - _axisContribution: 0 - _keyboardKeyCode: 0 - _modifierKey1: 0 - _modifierKey2: 0 - _modifierKey3: 0 - - _actionCategoryId: 0 - _actionId: 9 - _elementType: 1 - _elementIdentifierId: 11 - _axisRange: 0 - _invert: 0 - _axisContribution: 0 - _keyboardKeyCode: 0 - _modifierKey1: 0 - _modifierKey2: 0 - _modifierKey3: 0 - - _actionCategoryId: 0 - _actionId: 23 - _elementType: 1 - _elementIdentifierId: 11 - _axisRange: 0 - _invert: 0 - _axisContribution: 1 - _keyboardKeyCode: 0 - _modifierKey1: 0 - _modifierKey2: 0 - _modifierKey3: 0 - - _actionCategoryId: 0 - _actionId: 3 - _elementType: 0 - _elementIdentifierId: 5 - _axisRange: 0 - _invert: 0 - _axisContribution: 0 - _keyboardKeyCode: 0 - _modifierKey1: 0 - _modifierKey2: 0 - _modifierKey3: 0 - - _actionCategoryId: 0 - _actionId: 23 - _elementType: 0 - _elementIdentifierId: 5 - _axisRange: 1 - _invert: 0 - _axisContribution: 0 - _keyboardKeyCode: 0 - _modifierKey1: 0 - _modifierKey2: 0 - _modifierKey3: 0 - - _actionCategoryId: 0 - _actionId: 10 - _elementType: 1 - _elementIdentifierId: 12 - _axisRange: 0 - _invert: 0 - _axisContribution: 0 - _keyboardKeyCode: 0 - _modifierKey1: 0 - _modifierKey2: 0 - _modifierKey3: 0 - - _actionCategoryId: 0 - _actionId: 10 - _elementType: 1 - _elementIdentifierId: 13 - _axisRange: 0 - _invert: 0 - _axisContribution: 0 - _keyboardKeyCode: 0 - _modifierKey1: 0 - _modifierKey2: 0 - _modifierKey3: 0 - - _actionCategoryId: 0 - _actionId: 10 - _elementType: 1 - _elementIdentifierId: 14 - _axisRange: 0 - _invert: 0 - _axisContribution: 0 - _keyboardKeyCode: 0 - _modifierKey1: 0 - _modifierKey2: 0 - _modifierKey3: 0 - - _actionCategoryId: 0 - _actionId: 10 - _elementType: 1 - _elementIdentifierId: 15 - _axisRange: 0 - _invert: 0 - _axisContribution: 0 - _keyboardKeyCode: 0 - _modifierKey1: 0 - _modifierKey2: 0 - _modifierKey3: 0 - - _actionCategoryId: 0 - _actionId: -1 - _elementType: 1 - _elementIdentifierId: 16 - _axisRange: 0 - _invert: 0 - _axisContribution: 0 - _keyboardKeyCode: 0 - _modifierKey1: 0 - _modifierKey2: 0 - _modifierKey3: 0 - - _actionCategoryId: 0 - _actionId: -1 - _elementType: 1 - _elementIdentifierId: 17 - _axisRange: 0 - _invert: 0 - _axisContribution: 0 - _keyboardKeyCode: 0 - _modifierKey1: 0 - _modifierKey2: 0 - _modifierKey3: 0 - - _actionCategoryId: 0 - _actionId: 1 - _elementType: 1 - _elementIdentifierId: 18 - _axisRange: 0 - _invert: 0 - _axisContribution: 0 - _keyboardKeyCode: 0 - _modifierKey1: 0 - _modifierKey2: 0 - _modifierKey3: 0 - - _actionCategoryId: 0 - _actionId: 0 - _elementType: 1 - _elementIdentifierId: 19 - _axisRange: 0 - _invert: 0 - _axisContribution: 0 - _keyboardKeyCode: 0 - _modifierKey1: 0 - _modifierKey2: 0 - _modifierKey3: 0 - - _actionCategoryId: 0 - _actionId: 21 - _elementType: 1 - _elementIdentifierId: 19 - _axisRange: 0 - _invert: 0 - _axisContribution: 0 - _keyboardKeyCode: 0 - _modifierKey1: 0 - _modifierKey2: 0 - _modifierKey3: 0 - - _actionCategoryId: 0 - _actionId: 1 - _elementType: 1 - _elementIdentifierId: 20 - _axisRange: 0 - _invert: 0 - _axisContribution: 1 - _keyboardKeyCode: 0 - _modifierKey1: 0 - _modifierKey2: 0 - _modifierKey3: 0 - - _actionCategoryId: 0 - _actionId: 0 - _elementType: 1 - _elementIdentifierId: 21 - _axisRange: 0 - _invert: 0 - _axisContribution: 1 - _keyboardKeyCode: 0 - _modifierKey1: 0 - _modifierKey2: 0 - _modifierKey3: 0 - - _actionCategoryId: 0 - _actionId: 20 - _elementType: 1 - _elementIdentifierId: 21 - _axisRange: 0 - _invert: 0 - _axisContribution: 0 - _keyboardKeyCode: 0 - _modifierKey1: 0 - _modifierKey2: 0 - _modifierKey3: 0 - - _actionCategoryId: 0 - _actionId: 27 - _elementType: 1 - _elementIdentifierId: 17 - _axisRange: 0 + _axisRange: 2 _invert: 0 _axisContribution: 0 _keyboardKeyCode: 0 _modifierKey1: 0 _modifierKey2: 0 _modifierKey3: 0 - - id: 3 - categoryId: 0 - layoutId: 0 - name: - hardwareGuidString: 71dfe6c8-9e81-428f-a58e-c7e664b7fbed - customControllerUid: 0 - actionElementMaps: - _actionCategoryId: 0 - _actionId: 0 + _actionId: 23 _elementType: 0 - _elementIdentifierId: 0 + _elementIdentifierId: 3 _axisRange: 0 _invert: 0 _axisContribution: 0 @@ -1216,10 +721,10 @@ MonoBehaviour: _modifierKey2: 0 _modifierKey3: 0 - _actionCategoryId: 0 - _actionId: 21 - _elementType: 0 - _elementIdentifierId: 0 - _axisRange: 1 + _actionId: 7 + _elementType: 1 + _elementIdentifierId: 4 + _axisRange: 0 _invert: 0 _axisContribution: 0 _keyboardKeyCode: 0 @@ -1227,10 +732,10 @@ MonoBehaviour: _modifierKey2: 0 _modifierKey3: 0 - _actionCategoryId: 0 - _actionId: 20 - _elementType: 0 - _elementIdentifierId: 0 - _axisRange: 2 + _actionId: 6 + _elementType: 1 + _elementIdentifierId: 4 + _axisRange: 0 _invert: 0 _axisContribution: 0 _keyboardKeyCode: 0 @@ -1238,9 +743,9 @@ MonoBehaviour: _modifierKey2: 0 _modifierKey3: 0 - _actionCategoryId: 0 - _actionId: 1 - _elementType: 0 - _elementIdentifierId: 1 + _actionId: 31 + _elementType: 1 + _elementIdentifierId: 4 _axisRange: 0 _invert: 0 _axisContribution: 0 @@ -1249,9 +754,9 @@ MonoBehaviour: _modifierKey2: 0 _modifierKey3: 0 - _actionCategoryId: 0 - _actionId: 16 - _elementType: 0 - _elementIdentifierId: 2 + _actionId: 27 + _elementType: 1 + _elementIdentifierId: 5 _axisRange: 0 _invert: 0 _axisContribution: 0 @@ -1260,9 +765,9 @@ MonoBehaviour: _modifierKey2: 0 _modifierKey3: 0 - _actionCategoryId: 0 - _actionId: 15 - _elementType: 0 - _elementIdentifierId: 3 + _actionId: 8 + _elementType: 1 + _elementIdentifierId: 5 _axisRange: 0 _invert: 0 _axisContribution: 0 @@ -1271,9 +776,9 @@ MonoBehaviour: _modifierKey2: 0 _modifierKey3: 0 - _actionCategoryId: 0 - _actionId: 23 - _elementType: 0 - _elementIdentifierId: 3 + _actionId: 29 + _elementType: 1 + _elementIdentifierId: 5 _axisRange: 0 _invert: 0 _axisContribution: 0 @@ -1282,9 +787,9 @@ MonoBehaviour: _modifierKey2: 0 _modifierKey3: 0 - _actionCategoryId: 0 - _actionId: 6 + _actionId: -1 _elementType: 1 - _elementIdentifierId: 4 + _elementIdentifierId: 6 _axisRange: 0 _invert: 0 _axisContribution: 0 @@ -1293,9 +798,9 @@ MonoBehaviour: _modifierKey2: 0 _modifierKey3: 0 - _actionCategoryId: 0 - _actionId: 7 + _actionId: 4 _elementType: 1 - _elementIdentifierId: 4 + _elementIdentifierId: 7 _axisRange: 0 _invert: 0 _axisContribution: 0 @@ -1304,9 +809,9 @@ MonoBehaviour: _modifierKey2: 0 _modifierKey3: 0 - _actionCategoryId: 0 - _actionId: 27 + _actionId: 28 _elementType: 1 - _elementIdentifierId: 5 + _elementIdentifierId: 7 _axisRange: 0 _invert: 0 _axisContribution: 0 @@ -1315,9 +820,9 @@ MonoBehaviour: _modifierKey2: 0 _modifierKey3: 0 - _actionCategoryId: 0 - _actionId: 8 + _actionId: 11 _elementType: 1 - _elementIdentifierId: 5 + _elementIdentifierId: 8 _axisRange: 0 _invert: 0 _axisContribution: 0 @@ -1326,9 +831,9 @@ MonoBehaviour: _modifierKey2: 0 _modifierKey3: 0 - _actionCategoryId: 0 - _actionId: 4 + _actionId: 30 _elementType: 1 - _elementIdentifierId: 6 + _elementIdentifierId: 8 _axisRange: 0 _invert: 0 _axisContribution: 0 @@ -1337,9 +842,9 @@ MonoBehaviour: _modifierKey2: 0 _modifierKey3: 0 - _actionCategoryId: 0 - _actionId: 11 + _actionId: -1 _elementType: 1 - _elementIdentifierId: 7 + _elementIdentifierId: 9 _axisRange: 0 _invert: 0 _axisContribution: 0 @@ -1350,7 +855,7 @@ MonoBehaviour: - _actionCategoryId: 0 _actionId: 9 _elementType: 1 - _elementIdentifierId: 10 + _elementIdentifierId: 12 _axisRange: 0 _invert: 0 _axisContribution: 0 @@ -1361,7 +866,7 @@ MonoBehaviour: - _actionCategoryId: 0 _actionId: 23 _elementType: 1 - _elementIdentifierId: 10 + _elementIdentifierId: 12 _axisRange: 0 _invert: 0 _axisContribution: 1 @@ -1372,7 +877,7 @@ MonoBehaviour: - _actionCategoryId: 0 _actionId: 3 _elementType: 0 - _elementIdentifierId: 11 + _elementIdentifierId: 13 _axisRange: 0 _invert: 0 _axisContribution: 0 @@ -1383,7 +888,7 @@ MonoBehaviour: - _actionCategoryId: 0 _actionId: 23 _elementType: 0 - _elementIdentifierId: 11 + _elementIdentifierId: 13 _axisRange: 1 _invert: 0 _axisContribution: 0 @@ -1394,7 +899,7 @@ MonoBehaviour: - _actionCategoryId: 0 _actionId: 10 _elementType: 1 - _elementIdentifierId: 12 + _elementIdentifierId: 14 _axisRange: 0 _invert: 0 _axisContribution: 0 @@ -1405,7 +910,7 @@ MonoBehaviour: - _actionCategoryId: 0 _actionId: 10 _elementType: 1 - _elementIdentifierId: 13 + _elementIdentifierId: 15 _axisRange: 0 _invert: 0 _axisContribution: 0 @@ -1416,7 +921,7 @@ MonoBehaviour: - _actionCategoryId: 0 _actionId: 10 _elementType: 1 - _elementIdentifierId: 14 + _elementIdentifierId: 16 _axisRange: 0 _invert: 0 _axisContribution: 0 @@ -1427,7 +932,7 @@ MonoBehaviour: - _actionCategoryId: 0 _actionId: -1 _elementType: 1 - _elementIdentifierId: 15 + _elementIdentifierId: 17 _axisRange: 0 _invert: 0 _axisContribution: 0 @@ -1438,7 +943,7 @@ MonoBehaviour: - _actionCategoryId: 0 _actionId: -1 _elementType: 1 - _elementIdentifierId: 16 + _elementIdentifierId: 18 _axisRange: 0 _invert: 0 _axisContribution: 0 @@ -1449,7 +954,18 @@ MonoBehaviour: - _actionCategoryId: 0 _actionId: 1 _elementType: 1 - _elementIdentifierId: 17 + _elementIdentifierId: 19 + _axisRange: 0 + _invert: 0 + _axisContribution: 0 + _keyboardKeyCode: 0 + _modifierKey1: 0 + _modifierKey2: 0 + _modifierKey3: 0 + - _actionCategoryId: 0 + _actionId: 30 + _elementType: 1 + _elementIdentifierId: 19 _axisRange: 0 _invert: 0 _axisContribution: 0 @@ -1460,7 +976,18 @@ MonoBehaviour: - _actionCategoryId: 0 _actionId: 0 _elementType: 1 - _elementIdentifierId: 18 + _elementIdentifierId: 20 + _axisRange: 0 + _invert: 0 + _axisContribution: 0 + _keyboardKeyCode: 0 + _modifierKey1: 0 + _modifierKey2: 0 + _modifierKey3: 0 + - _actionCategoryId: 0 + _actionId: 29 + _elementType: 1 + _elementIdentifierId: 20 _axisRange: 0 _invert: 0 _axisContribution: 0 @@ -1471,7 +998,7 @@ MonoBehaviour: - _actionCategoryId: 0 _actionId: 21 _elementType: 1 - _elementIdentifierId: 18 + _elementIdentifierId: 20 _axisRange: 0 _invert: 0 _axisContribution: 0 @@ -1482,7 +1009,7 @@ MonoBehaviour: - _actionCategoryId: 0 _actionId: 1 _elementType: 1 - _elementIdentifierId: 19 + _elementIdentifierId: 21 _axisRange: 0 _invert: 0 _axisContribution: 1 @@ -1490,10 +1017,21 @@ MonoBehaviour: _modifierKey1: 0 _modifierKey2: 0 _modifierKey3: 0 + - _actionCategoryId: 0 + _actionId: 31 + _elementType: 1 + _elementIdentifierId: 21 + _axisRange: 0 + _invert: 0 + _axisContribution: 0 + _keyboardKeyCode: 0 + _modifierKey1: 0 + _modifierKey2: 0 + _modifierKey3: 0 - _actionCategoryId: 0 _actionId: 0 _elementType: 1 - _elementIdentifierId: 20 + _elementIdentifierId: 22 _axisRange: 0 _invert: 0 _axisContribution: 1 @@ -1501,10 +1039,21 @@ MonoBehaviour: _modifierKey1: 0 _modifierKey2: 0 _modifierKey3: 0 + - _actionCategoryId: 0 + _actionId: 28 + _elementType: 1 + _elementIdentifierId: 22 + _axisRange: 0 + _invert: 0 + _axisContribution: 0 + _keyboardKeyCode: 0 + _modifierKey1: 0 + _modifierKey2: 0 + _modifierKey3: 0 - _actionCategoryId: 0 _actionId: 20 _elementType: 1 - _elementIdentifierId: 20 + _elementIdentifierId: 22 _axisRange: 0 _invert: 0 _axisContribution: 0 @@ -1515,7 +1064,7 @@ MonoBehaviour: - _actionCategoryId: 0 _actionId: 27 _elementType: 1 - _elementIdentifierId: 16 + _elementIdentifierId: 18 _axisRange: 0 _invert: 0 _axisContribution: 0 @@ -1542,6 +1091,17 @@ MonoBehaviour: _modifierKey1: 0 _modifierKey2: 0 _modifierKey3: 0 + - _actionCategoryId: 0 + _actionId: 30 + _elementType: 1 + _elementIdentifierId: -1 + _axisRange: 0 + _invert: 0 + _axisContribution: 0 + _keyboardKeyCode: 119 + _modifierKey1: 0 + _modifierKey2: 0 + _modifierKey3: 0 - _actionCategoryId: 0 _actionId: 0 _elementType: 1 @@ -1553,6 +1113,17 @@ MonoBehaviour: _modifierKey1: 0 _modifierKey2: 0 _modifierKey3: 0 + - _actionCategoryId: 0 + _actionId: 28 + _elementType: 1 + _elementIdentifierId: -1 + _axisRange: 0 + _invert: 0 + _axisContribution: 0 + _keyboardKeyCode: 97 + _modifierKey1: 0 + _modifierKey2: 0 + _modifierKey3: 0 - _actionCategoryId: 0 _actionId: 20 _elementType: 1 @@ -1575,6 +1146,17 @@ MonoBehaviour: _modifierKey1: 0 _modifierKey2: 0 _modifierKey3: 0 + - _actionCategoryId: 0 + _actionId: 31 + _elementType: 1 + _elementIdentifierId: -1 + _axisRange: 0 + _invert: 0 + _axisContribution: 0 + _keyboardKeyCode: 115 + _modifierKey1: 0 + _modifierKey2: 0 + _modifierKey3: 0 - _actionCategoryId: 0 _actionId: 0 _elementType: 1 @@ -1586,6 +1168,17 @@ MonoBehaviour: _modifierKey1: 0 _modifierKey2: 0 _modifierKey3: 0 + - _actionCategoryId: 0 + _actionId: 29 + _elementType: 1 + _elementIdentifierId: -1 + _axisRange: 0 + _invert: 0 + _axisContribution: 0 + _keyboardKeyCode: 100 + _modifierKey1: 0 + _modifierKey2: 0 + _modifierKey3: 0 - _actionCategoryId: 0 _actionId: 21 _elementType: 1 @@ -1608,6 +1201,17 @@ MonoBehaviour: _modifierKey1: 0 _modifierKey2: 0 _modifierKey3: 0 + - _actionCategoryId: 0 + _actionId: 30 + _elementType: 1 + _elementIdentifierId: -1 + _axisRange: 0 + _invert: 0 + _axisContribution: 0 + _keyboardKeyCode: 273 + _modifierKey1: 0 + _modifierKey2: 0 + _modifierKey3: 0 - _actionCategoryId: 0 _actionId: 0 _elementType: 1 @@ -1619,6 +1223,17 @@ MonoBehaviour: _modifierKey1: 0 _modifierKey2: 0 _modifierKey3: 0 + - _actionCategoryId: 0 + _actionId: 28 + _elementType: 1 + _elementIdentifierId: -1 + _axisRange: 0 + _invert: 0 + _axisContribution: 0 + _keyboardKeyCode: 276 + _modifierKey1: 0 + _modifierKey2: 0 + _modifierKey3: 0 - _actionCategoryId: 0 _actionId: 20 _elementType: 1 @@ -1641,6 +1256,17 @@ MonoBehaviour: _modifierKey1: 0 _modifierKey2: 0 _modifierKey3: 0 + - _actionCategoryId: 0 + _actionId: 31 + _elementType: 1 + _elementIdentifierId: -1 + _axisRange: 0 + _invert: 0 + _axisContribution: 0 + _keyboardKeyCode: 274 + _modifierKey1: 0 + _modifierKey2: 0 + _modifierKey3: 0 - _actionCategoryId: 0 _actionId: 0 _elementType: 1 @@ -1652,6 +1278,17 @@ MonoBehaviour: _modifierKey1: 0 _modifierKey2: 0 _modifierKey3: 0 + - _actionCategoryId: 0 + _actionId: 29 + _elementType: 1 + _elementIdentifierId: -1 + _axisRange: 0 + _invert: 0 + _axisContribution: 0 + _keyboardKeyCode: 275 + _modifierKey1: 0 + _modifierKey2: 0 + _modifierKey3: 0 - _actionCategoryId: 0 _actionId: 21 _elementType: 1 @@ -1861,6 +1498,50 @@ MonoBehaviour: _modifierKey1: 0 _modifierKey2: 0 _modifierKey3: 0 + - _actionCategoryId: 0 + _actionId: 28 + _elementType: 1 + _elementIdentifierId: -1 + _axisRange: 0 + _invert: 0 + _axisContribution: 0 + _keyboardKeyCode: 104 + _modifierKey1: 0 + _modifierKey2: 0 + _modifierKey3: 0 + - _actionCategoryId: 0 + _actionId: 31 + _elementType: 1 + _elementIdentifierId: -1 + _axisRange: 0 + _invert: 0 + _axisContribution: 0 + _keyboardKeyCode: 106 + _modifierKey1: 0 + _modifierKey2: 0 + _modifierKey3: 0 + - _actionCategoryId: 0 + _actionId: 30 + _elementType: 1 + _elementIdentifierId: -1 + _axisRange: 0 + _invert: 0 + _axisContribution: 0 + _keyboardKeyCode: 107 + _modifierKey1: 0 + _modifierKey2: 0 + _modifierKey3: 0 + - _actionCategoryId: 0 + _actionId: 29 + _elementType: 1 + _elementIdentifierId: -1 + _axisRange: 0 + _invert: 0 + _axisContribution: 0 + _keyboardKeyCode: 108 + _modifierKey1: 0 + _modifierKey2: 0 + _modifierKey3: 0 mouseMaps: - id: 0 categoryId: 0 @@ -1940,7 +1621,7 @@ MonoBehaviour: controllerMapLayoutManagerRuleSets: [] controllerMapEnablerRuleSets: [] playerIdCounter: 2 - actionIdCounter: 28 + actionIdCounter: 32 actionCategoryIdCounter: 2 inputBehaviorIdCounter: 4 mapCategoryIdCounter: 1 From 840446b8e43b1140dc67bb4b0f4bed19062032d3 Mon Sep 17 00:00:00 2001 From: Joe Anderson Date: Mon, 15 Dec 2025 21:35:23 +0000 Subject: [PATCH 05/12] Track hits and misses --- Assets/Unity/Scenes/Test Rhythm Game.unity | 1 + Assets/src/RhythmGame.cs | 60 ++++++++++++++++++++++ Assets/src/RhythmGameInputTracks.cs | 33 +++++++----- 3 files changed, 81 insertions(+), 13 deletions(-) diff --git a/Assets/Unity/Scenes/Test Rhythm Game.unity b/Assets/Unity/Scenes/Test Rhythm Game.unity index e8b718f1..4dd4dccb 100644 --- a/Assets/Unity/Scenes/Test Rhythm Game.unity +++ b/Assets/Unity/Scenes/Test Rhythm Game.unity @@ -1018,6 +1018,7 @@ MonoBehaviour: Time: 17 - Type: 3 Time: 17.5 + MissThreshold: 0.16 --- !u!4 &1090514947 Transform: m_ObjectHideFlags: 0 diff --git a/Assets/src/RhythmGame.cs b/Assets/src/RhythmGame.cs index 97c25d0a..0d1a5461 100644 --- a/Assets/src/RhythmGame.cs +++ b/Assets/src/RhythmGame.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using UnityEngine; public enum RhythmGameInputType { @@ -12,6 +13,10 @@ public enum RhythmGameInputType { public class RhythmGameInput { public RhythmGameInputType Type; public float Time; + + public float RelativeTime(float currentTime) => Time - currentTime; + + public float RelativeTimeAbs(float currentTime) => Mathf.Abs(Time - currentTime); } [System.Serializable] @@ -23,10 +28,65 @@ public class RhythmGameSong { public class RhythmGame : MonoBehaviour { public RhythmGameInputTracks InputTracks; public RhythmGameSong Song; + public float MissThreshold; + + private HashSet MissedInputs = new(); public float CurrentTime => Time.time; void Start() { InputTracks.Init(this); } + + void Update() { + if (WrappedInput.GetButtonDown("Rhythm Game Left")) { + HandleButtonDown(RhythmGameInputType.Left); + } + + if (WrappedInput.GetButtonDown("Rhythm Game Down")) { + HandleButtonDown(RhythmGameInputType.Down); + } + + if (WrappedInput.GetButtonDown("Rhythm Game Up")) { + HandleButtonDown(RhythmGameInputType.Up); + } + + if (WrappedInput.GetButtonDown("Rhythm Game Right")) { + HandleButtonDown(RhythmGameInputType.Right); + } + + CheckForLateMisses(); + } + + private void HandleButtonDown(RhythmGameInputType inputType) { + IEnumerable eligibleInputs = VisibleInputs.Where(input => + input.Type == inputType && input.RelativeTimeAbs(CurrentTime) <= MissThreshold + ); + + if (eligibleInputs.Count() == 0) { + Debug.Log("Miss (no input found)"); + return; + } + + RhythmGameInput nearestInput = eligibleInputs.Aggregate( + (a, b) => a.RelativeTimeAbs(CurrentTime) <= b.RelativeTimeAbs(CurrentTime) ? a : b + ); + + InputTracks.HitInput(nearestInput); + } + + private void CheckForLateMisses() { + foreach (RhythmGameInput input in VisibleInputs) { + if (MissedInputs.Contains(input)) + continue; + + if (input.RelativeTime(CurrentTime) < -MissThreshold) { + Debug.Log("Miss (input not pressed)"); + MissedInputs.Add(input); + } + } + } + + private IEnumerable VisibleInputs => + InputTracks.SpawnedInputs.Select(spawnedInput => spawnedInput.Input); } diff --git a/Assets/src/RhythmGameInputTracks.cs b/Assets/src/RhythmGameInputTracks.cs index 1434d560..b9152d44 100644 --- a/Assets/src/RhythmGameInputTracks.cs +++ b/Assets/src/RhythmGameInputTracks.cs @@ -11,14 +11,15 @@ public class RhythmGameInputTracks : MonoBehaviour { public GameObject ModelInput; /** - * The amount of leeway added to SpawnY to ensure that inputs are spawned well - * before they're due to appear on the screen. + * The number of pixels added or subtracted to SpawnY and DespawnY to ensure + * that inputs are spawned well before they're due to appear on the screen, + * and have time to be marked as misses before they're despawned. */ - private const float SPAWN_Y_LEEWAY = -100f; + private const float SPAWN_DESPAWN_LEEWAY = 1080f; private RhythmGame RhythmGame; private bool AfterFirstUpdate = false; - private List SpawnedInputs = new(); + public List SpawnedInputs { get; private set; } = new(); private float TargetY; private float SpawnY; private float DespawnY; @@ -33,8 +34,8 @@ public void Init(RhythmGame rhythmGame) { float inputSize = ModelInput.GetComponent().rect.height; float bottomOfScreen = GetComponent().rect.yMin; float topOfScreen = GetComponent().rect.yMax; - SpawnY = bottomOfScreen - inputSize / 2f + SPAWN_Y_LEEWAY; - DespawnY = topOfScreen + inputSize / 2f; + SpawnY = bottomOfScreen - inputSize / 2f - SPAWN_DESPAWN_LEEWAY; + DespawnY = topOfScreen + inputSize / 2f + SPAWN_DESPAWN_LEEWAY; float secondsPerBeat = 60f / Song.BeatsPerMinute; InputSpeed = PixelsBetweenEachBeat / secondsPerBeat; @@ -65,10 +66,7 @@ void Update() { } // Despawn off-screen inputs - foreach (SpawnedInput spawnedInput in toDespawn) { - Destroy(spawnedInput.GameObject); - SpawnedInputs.Remove(spawnedInput); - } + toDespawn.ForEach(DespawnInput); // Spawn new inputs while (LastSpawnedIndex < Inputs.Count - 1) { @@ -83,9 +81,13 @@ void Update() { } } + public void HitInput(RhythmGameInput input) { + SpawnedInput spawnedInput = SpawnedInputs.Find(spawnedInput => spawnedInput.Input == input); + DespawnInput(spawnedInput); + } + private float YPositionForInput(RhythmGameInput input) { - float relativeTime = input.Time - CurrentTime; - float distanceToTarget = relativeTime * InputSpeed; + float distanceToTarget = input.RelativeTime(CurrentTime) * InputSpeed; return TargetY - distanceToTarget; } @@ -106,6 +108,11 @@ private void SpawnInput(RhythmGameInput input) { SpawnedInputs.Add(spawnedInput); } + void DespawnInput(SpawnedInput spawnedInput) { + Destroy(spawnedInput.GameObject); + SpawnedInputs.Remove(spawnedInput); + } + private Transform TargetForInputType(RhythmGameInputType inputType) { switch (inputType) { case RhythmGameInputType.Left: @@ -128,7 +135,7 @@ private Transform TargetForInputType(RhythmGameInputType inputType) { private List Inputs => Song.Inputs; private float CurrentTime => RhythmGame.CurrentTime; - class SpawnedInput { + public class SpawnedInput { public RhythmGameInput Input; public GameObject GameObject; } From abc589b9fd66a7dda0f51af53f77ab4b532f1735 Mon Sep 17 00:00:00 2001 From: Joe Anderson Date: Tue, 16 Dec 2025 10:00:03 +0000 Subject: [PATCH 06/12] Decouple RhythmGame and RhythmGameInputTracks --- Assets/src/LinearWindow.cs | 55 +++++++++++++++++++++ Assets/src/LinearWindow.cs.meta | 11 +++++ Assets/src/RhythmGame.cs | 57 +++++++++++----------- Assets/src/RhythmGameInputTracks.cs | 75 +++++++++++------------------ 4 files changed, 120 insertions(+), 78 deletions(-) create mode 100644 Assets/src/LinearWindow.cs create mode 100644 Assets/src/LinearWindow.cs.meta diff --git a/Assets/src/LinearWindow.cs b/Assets/src/LinearWindow.cs new file mode 100644 index 00000000..a65d9c1f --- /dev/null +++ b/Assets/src/LinearWindow.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +/** + * Utility class to track a moving window of items in a list for which some + * predicate is true. + * + * The list given to this class must not change, and items must enter the window + * in ascending order of their index. They can exit the window in any order. + */ +public class LinearWindow { + public List Current { get; } = new(); + + private T[] AllItems; + private Action OnEnterWindow; + private Action OnExitWindow; + private int LastEnteredWindow = -1; + + public LinearWindow( + IEnumerable allItems, + Action onEnterWindow = null, + Action onExitWindow = null + ) { + AllItems = allItems.ToArray(); + OnEnterWindow = onEnterWindow; + OnExitWindow = onExitWindow; + } + + public void Update(Func predicate) { + Current + .Where(item => !predicate(item)) + .ToList() + .ForEach(item => { + Current.Remove(item); + OnExitWindow?.Invoke(item); + }); + + while (LastEnteredWindow < AllItems.Count() - 1) { + T item = AllItems[LastEnteredWindow + 1]; + + if (predicate(item)) { + LastEnteredWindow++; + Current.Add(item); + OnEnterWindow?.Invoke(item); + } else { + break; + } + } + } + + public void RemoveEarly(T item) { + Current.Remove(item); + } +} diff --git a/Assets/src/LinearWindow.cs.meta b/Assets/src/LinearWindow.cs.meta new file mode 100644 index 00000000..c537c2e1 --- /dev/null +++ b/Assets/src/LinearWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d764386ed33914366a8caff74bc3335a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/src/RhythmGame.cs b/Assets/src/RhythmGame.cs index 0d1a5461..7dcad6cb 100644 --- a/Assets/src/RhythmGame.cs +++ b/Assets/src/RhythmGame.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Linq; using UnityEngine; public enum RhythmGameInputType { @@ -30,15 +29,16 @@ public class RhythmGame : MonoBehaviour { public RhythmGameSong Song; public float MissThreshold; - private HashSet MissedInputs = new(); - - public float CurrentTime => Time.time; + private LinearWindow PressableInputsWindow; void Start() { - InputTracks.Init(this); + PressableInputsWindow = new(Song.Inputs, onExitWindow: MissedInput); + InputTracks.Init(Song, getTime: () => CurrentTime); } void Update() { + PressableInputsWindow.Update(input => input.RelativeTimeAbs(CurrentTime) <= MissThreshold); + if (WrappedInput.GetButtonDown("Rhythm Game Left")) { HandleButtonDown(RhythmGameInputType.Left); } @@ -54,39 +54,36 @@ void Update() { if (WrappedInput.GetButtonDown("Rhythm Game Right")) { HandleButtonDown(RhythmGameInputType.Right); } - - CheckForLateMisses(); } private void HandleButtonDown(RhythmGameInputType inputType) { - IEnumerable eligibleInputs = VisibleInputs.Where(input => - input.Type == inputType && input.RelativeTimeAbs(CurrentTime) <= MissThreshold - ); - - if (eligibleInputs.Count() == 0) { - Debug.Log("Miss (no input found)"); - return; + /** + * If there are multiple matching inputs, return the earliest one so that + * inputs are pressed in the correct order. + */ + RhythmGameInput input = PressableInputs.Find(input => input.Type == inputType); + + if (input == null) { + MissedInput(inputType); + } else { + HitInput(input); } + } - RhythmGameInput nearestInput = eligibleInputs.Aggregate( - (a, b) => a.RelativeTimeAbs(CurrentTime) <= b.RelativeTimeAbs(CurrentTime) ? a : b - ); - - InputTracks.HitInput(nearestInput); + private void HitInput(RhythmGameInput input) { + PressableInputsWindow.RemoveEarly(input); + InputTracks.HitInput(input); } - private void CheckForLateMisses() { - foreach (RhythmGameInput input in VisibleInputs) { - if (MissedInputs.Contains(input)) - continue; + private void MissedInput(RhythmGameInput input) { + Debug.Log("Miss (input not pressed)"); + } - if (input.RelativeTime(CurrentTime) < -MissThreshold) { - Debug.Log("Miss (input not pressed)"); - MissedInputs.Add(input); - } - } + private void MissedInput(RhythmGameInputType inputType) { + Debug.Log("Miss (no input found)"); } - private IEnumerable VisibleInputs => - InputTracks.SpawnedInputs.Select(spawnedInput => spawnedInput.Input); + private float CurrentTime => Time.time; + + private List PressableInputs => PressableInputsWindow.Current; } diff --git a/Assets/src/RhythmGameInputTracks.cs b/Assets/src/RhythmGameInputTracks.cs index b9152d44..cf51a31b 100644 --- a/Assets/src/RhythmGameInputTracks.cs +++ b/Assets/src/RhythmGameInputTracks.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; @@ -17,17 +18,19 @@ public class RhythmGameInputTracks : MonoBehaviour { */ private const float SPAWN_DESPAWN_LEEWAY = 1080f; - private RhythmGame RhythmGame; + private LinearWindow VisibleInputsWindow; + private Func GetTime; + private Dictionary InputGameObjects = new(); + private bool AfterFirstUpdate = false; - public List SpawnedInputs { get; private set; } = new(); private float TargetY; private float SpawnY; private float DespawnY; private float InputSpeed; - private int LastSpawnedIndex = -1; - public void Init(RhythmGame rhythmGame) { - RhythmGame = rhythmGame; + public void Init(RhythmGameSong song, Func getTime) { + VisibleInputsWindow = new(song.Inputs, onEnterWindow: SpawnInput, onExitWindow: DespawnInput); + GetTime = getTime; TargetY = transform.InverseTransformPoint(LeftTarget.position).y; @@ -37,7 +40,7 @@ public void Init(RhythmGame rhythmGame) { SpawnY = bottomOfScreen - inputSize / 2f - SPAWN_DESPAWN_LEEWAY; DespawnY = topOfScreen + inputSize / 2f + SPAWN_DESPAWN_LEEWAY; - float secondsPerBeat = 60f / Song.BeatsPerMinute; + float secondsPerBeat = 60f / song.BeatsPerMinute; InputSpeed = PixelsBetweenEachBeat / secondsPerBeat; } @@ -51,39 +54,23 @@ void Update() { return; } - List toDespawn = new(); - - // Update spawned inputs - foreach (SpawnedInput spawnedInput in SpawnedInputs) { - float y = YPositionForInput(spawnedInput.Input); + VisibleInputsWindow.Update(input => { + float y = YPositionForInput(input); + return y >= SpawnY && y <= DespawnY; + }); - if (y >= DespawnY) { - toDespawn.Add(spawnedInput); - } else { - Transform inputTransform = spawnedInput.GameObject.transform; + foreach (RhythmGameInput input in VisibleInputs) { + if (InputGameObjects.ContainsKey(input)) { + float y = YPositionForInput(input); + Transform inputTransform = InputGameObjects[input].transform; inputTransform.localPosition = new Vector2(inputTransform.localPosition.x, y); } } - - // Despawn off-screen inputs - toDespawn.ForEach(DespawnInput); - - // Spawn new inputs - while (LastSpawnedIndex < Inputs.Count - 1) { - RhythmGameInput nextInput = Inputs[LastSpawnedIndex + 1]; - - if (DueToSpawnInput(nextInput)) { - SpawnInput(nextInput); - LastSpawnedIndex++; - } else { - break; - } - } } public void HitInput(RhythmGameInput input) { - SpawnedInput spawnedInput = SpawnedInputs.Find(spawnedInput => spawnedInput.Input == input); - DespawnInput(spawnedInput); + DespawnInput(input); + VisibleInputsWindow.RemoveEarly(input); } private float YPositionForInput(RhythmGameInput input) { @@ -91,11 +78,10 @@ private float YPositionForInput(RhythmGameInput input) { return TargetY - distanceToTarget; } - private bool DueToSpawnInput(RhythmGameInput input) => YPositionForInput(input) >= SpawnY; - private void SpawnInput(RhythmGameInput input) { GameObject inputGameObject = Instantiate(ModelInput, transform); inputGameObject.SetActive(true); + InputGameObjects.Add(input, inputGameObject); Transform target = TargetForInputType(input.Type); float spawnX = transform.InverseTransformPoint(target.position).x; @@ -103,14 +89,13 @@ private void SpawnInput(RhythmGameInput input) { inputGameObject.transform.localPosition = new Vector2(spawnX, SpawnY); inputGameObject.transform.localRotation = target.localRotation; inputGameObject.GetComponent().color = target.GetComponent().color; - - SpawnedInput spawnedInput = new SpawnedInput { Input = input, GameObject = inputGameObject }; - SpawnedInputs.Add(spawnedInput); } - void DespawnInput(SpawnedInput spawnedInput) { - Destroy(spawnedInput.GameObject); - SpawnedInputs.Remove(spawnedInput); + private void DespawnInput(RhythmGameInput input) { + if (InputGameObjects.ContainsKey(input)) { + Destroy(InputGameObjects[input]); + InputGameObjects.Remove(input); + } } private Transform TargetForInputType(RhythmGameInputType inputType) { @@ -131,12 +116,6 @@ private Transform TargetForInputType(RhythmGameInputType inputType) { throw new System.ArgumentException("Unexpected input type"); } - private RhythmGameSong Song => RhythmGame.Song; - private List Inputs => Song.Inputs; - private float CurrentTime => RhythmGame.CurrentTime; - - public class SpawnedInput { - public RhythmGameInput Input; - public GameObject GameObject; - } + public List VisibleInputs => VisibleInputsWindow.Current; + private float CurrentTime => GetTime(); } From 039efad79327f645081b4816038185e988094f54 Mon Sep 17 00:00:00 2001 From: Joe Anderson Date: Fri, 19 Dec 2025 13:35:20 +0000 Subject: [PATCH 07/12] Refactor 'input' to 'note' --- Assets/Unity/Scenes/Test Rhythm Game.unity | 12 +- Assets/src/RhythmGame.cs | 56 ++++---- Assets/src/RhythmGameInputTracks.cs | 121 ------------------ Assets/src/RhythmGameNotesView.cs | 114 +++++++++++++++++ ...ks.cs.meta => RhythmGameNotesView.cs.meta} | 0 5 files changed, 148 insertions(+), 155 deletions(-) delete mode 100644 Assets/src/RhythmGameInputTracks.cs create mode 100644 Assets/src/RhythmGameNotesView.cs rename Assets/src/{RhythmGameInputTracks.cs.meta => RhythmGameNotesView.cs.meta} (100%) diff --git a/Assets/Unity/Scenes/Test Rhythm Game.unity b/Assets/Unity/Scenes/Test Rhythm Game.unity index 4dd4dccb..2159c8f6 100644 --- a/Assets/Unity/Scenes/Test Rhythm Game.unity +++ b/Assets/Unity/Scenes/Test Rhythm Game.unity @@ -405,7 +405,7 @@ GameObject: - component: {fileID: 452012669} - component: {fileID: 452012668} m_Layer: 5 - m_Name: Input Targets + m_Name: Note Targets m_TagString: Untagged m_Icon: {fileID: 0} m_NavMeshLayer: 0 @@ -858,7 +858,7 @@ GameObject: - component: {fileID: 959757544} - component: {fileID: 959757543} m_Layer: 5 - m_Name: Model Input + m_Name: Model Note m_TagString: Untagged m_Icon: {fileID: 0} m_NavMeshLayer: 0 @@ -950,10 +950,10 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: da791d72e319d492e9eb6f6d293b66a6, type: 3} m_Name: m_EditorClassIdentifier: - InputTracks: {fileID: 1489695914} + NotesView: {fileID: 1489695914} Song: BeatsPerMinute: 120 - Inputs: + Notes: - Type: 2 Time: 2 - Type: 2 @@ -1049,7 +1049,7 @@ GameObject: - component: {fileID: 1489695915} - component: {fileID: 1489695917} m_Layer: 5 - m_Name: Input Tracks + m_Name: Notes View m_TagString: Untagged m_Icon: {fileID: 0} m_NavMeshLayer: 0 @@ -1093,7 +1093,7 @@ MonoBehaviour: DownTarget: {fileID: 250930357} UpTarget: {fileID: 2014153219} RightTarget: {fileID: 327090775} - ModelInput: {fileID: 959757541} + ModelNote: {fileID: 959757541} --- !u!114 &1489695915 MonoBehaviour: m_ObjectHideFlags: 0 diff --git a/Assets/src/RhythmGame.cs b/Assets/src/RhythmGame.cs index 7dcad6cb..ba293eaa 100644 --- a/Assets/src/RhythmGame.cs +++ b/Assets/src/RhythmGame.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using UnityEngine; -public enum RhythmGameInputType { +public enum RhythmGameNoteType { Left = 0, Down = 1, Up = 2, @@ -9,8 +9,8 @@ public enum RhythmGameInputType { }; [System.Serializable] -public class RhythmGameInput { - public RhythmGameInputType Type; +public class RhythmGameNote { + public RhythmGameNoteType Type; public float Time; public float RelativeTime(float currentTime) => Time - currentTime; @@ -21,69 +21,69 @@ public class RhythmGameInput { [System.Serializable] public class RhythmGameSong { public int BeatsPerMinute; - public List Inputs; + public List Notes; } public class RhythmGame : MonoBehaviour { - public RhythmGameInputTracks InputTracks; + public RhythmGameNotesView NotesView; public RhythmGameSong Song; public float MissThreshold; - private LinearWindow PressableInputsWindow; + private LinearWindow PressableNotesWindow; void Start() { - PressableInputsWindow = new(Song.Inputs, onExitWindow: MissedInput); - InputTracks.Init(Song, getTime: () => CurrentTime); + PressableNotesWindow = new(Song.Notes, onExitWindow: MissedNote); + NotesView.Init(Song, getTime: () => CurrentTime); } void Update() { - PressableInputsWindow.Update(input => input.RelativeTimeAbs(CurrentTime) <= MissThreshold); + PressableNotesWindow.Update(note => note.RelativeTimeAbs(CurrentTime) <= MissThreshold); if (WrappedInput.GetButtonDown("Rhythm Game Left")) { - HandleButtonDown(RhythmGameInputType.Left); + HandleButtonDown(RhythmGameNoteType.Left); } if (WrappedInput.GetButtonDown("Rhythm Game Down")) { - HandleButtonDown(RhythmGameInputType.Down); + HandleButtonDown(RhythmGameNoteType.Down); } if (WrappedInput.GetButtonDown("Rhythm Game Up")) { - HandleButtonDown(RhythmGameInputType.Up); + HandleButtonDown(RhythmGameNoteType.Up); } if (WrappedInput.GetButtonDown("Rhythm Game Right")) { - HandleButtonDown(RhythmGameInputType.Right); + HandleButtonDown(RhythmGameNoteType.Right); } } - private void HandleButtonDown(RhythmGameInputType inputType) { + private void HandleButtonDown(RhythmGameNoteType noteType) { /** - * If there are multiple matching inputs, return the earliest one so that - * inputs are pressed in the correct order. + * If there are multiple matching notes, return the earliest one so that + * notes are pressed in the correct order. */ - RhythmGameInput input = PressableInputs.Find(input => input.Type == inputType); + RhythmGameNote note = PressableNotes.Find(note => note.Type == noteType); - if (input == null) { - MissedInput(inputType); + if (note == null) { + MissedNote(noteType); } else { - HitInput(input); + HitNote(note); } } - private void HitInput(RhythmGameInput input) { - PressableInputsWindow.RemoveEarly(input); - InputTracks.HitInput(input); + private void HitNote(RhythmGameNote note) { + PressableNotesWindow.RemoveEarly(note); + NotesView.HitNote(note); } - private void MissedInput(RhythmGameInput input) { - Debug.Log("Miss (input not pressed)"); + private void MissedNote(RhythmGameNote note) { + Debug.Log("Miss (note not pressed)"); } - private void MissedInput(RhythmGameInputType inputType) { - Debug.Log("Miss (no input found)"); + private void MissedNote(RhythmGameNoteType noteType) { + Debug.Log("Miss (no note found)"); } private float CurrentTime => Time.time; - private List PressableInputs => PressableInputsWindow.Current; + private List PressableNotes => PressableNotesWindow.Current; } diff --git a/Assets/src/RhythmGameInputTracks.cs b/Assets/src/RhythmGameInputTracks.cs deleted file mode 100644 index cf51a31b..00000000 --- a/Assets/src/RhythmGameInputTracks.cs +++ /dev/null @@ -1,121 +0,0 @@ -using System; -using System.Collections.Generic; -using UnityEngine; -using UnityEngine.UI; - -public class RhythmGameInputTracks : MonoBehaviour { - public float PixelsBetweenEachBeat; - public Transform LeftTarget; - public Transform DownTarget; - public Transform UpTarget; - public Transform RightTarget; - public GameObject ModelInput; - - /** - * The number of pixels added or subtracted to SpawnY and DespawnY to ensure - * that inputs are spawned well before they're due to appear on the screen, - * and have time to be marked as misses before they're despawned. - */ - private const float SPAWN_DESPAWN_LEEWAY = 1080f; - - private LinearWindow VisibleInputsWindow; - private Func GetTime; - private Dictionary InputGameObjects = new(); - - private bool AfterFirstUpdate = false; - private float TargetY; - private float SpawnY; - private float DespawnY; - private float InputSpeed; - - public void Init(RhythmGameSong song, Func getTime) { - VisibleInputsWindow = new(song.Inputs, onEnterWindow: SpawnInput, onExitWindow: DespawnInput); - GetTime = getTime; - - TargetY = transform.InverseTransformPoint(LeftTarget.position).y; - - float inputSize = ModelInput.GetComponent().rect.height; - float bottomOfScreen = GetComponent().rect.yMin; - float topOfScreen = GetComponent().rect.yMax; - SpawnY = bottomOfScreen - inputSize / 2f - SPAWN_DESPAWN_LEEWAY; - DespawnY = topOfScreen + inputSize / 2f + SPAWN_DESPAWN_LEEWAY; - - float secondsPerBeat = 60f / song.BeatsPerMinute; - InputSpeed = PixelsBetweenEachBeat / secondsPerBeat; - } - - void Update() { - /** - * Do not spawn any inputs during the first update, since the positions can - * be incorrect. - */ - if (!AfterFirstUpdate) { - AfterFirstUpdate = true; - return; - } - - VisibleInputsWindow.Update(input => { - float y = YPositionForInput(input); - return y >= SpawnY && y <= DespawnY; - }); - - foreach (RhythmGameInput input in VisibleInputs) { - if (InputGameObjects.ContainsKey(input)) { - float y = YPositionForInput(input); - Transform inputTransform = InputGameObjects[input].transform; - inputTransform.localPosition = new Vector2(inputTransform.localPosition.x, y); - } - } - } - - public void HitInput(RhythmGameInput input) { - DespawnInput(input); - VisibleInputsWindow.RemoveEarly(input); - } - - private float YPositionForInput(RhythmGameInput input) { - float distanceToTarget = input.RelativeTime(CurrentTime) * InputSpeed; - return TargetY - distanceToTarget; - } - - private void SpawnInput(RhythmGameInput input) { - GameObject inputGameObject = Instantiate(ModelInput, transform); - inputGameObject.SetActive(true); - InputGameObjects.Add(input, inputGameObject); - - Transform target = TargetForInputType(input.Type); - float spawnX = transform.InverseTransformPoint(target.position).x; - - inputGameObject.transform.localPosition = new Vector2(spawnX, SpawnY); - inputGameObject.transform.localRotation = target.localRotation; - inputGameObject.GetComponent().color = target.GetComponent().color; - } - - private void DespawnInput(RhythmGameInput input) { - if (InputGameObjects.ContainsKey(input)) { - Destroy(InputGameObjects[input]); - InputGameObjects.Remove(input); - } - } - - private Transform TargetForInputType(RhythmGameInputType inputType) { - switch (inputType) { - case RhythmGameInputType.Left: - return LeftTarget; - - case RhythmGameInputType.Down: - return DownTarget; - - case RhythmGameInputType.Up: - return UpTarget; - - case RhythmGameInputType.Right: - return RightTarget; - } - - throw new System.ArgumentException("Unexpected input type"); - } - - public List VisibleInputs => VisibleInputsWindow.Current; - private float CurrentTime => GetTime(); -} diff --git a/Assets/src/RhythmGameNotesView.cs b/Assets/src/RhythmGameNotesView.cs new file mode 100644 index 00000000..b8e5d406 --- /dev/null +++ b/Assets/src/RhythmGameNotesView.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.UI; + +public class RhythmGameNotesView : MonoBehaviour { + public float PixelsBetweenEachBeat; + public Transform LeftTarget; + public Transform DownTarget; + public Transform UpTarget; + public Transform RightTarget; + public GameObject ModelNote; + + private LinearWindow VisibleNotesWindow; + private Func GetTime; + private Dictionary NoteGameObjects = new(); + + private bool AfterFirstUpdate = false; + private float TargetY; + private float SpawnY; + private float DespawnY; + private float NoteSpeed; + + public void Init(RhythmGameSong song, Func getTime) { + VisibleNotesWindow = new(song.Notes, onEnterWindow: SpawnNote, onExitWindow: DespawnNote); + GetTime = getTime; + + TargetY = transform.InverseTransformPoint(LeftTarget.position).y; + + float noteSize = ModelNote.GetComponent().rect.height; + float bottomOfScreen = GetComponent().rect.yMin; + float topOfScreen = GetComponent().rect.yMax; + SpawnY = bottomOfScreen - noteSize / 2f; + DespawnY = topOfScreen + noteSize / 2f; + + float secondsPerBeat = 60f / song.BeatsPerMinute; + NoteSpeed = PixelsBetweenEachBeat / secondsPerBeat; + } + + void Update() { + /** + * Do not spawn any notes during the first update, since the positions can + * be incorrect. + */ + if (!AfterFirstUpdate) { + AfterFirstUpdate = true; + return; + } + + VisibleNotesWindow.Update(note => { + float y = YPositionForNote(note); + return y >= SpawnY && y <= DespawnY; + }); + + foreach (RhythmGameNote note in VisibleNotes) { + if (NoteGameObjects.ContainsKey(note)) { + float y = YPositionForNote(note); + Transform noteTransform = NoteGameObjects[note].transform; + noteTransform.localPosition = new Vector2(noteTransform.localPosition.x, y); + } + } + } + + public void HitNote(RhythmGameNote note) { + DespawnNote(note); + VisibleNotesWindow.RemoveEarly(note); + } + + private float YPositionForNote(RhythmGameNote note) { + float distanceToTarget = note.RelativeTime(CurrentTime) * NoteSpeed; + return TargetY - distanceToTarget; + } + + private void SpawnNote(RhythmGameNote note) { + GameObject noteGameObject = Instantiate(ModelNote, transform); + noteGameObject.SetActive(true); + NoteGameObjects.Add(note, noteGameObject); + + Transform target = TargetForNoteType(note.Type); + float spawnX = transform.InverseTransformPoint(target.position).x; + + noteGameObject.transform.localPosition = new Vector2(spawnX, SpawnY); + noteGameObject.transform.localRotation = target.localRotation; + noteGameObject.GetComponent().color = target.GetComponent().color; + } + + private void DespawnNote(RhythmGameNote note) { + if (NoteGameObjects.ContainsKey(note)) { + Destroy(NoteGameObjects[note]); + NoteGameObjects.Remove(note); + } + } + + private Transform TargetForNoteType(RhythmGameNoteType noteType) { + switch (noteType) { + case RhythmGameNoteType.Left: + return LeftTarget; + + case RhythmGameNoteType.Down: + return DownTarget; + + case RhythmGameNoteType.Up: + return UpTarget; + + case RhythmGameNoteType.Right: + return RightTarget; + } + + throw new System.ArgumentException("Unexpected note type"); + } + + public List VisibleNotes => VisibleNotesWindow.Current; + private float CurrentTime => GetTime(); +} diff --git a/Assets/src/RhythmGameInputTracks.cs.meta b/Assets/src/RhythmGameNotesView.cs.meta similarity index 100% rename from Assets/src/RhythmGameInputTracks.cs.meta rename to Assets/src/RhythmGameNotesView.cs.meta From 1f645a3ed74a33eb6f0d2ed415f7cb0f845378ad Mon Sep 17 00:00:00 2001 From: Joe Anderson Date: Fri, 19 Dec 2025 15:20:10 +0000 Subject: [PATCH 08/12] Add support for held notes --- Assets/Unity/Scenes/Test Rhythm Game.unity | 241 +++++++++++++++++---- Assets/src/RhythmGame.cs | 58 ++++- Assets/src/RhythmGameNoteSprite.cs | 44 ++++ Assets/src/RhythmGameNoteSprite.cs.meta | 11 + Assets/src/RhythmGameNotesView.cs | 66 ++++-- 5 files changed, 351 insertions(+), 69 deletions(-) create mode 100644 Assets/src/RhythmGameNoteSprite.cs create mode 100644 Assets/src/RhythmGameNoteSprite.cs.meta diff --git a/Assets/Unity/Scenes/Test Rhythm Game.unity b/Assets/Unity/Scenes/Test Rhythm Game.unity index 2159c8f6..7be8855c 100644 --- a/Assets/Unity/Scenes/Test Rhythm Game.unity +++ b/Assets/Unity/Scenes/Test Rhythm Game.unity @@ -217,6 +217,81 @@ MonoBehaviour: OscillationScale: 0.025 XAxis: 1 YAxis: 1 +--- !u!1 &80893927 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 80893928} + - component: {fileID: 80893930} + - component: {fileID: 80893929} + m_Layer: 5 + m_Name: Trail + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &80893928 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 80893927} + 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: 959757542} + 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: 30, y: 100} + m_Pivot: {x: 0.5, y: 1} +--- !u!114 &80893929 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 80893927} + 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: 21300000, guid: 1b5f5f98320794ffcaab523e6b6d47fd, type: 3} + m_Type: 3 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 1 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &80893930 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 80893927} + m_CullTransparentMesh: 1 --- !u!1 &250930356 GameObject: m_ObjectHideFlags: 0 @@ -429,11 +504,11 @@ RectTransform: - {fileID: 327090775} m_Father: {fileID: 1489695913} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} - m_AnchorMin: {x: 0.5, y: 1} - m_AnchorMax: {x: 0.5, y: 1} - m_AnchoredPosition: {x: 0, y: -40} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: 0, y: 460} m_SizeDelta: {x: 0, y: 0} - m_Pivot: {x: 0.5, y: 1} + m_Pivot: {x: 0.5, y: 0.5} --- !u!114 &452012668 MonoBehaviour: m_ObjectHideFlags: 0 @@ -581,6 +656,81 @@ MonoBehaviour: m_EditorClassIdentifier: m_AspectMode: 3 m_AspectRatio: 1.7777778 +--- !u!1 &596422148 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 596422149} + - component: {fileID: 596422151} + - component: {fileID: 596422150} + m_Layer: 5 + m_Name: Arrow + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &596422149 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 596422148} + 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: 959757542} + 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: 120, y: 80} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &596422150 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 596422148} + 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: 21300000, guid: bfb48dca50c3049d4af953850214ffd9, type: 3} + m_Type: 0 + m_PreserveAspect: 1 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &596422151 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 596422148} + m_CullTransparentMesh: 1 --- !u!1 &740930167 GameObject: m_ObjectHideFlags: 0 @@ -855,34 +1005,31 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 959757542} - - component: {fileID: 959757544} - component: {fileID: 959757543} m_Layer: 5 - m_Name: Model Note + m_Name: Note Sprite Template m_TagString: Untagged m_Icon: {fileID: 0} m_NavMeshLayer: 0 m_StaticEditorFlags: 0 m_IsActive: 0 ---- !u!224 &959757542 -RectTransform: +--- !u!4 &959757542 +Transform: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 959757541} + 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_Children: + - {fileID: 80893928} + - {fileID: 596422149} m_Father: {fileID: 1489695913} 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: 120, y: 80} - m_Pivot: {x: 0.5, y: 0.5} --- !u!114 &959757543 MonoBehaviour: m_ObjectHideFlags: 0 @@ -892,35 +1039,13 @@ MonoBehaviour: m_GameObject: {fileID: 959757541} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Script: {fileID: 11500000, guid: 1a2ee74cef1fa490f899eb840223a9ef, 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: 21300000, guid: bfb48dca50c3049d4af953850214ffd9, type: 3} - m_Type: 0 - m_PreserveAspect: 1 - m_FillCenter: 1 - m_FillMethod: 4 - m_FillAmount: 1 - m_FillClockwise: 1 - m_FillOrigin: 0 - m_UseSpriteMesh: 0 - m_PixelsPerUnitMultiplier: 1 ---- !u!222 &959757544 -CanvasRenderer: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 959757541} - m_CullTransparentMesh: 1 + Arrow: {fileID: 596422150} + Trail: {fileID: 80893929} + ArrowTransform: {fileID: 596422149} + TrailTransform: {fileID: 80893928} --- !u!1 &1090514945 GameObject: m_ObjectHideFlags: 0 @@ -956,68 +1081,100 @@ MonoBehaviour: Notes: - Type: 2 Time: 2 + Duration: 0 - Type: 2 Time: 2.5 + Duration: 0 - Type: 1 Time: 3 + Duration: 0 - Type: 1 Time: 3.5 + Duration: 0 - Type: 0 Time: 4 + Duration: 0.5 - Type: 3 Time: 4.5 + Duration: 0.5 - Type: 0 Time: 5 + Duration: 0.5 - Type: 3 Time: 5.5 + Duration: 0.5 - Type: 2 Time: 6 + Duration: 0 - Type: 2 Time: 6.5 + Duration: 0 - Type: 1 Time: 7 + Duration: 0 - Type: 1 Time: 7.5 + Duration: 0 - Type: 0 Time: 8 + Duration: 0.5 - Type: 3 Time: 8.5 + Duration: 0.5 - Type: 0 Time: 9 + Duration: 0.5 - Type: 3 Time: 9.5 + Duration: 0.5 - Type: 2 Time: 10 + Duration: 0 - Type: 2 Time: 10.5 + Duration: 0 - Type: 1 Time: 11 + Duration: 0 - Type: 1 Time: 11.5 + Duration: 0 - Type: 0 Time: 12 + Duration: 0.5 - Type: 3 Time: 12.5 + Duration: 0.5 - Type: 0 Time: 13 + Duration: 0.5 - Type: 3 Time: 13.5 + Duration: 0.5 - Type: 2 Time: 14 + Duration: 0 - Type: 2 Time: 14.5 + Duration: 0 - Type: 1 Time: 15 + Duration: 0 - Type: 1 Time: 15.5 + Duration: 0 - Type: 0 Time: 16 + Duration: 0.5 - Type: 3 Time: 16.5 + Duration: 0.5 - Type: 0 Time: 17 + Duration: 0.5 - Type: 3 Time: 17.5 + Duration: 0.5 MissThreshold: 0.16 --- !u!4 &1090514947 Transform: @@ -1093,7 +1250,7 @@ MonoBehaviour: DownTarget: {fileID: 250930357} UpTarget: {fileID: 2014153219} RightTarget: {fileID: 327090775} - ModelNote: {fileID: 959757541} + NoteSpriteTemplate: {fileID: 959757543} --- !u!114 &1489695915 MonoBehaviour: m_ObjectHideFlags: 0 diff --git a/Assets/src/RhythmGame.cs b/Assets/src/RhythmGame.cs index ba293eaa..92c64ab7 100644 --- a/Assets/src/RhythmGame.cs +++ b/Assets/src/RhythmGame.cs @@ -12,10 +12,13 @@ public enum RhythmGameNoteType { public class RhythmGameNote { public RhythmGameNoteType Type; public float Time; + public float Duration = 0f; - public float RelativeTime(float currentTime) => Time - currentTime; + public bool IsHold => Duration > 0f; + public bool IsInstant => !IsHold; - public float RelativeTimeAbs(float currentTime) => Mathf.Abs(Time - currentTime); + public float RelativeTime(float currentTime, bool end = false) => + Time - currentTime + (end ? Duration : 0f); } [System.Serializable] @@ -27,9 +30,11 @@ public class RhythmGameSong { public class RhythmGame : MonoBehaviour { public RhythmGameNotesView NotesView; public RhythmGameSong Song; - public float MissThreshold; + + public const float MISS_THRESHOLD = 0.16f; private LinearWindow PressableNotesWindow; + private Dictionary HeldNotes = new(); void Start() { PressableNotesWindow = new(Song.Notes, onExitWindow: MissedNote); @@ -37,7 +42,10 @@ void Start() { } void Update() { - PressableNotesWindow.Update(note => note.RelativeTimeAbs(CurrentTime) <= MissThreshold); + PressableNotesWindow.Update(note => + note.RelativeTime(CurrentTime) <= MISS_THRESHOLD + && note.RelativeTime(CurrentTime, end: true) >= -MISS_THRESHOLD + ); if (WrappedInput.GetButtonDown("Rhythm Game Left")) { HandleButtonDown(RhythmGameNoteType.Left); @@ -54,6 +62,22 @@ void Update() { if (WrappedInput.GetButtonDown("Rhythm Game Right")) { HandleButtonDown(RhythmGameNoteType.Right); } + + if (WrappedInput.GetButtonUp("Rhythm Game Left")) { + HandleButtonUp(RhythmGameNoteType.Left); + } + + if (WrappedInput.GetButtonUp("Rhythm Game Down")) { + HandleButtonUp(RhythmGameNoteType.Down); + } + + if (WrappedInput.GetButtonUp("Rhythm Game Up")) { + HandleButtonUp(RhythmGameNoteType.Up); + } + + if (WrappedInput.GetButtonUp("Rhythm Game Right")) { + HandleButtonUp(RhythmGameNoteType.Right); + } } private void HandleButtonDown(RhythmGameNoteType noteType) { @@ -70,9 +94,35 @@ private void HandleButtonDown(RhythmGameNoteType noteType) { } } + private void HandleButtonUp(RhythmGameNoteType noteType) { + if (HeldNotes.ContainsKey(noteType)) { + ReleaseNote(HeldNotes[noteType]); + } + } + private void HitNote(RhythmGameNote note) { PressableNotesWindow.RemoveEarly(note); NotesView.HitNote(note); + + if (note.IsHold) { + HeldNotes.Add(note.Type, note); + + if (note.RelativeTime(CurrentTime) < -MISS_THRESHOLD) { + Debug.Log("Miss (hold note started too late"); + } + } + } + + private void ReleaseNote(RhythmGameNote note) { + float remainingDuration = note.RelativeTime(CurrentTime, end: true); + bool closeEnough = remainingDuration <= MISS_THRESHOLD; + + HeldNotes.Remove(note.Type); + NotesView.ReleaseNote(note, closeEnough: closeEnough); + + if (!closeEnough) { + Debug.Log("Released note too early"); + } } private void MissedNote(RhythmGameNote note) { diff --git a/Assets/src/RhythmGameNoteSprite.cs b/Assets/src/RhythmGameNoteSprite.cs new file mode 100644 index 00000000..b7ed90e3 --- /dev/null +++ b/Assets/src/RhythmGameNoteSprite.cs @@ -0,0 +1,44 @@ +using UnityEngine; +using UnityEngine.UI; + +public class RhythmGameNoteSprite : MonoBehaviour { + public Image Arrow; + public Image Trail; + public RectTransform ArrowTransform; + public RectTransform TrailTransform; + + public bool IsHolding { get; private set; } = false; + + private RhythmGameNote Note; + + public void Initialize(RhythmGameNote note, Color color, Quaternion rotation, float trailLength) { + Note = note; + Arrow.color = color; + Trail.color = color; + TrailTransform.sizeDelta = new Vector2(TrailTransform.sizeDelta.x, trailLength); + ArrowTransform.localRotation = rotation; + gameObject.SetActive(true); + } + + public void StartHolding() { + Arrow.enabled = false; + IsHolding = true; + } + + public void UpdateHolding(float time) { + float remainingDuration = Note.RelativeTime(time, end: true); + Trail.fillAmount = remainingDuration / Note.Duration; + } + + public void StopHolding(float time, bool closeEnough) { + IsHolding = false; + + if (closeEnough) { + Trail.enabled = false; + } else { + Trail.color = new Color(Trail.color.r, Trail.color.g, Trail.color.b, 0.5f); + } + } + + public float ArrowHeight => ArrowTransform.rect.height; +} diff --git a/Assets/src/RhythmGameNoteSprite.cs.meta b/Assets/src/RhythmGameNoteSprite.cs.meta new file mode 100644 index 00000000..24fd8a34 --- /dev/null +++ b/Assets/src/RhythmGameNoteSprite.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1a2ee74cef1fa490f899eb840223a9ef +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/src/RhythmGameNotesView.cs b/Assets/src/RhythmGameNotesView.cs index b8e5d406..01377ea7 100644 --- a/Assets/src/RhythmGameNotesView.cs +++ b/Assets/src/RhythmGameNotesView.cs @@ -9,11 +9,11 @@ public class RhythmGameNotesView : MonoBehaviour { public Transform DownTarget; public Transform UpTarget; public Transform RightTarget; - public GameObject ModelNote; + public RhythmGameNoteSprite NoteSpriteTemplate; private LinearWindow VisibleNotesWindow; private Func GetTime; - private Dictionary NoteGameObjects = new(); + private Dictionary NoteSprites = new(); private bool AfterFirstUpdate = false; private float TargetY; @@ -27,7 +27,7 @@ public void Init(RhythmGameSong song, Func getTime) { TargetY = transform.InverseTransformPoint(LeftTarget.position).y; - float noteSize = ModelNote.GetComponent().rect.height; + float noteSize = NoteSpriteTemplate.ArrowHeight; float bottomOfScreen = GetComponent().rect.yMin; float topOfScreen = GetComponent().rect.yMax; SpawnY = bottomOfScreen - noteSize / 2f; @@ -47,47 +47,67 @@ void Update() { return; } - VisibleNotesWindow.Update(note => { - float y = YPositionForNote(note); - return y >= SpawnY && y <= DespawnY; - }); + VisibleNotesWindow.Update(note => + YPositionForNote(note) >= SpawnY && YPositionForNote(note, end: true) <= DespawnY + ); foreach (RhythmGameNote note in VisibleNotes) { - if (NoteGameObjects.ContainsKey(note)) { + if (NoteSprites.ContainsKey(note)) { + RhythmGameNoteSprite noteSprite = NoteSprites[note]; + + // Update note position float y = YPositionForNote(note); - Transform noteTransform = NoteGameObjects[note].transform; + Transform noteTransform = noteSprite.transform; noteTransform.localPosition = new Vector2(noteTransform.localPosition.x, y); + + if (noteSprite.IsHolding) { + noteSprite.UpdateHolding(CurrentTime); + } } } } public void HitNote(RhythmGameNote note) { - DespawnNote(note); - VisibleNotesWindow.RemoveEarly(note); + if (note.IsInstant) { + DespawnNote(note); + VisibleNotesWindow.RemoveEarly(note); + } else if (NoteSprites.ContainsKey(note)) { + NoteSprites[note].StartHolding(); + } } - private float YPositionForNote(RhythmGameNote note) { - float distanceToTarget = note.RelativeTime(CurrentTime) * NoteSpeed; + public void ReleaseNote(RhythmGameNote note, bool closeEnough) { + if (NoteSprites.ContainsKey(note)) { + NoteSprites[note].StopHolding(CurrentTime, closeEnough: closeEnough); + } + } + + private float YPositionForNote(RhythmGameNote note, bool end = false) { + float distanceToTarget = note.RelativeTime(CurrentTime, end: end) * NoteSpeed; return TargetY - distanceToTarget; } private void SpawnNote(RhythmGameNote note) { - GameObject noteGameObject = Instantiate(ModelNote, transform); - noteGameObject.SetActive(true); - NoteGameObjects.Add(note, noteGameObject); - Transform target = TargetForNoteType(note.Type); float spawnX = transform.InverseTransformPoint(target.position).x; - noteGameObject.transform.localPosition = new Vector2(spawnX, SpawnY); - noteGameObject.transform.localRotation = target.localRotation; - noteGameObject.GetComponent().color = target.GetComponent().color; + GameObject noteSpriteGameObject = Instantiate(NoteSpriteTemplate.gameObject, transform); + RhythmGameNoteSprite noteSprite = noteSpriteGameObject.GetComponent(); + noteSprite.transform.localPosition = new Vector2(spawnX, SpawnY); + NoteSprites.Add(note, noteSprite); + + noteSprite.Initialize( + note: note, + color: target.GetComponent().color, + rotation: target.localRotation, + trailLength: note.Duration * NoteSpeed + ); } private void DespawnNote(RhythmGameNote note) { - if (NoteGameObjects.ContainsKey(note)) { - Destroy(NoteGameObjects[note]); - NoteGameObjects.Remove(note); + if (NoteSprites.ContainsKey(note)) { + Destroy(NoteSprites[note].gameObject); + NoteSprites.Remove(note); } } From e003ed9716febd9ab61b290948679c9610691d9d Mon Sep 17 00:00:00 2001 From: Joe Anderson Date: Sat, 20 Dec 2025 16:14:25 +0000 Subject: [PATCH 09/12] Grade and score note accuracy --- Assets/Unity/Scenes/Test Rhythm Game.unity | 521 ++++++++++++++++++++- Assets/src/RhythmGame.cs | 108 ++++- 2 files changed, 599 insertions(+), 30 deletions(-) diff --git a/Assets/Unity/Scenes/Test Rhythm Game.unity b/Assets/Unity/Scenes/Test Rhythm Game.unity index 7be8855c..747ad465 100644 --- a/Assets/Unity/Scenes/Test Rhythm Game.unity +++ b/Assets/Unity/Scenes/Test Rhythm Game.unity @@ -123,6 +123,86 @@ NavMeshSettings: debug: m_Flags: 0 m_NavMeshData: {fileID: 0} +--- !u!1 &50921000 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 50921001} + - component: {fileID: 50921003} + - component: {fileID: 50921002} + m_Layer: 5 + m_Name: Score and Note Grade + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &50921001 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 50921000} + 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: 2147322485} + - {fileID: 816043963} + - {fileID: 544028420} + m_Father: {fileID: 488498665} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 1} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: -192, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0, y: 1} +--- !u!114 &50921002 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 50921000} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 3245ec927659c4140ac4f8d17403cc18, type: 3} + m_Name: + m_EditorClassIdentifier: + m_HorizontalFit: 2 + m_VerticalFit: 2 +--- !u!114 &50921003 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 50921000} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 59f8146938fff824cb5fd77236b75775, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Padding: + m_Left: 10 + m_Right: 10 + m_Top: 10 + m_Bottom: 10 + m_ChildAlignment: 0 + m_Spacing: 5 + m_ChildForceExpandWidth: 1 + m_ChildForceExpandHeight: 1 + m_ChildControlWidth: 1 + m_ChildControlHeight: 1 + m_ChildScaleWidth: 0 + m_ChildScaleHeight: 0 + m_ReverseArrangement: 0 --- !u!1 &67349635 GameObject: m_ObjectHideFlags: 0 @@ -583,6 +663,7 @@ RectTransform: m_Children: - {fileID: 1489695913} - {fileID: 67349636} + - {fileID: 50921001} m_Father: {fileID: 740930168} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0} @@ -656,6 +737,141 @@ MonoBehaviour: m_EditorClassIdentifier: m_AspectMode: 3 m_AspectRatio: 1.7777778 +--- !u!1 &544028417 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 544028420} + - component: {fileID: 544028419} + - component: {fileID: 544028418} + m_Layer: 5 + m_Name: Note Grade + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &544028418 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 544028417} + 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: + m_isRightToLeft: 0 + m_fontAsset: {fileID: 11400000, guid: 4cd1de6e24c6e498f8e1d80f30605280, type: 2} + m_sharedMaterial: {fileID: 21776102841513868, guid: 4cd1de6e24c6e498f8e1d80f30605280, + 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: 72 + m_fontSizeBase: 72 + m_fontWeight: 400 + m_enableAutoSizing: 0 + m_fontSizeMin: 18 + m_fontSizeMax: 72 + m_fontStyle: 0 + m_HorizontalAlignment: 1 + m_VerticalAlignment: 256 + m_textAlignment: 65535 + m_characterSpacing: 0 + m_wordSpacing: 0 + m_lineSpacing: 0 + m_lineSpacingMax: 0 + m_paragraphSpacing: 0 + m_charWidthMaxAdj: 0 + m_enableWordWrapping: 1 + m_wordWrappingRatios: 0.4 + m_overflowMode: 0 + m_linkedTextComponent: {fileID: 0} + parentLinkedComponent: {fileID: 0} + m_enableKerning: 1 + m_enableExtraPadding: 0 + checkPaddingRequired: 0 + m_isRichText: 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 &544028419 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 544028417} + m_CullTransparentMesh: 1 +--- !u!224 &544028420 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 544028417} + 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: 50921001} + 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: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} --- !u!1 &596422148 GameObject: m_ObjectHideFlags: 0 @@ -921,6 +1137,141 @@ CanvasGroup: m_Interactable: 1 m_BlocksRaycasts: 1 m_IgnoreParentGroups: 0 +--- !u!1 &816043962 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 816043963} + - component: {fileID: 816043965} + - component: {fileID: 816043964} + m_Layer: 5 + m_Name: Score Value + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &816043963 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 816043962} + 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: 50921001} + 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: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &816043964 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 816043962} + 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: 000000 + m_isRightToLeft: 0 + m_fontAsset: {fileID: 11400000, guid: 4cd1de6e24c6e498f8e1d80f30605280, type: 2} + m_sharedMaterial: {fileID: 21776102841513868, guid: 4cd1de6e24c6e498f8e1d80f30605280, + 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: 72 + m_fontSizeBase: 72 + m_fontWeight: 400 + m_enableAutoSizing: 0 + m_fontSizeMin: 18 + m_fontSizeMax: 72 + m_fontStyle: 0 + m_HorizontalAlignment: 1 + m_VerticalAlignment: 256 + m_textAlignment: 65535 + m_characterSpacing: 0 + m_wordSpacing: 0 + m_lineSpacing: 0 + m_lineSpacingMax: 0 + m_paragraphSpacing: 0 + m_charWidthMaxAdj: 0 + m_enableWordWrapping: 1 + m_wordWrappingRatios: 0.4 + m_overflowMode: 0 + m_linkedTextComponent: {fileID: 0} + parentLinkedComponent: {fileID: 0} + m_enableKerning: 1 + m_enableExtraPadding: 0 + checkPaddingRequired: 0 + m_isRichText: 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 &816043965 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 816043962} + m_CullTransparentMesh: 1 --- !u!1001 &898746627 PrefabInstance: m_ObjectHideFlags: 0 @@ -1076,6 +1427,8 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: NotesView: {fileID: 1489695914} + ScoreText: {fileID: 816043964} + NoteGradeText: {fileID: 544028418} Song: BeatsPerMinute: 120 Notes: @@ -1093,16 +1446,16 @@ MonoBehaviour: Duration: 0 - Type: 0 Time: 4 - Duration: 0.5 + Duration: 0.25 - Type: 3 Time: 4.5 - Duration: 0.5 + Duration: 0.25 - Type: 0 Time: 5 - Duration: 0.5 + Duration: 0.25 - Type: 3 Time: 5.5 - Duration: 0.5 + Duration: 0.25 - Type: 2 Time: 6 Duration: 0 @@ -1117,16 +1470,16 @@ MonoBehaviour: Duration: 0 - Type: 0 Time: 8 - Duration: 0.5 + Duration: 0.25 - Type: 3 Time: 8.5 - Duration: 0.5 + Duration: 0.25 - Type: 0 Time: 9 - Duration: 0.5 + Duration: 0.25 - Type: 3 Time: 9.5 - Duration: 0.5 + Duration: 0.25 - Type: 2 Time: 10 Duration: 0 @@ -1141,16 +1494,16 @@ MonoBehaviour: Duration: 0 - Type: 0 Time: 12 - Duration: 0.5 + Duration: 0.25 - Type: 3 Time: 12.5 - Duration: 0.5 + Duration: 0.25 - Type: 0 Time: 13 - Duration: 0.5 + Duration: 0.25 - Type: 3 Time: 13.5 - Duration: 0.5 + Duration: 0.25 - Type: 2 Time: 14 Duration: 0 @@ -1165,17 +1518,16 @@ MonoBehaviour: Duration: 0 - Type: 0 Time: 16 - Duration: 0.5 + Duration: 0.25 - Type: 3 Time: 16.5 - Duration: 0.5 + Duration: 0.25 - Type: 0 Time: 17 - Duration: 0.5 + Duration: 0.25 - Type: 3 Time: 17.5 - Duration: 0.5 - MissThreshold: 0.16 + Duration: 0.25 --- !u!4 &1090514947 Transform: m_ObjectHideFlags: 0 @@ -1459,6 +1811,141 @@ CanvasGroup: m_Interactable: 1 m_BlocksRaycasts: 1 m_IgnoreParentGroups: 0 +--- !u!1 &2147322484 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2147322485} + - component: {fileID: 2147322487} + - component: {fileID: 2147322486} + m_Layer: 5 + m_Name: Score Label + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2147322485 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2147322484} + 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: 50921001} + 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: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &2147322486 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2147322484} + 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: Score + m_isRightToLeft: 0 + m_fontAsset: {fileID: 11400000, guid: 4cd1de6e24c6e498f8e1d80f30605280, type: 2} + m_sharedMaterial: {fileID: 21776102841513868, guid: 4cd1de6e24c6e498f8e1d80f30605280, + 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: 52 + m_fontSizeBase: 52 + m_fontWeight: 400 + m_enableAutoSizing: 0 + m_fontSizeMin: 18 + m_fontSizeMax: 72 + m_fontStyle: 0 + m_HorizontalAlignment: 1 + m_VerticalAlignment: 256 + m_textAlignment: 65535 + m_characterSpacing: 0 + m_wordSpacing: 0 + m_lineSpacing: 0 + m_lineSpacingMax: 0 + m_paragraphSpacing: 0 + m_charWidthMaxAdj: 0 + m_enableWordWrapping: 1 + m_wordWrappingRatios: 0.4 + m_overflowMode: 0 + m_linkedTextComponent: {fileID: 0} + parentLinkedComponent: {fileID: 0} + m_enableKerning: 1 + m_enableExtraPadding: 0 + checkPaddingRequired: 0 + m_isRichText: 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 &2147322487 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2147322484} + m_CullTransparentMesh: 1 --- !u!1660057539 &9223372036854775807 SceneRoots: m_ObjectHideFlags: 0 diff --git a/Assets/src/RhythmGame.cs b/Assets/src/RhythmGame.cs index 92c64ab7..07562b02 100644 --- a/Assets/src/RhythmGame.cs +++ b/Assets/src/RhythmGame.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using System.Linq; +using TMPro; using UnityEngine; public enum RhythmGameNoteType { @@ -29,16 +31,53 @@ public class RhythmGameSong { public class RhythmGame : MonoBehaviour { public RhythmGameNotesView NotesView; + public TMP_Text ScoreText; + public TMP_Text NoteGradeText; public RhythmGameSong Song; - public const float MISS_THRESHOLD = 0.16f; + /** + * Constants and scoring algorithm modified from the source code of Friday + * Night Funkin' and used under the following license: + * https://github.com/FunkinCrew/Funkin/blob/24250549406207988b2718f5a05bf31e5af7629e/LICENSE.md + */ + private const float MISS_THRESHOLD = 0.16f; + private const float DROP_THRESHOLD = 0.08f; + private const int MAX_NOTE_SCORE = 500; + private const int MIN_NOTE_SCORE = 9; + private const int MISS_NOTE_SCORE = -100; + private const float SCORING_SLOPE = 80f; + private const float SCORING_OFFSET = 0.05499f; + private const float HOLD_SCORE_PER_SECOND = 250f; + private const float DROP_SCORE_PER_SECOND = -125f; + + private readonly (float, string)[] NOTE_GRADES = { + (0.012f, "Poppin'"), + (0.045f, "Perfect"), + (0.09f, "Pretty good"), + (0.135f, "Poor"), + (MISS_THRESHOLD, "Pathetic"), + }; private LinearWindow PressableNotesWindow; private Dictionary HeldNotes = new(); + private float Score = 0f; + private int MaxPossibleScore; void Start() { PressableNotesWindow = new(Song.Notes, onExitWindow: MissedNote); NotesView.Init(Song, getTime: () => CurrentTime); + + MaxPossibleScore = Song + .Notes.Select(note => { + int perfectHitScore = ScoreNoteHit(0f); + int holdScore = (int)(note.Duration * HOLD_SCORE_PER_SECOND); + return perfectHitScore + holdScore; + }) + .Sum(); + + Debug.Log("Max possible score: " + MaxPossibleScore.ToString()); + + UpdateScore(0f); } void Update() { @@ -88,7 +127,7 @@ private void HandleButtonDown(RhythmGameNoteType noteType) { RhythmGameNote note = PressableNotes.Find(note => note.Type == noteType); if (note == null) { - MissedNote(noteType); + NoteNotFound(noteType); } else { HitNote(note); } @@ -106,31 +145,74 @@ private void HitNote(RhythmGameNote note) { if (note.IsHold) { HeldNotes.Add(note.Type, note); - - if (note.RelativeTime(CurrentTime) < -MISS_THRESHOLD) { - Debug.Log("Miss (hold note started too late"); - } } + + float relativeTime = Mathf.Abs(note.RelativeTime(CurrentTime)); + UpdateScore(ScoreNoteHit(relativeTime)); + ShowNoteGrade(GradeNoteHit(relativeTime)); } private void ReleaseNote(RhythmGameNote note) { float remainingDuration = note.RelativeTime(CurrentTime, end: true); - bool closeEnough = remainingDuration <= MISS_THRESHOLD; + + bool closeEnough = remainingDuration <= DROP_THRESHOLD; + if (closeEnough) + remainingDuration = 0f; + + float heldDuration = note.Duration - remainingDuration; HeldNotes.Remove(note.Type); NotesView.ReleaseNote(note, closeEnough: closeEnough); - if (!closeEnough) { - Debug.Log("Released note too early"); - } + float holdScore = heldDuration * HOLD_SCORE_PER_SECOND; + float dropScore = remainingDuration * DROP_SCORE_PER_SECOND; + int scoreChange = (int)(holdScore + dropScore); + UpdateScore(scoreChange); } private void MissedNote(RhythmGameNote note) { - Debug.Log("Miss (note not pressed)"); + ShowNoteGrade("Miss"); + UpdateScore(MISS_NOTE_SCORE); + } + + private void NoteNotFound(RhythmGameNoteType noteType) { + UpdateScore(MISS_NOTE_SCORE); + } + + private int ScoreNoteHit(float relativeTime) { + /** + * Although most misses are handled by MissedNote, this case can arise if a + * hold note is pressed late. + */ + if (relativeTime > MISS_THRESHOLD) + return MISS_NOTE_SCORE; + + /** + * Curve that outputs a value close to 1 at relativeTime = 0 and approaches + * 0 at approximately relativeTime = 0.15. + */ + float factor = 1f - 1f / (1f + Mathf.Exp(-SCORING_SLOPE * (relativeTime - SCORING_OFFSET))); + + return (int)(MAX_NOTE_SCORE * factor + MIN_NOTE_SCORE); + } + + private string GradeNoteHit(float relativeTime) { + foreach (var (threshold, label) in NOTE_GRADES) { + if (relativeTime <= threshold) + return label; + } + + return "Miss"; + } + + private void UpdateScore(float scoreChange) { + Score = Mathf.Max(0f, Score + scoreChange); + int maxScoreDigits = Mathf.CeilToInt(Mathf.Log10((float)MaxPossibleScore)); + ScoreText.text = Score.ToString().PadLeft(maxScoreDigits, '0'); } - private void MissedNote(RhythmGameNoteType noteType) { - Debug.Log("Miss (no note found)"); + private void ShowNoteGrade(string grade) { + NoteGradeText.text = grade; } private float CurrentTime => Time.time; From 16c3001209377cb2c182b0b9382287bbb180b782 Mon Sep 17 00:00:00 2001 From: Joe Anderson Date: Sun, 21 Dec 2025 07:12:51 +0000 Subject: [PATCH 10/12] Increaes frame rate to 300 --- Assets/Unity/Scenes/Test Rhythm Game.unity | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Assets/Unity/Scenes/Test Rhythm Game.unity b/Assets/Unity/Scenes/Test Rhythm Game.unity index 747ad465..6b80b469 100644 --- a/Assets/Unity/Scenes/Test Rhythm Game.unity +++ b/Assets/Unity/Scenes/Test Rhythm Game.unity @@ -1718,6 +1718,11 @@ PrefabInstance: propertyPath: m_LocalEulerAnglesHint.z value: 0 objectReference: {fileID: 0} + - target: {fileID: 5835818988283754399, guid: 310bd337419114b4595e5056c7463838, + type: 3} + propertyPath: FrameRate + value: 300 + objectReference: {fileID: 0} m_RemovedComponents: [] m_RemovedGameObjects: [] m_AddedGameObjects: [] From eb2b3b4dba248ba9d57cc3c3e79ac529037f0f12 Mon Sep 17 00:00:00 2001 From: Joe Anderson Date: Sun, 21 Dec 2025 09:18:50 +0000 Subject: [PATCH 11/12] Parse notes from MIDI --- .gitignore | 2 +- Assets/StreamingAssets.meta | 8 + Assets/StreamingAssets/Rhythm Game Data.meta | 8 + .../StreamingAssets/Rhythm Game Data/Demo.mid | Bin 0 -> 1040 bytes .../Rhythm Game Data/Demo.mid.meta | 7 + Assets/Unity/Scenes/Test Rhythm Game.unity | 101 +---- Assets/src/GraphicsSettingsMenuEvents.cs | 2 +- Assets/src/LinearWindow.cs | 2 +- Assets/src/MidiParser.cs | 346 ++++++++++++++++++ Assets/src/MidiParser.cs.meta | 11 + Assets/src/OptionsMenuEvents.cs | 2 +- Assets/src/RhythmGame.cs | 56 ++- 12 files changed, 436 insertions(+), 109 deletions(-) create mode 100644 Assets/StreamingAssets.meta create mode 100644 Assets/StreamingAssets/Rhythm Game Data.meta create mode 100644 Assets/StreamingAssets/Rhythm Game Data/Demo.mid create mode 100644 Assets/StreamingAssets/Rhythm Game Data/Demo.mid.meta create mode 100644 Assets/src/MidiParser.cs create mode 100644 Assets/src/MidiParser.cs.meta diff --git a/.gitignore b/.gitignore index 0e9a57f2..ef9e2c29 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,6 @@ vendor.zip /Assets/Plugins/FMOD/* /Assets/Plugins/FMOD.meta -/Assets/StreamingAssets* +/Assets/StreamingAssets/master* /FMOD/.* fmod_editor.log diff --git a/Assets/StreamingAssets.meta b/Assets/StreamingAssets.meta new file mode 100644 index 00000000..da1600a3 --- /dev/null +++ b/Assets/StreamingAssets.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 06140fb01b7d94fa3a53f257522e3888 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/StreamingAssets/Rhythm Game Data.meta b/Assets/StreamingAssets/Rhythm Game Data.meta new file mode 100644 index 00000000..f8e9c90d --- /dev/null +++ b/Assets/StreamingAssets/Rhythm Game Data.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 04cc33f171a944bf4b2a56eddba1f456 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/StreamingAssets/Rhythm Game Data/Demo.mid b/Assets/StreamingAssets/Rhythm Game Data/Demo.mid new file mode 100644 index 0000000000000000000000000000000000000000..80a2c8ecd5b5fa5783365e2a871673bccc7db1f4 GIT binary patch literal 1040 zcmcIiOG*Pl5Ph8qNe~aPPaaWRwcD+e9*XN0 z;fi6<>waOE0z{h<@fh(6(-l5|T{p(j8E$}ELiQQn@Xpodf`bVP0*tGCT)w1wx|B$z zN~A_41c+1;Y1s3533DW_UQRjk$$@a!@h?4I`;p4#q(e|3QPm*n)978#_5^Q3j?K{V zs&q$)$h+~D&B>dj7gh1awxK|_XyQ8+j$uQ#aKDbomDX`k9lweui}_g4#06C?$KWOF zWZi5`&hB3Gn|kP+_faXHEx1Z(R`w|N4@9c<%ug>B^b%O#*P(6RNlopjDA-Fg+o)$6 pBVJWy<>r(*c{8VKfOg0Eq^{oYt!q+~OLHUsnkwD@K{qQx`~ZAs2+jZi literal 0 HcmV?d00001 diff --git a/Assets/StreamingAssets/Rhythm Game Data/Demo.mid.meta b/Assets/StreamingAssets/Rhythm Game Data/Demo.mid.meta new file mode 100644 index 00000000..e74ec222 --- /dev/null +++ b/Assets/StreamingAssets/Rhythm Game Data/Demo.mid.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 44132a14146d6421c9df3d06abf77630 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Unity/Scenes/Test Rhythm Game.unity b/Assets/Unity/Scenes/Test Rhythm Game.unity index 6b80b469..e16283be 100644 --- a/Assets/Unity/Scenes/Test Rhythm Game.unity +++ b/Assets/Unity/Scenes/Test Rhythm Game.unity @@ -1429,105 +1429,6 @@ MonoBehaviour: NotesView: {fileID: 1489695914} ScoreText: {fileID: 816043964} NoteGradeText: {fileID: 544028418} - Song: - BeatsPerMinute: 120 - Notes: - - Type: 2 - Time: 2 - Duration: 0 - - Type: 2 - Time: 2.5 - Duration: 0 - - Type: 1 - Time: 3 - Duration: 0 - - Type: 1 - Time: 3.5 - Duration: 0 - - Type: 0 - Time: 4 - Duration: 0.25 - - Type: 3 - Time: 4.5 - Duration: 0.25 - - Type: 0 - Time: 5 - Duration: 0.25 - - Type: 3 - Time: 5.5 - Duration: 0.25 - - Type: 2 - Time: 6 - Duration: 0 - - Type: 2 - Time: 6.5 - Duration: 0 - - Type: 1 - Time: 7 - Duration: 0 - - Type: 1 - Time: 7.5 - Duration: 0 - - Type: 0 - Time: 8 - Duration: 0.25 - - Type: 3 - Time: 8.5 - Duration: 0.25 - - Type: 0 - Time: 9 - Duration: 0.25 - - Type: 3 - Time: 9.5 - Duration: 0.25 - - Type: 2 - Time: 10 - Duration: 0 - - Type: 2 - Time: 10.5 - Duration: 0 - - Type: 1 - Time: 11 - Duration: 0 - - Type: 1 - Time: 11.5 - Duration: 0 - - Type: 0 - Time: 12 - Duration: 0.25 - - Type: 3 - Time: 12.5 - Duration: 0.25 - - Type: 0 - Time: 13 - Duration: 0.25 - - Type: 3 - Time: 13.5 - Duration: 0.25 - - Type: 2 - Time: 14 - Duration: 0 - - Type: 2 - Time: 14.5 - Duration: 0 - - Type: 1 - Time: 15 - Duration: 0 - - Type: 1 - Time: 15.5 - Duration: 0 - - Type: 0 - Time: 16 - Duration: 0.25 - - Type: 3 - Time: 16.5 - Duration: 0.25 - - Type: 0 - Time: 17 - Duration: 0.25 - - Type: 3 - Time: 17.5 - Duration: 0.25 --- !u!4 &1090514947 Transform: m_ObjectHideFlags: 0 @@ -1597,7 +1498,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8112514d52eaa43adb5d0c47e84a79ed, type: 3} m_Name: m_EditorClassIdentifier: - PixelsBetweenEachBeat: 270 + PixelsBetweenEachBeat: 405 LeftTarget: {fileID: 787737352} DownTarget: {fileID: 250930357} UpTarget: {fileID: 2014153219} diff --git a/Assets/src/GraphicsSettingsMenuEvents.cs b/Assets/src/GraphicsSettingsMenuEvents.cs index 7e672234..376dfc67 100644 --- a/Assets/src/GraphicsSettingsMenuEvents.cs +++ b/Assets/src/GraphicsSettingsMenuEvents.cs @@ -24,7 +24,7 @@ protected override void LocalStart() { ); if (ResolutionStepper.Value == -1) - ResolutionStepper.Value = Screen.resolutions.Count() - 1; + ResolutionStepper.Value = Screen.resolutions.Length - 1; Resolution = Screen.resolutions[ResolutionStepper.Value]; diff --git a/Assets/src/LinearWindow.cs b/Assets/src/LinearWindow.cs index a65d9c1f..50829f6e 100644 --- a/Assets/src/LinearWindow.cs +++ b/Assets/src/LinearWindow.cs @@ -36,7 +36,7 @@ public void Update(Func predicate) { OnExitWindow?.Invoke(item); }); - while (LastEnteredWindow < AllItems.Count() - 1) { + while (LastEnteredWindow < AllItems.Length - 1) { T item = AllItems[LastEnteredWindow + 1]; if (predicate(item)) { diff --git a/Assets/src/MidiParser.cs b/Assets/src/MidiParser.cs new file mode 100644 index 00000000..45c61a0e --- /dev/null +++ b/Assets/src/MidiParser.cs @@ -0,0 +1,346 @@ +/** + * https://github.com/davidluzgouveia/midi-parser + * Retrieved 2025-12-21 + * + * MIT License + * + * Copyright (c) 2018 David Gouveia + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace MidiParser { + using System; + using System.Collections.Generic; + using System.IO; + using System.Text; + + public class MidiFile { + public readonly int Format; + + public readonly int TicksPerQuarterNote; + + public readonly MidiTrack[] Tracks; + + public readonly int TracksCount; + + public MidiFile(Stream stream) : this(Reader.ReadAllBytesFromStream(stream)) { } + + public MidiFile(string path) : this(File.ReadAllBytes(path)) { } + + public MidiFile(byte[] data) { + var position = 0; + + if (Reader.ReadString(data, ref position, 4) != "MThd") { + throw new FormatException("Invalid file header (expected MThd)"); + } + + if (Reader.Read32(data, ref position) != 6) { + throw new FormatException("Invalid header length (expected 6)"); + } + + this.Format = Reader.Read16(data, ref position); + this.TracksCount = Reader.Read16(data, ref position); + this.TicksPerQuarterNote = Reader.Read16(data, ref position); + + if ((this.TicksPerQuarterNote & 0x8000) != 0) { + throw new FormatException("Invalid timing mode (SMPTE timecode not supported)"); + } + + this.Tracks = new MidiTrack[this.TracksCount]; + + for (var i = 0; i < this.TracksCount; i++) { + this.Tracks[i] = ParseTrack(i, data, ref position); + } + } + + private static bool ParseMetaEvent( + byte[] data, + ref int position, + byte metaEventType, + ref byte data1, + ref byte data2 + ) { + switch (metaEventType) { + case (byte)MetaEventType.Tempo: + var mspqn = (data[position + 1] << 16) | (data[position + 2] << 8) | data[position + 3]; + data1 = (byte)(60000000.0 / mspqn); + position += 4; + return true; + + case (byte)MetaEventType.TimeSignature: + data1 = data[position + 1]; + data2 = (byte)Math.Pow(2.0, data[position + 2]); + position += 5; + return true; + + case (byte)MetaEventType.KeySignature: + data1 = data[position + 1]; + data2 = data[position + 2]; + position += 3; + return true; + + // Ignore Other Meta Events + default: + var length = Reader.ReadVarInt(data, ref position); + position += length; + return false; + } + } + + private static MidiTrack ParseTrack(int index, byte[] data, ref int position) { + if (Reader.ReadString(data, ref position, 4) != "MTrk") { + throw new FormatException("Invalid track header (expected MTrk)"); + } + + var trackLength = Reader.Read32(data, ref position); + var trackEnd = position + trackLength; + + var track = new MidiTrack { Index = index }; + var time = 0; + var status = (byte)0; + + while (position < trackEnd) { + time += Reader.ReadVarInt(data, ref position); + + var peekByte = data[position]; + + // If the most significant bit is set then this is a status byte + if ((peekByte & 0x80) != 0) { + status = peekByte; + ++position; + } + + // If the most significant nibble is not an 0xF this is a channel event + if ((status & 0xF0) != 0xF0) { + // Separate event type from channel into two + var eventType = (byte)(status & 0xF0); + var channel = (byte)((status & 0x0F) + 1); + + var data1 = data[position++]; + + // If the event type doesn't start with 0b110 it has two bytes of data (i.e. except 0xC0 and 0xD0) + var data2 = (eventType & 0xE0) != 0xC0 ? data[position++] : (byte)0; + + // Convert NoteOn events with 0 velocity into NoteOff events + if (eventType == (byte)MidiEventType.NoteOn && data2 == 0) { + eventType = (byte)MidiEventType.NoteOff; + } + + track.MidiEvents.Add( + new MidiEvent { + Time = time, + Type = eventType, + Arg1 = channel, + Arg2 = data1, + Arg3 = data2, + } + ); + } else { + if (status == 0xFF) { + // Meta Event + var metaEventType = Reader.Read8(data, ref position); + + // There is a group of meta event types reserved for text events which we store separately + if (metaEventType >= 0x01 && metaEventType <= 0x0F) { + var textLength = Reader.ReadVarInt(data, ref position); + var textValue = Reader.ReadString(data, ref position, textLength); + var textEvent = new TextEvent { + Time = time, + Type = metaEventType, + Value = textValue, + }; + track.TextEvents.Add(textEvent); + } else { + var data1 = (byte)0; + var data2 = (byte)0; + + // We only handle the few meta events we care about and skip the rest + if (ParseMetaEvent(data, ref position, metaEventType, ref data1, ref data2)) { + track.MidiEvents.Add( + new MidiEvent { + Time = time, + Type = status, + Arg1 = metaEventType, + Arg2 = data1, + Arg3 = data2, + } + ); + } + } + } else if (status == 0xF0 || status == 0xF7) { + // SysEx event + var length = Reader.ReadVarInt(data, ref position); + position += length; + } else { + ++position; + } + } + } + + return track; + } + + private static class Reader { + public static int Read16(byte[] data, ref int i) { + return (data[i++] << 8) | data[i++]; + } + + public static int Read32(byte[] data, ref int i) { + return (data[i++] << 24) | (data[i++] << 16) | (data[i++] << 8) | data[i++]; + } + + public static byte Read8(byte[] data, ref int i) { + return data[i++]; + } + + public static byte[] ReadAllBytesFromStream(Stream input) { + var buffer = new byte[16 * 1024]; + using (var ms = new MemoryStream()) { + int read; + while ((read = input.Read(buffer, 0, buffer.Length)) > 0) { + ms.Write(buffer, 0, read); + } + + return ms.ToArray(); + } + } + + public static string ReadString(byte[] data, ref int i, int length) { + var result = Encoding.ASCII.GetString(data, i, length); + i += length; + return result; + } + + public static int ReadVarInt(byte[] data, ref int i) { + var result = (int)data[i++]; + + if ((result & 0x80) == 0) { + return result; + } + + result &= 0x7F; + + for (var j = 0; j < 3; j++) { + var value = (int)data[i++]; + + result = (result << 7) | (value & 0x7F); + + if ((value & 0x80) == 0) { + break; + } + } + + return result; + } + } + } + + public class MidiTrack { + public int Index; + + public List MidiEvents = new List(); + + public List TextEvents = new List(); + } + + public struct MidiEvent { + public int Time; + + public byte Type; + + public byte Arg1; + + public byte Arg2; + + public byte Arg3; + + public MidiEventType MidiEventType => (MidiEventType)this.Type; + + public MetaEventType MetaEventType => (MetaEventType)this.Arg1; + + public int Channel => this.Arg1; + + public int Note => this.Arg2; + + public int Velocity => this.Arg3; + + public ControlChangeType ControlChangeType => (ControlChangeType)this.Arg2; + + public int Value => this.Arg3; + } + + public struct TextEvent { + public int Time; + + public byte Type; + + public string Value; + + public TextEventType TextEventType => (TextEventType)this.Type; + } + + public enum MidiEventType : byte { + NoteOff = 0x80, + + NoteOn = 0x90, + + KeyAfterTouch = 0xA0, + + ControlChange = 0xB0, + + ProgramChange = 0xC0, + + ChannelAfterTouch = 0xD0, + + PitchBendChange = 0xE0, + + MetaEvent = 0xFF, + } + + public enum ControlChangeType : byte { + BankSelect = 0x00, + + Modulation = 0x01, + + Volume = 0x07, + + Balance = 0x08, + + Pan = 0x0A, + + Sustain = 0x40, + } + + public enum TextEventType : byte { + Text = 0x01, + + TrackName = 0x03, + + Lyric = 0x05, + } + + public enum MetaEventType : byte { + Tempo = 0x51, + + TimeSignature = 0x58, + + KeySignature = 0x59, + } +} diff --git a/Assets/src/MidiParser.cs.meta b/Assets/src/MidiParser.cs.meta new file mode 100644 index 00000000..0bcb7026 --- /dev/null +++ b/Assets/src/MidiParser.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ab1653643aa6b4451923d854b260e4f1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/src/OptionsMenuEvents.cs b/Assets/src/OptionsMenuEvents.cs index e0d6a724..7cc7963d 100644 --- a/Assets/src/OptionsMenuEvents.cs +++ b/Assets/src/OptionsMenuEvents.cs @@ -23,7 +23,7 @@ protected override void LocalStart() { } protected override void LocalUpdate() { - if (WrappedInput.Player.controllers.Joysticks.Count() >= 1) + if (WrappedInput.Player.controllers.Joysticks.Count >= 1) ControllerIconsGameObject.SetActive(true); } diff --git a/Assets/src/RhythmGame.cs b/Assets/src/RhythmGame.cs index 07562b02..e0fe383b 100644 --- a/Assets/src/RhythmGame.cs +++ b/Assets/src/RhythmGame.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using MidiParser; using TMPro; using UnityEngine; @@ -25,15 +26,56 @@ public float RelativeTime(float currentTime, bool end = false) => [System.Serializable] public class RhythmGameSong { - public int BeatsPerMinute; + public float BeatsPerMinute; public List Notes; + + private static readonly Dictionary PITCH_TO_NOTE_TYPE = new() + { + { 65, RhythmGameNoteType.Left }, + { 69, RhythmGameNoteType.Down }, + { 72, RhythmGameNoteType.Up }, + { 76, RhythmGameNoteType.Right }, + }; + + public static RhythmGameSong ParseMidi(string path) { + MidiFile midi = new MidiFile(path); + + if (midi.Tracks.Length != 1) + throw new System.Exception("MIDI file should have exactly one track"); + + MidiTrack track = midi.Tracks[0]; + + List notes = new(); + float bpm = 120f; + + foreach (MidiEvent midiEvent in track.MidiEvents) { + switch (midiEvent.MidiEventType) { + case MidiEventType.MetaEvent: + if (midiEvent.MetaEventType == MetaEventType.Tempo) { + // TODO: Track a list of tempo change events on the song object + bpm = (int)midiEvent.Arg2; + } + break; + + case MidiEventType.NoteOn: + RhythmGameNoteType type = PITCH_TO_NOTE_TYPE[midiEvent.Note]; + float beat = (float)midiEvent.Time / midi.TicksPerQuarterNote; + float time = beat * 60f / bpm; + notes.Add(new() { Type = type, Time = time }); + break; + + // TODO: Parse hold notes + } + } + + return new RhythmGameSong { BeatsPerMinute = bpm, Notes = notes }; + } } public class RhythmGame : MonoBehaviour { public RhythmGameNotesView NotesView; public TMP_Text ScoreText; public TMP_Text NoteGradeText; - public RhythmGameSong Song; /** * Constants and scoring algorithm modified from the source code of Friday @@ -64,10 +106,14 @@ private readonly (float, string)[] NOTE_GRADES = { private int MaxPossibleScore; void Start() { - PressableNotesWindow = new(Song.Notes, onExitWindow: MissedNote); - NotesView.Init(Song, getTime: () => CurrentTime); + RhythmGameSong song = RhythmGameSong.ParseMidi( + System.IO.Path.Combine(Application.streamingAssetsPath, "Rhythm Game Data", "Demo.mid") + ); + + PressableNotesWindow = new(song.Notes, onExitWindow: MissedNote); + NotesView.Init(song, getTime: () => CurrentTime); - MaxPossibleScore = Song + MaxPossibleScore = song .Notes.Select(note => { int perfectHitScore = ScoreNoteHit(0f); int holdScore = (int)(note.Duration * HOLD_SCORE_PER_SECOND); From c73cf7c54fa9237d66e0e08ed657d1a2584ed78c Mon Sep 17 00:00:00 2001 From: Joe Anderson Date: Sun, 21 Dec 2025 18:00:51 +0000 Subject: [PATCH 12/12] Parse hold notes from MIDI --- .../StreamingAssets/Rhythm Game Data/Demo.mid | Bin 1040 -> 1065 bytes Assets/src/MidiParser.cs | 2 +- Assets/src/RhythmGame.cs | 72 +------------ Assets/src/RhythmGameNote.cs | 18 ++++ Assets/src/RhythmGameNote.cs.meta | 11 ++ Assets/src/RhythmGameSong.cs | 6 ++ Assets/src/RhythmGameSong.cs.meta | 11 ++ Assets/src/RhythmGameSongParser.cs | 100 ++++++++++++++++++ Assets/src/RhythmGameSongParser.cs.meta | 11 ++ 9 files changed, 159 insertions(+), 72 deletions(-) create mode 100644 Assets/src/RhythmGameNote.cs create mode 100644 Assets/src/RhythmGameNote.cs.meta create mode 100644 Assets/src/RhythmGameSong.cs create mode 100644 Assets/src/RhythmGameSong.cs.meta create mode 100644 Assets/src/RhythmGameSongParser.cs create mode 100644 Assets/src/RhythmGameSongParser.cs.meta diff --git a/Assets/StreamingAssets/Rhythm Game Data/Demo.mid b/Assets/StreamingAssets/Rhythm Game Data/Demo.mid index 80a2c8ecd5b5fa5783365e2a871673bccc7db1f4..1cab99e0d75bfc116de570c9edd296c2b60a7687 100644 GIT binary patch literal 1065 zcmcIiK}y3w6n)c{wun17?!*)H2pWcvGIZz^3MwK}NKtVSQWqhslEzZRg?N-6!JBvh z?~wjsCgOxd3gYT9GylK;=Djbc_aT5T7#RDM)5RmuIYQCydW%JrJfXPkbXv!K6jv>( zD<&I!d}8 zsbmB;RLKy`SBVeC)$>J7BUhUjjY5N$vSRA03PRR+hOEH=x~U`?gAN?)!|LWUWme{PF$AT=DRP^&wE)q|(%t8HFp zL>kcNgEBoE$?u(dU+aQ5CRz$JB#Q%DaqU+VWf|T#PPaaWRwcD+e9*XN0 z;fi6<>waOE0z{h<@fh(6(-l5|T{p(j8E$}ELiQQn@Xpodf`bVP0*tGCT)w1wx|B$z zN~A_41c+1;Y1s3533DW_UQRjk$$@a!@h?4I`;p4#q(e|3QPm*n)978#_5^Q3j?K{V zs&q$)$h+~D&B>dj7gh1awxK|_XyQ8+j$uQ#aKDbomDX`k9lweui}_g4#06C?$KWOF zWZi5`&hB3Gn|kP+_faXHEx1Z(R`w|N4@9c<%ug>B^b%O#*P(6RNlopjDA-Fg+o)$6 pBVJWy<>r(*c{8VKfOg0Eq^{oYt!q+~OLHUsnkwD@K{qQx`~ZAs2+jZi diff --git a/Assets/src/MidiParser.cs b/Assets/src/MidiParser.cs index 45c61a0e..2ff3bcd4 100644 --- a/Assets/src/MidiParser.cs +++ b/Assets/src/MidiParser.cs @@ -80,7 +80,7 @@ ref byte data2 switch (metaEventType) { case (byte)MetaEventType.Tempo: var mspqn = (data[position + 1] << 16) | (data[position + 2] << 8) | data[position + 3]; - data1 = (byte)(60000000.0 / mspqn); + data1 = (byte)Math.Round(60000000.0 / mspqn); position += 4; return true; diff --git a/Assets/src/RhythmGame.cs b/Assets/src/RhythmGame.cs index e0fe383b..6c87088a 100644 --- a/Assets/src/RhythmGame.cs +++ b/Assets/src/RhythmGame.cs @@ -1,77 +1,8 @@ using System.Collections.Generic; using System.Linq; -using MidiParser; using TMPro; using UnityEngine; -public enum RhythmGameNoteType { - Left = 0, - Down = 1, - Up = 2, - Right = 3, -}; - -[System.Serializable] -public class RhythmGameNote { - public RhythmGameNoteType Type; - public float Time; - public float Duration = 0f; - - public bool IsHold => Duration > 0f; - public bool IsInstant => !IsHold; - - public float RelativeTime(float currentTime, bool end = false) => - Time - currentTime + (end ? Duration : 0f); -} - -[System.Serializable] -public class RhythmGameSong { - public float BeatsPerMinute; - public List Notes; - - private static readonly Dictionary PITCH_TO_NOTE_TYPE = new() - { - { 65, RhythmGameNoteType.Left }, - { 69, RhythmGameNoteType.Down }, - { 72, RhythmGameNoteType.Up }, - { 76, RhythmGameNoteType.Right }, - }; - - public static RhythmGameSong ParseMidi(string path) { - MidiFile midi = new MidiFile(path); - - if (midi.Tracks.Length != 1) - throw new System.Exception("MIDI file should have exactly one track"); - - MidiTrack track = midi.Tracks[0]; - - List notes = new(); - float bpm = 120f; - - foreach (MidiEvent midiEvent in track.MidiEvents) { - switch (midiEvent.MidiEventType) { - case MidiEventType.MetaEvent: - if (midiEvent.MetaEventType == MetaEventType.Tempo) { - // TODO: Track a list of tempo change events on the song object - bpm = (int)midiEvent.Arg2; - } - break; - - case MidiEventType.NoteOn: - RhythmGameNoteType type = PITCH_TO_NOTE_TYPE[midiEvent.Note]; - float beat = (float)midiEvent.Time / midi.TicksPerQuarterNote; - float time = beat * 60f / bpm; - notes.Add(new() { Type = type, Time = time }); - break; - - // TODO: Parse hold notes - } - } - - return new RhythmGameSong { BeatsPerMinute = bpm, Notes = notes }; - } -} - public class RhythmGame : MonoBehaviour { public RhythmGameNotesView NotesView; public TMP_Text ScoreText; @@ -106,7 +37,7 @@ private readonly (float, string)[] NOTE_GRADES = { private int MaxPossibleScore; void Start() { - RhythmGameSong song = RhythmGameSong.ParseMidi( + RhythmGameSong song = RhythmGameSongParser.ParseMidi( System.IO.Path.Combine(Application.streamingAssetsPath, "Rhythm Game Data", "Demo.mid") ); @@ -151,7 +82,6 @@ void Update() { if (WrappedInput.GetButtonUp("Rhythm Game Left")) { HandleButtonUp(RhythmGameNoteType.Left); } - if (WrappedInput.GetButtonUp("Rhythm Game Down")) { HandleButtonUp(RhythmGameNoteType.Down); } diff --git a/Assets/src/RhythmGameNote.cs b/Assets/src/RhythmGameNote.cs new file mode 100644 index 00000000..4728256f --- /dev/null +++ b/Assets/src/RhythmGameNote.cs @@ -0,0 +1,18 @@ +public enum RhythmGameNoteType { + Left = 0, + Down = 1, + Up = 2, + Right = 3, +}; + +public class RhythmGameNote { + public RhythmGameNoteType Type; + public float Time; + public float Duration = 0f; + + public bool IsHold => Duration > 0f; + public bool IsInstant => !IsHold; + + public float RelativeTime(float currentTime, bool end = false) => + Time - currentTime + (end ? Duration : 0f); +} diff --git a/Assets/src/RhythmGameNote.cs.meta b/Assets/src/RhythmGameNote.cs.meta new file mode 100644 index 00000000..d8fdfd3c --- /dev/null +++ b/Assets/src/RhythmGameNote.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e67268fe7ddb74764a5603a4b36aef8c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/src/RhythmGameSong.cs b/Assets/src/RhythmGameSong.cs new file mode 100644 index 00000000..1f576fd9 --- /dev/null +++ b/Assets/src/RhythmGameSong.cs @@ -0,0 +1,6 @@ +using System.Collections.Generic; + +public class RhythmGameSong { + public float BeatsPerMinute; + public List Notes; +} diff --git a/Assets/src/RhythmGameSong.cs.meta b/Assets/src/RhythmGameSong.cs.meta new file mode 100644 index 00000000..cdfaa89d --- /dev/null +++ b/Assets/src/RhythmGameSong.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a123018e666484d48bd94845db490ef7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/src/RhythmGameSongParser.cs b/Assets/src/RhythmGameSongParser.cs new file mode 100644 index 00000000..6921d5ef --- /dev/null +++ b/Assets/src/RhythmGameSongParser.cs @@ -0,0 +1,100 @@ +using System.Collections.Generic; +using MidiParser; + +public class RhythmGameSongParser { + /** + * Each of the four note types is mapped to F4, A4, C5 and E5 respectively + * (the gaps on a treble stave in C Major). If a note's duration should be + * taken into consideration, it should be shifted up one degree in the C Major + * scale (i.e. on a line instead of a gap). + */ + private readonly Dictionary MIDI_PITCH_DATA = new() + { + { 65, (RhythmGameNoteType.Left, false) }, // F4 + { 67, (RhythmGameNoteType.Left, true) }, // G4 + { 69, (RhythmGameNoteType.Down, false) }, // A4 + { 71, (RhythmGameNoteType.Down, true) }, // B4 + { 72, (RhythmGameNoteType.Up, false) }, // C5 + { 74, (RhythmGameNoteType.Up, true) }, // D5 + { 76, (RhythmGameNoteType.Right, false) }, // E5 + { 77, (RhythmGameNoteType.Right, true) }, // F5 + }; + + private List Notes = new(); + private int TicksPerBeat; + private float BeatsPerMinute = 120f; + private Dictionary NoteOnEvents = new(); + + public static RhythmGameSong ParseMidi(string path) { + MidiFile midi = new MidiFile(path); + + if (midi.Tracks.Length != 1) + throw new System.Exception("MIDI file should have exactly one track"); + + RhythmGameSongParser parser = new(ticksPerBeat: midi.TicksPerQuarterNote); + midi.Tracks[0].MidiEvents.ForEach(parser.ParseMidiEvent); + return parser.GetSong(); + } + + private RhythmGameSongParser(int ticksPerBeat) { + TicksPerBeat = ticksPerBeat; + } + + private void ParseMidiEvent(MidiEvent midiEvent) { + switch (midiEvent.MidiEventType) { + case MidiEventType.MetaEvent: + ParseMetaEvent(midiEvent); + break; + + case MidiEventType.NoteOn: + ParseNoteOn(midiEvent); + break; + + case MidiEventType.NoteOff: + ParseNoteOff(midiEvent); + break; + } + } + + private void ParseMetaEvent(MidiEvent midiEvent) { + if (midiEvent.MetaEventType == MetaEventType.Tempo) { + // TODO: Track a list of tempo change events on the song object + BeatsPerMinute = (int)midiEvent.Arg2; + } + } + + private void ParseNoteOn(MidiEvent midiEvent) { + /** + * Store the note on event so that we can reference it from the note off + * event. + */ + NoteOnEvents[midiEvent.Note] = midiEvent; + } + + private void ParseNoteOff(MidiEvent midiEvent) { + int pitch = midiEvent.Note; + var (type, isHold) = MIDI_PITCH_DATA[pitch]; + + float onTime = TicksToSeconds(NoteOnEvents[pitch].Time); + float offTime = TicksToSeconds(midiEvent.Time); + float actualDuration = offTime - onTime; + float effectiveDuration = isHold ? actualDuration : 0f; + + AddNote(type, onTime, effectiveDuration); + } + + private void AddNote(RhythmGameNoteType type, float time, float duration) { + Notes.Add( + new() + { + Type = type, + Time = time, + Duration = duration, + } + ); + } + + private float TicksToSeconds(float ticks) => ticks / TicksPerBeat * 60f / BeatsPerMinute; + + private RhythmGameSong GetSong() => new() { BeatsPerMinute = BeatsPerMinute, Notes = Notes }; +} diff --git a/Assets/src/RhythmGameSongParser.cs.meta b/Assets/src/RhythmGameSongParser.cs.meta new file mode 100644 index 00000000..76b6ddd8 --- /dev/null +++ b/Assets/src/RhythmGameSongParser.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 27cdff99b9c214117bcbaf8810202565 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: