Portals Building Guide - Complete Reference
Overview
Portals is a platform for creating interactive 3D virtual spaces. This guide covers all building tools, the Interactive Studio (no-code gamification), and advanced scripting with Function Effects.
BUILDING BASICS
Creating a Space
- Sign in to Portals
- Click profile icon → My Spaces
- Click "Create New"
- Choose a template
- Name your space
- Click "Create New Space"
- You'll automatically load into your space
Build Mode
- Click the wrench icon to enter build mode
- Select an item from inventory
- Click in your space to place the item
BUILDING TOOLS (20 Total)
| Tool | Description |
|---|---|
| AI Item Generator | Automated 3D object creation |
| Image | Place 2D images in 3D space |
| Video | Embed video players |
| Build Block | Basic structural elements |
| Screenshare | Display sharing |
| Portal | Teleportation between spaces/locations |
| Custom GLB | Import custom 3D models |
| Light Source | Static lighting |
| Blink Light | Animated/flashing lights |
| Spotlight | Directional lighting |
| Spawn Point | Player entry locations |
| NPC | Non-player characters with dialogue/AI |
| Billboard | Text/image display surfaces |
| Jump Pad | Launch players into air |
| Trigger Cube | Invisible interaction zones |
| Collectible GLB | Gatherable 3D objects |
| Leaderboard | Score tracking display |
| Elemental | Environmental effects |
| Chart | Live data visualization (crypto) |
| World Text | 3D text in space |
Portal Tool
Creates teleportation between spaces or spawn points.
Configuration:
- Destination: Full space URL
- Spawn Name: Target spawn point (case-sensitive, leave blank for default)
- Auto Teleport: On = instant, Off = press X to activate
- Custom Message: Action prompt text (e.g., "teleport" shows "Press X to teleport")
Trigger Cube
Invisible zone that activates events when players enter.
Settings:
- Press X to Activate: Toggle between auto-trigger or manual activation
- Events: Add actions (open doors, play sounds, teleport, etc.)
- Custom Title: Label for organization
Important: Trigger cubes only activate when a player enters the cube (crosses the boundary from outside). If a player spawns or loads into the game already inside a trigger cube, the trigger will NOT fire.
Organization Tip: Place related triggers on Build Blocks (which have a color selector), give them distinct colors, and add On Hover Text Display labels to organize your game logic.
NPC (Non-Player Character)
Interactive characters with dialogue trees and AI.
Setup:
- Paste GLB avatar URL or choose preset
- Configure: Name, Auto Popup, Default Animation, AI Settings
NPC-Specific Effects:
- Turn To Player
- Walk to Position
- Record NPC Path
- Change Animation
- Change Avatar Mood
- Show/Hide NPC
Requirement: Must use rigged GLB avatars for animations.
INTERACTIVE STUDIO
The no-code system for gamifying spaces. Three core components:
Tasks
Tasks are progress trackers with three states:
- NotActive (default starting state)
- Active
- Completed
Tasks can transition between states in any order via triggers.
Task Types:
- Single Player Tasks
- Multiplayer Tasks
- Dependent Tasks (chains)
- Non-Persistent Tasks (reset on reload)
- Quests (visible in quest log)
Task Debug Panel
Access via Space Options > Task Debug Panel
Important: Task system must be turned on BEFORE opening debug panel.
State Indicators:
- Red circle = NotActive
- Yellow circle = Active
- Green circle = Completed
Features:
- Change state of any task (single or multiplayer)
- View complete history log of all state changes
- Essential for testing effects and triggers
Node View
Visual graph interface for analyzing game logic.
Access: Space Options > Tasks > Click 'Graph' button
Node Colors:
- Grey = Objects
- Green = Triggers
- Purple = Tasks
- Yellow = Effects
Features:
- View task dependencies (arrows show relationships)
- Create Tasks, Triggers, Effects directly in the graph
- Click any node to edit its configuration
- See which triggers fire which tasks
Triggers (17 Types) - Detailed Guide
Triggers are events that cause tasks to change state. Understanding when each trigger fires is critical for building reliable game logic.
Backpack Item Activated
What it does: Fires when a player uses an item from their backpack/inventory.
Use cases:
- Consumable items (health potions, power-ups)
- Tools that players can activate on demand
- Special abilities tied to inventory items
Example: Player clicks "Health Potion" in backpack → trigger fires → effect heals player
Click
What it does: Fires when a player clicks on the object this trigger is attached to.
Use cases:
- Interactive buttons and switches
- Doors that open when clicked
- NPCs that respond to clicks
- Any "press to interact" mechanic
Important: The click must be on the specific object. For area-based interactions, use User Enter Trigger instead.
Example: Player clicks a lever → trigger fires → door opens
Collision
What it does: Fires when the player's physics collider touches a trigger volume. This is for physics-based interactions.
Use cases:
- Detecting when player hits an obstacle
- Projectile impacts
- Physics puzzle elements
Important: For simply detecting when a player walks into an area, use "User Enter Trigger" instead. Collision is for physics interactions.
Item Collected
What it does: Fires when a player picks up a Collectible GLB object.
Use cases:
- Coin/gem collection games
- Scavenger hunts
- Key items for puzzles
- Tracking collection progress
Common pattern: Item Collected → Update Value (+1) → check if all items collected
Key Pressed
What it does: Fires when player presses a specific keyboard key.
Configuration: Select which key to listen for (E, F, numbers, etc.)
Use cases:
- Custom interaction keys beyond the default X
- Ability hotkeys (press 1 for sword, 2 for shield)
- Debug/admin commands
- Quick actions without clicking
Important: Only works when the Portals window has focus. If player clicks outside, key triggers won't fire.
Key Released
What it does: Fires when player releases a keyboard key they were holding.
Use cases:
- Charged abilities (hold to charge, release to fire)
- Sprint mechanics (hold to run, release to walk)
- Any hold-and-release interaction
Player Died
What it does: Fires when the player's health reaches zero.
Use cases:
- Respawn systems
- Death counters
- Game over screens
- Score penalties on death
- Team deathmatch kill tracking
Critical for games: This is how you detect kills. When Player A kills Player B, Player B's "Player Died" trigger fires. The killer is determined by which team the dead player was on (enemy team gets the point).
Example pattern:
Player Died →
Check which team player was on →
Award point to opposite team →
Respawn player after delay
Player Login
What it does: Fires once when a player enters/loads into the space.
Use cases:
- Initialize player variables (set team to 0, health to 100)
- Show welcome messages or tutorials
- Spawn HUD iframes
- Assign player to default state
- Start background music
Critical: This is your "on game start" trigger for each player. Use it to set up everything the player needs.
Common pattern:
Player Login →
Set Player_Team = 0
Set Player_Health = 100
Show HUD iframe
Player Started Moving
What it does: Fires when a stationary player begins moving (WASD or joystick).
Use cases:
- Tutorial prompts ("Great, you're moving!")
- Stealth games (movement breaks stealth)
- Idle detection systems
- Triggering ambient sounds when player moves
Player Stopped Moving
What it does: Fires when a moving player comes to a stop.
Use cases:
- Idle animations or effects
- "Stand still to interact" mechanics
- AFK detection
- Meditation/rest mechanics
Swap Volume
What it does: Fires when audio volume or track changes.
Use cases:
- Sync visual effects to music
- Trigger events on song changes
- Audio-reactive environments
Timer Stopped
What it does: Fires when a countdown timer reaches zero.
Use cases:
- Race finish detection
- Time-limited challenges
- Round timers
- Bomb defusal countdown
Note: This is for the built-in Portals timer, not custom iframe timers.
Timer Limitation: Portals does not currently have a native shared timer system that can be synchronized across players and pulled into an iframe. For multiplayer timer displays, use local JavaScript timing in iframes triggered by game state changes. Timer values shown will be approximate and client-side.
User Enter Trigger
What it does: Fires when a player walks into a Trigger Cube's volume.
This is one of the most important triggers. Use it for:
- Zone detection (player entered red base, blue base, danger zone)
- Automatic doors
- Checkpoint systems
- Team selection areas
- Teleport zones
- Any "walk here to do something" mechanic
Configuration:
- Attach to a Trigger Cube object
- Size the cube to cover the detection area
- Optional: "Press X to Activate" for manual activation instead of auto
Important: Trigger cubes only activate when a player enters the cube (crosses the boundary from outside). If a player spawns or loads into the game already inside a trigger cube, the trigger will NOT fire. Plan spawn point placement accordingly, or use Player Login trigger for logic that must run when players join.
Example: Player walks into red team zone → User Enter Trigger fires → Set Player_Team = 1
User Exit Trigger
What it does: Fires when a player leaves a Trigger Cube's volume.
Use cases:
- Close doors after player leaves
- Remove buffs when leaving an area
- Stop area-specific music
- Hide contextual UI
- Boundary warnings ("You're leaving the play area!")
Common pattern: Enter shows something, Exit hides it
User Enter → Show Token Swap UI
User Exit → Hide Token Swap UI
Value Updated
What it does: Fires whenever a specific variable changes value.
Configuration: Select which variable to watch
Use cases:
- React to score changes (update HUD when Red_Score changes)
- Threshold detection (when health drops below 20, show warning)
- Sync iframes with game state
- Chain reactions (when X changes, update Y)
Critical for iframes: Use this to automatically send updated values to iframes whenever variables change.
Example pattern:
Value Updated (Red_Score) →
Send Message To Iframes: red_|Red_Score|
Wearable Off
What it does: Fires when player unequips/removes a wearable item.
Use cases:
- Remove buffs when armor is removed
- Track equipment state
- Costume change effects
Wearable On
What it does: Fires when player equips a wearable item.
Use cases:
- Apply buffs when equipping items
- Unlock abilities with equipment
- Costume-specific permissions
- Achievement tracking
Effects (60+ Types) - Detailed Guide
Effects are actions that happen when a task changes state. They're the "do this" part of your game logic.
MOVEMENT & CAMERA EFFECTS
Teleport
What it does: Instantly moves the player to a named spawn point.
Configuration: Enter the exact spawn point name (case-sensitive)
Use cases:
- Respawn systems (teleport to RedSpawn1 after death)
- Fast travel between areas
- Team base assignment
- Returning players to lobby after game ends
- Checkpoint systems
Critical: Spawn point names are CASE-SENSITIVE. "RedSpawn1" is different from "redspawn1".
Example: After player joins red team → Teleport to "RedSpawn1"
Apply Velocity To Player
What it does: Pushes the player in a direction with force.
Use cases:
- Jump pads (launch player upward)
- Knockback effects
- Wind zones
- Boost pads
Change Camera Filter/State/Zoom
What it does: Modifies how the player's camera behaves or looks.
Use cases:
- Cinematic moments (zoom in on important object)
- Drunk/dizzy effects (apply filter)
- Scope/aim mode (zoom in)
- Dramatic reveals
Lock/Unlock Camera
What it does: Prevents or allows player from rotating their camera view.
Use cases:
- Cutscenes (force player to look at something)
- Dialogue sequences
- Tutorial moments
Lock/Unlock Movement
What it does: Prevents or allows player from moving (WASD/joystick).
Use cases:
- Cutscenes
- Dialogue with NPCs
- Puzzle moments where player must stay still
- Pre-game countdown ("Game starts in 3... 2... 1...")
Warning: Always make sure to unlock movement eventually, or player gets stuck!
Toggle Free Camera
What it does: Switches between normal third-person and free-flying camera.
Use cases:
- Spectator mode
- Photo mode
- Building/editing mode
VISUAL & ENVIRONMENT EFFECTS
Hide/Show Object
What it does: Makes an object invisible or visible.
Configuration: Select which object to hide/show
Use cases:
- Doors that disappear when opened
- Hidden passages revealed
- Removing obstacles after puzzle solved
- Showing/hiding visual indicators
Example: Player collects all keys → Show Object (exit door)
Change Fog
What it does: Adjusts the fog density/color in the scene.
Use cases:
- Spooky atmosphere
- Weather changes
- Zone-specific ambiance
- Revealing hidden areas (clear the fog)
Change Time of Day
What it does: Changes lighting to simulate different times.
Use cases:
- Day/night cycles
- Dramatic mood shifts
- Puzzle mechanics (things only visible at night)
Change Bloom
What it does: Adjusts the glow/bloom post-processing effect.
Use cases:
- Dreamy/magical areas
- Power-up visual feedback
- Environmental storytelling
PLAYER EFFECTS
Change Player Health
What it does: Sets or modifies the player's health value.
Configuration:
- Set: Sets health to exact value (e.g., Set to 100)
- Add: Adds to current health (e.g., Add 25 for healing)
- Subtract: Removes health (e.g., Subtract 10 for damage)
Use cases:
- Healing items/zones
- Damage zones (lava, spikes)
- Respawn (Set to 100 after death)
- Poison/DOT effects
Critical for combat games: Use "Set → 100" after respawning to fully heal the player.
Example pattern:
RespawnRed task (on Active):
1. Teleport → RedSpawn1
2. Change Player Health → Set → 100
3. Reset task
Change Avatar
What it does: Changes the player's 3D avatar/character model.
Use cases:
- Team uniforms (red team gets red armor)
- Power-ups that transform player
- Costume unlocks
Lock/Unlock Avatar Change
What it does: Prevents or allows player from changing their avatar.
Use cases:
- Enforce team uniforms during gameplay
- Lock cosmetics during competitive matches
Change Movement Profile
What it does: Modifies player movement speed, jump height, etc.
Use cases:
- Speed boost power-ups
- Slow zones (mud, water)
- Character classes with different mobility
Play Emote
What it does: Makes the player's avatar perform an animation.
Use cases:
- Celebration after winning
- Dance floors
- Social interactions
AUDIO EFFECTS
Play Sound Once
What it does: Plays an audio file one time.
Use cases:
- Door opening sounds
- Pickup sounds
- Victory/defeat fanfares
- Button click feedback
- Death sounds
Play Sound In Loop
What it does: Continuously plays audio until stopped.
Use cases:
- Background music
- Ambient sounds (rain, wind)
- Alarm sounds
Toggle Mute
What it does: Mutes or unmutes audio.
Use cases:
- Audio settings
- Cutscene audio control
UI & DISPLAY EFFECTS
Notification Pill
What it does: Shows a brief popup message to the player.
Configuration: Enter the message text
Use cases:
- "Joined Red Team!"
- "RED TEAM WINS!"
- "+10 Points"
- Tutorial hints
- Achievement unlocked messages
Best practice: Keep messages short and clear. Players only see them briefly.
Display Value
What it does: Shows a variable's value on screen.
Configuration: Select which variable to display
Use cases:
- Score displays
- Health bars
- Coin counters
- Timer displays
Note: For more control over display, use iframes instead.
Hide Value
What it does: Removes a displayed variable from screen.
Iframe
What it does: Opens an external webpage inside the Portals window.
This is extremely powerful. Iframes let you:
- Create custom HUDs with HTML/CSS/JavaScript
- Build complex UI that Portals can't do natively
- Display external content (leaderboards, guides)
- Create interactive menus
See the IFRAMES section for complete documentation.
Close Iframe
What it does: Closes an open iframe.
Use cases:
- Dismiss popups
- Clean up HUDs on game end
Send Message To Iframes
What it does: Sends data from Portals to all open iframes.
Configuration: Enter the message string. Use |variableName| to include variable values.
Built-in variables:
|username|- player's Portals ID or name as a quoted string|position|- all players' positions as an array keyed by username, e.g.{"buster"="(-32.49, 22.21, -99.82)"}
CRITICAL: Do NOT use JSON with colons. Colons break the parser. Use underscore format instead.
Correct: score_|Red_Score| → sends "score_25"
Wrong: {"score": |Red_Score|} → BREAKS
Use cases:
- Update HUD with current scores
- Send timer values
- Sync game state to iframe displays
GAME LOGIC EFFECTS
Update Value
What it does: Creates or modifies a variable.
Configuration:
- Variable name
- Operation (Set, Add, Subtract, Multiply, Divide)
- Value
Use cases:
- Score tracking (Add 1 to Red_Score)
- Health management
- Progress counters
- Any numeric game state
Note: For complex logic, use Function Effect instead.
Function Effect
What it does: Executes NCalc expressions for advanced logic.
This is the most powerful effect. It lets you:
- Read task states and variables
- Conditional logic (if/else)
- Set multiple values
- Complex calculations
- Delayed actions
See the FUNCTION EFFECT section for complete documentation.
Post Score to Leaderboard
What it does: Submits a player's score to a leaderboard.
Configuration: Value Label must match the leaderboard's score label exactly.
Use cases:
- High score submission
- Race time recording
- Competition rankings
Reset All Tasks
What it does: Sets all tasks back to NotActive.
Use cases:
- Full game reset
- New round starting
- Debug/testing
Warning: This resets EVERYTHING. Use carefully.
Run Trigger From Effector
What it does: Manually fires another trigger.
Use cases:
- Chain reactions
- Reusing trigger logic from multiple places
OBJECT EFFECTS
Move Item
What it does: Moves an object to a new position.
Use cases:
- Sliding doors
- Moving platforms
- Puzzle pieces
Duplicate Item
What it does: Creates a copy of an object.
Use cases:
- Spawning collectibles
- Particle effects
- Dynamic content generation
Portals Animation
What it does: Plays a built-in Portals animation on an object.
NPC EFFECTS
These effects only work on NPC objects, not regular 3D models.
Turn To Player
What it does: Rotates the NPC to face the player.
Use cases:
- NPCs that acknowledge player presence
- Shopkeepers looking at customers
- Guards tracking player movement
Walk to Position
What it does: Makes NPC walk to a specified location.
Use cases:
- NPC patrols
- Characters moving during cutscenes
- Quest givers walking to quest locations
Change Animation
What it does: Changes which animation the NPC is playing.
Configuration: Animation name (must match GLB file exactly, case-sensitive)
Use cases:
- NPC reactions (wave, point, scared)
- State changes (idle → talking)
- Combat animations
Show/Hide NPC
What it does: Makes NPC visible or invisible.
Use cases:
- NPCs appearing for quests
- Characters leaving after conversation
- Dramatic reveals
Message to NPC
What it does: Sends a command to an NPC's AI system.
Use cases:
- AI conversation prompts
- Behavior changes
GAME BUILDING METHODOLOGY
How to Approach Building Any Game
Before writing a single task or effect, spend 10-15 minutes planning. This prevents 90% of debugging headaches.
Step 1: Define Your Core Loop
Every game has a core loop. Write it in plain English first:
Example - Team Deathmatch:
1. Player joins a team (Red or Blue)
2. Player spawns at team base
3. Player kills enemy players
4. Team scores points for kills
5. First to 50 points OR time runs out → winner declared
6. Everyone returns to lobby
Example - Collectible Hunt:
1. Player enters the game area
2. Player finds and collects hidden items
3. Each item adds to their score
4. When all items collected → show victory
Step 2: Identify Your Variables
List every piece of data your game needs to track:
| Variable | Multiplayer? | Persistent? | Purpose |
|---|---|---|---|
Player_Team |
No | No | Which team this player is on (0=none, 1=red, 2=blue) |
Red_Score |
Yes | No | Red team's total points |
Blue_Score |
Yes | No | Blue team's total points |
Elapsed_Seconds |
Yes | No | Game timer |
Key questions:
- Does everyone need to see the same value? → Multiplayer = Yes
- Should it survive page refresh? → Persistent = Yes
- Is it per-player or global? → Per-player = Multiplayer No
Step 3: Identify Your Tasks
Tasks are your game's state machine. List the major "events" or "states":
| Task | Type | What triggers it? | What does it do? |
|---|---|---|---|
JoinRed |
Single Player | Player enters red zone | Set team=1, teleport to red spawn |
JoinBlue |
Single Player | Player enters blue zone | Set team=2, teleport to blue spawn |
KillHandler |
Single Player | Player dies | Award point to enemy team, respawn |
RedWins |
Multiplayer | Red reaches 50 OR time runs out | Show victory, reset game |
BlueWins |
Multiplayer | Blue reaches 50 OR time runs out | Show victory, reset game |
Key insight: Single Player tasks = actions that affect only one player. Multiplayer tasks = state changes everyone sees.
Step 4: Build in Order
Always build in this order:
- Variables first - Create all variables in Variable Manager
- Spawn points - Place all teleport destinations
- Core triggers - The main "what starts things" (Player Login, User Enter Trigger)
- Core tasks - One at a time, test each before moving on
- Win conditions - What ends the game
- Polish - HUDs, sounds, effects
Step 5: Test Each Piece Individually
DO NOT build everything then test. Build ONE task, test it works, then move to the next.
Testing checklist for each task:
- Open Task Debug Panel
- Manually set the task to each state
- Verify effects fire correctly
- Verify task resets properly
- Check Variable Manager for correct values
Complete Game Example: Team Deathmatch
Here's the complete implementation of a TDM game, step by step.
Phase 1: Setup Variables
Create these in Variable Manager:
| Variable | Multiplayer | Persistent | Initial |
|---|---|---|---|
Player_Team |
No | No | 0 |
Red_Score |
Yes | No | 0 |
Blue_Score |
Yes | No | 0 |
Phase 2: Create Spawn Points
Place and name these spawn points:
LobbySpawn- Where players startRedSpawn1,RedSpawn2,RedSpawn3- Red team spawnsBlueSpawn1,BlueSpawn2,BlueSpawn3- Blue team spawns
Phase 3: Team Selection
Task: JoinRed
- Type: Single Player
- Trigger: User Enter Trigger (red zone cube)
Effects (on Active):
1. Function Effect - Set team (only if not already on a team):
if($N{Player_Team} == 0.0,
SetVariable('Player_Team', 1.0, 0.0),
0.0
)
2. Teleport → RedSpawn1
3. Notification Pill → "Joined Red Team"
4. Function Effect - Reset task:
SetTask('JoinRed', 'NotActive', 0.1)
Task: JoinBlue - Same pattern with Player_Team = 2.0 and BlueSpawn1
Test: Walk into red zone. Check:
- Variable Manager shows Player_Team = 1
- You teleported to RedSpawn1
- Notification appeared
- Task reset to NotActive
Phase 4: Kill Handler
Task: KillHandler
- Type: Single Player
- Trigger: Player Died
Effects (on Active):
1. Function Effect - Award point to enemy team:
if($N{Player_Team} == 1.0,
SetVariable('Blue_Score', $N{Blue_Score} + 1.0, 0.0),
if($N{Player_Team} == 2.0,
SetVariable('Red_Score', $N{Red_Score} + 1.0, 0.0),
0.0
)
)
2. Function Effect - Respawn after 3 seconds:
if($N{Player_Team} == 1.0,
SetTask('RespawnRed', 'Active', 3.0),
if($N{Player_Team} == 2.0,
SetTask('RespawnBlue', 'Active', 3.0),
0.0
)
)
3. Function Effect - Check for winner:
SetTask('CheckWinner', 'Active', 0.1)
4. Function Effect - Reset:
SetTask('KillHandler', 'NotActive', 0.1)
Task: RespawnRed
- Type: Single Player
- Trigger: None (activated by KillHandler)
Effects (on Active):
1. Teleport → RedSpawn1
2. Change Player Health → Set → 100
3. Function Effect - Reset:
SetTask('RespawnRed', 'NotActive', 0.1)
Phase 5: Win Condition
Task: CheckWinner
- Type: Multiplayer
- Trigger: None (activated by KillHandler)
Effects (on Active):
1. Function Effect - Check scores:
if($N{Red_Score} >= 50.0,
SetTask('RedWins', 'Active', 0.0),
if($N{Blue_Score} >= 50.0,
SetTask('BlueWins', 'Active', 0.0),
0.0
)
)
2. Function Effect - Reset:
SetTask('CheckWinner', 'NotActive', 0.1)
Task: RedWins
- Type: Multiplayer
- Trigger: None
Effects (on Active):
1. Notification Pill → "RED TEAM WINS!"
2. Function Effect - Reset scores (2 sec delay):
SetVariable('Red_Score', 0.0, 2.0)
SetVariable('Blue_Score', 0.0, 2.0)
3. Function Effect - Reset player teams:
SetVariable('Player_Team', 0.0, 2.0)
4. Function Effect - Return to lobby:
SetTask('ReturnToLobby', 'Active', 2.0)
5. Function Effect - Reset self:
SetTask('RedWins', 'NotActive', 3.0)
Complete Game Example: Collectible Hunt
Variables
| Variable | Multiplayer | Purpose |
|---|---|---|
Items_Collected |
No | How many items this player found |
Total_Items |
No | Total items to find (set on login) |
Task: InitGame
- Trigger: Player Login
Effects:
1. SetVariable('Items_Collected', 0.0, 0.0)
2. SetVariable('Total_Items', 10.0, 0.0)
3. Notification Pill → "Find all 10 items!"
Task: ItemCollected
- Trigger: Item Collected (on each collectible)
Effects:
1. Function Effect:
SetVariable('Items_Collected', $N{Items_Collected} + 1.0, 0.0)
2. Notification Pill → "+1 Item Found!"
3. Function Effect - Check if all found:
if($N{Items_Collected} >= $N{Total_Items},
SetTask('Victory', 'Active', 0.0),
0.0
)
DEBUGGING GUIDE
Systematic Debugging Approach
When something doesn't work, follow this exact process:
Step 1: Identify the Symptom
Be precise about what's wrong:
- ❌ "It doesn't work"
- ✅ "The door doesn't open when I click it"
- ✅ "The score updates but the HUD doesn't show it"
- ✅ "Red team gets points when red players die (should be blue)"
Step 2: Trace the Flow
Write out what SHOULD happen:
1. Player clicks door → Click trigger fires
2. Click trigger → DoorOpen task becomes Active
3. DoorOpen Active → Hide Object effect runs
4. Door becomes invisible
Then identify WHERE it breaks:
- Does the trigger fire? (Check Task Debug Panel)
- Does the task change state? (Check Task Debug Panel)
- Does the effect run? (Check if object changes)
Step 3: Use Task Debug Panel
This is your most important debugging tool.
Access: Space Options > Task Debug Panel
What to check:
- Is the task in the expected state?
- Does clicking the trigger change the state?
- Look at the history log - what events fired?
Pro tip: Keep Task Debug Panel open while testing. Watch task states change in real-time.
Step 4: Check Common Causes
| Symptom | Likely Cause |
|---|---|
| Trigger doesn't fire | Wrong trigger type, object not selected, trigger cube too small |
| Task changes but no effect | Effect not added, wrong "on state" setting |
| Effect runs for wrong player | Task is Multiplayer when it should be Single Player |
| Variable not updating | Wrong variable name (case-sensitive), using wrong syntax |
| Function Effect does nothing | Missing "Trigger on Task Change" checkbox |
| Numbers look weird | Not using decimal notation (use 1.0 not 1) |
Step 5: Isolate and Test
If you can't find the bug:
- Create a NEW simple task that does just one thing
- Test if that works
- Add complexity one step at a time
- Find exactly which addition breaks it
Common Bugs and Fixes
Bug: "Task fires but effects don't run"
Causes:
- Effects attached to wrong state (e.g., effects on "Completed" but task goes to "Active")
- Missing "Trigger on Task Change" checkbox for Function Effects
- Condition in Function Effect always evaluates false
Fix: Check which state triggers your effects. Open the task, look at "On Active", "On Completed", etc.
Bug: "Variable shows wrong value"
Causes:
- Case mismatch (
Red_Scorevsred_score) - Using $T instead of $N (task state vs variable)
- Not using decimal notation
- Multiple places updating the same variable
Fix:
- Check exact spelling in Variable Manager
- Use $N{variableName} for variables
- Use 1.0 not 1
Bug: "Multiplayer task runs effects for all players"
Cause: That's what Multiplayer tasks do! When the task state changes, ALL players run the effects.
Fix: If only one player should be affected, use Single Player task instead.
Bug: "Timer speeds up when second player joins"
Cause: Each player is running their own timer loop, all incrementing the same variable.
Fix: Use Single Player tasks for timer loops. Multiplayer tasks cause all players to run the loop simultaneously.
Note: Portals does not have a native shared timer system. For multiplayer timer displays, use local JavaScript timing in iframes triggered by game state changes.
Bug: "Iframe not receiving messages"
Causes:
- JSON with colons (
:) breaks the parser - Wrong variable syntax (using $N instead of |pipes|)
- Iframe not loaded yet when message sent
Fix:
- Use underscore format:
score_|Red_Score|not{"score": |Red_Score|} - Use pipe syntax in Send Message To Iframes
- Use ready handshake pattern for timing
Bug: "Player can join both teams"
Cause: No check to prevent re-joining if already on a team.
Fix: Add condition before setting team:
if($N{Player_Team} == 0.0,
SetVariable('Player_Team', 1.0, 0.0),
0.0
)
Bug: "Task doesn't reset, only fires once"
Cause: Tasks don't auto-reset. Once they reach a state, they stay there.
Fix: Always add a reset effect:
SetTask('MyTask', 'NotActive', 0.1)
Bug: "Function Effect condition never true"
Causes:
- Comparing wrong types (string vs number)
- Not using decimal notation
- Logic error in condition
Debug approach:
- Simplify to just
SetVariable('debug', 1.0, 0.0)- does it run at all? - Add debug variables to see what values actually are
- Check if condition should use == or >= or !=
Bug: "Effects fire in wrong order"
Cause: Effects on the same task run in order, but there's no guarantee of timing between different tasks.
Fix: Use delays to enforce order:
SetTask('Step1', 'Active', 0.0)
SetTask('Step2', 'Active', 0.5) // Runs 0.5 sec after Step1
SetTask('Step3', 'Active', 1.0) // Runs 1 sec after Step1
Bug: "Multiplayer variables not syncing"
Cause: Multiplayer variables take 2-3 seconds to sync across all players.
Fix: Add delays before checking multiplayer variables:
// On Player Login, wait 3 seconds before checking shared state
SetTask('DelayedCheck', 'Active', 3.0)
Debugging Iframes
Enable Browser Console
When testing iframes:
- Open browser developer tools (F12)
- Go to Console tab
- Look for errors and your console.log() messages
Add Debug Logging
Add console.log() statements to track what's happening:
PortalsSdk.setMessageListener(function(message) {
console.log('[Iframe] Received:', message); // See what Portals sends
// Your parsing code...
console.log('[Iframe] Parsed value:', parsedValue); // See what you extracted
});
Test Iframe Standalone
Open your iframe URL directly in a browser to test:
- Does the page load without errors?
- Are there any console errors?
- Does the UI render correctly?
Then test the Portals integration separately.
Debug Display
Add a visible debug element to your iframe:
<div id="debug" style="position:fixed;bottom:10px;left:10px;background:black;color:lime;padding:5px;font-family:monospace;font-size:12px;z-index:9999;">
Waiting for messages...
</div>
<script>
function debug(msg) {
document.getElementById('debug').textContent = msg;
console.log('[Debug]', msg);
}
PortalsSdk.setMessageListener(function(message) {
debug('Received: ' + JSON.stringify(message).substring(0, 50));
// ... rest of handler
});
</script>
Remove this before publishing!
IFRAME BEST PRACTICES
The Golden Rules of Portals Iframes
Rule 1: Different Syntax for Each Direction
This is the #1 source of iframe bugs. The syntax is DIFFERENT depending on direction:
Portals → Iframe (Send Message To Iframes effect):
score_|Red_Score|_|Blue_Score|
- Use pipe syntax
|variableName|for variables - Built-in variables:
|username|(player ID/name),|position|(all players' positions) - Do NOT use
$N{variableName}- that's for Function Effects only - Do NOT use JSON with colons - it breaks the parser
Iframe → Portals (JavaScript):
PortalsSdk.sendMessageToUnity(JSON.stringify({
TaskName: 'myTask',
TaskTargetState: 'SetNotActiveToActive'
}));
- MUST use JSON.stringify()
- MUST use exact TaskTargetState values
- Raw objects cause "[object Object] is not supported" error
Rule 2: Iframes Load Asynchronously
The Problem: When you activate an iframe AND send a message in the same task, the message arrives BEFORE the iframe finishes loading. The iframe never receives it.
Console log showing this bug:
[12:00:01] Sending message to iframes: scores_50_32
[12:00:01] Activating Iframe Event https://example.com/game-over.html
[12:00:02] [Iframe] Initialized - waiting for messages ← Too late!
The Solution: Ready Handshake Pattern
- Pass static data in URL parameters (always available)
- Iframe sends "ready" signal when loaded
- Portals sends dynamic data AFTER ready signal
Task activates iframe with ?winner=red in URL
↓
Iframe loads, reads winner from URL
↓
Iframe sends: gameover_ready → Completed
↓
Portals function triggers on gameover_ready Completed
↓
Portals sends: scores_50_32
↓
Iframe receives scores message, displays result
Rule 3: Always Bust the Cache
GitHub Pages and browsers cache aggressively. Your changes won't appear without cache busting.
Add version parameter to ALL iframe URLs:
https://example.github.io/my-hud/index.html?v=1
Every time you update the iframe:
- Push to GitHub
- Wait 1-2 minutes for GitHub Pages to update
- Increment version:
?v=2,?v=3, etc. - Update the URL in Portals
Pro tip: Use a high random number during development (?v=99847) so you don't have to track versions.
Rule 4: Handle Message Parsing Defensively
Messages from Portals can arrive in different formats. Your parsing code must handle all cases:
PortalsSdk.setMessageListener(function(message) {
console.log('[Iframe] Raw message:', message, typeof message);
let msg = message;
// Step 1: If it's a string, try to parse as JSON
if (typeof message === 'string') {
try {
msg = JSON.parse(message);
} catch (e) {
// Not JSON - that's OK, keep as string
// DO NOT RETURN HERE - underscore format won't be JSON
msg = message;
}
}
// Step 2: Convert to string for pattern matching
const msgStr = typeof msg === 'string' ? msg : JSON.stringify(msg);
// Step 3: Handle your message patterns
if (msgStr.startsWith('score_')) {
const value = parseFloat(msgStr.substring(6));
if (!isNaN(value)) {
updateScore(value);
}
} else if (msgStr.startsWith('time_')) {
const value = parseFloat(msgStr.substring(5));
if (!isNaN(value)) {
updateTimer(value);
}
}
// Add more patterns as needed
});
Critical mistakes to avoid:
- Don't
returnafter JSON.parse fails - underscore format isn't JSON - Don't assume message type - always check
- Don't forget
!isNaN()check after parseFloat
Rule 5: Design for Late Joiners
In multiplayer, players can join mid-game. Your iframe needs to handle this:
Problem: Player joins when score is 25-18 and timer is at 5:32. Their iframe shows 0-0 and 10:00.
Solution: Sync on Load
Option A: Request current state
// Iframe requests sync when loaded
PortalsSdk.sendMessageToUnity(JSON.stringify({
TaskName: 'request_sync',
TaskTargetState: 'SetNotActiveToActive'
}));
// Portals responds with full state: sync_332_25_18
Option B: Continuous updates
// Portals sends updates every second via Value Updated triggers
// Late joiner receives next update within 1 second
Rule 6: Keep Iframes Stateless When Possible
Iframes can be closed and reopened. They can refresh. Don't rely on iframe state.
Bad: Iframe tracks score internally, only receives increments
let score = 0;
// If iframe refreshes, score resets to 0 but game score is 25
Good: Iframe receives absolute values, displays what it's told
// Portals always sends current total: score_25
// Iframe just displays 25, no internal tracking needed
Rule 7: Transparent Backgrounds for HUDs
For HUD overlays, you want only your UI visible, not a white/colored background:
html, body {
background: transparent; /* Critical! */
margin: 0;
padding: 0;
overflow: hidden;
}
.hud-container {
/* Apply visible background only to your content */
background: rgba(0, 0, 0, 0.8);
border-radius: 8px;
padding: 10px;
}
The key: Keep html/body transparent, apply backgrounds only to content containers.
Rule 8: Size Iframes Correctly
For HUDs: Size to match your content exactly
- If your HUD bar is 600x80px, set iframe to 600x80
- Extra space will be transparent but may block clicks
For Popups/Modals: Size to fit with padding
- Leave room for close button if visible
- Consider dif
…(truncated)