Communal Hyperfy Note Pool

itsmetamike

hiroP

Really basic: adding mesh to your world

To add a mesh to your world you can edit src/core/systems/ClientEnvironment.js

In there just add something like:

    const hiroLogo = await this.world.loader.load('glb', 'asset://hiroLogo01.glb')
    const hiroRoot = hiroLogo.toNodes()
    hiroRoot.activate({ world: this.world, physics: true })
    hiroRoot.position.set(0, 10, 10) // Adjust x,y,z as needed

to async start()

The asset in the above will be accessible in whatever directory you set in the .env

Adding collisions to meshes

Basic collider

There are a few occassions when you might want a simple collision but usually you'll need to structure your object with a hierarchy like below in the #Dynamic-collider section. Let's imagine you do actually want a simple sphere to act as a collider. You would create the following hierarchy in your Blender object:

MyBall (Empty)
├── BallMesh (Detailed sphere if you need one)
└── PhysicsSphere (Simple sphere mesh)
    └── Custom Properties:
        node = "rigidbody"
        type = "dynamic"
        node = "collider"
        convex = true

To set the custom properties you select your mesh (simple sphere in this example) and scroll down to the bottom and add a new custom property like this:
blender_hBt5JNVUNI

  • click OK and then set the value to 'collider' so it looks like this:
    blender_nEzAWBJJCu

If you want to create a fancy ball (eg one with football panels/decals etc. you'd add that as a sibling of PhysicsSphere so that it isn't used in the physics calculations.

For anything more complicated that this you will want to separate the collision and rigidbody meshes as shown in the next section.

Dynamic collider

To create a collider that responds to physics you need to create an object with the following structure:

MyDynamicObject (Empty/Group)
├── DetailedMesh (Complex mesh - what the thing looks like)
└── RigidBody (Empty or minimal mesh)
    └── Custom Properties:
        node = "rigidbody"
        type = "static" | "kinematic" | "dynamic"
        mass = [number value >= 0] (see note below)
        tag = [optional string, cannot be "player"]
    └── CollisionShape (Simplified collision mesh)
        └── Custom Properties:
            node = "collider"
            convex = [true/false]

Note: mass is not currently supported so just leave it out - it will be set to a default of 1.

RigidBody types

Looking at the RigidBody.js code, there are three types defined:

const types = ['static', 'kinematic', 'dynamic']

Here's what each does:

  1. static - For immovable objects like walls, floors, or fixed furniture. The code shows these create a createRigidStatic PhysX actor which:

    • Won't move when hit
    • Other objects will collide with it
    • Most efficient performance-wise
  2. dynamic - For objects that should move realistically with physics. Creates a createRigidDynamic PhysX actor that:

    • Will respond to gravity
    • Will bounce/collide with other objects
    • Will be pushed by forces
    • Uses the mass property for physics calculations
  3. kinematic - For objects that move but aren't affected by physics. Also creates a createRigidDynamic actor but sets the KINEMATIC flag:

    • Can be moved programmatically
    • Other objects will collide with it
    • Won't respond to gravity or forces
    • Mass is less important since it's not physics-driven

So for your example - if you want the object to fall and bounce realistically, use type="dynamic". If it should stay perfectly still, use type="static". Use kinematic if you want to move it programmatically but still have other objects collide with it.

peezy

Deploying Your Hyperfy World

This guide will walk you through deploying your Hyperfy world on a VPS. Don't worry if you're new to server deployment - we'll go through it step by step!

Initial VPS Setup

Before connecting to your VPS, you need to upload an SSH key to your provider's platform.

# Generate an SSH key if you don't have one
ssh-keygen -t ed25519 -C "your_email@example.com"

# Display your public key
cat ~/.ssh/id_ed25519.pub   # On Mac/Linux
# or
type $env:USERPROFILE\.ssh\id_ed25519.pub   # On Windows PowerShell

Copy the entire key and add it to your VPS provider's dashboard.

Setting Up Your Server

1. Initial Connection

# Connect to your VPS as root
ssh root@your_server_ip

2. Create a Deployment User

# Create new user (replace YOUR_USERNAME with your preferred username)
adduser YOUR_USERNAME
usermod -aG sudo YOUR_USERNAME

# Set up SSH for the new user
mkdir -p /home/YOUR_USERNAME/.ssh
cp ~/.ssh/authorized_keys /home/YOUR_USERNAME/.ssh/
chown -R YOUR_USERNAME:YOUR_USERNAME /home/YOUR_USERNAME/.ssh
chmod 700 /home/YOUR_USERNAME/.ssh
chmod 600 /home/YOUR_USERNAME/.ssh/authorized_keys

# Exit and reconnect as your user
exit
ssh YOUR_USERNAME@your_server_ip

3. Secure the SSH Configuration

After verifying you can log in as your user, secure the SSH service:

# Make a backup of the original configuration
sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak

# Edit the SSH configuration
sudo nano /etc/ssh/sshd_config

Make the following changes in the configuration file:

# Disable root login
PermitRootLogin no

# Disable password authentication
PasswordAuthentication no

# Enable public key authentication
PubkeyAuthentication yes

# Specify which users can connect via SSH
AllowUsers YOUR_USERNAME

# Optional but recommended: Limit SSH access attempts
MaxAuthTries 3

Apply the changes:

# Test the configuration for syntax errors
sudo sshd -t

# Restart the SSH service
sudo systemctl restart sshd

Important: Before closing your current SSH session:

  1. Open a new terminal
  2. Try to connect with the new configuration
  3. Make sure it works before closing your working session

If something goes wrong, you can restore the backup:

sudo cp /etc/ssh/sshd_config.bak /etc/ssh/sshd_config
sudo systemctl restart sshd

4. Install Node.js Via NVM

# Install required build tools
sudo apt update
sudo apt install -y curl git build-essential

# Install NVM
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash

# Load NVM (or reconnect to your server)
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"

# Install required Node version (22.11.0+)
nvm install 22

# Set as default
nvm alias default 22

4. Install PM2 and Nginx

# Install PM2 globally
npm install -g pm2

# Install Nginx
sudo apt install -y nginx

Deploying Your Hyperfy World

1. Clone and Setup Your World

# Go to your home directory
cd ~

# Clone the repository
git clone https://github.com/hyperfy-xyz/hyperfy.git my-world
cd my-world

# Copy environment file
cp .env.example .env

# Install dependencies
npm install

2. Configure Environment Variables

change the environment variables to point to your domain url

# Edit your environment file
nano .env
PUBLIC_WS_URL=https://YOUR_DOMAIN.com/ws
PUBLIC_API_URL=https://YOUR_DOMAIN.com/api
PUBLIC_ASSETS_URL=https://YOUR_DOMAIN.com/assets

3. Start Your Application with PM2

# Start the application
pm2 start npm --name "hyperfy" --interpreter bash -- start

# Set PM2 to start on system boot
pm2 startup
pm2 save

4. Configure Nginx as Reverse Proxy

# Remove default config
sudo rm /etc/nginx/sites-enabled/default

# Create new config (replace yourdomain.com with your actual domain)
sudo nano /etc/nginx/sites-available/yourdomain.com

Add this configuration:

server {
    listen 80;
    server_name your-domain.com;  # Change to your domain

    location / {
        proxy_pass http://localhost:3000;  # Change port if different
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        
        # WebSocket support
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

Enable and start Nginx:

# Enable the site
sudo ln -s /etc/nginx/sites-available/yourdomain.com /etc/nginx/sites-enabled/

# Test configuration
sudo nginx -t

# Start Nginx
sudo systemctl restart nginx

5. Configure Your Domain's DNS

Before enabling HTTPS, you need to point your domain to your server:

  1. Go to your domain registrar's website (like Namecheap, GoDaddy, etc.)
  2. Find the DNS settings or DNS management section
  3. Add or modify the following records:
    ​​​Type  | Name  | Value
    ​​​A     | @     | your_server_ip
    ​​​CNAME | www   | your_domain
    
  4. Save your changes

DNS changes can take anywhere from a few minutes to 48 hours to propagate. You can check the propagation by going on https://dnschecker.org/ and inserting your domain. it should point to your server ip

6. Set Up HTTPS with Certbot

Let's secure your site with a free SSL certificate from Let's Encrypt:

# Install Certbot and its Nginx plugin
sudo apt install certbot python3-certbot-nginx

# Obtain and install the certificate
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com

# When prompted:
# 1. Enter your email address
# 2. Agree to terms of service
# 3. Choose whether to redirect HTTP to HTTPS (recommended)

Certbot will automatically modify your Nginx configuration to use HTTPS. It also sets up automatic renewal of your certificates.

You can verify that the SSL certificate is set to auto-renew by checking the Certbot timer

sudo systemctl status certbot.timer

7. Configure Firewall

# Allow SSH, HTTP, and HTTPS
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'

# Enable firewall
sudo ufw enable

Updating Your World

When you need to update your world:

# Go to your world directory
cd ~/my-world

# Pull latest changes
git pull

# Install any new dependencies
npm install

# Restart the application
pm2 restart hyperfy

Monitoring and Troubleshooting

View Application Status

# Check status
pm2 status

# View logs
pm2 logs hyperfy

# Monitor CPU/Memory usage
pm2 monit

Check Nginx Status

# View Nginx status
sudo systemctl status nginx

# Check error logs
sudo tail -f /var/log/nginx/error.log

Common Issues

  1. Port 3000 already in use:
# Find what's using the port
sudo lsof -i :3000

# Kill the process
sudo kill -9 <PID>
  1. Node version issues:
# Verify Node version
node -v

# Switch version if needed
nvm use 22
  1. Permission issues:
# Fix ownership if needed
sudo chown -R YOUR_USERNAME:YOUR_USERNAME ~/my-world
  1. SSL Certificate issues:
# Check certificate status
sudo certbot certificates

# Renew certificates manually if needed
sudo certbot renew

# View Certbot logs
sudo tail -f /var/log/letsencrypt/letsencrypt.log

Remember to check your application logs (pm2 logs) for any specific error messages if you encounter issues. Most problems can be diagnosed through the logs!

JoelPlatoon

Hyperfy Meets IQ6900: A Scalable & Cost-Efficient Storage Solution

IQ6900's Code-In Technology provides a revolutionary approach to blockchain-based storage, enabling efficient, secure, and cost-effective solutions for managing 3D assets. Its unique ability to attach to any blockchain makes it a versatile choice for Hyperfy's infrastructure.

Key Features:

  • Storage Efficiency: Significantly reduces on-chain storage costs compared to traditional methods.
  • Cross-Chain Compatibility: Works seamlessly with Ethereum, Solana, and other blockchains.
  • Security: Supports encryption, chunking, and on-chain metadata for robust asset management.

GitHub Repository:

This plugin can be integrated into Hyperfy to store 3D assets more efficiently, leveraging IQ6900's innovative technology.

Setting up Code/Test Environment

1. Smart Contract Deployment

  • Ensure the code-in-for-eth smart contract is properly deployed on Ethereum with the correct ABI and address.
  • Verify that the storeMetadata and getMetadata functions in the contract work as intended.

2. Library Installation

Install all required libraries before running the code:

npm install web3 axios crypto fs

3. Environment Variables

Use environment variables (.env) to securely store sensitive data like private keys and API keys.

Example .env setup:

INFURA_PROJECT_ID=<your-infura-id>
PRIVATE_KEY=<your-private-key>
ENCRYPTION_KEY=<your-encryption-key>

4. Input Validation

Validate all user inputs and file paths to ensure no malformed data is processed.
Use libraries like path to sanitize file paths.

5. Chunk Size Optimization

Set an optimal chunk size based on IQ6900’s storage capacity and retrieval speeds.
Test the chunking process with different sizes (e.g., 10 KB, 100 KB).

6. Ethereum Gas Handling

Ensure sufficient ETH in the sender’s account to cover gas fees for transactions.
Use a gas price oracle to dynamically adjust gas prices.

7. Security Measures

Encryption

Use AES-256 for encrypting assets. Ensure the encryption key is long and stored securely.

Private Key Handling

Never hardcode private keys. Use environment variables or secure vaults (e.g., AWS Secrets Manager).

Access Control

Ensure storeMetadata and getMetadata functions on the Ethereum contract enforce proper access controls.

8. Error Handling

Add comprehensive try-catch blocks to handle errors gracefully and log issues for debugging.

9. Testing

Test the integration end-to-end with small assets before scaling up to larger files.
Simulate edge cases (e.g., network failure, insufficient funds).

Code Example

require('dotenv').config();
const Web3 = require('web3');
const axios = require('axios');
const crypto = require('crypto');
const fs = require('fs');

// Initialize Ethereum connection
const web3 = new Web3(`https://mainnet.infura.io/v3/${process.env.INFURA_PROJECT_ID}`);

// Ethereum smart contract configuration
const contractABI = [ /* ABI from code-in-for-eth */ ];
const contractAddress = '0xYourContractAddress';
const contract = new web3.eth.Contract(contractABI, contractAddress);

// Utility functions
function compressData(data) { return data; } // Placeholder
function decompressData(data) { return data; } // Placeholder
function encryptData(data, encryptionKey) {
  const cipher = crypto.createCipher('aes-256-cbc', encryptionKey);
  let encrypted = cipher.update(data, 'utf8', 'hex');
  encrypted += cipher.final('hex');
  return encrypted;
}
function decryptData(data, encryptionKey) {
  const decipher = crypto.createDecipher('aes-256-cbc', encryptionKey);
  let decrypted = decipher.update(data, 'hex', 'utf8');
  decrypted += decipher.final('utf8');
  return decrypted;
}
function splitIntoChunks(data, chunkSize) {
  const chunks = [];
  for (let i = 0; i < data.length; i += chunkSize) {
    chunks.push(data.slice(i, i + chunkSize));
  }
  return chunks;
}

// Main Plugin Class
class IQ6900HyperfyPlugin {
  constructor() {
    this.encryptionKey = process.env.ENCRYPTION_KEY;
    this.privateKey = process.env.PRIVATE_KEY;
  }
  async uploadAsset(filePath, userAddress) {
    // Implementation here
  }
  async retrieveAsset(metadataTxid) {
    // Implementation here
  }
}

// Example Usage
(async () => {
  const plugin = new IQ6900HyperfyPlugin();
  const userAddress = '0xYourEthereumAddress';
  const filePath = './path/to/3d-model.glb';

  const metadataTxid = await plugin.uploadAsset(filePath, userAddress);
  console.log('Metadata TxID:', metadataTxid);

  const assetData = await plugin.retrieveAsset(metadataTxid);
  console.log('Retrieved Asset Data:', assetData);
})();

Features

Secure Data Handling

  • Encryption: Protects 3D asset data during storage and retrieval.
  • Compression: Reduces file size to save costs and improve speed.

Chunked Uploads

  • Splits large assets into manageable 10 KB chunks for efficient storage and retrieval.

On-Chain Metadata

  • Stores chunk references (TxIDs) on Ethereum using the code-in-for-eth contract.

Seamless Retrieval

  • Retrieves all chunks from IQ6900, decrypts, decompresses, and reassembles the asset.

Ethereum Integration

  • Interacts with Hyperfy’s Ethereum environment for secure metadata management.

Cost Comparison: Ethereum vs. IQ6900 (Solana)

Asset Size Ethereum Cost (USD) IQ6900 Cost (USD) Savings (%)
500 KB $1,285.79 $0.3339 ~99.97%
2 MB $5,143.14 $1.3354 ~99.97%
10 MB $25,715.70 $6.677 ~99.97%

Insights on Cost Savings

🚀 Massive Cost Savings

  • IQ6900 reduces costs by ~99.97% compared to Ethereum storage.

💾 Efficient Storage Solution

  • Storing a 10 MB file:
    • IQ6900: $6.68
    • Ethereum: $25,715.70

🔗 Interoperable Architecture

  • Metadata on Ethereum: Store critical metadata for compatibility.
  • Assets on IQ6900: Offload large 3D assets (e.g., .glb files) for efficient, secure, and scalable storage.

📈 Scalable for Hyperfy

  • Enables Hyperfy to:
    • Store assets affordably.
    • Maintain fast access and high reliability.

b0gie

BuildMode System Documentation

Overview

BuildMode is a specialized camera control system for Hyperfy that enables free-flying
camera movement for building and world editing.
When activated, it temporarily disables player controls and provides smooth, unrestricted camera movement throughout the world.

Features

Camera Controls

  • Activation: Press B to toggle BuildMode
  • Movement:
    • W/A/S/D: Move camera forward/left/backward/right
    • Space: Move camera up
    • C: Move camera down
    • Mouse: Look around (smooth camera rotation)
    • Mouse Wheel: Adjust movement speed

Speed Control

  • Default movement speed: 10 units/second
  • Speed range: 1 to 50 units/second
  • Scroll up: Decrease speed
  • Scroll down: Increase speed
  • Speed adjustment factor: 1.2x per scroll tick

Player State Management

When BuildMode is activated:

  • Player model is hidden
  • Player animations are frozen
  • Player physics are locked
  • Player position is preserved
  • Player controls are disabled

Technical Implementation

Core Components

Camera System
// Camera initialization
this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)

// Smooth rotation handling
this.targetRotation = new THREE.Euler(0, 0, 0, 'YXZ')
this.currentRotation = new THREE.Euler(0, 0, 0, 'YXZ')
Movement System

The movement system uses quaternion-based directional movement for smooth camera control:

moveForward(distance) {
    const direction = new THREE.Vector3(0, 0, -1)
    direction.applyQuaternion(this.camera.quaternion)
    this.camera.position.addScaledVector(direction, distance)
}

State Management

Activation Process
  1. Store original camera and player states
  2. Disable player controls and visibility
  3. Enable free camera movement
  4. Lock player physics
  5. Enable pointer lock for smooth mouse control
toggleBuildMode() {
    if (this.active) {
        // Store states
        this.originalCameraPosition.copy(this.world.camera.position)
        this.originalCameraQuaternion.copy(this.world.camera.quaternion)
        
        // Disable player
        player.control.camera.unclaim()
        player.control.priority = -1
        player.visible = false
        
        // Enable build mode camera
        this.control.camera.claim()
    }
    // ...
}

Integration Guide

Adding BuildMode to Your World

  1. Register the BuildMode system with your world:
import { BuildMode } from './systems/BuildMode'

// In your world creation:
world.addSystem(BuildMode)
  1. Ensure proper control priorities are set:
// In ControlPriorities.js
export const ControlPriorities = {
    EDITOR: 100,    // BuildMode priority
    PLAYER: 0       // Normal player priority
}

Event Handling

BuildMode integrates with Hyperfy's event system:

  • Listens for key press events (B key for toggle)
  • Handles mouse movement for camera rotation
  • Processes scroll events for speed adjustment

Best Practices

  1. State Preservation

    • Always store original states before modification
    • Restore all states when deactivating
    • Handle edge cases (e.g., disconnection while in BuildMode)
  2. Performance

    • Use smooth interpolation for camera movement
    • Implement proper cleanup in the destroy method
    • Manage event listeners carefully
  3. User Experience

    • Provide smooth camera movement
    • Implement intuitive controls
    • Maintain consistent behavior across different scenarios

Common Issues and Solutions

  1. Player Visibility

    ​​​// Problem: Player still visible in BuildMode
    ​​​// Solution: Properly hide both avatar and base
    ​​​if (player.avatar) {
    ​​​    player.avatar.visible = false
    ​​​    player.avatar.mixer.stopAllAction()
    ​​​}
    ​​​if (player.base) {
    ​​​    player.base.visible = false
    ​​​}
    
  2. Camera Rotation

    ​​​// Problem: Camera flipping at extreme angles
    ​​​// Solution: Clamp rotation values
    ​​​this.targetRotation.x = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, this.targetRotation.x))
    

Future Enhancements

Potential improvements to consider:

  1. Grid system for precise placement
  2. Snap-to-surface functionality
  3. Object selection and manipulation
  4. Multiple camera modes (orbit, first-person, etc.)
  5. Customizable key bindings
  6. Save/restore camera positions

Contributing

When contributing to BuildMode:

  1. Maintain the existing control scheme
  2. Test edge cases thoroughly
  3. Document any changes or additions
  4. Ensure smooth state transitions
  5. Consider performance implications
  • Player Control System
  • Camera Control System
  • Physics System
  • Input Management System

First-Person Camera Implementation

Overview

This document outlines the implementation of a first-person camera system in Hyperfy, including camera positioning, rotation handling, and smooth transitions between first and third-person modes.

Key Features

  • Toggle between first and third-person modes using 'C' key
  • Camera positioned at eye level in first-person mode
  • Smooth transitions between camera modes
  • Proper collision handling and clipping prevention
  • Consistent interaction distances in both modes

Implementation Details

Camera Configuration

this.firstPersonCam = {
  position: new THREE.Vector3(),
  quaternion: new THREE.Quaternion(),
  rotation: new THREE.Euler(0, 0, 0, 'YXZ'),
  offset: new THREE.Vector3(0, 1.65, -0.7), // Eye level height and forward offset
  targetPosition: new THREE.Vector3(),
  targetQuaternion: new THREE.Quaternion(),
  lerpSpeed: 15
}

Key Components

  1. Camera Mode Toggle

    • Activated by pressing 'C'
    • Handles zoom and camera position transitions
    • Preserves VRM first-person settings
  2. Position Updates

    • Camera positioned at eye level (1.65 units up)
    • Forward offset (-0.7 units) to prevent clipping
    • Smooth position transitions using lerp
  3. Rotation Handling

    • Separate rotation limits for first-person (60 degrees) and third-person (90 degrees)
    • Base rotation only affected by left/right movement
    • Head bone rotation follows camera in first-person mode
  4. Interaction System

    • Consistent interaction distances in both modes
    • Rig position updated to match base position
    • Maintains proper 'E' action functionality

Code Examples

Camera Mode Toggle

if (code === 'KeyC') {
  this.isFirstPerson = !this.isFirstPerson
  if (this.isFirstPerson) {
    this.normalZoom = this.cam.zoom
    this.cam.zoom = 0
    this.control.camera.zoom = 0
  } else {
    this.cam.zoom = this.normalZoom
    this.control.camera.zoom = this.normalZoom
  }
}

Position Updates

// First-person camera position update
activeCam.targetPosition.copy(this.base.position)
activeCam.targetPosition.y += this.firstPersonCam.offset.y

const forward = new THREE.Vector3(0, 0, this.firstPersonCam.offset.z)
forward.applyQuaternion(this.base.quaternion)
activeCam.targetPosition.add(forward)

Rotation Updates

if (this.isFirstPerson) {
  activeCam.rotation.x = clamp(activeCam.rotation.x, -this.firstPersonRotationLimit, this.firstPersonRotationLimit)
  
  if (this.headBone) {
    this.headBone.rotation.set(0, 0, 0)
    this.headBone.rotation.x = activeCam.rotation.x
  }

  const baseRotation = new THREE.Euler(0, activeCam.rotation.y, 0, 'YXZ')
  this.base.quaternion.setFromEuler(baseRotation)
}

Best Practices

  1. Always update the rig position to match the base position for consistent interactions
  2. Use smooth transitions for camera movements to prevent jarring changes
  3. Implement proper collision checks to prevent camera clipping
  4. Maintain separate rotation limits for first and third-person modes
  5. Update head bone rotation to match camera rotation in first-person mode

Known Considerations

  • Camera position needs careful tuning to prevent seeing inside the character model
  • Rotation limits should be adjusted based on the desired level of head movement
  • Interaction distances should be consistent across both camera modes
  • VRM first-person settings should be respected for proper model rendering

Enhanced App Inspection System

This guide explains how to add position/rotation controls and a freeze toggle to Hyperfy's app inspection system.

Implementation Steps

1. Update InspectPane.js

First, modify src/client/components/InspectPane.js to add the new fields:

// Add state for transform values
function Fields({ app, blueprint }) {
  const [position, setPosition] = useState(app.root?.position || new THREE.Vector3())
  const [rotation, setRotation] = useState(app.root?.rotation || new THREE.Euler())
  const [frozen, setFrozen] = useState(app.frozen || false)
  
  // Add live updates
  useEffect(() => {
    const onUpdate = () => {
      if (app.root) {
        setPosition(app.root.position.clone())
        setRotation(app.root.rotation.clone())
      }
      setFrozen(app.frozen)
    }
    onUpdate()
    app.on('update', onUpdate)
    return () => app.off('update', onUpdate)
  }, [app])

  // Add transform fields to the UI
  const transformFields = [
    {
      type: 'section',
      key: 'transform',
      label: 'Transform',
    },
    {
      type: 'vector3',
      key: 'position',
      label: 'Position',
      value: position,
    },
    {
      type: 'euler',
      key: 'rotation',
      label: 'Rotation',
      value: rotation,
    },
    {
      type: 'switch',
      key: 'frozen',
      label: 'Freeze',
      value: frozen,
      options: [
        { value: true, label: 'Frozen' },
        { value: false, label: 'Unfrozen' }
      ]
    },
    ...fields
  ]
}

2. Add Transform Handlers

Add these handlers to the modify function in Fields:

const modify = (key, value) => {
  if (config[key] === value) return

  // Position updates
  if (key === 'position' && app.root) {
    app.root.position.copy(value)
    app.data.position = value.toArray()
    world.network.send('entityModified', {
      id: app.data.id,
      position: app.data.position
    })
    if (app.networkPos) {
      app.networkPos.pushArray(app.data.position)
    }
    setPosition(value.clone())
    return
  }

  // Rotation updates
  if (key === 'rotation' && app.root) {
    app.root.rotation.copy(value)
    const quaternion = new THREE.Quaternion().setFromEuler(value)
    app.data.quaternion = quaternion.toArray()
    world.network.send('entityModified', {
      id: app.data.id,
      quaternion: app.data.quaternion
    })
    if (app.networkQuat) {
      app.networkQuat.pushArray(app.data.quaternion)
    }
    setRotation(value.clone())
    return
  }

  // Freeze updates
  if (key === 'frozen') {
    app.frozen = value
    world.network.send('entityModified', {
      id: app.data.id,
      frozen: value
    })
    setFrozen(value)
    return
  }
}

3. Update App.js

Modify src/core/entities/App.js to handle frozen state:

export class App extends Entity {
  constructor(world, data, local) {
    super(world, data, local)
    // Add frozen state
    this.frozen = data.frozen || false
  }

  modify(data) {
    let rebuild
    // Handle frozen state
    if (data.hasOwnProperty('frozen')) {
      this.frozen = data.frozen
      this.data.frozen = data.frozen
      if (this.frozen && this.data.mover) {
        this.data.mover = null
        rebuild = true
      }
    }
    // Block position/rotation updates when frozen
    if (data.hasOwnProperty('position') && !this.frozen) {
      this.data.position = data.position
      this.networkPos.pushArray(data.position)
    }
    if (data.hasOwnProperty('quaternion') && !this.frozen) {
      this.data.quaternion = data.quaternion
      this.networkQuat.pushArray(data.quaternion)
    }
    if (rebuild) {
      this.build()
    }
  }

  move() {
    // Block movement when frozen
    if (this.frozen) return
    this.data.mover = this.world.network.id
    this.build()
    world.network.send('entityModified', { 
      id: this.data.id, 
      mover: this.data.mover 
    })
  }
}

Features

  • Direct numeric input for position/rotation
  • Freeze toggle to prevent movement
  • Real-time network sync
  • Works with existing movement system

Usage

  1. Right-click an app to open inspect pane
  2. Use numeric inputs for precise positioning
  3. Toggle freeze to lock in place
  4. Changes sync across network automatically

Testing

  1. Verify position/rotation inputs update in real-time
  2. Check freeze prevents all movement
  3. Confirm changes sync to other clients
  4. Test interaction with right-click move system

Door

A powerful HyperScript implementing a door for hyperfy

📋 Implementation Overview

Core State Variables

State Variable Purpose
isOpen Tracks whether the door is currently open or closed
isMoving Indicates if the door is in motion
currentPosition Current door position (0 = closed, 1 = open)
targetPosition Target door position (0 = closed, 1 = open)
openTimer Tracks duration before auto-closing

Configuration Options

{
  "type": "sliding",     // or "swinging"
  "direction": "outward", // or "inward"
  "slideDistance": 0.65,
  "maxRotation": 45
}

🎮 Door Components

it is important to match the names of the components in your blender scene to the names in the code.

  • doorFrame: Main door structure
  • doorL: Left door component
  • doorR: Right door component

⚙️ Configuration UI

Available Settings

  • Door Type
    • Sliding (default)
    • Swinging
  • Slide Distance
    • Default: 1.8 units
  • Max Rotation
    • Default: 45 degrees
  • Direction
    • Inward
    • Outward

🔄 Door Types

1. Sliding Doors FUTURE MODE

  • Horizontal movement along X-axis
  • Configurable slide distance
  • Smooth animation system

2. Swinging Doors SALOON MODE

  • Rotation-based movement
  • Adjustable maximum angle
  • Direction control

🎯 Interaction System

  1. Trigger action appears above door frame
  2. Players can toggle door state
  3. Action label updates dynamically
  4. Auto-close activates after 3 seconds
  5. Smooth animation transitions

🔧 Technical Notes

  • Uses delta time for consistent animations
  • Value clamping for stability
  • Automatic state management
  • Configurable through simple JSON

Pro Tip: All numerical values are automatically clamped to prevent unexpected behavior.

🚪 Setting Up Your Door in Blender

Proper setup in Blender is crucial for this script to function correctly. Follow these steps carefully:

0. 🔍 Figure out the door structure (important)

To achieve the door animation, you need to have a good hierarchy and naming of your rigid body components. In the example object the door is comprised of 4 parts:

  • Empty: The parent object of the door.
    • FrameRigidBody: The main structure of the door.
      • FrameCollider: The collider for the door frame
      • FrameMesh: The mesh for the door frame
    • LeftDoorRigidBody: The left panel of the door.
      • LeftDoorCollider: The collider for the left door.
      • LeftDoorMesh: The mesh for the left door.
    • RightDoorRigidBody: The right panel of the door.
      • RightDoorCollider: The collider for the right door.
      • RightDoorMesh: The mesh for the right door.


1. 🏗️ Door Frame (Rigid Body / static)
  • Name: Frame
  • Description: This is the main structure of your door. It should be a static object that serves as the parent for all door components.
  • Positioning: Place it where you want your door to appear in the environment.

2. ⬅️ Create the Left Door (Rigid Body / kinematic)
  • Name: LeftDoor
  • Description: This is the left panel of your door. It should be a child of the Empty object.

3. ➡️ Create the Right Door (Rigid Body / kinematic)
  • Name: RightDoor
  • Description: This is the right panel of your door. It should also be a child of the Empty object.

4. 💾 Export Your Meshes
  • Export your entire setup (Empty, Frame, LeftDoor, RightDoor) as a single .glb file for use in hyperfy.
  • Ensure all names match exactly as specified (Empty, Frame, LeftDoor, RightDoor).

Implementing Double Jump in Hyperfy

This guide explains how to add a double jump feature with a flip animation to your Hyperfy world.

1. Create the Double Jump System

Create a new file src/core/systems/DoubleJump.js:

import { System } from './System'
import * as THREE from '../extras/three'
import { Emotes, emotes } from '../extras/playerEmotes'

export class DoubleJump extends System {
  constructor(world) {
    super(world)
    this.lastJumpTime = 0
    this.DOUBLE_JUMP_FORCE = 9.75  // 1.5x the base jump force
    this.isDoubleJumping = false
  }

  async init({ loadPhysX }) {
    // Preload the flip animation
    await this.world.loader.load('emote', emotes[Emotes.DOUBLE_JUMP])
  }

  start() {
    // Bind to space key with priority 0
    this.control = this.world.controls.bind({
      priority: 0,
      onPress: code => this.handleKeyPress(code)
    })
  }

  getLocalPlayer() {
    return Array.from(this.world.entities.items.values())
      .find(entity => entity.isPlayer && entity.constructor.name === 'PlayerLocal')
  }

  update() {
    const localPlayer = this.getLocalPlayer()
    if (!localPlayer) return

    // Handle animation timing
    if (this.isDoubleJumping) {
      const timeSinceLastJump = performance.now() - this.lastJumpTime
      if (timeSinceLastJump > 400) {
        // Switch back to float after flip completes
        localPlayer.emote = Emotes.FLOAT
      }
      if (timeSinceLastJump > 800) {
        this.isDoubleJumping = false
      }
    }

    // Reset on landing
    if (localPlayer.grounded) {
      this.isDoubleJumping = false
      this.lastJumpTime = 0
    }
  }

  handleKeyPress(code) {
    if (code !== 'Space') return false
    
    const localPlayer = this.getLocalPlayer()
    if (!localPlayer) return false

    // Only double jump if:
    // 1. In the air (jumping/falling)
    // 2. Not already double jumping
    // 3. Not grounded
    if (!localPlayer.grounded && !this.isDoubleJumping && 
        (localPlayer.jumping || localPlayer.falling)) {
      
      // Apply upward force
      const currentVel = localPlayer.capsule.getLinearVelocity()
      v1.copy(currentVel)
      v1.y = this.DOUBLE_JUMP_FORCE
      localPlayer.capsule.setLinearVelocity(v1.toPxVec3())

      // Start flip animation
      this.isDoubleJumping = true
      this.lastJumpTime = performance.now()
      localPlayer.emote = Emotes.DOUBLE_JUMP

      // Sync animation with other players
      this.world.network.send('entityModified', {
        id: localPlayer.data.id,
        p: localPlayer.base.position.toArray(),
        q: localPlayer.base.quaternion.toArray(),
        e: Emotes.DOUBLE_JUMP,
      })

      return true
    }
    return false
  }
}

2. Add the Double Jump Emote

Add the following to src/core/extras/playerEmotes.js:

export const Emotes = {
  IDLE: 0,
  WALK: 1,
  RUN: 2,
  FLOAT: 3,
  DOUBLE_JUMP: 4,  // Add this line
  // ... other emotes
}

export const emotes = {
  0: 'asset://emote-idle.glb',
  1: 'asset://emote-walk.glb',
  2: 'asset://emote-run.glb',
  3: 'asset://emote-float.glb',
  4: 'asset://emote-flip.glb',  // Add this line
  // ... other emotes
}

3. Register the System

Add this to your world creation (usually in createClientWorld.js):

import { DoubleJump } from './systems/DoubleJump'

// In your world creation:
world.register(DoubleJump)

Features

The double jump system provides:

  • Double jump ability by pressing space while in the air
  • Front flip animation during the double jump
  • Network synchronization for multiplayer
  • Automatic transition back to floating animation
  • State reset upon landing

Technical Details

  • Double jump force is 1.5x the normal jump force (9.75)
  • Flip animation plays for 400ms before transitioning to float
  • Double jump state is tracked for 800ms to prevent additional jumps
  • Uses the existing player physics and animation systems
  • Integrates with the network layer for multiplayer support

Dodge System Documentation

Overview

The Dodge system is an advanced movement mechanic that allows players to perform both ground and aerial dodges. It seamlessly integrates with the existing movement systems, including double jumps, and provides fluid, momentum-based movement options for enhanced player mobility.

Features

Ground Dodges

  • Forward Roll: Performed while moving

    • Maintains directional momentum
    • Full dodge force (15 units)
    • 30% momentum preservation
    • Uses emote-roll.glb animation
  • Backstep: Performed while stationary

    • Moves backward relative to player orientation
    • Full dodge force (15 units)
    • 30% momentum preservation
    • Uses emote-backstep.glb animation

Air Dodges

The air dodge system features dynamic force adjustments based on the player's jump state:

  1. After First Jump

    • 80% of base dodge force
    • 50% momentum preservation
    • 0.3 units of upward force
    • Directional control based on movement input
  2. After Double Jump

    • 70% of base dodge force
    • 60% momentum preservation
    • 0.2 units of upward force
    • Enhanced momentum preservation for better control
  3. During Fall

    • 75% of base dodge force
    • 50% momentum preservation
    • 0.3 units of upward force
    • Direction based on movement or forward orientation

Technical Details

System Properties

dodgeForce = 15        // Base force for all dodges
dodgeDuration = 0.7    // Duration of dodge animation in seconds
dodgeCooldown = 2000   // Cooldown between dodges in milliseconds

Movement Mechanics

Force Calculation

finalForce = baseDodgeForce * stateMultiplier

Where stateMultiplier is:

  • Ground: 1.0
  • Air (first jump): 0.8
  • Air (double jump): 0.7
  • Air (falling): 0.75

Momentum Preservation

momentumFactor = {
    ground: 0.3,
    air: 0.5,
    doubleJump: 0.6
}

Upward Force (Air Dodges)

upwardForce = {
    normal: 0.3,
    afterDoubleJump: 0.2
}

Integration with Other Systems

Double Jump Integration

  • Cannot dodge during double jump animation
  • Reduced forces after double jump
  • Enhanced momentum preservation for better control
  • Reduced upward force to prevent excessive height gain

Physics Integration

  • Uses PhysX capsule for collision detection
  • Maintains momentum through velocity preservation
  • Applies forces relative to player orientation
  • Handles ground and air states separately

Animation System

  • Seamlessly transitions between dodge and movement animations
  • Respects animation priority with double jump system
  • Returns to appropriate idle/movement state after dodge
  • Network synchronized for multiplayer consistency

Usage Examples

Basic Usage

// Ground dodge while running forward
W + SHIFT = Forward roll

// Ground dodge while stationary
SHIFT = Backstep

// Air dodge while moving
JUMP + (direction) + SHIFT = Directional air dodge

// Air dodge after double jump
JUMP + JUMP + SHIFT = Reduced-force air dodge

Advanced Techniques

  1. Jump-Cancel Dodge

    • Jump during a ground dodge
    • Maintains dodge momentum into the jump
    • Can chain into double jump or air dodge
  2. Momentum-Preserved Air Dodge

    • Use movement momentum before air dodge
    • Higher preservation factor in air
    • Useful for covering large distances
  3. Double Jump to Air Dodge

    • Double jump for height
    • Air dodge for horizontal distance
    • Uses reduced force but higher momentum preservation

Network Synchronization

The system sends the following data for multiplayer synchronization:

{
    id: player.id,
    p: position.toArray(),
    q: quaternion.toArray(),
    e: emoteType
}

Performance Considerations

  • Efficient use of vector pooling (v1) for calculations
  • Minimal garbage collection impact
  • Optimized physics calculations
  • Smart animation state management

Debug Information

The system includes extensive debug logging:

  • Dodge state and progress
  • Force calculations
  • Movement state
  • Animation transitions
  • Physics velocity updates

Future Considerations

Potential areas for enhancement:

  1. Additional dodge types (side dodges, recovery dodges)
  2. Customizable dodge parameters per player
  3. Enhanced VFX/SFX integration
  4. Advanced combo system integration
  5. Dodge canceling mechanics

Dependencies

  • Three.js for vector math and quaternions
  • PhysX for physics simulation
  • Custom animation system for emotes
  • Network synchronization system
  • Player control system

Files

  • src/core/systems/Dodge.js - Main system implementation
  • src/core/extras/playerEmotes.js - Emote definitions
  • assets/emote-roll.glb - Roll animation
  • assets/emote-backstep.glb - Backstep animation

emote-roll.glb can be found in the hyperfy-ref
emote-backstep.glb can be found in the hyperfy-ref

Sky Controller Documentation (concept)

Overview

The Sky Controller is a Hyperfy script that allows dynamic control of the sky and environmental lighting in your world.
It provides a user-friendly interface to switch between different times of day and customize each state with specific sky textures and HDR environment maps.

Features

  • Four different time-of-day states: Day (☀️), Dusk (🌅), Night (🌙), and Aurora (🌌)
  • Custom sky texture support for each state
  • Custom HDR lighting support for each state
  • Real-time switching between states
  • Default engine sky fallback for day mode

Technical Implementation

Configuration UI

app.configure(() => {
    return [
        // Section Header
        {
            type: 'section',
            key: 'title',
            label: 'Sky',
        },
        // Time of Day Switcher
        {
            type: 'switch',
            key: 'switch',
            label: 'TOD',
            value: 1,
            options: [
                { value: 1, label: '☀️' },
                { value: 2, label: '🌅' },
                { value: 3, label: '🌙' },
                { value: 4, label: '🌌' }
            ]
        },
        // File inputs for each state...
    ]
})

The configuration UI is built using Hyperfy's configuration system, which automatically creates an inspector panel with:

  • A section header labeled "Sky"
  • A switch control for Time of Day (TOD)
  • File upload fields for each sky texture and HDR map

Sky States

Each state (except Day) requires two files:

  1. Sky Texture: A background image for the sky dome
    • File type: JPG or PNG
    • Purpose: Visual appearance of the sky
  2. HDR Map: A high dynamic range environment map
    • File type: HDR
    • Purpose: Environmental lighting and reflections

State Configurations:

  1. Day Mode (☀️)

    • Uses engine defaults
    • No files required
    • Clean, default lighting
  2. Dusk Mode (🌅)

    • sky1: Dusk sky texture
    • hdr1: Dusk HDR lighting
  3. Night Mode (🌙)

    • sky2: Night sky texture
    • hdr2: Night HDR lighting
  4. Aurora Mode (🌌)

    • sky3: Aurora sky texture
    • hdr3: Aurora HDR lighting

Core Functionality

Sky Creation

const sky = app.create('sky')
app.add(sky)

Creates and adds a sky entity to the world.

Update Function

function updateSky() {
    const mode = app.config.switch
    console.log('Current mode:', mode)

    if (mode === 4) {
        // Aurora settings
    } else if (mode === 3) {
        // Night settings
    } else if (mode === 2) {
        // Dusk settings
    } else {
        // Day settings
    }
}

The updateSky() function:

  • Reads the current mode from configuration
  • Sets appropriate sky texture (sky.bg) and HDR map (sky.hdr)
  • Falls back to engine defaults for day mode

Event Handling

// Initial setup
updateSky()

// Listen for configuration changes
app.on('config', () => {
    updateSky()
})

// Update every frame to ensure sync
app.on('update', dt => {
    updateSky()
})

The script maintains synchronization through:

  1. Initial state setup on load
  2. Configuration change listener for UI interactions
  3. Frame update listener for continuous synchronization

Usage Instructions

  1. Basic Setup:

    • Add the script to your world
    • Scale appropriately using app.scale.set(2.5, 2.5, 1.5)
  2. Customizing States:

    • Click the file upload fields in the inspector
    • Select appropriate sky textures (JPG/PNG) and HDR maps
    • Files are automatically uploaded and cached
  3. Switching States:

    • Use the TOD switch in the inspector
    • Changes take effect immediately
    • Switch between Day, Dusk, Night, and Aurora modes
  4. Best Practices:

    • Use appropriately sized textures for performance
    • Ensure HDR maps match their corresponding sky textures
    • Test transitions between states for smooth changes

Technical Notes

  • The script uses optional chaining (?.) for safe property access
  • Configuration updates are handled through Hyperfy's event system
  • Frame updates ensure consistent state synchronization
  • File uploads are managed by Hyperfy's asset system

ashxn

UV Scroll on Contact

const body = app.get('Body')
const mesh = app.get('Mesh')

let active = false

body.onContactStart = e => {
 console.log('start', e.player)
 if (e.player) {
   active = true
 }
}

body.onContactEnd = e => {
 console.log('end', e.player) 
 if (e.player) {
   active = false
 }
}

app.on('update', delta => {
 if (!active) return
 mesh.material.textureX += 1 * delta
 mesh.material.textureY += 1 * delta
})

devilsadvocate.sol

Getting Started With Hyperfy V2

Begin by dragging a glb into your hyperfy world. Remember to give yourself admin priviledges with /admin <admin-key>. Right click on the model to inspect it and click script to open the in world scripting UI.

const appId = app.id
console.log('appId: ', appId)

When you drag a model into a hyperfy world an app entity is automatically created with an appId matching the root model name. If you don't know this name, you can return it with app.id.

const model = app.get(app.id)
console.log('model',model)

World Hierarchy

Worlds
 └─ Entities
     ├─ Players
     └─ Apps
         └─ rootNodes (root model)
             └─ subNodes (sub-model e.g., collider)

A node or its subnodes can be accessed using app.get('node-id'). This is useful for assigning colliders, rigidbodies, or actions to specific subnodes of a root model.

const rb = app.create('rigidbody')
rb.type = 'dynamic'

Rigidbody Types

To allow your model to interact with physics it needs a rigidbody. There are three types: static(default), dynamic, or kinematic. Use the latter two options if you plan to move the object through code, with kinematic being most performant(clarity need on why this is). Other rigidbody attributes can be found in the github docs.

const collider = app.create('collider')
rb.add(collider)
app.add(rb)

Once we've created our rigidbody we can assign a collider to it. After creating the rb and adding the collider to it, we can add the rb to the app runtime making our object collidable. Colliders can also be returned from a model object using app.get('collider-name') if they have been included in the glb model as subnodes.

const action = app.create('action')
action.label = 'Water Plant'
action.position.set(0, 2.2, 0)
action.duration = 0.3

Actions

Another app attribute are actions which can be used to make objects interactable in the world. Actions include attributes like a label, position, and duration to control their appearance and function.

action.onTrigger= () => {
 console.log(appId)
}
app.add(action)

Additionally actions can trigger functions when they are interacted with by a player. Actions must be added directly to the app with app.add(action) or to their nodes/subnodes using app.get('node-to-add-action').add(action)

const appPosition = app.position
console.log('app', app)
console.log('action', action)
console.log('rb', rb)
console.log('collider', collider)
Select a repo