diff --git a/README.md b/README.md index fc5332f..fbd9a55 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ This template contains the following: | [Best Practices: Coding Standards](https://www.samuelasherrivello.com/best-practices) | Guidelines for consistent code style and format. | | [Best Practices: Project Structure](https://www.samuelasherrivello.com/best-practices) | Recommended project structure and organization. | | [Cinemachine](https://docs.unity3d.com/Packages/com.unity.cinemachine@latest) | Advanced camera system for dynamic shots. | +| [Cysharp/R3](https://github.com/Cysharp/R3) | Reactive Extensions for Unity providing ReactiveProperty and observables. | | [Physics](https://docs.unity3d.com/Manual/PhysicsSection.html) | Physics simulation for 2D and 3D games. | | [ProBuilder](https://docs.unity3d.com/Packages/com.unity.probuilder@latest) | 3D modeling and level design toolset. | | [Rendering: Post-Processing](https://docs.unity3d.com/Packages/com.unity.postprocessing@latest) | Visual effects like color grading and bloom. | diff --git a/Unity/Assets/Scripts/Runtime/RMC.MyProject.Runtime.asmdef b/Unity/Assets/Scripts/Runtime/RMC.MyProject.Runtime.asmdef index 216cfc5..d54ca86 100644 --- a/Unity/Assets/Scripts/Runtime/RMC.MyProject.Runtime.asmdef +++ b/Unity/Assets/Scripts/Runtime/RMC.MyProject.Runtime.asmdef @@ -4,7 +4,8 @@ "references": [ "GUID:75469ad4d38634e559750d17036d5f7c", "GUID:8d0b5587a0a8ae741afbfe3a79c2872a", - "GUID:9f522e882e6bd48429f616f259fa6318" + "GUID:9f522e882e6bd48429f616f259fa6318", + "Cysharp.R3" ], "includePlatforms": [], "excludePlatforms": [], diff --git a/Unity/Assets/Scripts/Runtime/RMC/[MyProject]/Scenes/Scene01_Intro.cs b/Unity/Assets/Scripts/Runtime/RMC/[MyProject]/Scenes/Scene01_Intro.cs index 9d1220a..5b66549 100644 --- a/Unity/Assets/Scripts/Runtime/RMC/[MyProject]/Scenes/Scene01_Intro.cs +++ b/Unity/Assets/Scripts/Runtime/RMC/[MyProject]/Scenes/Scene01_Intro.cs @@ -1,4 +1,5 @@ using System.Threading.Tasks; +using Cysharp.R3; using RMC.Audio; using RMC.MyProject.UI; using UnityEngine; @@ -32,7 +33,8 @@ public int Score set { _score = value; - HudUI.ScoreLabel.text = $"Score: {_score:000}/{ScoreMax:000}"; + // Update the reactive property, which will trigger the subscription + _reactiveScore.Value = value; } } @@ -45,7 +47,8 @@ public int Lives set { _lives = value; - HudUI.LivesLabel.text = $"Lives: {_lives:000}/{LivesMax:000}"; + // Update the reactive property, which will trigger the subscription + _reactiveLives.Value = value; } } @@ -107,6 +110,10 @@ private set private bool _isEnabledInput = true; private bool _isPlayerGrounded = false; + // R3 Reactive Property Demo + private ReactiveProperty _reactiveScore = new ReactiveProperty(0); + private ReactiveProperty _reactiveLives = new ReactiveProperty(LivesMax); + // Audio private const string PlayerResetAudioClip = "ItemRead01"; private const string GameWinAudioClip = "Music_Win01"; @@ -122,6 +129,21 @@ protected void Start() { Debug.Log($"{GetType().Name}.Start()"); + // R3 ReactiveProperty Demo - Subscribe to changes + _reactiveScore.Subscribe(newScore => + { + Debug.Log($"[R3 Demo] Reactive Score changed to: {newScore}"); + // Update the UI when the reactive score changes + HudUI.ScoreLabel.text = $"Score: {newScore:000}/{ScoreMax:000}"; + }).AddTo(this); + + _reactiveLives.Subscribe(newLives => + { + Debug.Log($"[R3 Demo] Reactive Lives changed to: {newLives}"); + // Update the UI when the reactive lives changes + HudUI.LivesLabel.text = $"Lives: {newLives:000}/{LivesMax:000}"; + }).AddTo(this); + // Input _movePlayerInputAction = InputSystem.actions.FindAction("MovePlayer"); _jumpPlayerInputAction = InputSystem.actions.FindAction("JumpPlayer"); @@ -158,6 +180,16 @@ private void SetTitle() HudUI.TitleLabel.text = $"{SceneManager.GetActiveScene().name} ({themeName})"; } + /// + /// Runs when the GameObject is destroyed. Clean up resources. + /// + protected void OnDestroy() + { + // Dispose the ReactiveProperties when the GameObject is destroyed + _reactiveScore?.Dispose(); + _reactiveLives?.Dispose(); + } + /// /// Runs every frame. Use for input/physics/gameplay /// diff --git a/Unity/Assets/Scripts/Tests/Runtime/RMC.MyProject.Runtime.Tests.asmdef b/Unity/Assets/Scripts/Tests/Runtime/RMC.MyProject.Runtime.Tests.asmdef index 1c2a3b2..d14be67 100644 --- a/Unity/Assets/Scripts/Tests/Runtime/RMC.MyProject.Runtime.Tests.asmdef +++ b/Unity/Assets/Scripts/Tests/Runtime/RMC.MyProject.Runtime.Tests.asmdef @@ -4,7 +4,8 @@ "references": [ "GUID:27619889b8ba8c24980f49ee34dbb44a", "GUID:0acc523941302664db1f4e527237feb3", - "GUID:1bd920aa2cc01364d8e15ad9ca79abf3" + "GUID:1bd920aa2cc01364d8e15ad9ca79abf3", + "Cysharp.R3" ], "includePlatforms": [], "excludePlatforms": [], diff --git a/Unity/Assets/Scripts/Tests/Runtime/RMC/MyProject/R3ReactivePropertyTest.cs b/Unity/Assets/Scripts/Tests/Runtime/RMC/MyProject/R3ReactivePropertyTest.cs new file mode 100644 index 0000000..ff8f588 --- /dev/null +++ b/Unity/Assets/Scripts/Tests/Runtime/RMC/MyProject/R3ReactivePropertyTest.cs @@ -0,0 +1,149 @@ +using System.Collections; +using Cysharp.R3; +using NUnit.Framework; +using RMC.MyProject.Scenes; +using UnityEngine; +using UnityEngine.TestTools; + +namespace RMC.MyProject.Tests +{ + /// + /// Tests for R3 ReactiveProperty integration in Scene01_Intro + /// + [Category("RMC.MyProject.R3")] + public class R3ReactivePropertyTest + { + // Fields ---------------------------------------- + private GameObject _parentGameObject = null; + private Scene01_Intro _scene01Intro = null; + + // Initialization -------------------------------- + + /// + /// Optional. Called before every [Test] method + /// + [SetUp] + public void Setup() + { + _parentGameObject = new GameObject(); + _scene01Intro = _parentGameObject.AddComponent(); + } + + /// + /// Optional. Called after every [Test] method + /// + [TearDown] + public void TearDown() + { + if (_parentGameObject != null) + { + if (Application.isPlaying) + { + // Unity prefers this while playing + GameObject.Destroy(_parentGameObject); + } + else + { + // Unity prefers this while NOT playing + GameObject.DestroyImmediate(_parentGameObject, false); + } + + _parentGameObject = null; + _scene01Intro = null; + } + } + + // Methods --------------------------------------- + + /// + /// Test that ReactiveProperty can be created and disposed + /// + [Test] + public void ReactiveProperty_CanBeCreatedAndDisposed() + { + // Arrange & Act + var reactiveProperty = new ReactiveProperty(42); + + // Assert + Assert.IsNotNull(reactiveProperty); + Assert.AreEqual(42, reactiveProperty.Value); + + // Cleanup + reactiveProperty.Dispose(); + } + + /// + /// Test that ReactiveProperty subscription works + /// + [Test] + public void ReactiveProperty_SubscriptionTriggersOnValueChange() + { + // Arrange + var reactiveProperty = new ReactiveProperty(0); + int callbackCount = 0; + int lastValue = -1; + + // Act + var subscription = reactiveProperty.Subscribe(value => + { + callbackCount++; + lastValue = value; + }); + + // Initial subscription should trigger immediately + Assert.AreEqual(1, callbackCount); + Assert.AreEqual(0, lastValue); + + // Change value + reactiveProperty.Value = 42; + + // Assert + Assert.AreEqual(2, callbackCount); + Assert.AreEqual(42, lastValue); + + // Cleanup + subscription.Dispose(); + reactiveProperty.Dispose(); + } + + /// + /// A [UnityTest] that verifies R3 works in Unity context + /// + [UnityTest] + public IEnumerator ReactiveProperty_WorksInUnityContext() + { + // Arrange + var reactiveProperty = new ReactiveProperty("initial"); + bool subscriptionTriggered = false; + string receivedValue = null; + + // Act + reactiveProperty.Subscribe(value => + { + subscriptionTriggered = true; + receivedValue = value; + }); + + // Wait a frame + yield return null; + + // Assert initial state + Assert.IsTrue(subscriptionTriggered); + Assert.AreEqual("initial", receivedValue); + + // Reset and test value change + subscriptionTriggered = false; + reactiveProperty.Value = "changed"; + + // Wait a frame + yield return null; + + // Assert + Assert.IsTrue(subscriptionTriggered); + Assert.AreEqual("changed", receivedValue); + + // Cleanup + reactiveProperty.Dispose(); + } + } +} \ No newline at end of file diff --git a/Unity/Assets/Scripts/Tests/Runtime/RMC/MyProject/R3ReactivePropertyTest.cs.meta b/Unity/Assets/Scripts/Tests/Runtime/RMC/MyProject/R3ReactivePropertyTest.cs.meta new file mode 100644 index 0000000..ee3f667 --- /dev/null +++ b/Unity/Assets/Scripts/Tests/Runtime/RMC/MyProject/R3ReactivePropertyTest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a1b2c3d4e5f6789012345678901234ab +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/Unity/Packages/manifest.json b/Unity/Packages/manifest.json index 07cc287..42a4086 100644 --- a/Unity/Packages/manifest.json +++ b/Unity/Packages/manifest.json @@ -1,5 +1,6 @@ { "dependencies": { + "com.cysharp.r3": "https://github.com/Cysharp/R3.git", "com.rmc.rmc-audio": "1.8.4", "com.rmc.rmc-core": "1.9.5", "com.rmc.rmc-readme": "1.2.2",