A foundational library for Stride Game Engine that makes coding feel like Unity! Get familiar script lifecycle, intuitive entity finding, and essential game development utilities.
Stride Engine is powerful but lacks some conveniences that Unity developers expect. This core library bridges that gap by providing:
The Problem: Stride's base scripts don't match common game development patterns:
- No
OnAwake(),OnStart(),OnEnable(),OnDisable()lifecycle - No built-in entity finding utilities
- No consistent random number generation
- No easy way to start scripts disabled
- Manual enable/disable management is verbose
The Solution: Drop-in base classes that work exactly like Unity's MonoBehaviour but for Stride.
Zero Breaking Changes - HS Core works alongside your existing Stride code:
✅ Mix and Match: Use HS scripts and regular Stride scripts in the same project
✅ Gradual Migration: Convert scripts one at a time, no pressure
✅ Existing Code Safe: Your current Stride scripts continue working unchanged
✅ Drop-in Addition: Add HS Core to existing projects without modification
// Your existing Stride script - STILL WORKS
public class OldEnemyAI : SyncScript
{
public override void Update() { /* existing logic */ }
}
// Your new HS script - ALSO WORKS
public class NewPlayerController : HSSyncScript
{
public override void OnUpdate() { /* new lifecycle */ }
}
// They can even communicate normally!
var enemy = Entity.Scene.FindEntityByName_HS("Enemy"); // HS utility
var aiScript = enemy.Get<OldEnemyAI>(); // Standard Stride- Download HS Stride Packer
- Download this library's
.stridepackagefile from Releases - Import using HS Stride Packer - everything installs automatically!
- Code is open source - you can still copy files manually if preferred
- Drag and drop the .cs files into your project. (Happenstance Folder)
- Stride Engine 4.2.0.2381 or newer
- HS Stride Packer (for easy installation)
// Before (Stride way)
public class MyScript : SyncScript
{
public override void Start() { /* everything mixed together */ }
public override void Update() { /* your update logic */ }
}
// After (Happenstance way)
public class MyScript : HSSyncScript
{
protected override void OnAwake() { /* initialization */ }
protected override void OnStart() { /* after awake */ }
protected override void OnUpdate() { /* your update logic */ }
protected override void OnEnable() { /* when enabled */ }
protected override void OnDisable() { /* when disabled */ }
}Unity-style lifecycle methods that work exactly as expected.
HSAsyncScript / HSSyncScript / HSStartupScript:
protected override void OnAwake() // Called first, for initialization
protected override void OnStart() // Called after Awake, for setup
protected override void OnUpdate() // Called every frame (SyncScript only)
protected override Task OnExecute() // Called for async operations (AsyncScript only)
protected override void OnEnable() // Called when entity/component enabled
protected override void OnDisable() // Called when entity/component disabled
protected override void OnDestroy() // Called when entity destroyed
protected override void OnTriggerEnter(Entity other) // Called when another entity enters trigger
protected override void OnTriggerExit(Entity other) // Called when another entity exits triggerStartDisabled Support:
[DataMember]
public bool StartDisabled { get; set; } // Set in Stride Studio- Script won't initialize until manually enabled
- Perfect for objects that should spawn inactive
- Handles lifecycle correctly when enabled later
Automatic trigger detection system that provides Unity-familiar OnTriggerEnter/OnTriggerExit callbacks for Stride's Bullet physics system.
You MUST enable CollisionDetection = true in the inspector before the script starts. This cannot be changed at runtime due to performance optimization.
// HSOnTriggerComponent is automatically added to all HS scripts
// Just override the methods you need:
public class TriggerZone : HSSyncScript
{
protected override void OnTriggerEnter(Entity other)
{
Logger.Info($"Entity {other.Name} entered trigger zone");
// Check for specific components
var player = other.Get<PlayerController>();
if (player != null)
{
OnPlayerEntered(player);
}
}
protected override void OnTriggerExit(Entity other)
{
Logger.Info($"Entity {other.Name} left trigger zone");
}
}- CollisionDetection = true in inspector or constructor
- Entity must have a
PhysicsComponent - Works with Stride's current Bullet physics system
- Will be updated when Bepu physics becomes stable
The system uses an early return pattern for optimal performance:
- Scripts with
CollisionDetection = falsehave zero overhead - Only entities that need triggers consume CPU cycles
- Cannot be enabled at runtime - this is intentional for performance
If you need to enable/disable triggers at runtime, you'll need custom implementation:
- Automatic Cleanup: Dead entities are properly removed from trigger tracking
- Performance Optimized: Periodic cleanup prevents memory leaks
- Enable/Disable Control: Use
CollisionDetectionproperty in inspector - Thread Safe: Handles async collision detection properly
- Zero Overhead: Scripts without collision detection have no performance cost
// ✅ Trigger zones (enable CollisionDetection)
public class CheckpointZone : HSSyncScript { }
// ✅ Pickups (enable CollisionDetection)
public class HealthPack : HSSyncScript { }
// ✅ Damage zones (enable CollisionDetection)
public class LavaZone : HSSyncScript { }
// ✅ Regular scripts (leave CollisionDetection = false)
public class WeaponController : HSSyncScript { }
public class AnimationController : HSSyncScript { }- Check that
CollisionDetection = truein inspector - Verify entity has
PhysicsComponent - Use Logger to confirm OnTriggerEnter/Exit are being called
- Remember: Only the receiving entity needs collision detection enabled
Unity-style extension methods that make entity management, scene searching, and transform operations intuitive and clean.
_HS suffix to prevent future conflicts with Stride Engine updates. This ensures the API remains stable even if Stride adds similar methods later.
Find children, get components, and manage entity relationships with Unity-familiar syntax.
Find Children:
// Find child by name (immediate children only)
Entity weapon = player.FindChildByName_HS("Weapon");
// Find child by name (recursive through hierarchy)
Entity scope = player.FindChildByNameRecursive_HS("Scope");Component from Children:
// Get component from immediate child
AudioEmitterComponent audio = player.GetComponentFromChild_HS<AudioEmitterComponent>("AudioSource");
// Get component from anywhere in hierarchy
ModelComponent model = player.GetComponentFromChildRecursive_HS<ModelComponent>("PlayerModel");Unity-Style Component Finding:
// Get all components of type in immediate children
List<Light> lights = player.GetComponentsInChildren_HS<Light>();
// Get all components in entire hierarchy (recursive)
List<Weapon> allWeapons = player.GetComponentsInAllDescendants_HS<Weapon>();
// Find component in parent entities (walking up hierarchy)
GameManager manager = someEntity.GetComponentInParent_HS<GameManager>();UI Element Finding:
// Find UI elements in entity's UI hierarchy
Button startButton = entity.GetUIElement_HS<Button>("StartButton");
List<Button> allButtons = entity.GetUIElements_HS<Button>();Find entities and components throughout the entire scene with clean extension syntax.
Find by Name:
// Find any entity by name (recursive through entire scene)
Entity player = Entity.Scene.FindEntityByName_HS("Player");Find by Component:
// Find all entities that have a specific component
List<Entity> cameras = Entity.Scene.FindEntitiesWithComponent_HS<CameraComponent>();
// Find all components of a specific type in the scene
List<AudioEmitterComponent> audioSources = Entity.Scene.FindAllComponents_HS<AudioEmitterComponent>();
// Find components that implement an interface
List<IDamageable> damageables = Entity.Scene.FindAllComponentsWithInterface_HS<IDamageable>();Clean, semantic transform operations that make rotation, positioning, and directional calculations simple and intuitive.
LookAt Operations (Rotation Made Simple):
// Instant snap to face target - no complex matrix math needed!
entity.Transform.LookAt_HS(player.Transform);
entity.Transform.LookAt_HS(player); // Convenience overload
entity.Transform.LookAt_HS(targetPosition);
// Smooth rotation towards target - perfect for AI
entity.Transform.SmoothLookAt_HS(player.Transform, rotationSpeed, deltaTime);
entity.Transform.SmoothLookAt_HS(targetPosition, rotationSpeed, deltaTime);Rotation & Euler Angles (Unity-Style Simplicity):
// Get rotation as degrees - no quaternion math!
Vector3 rotation = entity.Transform.GetEulerAngles_HS();
// Set rotation from degrees - incredibly simple
entity.Transform.SetEulerAngles_HS(new Vector3(45, 90, 0));
// Convert between quaternion and euler easily
Vector3 euler = someQuaternion.ToEulerAngles_HS();
Quaternion quat = HSTransform.FromEulerAngles_HS(new Vector3(pitch, yaw, roll));
// Smooth rotation control
entity.Transform.SmoothRotateTo_HS(targetRotation, speed, deltaTime);Distance Calculations:
// Clean distance methods - no manual Vector3.Distance calls
float distance = entity.Transform.DistanceFrom_HS(target.Transform);
float distance = entity.Transform.DistanceFrom_HS(target); // Convenience
float distance = entity.Transform.DistanceFrom_HS(worldPosition);
// Performance optimized for comparisons
float distSq = entity.Transform.DistanceSquaredFrom_HS(target.Transform);Direction & Positioning:
// Clean directional access
Vector3 forward = entity.Transform.GetForward_HS();
Vector3 right = entity.Transform.GetRight_HS();
Vector3 up = entity.Transform.GetUp_HS();
// Position utilities
Vector3 pos = entity.Transform.GetWorldPosition_HS(); // Force update
Vector3 pos = entity.Transform.GetWorldPositionFast_HS(); // When matrix is current
// Direction calculations for AI
float yaw = HSTransform.DirectionToYaw_HS(moveDirection);
float angle = entity.Transform.AngleTo_HS(target.Transform); // Vision cones
Quaternion lookRot = HSTransform.LookRotation_HS(direction);Thread-safe random number generation with game-development focused methods.
Basic Random:
// Random float between min and max (inclusive)
float damage = HSRandom.Range(10.0f, 25.0f);
// Random int between min and max (max exclusive)
int coins = HSRandom.Range(1, 10); // Returns 1-9
// Random value 0.0 to 1.0
float chance = HSRandom.Value;Game-Specific Random:
// Random item from list
List<string> weapons = new List<string> {"Sword", "Bow", "Staff"};
string randomWeapon = HSRandom.GetRandom(weapons);
// Percentage-based dice roll
if (HSRandom.DiceRoll(25f)) // 25% chance
{
SpawnRareItem();
}
// Multiple dice rolls with success threshold
bool criticalHit = HSRandom.MultiDiceRoll(50f, 3, 2); // 3 rolls at 50%, need 2 successesAdvanced Random:
// Weighted random selection
List<string> items = new List<string> {"Common", "Rare", "Epic"};
List<float> weights = new List<float> {70f, 25f, 5f};
string loot = HSRandom.WeightedRandom(items, weights);
// Unique random selection (no duplicates)
List<string> randomCards = HSRandom.GetRandomUnique(allCards, 5);
// Bell curve distribution (values cluster toward middle)
float statRoll = HSRandom.BellCurve(1f, 20f, 3); // More likely to roll ~10-11// Enable/disable (HS scripts only)
SetActive(false); // Disables this HS script entity and all children
// Parent/child management (works on ANY entity, even non-HS entities!)
Entity.SetParent_HS(parentEntity); // Make this entity a child of parent
Entity.SetChild_HS(anyEntity); // Make ANY entity a child of this one
Entity.ClearParent_HS(); // Remove from parent (become root)
Entity.ClearChild_HS(specificChild); // Remove specific child
Entity.ClearChildren_HS(); // Remove all childrenControlled logging system with debug mode, file output, and multiple destinations.
Why HSLogger over Stride's default logging:
- Debug Mode Control: Toggle debug logging on/off without code changes
- Automatic File Logging: Saves error logs to files for release builds
- Multiple Outputs: Console + file logging simultaneously
- Tag Removal: Cleans up formatted messages for file output
- Centralized Control: One place to manage all logging behavior
Automatic Logger Access in HS Scripts:
public class MyScript : HSSyncScript
{
protected override void OnStart()
{
Logger.Debug("Debug info (only shows if DebugMode = true)");
Logger.Info("Script started successfully");
Logger.Warning("This is a warning");
Logger.Error("Something went wrong"); // Always logged to error_log.txt
}
}HSLogger Configuration:
// In your scene, add HSLogger component
public class GameLogger : HSLogger
{
public override void Start()
{
DebugMode = true; // Enable debug logging and game_log.txt
base.Start();
}
}Release vs Debug Benefits:
- Development: Full logging to console + file with debug info
- Release: Error-only file logging, no console spam
- Troubleshooting: Users can send you error_log.txt files for support
- Performance: Zero debug logging overhead when DebugMode = false
%APPDATA%/Happenstance/YourGame/Logs/game_log.txt(debug mode only)%APPDATA%/Happenstance/YourGame/Logs/error_log.txt(always enabled)
Enumeration for handling different system languages in your game.
// HSSystemLanguage enum includes all major languages
HSSystemLanguage currentLang = HSSystemLanguage.English;HSSyncScript - For frame-based updates:
public class PlayerController : HSSyncScript
{
protected override void OnUpdate()
{
HandleInput();
UpdateMovement();
}
}HSAsyncScript - For async operations:
public class GameManager : HSAsyncScript
{
protected override async Task OnExecute()
{
await LoadGameData();
await InitializeSystems();
StartGameplay();
}
}HSStartupScript - For one-time initialization:
public class GameInitializer : HSStartupScript
{
protected override void OnStart()
{
SetupGameSettings();
InitializeServices();
}
}Manager Pattern:
public class AudioManager : HSSyncScript
{
public static AudioManager Instance { get; private set; }
protected override void OnAwake()
{
Instance = this;
}
}Component Communication:
public class PlayerHealth : HSSyncScript
{
protected override void OnStart()
{
var ui = Entity.Scene.FindAllComponents_HS<HealthUI>().FirstOrDefault();
ui?.UpdateHealthDisplay(currentHealth);
}
}public class SpawnManager : HSSyncScript
{
protected override void OnStart()
{
// Spawn enemies and organize them under this manager
var enemy1 = SpawnEnemy();
var enemy2 = SpawnEnemy();
var pickupItem = SpawnPickup();
// Parent ANY entity to this manager (even non-HS entities)
Entity.SetChild_HS(enemy1); // Now enemy1 is child of SpawnManager
Entity.SetChild_HS(enemy2); // Now enemy2 is child of SpawnManager
Entity.SetChild_HS(pickupItem); // Even pickup items can be organized
// Much easier than: enemy1.Transform.Parent = Entity.Transform;
Logger.Info($"Spawned and organized {Entity.Transform.Children.Count} entities");
}
public void CleanupAllSpawned()
{
// Remove all spawned entities from hierarchy (makes them root entities)
Entity.ClearChildren_HS();
}
}public class GameBootstrap : HSStartupScript
{
protected override void OnStart()
{
// Find all managers in the scene
var audioManager = Entity.Scene.FindAllComponents_HS<AudioManager>().FirstOrDefault();
var inputManager = Entity.Scene.FindAllComponents_HS<InputManager>().FirstOrDefault();
// Configure them
audioManager?.Initialize();
inputManager?.LoadBindings();
Logger.Info("Game systems initialized");
}
}public class EnemySpawner : HSAsyncScript
{
protected override async Task OnExecute()
{
while (IsEnabled)
{
if (HSRandom.DiceRoll(spawnChance))
{
var enemy = SpawnEnemy();
// Instantly organize under this spawner (works on ANY entity!)
Entity.SetChild_HS(enemy); // Much easier than enemy.Transform.Parent = Entity.Transform
Logger.Debug($"Spawned enemy, total children: {Entity.Transform.Children.Count}");
}
await Script.NextFrame();
}
}
public void DespawnAllEnemies()
{
// Easy cleanup - remove all children from hierarchy
Entity.ClearChildren_HS();
}
}public class PickupItem : HSSyncScript
{
[DataMember] public float healAmount = 25f;
[DataMember] public AudioComponent pickupSound;
protected override void OnTriggerEnter(Entity other)
{
// Check if player entered the pickup area
var player = other.Get<PlayerController>();
if (player != null)
{
// Heal the player
var health = player.Entity.Get<PlayerHealth>();
health?.Heal(healAmount);
// Play pickup sound
pickupSound?.Play();
Logger.Info($"Player picked up health item: +{healAmount} HP");
// Remove the pickup item
Entity.Scene.Entities.Remove(Entity);
}
}
}public class DamageZone : HSSyncScript
{
[DataMember] public float damagePerSecond = 10f;
[DataMember] public string damageType = "Fire";
private HashSet<Entity> entitiesInDamageZone = new HashSet<Entity>();
protected override void OnTriggerEnter(Entity other)
{
var damageable = other.Get<IDamageable>();
if (damageable != null)
{
entitiesInDamageZone.Add(other);
Logger.Info($"{other.Name} entered {damageType} damage zone");
}
}
protected override void OnTriggerExit(Entity other)
{
if (entitiesInDamageZone.Contains(other))
{
entitiesInDamageZone.Remove(other);
Logger.Info($"{other.Name} left {damageType} damage zone");
}
}
protected override void OnUpdate()
{
// Apply damage to all entities in zone
foreach (var entity in entitiesInDamageZone)
{
var damageable = entity.Get<IDamageable>();
damageable?.TakeDamage(damagePerSecond * (float)Game.UpdateTime.Elapsed.TotalSeconds);
}
}
}public class TeleportPad : HSSyncScript
{
[DataMember] public Entity destinationPad;
[DataMember] public AudioComponent teleportSound;
[DataMember] public ParticleSystemComponent teleportEffect;
protected override void OnTriggerEnter(Entity other)
{
// Only teleport entities with a transform and physics
if (other.Get<TransformComponent>() != null && other.Get<RigidbodyComponent>() != null)
{
TeleportEntity(other);
}
}
private void TeleportEntity(Entity entity)
{
if (destinationPad == null) return;
// Play effects
teleportSound?.Play();
teleportEffect?.Play();
// Teleport to destination
entity.Transform.Position = destinationPad.Transform.Position;
Logger.Info($"Teleported {entity.Name} to {destinationPad.Name}");
}
}- Replace
SyncScriptwithHSSyncScript - Replace
AsyncScriptwithHSAsyncScript - Replace
StartupScriptwithHSStartupScript - Move initialization from
Start()toOnAwake()orOnStart() - Move update logic from
Update()toOnUpdate()
base.Start(), base.Update(), or base.Execute() in HS scripts!
// ❌ OLD Stride Script
public override void Start()
{
base.Start(); // This was normal in Stride
// your code
}
// ✅ NEW HS Script
protected override void OnStart()
{
// base.OnStart(); // Only if needed, NO base.Start()!
// your code
}Why: HS scripts seal these methods to manage lifecycle. Calling base.Start() creates infinite recursion and stack overflow crash.
Start(), Update(), Execute(), Cancel()) are sealed in HS scripts to prevent bugs. You must use the OnMethod equivalents:
// ❌ This will cause compile errors
public override void Start() { } // SEALED - use OnStart()
public override void Update() { } // SEALED - use OnUpdate()
public override Task Execute() { } // SEALED - use OnExecute()
// ✅ Use these instead
protected override void OnAwake() { }
protected override void OnStart() { }
protected override void OnUpdate() { }
protected override async Task OnExecute() { }- EntityFinder methods are recursive and can be expensive on large scenes
- Cache frequently accessed entities/components
- Use specific finding methods rather than broad searches when possible
- Setup Required: Copy HSLogger prefab into scene OR attach HSLogger component to any entity
- Scripts will log errors if HSLogger is not found in the scene
- Logger is automatically discovered and cached by all HS scripts
- Set
DebugMode = trueon HSLogger for development,falsefor release - Error logs are always saved to file regardless of debug mode
- SetActive() only works on entities with HS scripts (enables/disables the entity hierarchy)
- Parent/Child management works on ANY entity in the scene, even non-HS entities
- Entity.SetChild_HS(anyEntity) from any HS script can parent ANY entity to it
- Much easier than Stride's manual Transform.Parent manipulation
- Perfect for dynamic spawning, organization, and hierarchy management
// Instead of this (Stride way):
spawnedEntity.Transform.Parent = managerEntity.Transform;
// You get this (HS way):
managerEntity.SetChild_HS(spawnedEntity);public abstract class MyCustomScript : HSSyncScript
{
protected MyGameManager GameManager { get; private set; }
protected override void OnAwake()
{
base.OnAwake();
GameManager = Entity.Scene.FindAllComponents_HS<MyGameManager>().FirstOrDefault();
}
}public interface IDamageable
{
void TakeDamage(float amount);
}
public class HealthSystem : HSSyncScript
{
protected override void OnStart()
{
// Find all damageable objects in scene
var damageables = Entity.Scene.FindAllComponentsWithInterface_HS<IDamageable>();
RegisterDamageables(damageables);
}
}MIT License - see LICENSE.txt for full text.
Happenstance Stride Engine Core
Copyright © 2025 Happenstance Games LLC