Foundation utilities for HelloDev packages. This is the base package that other HelloDev packages depend on.
- RuntimeScriptableObject - Abstract base class that auto-resets ScriptableObject state between play sessions
- UnityEvent Extensions - Safe event handling (
SafeInvoke,SafeSubscribe,SafeUnsubscribe) with support for 0-4 parameters - Transform Extensions - Transform and GameObject utilities (
DestroyAllChildrenwith optional condition filter) - Bootstrap Support -
IBootstrapInitializableinterface for controlled system initialization - GameContext - Generic service container for decoupled manager registration and lookup
- Locator Pattern -
LocatorBase_SObase class for locator ScriptableObjects - Tweening Abstractions -
ITweenProvider,ITweenHandle,EaseTypefor animation abstraction
Via Package Manager (Local):
- Open Unity Package Manager (Window > Package Manager)
- Click "+" > "Add package from disk"
- Navigate to this folder and select
package.json
The most common use case is creating ScriptableObjects that need runtime state:
using HelloDev.Utils;
using UnityEngine;
[CreateAssetMenu(menuName = "Game/Player Stats")]
public class PlayerStats_SO : RuntimeScriptableObject
{
[SerializeField] private int maxHealth = 100;
// Runtime state (not serialized, resets each play session)
private int _currentHealth;
public int CurrentHealth => _currentHealth;
public int MaxHealth => maxHealth;
protected override void OnScriptableObjectReset()
{
// Called automatically when entering play mode
_currentHealth = maxHealth;
}
public void TakeDamage(int amount)
{
_currentHealth = Mathf.Max(0, _currentHealth - amount);
}
}using HelloDev.Utils;
using UnityEngine.Events;
public class GameManager : MonoBehaviour
{
public UnityEvent<int> onScoreChanged;
void Start()
{
// Safe subscribe - prevents duplicate subscriptions
onScoreChanged.SafeSubscribe(HandleScoreChanged);
}
void OnDestroy()
{
// Null-safe unsubscribe
onScoreChanged.SafeUnsubscribe(HandleScoreChanged);
}
void AddScore(int points)
{
// Null-safe invoke
onScoreChanged.SafeInvoke(points);
}
void HandleScoreChanged(int newScore) { }
}- Open Unity Package Manager
- Click "+" > "Add package from disk"
- Navigate to this folder and select
package.json
Base class for ScriptableObjects that need state reset when entering play mode:
using HelloDev.Utils;
public class GameState_SO : RuntimeScriptableObject
{
private int cachedScore;
protected override void OnScriptableObjectReset()
{
cachedScore = 0; // Reset on play mode enter
}
}
// Access the description field
string desc = myScriptableObject.Description;Null-safe event operations that prevent duplicate subscriptions:
using HelloDev.Utils;
using UnityEngine.Events;
// Works with UnityEvent, UnityEvent<T>, up to UnityEvent<T0,T1,T2,T3>
// Null-safe invoke
myEvent.SafeInvoke();
myIntEvent.SafeInvoke(42);
myTwoParamEvent.SafeInvoke(arg1, arg2);
myFourParamEvent.SafeInvoke(a, b, c, d);
// Subscribe (removes old subscription first, prevents duplicates)
myEvent.SafeSubscribe(MyHandler);
// Unsubscribe (null-safe)
myEvent.SafeUnsubscribe(MyHandler);using HelloDev.Utils;
// Destroy all children of a transform (zero allocation, uses reverse iteration)
transform.DestroyAllChildren();
// For editor scripts (uses DestroyImmediate)
transform.DestroyAllChildren(immediate: true);
// Destroy only children matching a condition
transform.DestroyAllChildren(child => child.CompareTag("Enemy"));
transform.DestroyAllChildren(child => child.name.StartsWith("Temp_"), immediate: true);
// Also works on GameObjects directly
gameObject.DestroyAllChildren();
gameObject.DestroyAllChildren(child => child.gameObject.activeSelf == false);For controlled initialization order across multiple systems:
using HelloDev.Utils;
using System.Threading.Tasks;
public class MyManager : MonoBehaviour, IBootstrapInitializable
{
[SerializeField] private bool selfInitialize = true;
private bool _isInitialized;
private GameContext _context;
// IBootstrapInitializable implementation
public bool SelfInitialize { get => selfInitialize; set => selfInitialize = value; }
public int InitializationPriority => 150; // Game systems layer
public bool IsInitialized => _isInitialized;
public void ReceiveContext(GameContext context)
{
_context = context;
}
void OnEnable()
{
if (selfInitialize && !_isInitialized)
_ = InitializeAsync();
}
public Task InitializeAsync()
{
if (_isInitialized) return Task.CompletedTask;
// Initialize your system here
// Register with context for other systems to access
_context?.Register<MyManager>(this);
_isInitialized = true;
return Task.CompletedTask;
}
public void Shutdown()
{
_context?.Unregister<MyManager>();
_isInitialized = false;
}
}Priority Ranges:
| Range | Category | Examples |
|---|---|---|
| 0-99 | Core Services | Logging, Analytics, Input |
| 100-149 | Data Layer | WorldFlags, EventSystem |
| 150-199 | Game Systems | Quests, Inventory |
| 200-249 | Persistence | SaveManager |
| 250-299 | Data Loading | Load saves, restore state |
| 300+ | Gameplay | UI, Audio |
A generic service container that enables decoupled communication between managers:
using HelloDev.Utils;
// In a manager's InitializeAsync:
public Task InitializeAsync()
{
// Register yourself
_context?.Register<IMyManager>(this);
// Access other managers that initialized earlier
if (_context.TryGet<ISaveManager>(out var saveManager))
{
saveManager.RegisterSystem(this);
}
return Task.CompletedTask;
}
// Check if a service is available
if (_context.Has<IUpdateManager>())
{
var updateManager = _context.Get<IUpdateManager>();
}Benefits:
- GameBootstrap is completely decoupled from manager types
- Type-safe access via generics
- Testable - create mock context with mock services
- Scalable - add new managers without changing GameBootstrap
Base class for locator ScriptableObjects that provide decoupled access to managers:
using HelloDev.Utils;
using UnityEngine;
[CreateAssetMenu(menuName = "HelloDev/Locators/My Locator")]
public class MyLocator_SO : LocatorBase_SO
{
private MyManager _manager;
public override bool IsAvailable => _manager != null;
public MyManager Manager => _manager;
public void Register(MyManager manager) => _manager = manager;
public void Unregister(MyManager manager)
{
if (_manager == manager) _manager = null;
}
}None - this is the foundation package.
| Member | Description |
|---|---|
Description |
Read-only property exposing the inspector description field |
OnScriptableObjectReset() |
Abstract method called before first scene loads; override to reset runtime state |
| Method | Description |
|---|---|
DestroyAllChildren(bool immediate) |
Destroys all children using zero-allocation reverse iteration |
DestroyAllChildren(Predicate<Transform>, bool immediate) |
Destroys children matching condition |
| Method | Description |
|---|---|
SafeInvoke(...) |
Null-safe event invocation (0-4 parameters) |
SafeSubscribe(...) |
Subscribe with duplicate prevention |
SafeUnsubscribe(...) |
Null-safe unsubscribe |
| Member | Description |
|---|---|
SelfInitialize |
True = self-init in Unity lifecycle, False = wait for bootstrap |
InitializationPriority |
Sort order for initialization (lower = earlier) |
IsInitialized |
Whether initialization is complete |
ReceiveContext(GameContext) |
Called by bootstrap before InitializeAsync; store for service registration |
InitializeAsync() |
Async initialization method |
Shutdown() |
Cleanup method |
| Member | Description |
|---|---|
Register<T>(T service) |
Register a service by interface type |
Get<T>() |
Get a registered service (throws if not found) |
TryGet<T>(out T service) |
Try to get a service (returns false if not found) |
Has<T>() |
Check if a service is registered |
Unregister<T>() |
Unregister a service |
Clear() |
Clear all services (called on shutdown) |
| Member | Description |
|---|---|
IsAvailable |
Whether the locator's manager is registered and ready |
PrepareForBootstrap() |
Called before bootstrap begins; override to clear cached state |
MIT License