additional package setup

This commit is contained in:
2025-11-16 18:31:17 -05:00
parent 3da42beb46
commit 2ca8077013
55 changed files with 1746 additions and 12 deletions

View File

@@ -0,0 +1,92 @@
using System.Collections;
using UnityEngine;
using UnityEngine.Audio;
namespace Boxfriend.Utils
{
public class AudioManager : SingletonBehaviour<AudioManager>
{
private ObjectPoolCircular<AudioSource> _sources;
[SerializeField] private AudioMixerGroup _audioMixer;
[SerializeField] private AudioSource _sourcePrefab;
private const string _inWaitingName = "AudioManager - Ready";
private void Awake () => _sources = new ObjectPoolCircular<AudioSource>(Create, x => x.enabled = true, ReturnSource, DestroySource, 32);
private AudioSource Create ()
{
AudioSource source;
if (_sourcePrefab == null)
{
var go = new GameObject
{
name = _inWaitingName
};
go.transform.parent = transform;
source = go.AddComponent<AudioSource>();
} else
{
source = Instantiate(_sourcePrefab, Vector3.zero, Quaternion.identity);
}
source.outputAudioMixerGroup = _audioMixer;
source.enabled = false;
return source;
}
private AudioSource GetSource (string clipName, Vector3 position)
{
var source = _sources.FromPool();
source.name = $"AudioManager - Playing: {clipName}";
source.transform.position = position;
return source;
}
private void ReturnSource (AudioSource source)
{
source.name = _inWaitingName;
source.clip = null;
#if UNITY_2023_2_OR_NEWER
source.resource = null;
#endif
source.enabled = false;
}
private void DestroySource (AudioSource source) => Destroy(source.gameObject);
public void PlayOneShot (AudioClip clip, float volume = 1f) => PlayOneShot(clip, Vector3.zero, volume);
public void PlayOneShot (AudioClip clip, Vector3 position, float volume = 1f)
{
var source = GetSource(clip.name, position);
source.PlayOneShot(clip, volume);
StartCoroutine(ReturnWhenDone(source));
}
#if !UNITY_2023_2_OR_NEWER
public void Play (AudioClip clip) => Play(clip, Vector3.zero);
public void Play (AudioClip clip, Vector3 position)
{
var source = GetSource(clip.name, position);
source.clip = clip;
source.Play();
StartCoroutine(ReturnWhenDone(source));
}
#else
public void Play (AudioResource resource) => Play(resource, Vector3.zero);
public void Play(AudioResource resource, Vector3 position)
{
var source = GetSource(resource.name, position);
source.resource = resource;
source.Play();
StartCoroutine(ReturnWhenDone(source));
}
#endif
private IEnumerator ReturnWhenDone (AudioSource source)
{
yield return new WaitUntil(() => !source.isPlaying);
_sources.ToPool(source);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 83b4266b5af7b0e43bdc7a1b68fc823d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
namespace Boxfriend.Utils
{
public class EventManager : Singleton<EventManager>
{
public delegate void Event(object arg, object sender);
private readonly Dictionary<string, Event> _events = new ();
public void RegisterEvent(string name)
{
if (_events.ContainsKey(name))
throw new ArgumentException($"Event {name} already registered");
_events.Add(name, null);
}
public void RegisterEvent (string name, Event callback)
{
if (_events.ContainsKey(name))
throw new ArgumentException($"Event {name} already registered");
_events.Add(name, callback);
}
public void UnregisterEvent(string name)
{
if (!_events.ContainsKey(name))
throw new ArgumentException($"Event {name} not registered");
_events.Remove(name);
}
public void SubscribeEvent (string name, Event callback)
{
if (!_events.ContainsKey(name))
throw new ArgumentException($"Event {name} not registered");
_events[name] += callback ?? throw new ArgumentNullException($"Event {name} callback is null");
}
public void UnsubscribeEvent (string name, Event callback)
{
if (!_events.ContainsKey(name))
throw new ArgumentException($"Event {name} not registered");
_events[name] -= callback ?? throw new ArgumentNullException($"Event {name} callback is null");
}
public void InvokeEvent (string name, object arg, object sender)
{
if (!_events.ContainsKey(name))
throw new ArgumentException($"Event {name} not registered");
_events[name]?.Invoke(arg, sender);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 11ec534d1045abc468621d74f78bfe8e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,7 @@
using System;
///<summary>
/// Indicates a variable should only be assigned to in the inspector. Also allowed are field initializers and assignment in unity's Reset method.
/// Requires Boxfriend.Analyzers to function.
///</summary>
public class InspectorOnlyAttribute : Attribute { }

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0c345dad59be440eadbaa7290ff86835

View File

@@ -0,0 +1,97 @@
using System;
using System.Collections.Generic;
namespace Boxfriend.Utils
{
/// <summary>
/// Pools objects of type T, will create new objects as necessary
/// </summary>
public class ObjectPool<T> where T : class
{
private readonly Stack<T> _stack = new ();
private readonly Func<T> _objectCreator;
private readonly Action<T> _returnObjectToPool, _getObjectFromPool, _destroyObject;
private readonly int _maxSize;
/// <summary>
/// Number of objects currently in the pool.
/// </summary>
public int Count => _stack.Count;
/// <param name="createObject">Creates and returns an object of the specified type.</param>
/// <param name="getObjectFromPool">Action called on object when pulled from the pool or created.</param>
/// <param name="returnObjectToPool">Action called on object when returned to pool.</param>
/// <param name="onDestroyObject">Action called on object when it is to be destroyed. Can be null</param>
/// <param name="defaultSize">Number of objects to immediately add to the pool</param>
/// <param name="maxSize">Maximum number of objects in the pool</param>
/// <exception cref="ArgumentOutOfRangeException"></exception>
/// <exception cref="ArgumentNullException"></exception>
public ObjectPool (Func<T> createObject, Action<T> getObjectFromPool, Action<T> returnObjectToPool, Action<T> onDestroyObject = null, int defaultSize = 10, int maxSize = 100)
{
if (maxSize < defaultSize)
throw new ArgumentOutOfRangeException(nameof(maxSize), "maxSize must be greater than or equal to defaultSize");
if (defaultSize < 0)
throw new ArgumentOutOfRangeException(nameof(defaultSize), "defaultSize must be greater than or equal to 0");
_returnObjectToPool = returnObjectToPool ?? throw new ArgumentNullException(nameof(returnObjectToPool));
_getObjectFromPool = getObjectFromPool ?? throw new ArgumentNullException(nameof(getObjectFromPool));
_objectCreator = createObject ?? throw new ArgumentNullException(nameof(createObject));
_destroyObject = onDestroyObject;
_maxSize = maxSize;
for (var i = 0; i < defaultSize; i++)
{
ToPool(_objectCreator());
}
}
/// <summary>
/// Gets an object from the pool or creates a new one if the pool is empty. Calls <see langword="Action"/> <see cref="_getObjectFromPool"/> on the object
/// </summary>
public T FromPool ()
{
var poolObject = _stack.Count > 0 ? _stack.Pop() : _objectCreator();
_getObjectFromPool(poolObject);
return poolObject;
}
/// <summary>
/// Adds an item to the pool and calls <see langword="Action"/> <see cref="_returnObjectToPool"/> on it
/// </summary>
/// <param name="item">Item to be added</param>
public void ToPool (T item)
{
if (item == null) throw new ArgumentNullException(nameof(item));
_returnObjectToPool(item);
if(_stack.Count >= _maxSize)
{
_destroyObject?.Invoke(item);
return;
}
_stack.Push(item);
}
/// <summary>
/// Removes all items from the pool, calling <see langword="Action"/> <see cref="_destroyObject"/> on it if not null.
/// </summary>
public void EmptyPool()
{
if(_destroyObject is null)
{
_stack.Clear();
return;
}
while(_stack.Count > 0)
{
var obj = _stack.Pop();
_destroyObject(obj);
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d79c194a0d7106f4dae9d2b503bae707
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,145 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Boxfriend.Utils
{
/// <summary>
/// Pools a specific number of objects, will reuse oldest active objects when all objects are in use.
/// </summary>
public class ObjectPoolCircular<T> where T : class
{
private Queue<T> _activeQueue, _inactiveQueue;
private readonly Func<T> _objectCreator;
private readonly Action<T> _returnObjectToPool, _getObjectFromPool, _destroyObject;
private readonly int _size;
/// <summary>
/// Total number of objects in the pool.
/// </summary>
public int Count => _size;
/// <summary>
/// Total number of currently active pooled objects
/// </summary>
public int ActiveCount => _activeQueue.Count;
/// <summary>
/// Total number of currently inactive pooled objects
/// </summary>
public int InactiveCount => _inactiveQueue.Count;
/// <param name="createObject">Creates and returns an object of the specified type.</param>
/// <param name="getObjectFromPool">Action called on object when pulled from the pool or created.</param>
/// <param name="returnObjectToPool">Action called on object when returned to pool.</param>
/// <param name="onDestroyObject">Action called on object when it is to be destroyed. Can be null</param>
/// <param name="size">Total number of objects in the pool</param>
/// <exception cref="ArgumentOutOfRangeException">Size must be greater than zero</exception>
/// <exception cref="ArgumentNullException"></exception>
public ObjectPoolCircular (Func<T> createObject, Action<T> getObjectFromPool, Action<T> returnObjectToPool, Action<T> onDestroyObject = null, int size = 100)
{
if (size <= 0)
throw new ArgumentOutOfRangeException(nameof(size), "size must be greater than zero");
_returnObjectToPool = returnObjectToPool ?? throw new ArgumentNullException(nameof(returnObjectToPool));
_getObjectFromPool = getObjectFromPool ?? throw new ArgumentNullException(nameof(getObjectFromPool));
_objectCreator = createObject ?? throw new ArgumentNullException(nameof(createObject));
_destroyObject = onDestroyObject;
_size = size;
_inactiveQueue = new(size);
_activeQueue = new(size);
for (var i = 0; i < size; i++)
{
var obj = _objectCreator();
_returnObjectToPool(obj);
_inactiveQueue.Enqueue(obj);
}
}
/// <summary>
/// Gets an object from the pool or reuses the oldest active object if all pooled objects are in use. Calls <see langword="Action"/> <see cref="_getObjectFromPool"/> on the object
/// Will call <see langword="Action"/> <see cref="_returnObjectToPool"/> if reusing an active object.
/// </summary>
public T FromPool ()
{
if(_inactiveQueue.Count + _activeQueue.Count == 0)
throw new InvalidOperationException("Object pool has been cleared, there is nothing left to get");
T poolObject;
if(_inactiveQueue.Count == 0)
{
poolObject = _activeQueue.Dequeue();
_returnObjectToPool(poolObject);
}
else
{
poolObject = _inactiveQueue.Dequeue();
}
_getObjectFromPool(poolObject);
_activeQueue.Enqueue(poolObject);
return poolObject;
}
/// <summary>
/// Adds an item to the pool and calls <see langword="Action"/> <see cref="_returnObjectToPool"/> on it
/// Generates garbage if item is not the oldest object pulled from the pool
/// </summary>
/// <param name="item">Item to be added</param>
public void ToPool (T item)
{
if (item == null) throw new ArgumentNullException(nameof(item));
if(_activeQueue.Peek() == item)
_activeQueue.Dequeue();
else
_activeQueue = new Queue<T>(_activeQueue.Where(x => x != item));
_returnObjectToPool(item);
_inactiveQueue.Enqueue(item);
}
/// <summary>
/// Returns all active items to the inactive queue
/// </summary>
public void ReturnAllToPool()
{
while(ActiveCount > 0)
{
var obj = _activeQueue.Dequeue();
_returnObjectToPool(obj);
_inactiveQueue.Enqueue(obj);
}
}
/// <summary>
/// Removes all items from the pool, calling <see langword="Action"/> <see cref="_destroyObject"/> on it if not null.
/// Does not call <see langword="Action"/> <see cref="_returnObjectToPool"/>.
/// </summary>
public void EmptyPool()
{
if(_destroyObject is null)
{
_activeQueue.Clear();
_inactiveQueue.Clear();
return;
}
while(_activeQueue.Count > 0)
{
var obj = _activeQueue.Dequeue();
_destroyObject(obj);
}
while(_inactiveQueue.Count > 0)
{
var obj = _inactiveQueue.Dequeue();
_destroyObject(obj);
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c9d2a88b19aa9ad4ab0b9adb718f2a0a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,81 @@
using UnityEngine;
namespace Boxfriend.Utils
{
/// <summary>
/// Class to draw debug information such as physics2d casts
/// </summary>
public static class PhysicsCastDebug
{
/// <summary>
/// Casts a Physics2D BoxCast with debug lines drawn
/// </summary>
public static RaycastHit2D BoxCast(Vector2 origin,
Vector2 size,
float angle,
Vector2 direction,
float distance = 0,
int layerMask = Physics2D.AllLayers,
float minDepth = -Mathf.Infinity,
float maxDepth = Mathf.Infinity)
{
var hit = Physics2D.BoxCast(origin, size, angle, direction, distance, layerMask, minDepth, maxDepth);
//Setting up the points to draw the origin box and end box
var points = new Vector2[8];
var width = size.x * 0.5f;
var height = size.y * 0.5f;
points[0] = new Vector2(-width, height); //Upper left corner
points[1] = new Vector2(width, height); //Upper right corner
points[2] = new Vector2(width, -height); //Lower right corner
points[3] = new Vector2(-width, -height); //Lower left corner
//Calculates origin box corners using provided angle and origin point
var q = Quaternion.AngleAxis(angle, new Vector3(0, 0, 1));
for (var i = 0; i < 4; i++)
{
points[i] = q * points[i];
points[i] += origin;
}
//Calculates end points using origin box points and provided distance
var realDistance = direction.normalized * distance;
for (var i = 0; i < 4; i++)
{
points[i + 4] = points[i] + realDistance;
}
//Draw hit normal if a hit was detected
if (hit) Debug.DrawLine(hit.point, hit.point + hit.normal.normalized*0.2f, Color.yellow);
//Draw boxes
var color = hit ? Color.green : Color.red;
for (var i = 0; i < 4; i++)
{
var j = i == 3 ? 0 : i + 1;
//Draws origin box using first 4 points
Debug.DrawLine(points[i],points[j], color);
}
//Exit early if distance is 0, don't need to draw end position or translation if there is no distance
if (distance == 0) return hit;
//Draws end box using last 4 points
for (var i = 0; i < 4; i++)
{
var j = i == 3 ? 0 : i + 1;
Debug.DrawLine(points[i+4],points[j+4], color);
}
//Shows translation from origin box to end box in grey
for (var i = 0; i < 4; i++)
{
var j = i + 4;
Debug.DrawLine(points[i],points[j], Color.grey);
}
return hit;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 749412a4d084e6d43902f628e84d38e7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,14 @@
using System;
namespace Boxfriend.Utils
{
public abstract class Singleton<T> where T : class, new()
{
public static T Instance => _instance;
private static T _instance;
private static T InitializeSingleton (T obj = null) => _instance = obj ?? new T();
public Singleton():this(null){}
public Singleton (T obj) => InitializeSingleton(obj);
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d5aa066b76ef4b48b79d2921c301f50f
timeCreated: 1640819249

View File

@@ -0,0 +1,40 @@
using UnityEngine;
namespace Boxfriend.Utils
{
public abstract class SingletonBehaviour<T> : MonoBehaviour where T : SingletonBehaviour<T>
{
private static T _instance;
[SerializeField] protected bool _dontDestroy;
public static T Instance
{
get
{
if( _instance == null )
{
var go = new GameObject(typeof(T).Name);
go.AddComponent<T>();
}
return _instance;
}
private set
{
if (_instance == null)
_instance = value;
else if (value != _instance)
Destroy(value.gameObject);
}
}
protected virtual void __internalAwake ()
{
Instance = (T)this;
if(_dontDestroy)
DontDestroyOnLoad(gameObject);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a532c8d866cc4ff1a614091cc5e3c80b
timeCreated: 1640820898

View File

@@ -0,0 +1,89 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Boxfriend.Utils
{
/// <summary>
/// Simple timer class that will call an action on timer complete
/// </summary>
public class TimerUtil
{
public static TimerUtil Timer (Action action, float time, string name = "TimerObject")
{
GameObject timerObj = new GameObject(name, typeof(TimerMonoBehaviour));
TimerUtil timerUtil = new TimerUtil(action, time, timerObj);
timerObj.GetComponent<TimerMonoBehaviour>().onUpdate = timerUtil.UpdateTimer;
return timerUtil;
}
private class TimerMonoBehaviour : MonoBehaviour
{
public Action onUpdate;
private void Update ()
{
onUpdate();
}
}
private Action _act;
private float _time;
private GameObject _timerObj;
public bool isEnded { get; private set; }
public bool isPaused { get; set; }
public float TimeRemaining => _time;
private TimerUtil (Action action, float time, GameObject timerObj)
{
_act = action;
_time = time;
_timerObj = timerObj;
//Ensuring the bools are correctly initialized as false
isEnded = false;
isPaused = false;
}
private void UpdateTimer ()
{
if (isEnded || isPaused) return;
_time -= Time.deltaTime;
if (_time <= 0)
{
EndWithAction();
}
}
/// <summary>
/// Ends the timer and destroys associated GameObject. Cannot be undone
/// </summary>
public void EndTimer ()
{
isEnded = true;
UnityEngine.Object.Destroy(_timerObj);
}
/// <summary>
/// Ends the timer and invokes its action. Cannot be undone.
/// </summary>
public void EndWithAction ()
{
_act();
EndTimer();
}
/// <summary>
/// Adds specified time to the currently active timer
/// </summary>
public void AddTime (float time)
{
if (isEnded) return;
_time += time;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2f923888d5435594a9fd44c8ba579913
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,36 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Boxfriend.Utils
{
/// <summary>
/// Random useful methods
/// </summary>
public class Utils
{
/// <summary>
/// Determines if the supplied ints have opposite signs
/// </summary>
public static bool OppositeSigns (int x, int y)
{
return ((x ^ y) < 0);
}
/// <summary>
/// Formats number of bytes to string
/// </summary>
public static string FormatBytes(long bytes)
{
string[] Suffix = {"B", "KB", "MB", "GB", "TB"};
int i;
double dblSByte = bytes;
for (i = 0; i < Suffix.Length && bytes >= 1000; i++, bytes /= 1000)
{
dblSByte = bytes / 1000.0;
}
return $"{dblSByte:0.##} {Suffix[i]}";
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cca214112e1e41a48b15b4b1e145cbd4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: