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
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:
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.
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.
Looking at the RigidBody.js code, there are three types defined:
const types = ['static', 'kinematic', 'dynamic']
Here's what each does:
static
- For immovable objects like walls, floors, or fixed furniture. The code shows these create a createRigidStatic
PhysX actor which:
dynamic
- For objects that should move realistically with physics. Creates a createRigidDynamic
PhysX actor that:
kinematic
- For objects that move but aren't affected by physics. Also creates a createRigidDynamic
actor but sets the KINEMATIC
flag:
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.
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!
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.
# Connect to your VPS as root
ssh root@your_server_ip
# 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
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:
If something goes wrong, you can restore the backup:
sudo cp /etc/ssh/sshd_config.bak /etc/ssh/sshd_config
sudo systemctl restart sshd
# 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
# Install PM2 globally
npm install -g pm2
# Install Nginx
sudo apt install -y nginx
# 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
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
# Start the application
pm2 start npm --name "hyperfy" --interpreter bash -- start
# Set PM2 to start on system boot
pm2 startup
pm2 save
# 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
Before enabling HTTPS, you need to point your domain to your server:
Type | Name | Value
A | @ | your_server_ip
CNAME | www | your_domain
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
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
# Allow SSH, HTTP, and HTTPS
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
# Enable firewall
sudo ufw enable
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
# Check status
pm2 status
# View logs
pm2 logs hyperfy
# Monitor CPU/Memory usage
pm2 monit
# View Nginx status
sudo systemctl status nginx
# Check error logs
sudo tail -f /var/log/nginx/error.log
# Find what's using the port
sudo lsof -i :3000
# Kill the process
sudo kill -9 <PID>
# Verify Node version
node -v
# Switch version if needed
nvm use 22
# Fix ownership if needed
sudo chown -R YOUR_USERNAME:YOUR_USERNAME ~/my-world
# 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!
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.
This plugin can be integrated into Hyperfy to store 3D assets more efficiently, leveraging IQ6900's innovative technology.
code-in-for-eth
smart contract is properly deployed on Ethereum with the correct ABI and address.storeMetadata
and getMetadata
functions in the contract work as intended.Install all required libraries before running the code:
npm install web3 axios crypto fs
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>
Validate all user inputs and file paths to ensure no malformed data is processed.
Use libraries like path to sanitize file paths.
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).
Ensure sufficient ETH in the sender’s account to cover gas fees for transactions.
Use a gas price oracle to dynamically adjust gas prices.
Use AES-256 for encrypting assets. Ensure the encryption key is long and stored securely.
Never hardcode private keys. Use environment variables or secure vaults (e.g., AWS Secrets Manager).
Ensure storeMetadata and getMetadata functions on the Ethereum contract enforce proper access controls.
Add comprehensive try-catch blocks to handle errors gracefully and log issues for debugging.
Test the integration end-to-end with small assets before scaling up to larger files.
Simulate edge cases (e.g., network failure, insufficient funds).
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);
})();
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% |
.glb
files) for efficient, secure, and scalable storage.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.
B
to toggle BuildModeW/A/S/D
: Move camera forward/left/backward/rightSpace
: Move camera upC
: Move camera downMouse
: Look around (smooth camera rotation)Mouse Wheel
: Adjust movement speedWhen BuildMode is activated:
// 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')
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)
}
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()
}
// ...
}
import { BuildMode } from './systems/BuildMode'
// In your world creation:
world.addSystem(BuildMode)
// In ControlPriorities.js
export const ControlPriorities = {
EDITOR: 100, // BuildMode priority
PLAYER: 0 // Normal player priority
}
BuildMode integrates with Hyperfy's event system:
State Preservation
Performance
User Experience
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
}
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))
Potential improvements to consider:
When contributing to BuildMode:
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.
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
}
Camera Mode Toggle
Position Updates
Rotation Handling
Interaction System
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
}
}
// 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)
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)
}
This guide explains how to add position/rotation controls and a freeze toggle to Hyperfy's app inspection system.
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
]
}
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
}
}
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
})
}
}
A powerful HyperScript implementing a door for hyperfy
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 |
{
"type": "sliding", // or "swinging"
"direction": "outward", // or "inward"
"slideDistance": 0.65,
"maxRotation": 45
}
it is important to match the names of the components in your blender scene to the names in the code.
doorFrame
: Main door structuredoorL
: Left door componentdoorR
: Right door componentPro Tip: All numerical values are automatically clamped to prevent unexpected behavior.
Proper setup in Blender is crucial for this script to function correctly. Follow these steps carefully:
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:
Frame
LeftDoor
Empty
object.RightDoor
Empty
object.Empty
, Frame
, LeftDoor
, RightDoor
).This guide explains how to add a double jump feature with a flip animation to your Hyperfy world.
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
}
}
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
}
Add this to your world creation (usually in createClientWorld.js
):
import { DoubleJump } from './systems/DoubleJump'
// In your world creation:
world.register(DoubleJump)
The double jump system provides:
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.
Forward Roll: Performed while moving
emote-roll.glb
animationBackstep: Performed while stationary
emote-backstep.glb
animationThe air dodge system features dynamic force adjustments based on the player's jump state:
After First Jump
After Double Jump
During Fall
dodgeForce = 15 // Base force for all dodges
dodgeDuration = 0.7 // Duration of dodge animation in seconds
dodgeCooldown = 2000 // Cooldown between dodges in milliseconds
finalForce = baseDodgeForce * stateMultiplier
Where stateMultiplier
is:
momentumFactor = {
ground: 0.3,
air: 0.5,
doubleJump: 0.6
}
upwardForce = {
normal: 0.3,
afterDoubleJump: 0.2
}
// 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
Jump-Cancel Dodge
Momentum-Preserved Air Dodge
Double Jump to Air Dodge
The system sends the following data for multiplayer synchronization:
{
id: player.id,
p: position.toArray(),
q: quaternion.toArray(),
e: emoteType
}
The system includes extensive debug logging:
Potential areas for enhancement:
src/core/systems/Dodge.js
- Main system implementationsrc/core/extras/playerEmotes.js
- Emote definitionsassets/emote-roll.glb
- Roll animationassets/emote-backstep.glb
- Backstep animationemote-roll.glb can be found in the hyperfy-ref
emote-backstep.glb can be found in the hyperfy-ref
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.
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:
Each state (except Day) requires two files:
Day Mode (☀️)
Dusk Mode (🌅)
sky1
: Dusk sky texturehdr1
: Dusk HDR lightingNight Mode (🌙)
sky2
: Night sky texturehdr2
: Night HDR lightingAurora Mode (🌌)
sky3
: Aurora sky texturehdr3
: Aurora HDR lightingconst sky = app.create('sky')
app.add(sky)
Creates and adds a sky entity to the world.
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:
sky.bg
) and HDR map (sky.hdr
)// 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:
Basic Setup:
app.scale.set(2.5, 2.5, 1.5)
Customizing States:
Switching States:
Best Practices:
?.
) for safe property accessconst 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
})
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)
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'
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
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)