Skip to content

Reflection Access

David O'Donoghue edited this page Feb 10, 2026 · 12 revisions

Reflection Access

Access game state via reflection using Search.Reflect(). This allows tests to validate internal game state without direct assembly references, making it useful when tests are in a separate assembly from the game code.

Overview

Static paths provide a way to access any static field, property, or nested member in your codebase:

// Access static properties
var health = Search.Reflect("Player.Instance.Health").GetValue<float>();
var score = Search.Reflect("GameManager.Instance.Score").GetValue<int>();
var isReady = Search.Reflect("GameManager.Instance.IsReady").GetValue<bool>();

// Access Unity APIs
var screenWidth = Search.Reflect("Screen.width").GetValue<int>();
var isPlaying = Search.Reflect("Application.isPlaying").GetValue<bool>();

Creating Static Paths

Search.Reflect()

Create a Search object from a dot-separated path:

// Simple static property
var instance = Search.Reflect("GameManager.Instance");

// Nested property access
var playerHealth = Search.Reflect("GameManager.Instance.Player.Health");

// Works with any static type
var timeScale = Search.Reflect("Time.timeScale");

Property Navigation

Chain .Property() to navigate deeper into objects. You can use either chained calls or dot notation within a single call:

// Navigate to nested properties (chained)
var damageController = Search.Reflect("Player.Instance").Property("DamageController");
var maxHealth = damageController.Property("MaxHealth").GetValue<float>();

// Multiple levels of navigation (chained)
var weaponDamage = Search.Reflect("Player.Instance")
    .Property("Inventory")
    .Property("EquippedWeapon")
    .Property("Damage")
    .GetValue<float>();

// Dot notation in Property() - equivalent to chained calls
var weaponDamage2 = Search.Reflect("Player.Instance")
    .Property("Inventory.EquippedWeapon.Damage")
    .GetValue<float>();

// Useful when property path is dynamic or from configuration
string propertyPath = "loadedLevel.player.racingLine";
var value = Search.Reflect("GameModeDrag.Instance").Property(propertyPath).Value;

Property on UI Element Searches

.Property() also works on normal Text(), Name(), and Type() searches to access component properties via reflection. The search finds the first matching element, then .Property() accesses its properties:

// Access properties on found UI elements (finds first match)
var buttonText = new Search().Name("SubmitButton").Property("interactable").GetValue<bool>();
var sliderValue = new Search().Name("VolumeSlider").Property("value").GetValue<float>();

// Access nested component properties
var tmpColor = new Search().Name("Title").Property("color").Value;

// Access any property on any component
var rectWidth = new Search().Name("Panel").Property("rect").Property("width").GetValue<float>();

// Combine with other search methods to narrow down which element
var specificButton = new Search().Name("Submit*").Type<Button>().Property("interactable").GetValue<bool>();

This is useful when you need to check a property that isn't exposed through GetValue<T>. Note that the search will grab the first matching entry, so use additional search criteria to ensure you get the right element.

Value Access

Get typed values from static paths using GetValue<T>() or .Value for raw object access:

Method / Property Return Type Description
Value object Raw object value
GetValue<string>() string String value
GetValue<bool>() bool Boolean value
GetValue<int>() int Integer value
GetValue<float>() float Float value
GetValue<Vector3>() Vector3 Vector3 from static path or transform position
GetValue<Vector2>() Vector2 Vector2 from static path or RectTransform position
GetValue<Color>() Color Color from static path or Image/Text color
GetValue<Quaternion>() Quaternion Quaternion from static path or transform rotation
GetValue<T[]>() T[] Array of typed values
var path = Search.Reflect("Player.Instance");

// String
string name = path.Property("Name").GetValue<string>();

// Boolean
bool isAlive = path.Property("IsAlive").GetValue<bool>();

// Integer
int score = path.Property("Score").GetValue<int>();

// Float
float health = path.Property("Health").GetValue<float>();

// Raw object
object data = path.Property("CustomData").Value;

Unity Type Values

Access Unity types directly from static paths or UI elements:

// Vector3 from static path
var position = Search.Reflect("Player.Instance.Transform.position").GetValue<Vector3>();

// Vector2 from UI element (gets RectTransform.anchoredPosition)
var uiPos = new Search().Name("Button").GetValue<Vector2>();

// Color from static path
var color = Search.Reflect("Theme.PrimaryColor").GetValue<Color>();

// Color from UI element (gets Image or Text color)
var btnColor = new Search().Name("SubmitButton").GetValue<Color>();

// Quaternion from static path
var rotation = Search.Reflect("Player.Instance.Transform.rotation").GetValue<Quaternion>();

// Quaternion from UI element (gets transform rotation)
var uiRotation = new Search().Name("RotatedPanel").GetValue<Quaternion>();

GetValue

Get strongly-typed values with automatic conversion:

// From Search.Reflect()
var truck = Search.Reflect("TestTrucks.PlayerTruck");
string name = truck.GetValue<string>("Name");
float health = truck.GetValue<float>("Health");
int count = truck.GetValue<int>("Count");

// For arrays
string[] tags = truck.Property("Tags").GetValue<string[]>();
int[] scores = truck.Property("Scores").GetValue<int[]>();

// Direct static path access via ActionExecutor
string playerName = ActionExecutor.GetValue<string>("Player.Instance.Name");
float playerHealth = ActionExecutor.GetValue<float>("Player.Instance.Health");

Method Invocation

Call methods on objects via reflection using .Invoke():

// Call methods on static instances
Search.Reflect("GameManager.Instance").Invoke("StartGame");
Search.Reflect("Player.Instance").Invoke("TakeDamage", 10f);

// Call static methods on a type (no instance needed)
Search.Reflect("ParseAccountAPI").Invoke("ForceInvalidateCurrentUser");
Search.Reflect("PlayerPrefs").Invoke("DeleteAll");

// Get return values with Invoke<T>
bool isValid = Search.Reflect("Validator.Instance").Invoke<bool>("Validate", inputData);
string result = Search.Reflect("Calculator.Instance").Invoke<string>("Calculate", 5, 10);

// Call methods on UI elements (finds first match)
new Search().Name("DialogBox").Invoke("Close");
new Search().Name("AudioManager").Invoke("PlaySound", "click");

// Chain with Property navigation
Search.Reflect("Player.Instance")
    .Property("Inventory")
    .Invoke("AddItem", "Sword", 1);

// Get return value from UI element method
int count = new Search().Name("Counter").Invoke<int>("GetCount");

Async Method Invocation

Call async methods and await their completion using .InvokeAsync():

// Await an async static method
await Search.Reflect("ParseAccountAPI").InvokeAsync("LogoutAsync");

// Await an async instance method
await Search.Reflect("GameManager.Instance").InvokeAsync("SaveAsync");

// Get typed return value from async method
var user = await Search.Reflect("UserService").InvokeAsync<User>("GetCurrentUserAsync");
var data = await Search.Reflect("DataLoader.Instance").InvokeAsync<string>("LoadAsync", "config.json");

Method Resolution

Methods are resolved using smart signature matching:

  1. Exact Match: First attempts to match method by exact argument types
  2. Type-Compatible Match: Falls back to scoring candidates by type compatibility (assignable types, implicit numeric conversions)
  3. Optional Parameters: Methods with default/optional parameters are matched — omitted args are filled with their default values
  4. Static vs Instance: Automatically uses static binding when Reflect() resolves to a type name (no instance)
  5. Private Access: Both public and private methods are accessible
  6. Component Search: For UI elements, searches all components for the method
// Works with private methods
Search.Reflect("GameManager.Instance").Invoke("InternalReset");

// Works with overloaded methods (matches by argument types)
Search.Reflect("Calculator").Invoke("Add", 5, 10);      // Add(int, int)
Search.Reflect("Calculator").Invoke("Add", 5.0f, 10.0f); // Add(float, float)

// Works with optional/default parameters
// Given: void Configure(string name, int retries = 3, bool verbose = false)
Search.Reflect("Server.Instance").Invoke("Configure", "main");           // uses defaults
Search.Reflect("Server.Instance").Invoke("Configure", "main", 5);       // overrides retries
Search.Reflect("Server.Instance").Invoke("Configure", "main", 5, true); // overrides all

// Implicit numeric conversions (int -> long, float -> double, etc.)
// Given: void SetValue(long amount)
Search.Reflect("Counter.Instance").Invoke("SetValue", 42); // int passed to long param

// Clear error messages when no match found
// Throws: "Method 'Foo' not found on type 'Bar' matching arg types [String, Int32].
//          Available signatures:
//            Void Foo(String name, Float value)
//            Void Foo(Int32 count)"

Indexer Access

Access array elements, list items, and dictionary values using indexers:

Index() Method

Use .Index(int) for arrays/lists or .Index(string) for dictionaries:

// Access array element
var firstItem = Search.Reflect("Inventory.Items").Index(0);
var itemName = firstItem.Property("Name").GetValue<string>();

// Access list element
var secondPlayer = Search.Reflect("GameManager.Players").Index(1);
var score = secondPlayer.Property("Score").GetValue<int>();

// Access dictionary by key
var settings = Search.Reflect("Config.Settings").Index("volume").GetValue<float>();
var player = Search.Reflect("PlayerManager.PlayersByName").Index("Player1");

C# Indexer Syntax

Use native C# indexer syntax for cleaner code:

// Array/list access with []
var weapon = Search.Reflect("Player.Inventory").Property("Weapons")[0];
var damage = weapon.Property("Damage").GetValue<int>();

// Dictionary access with []
var value = Search.Reflect("Config.Settings")["musicVolume"].GetValue<float>();

// Chain multiple indexers
var cell = Search.Reflect("Grid.Cells")[2][3].Property("Value").GetValue<int>();

Inline Indexer Syntax

Include indexers directly in the path string:

// Array index in path
var name = Search.Reflect("Game.Players[0].Name").GetValue<string>();

// Dictionary key in path (use quotes)
var setting = Search.Reflect("Config.Settings[\"volume\"]").GetValue<float>();

// Single quotes also work
var value = Search.Reflect("Config.Settings['music']").GetValue<float>();

// Chained indexers
var cell = Search.Reflect("Grid.Data[1][2]").GetValue<int>();

// Combined with property navigation
var score = Search.Reflect("Game.Teams[0].Players[2].Score").GetValue<int>();

Property() with Indexers

Use indexer syntax within .Property() calls:

// Access indexed property
var item = Search.Reflect("Inventory").Property("Items[0]");

// Dictionary access in Property
var setting = Search.Reflect("Config").Property("Settings[\"audio\"]");

Nested Type Syntax

Access nested types using dot notation (in addition to the CLR + syntax):

// These are equivalent:
Search.Reflect("GameConfig.Settings.MaxPlayers")     // Dot syntax (cleaner)
Search.Reflect("GameConfig+Settings.MaxPlayers")    // CLR nested type syntax

// Access static fields on nested types
var defaultHealth = Search.Reflect("PlayerStats.Defaults.Health").GetValue<float>();

Array Iteration

Static paths that resolve to arrays can be iterated:

// Iterate over array items
foreach (var truck in Search.Reflect("TruckManager.AllTrucks"))
{
    var name = truck.Property("Name").GetValue<string>();
    var health = truck.Property("Health").GetValue<float>();
    Debug.Log($"{name}: {health} HP");
}

// Access nested properties during iteration
foreach (var player in Search.Reflect("GameManager.Players"))
{
    var weaponDamage = player.Property("Weapon").Property("Damage").GetValue<float>();
}

Path Resolution

Static paths are resolved using reflection:

  1. Type Resolution: The first part identifies the type (e.g., GameManager)
  2. Member Access: Subsequent parts are properties or fields (e.g., Instance.Score)
  3. Private Access: Both public and private members are accessible
  4. Cross-Assembly: Works across all loaded assemblies
// Examples of valid paths:
"GameManager.Instance"              // Static property
"GameManager.Instance.Score"        // Instance property on static instance
"Player.Instance.health"            // Private field (lowercase convention)
"Application.isPlaying"             // Unity API
"Screen.width"                      // Unity property
"MyNamespace.MyClass.StaticField"   // Fully qualified type name

Type Name Resolution

Short type names are auto-resolved if unique across all assemblies:

// Short name (works if GameManager is unique)
Search.Reflect("GameManager.Instance")

// Fully qualified (required if multiple GameManager types exist)
Search.Reflect("MyGame.Managers.GameManager.Instance")

Using with NUnit Assertions

Combine static paths with NUnit for game state validation:

// Verify instance exists
Assert.IsNotNull(Search.Reflect("GameManager.Instance").Value);

// Verify boolean state
Assert.IsTrue(Search.Reflect("GameManager.Instance.IsPlaying").GetValue<bool>());

// Verify numeric values
Assert.AreEqual(100, Search.Reflect("Player.Health").GetValue<int>());
Assert.Greater(Search.Reflect("Player.Score").GetValue<int>(), 0);
Assert.Less(Search.Reflect("Game.TimeRemaining").GetValue<float>(), 60f);

// Verify string values
Assert.AreEqual("MainLevel", Search.Reflect("LevelManager.CurrentLevel.Name").GetValue<string>());

WaitFor with Static Paths

Wait for game state conditions:

// Wait for instance to exist
await WaitFor(Search.Reflect("Player.Instance"), timeout: 30f);

// Wait for boolean to become true
await WaitFor(Search.Reflect("GameManager.Instance.IsReady"));

// Wait for specific value
await WaitFor(Search.Reflect("Player.Score"), 100, timeout: 60f);
await WaitFor(Search.Reflect("Game.State"), "Playing", timeout: 10f);

Truthy Values

WaitFor(path) checks for "truthy" values:

Type Truthy Falsy
object not null null
bool true false
int, long != 0 0
float, double != 0 0
string not empty null or ""

Component Access

Get a component from a GameObject and access its properties via reflection using .Component<T>() or .Component(string):

// Get Rigidbody component and set isKinematic
Search.Reflect("GameModeDrag.Instance.competitorControllers[1]")
    .Component<Rigidbody>()
    .Property("isKinematic")
    .SetValue(true);

// String-based version (useful when type isn't available at compile time)
Search.Reflect("Player.Instance")
    .Component("Rigidbody")
    .Property("velocity")
    .SetValue(Vector3.zero);

// From UI element searches
new Search().Name("CarController")
    .Component<Rigidbody>()
    .Property("isKinematic")
    .SetValue(true);

// Access custom component properties
new Search().Name("Player")
    .Component("PlayerStats")
    .Property("health")
    .SetValue(100f);

Component vs Property

  • Component() / Component(string): Gets a Unity component from a GameObject. Use when you need to access a component that exists on the object.
  • Property(): Navigates to a property or field on the current object. Use when you have a reference and want to access its members.
// If Chassis IS a Rigidbody property on the controller:
Search.Reflect("GameModeDrag.Instance.competitorControllers[1]")
    .Property("Chassis")           // Chassis is a Rigidbody
    .Property("isKinematic")       // Access Rigidbody properties
    .SetValue(true);

// If you need to get a component from the controller's GameObject:
Search.Reflect("GameModeDrag.Instance.competitorControllers[1]")
    .Component<Rigidbody>()        // GetComponent<Rigidbody>()
    .Property("isKinematic")
    .SetValue(true);

Creating Instances

Create new instances of types for test data using Search.New():

// Create from JSON with generic type
var player = Search.New<PlayerData>(@"{ ""Name"": ""Test"", ""Health"": 100 }");
var data = player.Value as PlayerData;

// Create from JSON by type name (no generic needed)
var player2 = Search.New("PlayerData", @"{ ""Name"": ""Test"", ""Health"": 100 }");

// Create empty instance and set properties
var empty = Search.New("PlayerData");
empty.Property("Name").SetValue("Test");
empty.Property("Health").SetValue(100f);

// Use with SetValue to assign to game state
var newData = Search.New("PlayerData", @"{ ""Name"": ""TestPlayer"" }");
Search.Reflect("GameManager.Instance").Property("PlayerData").SetValue(newData.Value);

JSON Serialization

Serialize and deserialize values to/from JSON using Serialize() and Deserialize() (uses Newtonsoft.Json):

Serialize()

Serialize the current value to a JSON string:

// Get JSON of current state
var json = Search.Reflect("Player.Instance").Property("Stats").Serialize();

// Get formatted/indented JSON
var prettyJson = Search.Reflect("Config.Instance").Property("Settings").Serialize(indented: true);

// Useful for debugging
Debug.Log(Search.Reflect("GameManager.Instance").Property("PlayerData").Serialize(true));

// Save state for later restoration
var savedState = Search.Reflect("LevelManager.Instance").Property("LevelData").Serialize();

Deserialize()

Deserialize JSON and set it as the property value. The JSON is automatically deserialized to match the property's type:

// Set a complex object from JSON
Search.Reflect("GameManager.Instance")
    .Property("PlayerData")
    .Deserialize(@"{ ""Name"": ""TestPlayer"", ""Score"": 100, ""Health"": 100.0 }");

// Set an array from JSON
Search.Reflect("Config.Instance")
    .Property("Waypoints")
    .Deserialize(@"[{""x"":0,""y"":1,""z"":0},{""x"":10,""y"":1,""z"":5}]");

// Restore previously saved state
Search.Reflect("LevelManager.Instance")
    .Property("LevelData")
    .Deserialize(savedState);

// Set test data for a test scenario
Search.Reflect("Inventory.Instance")
    .Property("Items")
    .Deserialize(@"[
        { ""Id"": ""sword"", ""Count"": 1 },
        { ""Id"": ""potion"", ""Count"": 5 }
    ]");

Common Use Cases

// Test setup - initialize game state from JSON
[SetUp]
public void SetUp()
{
    Search.Reflect("GameManager.Instance")
        .Property("PlayerData")
        .Deserialize(@"{ ""Name"": ""Test"", ""Level"": 1, ""XP"": 0 }");
}

// Snapshot and restore pattern
var originalState = Search.Reflect("Config.Instance").Property("Settings").Serialize();
try
{
    // Modify settings for test
    Search.Reflect("Config.Instance").Property("Settings")
        .Deserialize(@"{ ""Difficulty"": ""Hard"", ""Volume"": 0 }");

    // Run test...
}
finally
{
    // Restore original settings
    Search.Reflect("Config.Instance").Property("Settings").Deserialize(originalState);
}

SetValue

Set property or field values via reflection using .SetValue():

// Set property via Property() chain
Search.Reflect("GameManager.Instance")
    .Property("Score")
    .SetValue(100);

// Set on component accessed via Component()
Search.Reflect("Player.Instance")
    .Component<Rigidbody>()
    .Property("isKinematic")
    .SetValue(true);

// Set on UI element property
new Search().Name("VolumeSlider")
    .Property("value")
    .SetValue(0.5f);

// Chain multiple Property() calls then SetValue
Search.Reflect("Config.Instance")
    .Property("Audio")
    .Property("MasterVolume")
    .SetValue(0.8f);

Note: SetValue() only works on properties accessed via .Property(). It won't work directly on Search.Reflect() results.

Common Patterns

Game State Validation

using static ODDGames.UIAutomation.ActionExecutor;

[TestFixture]
public class GameStateTest
{
    [Test]
    public async Task VerifyGameState()
    {
        // Start game
        await Click("StartGame");

        // Wait for game to initialize
        await WaitFor(Search.Reflect("GameManager.Instance.IsPlaying"));

        // Verify initial state
        Assert.Greater(Search.Reflect("Player.Health").GetValue<float>(), 0);
        Assert.AreEqual(0, Search.Reflect("Player.Score").GetValue<int>());

        // Perform game actions
        await Click("CollectCoin");

        // Verify score increased
        Assert.Greater(Search.Reflect("Player.Score").GetValue<int>(), 0);
    }
}

Inventory Validation

// Check inventory contents
foreach (var item in Search.Reflect("Inventory.Items"))
{
    var itemName = item.Property("Name").GetValue<string>();
    var quantity = item.Property("Quantity").GetValue<int>();
    Assert.Greater(quantity, 0, $"{itemName} should have positive quantity");
}

// Check specific item
var sword = Search.Reflect("Inventory.Instance")
    .Property("EquippedWeapon");
Assert.AreEqual("IronSword", sword.Property("Name").GetValue<string>());
Assert.AreEqual(50, sword.Property("Damage").GetValue<int>());

Settings Validation

// Verify settings persistence
await Click("Settings");
await Click(Name("SoundToggle")); // Toggle sound off

// Check game state updated
Assert.IsFalse(Search.Reflect("Settings.Instance.SoundEnabled").GetValue<bool>());

// Verify slider value
await DragSlider(Name("VolumeSlider"), 0f, 0.75f);
Assert.AreEqual(0.75f, Search.Reflect("Settings.Instance.Volume").GetValue<float>(), 0.01f);

Related Pages

Clone this wiki locally