A rough concept based on GI’s interaction system.
*Code provided in this post is unoptimized and for demonstration purposes only.
This is used for proximity interactions such as picking up items, interacting with NPC’s, interacting with doors, etc. All gameobjects that need to appear at runtime should pooled, and state updates should be event-based to ensure scalability.
Features:
– Show a list of buttons on UI for nearby items within a certain range.
– When an item enters range, add its button to the bottom of the list.
– When an item leaves range, remove its button from the list, move up all the other buttons below it to fill the gap.
– A selection indicator (UI arrow icon) always follows the currently selected button, smoothly interpolating between positions.
– Use mouse scrollwheel to navigate list. Press key to select.
– All UI element motions use tweening.
We’re going to integrate this into a MVC-like UI system, using C# events to bind logic to UI code. Eg. when an item enters range, invoke an event. The UI interaction list controller receives this and spawns a pooled UI interaction option view prefab. This view should have a button clicked event that sends the corresponding interaction option (no direct reference to the interaction option needed, we can use something like index id to discern interaction options).
Object Pool
(Update 2022): This section is outdated. NekoLib provides a more optimized and flexible pooling solution. Alternatively, check out Unity’s new built in pooling solution.
This handles object pooling for the entire project. Only supports Game Objects, grabbing pooled items is often followed by GetComponent calls. Will look into this at a later time… Latest version of this singleton MonoBehaviour object pool can be found here: Centralized Object Pool
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// Manages an array of object pools.
/// </summary>
public class ObjectPool : Singleton<ObjectPool>
{
#region VARIABLES =====
public Pool[] pools;
public Dictionary<string, Pool> poolDict;
[System.Serializable]
public class Pool
{
public string tag;
public GameObject prefab; // Prefab to pool.
public int size = 10; // Amount to instantiate initially.
public int refill = 0; // Additional prefab instances to instantiate when needed.
public int maxSize = 30; // Max amount.
public LinkedList<GameObject> pooledObjects = new LinkedList<GameObject>();
private int count = 0;
// Get and remove the first pooled object from linked list.
public GameObject GrabFirst()
{
GameObject _go = pooledObjects.First.Value;
pooledObjects.RemoveFirst();
count -= 1;
return _go;
}
// Add pooled object to the head of linked list.
public void AddFirst(GameObject go)
{
pooledObjects.AddFirst(go);
count += 1;
}
// Add pooled object to the tail of linked list.
public void AddLast(GameObject go)
{
pooledObjects.AddLast(go);
count += 1;
}
// Instantiate prefabs to refill pool.
public void Refill()
{
if (refill > 0 && count + refill <= maxSize)
{
for (int i = 0; i < refill; i++)
{
GameObject _newGo = Instantiate(prefab);
_newGo.SetActive(false);
AddFirst(_newGo);
}
}
}
public GameObject Peek() { return pooledObjects.First.Value; }
public int GetCount() { return count; }
}
#endregion
protected override void Awake()
{
base.Awake();
poolDict = new Dictionary<string, Pool>();
foreach (Pool pool in pools)
{
// For each pool, populate a linked list with instantiated gameObjects;
for (int i = 0; i < pool.size; i++)
{
GameObject _go = Instantiate(pool.prefab);
_go.SetActive(false);
pool.AddLast(_go);
}
// Add to tag <-> Pool dictionary.
poolDict.Add(pool.tag, pool);
}
}
// Call from other scripts to get a pooled GameObject.
public GameObject SpawnFromPool(string tag, Vector3 position, Quaternion rotation, Transform parent = null)
{
if (poolDict.TryGetValue(tag, out Pool _pool))
{
// If next pooled object is active, we assume all pooled objects are now in use,
//... so we do a refill.
if (_pool.Peek().activeSelf == true)
_pool.Refill();
// "Dequeue" a GameObject.
GameObject _obj = _pool.GrabFirst();
if (_obj == null)
{
Debug.LogWarning("Pooled object" + tag + "is null");
return null;
}
// Initialize its transform.
if (parent != null)
_obj.transform.SetParent(parent);
_obj.transform.position = position;
_obj.transform.rotation = rotation;
//obj.SetActive(true);
// "Queue" this GameObject to end of linked list.
_pool.AddLast(_obj);
return _obj;
}
else
{
Debug.LogWarning("Object pool with tag " + tag + " doesn't exist");
return null;
}
}
}
Interactable
Interactable items implement an interface.
It is fine to just use colliders for collision detection. But if you want to try some (potentially unecessary) micro-optimization, poll for InteractionController at set intervals. Intervals can be extended or randomized to save performance. Collision can be detected by position difference sqrMagnitude or OverlapSphere. Something like:
private IEnumerator CheckDistance()
{
while (true)
{
float _distanceSqr = (transform.position - target.position).sqrMagnitude;
if (_distanceSqr <= radius * radius)
{
// ...
}
yield return new WaitForSeconds(0.2f);
}
}
UI Interaction List Controller
Following a loose MVC pattern. We can have a controller that manages collection of interactables, and pushes changes to the corresponding view .
public void AddInteractable(Interactable interactable)
{
// Add this interactable to list;
ProximityList.Add(interactable);
canSelect = true;
// Add this displayed item on UI;
AddDisplayItem(interactable);
// Update currently selected interactable item and UI indicator position;
UpdateSelection();
}
public void RemoveInteractable(Interactable interactable)
{
// Keep track of interactables avaliable for selection;
curInteractableCount -= 1;
if(curInteractableCount == 0) canSelect = false;
// Maintain current selection index if we want to remove an index before it;
// Get index in list and selection UI;
int _idx = ProximityList.IndexOf(interactable);
if (_idx <= curIndex)
{
if (curIndex > 0) curIndex -= 1;
}
//Debug.Log("Removed interactable at: " + _idx);
// Update currently selected interactable item and UI indicator position;
UpdateSelection();
// Remove this displayed item from UI;
RemoveDisplayItem(_idx);
// Remove this interactable;
ProximityList.Remove(interactable);
}
// Add a displayed item to interactable selection UI;
public void AddDisplayItem(Interactable interactable)
{
// New displayed item's position based on curInteractable count;
Vector2 _pos = new Vector2(
interactableCanvas.initialPos.x,
interactableCanvas.initialPos.y + (interactableCanvas.posMarginY * ((float)curInteractableCount + 1))
);
InteractableDisplay _display = objectPool.SpawnFromPool("UI_InteractablePrefab",
transform.position, Quaternion.identity, interactableCanvas.displayPanel.transform)
.GetComponent<InteractableDisplay>();
_display.displayName.SetText(interactable.displayName); // Set displayed item's text;
_display.rectTransform.localPosition = _pos; // Set display item's position;
interactableCanvas.displayList.Add(_display);
curInteractableCount += 1; // Keep track of interactables avaliable for selection;
}
// Remove a displayed item from interactable selection UI;
public void RemoveDisplayItem(int idx)
{
// Remove this index in interactableList Canvas,
// Move other displays below it (i.e. with higher index) up by posMarginY;
interactableCanvas.RemoveDisplay(idx);
}
UI Interaction Canvas
The canvas view for a single interaction option. This class should expose a button click event for the corresponding interaction option.
public void RemoveDisplay(int idx)
{
// Remove displayed item at this index;
// Return to pool and remove from displayList;
int _count = displayList.Count;
for (int a = 0; a < _count; a++)
{
if (a == idx)
{
if(displayList[a]!=null)
{
displayList[a].gameObject.SetActive(false);
break;
}
}
}
displayList.Remove(displayList[idx]);
if (idx >= _count - 1) return;
// For remaining display items with higher index;
// move each display item upwards by one margin to fill the gap;
for (int a = idx; a < _count - 1; a++)
{
Vector3 _newPos = new Vector2(
displayList[a].rectTransform.localPosition.x,
displayList[a].rectTransform.localPosition.y - posMarginY);
displayList[a].TweenToPos(_newPos);
}
}
tbc…