diff --git a/.idea/.idea.SpringJam2026/.idea/workspace.xml b/.idea/.idea.SpringJam2026/.idea/workspace.xml
index 232051c..b5bbc0d 100644
--- a/.idea/.idea.SpringJam2026/.idea/workspace.xml
+++ b/.idea/.idea.SpringJam2026/.idea/workspace.xml
@@ -5,9 +5,10 @@
-
-
-
+
+
+
+
@@ -18,12 +19,17 @@
-
+
+
+
+
+
+
@@ -34,7 +40,7 @@
"ModuleVcsDetector.initialDetectionPerformed": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.git.unshallow": "true",
- "git-widget-placeholder": "feature/services",
+ "git-widget-placeholder": "feat/state",
"ignore.virus.scanning.warn.message": "true",
"node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true",
@@ -108,7 +114,7 @@
1777050991106
-
+
diff --git a/Assets/Prefabs.meta b/Assets/Prefabs.meta
new file mode 100644
index 0000000..0deeebd
--- /dev/null
+++ b/Assets/Prefabs.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: ad6f5bb99bf5483428907989a6f4523b
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Prefabs/UI.meta b/Assets/Prefabs/UI.meta
new file mode 100644
index 0000000..e3518df
--- /dev/null
+++ b/Assets/Prefabs/UI.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: c424f0b586251414baa6d6a6504c51d8
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Prefabs/UI/MainMenu.prefab b/Assets/Prefabs/UI/MainMenu.prefab
new file mode 100644
index 0000000..fabc4c5
--- /dev/null
+++ b/Assets/Prefabs/UI/MainMenu.prefab
@@ -0,0 +1,70 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!1 &1522947426740712753
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 4714161115413790785}
+ - component: {fileID: 5464361591880727452}
+ - component: {fileID: 4116044285910179794}
+ m_Layer: 5
+ m_Name: MainMenu
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &4714161115413790785
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1522947426740712753}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!114 &5464361591880727452
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1522947426740712753}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 19102, guid: 0000000000000000e000000000000000, type: 0}
+ m_Name:
+ m_EditorClassIdentifier: UnityEngine.dll::UnityEngine.UIElements.UIDocument
+ m_PanelSettings: {fileID: 11400000, guid: 4f01bbaf43abd294b9e7341d0530a9aa, type: 2}
+ m_ParentUI: {fileID: 0}
+ sourceAsset: {fileID: 9197481963319205126, guid: d29bfcc0217a10b488edd76a7ae59cf2, type: 3}
+ m_SortingOrder: 0
+ m_Position: 0
+ m_WorldSpaceSizeMode: 1
+ m_WorldSpaceWidth: 1920
+ m_WorldSpaceHeight: 1080
+ m_PivotReferenceSize: 0
+ m_Pivot: 0
+ m_WorldSpaceCollider: {fileID: 0}
+--- !u!114 &4116044285910179794
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1522947426740712753}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 313c38e8548656447b712e4191d6e57c, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Assembly-CSharp::UI.MainMenu
diff --git a/Assets/Prefabs/UI/MainMenu.prefab.meta b/Assets/Prefabs/UI/MainMenu.prefab.meta
new file mode 100644
index 0000000..4469898
--- /dev/null
+++ b/Assets/Prefabs/UI/MainMenu.prefab.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 0453e357da6c33c468bb3d3726552b8e
+PrefabImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scenes/Bootstrap.unity b/Assets/Scenes/Bootstrap.unity
index 4c2613f..fc0f1bb 100644
--- a/Assets/Scenes/Bootstrap.unity
+++ b/Assets/Scenes/Bootstrap.unity
@@ -262,6 +262,51 @@ AudioSource:
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
+--- !u!1 &1469922749
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 1469922751}
+ - component: {fileID: 1469922750}
+ m_Layer: 0
+ m_Name: GameStateMachine
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!114 &1469922750
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1469922749}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 8ceca53f0e424e51a6c75d7a4bd1074d, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Assembly-CSharp::State.Game.MainStateMachine
+ mainMenuPrefab: {fileID: 1522947426740712753, guid: 0453e357da6c33c468bb3d3726552b8e, type: 3}
+--- !u!4 &1469922751
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1469922749}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &1671688483
GameObject:
m_ObjectHideFlags: 0
@@ -271,6 +316,7 @@ GameObject:
serializedVersion: 6
m_Component:
- component: {fileID: 1671688485}
+ - component: {fileID: 1671688486}
- component: {fileID: 1671688484}
m_Layer: 0
m_Name: Services
@@ -308,6 +354,20 @@ Transform:
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!114 &1671688486
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1671688483}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 4b256c4001284134990a2af43c375455, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Assembly-CSharp::Bootstrap
+ startType: 1
+ gameStateMachine: {fileID: 1469922750}
--- !u!1 &1755353616
GameObject:
m_ObjectHideFlags: 0
@@ -457,3 +517,4 @@ SceneRoots:
- {fileID: 1671688485}
- {fileID: 1755353619}
- {fileID: 1327356648}
+ - {fileID: 1469922751}
diff --git a/Assets/Scenes/MainMenu.unity b/Assets/Scenes/MainMenu.unity
new file mode 100644
index 0000000..92591d6
--- /dev/null
+++ b/Assets/Scenes/MainMenu.unity
@@ -0,0 +1,263 @@
+%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: 10
+ 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: 13
+ m_BakeOnSceneLoad: 0
+ 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_ReflectionCompression: 2
+ m_MixedBakeMode: 2
+ m_BakeBackend: 2
+ m_PVRSampling: 1
+ m_PVRDirectSampleCount: 32
+ m_PVRSampleCount: 512
+ m_PVRBounces: 2
+ m_PVREnvironmentSampleCount: 256
+ m_PVREnvironmentReferencePointCount: 2048
+ m_PVRFilteringMode: 1
+ m_PVRDenoiserTypeDirect: 1
+ m_PVRDenoiserTypeIndirect: 1
+ m_PVRDenoiserTypeAO: 1
+ m_PVRFilterTypeDirect: 0
+ m_PVRFilterTypeIndirect: 0
+ m_PVRFilterTypeAO: 0
+ m_PVREnvironmentMIS: 1
+ m_PVRCulling: 1
+ m_PVRFilteringGaussRadiusDirect: 1
+ m_PVRFilteringGaussRadiusIndirect: 1
+ m_PVRFilteringGaussRadiusAO: 1
+ m_PVRFilteringAtrousPositionSigmaDirect: 0.5
+ m_PVRFilteringAtrousPositionSigmaIndirect: 2
+ m_PVRFilteringAtrousPositionSigmaAO: 1
+ m_ExportTrainingData: 0
+ m_TrainingDataDestination: TrainingData
+ m_LightProbeSampleCountMultiplier: 4
+ m_LightingDataAsset: {fileID: 20201, guid: 0000000000000000f000000000000000, type: 0}
+ m_LightingSettings: {fileID: 0}
+--- !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 &478016687
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 478016690}
+ - component: {fileID: 478016689}
+ - component: {fileID: 478016688}
+ - component: {fileID: 478016691}
+ m_Layer: 0
+ m_Name: Main Camera
+ m_TagString: MainCamera
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!81 &478016688
+AudioListener:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 478016687}
+ m_Enabled: 1
+--- !u!20 &478016689
+Camera:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 478016687}
+ m_Enabled: 1
+ serializedVersion: 2
+ m_ClearFlags: 1
+ m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0}
+ m_projectionMatrixMode: 1
+ m_GateFitMode: 2
+ m_FOVAxisMode: 0
+ m_Iso: 200
+ m_ShutterSpeed: 0.005
+ m_Aperture: 16
+ m_FocusDistance: 10
+ m_FocalLength: 50
+ m_BladeCount: 5
+ m_Curvature: {x: 2, y: 11}
+ m_BarrelClipping: 0.25
+ m_Anamorphism: 0
+ m_SensorSize: {x: 36, y: 24}
+ m_LensShift: {x: 0, y: 0}
+ m_NormalizedViewPortRect:
+ serializedVersion: 2
+ x: 0
+ y: 0
+ width: 1
+ height: 1
+ near clip plane: 0.3
+ far clip plane: 1000
+ field of view: 60
+ orthographic: 1
+ orthographic size: 5
+ m_Depth: -1
+ m_CullingMask:
+ serializedVersion: 2
+ m_Bits: 4294967295
+ m_RenderingPath: -1
+ m_TargetTexture: {fileID: 0}
+ m_TargetDisplay: 0
+ m_TargetEye: 3
+ m_HDR: 1
+ m_AllowMSAA: 1
+ m_AllowDynamicResolution: 0
+ m_ForceIntoRT: 0
+ m_OcclusionCulling: 1
+ m_StereoConvergence: 10
+ m_StereoSeparation: 0.022
+--- !u!4 &478016690
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 478016687}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: -10}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!114 &478016691
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 478016687}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: a79441f348de89743a2939f4d699eac1, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.UniversalAdditionalCameraData
+ m_RenderShadows: 1
+ m_RequiresDepthTextureOption: 2
+ m_RequiresOpaqueTextureOption: 2
+ m_CameraType: 0
+ m_Cameras: []
+ m_RendererIndex: -1
+ m_VolumeLayerMask:
+ serializedVersion: 2
+ m_Bits: 1
+ m_VolumeTrigger: {fileID: 0}
+ m_VolumeFrameworkUpdateModeOption: 2
+ m_RenderPostProcessing: 0
+ m_Antialiasing: 0
+ m_AntialiasingQuality: 2
+ m_StopNaN: 0
+ m_Dithering: 0
+ m_ClearDepth: 1
+ m_AllowXRRendering: 1
+ m_AllowHDROutput: 1
+ m_UseScreenCoordOverride: 0
+ m_ScreenSizeOverride: {x: 0, y: 0, z: 0, w: 0}
+ m_ScreenCoordScaleBias: {x: 0, y: 0, z: 0, w: 0}
+ m_RequiresDepthTexture: 0
+ m_RequiresColorTexture: 0
+ m_TaaSettings:
+ m_Quality: 3
+ m_FrameInfluence: 0.1
+ m_JitterScale: 1
+ m_MipBias: 0
+ m_VarianceClampScale: 0.9
+ m_ContrastAdaptiveSharpening: 0
+ m_Version: 2
+--- !u!1660057539 &9223372036854775807
+SceneRoots:
+ m_ObjectHideFlags: 0
+ m_Roots:
+ - {fileID: 478016690}
diff --git a/Assets/Scenes/MainMenu.unity.meta b/Assets/Scenes/MainMenu.unity.meta
new file mode 100644
index 0000000..81205d2
--- /dev/null
+++ b/Assets/Scenes/MainMenu.unity.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: d9e43626d738a3f448e4acc7b2c1dfe3
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/Management/BGM.cs b/Assets/Scripts/Management/BGM.cs
index 63b498e..f7a3d20 100644
--- a/Assets/Scripts/Management/BGM.cs
+++ b/Assets/Scripts/Management/BGM.cs
@@ -3,13 +3,26 @@ using UnityEngine;
namespace Management
{
+ ///
+ /// Singleton pattern object that holds an audio source, designed as single source of
+ /// truth and driver for the BGM music.
+ ///
[RequireComponent(typeof(AudioSource))]
public class BGM : MonoBehaviour
{
+ // hold a static reference to enforce singleton pattern
private static BGM _instance;
+
+ // the audiosource that will play the music
private AudioSource _audioSource;
+
+ // get the original volume set in the editor so when we fade in/out we know
+ // what volume to return to.
private float _originalVolume;
+ // Fades out the currently playing music if playing, then fades in
+ // the provided clip over the amount of time specified.
+ // if t == 0 swap is instant.
public void FadeIn(AudioClip clip, float t = 1f)
{
if (clip == null)
@@ -66,6 +79,7 @@ namespace Management
private void Awake()
{
+ // enforce singleton
if (_instance != null && _instance != this)
{
Destroy(gameObject);
diff --git a/Assets/Scripts/Management/Bootstrap.cs b/Assets/Scripts/Management/Bootstrap.cs
new file mode 100644
index 0000000..9f11d77
--- /dev/null
+++ b/Assets/Scripts/Management/Bootstrap.cs
@@ -0,0 +1,76 @@
+using System;
+using State;
+using State.Game;
+using UnityEngine;
+
+namespace Management
+{
+ ///
+ /// Bootstrap is the first user added object that should be run.
+ /// It should be added to a game object in the Bootstrap scene, which should be the first
+ /// scene loaded when the game starts.
+ ///
+ /// Bootstrap initializes the game state machine, and starts it based on
+ /// the value of "Start Type" selected in the Inspector.
+ ///
+ public class Bootstrap : MonoBehaviour
+ {
+ // Hold a static reference to this instance so we can enforce singleton
+ private static Bootstrap _instance;
+
+ // Determines what state the configure state machine will start in
+ private enum StartType
+ {
+ Splash,
+ MainMenu,
+ Game
+ }
+
+ [SerializeField] private StartType startType = StartType.Splash;
+ [SerializeField] private Machine gameStateMachine;
+
+ private void Awake()
+ {
+ // enforce singleton
+ if (_instance != null && _instance != this)
+ {
+ Destroy(gameObject);
+ return;
+ }
+
+ // if we got this far, this object didn't exist, set the static instance to this and continue normally.
+ _instance = this;
+
+ // ensure object survives scene loads
+ DontDestroyOnLoad(gameObject);
+ }
+
+ private void Start()
+ {
+ // exit game early if we're misconfigured.
+ if (gameStateMachine == null)
+ {
+ Debug.LogError("no state machine for bootstrap");
+ #if UNITY_EDITOR
+ UnityEditor.EditorApplication.isPlaying = false;
+ #else
+ Application.Quit();
+ #endif
+ return;
+ }
+
+ // determine what state to start machine in. Useful for quick testing.
+ // Should be "Splash" for release.
+ switch (startType)
+ {
+ case StartType.Splash:
+ case StartType.Game:
+ throw new NotImplementedException();
+ case StartType.MainMenu:
+ default:
+ gameStateMachine.ChangeState(new MainMenuState());
+ break;
+ }
+ }
+ }
+}
diff --git a/Assets/Scripts/Management/Bootstrap.cs.meta b/Assets/Scripts/Management/Bootstrap.cs.meta
new file mode 100644
index 0000000..cc0a968
--- /dev/null
+++ b/Assets/Scripts/Management/Bootstrap.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 4b256c4001284134990a2af43c375455
\ No newline at end of file
diff --git a/Assets/Scripts/Management/SFX.cs b/Assets/Scripts/Management/SFX.cs
index 89b3df0..abd58cd 100644
--- a/Assets/Scripts/Management/SFX.cs
+++ b/Assets/Scripts/Management/SFX.cs
@@ -4,15 +4,30 @@ using UnityEngine;
namespace Management
{
+ ///
+ /// Single drive to play all SFX in game. Contains a limiter that limits amount of times
+ /// any one clip will play to prevent volume runaways and other sound wave based oddities.
+ ///
[RequireComponent(typeof(AudioSource))]
public class SFX : MonoBehaviour
{
+ // The maximum number any individual clip can be played.
[SerializeField] private int maxActiveClips;
+ // singleton pattern
private static SFX _instance;
+
+ // audio driver
private AudioSource _audioSource;
+
+ // Playing clip registry. Used as data source to limit amount of times a clip will play simultaneously
private Dictionary _clipsInProgress;
+ ///
+ /// Plpays the given audio clip through the audio driver
+ ///
+ /// The sound to play
+ /// The normalized volume it will play at
public void PlayOneShot(AudioClip clip, float volume = 1f)
{
if (clip == null) return;
@@ -26,6 +41,7 @@ namespace Management
StartCoroutine(ClearClip(clip));
}
+ // Coroutine to clear registry once sound is finished.
private IEnumerator ClearClip(AudioClip clip)
{
yield return new WaitForSeconds(clip.length);
diff --git a/Assets/Scripts/Management/Services.cs b/Assets/Scripts/Management/Services.cs
index e60fe09..31e043c 100644
--- a/Assets/Scripts/Management/Services.cs
+++ b/Assets/Scripts/Management/Services.cs
@@ -2,10 +2,19 @@ using UnityEngine;
namespace Management
{
+ ///
+ /// Global access pattern for services.
+ /// If you need a component or some data that needs to be used in many places, here's a good spot.
+ ///
public class Services : MonoBehaviour
{
+ // Enforce singleton pattern
public static Services Instance { get; private set; }
+
+ // BGM service for playing/changing background music.
public BGM BGM => bgm;
+
+ // SFX service to serve as single spot SFX plays from.
public SFX SFX => sfx;
[SerializeField] private BGM bgm;
@@ -13,6 +22,7 @@ namespace Management
private void Awake()
{
+ // enforce singleton
if (Instance != null && Instance != this)
{
Destroy(gameObject);
@@ -20,6 +30,8 @@ namespace Management
}
Instance = this;
+
+ // survive scene reloads
DontDestroyOnLoad(gameObject);
}
}
diff --git a/Assets/Scripts/State.meta b/Assets/Scripts/State.meta
new file mode 100644
index 0000000..3d13d18
--- /dev/null
+++ b/Assets/Scripts/State.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 67f88045e5c39dd42af3559790c4c117
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/State/Game.meta b/Assets/Scripts/State/Game.meta
new file mode 100644
index 0000000..3bd0bb2
--- /dev/null
+++ b/Assets/Scripts/State/Game.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: c5e93b34c6364ca0ad208993a593ac33
+timeCreated: 1777053968
\ No newline at end of file
diff --git a/Assets/Scripts/State/Game/MainGameState.cs b/Assets/Scripts/State/Game/MainGameState.cs
new file mode 100644
index 0000000..72894e6
--- /dev/null
+++ b/Assets/Scripts/State/Game/MainGameState.cs
@@ -0,0 +1,7 @@
+namespace State.Game
+{
+ public class MainGameState : GameState
+ {
+ protected MainStateMachine MainStateMachine => (MainStateMachine)StateMachine;
+ }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/State/Game/MainGameState.cs.meta b/Assets/Scripts/State/Game/MainGameState.cs.meta
new file mode 100644
index 0000000..8fbb41b
--- /dev/null
+++ b/Assets/Scripts/State/Game/MainGameState.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 03265909fca144b289ccf928c08acd42
+timeCreated: 1777056025
\ No newline at end of file
diff --git a/Assets/Scripts/State/Game/MainMenuState.cs b/Assets/Scripts/State/Game/MainMenuState.cs
new file mode 100644
index 0000000..8567a1a
--- /dev/null
+++ b/Assets/Scripts/State/Game/MainMenuState.cs
@@ -0,0 +1,55 @@
+using System.Collections;
+using UI;
+using UnityEngine;
+using UnityEngine.SceneManagement;
+
+namespace State.Game
+{
+ public class MainMenuState : MainGameState
+ {
+ private MainMenu _mainMenuInstance;
+ public override void OnEnter(Machine machine)
+ {
+ base.OnEnter(machine);
+ var handle = SceneManager.LoadSceneAsync("MainMenu", LoadSceneMode.Additive);
+ machine.StartCoroutine(LoadMenu(handle));
+ }
+
+ private IEnumerator LoadMenu(AsyncOperation handle)
+ {
+ while (!handle.isDone)
+ {
+ yield return null;
+ }
+
+ SceneManager.SetActiveScene(SceneManager.GetSceneByName("MainMenu"));
+ _mainMenuInstance = Object.Instantiate(MainStateMachine.MainMenuPrefab).GetComponent();
+ if (_mainMenuInstance == null)
+ {
+ Debug.LogError("Error: MainMenu instance is null");
+ }
+
+ _mainMenuInstance.OnStartGame += HandleStartGame;
+ _mainMenuInstance.OnExitGame += () =>
+ {
+#if UNITY_EDITOR
+ UnityEditor.EditorApplication.isPlaying = false;
+#else
+ Application.Quit();
+#endif
+ };
+ }
+
+ public override void OnExit()
+ {
+ Object.Destroy(_mainMenuInstance.gameObject);
+ base.OnExit();
+ }
+
+ private void HandleStartGame()
+ {
+ _mainMenuInstance.OnStartGame -= HandleStartGame;
+ Debug.Log("Starting game");
+ }
+ }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/State/Game/MainMenuState.cs.meta b/Assets/Scripts/State/Game/MainMenuState.cs.meta
new file mode 100644
index 0000000..9ba2c76
--- /dev/null
+++ b/Assets/Scripts/State/Game/MainMenuState.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: c84ce8701033494696a7447eb5c58b8b
+timeCreated: 1777054000
\ No newline at end of file
diff --git a/Assets/Scripts/State/Game/MainStateMachine.cs b/Assets/Scripts/State/Game/MainStateMachine.cs
new file mode 100644
index 0000000..1168db7
--- /dev/null
+++ b/Assets/Scripts/State/Game/MainStateMachine.cs
@@ -0,0 +1,11 @@
+using UnityEngine;
+
+namespace State.Game
+{
+ public class MainStateMachine : Machine
+ {
+ public GameObject MainMenuPrefab => mainMenuPrefab;
+
+ [SerializeField] private GameObject mainMenuPrefab;
+ }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/State/Game/MainStateMachine.cs.meta b/Assets/Scripts/State/Game/MainStateMachine.cs.meta
new file mode 100644
index 0000000..899795b
--- /dev/null
+++ b/Assets/Scripts/State/Game/MainStateMachine.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 8ceca53f0e424e51a6c75d7a4bd1074d
+timeCreated: 1777055906
\ No newline at end of file
diff --git a/Assets/Scripts/State/GameState.cs b/Assets/Scripts/State/GameState.cs
new file mode 100644
index 0000000..3c303ad
--- /dev/null
+++ b/Assets/Scripts/State/GameState.cs
@@ -0,0 +1,27 @@
+using UnityEngine;
+
+namespace State
+{
+ ///
+ /// State that drives the engine. OnEnter, OnUpdate, OnFixedUpdate, and OnExit called by parent state machine
+ ///
+ public class GameState
+ {
+ protected Machine StateMachine;
+
+ ///
+ /// Called when parent state machine loads this state.
+ /// Gives a reference to the parent state machine to the state.
+ ///
+ ///
+ public virtual void OnEnter(Machine machine)
+ {
+ StateMachine = machine;
+ }
+
+ // Called when parent StateMachine ChangeState is about to leave this state
+ public virtual void OnExit() {}
+ public virtual void OnUpdate(float deltaTime) {}
+ public virtual void OnFixedUpdate(float deltaTime) {}
+ }
+}
diff --git a/Assets/Scripts/State/GameState.cs.meta b/Assets/Scripts/State/GameState.cs.meta
new file mode 100644
index 0000000..ae58e8e
--- /dev/null
+++ b/Assets/Scripts/State/GameState.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: aece5e36c5691b84592b559518dc979c
\ No newline at end of file
diff --git a/Assets/Scripts/State/Machine.cs b/Assets/Scripts/State/Machine.cs
new file mode 100644
index 0000000..c8af3f5
--- /dev/null
+++ b/Assets/Scripts/State/Machine.cs
@@ -0,0 +1,32 @@
+using UnityEngine;
+
+namespace State
+{
+ ///
+ /// Simple state machine that provides OnEnter, OnExit, Update and FixedUpdate callbacks
+ /// to the current state.
+ ///
+ public class Machine : MonoBehaviour
+ {
+ private GameState _currentState;
+
+ public void Update()
+ {
+ _currentState?.OnUpdate(Time.deltaTime);
+ }
+
+ public void FixedUpdate()
+ {
+ _currentState?.OnFixedUpdate(Time.fixedDeltaTime);
+ }
+
+ // ChangeState calls the Exit callback on the currently loaded state if one is loaded,
+ // then loads up the new given state, and calls OnEnter on it.
+ public void ChangeState(GameState newState)
+ {
+ _currentState?.OnExit();
+ _currentState = newState;
+ _currentState.OnEnter(this);
+ }
+ }
+}
diff --git a/Assets/Scripts/State/Machine.cs.meta b/Assets/Scripts/State/Machine.cs.meta
new file mode 100644
index 0000000..9cf93a7
--- /dev/null
+++ b/Assets/Scripts/State/Machine.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: c58c43f9d69af404f997461c6348049d
\ No newline at end of file
diff --git a/Assets/Scripts/UI.meta b/Assets/Scripts/UI.meta
new file mode 100644
index 0000000..f7b9f19
--- /dev/null
+++ b/Assets/Scripts/UI.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 53bd1392872c7b945bfd564da3f84986
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/UI/MainMenu.cs b/Assets/Scripts/UI/MainMenu.cs
new file mode 100644
index 0000000..0bf702e
--- /dev/null
+++ b/Assets/Scripts/UI/MainMenu.cs
@@ -0,0 +1,33 @@
+using System;
+using UnityEngine;
+using UnityEngine.UIElements;
+
+namespace UI
+{
+ [RequireComponent(typeof(UIDocument))]
+ public class MainMenu : MonoBehaviour
+ {
+ public event Action OnStartGame;
+ public event Action OnExitGame;
+
+ private Button _startGameButton;
+ private Button _exitGameButton;
+
+ private void Awake()
+ {
+ _startGameButton = GetComponent().rootVisualElement.Q