Defining Bundle Entities
Defining Bundle Entities
Entity definitions describe reusable things that can exist on the map: characters, cursors, temporary effects, NPCs, projectiles, pickups, and anything else that should be spawned as an entity.
Entity definitions live in the common entities registry. Both the server and client can read the same definition, but each side only creates the components it understands.
Where entities live
Put entity definitions under common/data/<namespace>/, where <namespace> becomes the namespace part of each entity identifier.
You can define multiple entities in one registry file:
my-bundle/
bundle.json
common/
data/
myworld/
entities.json
Or split each entity into its own JSON file:
my-bundle/
common/
data/
myworld/
entities/
characters/
guard.json
The merged entities.json form stores entries by local name. This defines myworld:characters/guard:
{
"entries": {
"characters/guard": {
"components": {
"myworld:visual": {
"type": "visual",
"visual": "myworld:characters/guard"
}
}
}
}
}
The split-file form uses the file path as the local name. For example, common/data/myworld/entities/characters/guard.json also defines myworld:characters/guard.
Entity identifiers
Entity identifiers use the same namespace:name format as other Selene registries:
myworld:characters/guard
myworld:effects/sparkle
illarion:race/human_male
The namespace comes from the directory below common/data/. The name comes from either the key inside entities.json or the relative path of a file below entities/.
Use stable names once scripts, saves, spawn tables, or other bundle data refer to an entity. Renaming an entity definition means updating every reference.
Entity shape
An entity definition has three top-level fields:
{
"components": {},
"tags": [],
"metadata": {}
}
All fields are optional in the engine:
componentsdeclares client and server behavior for entities created from this definition.tagsare strings used for filtering and classification.metadatastores custom data and can be searched throughselene.registries.
In practice, most useful entities have at least one component.
Components
components is an object keyed by component identifier. The key is the component instance name, not a registered component type. Use stable, namespaced keys so scripts can refer to components without colliding with other bundles.
{
"components": {
"myworld:visual": {
"type": "visual",
"visual": "myworld:characters/guard"
},
"myworld:client_script": {
"type": "client_script",
"script": "my-bundle.client.lua.entities.guard"
},
"myworld:server_script": {
"type": "server_script",
"script": "my-bundle.server.lua.entities.guard"
}
}
}
The component type controls which configuration is decoded.
Visual components
Use visual to attach a client visual from the visuals registry:
{
"type": "visual",
"visual": "myworld:characters/guard"
}
The client resolves the visual identifier, creates a reloadable visual instance, and renders it with the entity. The server ignores visual components.
Visual components can also include a position object:
{
"type": "visual",
"visual": "myworld:characters/nameplate",
"position": {
"origin": "top",
"offsetX": 0,
"offsetY": 4
}
}
Supported origins are:
none: render at the entity's normal screen position plus offsets.top: render above the bounds of visuals that were rendered earlier for the same entity.
Component order follows the JSON object order used by the loaded definition. Put base visuals before overlays that use origin: "top".
Visual overrides
Visual components may pass per-entity data through overrides:
{
"type": "visual",
"visual": "myworld:labels/name",
"overrides": {
"text": "Guard"
}
}
The overrides are available to the visual creation context. Use them for data that changes how a visual instance is created, such as text content or variant-specific values.
Client script components
Use client_script for client-only entity behavior:
{
"type": "client_script",
"script": "my-bundle.client.lua.entities.sparkle"
}
The client loads the Lua module and calls functions if they exist:
local M = {}
function M.Initialize(entity, data)
data.started = true
end
function M.TickEntity(entity, data, delta)
-- Return normally to keep the component enabled.
end
return M
Initialize(entity, data) runs before the first tick. TickEntity(entity, data, delta) runs each client update. If either function errors, Selene disables that script component for the entity.
Server script components
Use server_script for server-side entity behavior:
{
"type": "server_script",
"script": "my-bundle.server.lua.entities.wanderer"
}
Server entity scripts use the same optional functions:
local M = {}
function M.Initialize(entity, data)
data.time = 0
end
function M.TickEntity(entity, data, delta)
data.time = data.time + delta
end
return M
The server creates only server_script components. It ignores visual and client script components. The client creates visual and client_script components, and ignores server_script.
Tags and metadata
Use tags for broad categories:
{
"tags": [
"myworld:character",
"myworld:npc"
]
}
Client Lua can filter entities at a coordinate by tag:
local Entities = require("selene.entities")
local npcs = Entities.findEntitiesAt(100, 100, 0, {
tag = "myworld:npc"
})
Use metadata for structured lookup data:
{
"metadata": {
"spawnGroup": "town",
"level": 3
}
}
Registry metadata can be queried from Lua:
local Registries = require("selene.registries")
local guard = Registries.findByMetadata("entities", "spawnGroup", "town")
"entities" resolves to the built-in selene:entities registry. Entity identifiers themselves should still use your bundle namespace, such as myworld:characters/guard.
Complete example
common/data/myworld/entities.json
{
"entries": {
"characters/guard": {
"tags": [
"myworld:character",
"myworld:npc"
],
"components": {
"myworld:body": {
"type": "visual",
"visual": "myworld:characters/guard"
},
"myworld:nameplate": {
"type": "visual",
"visual": "myworld:ui/nameplate",
"position": {
"origin": "top",
"offsetX": 0,
"offsetY": 6
},
"overrides": {
"text": "Guard"
}
},
"myworld:server_ai": {
"type": "server_script",
"script": "my-bundle.server.lua.entities.guard"
}
},
"metadata": {
"spawnGroup": "town",
"level": 3
}
}
}
}
This assumes matching visual definitions such as myworld:characters/guard and myworld:ui/nameplate exist in client/data/myworld/visuals.json or client/data/myworld/visuals/.
Creating entities from Lua
Look up an entity definition through selene.registries, then pass it to selene.entities.
Server Lua:
local Registries = require("selene.registries")
local Entities = require("selene.entities")
local guardDef = Registries.findByName("entities", "myworld:characters/guard")
local guard = Entities.create(guardDef)
guard:SetCoordinate(100, 100, 0)
guard:Spawn(dimension)
The server-created entity is saved and synchronized with clients. Use createTransient for fire-and-forget entities that are synchronized but not assigned a persistent network id:
local effect = Entities.createTransient(guardDef)
effect:SetCoordinate(100, 100, 0)
effect:Spawn(dimension)
Client Lua can create local-only entities:
local Registries = require("selene.registries")
local Entities = require("selene.entities")
local cursorDef = Registries.findByName("entities", "myworld:cursor")
local cursor = Entities.create(cursorDef)
cursor:SetCoordinate(100, 100, 0)
cursor:Spawn()
Use client-created entities for cursors, previews, local effects, and UI-adjacent map overlays. Use server-created entities for gameplay objects that other players should see.
Hot reload behavior
Entity definitions are loaded through the common bundle registry system.
- Updating
common/data/<namespace>/entities.jsonreloads the merged entity file. - Updating a file below
common/data/<namespace>/entities/reloads that single entity entry. - If both
entities.jsonand anentities/directory exist for the same namespace in the same bundle,entities.jsonis authoritative.
Existing client entities subscribe to their definition reference. When an entity definition reloads, the client rebuilds that entity's components from the new definition.
Recommended layout
For most bundles:
my-bundle/
common/
data/
myworld/
entities.json
client/
data/
myworld/
visuals.json
lua/
entities/
sparkle.lua
server/
lua/
entities/
guard.lua
Keep the entity definition in common/data, client visuals in client/data, and platform-specific behavior in platform-specific Lua directories.
Checklist
- Put entity definitions under
common/data/<namespace>/entities.jsonorcommon/data/<namespace>/entities/. - Reference visuals by full identifier, such as
myworld:characters/guard. - Use namespaced component keys, such as
myworld:visual, to avoid collisions. - Use
visualandclient_scriptfor client behavior, andserver_scriptfor server behavior. - Use
tagsfor filtering andmetadatafor registry lookup. - Create gameplay entities on the server; create cursors, previews, and local effects on the client.