Common Pitfalls When Migrating from Other Unity Frameworks to TEngine
The most common pitfalls when migrating to TEngine include attempting to preserve monolithic singleton managers instead of adopting the module-based ModuleSystem, continuing to use Addressables or Resources.Load rather than the YooAsset-wrapped ResourceModule, relying on string-based events instead of zero-GC int IDs, and failing to run mandatory code generators for UI bindings and hot-fix assemblies.
TEngine by alex-rachel is a high-performance Unity framework that replaces traditional singleton architectures with a modular service locator pattern. While migrating from other Unity frameworks, developers often encounter mismatches in asset pipelines, event systems, and lifecycle management that require specific architectural adjustments to leverage TEngine’s low-allocation design.
Assuming Singleton Architecture Instead of Modules
The most fundamental mistake is expecting a global GameManager or static service locator. In Assets/TEngine/Runtime/Core/ModuleSystem.cs, TEngine implements a strict module system where every service (resource, UI, audio, etc.) inherits from the Module base class and implements optional IUpdateModule for frame updates.
Modules are created lazily on first access via ModuleSystem.GetModule<T>() and stored in an internal dictionary. If you need a custom service, you must inherit from Module and register it explicitly:
using TEngine;
public class AnalyticsModule : Module, IUpdateModule
{
public override int Priority => 5; // Higher value = earlier execution
public void Update(float elapseSeconds, float realElapseSeconds)
{
// Per-frame analytics logic
}
}
// Registration during bootstrap
var analytics = new AnalyticsModule();
ModuleSystem.RegisterModule<IAnalyticsModule>(analytics);
Key implementation detail: The ModuleSystem maintains an internal _updateExecuteList that orders modules by their Priority property. If your module relies on execution order, you must override Priority in ModuleSystem.cs rather than using Unity’s Script Execution Order settings.
Using Addressables or Resources.Load Directly
TEngine’s ResourceModule located in Assets/TEngine/Runtime/Module/ResourceModule/ResourceModule.cs wraps YooAsset and completely abstracts Unity’s built-in loading mechanisms. Calling Addressables.LoadAssetAsync or Resources.Load bypasses TEngine’s reference counting and hot-update support.
The ResourceModule provides synchronous LoadAsset<T> and asynchronous LoadAssetAsync<T> methods that return UniTask<T>:
using TEngine;
using Cysharp.Threading.Tasks;
public async UniTask<GameObject> InstantiatePlayer()
{
var resourceModule = ModuleSystem.GetModule<IResourceModule>();
// Async load with YooAsset package name "entities"
GameObject prefab = await resourceModule.LoadAssetAsync<GameObject>(
"Player/Avatar",
"entities"
);
return Object.Instantiate(prefab);
}
Critical fix: Remove all Addressables callback chains and convert them to async/await patterns using UniTask. The module handles asset reference counting internally, so you must release handles through ResourceModule rather than Addressables' release methods.
String-Based Events Causing GC Allocations
TEngine’s GameEvent system in Tools/GameEventSourceGenerator/SourceGenerator/Generator eliminates GC pressure by using int IDs instead of string keys. Relying on string event names or UnityEvent assets creates allocations that defeat TEngine’s zero-GC design.
The framework generates a GameEventId enum from your event definitions. You subscribe and publish using these integer constants:
using TEngine;
public class CombatSystem
{
private readonly int _damageEventId = (int)GameEventId.UnitTakeDamage;
public CombatSystem()
{
// Zero-allocation subscription
GameEvent.Subscribe(_damageEventId, OnDamageReceived);
}
private void OnDamageReceived(object payload)
{
var damageData = (DamagePayload)payload;
// Process damage...
}
public void ApplyDamage(DamagePayload data)
{
// Zero-allocation dispatch
GameEvent.Publish(_damageEventId, data);
}
}
Migration step: Replace UnityEvent, Action<string>, or event bus systems with GameEvent.Publish and GameEvent.Subscribe, using only the generated GameEventId enum values.
Hand-Written UI MonoBehaviours Without Code Generation
TEngine requires the UI code generator located in Assets/Editor/UIScriptGenerator/UIComponentInspectorEditor.cs. Manually writing GetComponent calls or inspector references breaks the framework’s binding automation.
UI windows must inherit from UIWindow and use a generated partial class (e.g., MainWindowBinding) that the generator produces from your prefab structure:
- Design your UI in Unity Editor
- Run the generator via
ScriptGenerator.cs - Derive your logic from the generated binding class
This approach eliminates manual UI element lookups and ensures type-safe references that survive prefab changes.
Custom Config Parsers Instead of Luban
TEngine uses Luban configuration tables accessed through Configs/GameConfig/CustomTemplate/ConfigSystem.cs. Maintaining custom CSV or JSON parsers misses TEngine’s lazy-loading and type-safe config classes.
Access configuration data via ConfigSystem.GetConfig<T>():
using TEngine;
public class ItemDatabase
{
public ItemData GetItem(int id)
{
// Lazy-loads ItemConfig if not cached; supports async preload
var config = ConfigSystem.GetConfig<ItemConfig>();
return config.Get(id);
}
}
Migration requirement: Convert your existing config files to Luban format and regenerate C# classes using the gen_code_bin_to_server.sh script. Remove all manual JSON parsing or ScriptableObject references for configuration data.
Incorrect Hot-Fix Assembly Structure
TEngine expects a specific HybridCLR layout under Assets/GameScripts/HotFix containing four assemblies: GameBase, GameProto, and GameLogic. The entry point is GameApp.cs, where modules are registered via GameApp_RegisterSystem.cs.
Placing hot-fix code in arbitrary folders or using different assembly names prevents the HybridCLR integration from compiling properly. Reorganize your hot-fix logic to match this structure and ensure [HybridCLR] assembly definitions are configured correctly.
Ignoring Module Priority in Update Loops
Unlike Unity’s default Update order, TEngine’s ModuleSystem builds an ordered execution list at runtime based on the Priority property. If your module must execute before or after core systems (like ProcedureModule or ResourceModule), you must set an explicit priority:
- Core modules typically use priorities 0-4
- Custom modules should use 5+ or negative values depending on requirements
In ModuleSystem.cs, the _updateExecuteList sorts modules by Priority descending, meaning higher integers execute earlier in the frame.
Using Third-Party Object Poolers
TEngine provides native pooling via ObjectPoolModule and MemoryPoolModule (located in the Runtime Module directory). Using external pool libraries creates API fragmentation.
Replace external pool calls with:
var objectPool = ModuleSystem.GetModule<IObjectPoolModule>();
var pooledObject = objectPool.Get<MyPooledType>(factoryMethod);
Register custom pool types through the module system to ensure they respect TEngine’s lifecycle and shutdown procedures.
Summary
- Access all services through
ModuleSystem.GetModule<T>()rather than static singletons or service locators - Migrate asset loading to
ResourceModuleusingLoadAssetAsync<T>withUniTaskinstead of Addressables or Resources - Adopt int-based
GameEventIDs from the source generator to maintain zero-GC event dispatch - Run UI code generators after creating or modifying UI prefabs to produce binding classes
- Use
ConfigSystemwith Luban-generated classes for all configuration data access - Respect module
Priorityvalues to control update execution order instead of Unity’s Script Execution Order
Frequently Asked Questions
Can I keep using Addressables alongside TEngine during migration?
No. TEngine’s ResourceModule in Assets/TEngine/Runtime/Module/ResourceModule/ResourceModule.cs manages asset handles through YooAsset’s reference counting system. Mixing Addressables calls bypasses TEngine’s hot-update support and can cause asset leaks because the framework cannot track external handle lifetimes. Migrate all asset loading to LoadAssetAsync<T> or LoadAsset<T> immediately.
How do I replace my existing Singleton GameManager?
Remove your singleton instance property and inherit from Module instead. In Assets/TEngine/Runtime/Core/ModuleSystem.cs, the RegisterModule<TInterface>(Module module) method accepts your custom module during application startup (typically in GameApp.cs or a bootstrap procedure). Access the service later via ModuleSystem.GetModule<TInterface>(), which creates the module on first call if not already registered.
Why are my UI scripts failing to compile after migration?
TEngine requires the UI binding generator to create partial classes for UIWindow derivatives. If you see missing references to XxxWindowBinding classes, you have not run the generator located in Assets/Editor/UIScriptGenerator/ScriptGenerator.cs. After designing your UI in the Editor, run the generator to produce the binding code that links your logic to the prefab’s components.
What causes GC spikes when using events in TEngine?
Using string keys or boxing value types in event payloads causes allocations. Ensure you are using the generated GameEventId enum (located in the generated code under Tools/GameEventSourceGenerator) which maps to int IDs. The GameEvent.Publish(int eventId, object payload) method in the generated GameEvent class is optimized for zero-GC dispatch when using the integer event IDs and proper payload types.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →