GUIA PRACTICA
Desarrollo de addons para Minecraft Bedrock
Patrones para construir behavior packs, sistemas de eventos y minijuegos mantenibles con JavaScript. Los ejemplos fueron redactados desde cero a partir de las tecnicas recurrentes encontradas en los packs locales.
Estructura del behavior pack
Organiza el contenido para que Minecraft encuentre cada recurso y el proyecto pueda crecer.
mi_addon/
├── manifest.json
├── pack_icon.png
├── scripts/
│ ├── main.js
│ ├── config.js
│ └── systems/
├── entities/
├── items/
├── blocks/
├── functions/
└── structures/
- Usa
main.jssolo para iniciar modulos y suscripciones globales. - Separa cada minijuego o sistema en su propio archivo.
- Centraliza IDs, tags y objetivos en
config.js. - No incluyas backups ni archivos de desarrollo en el pack publicado.
Manifest y dependencias
Declara el modulo de datos, el punto de entrada JavaScript y las APIs necesarias.
{
"format_version": 2,
"header": {
"name": "Mi Addon",
"description": "Sistema de ejemplo",
"uuid": "UUID-UNICO-DEL-HEADER",
"version": [1, 0, 0],
"min_engine_version": [1, 21, 0]
},
"modules": [
{
"type": "data",
"uuid": "UUID-UNICO-DATA",
"version": [1, 0, 0]
},
{
"type": "script",
"language": "javascript",
"entry": "scripts/main.js",
"uuid": "UUID-UNICO-SCRIPT",
"version": [1, 0, 0]
}
],
"dependencies": [
{ "module_name": "@minecraft/server", "version": "2.0.0" },
{ "module_name": "@minecraft/server-ui", "version": "2.0.0" }
]
}
Punto de entrada del script
Mantiene el arranque legible y evita registrar dos veces el mismo evento.
import { world } from "@minecraft/server";
import { registerAdminMenu } from "./systems/admin-menu.js";
import { registerGameEvents } from "./systems/game-events.js";
let initialized = false;
function initialize() {
if (initialized) return;
initialized = true;
registerAdminMenu();
registerGameEvents();
console.warn("[Mi Addon] Sistemas iniciados.");
}
world.afterEvents.worldLoad.subscribe(initialize);
Si tu version no expone worldLoad, inicia los modulos al importarlos y
conserva una bandera para impedir registros duplicados durante pruebas.
Organizacion modular
Divide responsabilidades sin convertir cada funcion en un archivo distinto.
Modulos recomendados
config.js: IDs y valores configurables.state.js: estado temporal.storage.js: datos persistentes.permissions.js: roles.game.js: reglas principales.
Evita
- Un
main.jscon miles de lineas. - Tags escritos diferente en cada archivo.
- Importaciones circulares.
- Estados globales duplicados.
Eventos del mundo
Reacciona a acciones reales y reduce la necesidad de comprobar todo en cada tick.
import { world } from "@minecraft/server";
world.afterEvents.playerSpawn.subscribe(({ player, initialSpawn }) => {
if (!initialSpawn) return;
player.sendMessage("Bienvenido al evento.");
});
world.afterEvents.itemUse.subscribe(({ source, itemStack }) => {
if (itemStack.typeId !== "pavi:menu") return;
openMainMenu(source);
});
world.afterEvents.entityDie.subscribe(({ deadEntity, damageSource }) => {
if (deadEntity.typeId !== "minecraft:player") return;
registerDeath(deadEntity, damageSource.damagingEntity);
});
En los packs analizados, itemUse, entityHitEntity,
playerSpawn y entityDie son los eventos mas frecuentes.
Temporizadores y ticks
Minecraft intenta ejecutar 20 ticks por segundo. Programa pensando en ese presupuesto.
import { system } from "@minecraft/server";
const countdownId = system.runInterval(() => {
game.secondsLeft -= 1;
updateTimerHud(game.secondsLeft);
if (game.secondsLeft <= 0) {
system.clearRun(countdownId);
finishRound();
}
}, 20);
system.runTimeout(startRound, 60); // 3 segundos
- Guarda el ID retornado para cancelar intervalos.
- No abras un intervalo nuevo por cada jugador.
- Reparte el trabajo cuando proceses listas grandes.
Jugadores y entidades
Filtra desde la consulta para evitar recorrer entidades innecesarias.
import { world } from "@minecraft/server";
const overworld = world.getDimension("overworld");
const activePlayers = world.getPlayers({ tags: ["event:playing"] });
const markers = overworld.getEntities({
type: "pavi:zone_marker",
location: { x: 0, y: 64, z: 0 },
maxDistance: 40
});
for (const player of activePlayers) {
if (!player.isValid) continue;
player.onScreenDisplay.setActionBar("Partida activa");
}
Tags y roles
Funcionan bien para estados visibles y temporales, pero no deben ser la unica seguridad.
const ROLE_TAGS = {
owner: "role:owner",
admin: "role:admin",
player: "role:player"
};
function hasRole(player, role) {
return player.hasTag(ROLE_TAGS[role]);
}
function canOpenAdminMenu(player) {
return hasRole(player, "owner") || hasRole(player, "admin");
}
Scoreboards
Ideales para puntuaciones visibles, monedas y valores consultados por comandos.
import { world } from "@minecraft/server";
function getObjective(id, displayName = id) {
return world.scoreboard.getObjective(id)
?? world.scoreboard.addObjective(id, displayName);
}
function setCoins(player, amount) {
getObjective("mikucoins", "MikuCoins")
.setScore(player, Math.max(0, Math.floor(amount)));
}
function getCoins(player) {
return getObjective("mikucoins").getScore(player) ?? 0;
}
Propiedades dinamicas
Guarda configuraciones y estado persistente que no necesita mostrarse en un scoreboard.
import { world } from "@minecraft/server";
const KEY = "pavi:event_state";
export function saveEventState(state) {
world.setDynamicProperty(KEY, JSON.stringify(state));
}
export function loadEventState() {
const raw = world.getDynamicProperty(KEY);
if (typeof raw !== "string") return null;
try {
return JSON.parse(raw);
} catch {
return null;
}
}
- Versiona el formato si puede cambiar con futuras actualizaciones.
- No escribas el mismo JSON cada tick.
- Valida tipos y valores al cargar.
Comandos y dimensiones
Usa la API directa cuando exista y reserva comandos para operaciones sin equivalente claro.
import { world } from "@minecraft/server";
const nether = world.getDimension("nether");
function movePlayer(player) {
player.teleport(
{ x: 12.5, y: 72, z: -8.5 },
{ dimension: nether, facingLocation: { x: 12.5, y: 72, z: 0 } }
);
}
function clearNearbyItems(dimension) {
try {
dimension.runCommand("kill @e[type=item,r=12]");
} catch (error) {
console.warn(`No se pudieron limpiar items: ${error}`);
}
}
Maquina de estados para minijuegos
Evita booleanos contradictorios durante lobby, cuenta regresiva y partida.
const Phase = Object.freeze({
LOBBY: "lobby",
COUNTDOWN: "countdown",
PLAYING: "playing",
RESULTS: "results"
});
const game = { phase: Phase.LOBBY, round: 0, players: new Set() };
function transitionTo(nextPhase) {
const allowed = {
[Phase.LOBBY]: [Phase.COUNTDOWN],
[Phase.COUNTDOWN]: [Phase.PLAYING, Phase.LOBBY],
[Phase.PLAYING]: [Phase.RESULTS],
[Phase.RESULTS]: [Phase.LOBBY]
};
if (!allowed[game.phase].includes(nextPhase)) return false;
game.phase = nextPhase;
return true;
}
Formularios con server-ui
Construye menus claros y vuelve a validar permisos despues de la respuesta.
import { ActionFormData } from "@minecraft/server-ui";
async function openAdminMenu(player) {
if (!canOpenAdminMenu(player)) return;
const response = await new ActionFormData()
.title("Panel del evento")
.body("Selecciona una accion.")
.button("Iniciar partida")
.button("Cancelar partida")
.show(player);
if (response.canceled || !canOpenAdminMenu(player)) return;
if (response.selection === 0) startGame();
if (response.selection === 1) cancelGame();
}
Inventarios e items
Comprueba que el componente y el contenedor existan antes de leer slots.
function countItem(player, typeId) {
const inventory = player.getComponent("minecraft:inventory");
const container = inventory?.container;
if (!container) return 0;
let total = 0;
for (let slot = 0; slot < container.size; slot += 1) {
const item = container.getItem(slot);
if (item?.typeId === typeId) total += item.amount;
}
return total;
}
Combate, muerte y vidas
Separa la muerte de Minecraft de la eliminacion definitiva del evento.
world.afterEvents.entityDie.subscribe(({ deadEntity, damageSource }) => {
if (deadEntity.typeId !== "minecraft:player") return;
const remaining = Math.max(0, getLives(deadEntity) - 1);
setLives(deadEntity, remaining);
const killer = damageSource.damagingEntity;
if (killer?.typeId === "minecraft:player") addKill(killer);
if (remaining === 0) eliminatePlayer(deadEntity);
else scheduleRespawn(deadEntity, remaining);
});
- Registra el ultimo atacante con expiracion para asistencias.
- No otorgues dos kills desde eventos diferentes.
- Haz idempotente la eliminacion.
Zonas y deteccion
Comprueba volumenes simples y dispara acciones solo al entrar o salir.
function isInsideBox(location, min, max) {
return location.x >= min.x && location.x <= max.x
&& location.y >= min.y && location.y <= max.y
&& location.z >= min.z && location.z <= max.z;
}
const inside = new Set();
function updateZone(players, zone) {
for (const player of players) {
const nowInside = isInsideBox(player.location, zone.min, zone.max);
const wasInside = inside.has(player.id);
if (nowInside && !wasInside) onZoneEnter(player);
if (!nowInside && wasInside) onZoneLeave(player);
nowInside ? inside.add(player.id) : inside.delete(player.id);
}
}
Sonidos y particulas
Usa efectos como respuesta a acciones concretas y limita su alcance.
function playRoundStart(player) {
player.playSound("pavi.round_start", {
location: player.location,
volume: 0.8,
pitch: 1
});
player.dimension.spawnParticle("pavi:round_flash", {
x: player.location.x,
y: player.location.y + 1,
z: player.location.z
});
}
Componentes personalizados
Conecta items o bloques JSON con funciones JavaScript reutilizables.
import { system } from "@minecraft/server";
system.beforeEvents.startup.subscribe(({ itemComponentRegistry }) => {
itemComponentRegistry.registerCustomComponent("pavi:open_menu", {
onUse({ source }) {
if (source?.typeId !== "minecraft:player") return;
openMainMenu(source);
}
});
});
{
"minecraft:item": {
"description": { "identifier": "pavi:menu_item" },
"components": { "pavi:open_menu": {} }
}
}
Script events
Comunica funciones, command blocks y scripts mediante IDs con namespace.
system.afterEvents.scriptEventReceive.subscribe((event) => {
if (event.id !== "pavi:event") return;
const [action, value = ""] = event.message.trim().split(/\s+/, 2);
if (action === "start") startGame();
if (action === "round") setRound(Number(value));
}, { namespaces: ["pavi"] });
Valida el origen y los argumentos. No conviertas un mensaje recibido directamente en un comando.
ServerNet y servicios web
Sincroniza datos desde Bedrock Dedicated Server con una API controlada.
import {
http, HttpHeader, HttpRequest, HttpRequestMethod
} from "@minecraft/server-net";
async function sendSnapshot(payload, token) {
const request = new HttpRequest("https://api.example.com/event/live");
request.method = HttpRequestMethod.Post;
request.headers = [
new HttpHeader("content-type", "application/json"),
new HttpHeader("authorization", `Bearer ${token}`)
];
request.body = JSON.stringify(payload);
request.timeout = 10;
const response = await http.request(request);
if (response.status < 200 || response.status >= 300) {
throw new Error(`HTTP ${response.status}`);
}
}
- Nunca incluyas la
service_rolede Supabase en el pack. - Usa un token limitado y rotatorio.
- Envia cambios agrupados, no una solicitud por jugador cada tick.
- Implementa timeout y reintentos limitados.
Rendimiento y watchdog
Reduce el trabajo por tick y evita ciclos o transformaciones costosas.
No recorras jugadores si ya existe un evento.
HUD y zonas suelen funcionar cada 5, 10 o 20 ticks.
Divide listas grandes entre varios ticks.
Registra y limpia intervalos una sola vez.
const queue = [];
system.runInterval(() => {
for (let index = 0; index < 10 && queue.length; index += 1) {
const task = queue.shift();
try {
task();
} catch (error) {
console.warn(`[Queue] ${error}`);
}
}
}, 1);
Seguridad y permisos
La ofuscacion retrasa la lectura; la proteccion real depende de validaciones consistentes.
- Valida el rol al mostrar el menu y al ejecutar cada accion.
- Centraliza permisos para evitar reglas contradictorias.
- No confies solo en tags agregables manualmente.
- No almacenes secretos administrativos en el cliente.
- Comprueba rangos, IDs y estado en toda entrada externa.
- Registra acciones administrativas importantes.
function assertGameAction(player, action) {
if (!player?.isValid) return false;
if (!game.players.has(player.id)) return false;
if (game.phase !== "playing") return false;
if (game.turnPlayerId !== player.id) return false;
if (!ALLOWED_ACTIONS.has(action)) return false;
return true;
}
Depuracion y publicacion
Prueba sistemas por separado y deja mensajes utiles sin revelar datos sensibles.
function safeRun(label, callback) {
try {
return callback();
} catch (error) {
console.warn(`[${label}] ${error?.stack ?? error}`);
return undefined;
}
}
Prueba con eventos, vidas, formularios, HTTP o rendimiento.