Skip to content

Prefabs & Components

Overview

Balancy's Prefab & Component System brings Unity-like patterns to web UI development. Build reusable templates, attach script behaviors, and compose complex interfaces from simple building blocks.

Key Concepts:

  • Prefabs: Reusable HTML templates (like Unity prefabs)
  • Components: Script behaviors attached to elements (like MonoBehaviour)
  • Element Objects: Wrappers around DOM elements with components
  • Unlimited Nesting: Prefabs can contain other prefabs at any depth

Core Concepts

Element Objects

An ElementObject wraps a DOM element and can have multiple script components attached. Think of it as Unity's GameObject.

Element Behaviour

ElementBehaviour is the base class for all custom scripts. It provides Unity-like lifecycle methods:

class MyScript extends balancy.ElementBehaviour {
    awake() {
        // Called when component is created
    }

    start() {
        // Called before first update
    }

    update(deltaTime) {
        // Called every frame (60 FPS)
    }

    onDestroy() {
        // Called when element is destroyed
    }
}

Prefabs

Prefabs are reusable HTML templates that can be instantiated multiple times. They support: - Script components - UI elements (images, text, buttons) - Nested prefabs at any depth - Serialized parameters

Creating a Prefab

1. Design the Template

In UI Builder, create a new View and then you can use it as a Prefab.

2. Use in Other Views

In the Tools Panel (right), find your prefab under the "Prefabs" section. Drag it into any other view to instantiate it.

Script Components

Create a Script

In the Balancy Assets section, open JavaScript tab and create a new script file. For example:

class HealthBar extends balancy.ElementBehaviour {
    // @serialize {number}
    maxHealth = 100;

    awake() {
        console.log('HealthBar created with maxHealth:', this.maxHealth);
    }

    update(deltaTime) {
        // Update health bar display
    }
}

Serialized Parameters

Mark fields as serializable using @serialize comments:

class MyScript extends balancy.ElementBehaviour {
    // @serialize {number}
    speed = 5.0;

    // @serialize {string}
    playerName = "Player";

    // @serialize {boolean}
    isActive = true;

    // @serialize {element}
    targetElement = null;  // Reference to another ElementObject
}

Supported Types:

  • number: Numeric values (integer or float)
  • string: Text values
  • boolean: true/false
  • element: Reference to another element by GUID

Element References

The element type allows scripts to reference other elements:

class HealthDisplay extends balancy.ElementBehaviour {
    // @serialize {element}
    target = null;

    start() {
        // Access the referenced element
        if (this.target) {
            const healthBar = this.target.getComponent(HealthBar);
            console.log('Health:', healthBar.currentHealth);
        }
    }
}

Lifecycle Methods

awake()

Called immediately when the component is created, after parameters are applied.

awake() {
    // Initialize references
    this.health = this.maxHealth;

    // Set up event listeners
    this.elementObject.element.addEventListener('click', () => this.onClick());
}

Use for:

  • Initialization
  • Setting up event listeners
  • Applying serialized parameters

start()

Called once before the first update(), after all components have called awake().

start() {
    // Get references to other components
    this.movement = this.getComponent(Movement);

    // Start animations
    this.startAnimation();
}

Use for:

  • Getting component references
  • Starting animations
  • Initial state setup that depends on other components

update(deltaTime)

Called every frame (typically 60 FPS). deltaTime is in seconds (e.g., 0.016 for 60fps).

update(deltaTime) {
    // Update position
    this.position += this.velocity * deltaTime;

    // Update UI
    this.updateDisplay();
}

Use for:

  • Animations
  • Real-time updates
  • User input handling

onEnable() / onDisable()

Called when the component or element becomes active/inactive.

onEnable() {
    console.log('Component activated');
    this.startTimer();
}

onDisable() {
    console.log('Component deactivated');
    this.stopTimer();
}

Triggered by:

  • balancy.ElementsManager.setActive(element, true/false)
  • component.enabled = true/false
  • Parent element visibility changes

onDestroy()

Called when the element is being destroyed.

onDestroy() {
    // Cleanup
    this.stopTimers();
    this.removeEventListeners();
    this.releaseResources();
}

Use for:

  • Cleanup
  • Releasing resources
  • Saving state

Component Communication

getComponent()

Get a component on the same element:

class AIController extends balancy.ElementBehaviour {
    start() {
        // Get reference using class constructor
        this.movement = this.getComponent(Movement);
        this.healthBar = this.getComponent(HealthBar);
    }

    update(deltaTime) {
        if (this.healthBar.currentHealth <= 0) {
            this.movement.enabled = false;
        }
    }
}

getComponents()

Get all components of a type:

start() {
    const allHealthBars = this.getComponents(HealthBar);
    console.log('Found', allHealthBars.length, 'health bars');
}

getComponentInChildren()

Get a component in child elements:

start() {
    const childButton = this.getComponentInChildren(BuyButton);
    if (childButton) {
        childButton.init({ price: this.price });
    }
}

Nested Prefabs

Prefabs can contain other prefabs at unlimited depth. The system processes them recursively.

Example: Three-Level Nesting

<!-- Level 1: Shop -->
<div data-prefab-id="shop"></div>

<!-- shop prefab contains: -->
<div class="shop">
    <div data-prefab-id="shop-page"></div>
</div>

<!-- shop-page prefab contains: -->
<div class="page">
    <div data-prefab-id="item-card"></div>
    <div data-prefab-id="item-card"></div>
</div>

<!-- item-card prefab contains: -->
<div class="card">
    <img data-image-id="123">
    <div data-script-id="BuyButton" style="display:none"></div>
</div>

Processing order: 1. Load shop → Process its nested shop-page 2. Load shop-page → Process its nested item-card prefabs 3. Load item-card → Process images and scripts 4. All UI elements prepared, all scripts initialized

Instantiation

Static Instantiation (Template in DOM)

Use an existing element as a template:

const template = document.getElementById('enemy-template');
const enemy = balancy.ElementsManager.instantiate(template);
document.getElementById('enemies').appendChild(enemy);

Dynamic Instantiation (From Element Reference)

Instantiate a prefab using a serialized element reference:

class RewardsList extends balancy.ElementBehaviour {
    // @serialize {element}
    itemPrefab = null;  // Reference to prefab element
    // @serialize {element}
    container = null;

    displayItems(items) {
        // Clear container
        this.container.element.innerHTML = '';

        // Instantiate prefab for each item
        for (const item of items) {
            const itemObject = this.itemPrefab.instantiate();
            const script = itemObject.getComponent(Balancy_UI_LiveOps_ItemWithAmount);
            script.init(item);
            this.container.element.appendChild(itemObject.element);
        }
    }
}

How it works:

  1. Designer attaches prefab element reference to itemPrefab parameter in UI Builder
  2. Script uses this.itemPrefab.instantiate() to create instances
  3. No need to know prefab IDs - all done visually!

What happens during instantiation: 1. Element tree is deep-cloned 2. All GUIDs are regenerated 3. Element references are updated intelligently: - Internal refs (within cloned tree): Updated to new GUIDs - External refs (outside tree): Kept unchanged 4. Scripts are initialized (awake(), start()) 5. Element is returned, ready for DOM insertion

Active & Enabled States

Component Enabled

Control individual components:

class AIController extends balancy.ElementBehaviour {
    freezeEnemy() {
        const movement = this.getComponent(Movement);
        movement.enabled = false;  // Stops update(), calls onDisable()
    }

    unfreezeEnemy() {
        const movement = this.getComponent(Movement);
        movement.enabled = true;  // Resumes update(), calls onEnable()
    }
}

Element Active

Control entire elements:

const enemy = document.getElementById('enemy-1');

// Deactivate: hides element, calls onDisable() on all components
balancy.ElementsManager.setActive(enemy, false);

// Reactivate: shows element, calls onEnable() on all components
balancy.ElementsManager.setActive(enemy, true);

Hierarchy

A component is active only when: 1. component.enabled === true (intrinsic state) 2. AND all parent elements are visible (extrinsic state)

Example:

// Both conditions must be true for update() to be called
component.enabled = true;  // ✓ Component enabled
parent.style.display = ''; // ✓ Parent visible
// → update() is called

parent.style.display = 'none';  // ✗ Parent hidden
// → update() stops, onDisable() called
// → component.enabled still true (preserved)

parent.style.display = '';  // ✓ Parent visible again
// → update() resumes, onEnable() called

Destroying Elements

const enemy = document.getElementById('enemy-1');
balancy.ElementsManager.destroy(enemy);
// → onDestroy() called on all components
// → Element removed from DOM
// → Internal references cleaned up

Pausing & Resuming

Global pause (e.g., game pause menu):

// Pause all updates
balancy.ElementsManager.pause();

// Resume all updates
balancy.ElementsManager.resume();

Note: Does NOT call onEnable()/onDisable() - only stops/resumes update() calls.

Best Practices

1. Keep Scripts Focused

Create small, single-purpose scripts:

// ✓ Good: Focused responsibility
class HealthBar extends balancy.ElementBehaviour { }
class Movement extends balancy.ElementBehaviour { }
class AIController extends balancy.ElementBehaviour { }

// ✗ Avoid: One script doing everything
class PlayerController extends balancy.ElementBehaviour {
    // Health, movement, AI, inventory, etc. all in one class
}

2. Use Prefabs for Reusability

If you use something more than once, make it a prefab:

// ✓ Good: Reusable buy button prefab
<div data-prefab-id="buy-button"></div>

// ✗ Avoid: Copying button HTML everywhere
<div class="button">...</div>
<div class="button">...</div>
<div class="button">...</div>

3. Serialize Configuration

Let designers configure scripts without code:

class Rotator extends balancy.ElementBehaviour {
    // @serialize {number}
    speed = 1.0;  // Designer can change per instance

    // @serialize {boolean}
    clockwise = true;
}

4. Clean Up Resources

Always clean up in onDestroy():

onDestroy() {
    // Stop timers
    if (this.timer) clearInterval(this.timer);

    // Remove listeners
    this.element.removeEventListener('click', this.clickHandler);

    // Release references
    this.targetElement = null;
}

5. Check References

Always check element references before use:

start() {
    if (this.targetElement) {
        const health = this.targetElement.getComponent(HealthBar);
        if (health) {
            this.observeHealth(health);
        }
    }
}

Common Patterns

1. Composition Over Inheritance

Combine simple components instead of complex hierarchies:

// ✓ Good: Multiple focused components
<div id="player">
    <div data-script-id="Health"></div>
    <div data-script-id="Movement"></div>
    <div data-script-id="Inventory"></div>
</div>

class GameController {
    start() {
        this.health = this.getComponent(Health);
        this.movement = this.getComponent(Movement);
        this.inventory = this.getComponent(Inventory);
    }
}

2. Event-Driven Communication

Use callbacks for loosely coupled components:

class HealthBar extends balancy.ElementBehaviour {
    // @serialize {function}
    onDeath = null;

    takeDamage(amount) {
        this.health -= amount;
        if (this.health <= 0) {
            this.onDeath?.();
        }
    }
}

class AIController extends balancy.ElementBehaviour {
    start() {
        const health = this.getComponent(HealthBar);
        health.onDeath = () => this.handleDeath();
    }
}

3. Template-View Pattern

Separate data (template) from presentation (component):

class ItemCard extends balancy.ElementBehaviour {
    // @serialize {element}
    nameText = null;
    // @serialize {element}
    iconImage = null;

    init(itemData) {
        // Update UI with data
        this.nameText.element.textContent = itemData.name;
        balancy.setImage(this.iconImage.element, itemData.icon);
    }
}

// Usage
const card = await balancy.instantiatePrefabById('item-card');
const cardScript = card.getComponent(ItemCard);
cardScript.init(myItemData);

Troubleshooting

Scripts Not Running

Problem: Component's update() not being called.

Solutions:

  • Check if balancy.ElementsManager.initialize() was called
  • Verify script is registered: balancy.ElementsManager.registerScript('MyScript', MyScript)
  • Ensure data-script-id matches registered name
  • Check component is enabled: component.enabled === true
  • Verify element is active in hierarchy

Element References Not Working

Problem: Serialized element reference is null.

Solutions:

  • Verify referenced element has data-guid attribute
  • Check GUID in data-script-params matches the target element
  • Ensure referenced element exists when script initializes
  • For prefab clones, verify GUIDs were updated correctly

Prefabs Not Loading

Problem: data-prefab-id element stays empty.

Solutions:

  • Check prefab ID is correct
  • Verify prefab loader is set: prefabsManager.setLoader(loaderFn)
  • Look for errors in console
  • Ensure processPrefabPlaceholders() was called

Next Steps