Defining Bundle Entities

Define entity registry entries in Selene bundles, attach visuals and scripts, and create entities from Lua.

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:

  • components declares client and server behavior for entities created from this definition.
  • tags are strings used for filtering and classification.
  • metadata stores custom data and can be searched through selene.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.json reloads the merged entity file.
  • Updating a file below common/data/<namespace>/entities/ reloads that single entity entry.
  • If both entities.json and an entities/ directory exist for the same namespace in the same bundle, entities.json is 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.

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.json or common/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 visual and client_script for client behavior, and server_script for server behavior.
  • Use tags for filtering and metadata for registry lookup.
  • Create gameplay entities on the server; create cursors, previews, and local effects on the client.