# How TEngine's MVE Architecture Handles Data Flow Between Model and View

> Discover how TEngine's MVE architecture manages data flow. Learn how GameEvent acts as a zero-GC intermediary for efficient state broadcasting and consumption.

- Repository: [ALEX/tengine](https://github.com/alex-rachel/tengine)
- Tags: architecture
- Published: 2026-02-24

---

**TEngine implements a strict Model-View-Event (MVE) pattern where the `GameEvent` module acts as a zero-GC intermediary, allowing Models to broadcast state changes via integer event IDs while Views consume these events through lifecycle-managed listeners that automatically clean up on destruction.**

TEngine is an open-source Unity game framework that enforces strict separation of concerns through its Model-View-Event (MVE) architecture. In this design, the Model (game data and logic) never holds direct references to the View (UI components); instead, all communication flows through a lightweight event system. This article examines the exact data flow mechanism, referencing the source implementation in the `alex-rachel/tengine` repository.

## Understanding the Model-View-Event Pattern

TEngine's MVE architecture enforces one-way communication: Model → Event → View. The **Model** manages game state and business logic, the **View** handles presentation and user input, and the **Event** layer acts as a decoupled messaging bus. This strict separation prevents circular dependencies and ensures that UI components can be destroyed or recreated without affecting underlying game systems.

According to the framework documentation in `Books/2-框架概览.md`, the architecture achieves zero-garbage-collection overhead by using `int` event IDs rather than string hashes. These IDs are generated via `RuntimeId.ToRuntimeId()` and serve as compile-time contracts between Models and Views.

## Broadcasting State Changes from the Model

When a Model's state changes, it broadcasts an event through the global `GameEvent` facade located in [`Runtime/Core/GameEvent/GameEvent.cs`](https://github.com/alex-rachel/tengine/blob/main/Runtime/Core/GameEvent/GameEvent.cs). This static class provides the `Send()` method that distributes messages to all registered listeners via an internal `EventMgr` instance.

### Defining Integer Event IDs

Models and Views share a contract through predefined integer constants. The framework recommends generating these from string identifiers for readability while maintaining runtime performance:

```csharp
// PlayerEventDefined.cs
public static class PlayerEventDefined
{
    // Recommended: use int IDs for performance
    public static readonly int OnHpChange = RuntimeId.ToRuntimeId("Player.HpChange");
}

```

### Sending Events via GameEvent

The Model calls `GameEvent.Send()` whenever relevant data mutates, passing the event ID and optional payload:

```csharp
// PlayerManager.cs (model)
private void ApplyDamage(int dmg)
{
    hp -= dmg;
    // Notify everyone that HP changed, passing the new value
    GameEvent.Send(PlayerEventDefined.OnHpChange, hp);
}

```

Because `GameEvent` operates as a global singleton facade, any system component can broadcast state changes without knowing which Views (if any) are currently active.

## Consuming Events in the View Layer

Views inherit from `UIWindow` or `UIWidget`, both of which derive from `UIBase` in the UI module (`Runtime/Module/UI`). These classes override `RegisterEvent()` to subscribe to specific event IDs using the `AddUIEvent()` method, which automatically binds listeners to the component's lifecycle.

### Registering Listeners in UI Components

UI classes implement `RegisterEvent()` to establish subscriptions during initialization:

```csharp
// BattleMainUI.cs (UIWindow)
public override void RegisterEvent()
{
    // AddUIEvent binds the listener to the UIWindow's lifecycle
    AddUIEvent(PlayerEventDefined.OnHpChange, RefreshHpBar);
}

// Called automatically when the event fires
private void RefreshHpBar(int currentHp)
{
    m_hpBar.value = (float)currentHp / maxHp;
}

```

The `AddUIEvent` method, implemented in the UI base classes, registers the handler with the global `GameEvent` system while maintaining a local reference for automatic cleanup.

### Automatic Lifecycle Management

TEngine's UI architecture prevents memory leaks by automatically unregistering event listeners when a UI component is destroyed. When `AddUIEvent` is called, the framework associates the listener with the UI instance's lifetime scope. Upon destruction, the `OnDestroy` lifecycle method in `UIBase` automatically removes all registered handlers, preventing dangling callbacks and null reference exceptions.

## Local Event Scopes for Component-Level Communication

For scenarios requiring isolated communication between specific objects, TEngine provides `GameEventMgr` through the `MemoryPool` system. This allows Models to maintain private event buses that do not broadcast globally, as detailed in `Books/3-2-事件模块.md`.

### Object-Scoped Event Managers

Individual components can instantiate local event managers for encapsulated messaging:

```csharp
// Player.cs (local model with its own event manager)
private readonly GameEventMgr _eventMgr = MemoryPool.Acquire<GameEventMgr>();
public GameEventMgr Event => _eventMgr;

public void UpdateLevel(int newLevel)
{
    // Send a local event, only listeners attached to this player will receive it
    _eventMgr.Send(PlayerEventDefined.OnLevelUp, newLevel);
}

```

### Listening to Local Events

UI widgets attached to specific entities can subscribe to these local managers rather than the global event system:

```csharp
// PlayerInfoPanel.cs (UIWidget attached to a Player instance)
public void SetPlayer(Player player)
{
    // Automatically unregister when the widget is destroyed
    AddUIEvent(PlayerEventDefined.OnLevelUp, OnLevelUp);
    // OR listen to the player's own manager:
    player.Event.AddEventListener<int>(PlayerEventDefined.OnLevelUp, OnLevelUp);
}

```

This dual approach—global events for system-wide state changes and local events for entity-specific updates—provides granular control over data flow scope while maintaining the architectural constraint that Models never directly reference Views.

## Summary

- **TEngine's MVE architecture** enforces strict separation between Models and Views, with all communication routed through the `GameEvent` event bus.
- **Integer event IDs** generated via `RuntimeId.ToRuntimeId()` eliminate string hashing overhead and garbage collection pressure.
- **Models broadcast** state changes using `GameEvent.Send()` without knowing which Views exist.
- **Views consume** events through `AddUIEvent()` in their `RegisterEvent()` overrides, with automatic lifecycle management preventing memory leaks.
- **Local event scopes** via `GameEventMgr` enable component-specific communication without global broadcasts.

## Frequently Asked Questions

### What is the difference between `GameEvent` and `GameEventMgr` in TEngine?

`GameEvent` is a static facade class located at [`Runtime/Core/GameEvent/GameEvent.cs`](https://github.com/alex-rachel/tengine/blob/main/Runtime/Core/GameEvent/GameEvent.cs) that provides global event broadcasting across the entire application. `GameEventMgr` is an instance-based event manager typically acquired via `MemoryPool.Acquire<GameEventMgr>()` for local, object-scoped communication. Use `GameEvent` for system-wide state changes (like player death) and `GameEventMgr` for entity-specific updates (like a single enemy's health change) that should not pollute the global event space.

### How does TEngine prevent memory leaks with UI event listeners?

The `AddUIEvent` method in `UIWindow` and `UIWidget` (derived from `UIBase`) automatically ties listener registrations to the UI component's lifecycle. When the UI object is destroyed, the framework automatically unregisters all associated event handlers, preventing dangling references that could otherwise keep destroyed GameObjects in memory or cause null reference exceptions when events fire after destruction.

### Why does TEngine use integer event IDs instead of strings?

TEngine uses `int` event IDs—generated by `RuntimeId.ToRuntimeId()`—to achieve **zero-GC messaging**. String-based event systems require hashing operations that allocate memory and trigger garbage collection. Integer comparisons are CPU-efficient and allocation-free, making the event system suitable for high-frequency game loops where performance is critical, as implemented in the core runtime.

### Can a Model directly reference a View in TEngine's MVE architecture?

No. The architecture explicitly prohibits Models from holding references to Views. According to the framework design documented in `Books/2-框架概览.md` and implemented in the UI module (`Runtime/Module/UI`), Models communicate only through the Event layer. This loose coupling allows Views to be created, destroyed, or modified without requiring changes to the underlying Model logic, adhering to strict separation of concerns.