├── .github ├── ExampleScenario1-Simplistic.png ├── ExampleScenario1-EventDriven.png └── README.md ├── ScriptableObjects ├── Events │ ├── FloatEvent.cs │ ├── Vector3Event.cs │ ├── _Base.meta │ ├── FloatEvent.cs.meta │ ├── GameEvent.cs.meta │ ├── Vector3Event.cs.meta │ ├── FloatEvent_Invokable.cs.meta │ ├── GameEvent_Invokable.cs.meta │ ├── _Base │ │ ├── GameEventBase.cs.meta │ │ └── GameEventBase.cs │ ├── Vector3Event_Invokable.cs.meta │ ├── GameEvent_Invokable.cs │ ├── FloatEvent_Invokable.cs │ ├── Vector3Event_Invokable.cs │ └── GameEvent.cs ├── Events.meta ├── Properties.meta └── Properties │ ├── _Base.meta │ ├── IntProperty.cs │ ├── FloatProperty.cs │ ├── ColorProperty.cs │ ├── BoundsProperty.cs │ ├── Vector3Property.cs │ ├── IntProperty.cs.meta │ ├── BoundsProperty.cs.meta │ ├── ColorProperty.cs.meta │ ├── FloatProperty.cs.meta │ ├── Vector3Property.cs.meta │ ├── IntProperty_Writeable.cs.meta │ ├── _Base │ ├── GameProperty.cs.meta │ └── GameProperty.cs │ ├── BoundsProperty_Writeable.cs.meta │ ├── ColorProperty_Writeable.cs.meta │ ├── FloatProperty_Writeable.cs.meta │ ├── Vector3Property_Writeable.cs.meta │ ├── IntProperty_Writeable.cs │ ├── BoundsProperty_Writeable.cs │ ├── ColorProperty_Writeable.cs │ ├── FloatProperty_Writeable.cs │ └── Vector3Property_Writeable.cs ├── Utility.meta ├── Interfaces.meta ├── MonoBehaviours.meta ├── _prototype.meta ├── MonoBehaviours ├── GUI.meta ├── _Base.meta ├── EventCountOutput.cs.meta ├── GUI │ ├── Interactable.cs.meta │ ├── ImageFillSetter.cs.meta │ ├── GraphicColorSetter.cs.meta │ ├── GraphicFadeBehaviour.cs.meta │ ├── SelectableColorSetter.cs.meta │ ├── GraphicColorSetter.cs │ ├── ImageFillSetter.cs │ ├── SelectableColorSetter.cs │ ├── GraphicFadeBehaviour.cs │ └── Interactable.cs ├── SpriteSizeSetter.cs.meta ├── CameraColorSetter.cs.meta ├── CameraFrustumTracker.cs.meta ├── EventCountDisplay.cs.meta ├── GameDistanceTracker.cs.meta ├── ParticleSystemTrigger.cs.meta ├── SpriteColorSetter.cs.meta ├── _Base │ ├── ColorSetterBase.cs.meta │ ├── ComponentTriggerBase.cs.meta │ ├── SubscriptionHelperMonoBehaviour.cs.meta │ ├── ComponentTriggerBase.cs │ ├── ColorSetterBase.cs │ └── SubscriptionHelperMonoBehaviour.cs ├── AudioPlaybackModulator.cs.meta ├── CameraColorSetter.cs ├── ParticleSystemTrigger.cs ├── SpriteColorSetter.cs ├── EventCountOutput.cs ├── EventCountDisplay.cs ├── SpriteSizeSetter.cs ├── CameraFrustumTracker.cs ├── GameDistanceTracker.cs └── AudioPlaybackModulator.cs ├── ScriptableObjects.meta ├── _prototype ├── _Editor.meta ├── CompositeGameEvent.cs.meta ├── InterfaceWrapping.cs.meta ├── CompositeMonoBehaviour.cs.meta ├── GameProperty_Writeable.cs.meta ├── _Editor │ ├── GameStateEditor.cs.meta │ └── GameStateEditor.cs ├── GameProperty_Writeable.cs ├── CompositeMonoBehaviour.cs ├── CompositeGameEvent.cs └── InterfaceWrapping.cs ├── Utility ├── MyMath.cs.meta └── MyMath.cs ├── Interfaces ├── IEventSource.cs.meta └── IEventSource.cs └── LICENSE /.github/ExampleScenario1-Simplistic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baratgabor/Unity3D-ReactiveScriptables/HEAD/.github/ExampleScenario1-Simplistic.png -------------------------------------------------------------------------------- /.github/ExampleScenario1-EventDriven.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baratgabor/Unity3D-ReactiveScriptables/HEAD/.github/ExampleScenario1-EventDriven.png -------------------------------------------------------------------------------- /ScriptableObjects/Events/FloatEvent.cs: -------------------------------------------------------------------------------- 1 | namespace LeakyAbstraction.ReactiveScriptables 2 | { 3 | public class FloatEvent : GameEvent 4 | { } 5 | } -------------------------------------------------------------------------------- /ScriptableObjects/Events/Vector3Event.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace LeakyAbstraction.ReactiveScriptables 4 | { 5 | public class Vector3Event : GameEvent 6 | { } 7 | } -------------------------------------------------------------------------------- /Utility.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a8bc9c309545b4f45bf1e0e855388777 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Interfaces.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: fc77f6303d3ba2d4bbea6ed7f192c78a 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /MonoBehaviours.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d83f8fd742857b248a69f408766ad2ca 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /_prototype.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 87b6e0bec7c2392458cdc05ba9fa5c40 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /MonoBehaviours/GUI.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d04023595e5f2694eb6ec290635add77 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /ScriptableObjects.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f6b31f24af395ce4596d7e63b909a776 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /_prototype/_Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 15d17f7e93011534d95ac1e87d612076 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /MonoBehaviours/_Base.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: af7a8df094ce21045837db5544a518b6 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /ScriptableObjects/Events.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7815518a074923d4e8e7f4a76be76cd1 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /ScriptableObjects/Properties.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c72499948f44390409dc6e4b482b40d7 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /ScriptableObjects/Events/_Base.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 04da134cef6dada489aa5fafcdbcaf6b 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /ScriptableObjects/Properties/_Base.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b7c63ecefe772ab4e91ffc1bc6a82bb2 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /ScriptableObjects/Properties/IntProperty.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace LeakyAbstraction.ReactiveScriptables 4 | { 5 | [CreateAssetMenu(menuName = "Game Properties/Int Property")] 6 | public class IntProperty : GameProperty 7 | { } 8 | } 9 | -------------------------------------------------------------------------------- /ScriptableObjects/Properties/FloatProperty.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace LeakyAbstraction.ReactiveScriptables 4 | { 5 | [CreateAssetMenu(menuName = "Game Properties/Float Property")] 6 | public class FloatProperty : GameProperty 7 | { } 8 | } -------------------------------------------------------------------------------- /ScriptableObjects/Properties/ColorProperty.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace LeakyAbstraction.ReactiveScriptables 4 | { 5 | [CreateAssetMenu(menuName = "Game Properties/Color Property")] 6 | public class ColorProperty : GameProperty 7 | { } 8 | } 9 | -------------------------------------------------------------------------------- /ScriptableObjects/Properties/BoundsProperty.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace LeakyAbstraction.ReactiveScriptables 4 | { 5 | [CreateAssetMenu(menuName = "Game Properties/Bounds Property")] 6 | public class BoundsProperty : GameProperty 7 | { } 8 | } 9 | -------------------------------------------------------------------------------- /ScriptableObjects/Properties/Vector3Property.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace LeakyAbstraction.ReactiveScriptables 4 | { 5 | [CreateAssetMenu(menuName = "Game Properties/Vector3 Property")] 6 | public class Vector3Property : GameProperty 7 | { } 8 | } 9 | -------------------------------------------------------------------------------- /Utility/MyMath.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b3491c016e1066d47b8212d2a0203f41 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Interfaces/IEventSource.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: dc8032c09b04f964e8d1d86465489bea 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /MonoBehaviours/EventCountOutput.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: bf74e137c342aa544bf518ca61e3355a 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /MonoBehaviours/GUI/Interactable.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 966e9632bb7f47b41ab411c1de0708df 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /MonoBehaviours/SpriteSizeSetter.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 85ff7dc630fc94e46bd686c49a0e70dc 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /_prototype/CompositeGameEvent.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 35120fa380100e14ea81ad29030d0bab 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /_prototype/InterfaceWrapping.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d128e8cdced73a04e8b837c211c214fb 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /MonoBehaviours/CameraColorSetter.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: fca05017a0fa4d3429ae8fe2ac6c0831 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /MonoBehaviours/CameraFrustumTracker.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1d7eaeeaec29c554385a30cdaff60ed5 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /MonoBehaviours/EventCountDisplay.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 078a9d32d5ec7c444ab88cb99958ee8c 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /MonoBehaviours/GUI/ImageFillSetter.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c0c5b4fbad2b0a642acf25eaff598c8a 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /MonoBehaviours/GameDistanceTracker.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 239700b0bba53d44499380e99efba1fb 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /MonoBehaviours/ParticleSystemTrigger.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 22ebd26048cecf245aa0060997b0adf9 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /MonoBehaviours/SpriteColorSetter.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 64fbe6f5a13056741913e4ad12bd18df 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /MonoBehaviours/_Base/ColorSetterBase.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c2e514f5fe09e6e4cbe5a8949d3fd8df 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /ScriptableObjects/Events/FloatEvent.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0aee33976c194d9449d6a1be43d20259 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /ScriptableObjects/Events/GameEvent.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 937dbcbaee508ce4cac051a4efe57bc6 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /_prototype/CompositeMonoBehaviour.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b8fe7934009dec949bb35ab26f53c731 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /_prototype/GameProperty_Writeable.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 00a1962054d4d3d4ead8f98fccea480c 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /_prototype/_Editor/GameStateEditor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ec2992dfe3d40e94b9b6be8ba609732f 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /MonoBehaviours/AudioPlaybackModulator.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1fa8a512e5f5ebf47b12e0ab27a36183 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /MonoBehaviours/GUI/GraphicColorSetter.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f84dea21458ff5248b82ae7802b0408a 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /MonoBehaviours/GUI/GraphicFadeBehaviour.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c5d73f6d3cb1d964286ed7105b06c330 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /MonoBehaviours/GUI/SelectableColorSetter.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c02a9ccd36722c44396c77d9e5c0333d 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /MonoBehaviours/_Base/ComponentTriggerBase.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d97efeab0d8e92842ae9fbbb17f07191 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /ScriptableObjects/Events/Vector3Event.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8a3ef4769f01c8a43adc4fcd59bfc216 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /ScriptableObjects/Properties/IntProperty.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 16c1313187d71ba479525113f26e4b43 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /ScriptableObjects/Events/FloatEvent_Invokable.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 55c0f63ad54f80b4ea02bf14c75e50e4 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /ScriptableObjects/Events/GameEvent_Invokable.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 364c10bf731ea674dae33a54a4d1d2bf 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /ScriptableObjects/Events/_Base/GameEventBase.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b10e8520d96de3b4bacd6d9c1d3efc31 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /ScriptableObjects/Properties/BoundsProperty.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0ac4702e37a681b449c0204ee84d9d5a 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /ScriptableObjects/Properties/ColorProperty.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c0de16eebb8dd29409ae4727e85ff923 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /ScriptableObjects/Properties/FloatProperty.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9ca6d69d8c2b07f4a9cd670806e0f4e8 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /ScriptableObjects/Properties/Vector3Property.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 4055ea6e866d02d44b677c6a4c0ecd9d 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /ScriptableObjects/Events/Vector3Event_Invokable.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 43ef5a76c76091149962def56464b48d 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /ScriptableObjects/Properties/IntProperty_Writeable.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 993eae93af2d7af4b9074cf617aeb572 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /ScriptableObjects/Properties/_Base/GameProperty.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 85c76fc8c2cbf954ba255890606ad1a9 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /MonoBehaviours/_Base/SubscriptionHelperMonoBehaviour.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a00f7b079d7ccc74e968717ad98948be 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /ScriptableObjects/Events/GameEvent_Invokable.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace LeakyAbstraction.ReactiveScriptables 4 | { 5 | [CreateAssetMenu(menuName = "Game Events/Game Event - Invokable")] 6 | public class GameEvent_Invokable : GameEvent 7 | { 8 | public void Invoke() 9 | => OnEvent(this); 10 | } 11 | } -------------------------------------------------------------------------------- /ScriptableObjects/Properties/BoundsProperty_Writeable.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 4388636ce1c47a04eadc2de251d85251 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /ScriptableObjects/Properties/ColorProperty_Writeable.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9aeecc3f46a668e4bb25d1fa773d4eee 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /ScriptableObjects/Properties/FloatProperty_Writeable.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5691b20e9e7ef4347b7cc44ae0967207 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /ScriptableObjects/Properties/Vector3Property_Writeable.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 3d7eda7f916ea5e4b8e1d46c0309935f 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /ScriptableObjects/Events/FloatEvent_Invokable.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace LeakyAbstraction.ReactiveScriptables 4 | { 5 | [CreateAssetMenu(menuName = "Game Events/Float Event - Invokable")] 6 | public class FloatEvent_Invokable : FloatEvent 7 | { 8 | public void Invoke(float eventData) 9 | => OnEvent(eventData); 10 | } 11 | } -------------------------------------------------------------------------------- /ScriptableObjects/Properties/IntProperty_Writeable.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace LeakyAbstraction.ReactiveScriptables 4 | { 5 | [CreateAssetMenu(menuName = "Game Properties/Int Property - Writeable")] 6 | public class IntProperty_Writeable : IntProperty 7 | { 8 | new public void Set(int value) 9 | => SetAndNotify(value); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Interfaces/IEventSource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LeakyAbstraction.ReactiveScriptables 4 | { 5 | /// 6 | /// Facilitates the unified handling of GameEvent and GameProperty objects. 7 | /// 8 | /// 9 | public interface IEventSource 10 | { 11 | event Action Event; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /MonoBehaviours/CameraColorSetter.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace LeakyAbstraction.ReactiveScriptables 4 | { 5 | [ExecuteInEditMode] 6 | [RequireComponent(typeof(Camera))] 7 | public class CameraColorSetter : ColorSetterBase 8 | { 9 | protected override void SetColor(Color color) 10 | => _component.backgroundColor = color; 11 | } 12 | } -------------------------------------------------------------------------------- /MonoBehaviours/ParticleSystemTrigger.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace LeakyAbstraction.ReactiveScriptables 4 | { 5 | [RequireComponent(typeof(ParticleSystem))] 6 | public class ParticleSystemTrigger : ComponentTriggerBase 7 | { 8 | protected override void DoTrigger() 9 | { 10 | _component.Play(); 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /ScriptableObjects/Events/Vector3Event_Invokable.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace LeakyAbstraction.ReactiveScriptables 4 | { 5 | [CreateAssetMenu(menuName = "Game Events/Vector3 Event - Invokable")] 6 | public class Vector3Event_Invokable : Vector3Event 7 | { 8 | public void Invoke(Vector3 eventData) 9 | => OnEvent(eventData); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /MonoBehaviours/SpriteColorSetter.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace LeakyAbstraction.ReactiveScriptables 4 | { 5 | [ExecuteInEditMode] 6 | [RequireComponent(typeof(SpriteRenderer))] 7 | public class SpriteColorSetter : ColorSetterBase 8 | { 9 | protected override void SetColor(Color color) 10 | => _component.color = color; 11 | } 12 | } -------------------------------------------------------------------------------- /ScriptableObjects/Properties/BoundsProperty_Writeable.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace LeakyAbstraction.ReactiveScriptables 4 | { 5 | [CreateAssetMenu(menuName = "Game Properties/Bounds Property - Writeable")] 6 | public class BoundsProperty_Writeable : BoundsProperty 7 | { 8 | new public void Set(Bounds value) 9 | => SetAndNotify(value); 10 | } 11 | } -------------------------------------------------------------------------------- /ScriptableObjects/Properties/ColorProperty_Writeable.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace LeakyAbstraction.ReactiveScriptables 4 | { 5 | [CreateAssetMenu(menuName = "Game Properties/Color Property - Writeable")] 6 | public class ColorProperty_Writeable : ColorProperty 7 | { 8 | new public void Set(Color value) 9 | => SetAndNotify(value); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ScriptableObjects/Properties/FloatProperty_Writeable.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace LeakyAbstraction.ReactiveScriptables 4 | { 5 | [CreateAssetMenu(menuName = "Game Properties/Float Property - Writeable")] 6 | public class FloatProperty_Writeable : FloatProperty 7 | { 8 | new public void Set(float value) 9 | => SetAndNotify(value); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ScriptableObjects/Properties/Vector3Property_Writeable.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace LeakyAbstraction.ReactiveScriptables 4 | { 5 | [CreateAssetMenu(menuName = "Game Properties/Vector3 Property - Writeable")] 6 | public class Vector3Property_Writeable : Vector3Property 7 | { 8 | new public void Set(Vector3 value) 9 | => SetAndNotify(value); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /MonoBehaviours/GUI/GraphicColorSetter.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEngine.UI; 3 | 4 | namespace LeakyAbstraction.ReactiveScriptables 5 | { 6 | [ExecuteInEditMode] 7 | [RequireComponent(typeof(Graphic))] 8 | public class GraphicColorSetter : ColorSetterBase 9 | { 10 | protected override void SetColor(Color color) 11 | => _component.color = color; 12 | } 13 | } -------------------------------------------------------------------------------- /ScriptableObjects/Events/GameEvent.cs: -------------------------------------------------------------------------------- 1 | namespace LeakyAbstraction.ReactiveScriptables 2 | { 3 | public class GameEvent : GameEvent 4 | { 5 | // The only purpose of this class is to allow MonoBehaviour components to declare fields of this type, for read-only access to events. 6 | // To these fields the invokable version of the event class will be assigned to, as read-only. 7 | 8 | // Having the type parameter set to itself means it will let the subsribers know which GameEvent instance is the origin of the event trigger they are receiving. 9 | } 10 | } -------------------------------------------------------------------------------- /ScriptableObjects/Events/_Base/GameEventBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | 4 | namespace LeakyAbstraction.ReactiveScriptables 5 | { 6 | public abstract class GameEvent : ScriptableObject, IEventSource 7 | { 8 | public event Action Event; 9 | 10 | [SerializeField] 11 | [Tooltip("If set to true, will issue a warning if invocation is requested while there was no subscriber.")] 12 | private bool _mustHaveSubscriber = false; 13 | 14 | protected virtual void OnEvent(T eventData) 15 | { 16 | if (Event != null) 17 | Event.Invoke(eventData); 18 | else if (_mustHaveSubscriber) 19 | Debug.LogWarning("GameEvent flagged as must have subscriber did not have a subscriber."); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /MonoBehaviours/GUI/ImageFillSetter.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEngine.UI; 3 | 4 | namespace LeakyAbstraction.ReactiveScriptables 5 | { 6 | public class ImageFillSetter : MonoBehaviour 7 | { 8 | [Tooltip("Value to use as the current ")] 9 | public FloatProperty Variable; 10 | 11 | [Tooltip("Min value that Variable to have no fill on Image.")] 12 | public float Min; 13 | 14 | [Tooltip("Max value that Variable can be to fill Image.")] 15 | public FloatProperty Max; 16 | 17 | [Tooltip("Image to set the fill amount on.")] 18 | public Image Image; 19 | 20 | private void Update() 21 | { 22 | Image.fillAmount = Mathf.Clamp01( 23 | Mathf.InverseLerp(Min, Max.Get(), Variable.Get())); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /MonoBehaviours/EventCountOutput.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | 4 | namespace LeakyAbstraction.ReactiveScriptables 5 | { 6 | public class EventCountOutput : SubscriptionHelperMonoBehaviour 7 | { 8 | [SerializeField] 9 | private GameEvent _gameEvent = default; 10 | 11 | [SerializeField] 12 | private IntProperty_Writeable _countOutput = default; 13 | 14 | private int _counter = 0; 15 | 16 | void Start() 17 | { 18 | if (_gameEvent == null || _countOutput == null) 19 | throw new Exception("Depencencies not set."); 20 | 21 | _countOutput.Set(0); 22 | AddSubscription(_gameEvent, Increment); 23 | } 24 | 25 | private void Increment(GameEvent _) 26 | => _countOutput.Set(++_counter); 27 | } 28 | } -------------------------------------------------------------------------------- /MonoBehaviours/EventCountDisplay.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | using UnityEngine.UI; 4 | 5 | namespace LeakyAbstraction.ReactiveScriptables 6 | { 7 | [RequireComponent(typeof(Text))] 8 | public class EventCountDisplay : SubscriptionHelperMonoBehaviour 9 | { 10 | [SerializeField] 11 | private GameEvent _gameEvent = default; 12 | 13 | private Text _text; 14 | 15 | private int _counter = 0; 16 | 17 | private void Start() 18 | { 19 | if (_gameEvent == null) 20 | throw new Exception("Dependency not set."); 21 | 22 | _text = GetComponent(); 23 | AddSubscription(_gameEvent, Increment); 24 | } 25 | 26 | private void Increment(GameEvent _) 27 | => _text.text = (++_counter).ToString(); 28 | } 29 | } -------------------------------------------------------------------------------- /_prototype/GameProperty_Writeable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LeakyAbstraction.ReactiveScriptables.Prototyping 4 | { 5 | /// 6 | /// Doesn't work. If you derive from this abstract class, your class won't be Editor-compatible 7 | /// with a readonly derivation of GameProperty. 8 | /// So, for example FloatProperty_Writeable : GameProperty_Writeable won't be assignable 9 | /// to readonly FloatVariable : GameProperty fields in the editor. 10 | /// Thus, all writeable concrete classes NEED TO DERIVE DIRECTLY FROM THE READONLY VERSION. 11 | /// 12 | [Obsolete("Don't derive from this class. Read the class description.")] 13 | public abstract class GameProperty_Writeable : GameProperty 14 | { 15 | new public void Set(T value) 16 | => SetAndNotify(value); 17 | } 18 | } -------------------------------------------------------------------------------- /_prototype/_Editor/GameStateEditor.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor; 2 | using UnityEngine; 3 | 4 | namespace LeakyAbstraction.ReactiveScriptables.Prototyping 5 | { 6 | 7 | // Current design doesn't require invocation helper 8 | 9 | //[CustomEditor(typeof(GameData), true)] 10 | //public class GameVariableEditor : Editor 11 | //{ 12 | // //string[] propertiesInBaseClass = new string[] { }; 13 | 14 | // public override void OnInspectorGUI() 15 | // { 16 | // //EditorGUILayout.LabelField("Special A Drawing"); 17 | 18 | // //DrawPropertiesExcluding(serializedObject, propertiesInBaseClass); 19 | 20 | // base.OnInspectorGUI(); 21 | 22 | // GUI.enabled = Application.isPlaying; 23 | 24 | // GameData v = target as GameData; 25 | // if (GUILayout.Button("Invoke value changed event")) 26 | // v.Notify(); 27 | // } 28 | //} 29 | } -------------------------------------------------------------------------------- /_prototype/CompositeMonoBehaviour.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | 5 | namespace LeakyAbstraction.ReactiveScriptables.Prototyping 6 | { 7 | 8 | public interface IMonoBehaviourComponent 9 | { 10 | void MyOnEnable(); 11 | void MyOnDisable(); 12 | void MyOnAwake(); 13 | } 14 | 15 | public abstract class CompositeMonoBehaviour : MonoBehaviour 16 | { 17 | // Expose component methods how exactly? 18 | 19 | private List _components = new List(); 20 | 21 | private void Awake() 22 | { 23 | foreach (var c in _components) 24 | c.MyOnAwake(); 25 | } 26 | 27 | private void OnEnable() 28 | { 29 | foreach (var c in _components) 30 | c.MyOnEnable(); 31 | } 32 | 33 | private void OnDisable() 34 | { 35 | foreach (var c in _components) 36 | c.MyOnDisable(); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Gabor Barat 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MonoBehaviours/_Base/ComponentTriggerBase.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using System; 3 | 4 | namespace LeakyAbstraction.ReactiveScriptables 5 | { 6 | public abstract class ComponentTriggerBase : SubscriptionHelperMonoBehaviour 7 | where T : Component 8 | { 9 | [SerializeField] 10 | private GameEvent _event = default; 11 | 12 | [SerializeField] 13 | private GameSound _soundToPlay = GameSound.None; 14 | 15 | protected T _component; 16 | 17 | private void Awake() 18 | { 19 | _component = GetComponent(); 20 | 21 | if (_component == null) 22 | throw new Exception($"Cannot get component of type '{nameof(T)}'."); 23 | 24 | if (_event == null) 25 | Debug.LogWarning("No GameEvent set. This event trigger will never trigger."); 26 | 27 | AddSubscription(_event, OnTrigger); 28 | } 29 | 30 | private void OnTrigger(GameEvent sender) 31 | { 32 | if (_soundToPlay != GameSound.None) 33 | SoundManager.Instance.PlaySound(_soundToPlay); 34 | 35 | DoTrigger(); 36 | } 37 | 38 | // Implement in concrete classes to execute expected trigger behavior 39 | protected abstract void DoTrigger(); 40 | } 41 | } -------------------------------------------------------------------------------- /MonoBehaviours/SpriteSizeSetter.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace LeakyAbstraction.ReactiveScriptables 4 | { 5 | [RequireComponent(typeof(SpriteRenderer))] 6 | public class SpriteSizeSetter : MonoBehaviour 7 | { 8 | [SerializeField] 9 | private BoundsProperty _cameraBounds = default; 10 | private SpriteRenderer _spriteRenderer; 11 | 12 | private void Awake() 13 | { 14 | if (_cameraBounds == null) 15 | throw new System.Exception("Dependency not set."); 16 | 17 | _spriteRenderer = GetComponent(); 18 | } 19 | 20 | private void Start() 21 | { 22 | ApplyBounds(_cameraBounds.Get()); 23 | } 24 | 25 | private void OnEnable() 26 | => _cameraBounds.Event += ApplyBounds; 27 | 28 | private void OnDisable() 29 | => _cameraBounds.Event -= ApplyBounds; 30 | 31 | private void ApplyBounds(Bounds bounds) 32 | { 33 | Debug.Log("SpriteSizeSetter ran."); 34 | 35 | var spriteBounds = _spriteRenderer.bounds; 36 | var spriteWidth = spriteBounds.size.x; 37 | var spriteHeight = spriteBounds.size.y; 38 | 39 | var spriteScale = transform.localScale; 40 | float unitWidth = spriteWidth / spriteScale.x; 41 | float unitHeight = spriteHeight / spriteScale.y; 42 | 43 | float newScaleX = bounds.size.x / unitWidth * 1.1f; 44 | float newScaleY = bounds.size.y / unitHeight * 1.1f; 45 | 46 | transform.localScale = new Vector3(newScaleX, newScaleY); 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /ScriptableObjects/Properties/_Base/GameProperty.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | 5 | namespace LeakyAbstraction.ReactiveScriptables 6 | { 7 | /// 8 | /// Generic base class for concrete classes that store state and provide change notifications. 9 | /// Concrete GameProperty types has to be derived from this class, because the Unity Editor doesn't support generic classes. 10 | /// T is the type of the data stored, also the type of the event payload. 11 | /// 12 | public abstract class GameProperty : ScriptableObject, IEventSource 13 | { 14 | protected virtual void Set(T t) 15 | => SetAndNotify(t); 16 | 17 | public T Get() 18 | => _value; 19 | 20 | public event Action Event; 21 | 22 | [SerializeField] 23 | protected T _value; 24 | 25 | #if UNITY_EDITOR 26 | private void OnValidate() 27 | { 28 | // If the user made changes in the Editor, send the new value to the subscribers, both in Play mode and Edit mode. 29 | // Important for supporting components that run in edit mode (i.e. marked with the [ExecuteInEditMode] attribute). 30 | if (GUI.changed) 31 | Event?.Invoke(_value); 32 | } 33 | #endif 34 | 35 | protected void SetAndNotify(T newValue) 36 | { 37 | //TODO: Look into performance/allocation concerns with different types 38 | if (EqualityComparer.Default.Equals(newValue, _value)) 39 | return; 40 | 41 | _value = newValue; 42 | Event?.Invoke(_value); 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /MonoBehaviours/CameraFrustumTracker.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace LeakyAbstraction.ReactiveScriptables 4 | { 5 | [RequireComponent(typeof(Camera))] 6 | public class CameraFrustumTracker : MonoBehaviour 7 | { 8 | [SerializeField] 9 | private BoundsProperty_Writeable _cameraBoundsOutput = default; 10 | 11 | private Camera _camera; 12 | private Bounds _previousbounds = default; 13 | 14 | private float _previousScreenWidth = default; 15 | private float _previousScreenHeight = default; 16 | 17 | void Awake() 18 | { 19 | _camera = GetComponent(); 20 | 21 | _previousScreenWidth = Screen.width; 22 | _previousScreenHeight = Screen.height; 23 | 24 | var bounds = OrthographicBounds(_camera); 25 | _cameraBoundsOutput.Set(bounds); 26 | _previousbounds = bounds; 27 | } 28 | 29 | private void Update() 30 | { 31 | if (Screen.width != _previousScreenWidth || Screen.height != _previousScreenHeight) 32 | _cameraBoundsOutput.Set(OrthographicBounds(_camera)); 33 | 34 | //var newBounds = OrthographicBounds(_camera); 35 | //if (newBounds != _previousbounds) 36 | //{ 37 | // _cameraBoundsOutput.Value = newBounds; 38 | // _previousbounds = newBounds; 39 | //} 40 | } 41 | 42 | public static Bounds OrthographicBounds(Camera camera) 43 | { 44 | float screenAspect = (float)Screen.width / (float)Screen.height; 45 | float cameraHeight = camera.orthographicSize * 2; 46 | Bounds bounds = new Bounds( 47 | camera.transform.position, 48 | new Vector3(cameraHeight * screenAspect, cameraHeight, 0)); 49 | return bounds; 50 | } 51 | 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /MonoBehaviours/GUI/SelectableColorSetter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | using UnityEngine.UI; 4 | 5 | namespace LeakyAbstraction.ReactiveScriptables 6 | { 7 | [RequireComponent(typeof(Selectable))] 8 | public class SelectableColorSetter : MonoBehaviour 9 | { 10 | [SerializeField] 11 | private ColorProperty _colorVariable = default; 12 | 13 | [SerializeField] 14 | private ColorTransformation _normalColorSetter = new ColorTransformation(); 15 | [SerializeField] 16 | private ColorTransformation _highlightedColorSetter = new ColorTransformation(); 17 | [SerializeField] 18 | private ColorTransformation _pressedColorSetter = new ColorTransformation(); 19 | 20 | private Selectable _selectable => ___selectable ?? (___selectable = GetComponent()); 21 | private Selectable ___selectable; 22 | 23 | void Start() 24 | { 25 | ___selectable = GetComponent(); 26 | 27 | if (_colorVariable == null) 28 | throw new Exception("Color preset missing."); 29 | OnColorChanged(_colorVariable.Get()); 30 | } 31 | 32 | private void OnEnable() 33 | => _colorVariable.Event += OnColorChanged; 34 | 35 | private void OnDisable() 36 | => _colorVariable.Event -= OnColorChanged; 37 | 38 | private void OnColorChanged(Color color) 39 | { 40 | var colorBlock = _selectable.colors; 41 | 42 | if (_normalColorSetter.Enabled) 43 | colorBlock.normalColor = _normalColorSetter.Transform(color); 44 | if (_highlightedColorSetter.Enabled) 45 | colorBlock.highlightedColor = _highlightedColorSetter.Transform(color); 46 | if (_pressedColorSetter.Enabled) 47 | colorBlock.pressedColor = _pressedColorSetter.Transform(color); 48 | 49 | _selectable.colors = colorBlock; 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /MonoBehaviours/GameDistanceTracker.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | 4 | namespace LeakyAbstraction.ReactiveScriptables 5 | { 6 | public class GameDistanceTracker : SubscriptionHelperMonoBehaviour 7 | { 8 | [SerializeField] 9 | private FloatProperty _speedInput = default; 10 | [SerializeField] 11 | private FloatProperty_Writeable _distanceOutput = default; 12 | 13 | [SerializeField] 14 | private GameEvent _startGameEvent = default; 15 | [SerializeField] 16 | private GameEvent _stopGameEvent = default; 17 | 18 | [SerializeField] 19 | private GameEvent _pauseGameEvent = default; 20 | [SerializeField] 21 | private GameEvent _unpauseGameEvent = default; 22 | 23 | private bool _tracking; 24 | 25 | #if UNITY_EDITOR 26 | private void OnValidate() 27 | { 28 | if (GUI.changed) 29 | AddSubscriptions(); 30 | } 31 | #endif 32 | 33 | private void Start() 34 | { 35 | if (_speedInput == null) 36 | Debug.LogException(new Exception("Speed input dependency not assigned.")); 37 | 38 | if (_distanceOutput == null) 39 | Debug.LogException(new Exception("Distance output dependency not assigned.")); 40 | 41 | _distanceOutput.Set(0); 42 | 43 | AddSubscriptions(); 44 | } 45 | 46 | private void AddSubscriptions() 47 | { 48 | AddSubscription(_startGameEvent, EnableTracking); 49 | AddSubscription(_unpauseGameEvent, EnableTracking); 50 | AddSubscription(_stopGameEvent, DisableTracking); 51 | AddSubscription(_pauseGameEvent, DisableTracking); 52 | } 53 | 54 | private void EnableTracking(GameEvent sender) 55 | => _tracking = true; 56 | 57 | private void DisableTracking(GameEvent sender) 58 | => _tracking = false; 59 | 60 | private void Update() 61 | { 62 | if (_tracking) 63 | _distanceOutput.Set(_distanceOutput.Get() + Mathf.Abs(_speedInput.Get() * Time.deltaTime)); 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /_prototype/CompositeGameEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | 5 | namespace LeakyAbstraction.ReactiveScriptables.Prototyping 6 | { 7 | public class CompositeGameEvent : GameEvent 8 | { 9 | public enum Logic 10 | { 11 | All, 12 | Any 13 | } 14 | 15 | // Timeout settings? How long does an activation count? 16 | 17 | [SerializeField] 18 | private Logic _logic = Logic.All; 19 | 20 | [SerializeField] 21 | private GameEvent[] _events; 22 | 23 | private Dictionary _eventStates = new Dictionary(); 24 | private int _eventsNum = 0; 25 | private int _eventsNum_Activated = 0; 26 | 27 | #if UNITY_EDITOR 28 | private void OnValidate() 29 | { 30 | if (GUI.changed) 31 | SubscribeAll(); 32 | } 33 | #endif 34 | 35 | private void OnEnable() 36 | { 37 | SubscribeAll(); 38 | } 39 | 40 | private void SubscribeAll() 41 | { 42 | _eventStates.Clear(); 43 | 44 | foreach (var e in _events) 45 | { 46 | e.Event += OnSubEventActivate; 47 | _eventStates.Add(e, false); 48 | } 49 | 50 | _eventsNum = _eventStates.Count; 51 | } 52 | 53 | private void OnSubEventActivate(GameEvent eventSource) 54 | { 55 | if (_eventStates[eventSource] == false) 56 | { 57 | _eventStates[eventSource] = true; 58 | _eventsNum_Activated++; 59 | } 60 | 61 | // All fired? 62 | if (_eventsNum_Activated >= _eventsNum) 63 | ActivateAggregate(); 64 | } 65 | 66 | private void ActivateAggregate() 67 | { 68 | OnEvent(this); 69 | } 70 | } 71 | 72 | 73 | 74 | public class GameEventLogic : GameEvent 75 | { 76 | [Serializable] 77 | public class LogicEntity 78 | { 79 | public Logic logic; 80 | public GameEvent gameEvent; 81 | } 82 | 83 | public enum Logic 84 | { 85 | And, 86 | Or 87 | } 88 | 89 | [Header("")] 90 | 91 | [SerializeField] 92 | LogicEntity[] _logicEntities = default; 93 | 94 | private void Awake() 95 | { 96 | 97 | } 98 | 99 | private void OnEnable() 100 | { 101 | foreach (var e in _logicEntities) 102 | { 103 | e.gameEvent.Event += OnTrigger; 104 | } 105 | } 106 | 107 | private void OnTrigger(GameEvent sender) 108 | { 109 | 110 | } 111 | 112 | private void OnDisable() 113 | { 114 | 115 | } 116 | } 117 | } -------------------------------------------------------------------------------- /MonoBehaviours/AudioPlaybackModulator.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace LeakyAbstraction.ReactiveScriptables 4 | { 5 | public class AudioPlaybackModulator : MonoBehaviour 6 | { 7 | [Header("Identifier (cosmetic):")] 8 | [SerializeField] 9 | private string _modulatorName = default; 10 | 11 | [Header("Audio source to modulate:")] 12 | [SerializeField] 13 | private AudioSource _audioSource = default; 14 | 15 | [Header("Source settings:")] 16 | [SerializeField] 17 | private FloatProperty _modulationSource = default; 18 | [SerializeField] 19 | private float _sourceRangeStart = default; 20 | [SerializeField] 21 | private float _sourceRangeEnd = default; 22 | 23 | [Header("Pitch modulation:")] 24 | [SerializeField] 25 | private bool _modulatePitch = false; 26 | [SerializeField] 27 | [Range(0, 3)] 28 | private float _pitchRangeStart = 1; 29 | [SerializeField] 30 | [Range(0, 3)] 31 | private float _pitchRangeEnd = 1; 32 | 33 | [Header("Volume modulation:")] 34 | [SerializeField] 35 | private bool _modulateVolume = false; 36 | [SerializeField] 37 | [Range(0, 1)] 38 | private float _volumeRangeStart = 1; 39 | [SerializeField] 40 | [Range(0, 1)] 41 | private float _volumeRangeEnd = 1; 42 | 43 | [Header("Smoothing:")] 44 | [SerializeField] 45 | private bool _applySmoothing = false; 46 | [SerializeField] 47 | private float _smoothingTimeInSeconds = 1; 48 | 49 | [Header("Easing:")] 50 | [SerializeField] 51 | private bool _applyEasing = false; 52 | [SerializeField] 53 | private MyMath.Easing _easingType = MyMath.Easing.None; 54 | 55 | private float _previousFraction = 0; 56 | 57 | private void Start() 58 | { 59 | if (_audioSource == null || _modulationSource == null) 60 | throw new System.Exception("Dependencies not set."); 61 | 62 | if (_audioSource.clip == null) 63 | throw new System.Exception("Dependency not configured: no AudioClip selected."); 64 | } 65 | 66 | private void Update() 67 | { 68 | if (!_modulateVolume && !_modulatePitch) 69 | return; 70 | 71 | var fractionValue = _modulationSource.Get() 72 | .MapTo01(_sourceRangeStart, _sourceRangeEnd); 73 | 74 | if (_applyEasing) 75 | fractionValue = MyMath.Ease01(fractionValue, _easingType); 76 | 77 | if (_applySmoothing) 78 | fractionValue = Mathf.MoveTowards(_previousFraction, fractionValue, Time.deltaTime * (1 / _smoothingTimeInSeconds)); 79 | 80 | if (_modulateVolume) 81 | _audioSource.volume = fractionValue.Map01To(_volumeRangeStart, _volumeRangeEnd); 82 | 83 | if (_modulatePitch) 84 | _audioSource.pitch = fractionValue.Map01To(_pitchRangeStart, _pitchRangeEnd); 85 | 86 | _previousFraction = fractionValue; 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /MonoBehaviours/_Base/ColorSetterBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | 4 | namespace LeakyAbstraction.ReactiveScriptables 5 | { 6 | [Serializable] 7 | public class ColorTransformation 8 | { 9 | public bool Enabled; 10 | [Range(0, 10f)] 11 | public float HueMultiplier = 1f; 12 | [Range(0, 10f)] 13 | public float SaturationMultiplier = 1f; 14 | [Range(0, 10f)] 15 | public float ValueMultiplier = 1f; 16 | [Range(0, 10f)] 17 | public float AlphaMultiplier = 1f; 18 | 19 | public Color Transform(Color color) 20 | { 21 | Color.RGBToHSV(color, 22 | out float H, 23 | out float S, 24 | out float V); 25 | 26 | H = Mathf.Clamp01(H * HueMultiplier); 27 | S = Mathf.Clamp01(S * SaturationMultiplier); 28 | V = Mathf.Clamp01(V * ValueMultiplier); 29 | 30 | var newColor = Color.HSVToRGB(H, S, V); 31 | newColor.a = Mathf.Clamp01(color.a * AlphaMultiplier); 32 | 33 | return newColor; 34 | } 35 | } 36 | 37 | [ExecuteInEditMode] 38 | [DisallowMultipleComponent] 39 | public abstract class ColorSetterBase : SubscriptionHelperMonoBehaviour 40 | where T : UnityEngine.Object 41 | { 42 | [SerializeField] 43 | private ColorProperty _colorVariable = default; 44 | 45 | [SerializeField] 46 | private ColorTransformation _colorTransformation = new ColorTransformation(); 47 | 48 | protected T _component => ___component ?? (___component = GetComponent()); 49 | private T ___component; 50 | 51 | // To be implemented in concrete classes 52 | protected abstract void SetColor(Color color); 53 | 54 | #if UNITY_EDITOR 55 | private void OnValidate() 56 | { 57 | // Execute color update if settings are changed in the editor 58 | if (GUI.changed && _colorVariable != null) 59 | { 60 | OnColorChanged(_colorVariable.Get()); 61 | } 62 | } 63 | 64 | protected override void OnEnable() 65 | { 66 | base.OnEnable(); 67 | 68 | // Execute subscription and initial color update in Editor 69 | if (_colorVariable != null) 70 | { 71 | ___component = GetComponent(); // Pull component manually because of that bloody Unity hack that assignes some bogus shit in the Editor which is not actually the object, but still not null. 72 | AddSubscription(_colorVariable, OnColorChanged, Resume.Push); 73 | OnColorChanged(_colorVariable.Get()); 74 | } 75 | } 76 | #endif 77 | 78 | void Start() 79 | { 80 | ___component = GetComponent(); 81 | 82 | if (_colorVariable == null) 83 | { 84 | Debug.LogWarning("ColorVariable dependency not set."); 85 | return; 86 | } 87 | 88 | AddSubscription(_colorVariable, OnColorChanged, Resume.Push); 89 | OnColorChanged(_colorVariable.Get()); 90 | } 91 | 92 | private void OnColorChanged(Color color) 93 | { 94 | if (_colorTransformation.Enabled) 95 | color = _colorTransformation.Transform(color); 96 | 97 | SetColor(color); 98 | } 99 | } 100 | } -------------------------------------------------------------------------------- /_prototype/InterfaceWrapping.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using UnityEngine; 5 | 6 | namespace LeakyAbstraction.ReactiveScriptables.Prototyping 7 | { 8 | 9 | // 1) Resolve dependencies by defining services as proper abstractions; i.e. interfaces 10 | // Still doesn't resolve the dependency on these enums, but that will probably stay as it is 11 | public interface ISoundPlaybackService 12 | { 13 | AudioSource PlaySound(GameSound soundType, Action playFinishedCallback = null); 14 | AudioSource PlaySoundPositioned(GameSound soundType, Vector3 soundPosition, Action playFinishedCallback = null); 15 | AudioSource PlaySoundPositioned(GameSound soundType, Transform targetTransform, Action playFinishedCallback = null); 16 | AudioSource PlaySound(GameSound soundType, float volumeMultiplier, float pitchMultiplier, Action playFinishedCallback = null); 17 | AudioSource PlaySoundPositioned(GameSound soundType, float volumeMultiplier, float pitchMultiplier, Vector3 soundPosition, Action playFinishedCallback = null); 18 | AudioSource PlaySoundPositioned(GameSound soundType, float volumeMultiplier, float pitchMultiplier, Transform targetTransform, Action playFinishedCallback = null); 19 | } 20 | 21 | public interface IAnimationLookup 22 | { 23 | Animation Get(GameAnimation animationType); 24 | } 25 | // Etc. 26 | 27 | // 2) Create service interface broadcast containers for injection into dependent components 28 | // 'Property' sounds horrible here - rename to Container perhaps? Sigh... 29 | // 'Writeable' also sound crap here - Settable would be more universal? 30 | public class SoundServiceProperty : GameProperty 31 | { } 32 | 33 | public class SoundServiceProperty_Writeable : SoundServiceProperty 34 | { 35 | new public void Set(ISoundPlaybackService service) 36 | => SetAndNotify(service); 37 | } 38 | 39 | public class AnimationLookupProperty : GameProperty 40 | { } 41 | 42 | public class AnimationLookupProperty_Writeable : AnimationLookupProperty 43 | { 44 | new public void Set(IAnimationLookup animationLookup) 45 | => SetAndNotify(animationLookup); 46 | } 47 | // Etc. 48 | 49 | // Component that couples servicecomponents to service interface containers in a centralized way, 50 | // and sends them out (injecting them) into the dependent components in the scene 51 | public class ServiceCoupler : MonoBehaviour 52 | { 53 | // Editor-exposed field to assign component to 54 | // Normal MonoBehaviour component, for Editor-assignability (and access to MonoBehaviour-based services) 55 | //[SerializeField] 56 | //private SoundManager _soundManager = default; 57 | 58 | // Better yet: Instead of assigning, try to directly instantiate service components, i.e. make them non-MonoBehaviour, 59 | // inject the useful MonoBehaviour base class members (transform, gameObject) into them as an interface, e.g. IUnityGateway. 60 | // This would mean this class can serve as sort of a 'composition root'. 61 | // There doesn't appear to be any benefit of Editor-assignability here for service components; no reason for assigning 'different versions', etc. 62 | 63 | // Editor-exposed field to assign the broadcasting container that will send the service to other components as an interface 64 | // All need to be tied to specific service component representations 65 | [SerializeField] 66 | private SoundServiceProperty_Writeable _soundServiceOut = default; 67 | 68 | // Nope 69 | //[Serializable] 70 | //public class CoupledEntity 71 | //{ 72 | // // Run assignability checks in OnValidate()? 73 | // public Component Component; 74 | // public ScriptableObject ScriptableObject; 75 | //} 76 | } 77 | 78 | // new name idea for submodule: ScriptableInject? sigh... 79 | 80 | public interface IUnityGateway 81 | { 82 | Transform transform { get; } 83 | GameObject gameObject { get; } 84 | T GetComponent(); 85 | } 86 | 87 | } -------------------------------------------------------------------------------- /Utility/MyMath.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using UnityEngine; 5 | 6 | namespace LeakyAbstraction.ReactiveScriptables 7 | { 8 | public static class MyMath 9 | { 10 | public enum Easing 11 | { 12 | None, 13 | SmoothStart2, 14 | SmoothStart3, 15 | SmoothStart4, 16 | SmoothStop2, 17 | SmoothStop3, 18 | SmoothStop4 19 | } 20 | 21 | private static Dictionary> _easingMap = new Dictionary>() 22 | { 23 | [Easing.None] = (x) => x, 24 | [Easing.SmoothStart2] = (x) => x * x, 25 | [Easing.SmoothStart3] = (x) => x * x * x, 26 | [Easing.SmoothStart4] = (x) => x * x * x * x, 27 | [Easing.SmoothStop2] = (x) => 1 - ((1 - x) * (1 - x)), 28 | [Easing.SmoothStop3] = (x) => 1 - ((1 - x) * (1 - x) * (1 - x)), 29 | [Easing.SmoothStop4] = (x) => 1 - ((1 - x) * (1 - x) * (1 - x) * (1 - x)), 30 | }; 31 | 32 | public static float Map(this float x, float xMin, float xMax, float outputMin, float outputMax) 33 | => x.MapTo01(xMin, xMax) 34 | .Map01To(outputMin, outputMax); 35 | 36 | public static float Map(this float x, float xMin, float xMax, float outputMin, float outputMax, Easing easingType) 37 | => x.MapTo01(xMin, xMax) 38 | .Ease01(easingType) 39 | .Map01To(outputMin, outputMax); 40 | 41 | public static float Ease01(this float x, Easing easingType) 42 | => _easingMap[easingType].Invoke(x); 43 | 44 | /// 45 | /// Converts a float to its 0-1 position in the given range. 46 | /// 47 | public static float MapTo01(this float x, float xMin, float xMax) 48 | => Mathf.Clamp01(MapTo(x, xMin, xMax)); 49 | 50 | /// 51 | /// Converts a float to 0-1 its position in the given range. 52 | /// If the float is outside the range, returns less than 0 or greater than 1. 53 | /// 54 | public static float MapTo(this float x, float xMin, float xMax) 55 | => (x - xMin) / (xMax - xMin); 56 | 57 | /// 58 | /// Converts float in 0-1 range to another range. 59 | /// 60 | public static float Map01To(this float x, float outMin, float outMax) 61 | => (Mathf.Clamp01(x) * (outMax - outMin)) + outMin; 62 | 63 | public static float Invert01(this float x) 64 | => Math.Abs(x - 1); 65 | 66 | // Output stuff at given stage while chaining, without having to break the chain. 67 | public static float OutputThis(this float x, out float output) 68 | => output = x; 69 | 70 | public static bool FastApproximately(float a, float b, float threshold) 71 | { 72 | return ((a - b) < 0 ? ((a - b) * -1) : (a - b)) <= threshold; 73 | 74 | // return Math.Abs(a-b) <= threshold; 75 | // return ((a < b) ? (b - a) : (a - b)) <= threshold; 76 | } 77 | 78 | /* 79 | // (1-(1-x)^2) + (0.2 * ((1-(1-x)^3) - (1-(1-x)^2))) 80 | // x^2 + (0.2 * (x^3 - x^2)) 81 | // smoothstep: x^2 + (-2 * (x^3 - x^2)) 82 | public static float Mix(float a, float b, float weightB, float t) 83 | { 84 | return a + (weightB * (b - a)); 85 | } 86 | 87 | public static float Crossfade(float a, float b, float t) 88 | { 89 | return a + t * (b - a); 90 | // (1-t)*a + (t)*b 91 | } 92 | 93 | public static float Scale(float x, float t) 94 | { 95 | return t * x; 96 | } 97 | 98 | public static float ReverseScale(float x, float t) 99 | { 100 | return (1 - t) * x; 101 | } 102 | 103 | public static float Arch(float t) 104 | { 105 | // Scale(Flip(t)) 106 | return t * (1 - t); 107 | } 108 | 109 | public static float SmoothStartArch(float x) 110 | { 111 | // Scale(Arch2, t) 112 | return x * x * (1 - x); 113 | } 114 | 115 | public static float NormalizedBezier3(float b, float c, float t) 116 | { 117 | float s = 1 - t; 118 | float t2 = t * t; 119 | float s2 = s * s; 120 | float t3 = t2 * t; 121 | return (3 * b * s2 * t) + (3 * c * s * t2) + (t3); 122 | } 123 | */ 124 | } 125 | } -------------------------------------------------------------------------------- /MonoBehaviours/_Base/SubscriptionHelperMonoBehaviour.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | 5 | namespace LeakyAbstraction.ReactiveScriptables 6 | { 7 | /// 8 | /// MonoBehaviour extension with helper features for subcribing to and unsubscribing from ScriptableObject-based GameEvent and GameProperty objects. 9 | /// 10 | public abstract class SubscriptionHelperMonoBehaviour : MonoBehaviour 11 | { 12 | /// 13 | /// Specifies what should happen when the GameObject is re-enabled: 14 | /// 'Push' means that the event callback will be instantly called with the current value 15 | /// 16 | protected enum Resume 17 | { 18 | Push, 19 | NoPush 20 | } 21 | 22 | //private List<(Action Subscribe, Action Unsubscribe, Action Push)> _actionSets = new List<(Action, Action, Action)>(); 23 | private Dictionary _actionMap = new Dictionary(); 24 | 25 | /// 26 | /// Subscribes to a GameEvent, and handles all relevant responsibilities, i.e. automatically suspends subscription while the GameObject is disabled. 27 | /// 28 | /// The GameEvent to subscribe to. 29 | /// The method to be called when the event fires. 30 | protected void AddSubscription(GameEvent gameEvent, Action callback) 31 | => AddSubscription_internal(gameEvent, callback, null); 32 | 33 | /// 34 | /// Subscribes to a GameProperty, and handles all relevant responsibilities, i.e. automatically suspends subscription while the GameObject is disabled. 35 | /// 36 | /// The GameProperty to subscribe to. 37 | /// The method to be called when the change notification event fires. 38 | /// 'Push' means the current value will be instantly sent when the GameObject becomes enabled after it was disabled. Can be useful for making sure that the component is up to date with the current game state. 39 | protected void AddSubscription(GameProperty gameProperty, Action callback, Resume resumeBehaviour) 40 | { 41 | Action pushAction = null; 42 | if (resumeBehaviour == Resume.Push) 43 | pushAction = () => callback(gameProperty.Get()); 44 | 45 | AddSubscription_internal(gameProperty, callback, pushAction); 46 | } 47 | 48 | private void AddSubscription_internal(IEventSource eventSource, Action callback, Action pushAction) 49 | { 50 | // Can be null if a field of this type is not assigned in Editor 51 | if (eventSource == null) 52 | { 53 | Debug.LogWarning($"Cannot add subscription, event source is Null. Field probably not assigned in Editor (which might be normal)."); 54 | return; 55 | } 56 | 57 | // Already subscribed to given event source 58 | if (_actionMap.ContainsKey(eventSource)) 59 | return; 60 | 61 | eventSource.Event += callback; 62 | 63 | // Store actions for subsequent subscription suspend and resume 64 | { 65 | _actionMap.Add( 66 | key: eventSource, 67 | value: 68 | //TODO: Factor out closure allocations, ideally 69 | (Subscribe: () => eventSource.Event += callback, 70 | Unsubscribe: () => eventSource.Event -= callback, 71 | Push: pushAction) 72 | ); 73 | } 74 | } 75 | 76 | /// 77 | /// Entirely removes the subscription to a given GameProperty or GameEvent. 78 | /// Don't use it for OnDisable/OnEnable safety; this functionality is automatic. 79 | /// 80 | protected void RemoveSubscription(IEventSource eventSource) 81 | => RemoveSubscription_internal(eventSource); 82 | 83 | /// 84 | /// If you override this Unity Magic™ method, BE SURE TO CALL base.OnEnable() from your method 85 | /// 86 | protected virtual void OnEnable() 87 | => ResumeSubscriptions(); 88 | 89 | /// 90 | /// If you override this Unity Magic™ method, BE SURE TO CALL base.OnDisable() from your method 91 | /// 92 | protected virtual void OnDisable() 93 | => UnsubscribeAll(); 94 | 95 | /// 96 | /// If you override this Unity Magic™ method, BE SURE TO CALL base.OnDestroy() from your method 97 | /// 98 | protected virtual void OnDestroy() 99 | => UnsubscribeAll(); // No need for record removal, right? It will be destroyed after all. 100 | 101 | private void RemoveSubscription_internal(object key) 102 | { 103 | if (!_actionMap.TryGetValue(key, 104 | out var actionSet)) // Out 105 | return; 106 | 107 | actionSet.Unsubscribe(); 108 | _actionMap.Remove(key); 109 | } 110 | 111 | private void ResumeSubscriptions() 112 | { 113 | foreach (var e in _actionMap) // Iterate over KVPs instead of .Values to avoid heap allocation of ValueCollection 114 | { 115 | e.Value.Subscribe(); 116 | e.Value.Push?.Invoke(); // Push delegate is non-null only if push was specifically requested at subscription 117 | } 118 | } 119 | 120 | private void UnsubscribeAll() 121 | { 122 | foreach (var e in _actionMap) 123 | e.Value.Unsubscribe(); 124 | } 125 | } 126 | } -------------------------------------------------------------------------------- /MonoBehaviours/GUI/GraphicFadeBehaviour.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using UnityEngine; 4 | using UnityEngine.UI; 5 | 6 | namespace LeakyAbstraction.ReactiveScriptables 7 | { 8 | [RequireComponent(typeof(Graphic))] 9 | public class GraphicFadeBehaviour : SubscriptionHelperMonoBehaviour 10 | { 11 | enum StartupAction 12 | { 13 | None, 14 | FadeIn, 15 | FadeOut 16 | } 17 | 18 | [Header("Startup settings:")] 19 | [SerializeField] 20 | StartupAction _startupAction = StartupAction.None; 21 | 22 | [SerializeField] 23 | [Range(0, 1)] 24 | private float _startupFadeValue = 0; 25 | 26 | [Header("No fade behavior:")] 27 | [SerializeField] 28 | private bool _disableGraphicOnZeroFade = true; 29 | 30 | [Header("Fade duration:")] 31 | [SerializeField] 32 | [Range(0.1f, 2)] 33 | private float _duration = 0.5f; 34 | [SerializeField] 35 | private bool _constantDuration = true; 36 | 37 | [Header("Fade In settings:")] 38 | [SerializeField] 39 | private GameEvent _fadeInOn = default; 40 | 41 | [SerializeField] 42 | [Range(0, 1)] 43 | private float _fadeInTarget = 1; 44 | 45 | [Header("Fade Out settings:")] 46 | [SerializeField] 47 | private GameEvent _fadeOutOn = default; 48 | 49 | [SerializeField] 50 | [Range(0, 1)] 51 | private float _fadeOutTarget = 0; 52 | 53 | [Header("Invoke when fade finished:")] 54 | [SerializeField] 55 | private GameEvent_Invokable _onFadeFinished = default; 56 | 57 | private Graphic _graphic; 58 | private Coroutine _activeFadeJob; 59 | private bool _graphicDisabled = false; 60 | 61 | private void Awake() 62 | { 63 | _graphic = GetComponent(); 64 | 65 | // Start subscription handling of events we're interested in 66 | AddSubscription(_fadeInOn, OnFadeIn); 67 | AddSubscription(_fadeOutOn, OnFadeOut); 68 | 69 | // Push initial value 70 | SetAlpha(_startupFadeValue); 71 | 72 | DoStartupAction(); 73 | } 74 | 75 | private void DoStartupAction() 76 | { 77 | switch (_startupAction) 78 | { 79 | case StartupAction.None: 80 | return; 81 | case StartupAction.FadeIn: 82 | if (_startupFadeValue != _fadeInTarget) 83 | FadeIn(); 84 | break; 85 | case StartupAction.FadeOut: 86 | if (_startupFadeValue != _fadeOutTarget) 87 | FadeOut(); 88 | break; 89 | default: 90 | throw new Exception($"'{nameof(StartupAction)}' Enum value '{_startupAction}' unknown."); 91 | } 92 | } 93 | 94 | private void OnFadeIn(GameEvent sender) 95 | => FadeIn(); 96 | 97 | public void FadeIn() 98 | => Fade(_fadeInTarget); 99 | 100 | private void OnFadeOut(GameEvent sender) 101 | => FadeOut(); 102 | 103 | public void FadeOut() 104 | => Fade(_fadeOutTarget); 105 | 106 | // Only here for UnityEvent binding 107 | public void Fade(float targetAlpha) 108 | => Fade(targetAlpha, null); 109 | 110 | public void Fade(float targetAlpha, Action callbackOnFinished = null) 111 | { 112 | // Interrupt running job, if any; to start approaching our new target 113 | if (_activeFadeJob != null) 114 | StopCoroutine(_activeFadeJob); 115 | 116 | // Make sure value is in valid range 117 | targetAlpha = Mathf.Clamp01(targetAlpha); 118 | 119 | float alphaDifference = Mathf.Abs(targetAlpha - _graphic.color.a); 120 | 121 | // No difference, nothing to do 122 | if (alphaDifference == 0) 123 | return; 124 | 125 | // Decrease duration to fraction of full, if requested 126 | float duration = _duration; 127 | if (!_constantDuration) 128 | duration = _duration * alphaDifference; 129 | 130 | _activeFadeJob = StartCoroutine( 131 | FadeJob(targetAlpha, duration, callbackOnFinished) 132 | ); 133 | } 134 | 135 | private IEnumerator FadeJob(float targetAlpha, float duration, Action callback) 136 | { 137 | float newAlpha = 0; 138 | float originalAlpha = _graphic.color.a; 139 | float elapsedTime = 0; 140 | 141 | do 142 | { 143 | elapsedTime += Time.deltaTime; 144 | 145 | newAlpha = elapsedTime 146 | .MapTo01(0, duration) 147 | .Map01To(originalAlpha, targetAlpha); 148 | 149 | SetAlpha(newAlpha); 150 | 151 | yield return null; 152 | } 153 | while (newAlpha != targetAlpha); 154 | 155 | // Job finished, clear the cached instance 156 | // Not the responsibily of this method; shouldn't be here 157 | _activeFadeJob = null; 158 | // Same here, really 159 | callback?.Invoke(); 160 | _onFadeFinished?.Invoke(); 161 | } 162 | 163 | public void SetAlpha(float value) 164 | { 165 | // Enable if previously was disabled, and now we're setting non-zero alpha 166 | if (value != 0 && _graphicDisabled) 167 | SetImage(enabled: true); 168 | 169 | var color = _graphic.color; 170 | color.a = value; 171 | _graphic.color = color; 172 | 173 | // Disable when alpha is 0, if requested 174 | if (value == 0 && _disableGraphicOnZeroFade) 175 | SetImage(enabled: false); 176 | 177 | void SetImage(bool enabled) 178 | { 179 | _graphic.enabled = enabled; 180 | _graphicDisabled = !enabled; 181 | } 182 | } 183 | } 184 | } -------------------------------------------------------------------------------- /MonoBehaviours/GUI/Interactable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using UnityEngine.Events; 5 | using UnityEngine.EventSystems; 6 | using UnityEditor; 7 | using System.Collections; 8 | 9 | public enum GameAnimation 10 | { 11 | None, 12 | AppearByUpscale, 13 | DisappearByDownscale, 14 | ExpandSnap, 15 | HorizontalSnap, 16 | FadeIn, 17 | FadeOut, 18 | ExpandSnapStrong 19 | } 20 | 21 | namespace LeakyAbstraction.ReactiveScriptables 22 | { 23 | /// 24 | /// Adds configurable interactability to objects, primarily designed for UI elements. 25 | /// 26 | public class Interactable : MonoBehaviour, IPointerClickHandler, IPointerDownHandler, IPointerUpHandler, IBeginDragHandler, IEndDragHandler 27 | { 28 | [Serializable] 29 | public class Interaction 30 | { 31 | [Header("Interaction sound and animation:")] 32 | public GameSound sound; 33 | public GameAnimation animation; 34 | 35 | [Header("Delay execution of actions:")] 36 | public bool waitForSound; 37 | public bool waitForAnimation; 38 | 39 | [Header("Actions to execute:")] 40 | public GameEvent_Invokable gameEvent; 41 | public UnityEvent unityEvent; 42 | } 43 | 44 | [Header("Pointer Down:")] 45 | [SerializeField] 46 | private Interaction _pointerDown = new Interaction(); 47 | 48 | [Header("Pointer Click:")] 49 | [SerializeField] 50 | private Interaction _pointerClick = new Interaction(); 51 | 52 | [Header("Pointer Up:")] 53 | [SerializeField] 54 | private Interaction _pointerUp = new Interaction(); 55 | 56 | [Header("Pointer Drag Begin:")] 57 | [SerializeField] 58 | private Interaction _pointerDragBegin = new Interaction(); 59 | 60 | [Header("Pointer Drag End:")] 61 | [SerializeField] 62 | private Interaction _pointerDragEnd = new Interaction(); 63 | 64 | // Return Animation component, or set and return it if null 65 | private Animation _animation => ___animation ?? (___animation = GetAnimationComponent()); 66 | private Animation ___animation; 67 | 68 | private Interaction[] _interactions; 69 | private HashSet _registeredClips = new HashSet(); 70 | private bool _awaitedExecutionRunning = false; 71 | 72 | #if UNITY_EDITOR 73 | private void OnValidate() 74 | { 75 | // If we modify the settings in the Inspector while playing, animations need to be re-registered to ensure they can play 76 | if (GUI.changed && EditorApplication.isPlaying && AnyAnimationRequested()) 77 | { 78 | Debug.Log($"{nameof(Interactable)} detected GUI change. Re-registering AnimationClips."); 79 | RegisterAnimationClips(); 80 | } 81 | } 82 | #endif 83 | 84 | private void Start() 85 | { 86 | // IMPORTANT: Extend this if new interactions are added to the class. 87 | // This array facilitates operating executed on all interactions, without having to hardcode them individually in multiple places. 88 | _interactions = new Interaction[] { 89 | _pointerDown, 90 | _pointerClick, 91 | _pointerUp, 92 | _pointerDragBegin, 93 | _pointerDragEnd 94 | }; 95 | 96 | if (AnyAnimationRequested()) 97 | RegisterAnimationClips(); 98 | } 99 | 100 | private Animation GetAnimationComponent() 101 | { 102 | Animation animation = GetComponent(); 103 | 104 | if (animation == null) 105 | animation = gameObject.AddComponent(); 106 | 107 | return animation; 108 | } 109 | 110 | /// 111 | /// Register animations clips in Animation component. 112 | /// Unfortunately this has to be done, because setting the default clip doesn't add it automatically. 113 | /// 114 | private void RegisterAnimationClips() 115 | { 116 | foreach (var i in _interactions) 117 | TryRegister(i); 118 | 119 | void TryRegister(Interaction interaction) 120 | { 121 | if (interaction.animation == GameAnimation.None) 122 | return; 123 | 124 | var clip = TestLookupBehaviour.Instance.Get(interaction.animation); 125 | 126 | if (!_registeredClips.Contains(clip)) 127 | { 128 | //Debug.Log($"Registering Animation Clip '{clip.name}'"); 129 | _animation.AddClip(clip, clip.name); 130 | _registeredClips.Add(clip); 131 | } 132 | } 133 | } 134 | 135 | public void OnPointerClick(PointerEventData eventData) 136 | => ExecuteInteraction(_pointerClick); 137 | 138 | public void OnPointerDown(PointerEventData eventData) 139 | => ExecuteInteraction(_pointerDown); 140 | 141 | public void OnPointerUp(PointerEventData eventData) 142 | => ExecuteInteraction(_pointerUp); 143 | 144 | public void OnBeginDrag(PointerEventData eventData) 145 | => ExecuteInteraction(_pointerDragBegin); 146 | 147 | public void OnEndDrag(PointerEventData eventData) 148 | => ExecuteInteraction(_pointerDragEnd); 149 | 150 | private void ExecuteInteraction(Interaction interaction) 151 | { 152 | AudioSource audioSource = null; 153 | if (interaction.sound != GameSound.None) 154 | audioSource = SoundManager.Instance.PlaySound(interaction.sound); 155 | 156 | if (interaction.animation != GameAnimation.None) 157 | PlayAnimation(TestLookupBehaviour.Instance.Get(interaction.animation)); 158 | 159 | // If any action is requested, and currently no delayed execution is running 160 | if (!_awaitedExecutionRunning && (interaction.gameEvent != null || interaction.unityEvent != null)) 161 | { 162 | // If any waiting is requested 163 | if (interaction.waitForAnimation || interaction.waitForSound) 164 | StartCoroutine(ExecuteActionsAwaited(interaction, _animation, audioSource)); 165 | else 166 | ExecuteActions(interaction); 167 | } 168 | } 169 | 170 | //TODO: Consider encapsulating interaction-processing into a separate class, or make AwaitedExecutionRunning checks per-interaction. 171 | private IEnumerator ExecuteActionsAwaited(Interaction interaction, Animation animation, AudioSource audioSource) 172 | { 173 | _awaitedExecutionRunning = true; 174 | 175 | //TODO: Consider less simplistic (but similarly dependable) waiting implementations, preferably without polling 176 | if (interaction.waitForAnimation && animation != null) 177 | { 178 | do 179 | { 180 | yield return null; 181 | } while (animation.isPlaying); 182 | } 183 | 184 | if (interaction.waitForSound && audioSource != null) 185 | while (audioSource.isPlaying) yield return null; 186 | 187 | ExecuteActions(interaction); 188 | 189 | _awaitedExecutionRunning = false; 190 | } 191 | 192 | private void ExecuteActions(Interaction interaction) 193 | { 194 | if (interaction.gameEvent != null) 195 | interaction.gameEvent.Invoke(); 196 | 197 | if (interaction.unityEvent != null) 198 | interaction.unityEvent.Invoke(); 199 | } 200 | 201 | private void PlayAnimation(AnimationClip clip) 202 | { 203 | //TODO: Make this less naive 204 | if (_animation.isPlaying) 205 | return; 206 | 207 | _animation.clip = clip; 208 | _animation.Play(); 209 | } 210 | 211 | private bool AnyAnimationRequested() 212 | { 213 | bool any = false; 214 | foreach (var i in _interactions) 215 | any = any || i.animation != GameAnimation.None; 216 | 217 | return any; 218 | } 219 | } 220 | } -------------------------------------------------------------------------------- /.github/README.md: -------------------------------------------------------------------------------- 1 | # ScriptableObjects-based game architectural scaffolding for Unity3D game development 2 | ### Inspired by: [Unite Austin 2017 - Game Architecture with Scriptable Objects](https://www.youtube.com/watch?v=raQ3iHhE_Kk) 3 | --- 4 | **State:** Work in progress, but fully usable; not unit-tested, but master should be stable. 5 | 6 | **Requirements:** Tested in Unity 2018.3, requires scripting runtime set to '.Net 4.x equivalent' (uses C# 7 features). 7 | 8 | --- 9 | 10 | **Sadly, this repo, like all my Unity3D repos, is sort of abandoned at the moment. I had plans expanding and polishing this scaffolding project, to make it more feature-rich and intuitive to use for others, but the reality is that I had to start looking for an actual paying job, and Unity3D didn't seem like a viable option. But I'll most likely continue working on it a bit later.** 11 | 12 | Essentially this is an adapted/extended version of the ScriptableObject-based architectural approach introduced in the Unite talk linked above. 13 | 14 | I will try to show with some example scenarios and pictures why I think this architecture works pretty well for a lot of common problems. 15 | 16 | This is used as a Git submodule in my project; in 2-3 classes I still need to work out some solution to a few dependencies on my sound and animation manager (notably in [Interactable.cs](https://github.com/baratgabor/Unity3D-ReactiveScriptables/blob/master/MonoBehaviours/GUI/Interactable.cs)). However, you can actually find the [SoundManager here on GitHub](https://github.com/baratgabor/Unity3D-SoundManager). The solution I'll implement is probably injecting these dependencies as interfaces wrapped into `GameProperty` (one of the classes this 'framework' uses). 17 | 18 | *(FYI the naming is a real struggle for me here. I spent literally like 2 days thinking about how to call this module, plus how to call the state holding class, etc. I didn't like the original GameVariable name, and I wanted to differentiate its use from the normal variables/fields we use (that's why I also went with `Get()` and `Set()` instead of property accessors). So it's entirely possible that I'll rename a bunch of things.)* 19 | 20 | ### Main selling point: 21 | 22 | - Powerful Editor-configurability for teams with non-programmer workers, e.g. artists and designers. Because obviously it would be much easier to just use for example a message bus / event aggregator to send payloads to listeners. 23 | 24 | ### Suitable for: 25 | 26 | - Injecting pre-defined data or configuration into `MonoBehaviour` components. 27 | - Exchanging pre-defined types of data between `MonoBehaviour` components, either through polling or event subscription, without creating hard references between them. 28 | - Creating reactive, or event-driven, workflow between components with events and change notifications. (But for now don't expect real reactive features, like map, filter, etc. ;)) 29 | - Driving GUI behaviour and interactivity. 30 | 31 | ### Probably not suitable for: 32 | 33 | - Highly complex games, because the data types are really fine-grained, and if you need to create hundreds of them, that would probably get messy. However, you can easily extend this system with your own, less fine grained types. 34 | - Scenarios where you need to create and propagate state dynamically, since this is all about using pre-defined `ScriptableObject` instances. Of course in a lot of cases what you actually need is to hook the components onto a communication channel, and these channels are usually pre-definable. 35 | 36 | ## Example Usage Scenario: Handling item pickups / projectile impacts 37 | 38 | The common scenario of item pickups (e.g. coins in platformers), or projectile impacts in 3D games. This often requires triggering `ParticleSystems`, `AudioSources`, and updating game statistics / UI, especially in more polished projects. 39 | 40 | ### • Simplistic Unity approach: 41 | 42 | ![GitHub Logo](/.github/ExampleScenario1-Simplistic.png) 43 | 44 | **Workflow:** 45 | 46 | - You add a `ParticleSystem` and an `AudioSource` component directly to your `GameObject`. 47 | - You reference various other components via e.g. singletons, Editor-associations or `GetComponent()`, for directly calling methods on them. 48 | - On trigger/collision enter you hide the `GameObject`'s renderer, execute all the necessary calls on the referenced components, and destroy/disable the `GameObject` in a delayed manner (since the `ParticleSystem` and `AudioSource` still need to finish). 49 | 50 | **Key characteristics:** 51 | 52 | - Very simple and easy to learn approach, which doesn't require any framework, or understanding of software architecture. 53 | - The `GameObject` itself assumes responsibility for everything that needs to happen when it's triggered or collided into. 54 | - Many components, e.g. `ParticleSystems` and `AudioSources`, are duplicated on each `GameObject` instance. 55 | - Even simple `GameObjects` and prefabs start to feel tangled and bloated as you add more polish to the game and include particle effects, sounds, UI updates, game statistics, etc. 56 | 57 | ### • ScriptableObject-based event-driven approach: 58 | 59 | ![GitHub Logo](/.github/ExampleScenario1-EventDriven.png) 60 | 61 | **Workflow:** 62 | 63 | - *(Required only first time)* You create a `struct` or `class` that will contain all the data relevant to your event (or skip this, and just use a primitive type, if that's enough). 64 | - *(Required only first time)* You create a `ScriptableObject`-based asset that will serve as an Editor-assignable send/receive channel for your event data. 65 | - You assign this created `ScriptableObject`-based communication asset to all your `GameObject` script (as *invokable*), and to all other components that want to listen and react (as *readonly*). 66 | - Your script simply invokes the event, which notifies all subscribers. 67 | 68 | **Key characteristics:** 69 | 70 | - Requires more work and understanding to set up first. But, after the initial setup it's easy to use for non-programmers, because many aspects of game logic are Editor-declarable and -configurable. 71 | - The `GameObject` has a single responsibility, and the other components which subscribe to this event take care of their own relevant responsibility. 72 | - The number of components on each `GameObject` instances can be minimized; often even `ParticleSystems` and `AudioSources` can be removed and handled in a separate single component which is responsible for reacting to events at world coordinates. 73 | - Your `GameObjects` and prefabs can remain very simple, even in a game that is highly polished with dozens of various audio/visual/UI reactions to events. 74 | - Easy to create general, reusable components. For example you can create a `ParticleSystem` trigger component to which you can associate any event in the Editor. Or a `Text` updater component that displays the content of the event payload, counts and displays the number of event invocations, etc. These simple, reusable components are easy to understand, and anybody can use and combine them to add simpler game features. 75 | 76 | ## Main Features 77 | 78 | ### Differentiated read-only and writeable use 79 | - You can create read-only and writeable instances of the data-holding classes. 80 | - You can associate your writeable data classes (and invokable event classes) with readonly fields in the Editor. 81 | - This means you can express clearly the intent that an event or data is an input of your component, and have automaticly enforced write-protection. 82 | - Essentially you can avoid the situation of creating mutable shared state in your architecture. Which almost always leads to problems. 83 | - My recommendation is that for each writeable instance you should have a single component that writes to it, and the rest should only listen. 84 | 85 | ### Built-in change notifiations 86 | - Not just the event classes, but the data-holding classes have an event too that notifies of value changes. 87 | - Works sort of similarly to the `INotifyPropertyChanged` interface in .Net, in the sense that you can listen to changes related to a unit of data, and react in an event-driven manner. 88 | 89 | ### Easily extendable generic base classes 90 | - The common types, e.g. `float`, `int`, `Vector3`, `Bounds` already have built-in concrete classes, but you can also create your own classes, including ones based on your custom types. Basically this is all you need to create a concrete type that you can use in the Editor (as we know, the Editor doesn't support generics, so you need to create non-generic derived classes): 91 | 92 | ```csharp 93 | [CreateAssetMenu] 94 | public class BoundsProperty : GameProperty 95 | { } 96 | ``` 97 | 98 | ### Subscription helper MonoBehaviour extension class 99 | - If you derive your components from `SubscriptionHelperMonoBehaviour` instead of `MonoBehaviour`, you can add subscriptions easily by invoking the `AddSubscription()` method. 100 | - This automatically handles all subscription-related responsibilities, i.e. unsubscribing in `OnDisable()`, resubscribing in `OnEnable()`, and again unsubscribing in `OnDestroy()`. 101 | - Supports all data types automatically, including your own custom made types which derive from `GameEvent` or `GameProperty`. 102 | - (Still need to refactor this, because it uses closing/allocating lambdas currently.) 103 | 104 | ### Built-in, ready to use generic MonoBehaviour components 105 | - I included plenty of `MonoBehaviour` components I made over the last few weeks for myself; generally these are: 106 | - Component triggers and event counters 107 | - **GUI interactivity helpers** for replacing the inflexible `Button` component 108 | - **GUI skinning helpers** for defining and associating colors which synchronize automatically (even in Edit mode). 109 | - Includes color transformation capability, for defining H, S, V, A transformation on the received color. 110 | - Based And a generic base class you can use for making color setters. 111 | - **Audio playback modulation** for creating dynamic car engine, etc. sounds based on a float (e.g. speed) 112 | 113 | ### Lightweight, not too OOP 114 | - Has a 2-3 levels deep inheritance hierarchy here and there, but generally it's not overstructured and overcomplicated. I'd be glad to rely more on interfaces, composition and abstractions, but sadly it seems nearly impossible in Unity (if you want to keep things Editor-compatible). 115 | --------------------------------------------------------------------------------