**Approach (brief)** Use UnityEngine.EventSystems to find the currently selected element, collect all selectable UI elements that are active and interactable, compute best candidate in given direction using dot product / angle + distance tie-breaker, support optional wrap-around by allowing search to continue on opposite side, then set EventSystem.current.SetSelectedGameObject(candidate).csharp
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using System.Linq;
using System.Collections.Generic;
public static class FocusMover {
// direction: normalized (x,y) where x>0 is right etc.
public static void MoveFocus(Vector2 direction, bool wrap) {
var es = EventSystem.current;
if (es == null) return;
var currentGO = es.currentSelectedGameObject;
// collect all Selectable that are active and interactable
var all = Selectable.allSelectablesArray
.Where(s => s != null && s.IsActive() && s.IsInteractable())
.Select(s => s.gameObject).ToList();
if (!all.Any()) { es.SetSelectedGameObject(null); return; }
Vector2 fromPos;
if (currentGO != null) {
var rc = currentGO.GetComponent<RectTransform>();
fromPos = rc != null ? (Vector2)rc.TransformPoint(rc.rect.center) : (Vector2)currentGO.transform.position;
} else {
// if nothing selected pick center of screen
fromPos = new Vector2(Screen.width / 2f, Screen.height / 2f);
}
GameObject best = null;
float bestScore = float.NegativeInfinity;
// helper to evaluate candidates; if tie break by closer distance
System.Action<GameObject> eval = (go) => {
if (go == currentGO) return;
var rc = go.GetComponent<RectTransform>();
Vector2 pos = rc != null ? (Vector2)rc.TransformPoint(rc.rect.center) : (Vector2)go.transform.position;
Vector2 to = (pos - fromPos);
float dist = to.magnitude;
if (dist < 0.01f) return;
Vector2 dirTo = to / dist;
float alignment = Vector2.Dot(direction.normalized, dirTo); // -1..1
if (alignment <= 0 && !wrap) return; // not in forward half
// score favors alignment then closeness
float score = alignment * 1000f - dist;
if (score > bestScore) { bestScore = score; best = go; }
};
foreach (var go in all) eval(go);
// if wrap enabled and no candidate found, allow any direction (pick nearest in rotated space)
if (best == null && wrap) {
foreach (var go in all) eval(go);
}
es.SetSelectedGameObject(best);
}
}
Edge cases and notes:- Use RectTransform.TransformPoint to get world screen coords for accurate spatial picks in canvases with different render modes.- Ensure direction is non-zero; normalize before calling.- Skip Selectables that are inactive, not interactable, or not raycastable if needed.- If multiple candidates tie, break by distance or explicit ordering (e.g., navigation overrides).- Consider controller deadzone, repeated input (use input buffering/cooldown), and keyboard repeat behavior.