Overview
Entities are the core game objects in Hyperscape. Every player, mob, item, and resource is an Entity with 3D representation, networking, physics, and component-based functionality.
Entity Hierarchy
// From packages/shared/src/entities/Entity.ts (lines 26-38)
// Entity (base class)
// ├── InteractableEntity (can be interacted with)
// │ ├── ResourceEntity (trees, rocks, fishing spots)
// │ ├── ItemEntity (ground items)
// │ └── NPCEntity (dialogue, shops)
// ├── CombatantEntity (can fight)
// │ ├── PlayerEntity (base player)
// │ │ ├── PlayerLocal (client-side local player)
// │ │ └── PlayerRemote (client-side remote players)
// │ └── MobEntity (enemies)
// └── HeadstoneEntity (player death markers)
Key Features
From packages/shared/src/entities/Entity.ts:
// Key Features:
// - **3D Representation**: Three.js Object3D with mesh, position, rotation, scale
// - **Component System**: Modular components for combat, stats, interaction, etc.
// - **Physics**: Optional PhysX rigid body integration
// - **Networking**: State synchronization across clients
// - **Lifecycle**: spawn(), update(), fixedUpdate(), destroy()
// - **Events**: Local and world event system for inter-entity communication
// - **Serialization**: Network serialization for state sync
Entity Class
// From packages/shared/src/entities/Entity.ts (lines 111-145)
export class Entity implements IEntity {
world: World;
data: EntityData;
id: string;
name: string;
type: string;
node: THREE.Object3D<THREE.Object3DEventMap>;
components: Map<string, Component>;
velocity: Vector3;
isPlayer: boolean;
active: boolean = true;
destroyed: boolean = false;
// Physics body reference
private rigidBody?: PhysXRigidDynamic;
// UI elements
protected nameSprite: THREE.Sprite | null = null;
protected healthSprite: THREE.Sprite | null = null;
// Network state
public networkDirty = false;
public networkVersion = 0;
}
Lifecycle Methods
// 1. Constructor: Creates entity with initial data/config
constructor(world: World, dataOrConfig: EntityData | EntityConfig, local?: boolean)
// 2. init(): Async initialization (load models, set up interactions)
async init(): Promise<void>
// 3. update(delta): Called every frame for visual updates
update(delta: number): void
// 4. fixedUpdate(delta): Called at fixed timestep (30 FPS) for physics
fixedUpdate(delta: number): void
// 5. destroy(): Cleanup when entity is removed
destroy(local?: boolean): void
Component Management
// Add a component
entity.addComponent("health", {
current: 100,
max: 100,
regenerationRate: 1.0,
isDead: false,
});
// Get a component
const health = entity.getComponent<HealthComponent>("health");
if (health?.data) {
console.log(`Health: ${health.data.current}/${health.data.max}`);
}
// Check if entity has component
if (entity.hasComponent("combat")) {
// Entity can fight
}
// Remove a component
entity.removeComponent("movement");
Default Components
Every entity automatically gets these components:
// From packages/shared/src/entities/Entity.ts (lines 752-794)
protected initializeRPGComponents(): void {
this.addHealthComponent(); // Health, regen, isDead
this.addCombatComponent(); // Combat state, target, cooldown
this.addVisualComponent(); // Mesh, name sprite, health sprite
}
Network Synchronization
// Mark entity as needing network sync
entity.markNetworkDirty();
// Get data for network transmission
const networkData = entity.getNetworkData();
// Returns: { id, type, name, position, rotation, scale, visible, networkVersion, properties }
// Serialize for network packet
const serialized = entity.serialize();
// Returns: EntityData with current position/quaternion from node
Health and Damage
// From Entity base class
entity.setHealth(newHealth); // Set health (clamped to max, updates UI)
entity.damage(amount, source); // Apply damage, emit event
entity.heal(amount); // Heal entity
entity.isAlive(); // Check if health > 0
entity.isDead(); // Check if health <= 0
entity.getHealth(); // Get current health
entity.getMaxHealth(); // Get max health
// Get/set position
const pos = entity.getPosition(); // Returns Position3D { x, y, z }
entity.setPosition({ x: 10, y: 5, z: 10 });
entity.setPosition(10, 5, 10); // Overload with x, y, z
// Distance calculations
const distance = entity.getDistanceTo(otherPosition);
const inRange = entity.isPlayerInRange(playerPosition);
PlayerEntity
The player entity extends CombatantEntity with player-specific features:
// From packages/shared/src/entities/player/PlayerEntity.ts (lines 105-123)
// Default skills on construction:
playerData.skills = {
attack: { level: 1, xp: 0 },
strength: { level: 1, xp: 0 },
defense: { level: 1, xp: 0 },
constitution: { level: 10, xp: 1154 }, // Starts at level 10
ranged: { level: 1, xp: 0 },
prayer: { level: 1, xp: 0 },
woodcutting: { level: 1, xp: 0 },
fishing: { level: 1, xp: 0 },
mining: { level: 1, xp: 0 },
firemaking: { level: 1, xp: 0 },
cooking: { level: 1, xp: 0 },
smithing: { level: 1, xp: 0 },
agility: { level: 1, xp: 0 },
};
Player Components
// Player-specific components added in constructor:
this.addComponent("stamina", { current: 100, max: 100, drainRate: 20.0, regenRate: 15.0 });
this.addComponent("movement", { isMoving: false, isRunning: false, speed: 3.0, runSpeed: 6.0 });
this.addComponent("inventory", { items: [], capacity: 28, coins: 0 });
this.addComponent("equipment", { weapon: null, shield: null, helmet: null, body: null, legs: null, arrows: null });
this.addComponent("stats", { attack, strength, defense, constitution, ranged, prayer, woodcutting, fishing, mining, firemaking, cooking, smithing, agility });
The stamina system is affected by both weight (inventory load) and agility level. Heavier loads increase stamina drain, while higher agility increases regeneration.
MobEntity
Mob entities represent hostile NPCs with AI behavior:
// MobEntity AI States (from types/entities.ts)
export enum MobAIState {
IDLE = "idle",
WANDER = "wander",
CHASE = "chase",
ATTACK = "attack",
RETURN = "return",
DEAD = "dead",
}
Events
Entities can emit and listen to events:
// Local entity events
entity.on("health-changed", (data) => { /* ... */ });
entity.emit("player-died", { playerId: this.playerId, position: this.getPosition() });
entity.off("health-changed", handler);
// World events (propagated globally)
this.world.emit(EventType.ENTITY_DAMAGED, { entityId: this.id, damage: amount });
Best Practices
- Use components for data - Don’t add properties directly to entities
- Check component existence - Always null-check
getComponent() results
- Mark network dirty - Call
markNetworkDirty() after state changes
- Override lifecycle methods - Implement
createMesh(), onInit(), onInteract() in subclasses
- Clean up properly - Override
destroy() and call super.destroy()