From 036150b99a745dfafa26d9acda6369b42c6d2ac1 Mon Sep 17 00:00:00 2001 From: vontell Date: Wed, 27 Nov 2024 00:18:29 -0500 Subject: [PATCH 01/16] Initial validation code --- .../Editor/Scripts/Validation.meta | 3 + .../Scripts/Validation/RGValidationPane.cs | 21 ++++ .../Validation/RGValidationPane.cs.meta | 3 + .../BotSegments/Models/CVService.meta | 8 ++ .../Runtime/Scripts/Validation.meta | 3 + .../Runtime/Scripts/Validation/RGCondition.cs | 27 ++++ .../Scripts/Validation/RGCondition.cs.meta | 3 + .../Runtime/Scripts/Validation/RGValidate.cs | 28 +++++ .../Scripts/Validation/RGValidate.cs.meta | 3 + .../Scripts/Validation/RGValidateBehaviour.cs | 115 ++++++++++++++++++ .../Validation/RGValidateBehaviour.cs.meta | 3 + .../Scripts/Validation/RGValidatorResult.cs | 9 ++ .../Validation/RGValidatorResult.cs.meta | 3 + 13 files changed, 229 insertions(+) create mode 100644 src/gg.regression.unity.bots/Editor/Scripts/Validation.meta create mode 100644 src/gg.regression.unity.bots/Editor/Scripts/Validation/RGValidationPane.cs create mode 100644 src/gg.regression.unity.bots/Editor/Scripts/Validation/RGValidationPane.cs.meta create mode 100644 src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/CVService.meta create mode 100644 src/gg.regression.unity.bots/Runtime/Scripts/Validation.meta create mode 100644 src/gg.regression.unity.bots/Runtime/Scripts/Validation/RGCondition.cs create mode 100644 src/gg.regression.unity.bots/Runtime/Scripts/Validation/RGCondition.cs.meta create mode 100644 src/gg.regression.unity.bots/Runtime/Scripts/Validation/RGValidate.cs create mode 100644 src/gg.regression.unity.bots/Runtime/Scripts/Validation/RGValidate.cs.meta create mode 100644 src/gg.regression.unity.bots/Runtime/Scripts/Validation/RGValidateBehaviour.cs create mode 100644 src/gg.regression.unity.bots/Runtime/Scripts/Validation/RGValidateBehaviour.cs.meta create mode 100644 src/gg.regression.unity.bots/Runtime/Scripts/Validation/RGValidatorResult.cs create mode 100644 src/gg.regression.unity.bots/Runtime/Scripts/Validation/RGValidatorResult.cs.meta diff --git a/src/gg.regression.unity.bots/Editor/Scripts/Validation.meta b/src/gg.regression.unity.bots/Editor/Scripts/Validation.meta new file mode 100644 index 00000000..1bcb9ad3 --- /dev/null +++ b/src/gg.regression.unity.bots/Editor/Scripts/Validation.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: db36d9d0ead24050a8e8e80f50b0d176 +timeCreated: 1732654555 \ No newline at end of file diff --git a/src/gg.regression.unity.bots/Editor/Scripts/Validation/RGValidationPane.cs b/src/gg.regression.unity.bots/Editor/Scripts/Validation/RGValidationPane.cs new file mode 100644 index 00000000..73b78a94 --- /dev/null +++ b/src/gg.regression.unity.bots/Editor/Scripts/Validation/RGValidationPane.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Linq; +using UnityEditor; +using UnityEngine; +using UnityEngine.UIElements; + +namespace RegressionGames.Editor.Validation +{ + public class RGValidationPane : EditorWindow + { + + [MenuItem("Regression Games/Validation Results")] + public static void ShowMyEditor() + { + // This method is called when the user selects the menu item in the Editor + EditorWindow wnd = GetWindow(); + wnd.titleContent = new GUIContent("Regression Games Validation Results"); + } + + } +} \ No newline at end of file diff --git a/src/gg.regression.unity.bots/Editor/Scripts/Validation/RGValidationPane.cs.meta b/src/gg.regression.unity.bots/Editor/Scripts/Validation/RGValidationPane.cs.meta new file mode 100644 index 00000000..f886ad58 --- /dev/null +++ b/src/gg.regression.unity.bots/Editor/Scripts/Validation/RGValidationPane.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 70bbf9b54c3640449d6e62c968cf51a0 +timeCreated: 1732654566 \ No newline at end of file diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/CVService.meta b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/CVService.meta new file mode 100644 index 00000000..07751df3 --- /dev/null +++ b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/CVService.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5c74a766426ce23499df244c81e55ee7 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/Validation.meta b/src/gg.regression.unity.bots/Runtime/Scripts/Validation.meta new file mode 100644 index 00000000..c0d7d704 --- /dev/null +++ b/src/gg.regression.unity.bots/Runtime/Scripts/Validation.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: eac368faa0a64e8a844004066126aa33 +timeCreated: 1732638994 \ No newline at end of file diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/Validation/RGCondition.cs b/src/gg.regression.unity.bots/Runtime/Scripts/Validation/RGCondition.cs new file mode 100644 index 00000000..9d886b52 --- /dev/null +++ b/src/gg.regression.unity.bots/Runtime/Scripts/Validation/RGCondition.cs @@ -0,0 +1,27 @@ +namespace RegressionGames.Validation +{ + + /** + * The condition that the test should be evaluated as. + */ + public enum RGCondition + { + /** + * This condition should be true on every frame that it is + * evaluated. + */ + ALWAYS_TRUE, + + /** + * This condition should never be true for any frame it is + * evaluated. + */ + NEVER_TRUE, + + /** + * This condition should be true at least once during the + * test. + */ + EVENTUALLY_TRUE + } +} \ No newline at end of file diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/Validation/RGCondition.cs.meta b/src/gg.regression.unity.bots/Runtime/Scripts/Validation/RGCondition.cs.meta new file mode 100644 index 00000000..612ad337 --- /dev/null +++ b/src/gg.regression.unity.bots/Runtime/Scripts/Validation/RGCondition.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 5505d0cf8e874516a2972f0795ebe13e +timeCreated: 1732641803 \ No newline at end of file diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/Validation/RGValidate.cs b/src/gg.regression.unity.bots/Runtime/Scripts/Validation/RGValidate.cs new file mode 100644 index 00000000..e3e62c93 --- /dev/null +++ b/src/gg.regression.unity.bots/Runtime/Scripts/Validation/RGValidate.cs @@ -0,0 +1,28 @@ +using System; +using UnityEngine; + +namespace RegressionGames.Validation +{ + + /** + * An attribute that marks a method as a validation method + * to be run by Regression Games during a bot sequence or bot segment. + * You can set a frequency attribute to indicate how often the validation + * is running (e.g. every 10th frame). + * You can also indicate whether this is a validation that should + * always be true, never be true, or be true at least once. + */ + [AttributeUsage(AttributeTargets.Method)] + public class RGValidate: Attribute { + + public int Frequency { get; private set; } + public RGCondition Condition { get; private set; } + + public RGValidate(RGCondition condition, int frequency = 1) + { + Condition = condition; + Frequency = Mathf.Max(1, frequency); // Ensure frequency is at least 1 + } + + } +} \ No newline at end of file diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/Validation/RGValidate.cs.meta b/src/gg.regression.unity.bots/Runtime/Scripts/Validation/RGValidate.cs.meta new file mode 100644 index 00000000..ee87a2f4 --- /dev/null +++ b/src/gg.regression.unity.bots/Runtime/Scripts/Validation/RGValidate.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 15d18d146c8f453d9154e544fa22353e +timeCreated: 1732639009 \ No newline at end of file diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/Validation/RGValidateBehaviour.cs b/src/gg.regression.unity.bots/Runtime/Scripts/Validation/RGValidateBehaviour.cs new file mode 100644 index 00000000..ae2d1d4a --- /dev/null +++ b/src/gg.regression.unity.bots/Runtime/Scripts/Validation/RGValidateBehaviour.cs @@ -0,0 +1,115 @@ +using System.Collections.Generic; +using System.Reflection; +using UnityEngine; + +namespace RegressionGames.Validation +{ + public class RGValidateBehaviour: MonoBehaviour + { + + private class ValidatorData + { + public MethodInfo Method { get; set; } + public int Frequency { get; set; } + public RGCondition Condition { get; set; } + public RGValidatorResult Result { get; set; } = RGValidatorResult.NOT_SET; + public System.Exception ThrownException { get; set; } + } + + private List validators; + + private int frame = 0; + private ValidatorData currentValidator; + private RGValidatorResult currentStatus = RGValidatorResult.NOT_SET; + + private void Awake() + { + // Cache all methods with UpdateMethod attribute + validators = new List(); + var methods = GetType().GetMethods(BindingFlags.Instance | + BindingFlags.NonPublic | + BindingFlags.Public); + + foreach (var method in methods) + { + var attribute = method.GetCustomAttribute(); + if (attribute != null) + { + validators.Add(new ValidatorData + { + Method = method, + Frequency = attribute.Frequency, + Condition = attribute.Condition + }); + Debug.Log("[RGValidator] Activated " + method.Name); + } + } + } + + public void AssertAsTrue(string message = null) + { + currentStatus = RGValidatorResult.PASSED; + } + + public void AssertAsFalse(string message = null) + { + currentStatus = RGValidatorResult.FAILED; + } + + private void Update() + { + + // Call tagged methods based on their frequency + foreach (var validator in validators) + { + + // Check that we should run on this frame + if (frame % validator.Frequency != 0) + { + continue; + } + + // Skip over validators that are already passed for failed + if (validator.Result is RGValidatorResult.PASSED or RGValidatorResult.FAILED) + { + continue; + } + + // Run the validator + try + { + currentStatus = RGValidatorResult.NOT_SET; + currentValidator = validator; + validator.Method.Invoke(this, null); + } + catch (System.Exception e) + { + Debug.LogError($"Error running validation method {validator.Method.Name}: {e.Message}"); + validator.ThrownException = e; + } + + // After the method is called, we can figure out what to do with the validators that should immediately + // fail or pass + if (currentValidator.Condition == RGCondition.NEVER_TRUE && currentStatus == RGValidatorResult.PASSED) + { + Debug.LogError($"Validation method {validator.Method.Name} should never pass, but it did."); + currentValidator.Result = RGValidatorResult.FAILED; + } + else if (currentValidator.Condition == RGCondition.ALWAYS_TRUE && currentStatus is RGValidatorResult.FAILED or RGValidatorResult.NOT_SET) + { + Debug.LogError($"Validation method {validator.Method.Name} should always pass, but it did not."); + currentValidator.Result = RGValidatorResult.FAILED; + } + else if (currentValidator.Condition == RGCondition.EVENTUALLY_TRUE && currentStatus == RGValidatorResult.PASSED) + { + Debug.LogError($"Validation method {validator.Method.Name} finally passed."); + currentValidator.Result = RGValidatorResult.PASSED; + } + + } + + frame++; + } + + } +} \ No newline at end of file diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/Validation/RGValidateBehaviour.cs.meta b/src/gg.regression.unity.bots/Runtime/Scripts/Validation/RGValidateBehaviour.cs.meta new file mode 100644 index 00000000..fd83d904 --- /dev/null +++ b/src/gg.regression.unity.bots/Runtime/Scripts/Validation/RGValidateBehaviour.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 933a62accbff41e28af49b5e7cc982a4 +timeCreated: 1732641487 \ No newline at end of file diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/Validation/RGValidatorResult.cs b/src/gg.regression.unity.bots/Runtime/Scripts/Validation/RGValidatorResult.cs new file mode 100644 index 00000000..8274f81f --- /dev/null +++ b/src/gg.regression.unity.bots/Runtime/Scripts/Validation/RGValidatorResult.cs @@ -0,0 +1,9 @@ +namespace RegressionGames.Validation +{ + public enum RGValidatorResult + { + PASSED, + FAILED, + NOT_SET, + } +} \ No newline at end of file diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/Validation/RGValidatorResult.cs.meta b/src/gg.regression.unity.bots/Runtime/Scripts/Validation/RGValidatorResult.cs.meta new file mode 100644 index 00000000..87f3e6bc --- /dev/null +++ b/src/gg.regression.unity.bots/Runtime/Scripts/Validation/RGValidatorResult.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: acbeaf3f90cd4b7c869097c74bb6329c +timeCreated: 1732643939 \ No newline at end of file From 2e48658f2a6909e1b46178362c7a49df654f86f7 Mon Sep 17 00:00:00 2001 From: vontell Date: Sun, 1 Dec 2024 14:27:11 -0500 Subject: [PATCH 02/16] Add new capabilities and docs --- .../Scripts/Validation/RGValidationPane.cs | 87 +++++- .../Validation/RGValidationPane.cs.meta | 14 +- .../Runtime/Scripts/Validation/README.md | 248 ++++++++++++++++++ .../Runtime/Scripts/Validation/README.md.meta | 3 + .../Runtime/Scripts/Validation/RGCondition.cs | 8 +- .../Runtime/Scripts/Validation/RGValidate.cs | 2 + .../Scripts/Validation/RGValidateBehaviour.cs | 82 +++++- .../Runtime/Scripts/Validation/demo.png | 3 + 8 files changed, 431 insertions(+), 16 deletions(-) create mode 100644 src/gg.regression.unity.bots/Runtime/Scripts/Validation/README.md create mode 100644 src/gg.regression.unity.bots/Runtime/Scripts/Validation/README.md.meta create mode 100644 src/gg.regression.unity.bots/Runtime/Scripts/Validation/demo.png diff --git a/src/gg.regression.unity.bots/Editor/Scripts/Validation/RGValidationPane.cs b/src/gg.regression.unity.bots/Editor/Scripts/Validation/RGValidationPane.cs index 73b78a94..671129e9 100644 --- a/src/gg.regression.unity.bots/Editor/Scripts/Validation/RGValidationPane.cs +++ b/src/gg.regression.unity.bots/Editor/Scripts/Validation/RGValidationPane.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; -using System.Linq; +using JetBrains.Annotations; +using RegressionGames.Validation; using UnityEditor; using UnityEngine; using UnityEngine.UIElements; @@ -8,13 +9,95 @@ namespace RegressionGames.Editor.Validation { public class RGValidationPane : EditorWindow { + private ScrollView _mainScrollPane; + + [CanBeNull] public static RGValidationPane Instance = null; [MenuItem("Regression Games/Validation Results")] public static void ShowMyEditor() { // This method is called when the user selects the menu item in the Editor EditorWindow wnd = GetWindow(); - wnd.titleContent = new GUIContent("Regression Games Validation Results"); + wnd.titleContent = new GUIContent("RG Validation Results"); + } + + private void OnEnable() + { + Instance = this; + } + + void CreateGUI() + { + + _mainScrollPane = new ScrollView(ScrollViewMode.VerticalAndHorizontal); + rootVisualElement.Add(_mainScrollPane); + + UpdateGUI(); + + } + + void UpdateGUI() + { + + _mainScrollPane.Clear(); + UnsubscribeFromValidationEvents(); + + var validationScripts = FindObjectsOfType(); + + foreach (var validationScript in validationScripts) + { + var scriptContainer = new VisualElement(); + scriptContainer.Add(new Label("Suite: " + validationScript.GetType().Name)); + _mainScrollPane.Add(scriptContainer); + + validationScript.OnValidationsUpdated += UpdateGUI; + + foreach (var validator in validationScript.Validators) + { + var validatorContainer = new VisualElement(); + validatorContainer.style.paddingLeft = 20; + + scriptContainer.Add(validatorContainer); + + var testText = ""; + Color? color = null; + + switch (validator.Result) + { + case RGValidatorResult.NOT_SET: + testText += "? "; + break; + case RGValidatorResult.PASSED: + // If pass, show a green checkmark + testText += "PASS "; + color = Color.green; + break; + case RGValidatorResult.FAILED: + testText += "FAIL "; + color = Color.red; + break; + } + + testText += validator.Method.Name; + var testLabel = new Label(testText); + if (color != null) + { + testLabel.style.color = color.Value; + } + + validatorContainer.Add(testLabel); + } + } + + } + + private void UnsubscribeFromValidationEvents() + { + var validationScripts = FindObjectsOfType(); + foreach (var validationScript in validationScripts) + { + validationScript.OnValidationsUpdated -= UpdateGUI; + } } } diff --git a/src/gg.regression.unity.bots/Editor/Scripts/Validation/RGValidationPane.cs.meta b/src/gg.regression.unity.bots/Editor/Scripts/Validation/RGValidationPane.cs.meta index f886ad58..a45d137f 100644 --- a/src/gg.regression.unity.bots/Editor/Scripts/Validation/RGValidationPane.cs.meta +++ b/src/gg.regression.unity.bots/Editor/Scripts/Validation/RGValidationPane.cs.meta @@ -1,3 +1,13 @@ -fileFormatVersion: 2 +fileFormatVersion: 2 guid: 70bbf9b54c3640449d6e62c968cf51a0 -timeCreated: 1732654566 \ No newline at end of file +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: + - m_ViewDataDictionary: {instanceID: 0} + - uxml: {fileID: 9197481963319205126, guid: 684a1d5fe2f954980bc151632c20cf15, type: 3} + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/Validation/README.md b/src/gg.regression.unity.bots/Runtime/Scripts/Validation/README.md new file mode 100644 index 00000000..21245882 --- /dev/null +++ b/src/gg.regression.unity.bots/Runtime/Scripts/Validation/README.md @@ -0,0 +1,248 @@ +# Prototype - Code-based Validations + +This partial feature is meant to convey some ideas around code-based validations. I wanted +to explore how easy it is to write validations for a bot sequence within code, and while +there are more features I would want to implement here, I wanted to share this with the team +and get some feedback. + +## Overall Idea + +The overall idea here is to allow developers to write simple functions that act as +validators for a sequence. For instance, you may want to validate that: + +- A certain UI element appears +- The animation of some particle system is playing correctly +- That a certain relationship between entities is present + +The primary goal here is **simplicity**, making sure it feels easy and straightforward to +write these rules. I therefore created a system that felt very similar to Unit tests, but +with some additional pieces to account for the fact that these validators run often (due to +the update loop). + +**NOTE TO REVIEWERS:** I hope to get across these main points with this system, to clarify further from +our conversation in the validators Slack channel where there were some doubts with this system. + +1. These are not thought of as "writing a MonoBehaviour". There is no concept of a MonoBehaviour exposed here. +2. I provide some simple test examples here, but the goal is to use this for situations where simple relationships are + easy to write in code, but difficult in no-code ways. See the pots example for a situation where the code is easy, + but getting that exact logic in no-code would be tricky. + +The resulting prototype is an `RGValidate` attribute that says "this method is a validator, it runs +at a certain frequency, and we expect it to be either always true, eventually true, or never +true". These are automatically evaluated and are shown in a pane in the editor. + +Here is a quick example: + +```csharp +[RGValidate(RGCondition.EVENTUALLY_TRUE, 100)] +void ProfileButtonAppears() +{ + var profileButton = GameObject.Find("Profile Button"); + if (profileButton) + { + AssertAsTrue("Profile button has appeared"); + } +} +``` + +Here is what this does: + +1. Run this function every 100 frames (the default is every frame) +2. This function should "assert true" at some point. If it doesn't, the sequence will fail. +3. Once this is marked as true, this function will no longer run. + +Once validations are passing, failing, or waiting, it will show directly in the editor. +Imagine that this would be tied to whatever system we use for tracking results. + +![Validation Example](demo.png) + +## RGValidate API + +In order to get these validators running automatically, you create a class that inherits +from `RGValidateBehaviour` and then add the `RGValidate` attribute to any method you want +to run as a validator. The `RGValidate` attribute has the following parameters: + +- `condition`: The condition that this validator should meet. This can be `ALWAYS_TRUE`, + `EVENTUALLY_TRUE`, `NEVER_TRUE`, or `ONCE_TRUE_ALWAYS_TRUE`. +- `frequency`: How often this validator should run (i.e. number of frames). The default is every frame, but I would recommend setting this to something higher for performance reasons. + +### Conditions + +- `RGCondition.ALWAYS_TRUE`: This validator should always assert true. +- `RGCondition.EVENTUALLY_TRUE`: This validator should eventually assert true. It can fail to assert or assert false for any number of times, but must be true by the end of the run. +- `RGCondition.NEVER_TRUE`: This validator should never assert true. It can fail to assert or assert false any number of times, but should never assert true. +- `RGCondition.ONCE_TRUE_ALWAYS_TRUE`: This validator should assert true once, and then must always assert true after that. It can fail to assert or assert false before that. + +Essentially, a validator can choose to do one of three things: +* Do nothing - this may be the case where you are waiting for an entity to appear. It is not false or true, but is internally marked as NOT_SET. +* Assert true - this is the case where the condition being checked is now true. Note that this doesn't mean pass necessarily, because a NEVER_TRUE condition that asserts as true is technically a FAIL +* Assert false - this is the case where the condition being checked is now false. This is a FAIL. + +## Getting Started on your Own + +Before getting feedback, I'd love to have you all actually try this to really see how it +feels. You can get started in literally minutes, all you need to do is: + +```bash +# In RGBossRoom +git checkout prototype-rg-validators + +# In RGUnityBots +git checkout prototype-rg-validators +``` + +Then open either `Assets/Scripts/RegressionGames/ValidateUI.cs` or `Assets/Scripts/RegressionGames/ValidateCharacters.cs` and start writing your own validators. +If you want to make your own new test file, you need to add it as a component to the RGOverlay object. **This is obviously not the final +approach - it seemed like overkill for the first pass, but these would be added a "validator" field in the sequence perhaps.** + +Then, just open up the pane with the menu Regression Games > Validation Results. + +## Full Examples + +```csharp +using RegressionGames.Validation; +using Unity.BossRoom.Gameplay.GameplayObjects; +using UnityEngine; + +namespace Unity.Multiplayer.Samples.BossRoom.RegressionGames +{ + public class ValidateCharacters: RGValidateBehaviour + { + + [RGValidate(RGCondition.NEVER_TRUE, 100)] + void CheatsPopupPanelShouldNotExist() + { + var cheatsPopupPanel = GameObject.Find("CheatsPopupPanel"); + if (cheatsPopupPanel) + { + AssertAsTrue("CheatsPopupPanel was found"); + } + } + + [RGValidate(RGCondition.EVENTUALLY_TRUE, 100)] + void SomePotsShouldBeNearPlayer() + { + + // The collective distance of all the pots from the player should be less than a certain amount + var player = GameObject.FindWithTag("Player"); + var pots = FindObjectsOfType(); + + var countNearby = 0; + if (player != null && pots.Length > 0) + { + // Make sure there are at least 3 pots within 10 units of the player + foreach (var pot in pots) + { + if (Vector3.Distance(player.transform.position, pot.transform.position) < 10) + { + countNearby++; + } + } + } + + if (countNearby < 3) + { + AssertAsFalse("Less than 3 pots are near the player"); + } + else + { + AssertAsTrue("At least 3 pots are near the player"); + } + + } + + } +} +``` + +```csharp +using System; +using RegressionGames.Validation; +using UnityEngine; +using UnityEngine.UI; + +namespace Unity.Multiplayer.Samples.BossRoom.RegressionGames +{ + public class ValidateUI: RGValidateBehaviour + { + private bool profileDialogAppeared = false; + private float previousParticleTime = float.NegativeInfinity; + + [RGValidate(RGCondition.EVENTUALLY_TRUE, 100)] + void ProfileButtonAppears() + { + var profileButton = GameObject.Find("Profile Button"); + if (profileButton) + { + AssertAsTrue("Profile button has appeared"); + } + } + + [RGValidate(RGCondition.EVENTUALLY_TRUE, 100)] + void ChangeProfileDialogAppears() + { + var profileButton = GameObject.Find("ProfilePopup"); + if (profileButton) + { + var canvasGroup = profileButton.GetComponent(); + if (canvasGroup.alpha > 0) + { + profileDialogAppeared = true; + AssertAsTrue("Profile dialog has appeared"); + } + } + } + + [RGValidate(RGCondition.EVENTUALLY_TRUE, 100)] + void RGButtonToBeInteractableAfterProfileDialog() + { + var startButton = GameObject.Find("RG Start Button"); + if (startButton) + { + var button = startButton.GetComponent