Mise en œuvre Dans le développement avec Unity, un système de sélection d'objets est crucial pour les outils de l'éditeur ou les fonctionnalités interactives. Cet article explique comment créer un gestionnaire de sélection qui supporte la sélection par encadrement et la détection de l'inclusion complète, idéal pour des scènes d'édition ou de gestion de ressources nécessitant une sélection précise.
Instructions d'utilisation
- Attachez le script au caméra principale (Main Camera).
- Dans l'Inspector, configurez : - Les couches sélectionnables (selectableLayers), le matériau du cadre de sélection (selectionBoxMaterial) et le canvas.
- Activez la détection d'inclusion complète en cochant requireFullyContained.
- Liez l'événement onSelectionChanged pour gérer les changements de sélection.
Fonctionnement
- En faisant glisser la souris, un cadre semi-transparent apparaît. Lorsque la souris est relâchée, les objets sont sélectionnés selon le mode choisi.
- Mode d'inclusion complète : seuls les objets entièrement inclus dans le cadre sont sélectionnés, ce qui est utile pour des sélections précises dans des scènes denses.
Logique
- Récupération des objets visibles dans la vue de la caméra.
- Affichaeg du cadre de sélection sur l'UI.
- Vérification de la position des objets sur l'écran pour déterminer s'ils sont dans le cadre de sélection.
- Sélection des objets et déclenchement de l'événement de sélection.
Code
GestionnaireDeSélection.cs
using UnityEngine;
using System.Collections.Generic;
using System.Linq;
using UnityEngine.EventSystems;
using UnityEngine.UI;
/// <summary>
/// Gestionnaire de sélection d'objets, supportant la sélection unique et multiple.
/// Note : Ce script doit être attaché à la caméra principale.
/// </summary>
public class GestionnaireDeSélection : MonoBehaviour
{
[Header("Configuration de la sélection")]
[SerializeField] private LayerMask selectableLayers = -1; // Masque de couche pour les objets sélectionnables
[SerializeField] private Material selectionBoxMaterial; // Matériau du cadre de sélection
[SerializeField] private Canvas canvas; // Canvas pour afficher le cadre de sélection
[SerializeField] private Image selectionBoxImage; // Composant d'image du cadre de sélection
[SerializeField] private float minSelectionArea = 100f; // Aire minimale du cadre de sélection (en pixels²)
[SerializeField] private float selectionDelay = 0.1f; // Délai de confirmation de la sélection (en secondes)
[Header("Mode de sélection")]
[SerializeField] private bool requireFullyContained; // Exiger que tous les bounds soient complètement inclus
[Header("Système d'événements")]
public UnityEngine.Events.UnityEvent<List<GameObject>> onSelectionChanged; // Événement de changement de sélection
private Camera mainCamera; // Référence à la caméra principale
private Vector3 selectionStartPosition; // Position de départ de la sélection sur l'écran
private Vector3 selectionEndPosition; // Position de fin de la sélection sur l'écran
private bool isSelecting; // Indicateur de sélection en cours
private bool isMouseDown; // Indicateur de clic de la souris
private float lastSelectionTime; // Timestamp de la dernière sélection
private readonly List<GameObject> selectedObjects = new(); // Liste des objets sélectionnés
private void Start()
{
mainCamera = Camera.main;
if (!selectionBoxMaterial)
{
selectionBoxMaterial = new Material(Shader.Find("Unlit/Color"))
{
color = new Color(0, 0.5f, 1, 0.3f)
};
}
if (!selectionBoxImage)
{
CreateSelectionBoxUI();
}
}
private void CreateSelectionBoxUI()
{
var selectionBoxObj = new GameObject("SelectionBox");
selectionBoxObj.transform.SetParent(canvas.transform, false);
selectionBoxImage = selectionBoxObj.AddComponent<Image>();
var rectTransform = selectionBoxImage.rectTransform;
rectTransform.anchorMin = new Vector2(0, 1);
rectTransform.anchorMax = new Vector2(0, 1);
rectTransform.pivot = new Vector2(0, 1);
selectionBoxImage.color = new Color(0, 0.5f, 1, 0.1f);
selectionBoxImage.enabled = false;
}
private void Update()
{
if (Input.GetMouseButtonUp(0) && isMouseDown)
{
EndSelection();
}
if (EventSystem.current.IsPointerOverGameObject())
return;
if (Input.GetMouseButtonDown(0))
{
BeginSelection();
}
if (isSelecting)
{
UpdateSelection();
}
}
private void BeginSelection()
{
isSelecting = true;
isMouseDown = true;
selectionStartPosition = Input.mousePosition;
selectionBoxImage.enabled = true;
}
private void UpdateSelection()
{
selectionEndPosition = Input.mousePosition;
UpdateSelectionBoxVisual();
}
private void EndSelection()
{
isSelecting = false;
isMouseDown = false;
selectionEndPosition = Input.mousePosition;
UpdateSelectionBoxVisual();
if (Time.time - lastSelectionTime >= selectionDelay && IsSelectionAreaValid())
{
PerformSelection();
lastSelectionTime = Time.time;
}
selectionBoxImage.enabled = false;
}
private void PerformSelection()
{
ClearSelection();
var selectionRect = GetScreenSelectionRect();
Debug.Log($"[Zone de sélection] Rect: {selectionRect}");
var visibleObjects = GetVisibleObjectsInCamera();
foreach (var obj in from obj in visibleObjects
where IsObjectSelectable(obj)
let worldPos = obj.transform.position
let screenPos = mainCamera.WorldToScreenPoint(worldPos)
where IsObjectInSelectionRect(obj, selectionRect)
select obj)
{
SelectObject(obj);
}
Debug.Log($"[Nombre d'objets sélectionnés] {selectedObjects.Count} objets");
onSelectionChanged?.Invoke(selectedObjects);
}
private List<GameObject> GetVisibleObjectsInCamera()
{
var frustumPlanes = GeometryUtility.CalculateFrustumPlanes(mainCamera);
var allRenderers = FindObjectsOfType<Renderer>();
var result = (from renderer in allRenderers
where GeometryUtility.TestPlanesAABB(frustumPlanes, renderer.bounds) &&
IsObjectSelectable(renderer.gameObject)
select renderer.gameObject).ToList();
return result;
}
private bool IsObjectSelectable(GameObject obj)
{
return ((1 << obj.layer) & selectableLayers.value) != 0;
}
private bool IsObjectInSelectionRect(GameObject obj, Rect selectionRect)
{
if (!requireFullyContained)
{
var screenPos = mainCamera.WorldToScreenPoint(obj.transform.position);
var canvasHeight = canvas.GetComponent<RectTransform>().sizeDelta.y;
var screenPosInCanvas = new Vector2(
screenPos.x,
canvasHeight - screenPos.y
);
return selectionRect.Contains(screenPosInCanvas);
}
return AreAllBoundsInSelection(obj, selectionRect);
}
private bool AreAllBoundsInSelection(GameObject obj, Rect selectionRect)
{
var renderer = obj.GetComponent<Renderer>();
if (!renderer) return false;
var bounds = renderer.bounds;
var vertices = new[]
{
bounds.min,
new(bounds.min.x, bounds.min.y, bounds.max.z),
new(bounds.min.x, bounds.max.y, bounds.min.z),
new(bounds.min.x, bounds.max.y, bounds.max.z),
new(bounds.max.x, bounds.min.y, bounds.min.z),
new(bounds.max.x, bounds.min.y, bounds.max.z),
new(bounds.max.x, bounds.max.y, bounds.min.z),
bounds.max
};
return (from vertex in vertices
select mainCamera.WorldToScreenPoint(vertex)
into screenPos
let canvasHeight = canvas.GetComponent<RectTransform>().sizeDelta.y
select new Vector2(screenPos.x, canvasHeight - screenPos.y))
.All(selectionRect.Contains);
}
private Rect GetScreenSelectionRect()
{
var min = Vector3.Min(selectionStartPosition, selectionEndPosition);
var max = Vector3.Max(selectionStartPosition, selectionEndPosition);
var canvasHeight = canvas.GetComponent<RectTransform>().sizeDelta.y;
return new Rect(
min.x,
canvasHeight - max.y,
max.x - min.x,
max.y - min.y
);
}
private void SelectObject(GameObject obj)
{
selectedObjects.Add(obj);
var highlight = obj.GetComponent<MiseEnLumière>() ??
obj.AddComponent<MiseEnLumière>();
highlight.MettreEnLumière();
}
private void ClearSelection()
{
foreach (var highlight in selectedObjects.Select(obj => obj.GetComponent<MiseEnLumière>())
.Where(highlight => highlight))
{
highlight.SupprimerMiseEnLumière();
}
selectedObjects.Clear();
}
private bool IsSelectionAreaValid()
{
var selectionRect = GetScreenSelectionRect();
return selectionRect.width * selectionRect.height >= minSelectionArea;
}
private void UpdateSelectionBoxVisual()
{
if (!selectionBoxImage || !selectionBoxImage.gameObject.activeInHierarchy)
return;
var selectionRect = GetScreenSelectionRect();
var rectTransform = selectionBoxImage.rectTransform;
rectTransform.anchoredPosition = new Vector2(selectionRect.xMin, -selectionRect.yMin);
rectTransform.sizeDelta = new Vector2(selectionRect.width, selectionRect.height);
}
}
MiseEnLumière.cs
using UnityEngine;
/// <summary>
/// Fonction de mise en lumière des objets sélectionnés
/// </summary>
public class MiseEnLumière : MonoBehaviour
{
private Material originalMaterial;
private Renderer objectRenderer;
private static Material highlightMaterial;
[SerializeField] private Color highlightColor = Color.cyan;
private void Awake()
{
objectRenderer = GetComponent<Renderer>();
if (objectRenderer != null)
originalMaterial = objectRenderer.material;
if (highlightMaterial == null)
{
highlightMaterial = new Material(Shader.Find("Unlit/Color"));
highlightMaterial.color = highlightColor;
}
}
public void MettreEnLumière()
{
if (objectRenderer != null)
objectRenderer.material = highlightMaterial;
}
public void SupprimerMiseEnLumière()
{
if (objectRenderer != null && originalMaterial != null)
objectRenderer.material = originalMaterial;
}
private void OnDestroy()
{
SupprimerMiseEnLumière();
}
}