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 valuesboolean: true/falseelement: 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:
- Designer attaches prefab element reference to
itemPrefabparameter in UI Builder - Script uses
this.itemPrefab.instantiate()to create instances - 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-idmatches 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-guidattribute - Check GUID in
data-script-paramsmatches 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¶
- Learn about Built-in Scripts for ready-to-use components
- Check Balancy API for helper methods
- Explore Templates for complete examples