7  JavaScript

7.1 Introducción

JavaScript es un lenguaje de programación que permite incorporar interactividad en las páginas web, lo que lo convierte en una herramienta fundamental para el desarrollo de videojuegos web. Con JavaScript se puede modificar la página y ejecutar código cuando se interactúa con ella a través del modelo de objetos del documento (DOM). También se pueden hacer peticiones al servidor web en segundo plano y actualizar el contenido de la web con los resultados (AJAX), aunque en este capítulo nos centraremos en el lenguaje en sí y en su entorno de desarrollo.

Características de JavaScript

JavaScript es un lenguaje de programación basado en el estándar ECMAScript de ECMA (una organización diferente al W3C). Aunque en el pasado existían diferencias significativas en la implementación de JavaScript entre navegadores, actualmente todos son bastante compatibles entre sí.

Versiones de ECMAScript

ES5 (2011): La versión del estándar que popularizó el lenguaje ECMAScript fue la 5.1 (2011), aunque generalmente se la conoce como ES5. Prácticamente todos los navegadores modernos soportan la mayoría de las características definidas en el estándar 5.1.

ES2015 (ES6): En junio 2015 finalizó el desarrollo de ES6 con una evolución importante del lenguaje. A última hora decidieron llamarle oficialmente ES2015. Está soportada casi al completo por casi todos los navegadores modernos. Introdujo características fundamentales como clases, módulos, arrow functions y promises.

Versiones actuales: Desde ES2015, ECMAScript sigue un ciclo de actualizaciones anuales con compatibilidad hacia atrás:

  • ECMAScript 2016-2025 (versiones 7-16)
  • Cada versión añade nuevas características manteniendo compatibilidad
  • Los navegadores modernos soportan las características de las versiones más recientes del estándar

JavaScript vs Java

Aunque algunos elementos de la sintaxis recuerden a Java, son lenguajes completamente diferentes. El nombre JavaScript se eligió al publicar el lenguaje en una época en la que Java estaba en auge y fue principalmente por marketing (inicialmente se llamó LiveScript).

Características principales de JavaScript

Scripting: No necesita compilador. Inicialmente era un lenguaje interpretado, pero actualmente se ejecuta en máquinas virtuales en los navegadores, proporcionando mayor velocidad de ejecución y eficiencia de memoria.

Tipado dinámico: Habitual en los lenguajes de script. Las variables no requieren declaración de tipo.

Funcional: Las funciones son elementos de primer orden, pueden asignarse a variables y pasarse como parámetros.

Orientado a objetos: Basado en prototipos, no en clases como Java, C++, Ruby, aunque ES2015 introdujo una sintaxis de clases más familiar.

DOM y BOM

El navegador web proporciona al JavaScript dos APIs fundamentales para interactuar con la página y el entorno del navegador. Entender la diferencia entre estas dos es crucial para el desarrollo de videojuegos web.

DOM (Document Object Model): Es una representación en forma de árbol del documento HTML cargado en el navegador. Cada elemento HTML (divs, canvas, botones, etc.) se convierte en un objeto que JavaScript puede manipular. Para videojuegos, el DOM es esencial para:

  • Acceder al elemento <canvas> donde se renderiza el juego
  • Crear y manipular elementos de la interfaz de usuario (menús, HUD, puntuaciones)
  • Gestionar eventos de entrada del usuario (clicks, teclas presionadas, movimientos del ratón)
  • Modificar estilos CSS dinámicamente (pantallas de carga, efectos visuales)
  • Insertar y eliminar elementos HTML en tiempo real (mensajes, notificaciones)

BOM (Browser Object Model): Proporciona acceso a funcionalidades del navegador más allá del documento HTML. El BOM incluye al DOM como uno de sus componentes, pero añade capacidades adicionales críticas para videojuegos:

  • window: El objeto global que representa la ventana del navegador
    • Control del tamaño de la ventana
    • Detección de cambio de tamaño (responsive design)
    • Gestión del foco de la ventana (pausar el juego cuando se cambia de pestaña)
  • navigator: Información sobre el navegador y el dispositivo
    • Detección de características soportadas (WebGL, audio, gamepad)
    • Información del sistema operativo y navegador
    • Acceso a la geolocalización y otros sensores
  • location: Control de la URL actual
    • Navegación entre diferentes pantallas del juego
    • Gestión del historial del navegador
  • localStorage/sessionStorage: Almacenamiento persistente de datos
    • Guardar progreso del jugador
    • Almacenar configuraciones y preferencias
  • fetch/XMLHttpRequest: Comunicación con servidores
    • Cargar recursos del juego dinámicamente
    • Enviar y recibir datos de partidas multijugador
    • Comunicación con APIs REST
  • console: Herramienta de debugging
    • Logging de errores y mensajes de debug
    • Medición de rendimiento del juego

En resumen: el DOM se centra en el contenido de la página HTML, mientras que el BOM proporciona acceso a todas las capacidades del navegador. Para videojuegos web, ambos son fundamentales y trabajan juntos para crear experiencias interactivas completas.

Librerías y Frameworks JavaScript

Existen multitud de bibliotecas JavaScript para el desarrollo de aplicaciones de videojuegos:

Bibliotecas de propósito general:

  • Lodash: Biblioteca moderna que reemplaza a underscore.js para trabajar con estructuras de datos con un enfoque funcional
  • Axios: Cliente HTTP moderno que reemplaza las peticiones AJAX tradicionales

Frameworks para videojuegos:

  • Phaser: Framework completo para desarrollo de juegos 2D
  • Three.js: Biblioteca para gráficos 3D y WebGL
  • Babylon.js: Motor 3D completo para juegos web
  • PixiJS: Renderizador 2D de alta performance

Frameworks de aplicación moderna:

  • React: Biblioteca para interfaces de usuario componentizadas
  • Vue.js: Framework progresivo para aplicaciones web
  • Angular: Framework completo para aplicaciones de gran escala

7.2 Configuración del Entorno de Desarrollo con Node.js

Introducción a Node.js

Node.js es un entorno de ejecución para JavaScript construido sobre el motor V8 de Chrome. Permite ejecutar JavaScript fuera del navegador y se ha convertido en el estándar para el desarrollo de aplicaciones JavaScript modernas.

Instalación de Node.js

  1. Descargar Node.js: Visita nodejs.org y descarga la versión LTS (Long Term Support)

  2. Verificar instalación: Abre una terminal y ejecuta:

    node --version
    npm --version

Gestión de Paquetes con npm

npm (Node Package Manager) es el gestor de paquetes oficial de Node.js que permite instalar y gestionar dependencias.

# Verificar versión de npm
npm --version

# Actualizar npm
npm install -g npm@latest

# Obtener ayuda
npm help

Creación de un Proyecto

Inicializar un proyecto

Todo proyecto de JavaScript moderno comienza con la inicialización de npm, que crea el archivo de configuración principal del proyecto. Este archivo, llamado package.json, actúa como el “documento de identidad” del proyecto, describiendo sus características, dependencias y comandos de ejecución.

# Crear directorio del proyecto
mkdir mi-juego-web
cd mi-juego-web

# Inicializar proyecto npm
npm init -y

El comando npm init -y crea automáticamente el archivo package.json con valores por defecto. La opción -y (yes) acepta todas las opciones por defecto sin preguntar, acelerando el proceso. Si omitimos -y, npm nos hará preguntas interactivas sobre cada campo.

Esto crea un archivo package.json que podemos personalizar para nuestro videojuego:

{
  "name": "mi-juego-web",
  "version": "1.0.0",
  "description": "Un videojuego web desarrollado en JavaScript",
  "main": "src/index.js",
  "scripts": {
    "start": "node server.js",
    "dev": "webpack serve --mode development",
    "build": "webpack --mode production"
  },
  "keywords": ["juego", "javascript", "web"],
  "author": "Tu Nombre",
  "license": "MIT"
}

Explicación de cada campo del package.json:

  • name: Identificador único del proyecto. Debe ser en minúsculas, sin espacios (usar guiones). Este nombre se usa si publicamos el proyecto en npm. Para nuestro videojuego, elegimos un nombre descriptivo como “mi-juego-web”.

  • version: Número de versión siguiendo el formato semántico (semantic versioning): MAJOR.MINOR.PATCH. Comenzamos en “1.0.0” y actualizamos según los cambios: cambios mayores incrementan el primer número, nuevas características el segundo, y correcciones de bugs el tercero.

  • description: Texto breve que explica qué es el proyecto. Útil cuando otros desarrolladores encuentren nuestro código o cuando busquemos proyectos en npm. Para videojuegos, describimos el género o concepto del juego.

  • main: Punto de entrada principal del proyecto. Indica qué archivo JavaScript se ejecuta cuando alguien importa nuestro proyecto. Para videojuegos web, típicamente apunta al archivo principal del juego en src/index.js o src/main.js.

  • scripts: Objeto que define comandos personalizados que podemos ejecutar con npm run <nombre>. Estos scripts automatizan tareas comunes:

    • "start": Comando para ejecutar el proyecto en producción
    • "dev": Inicia el servidor de desarrollo con recarga automática
    • "build": Genera los archivos optimizados para producción

    Ejecutamos estos scripts con npm start, npm run dev, o npm run build.

  • keywords: Array de palabras clave que describen el proyecto. Útil para que otros encuentren el proyecto si lo publicamos en npm. Para videojuegos, incluimos términos como el género, tecnologías usadas, etc.

  • author: Nombre del creador o equipo de desarrollo. Puede incluir email: "Tu Nombre <email@ejemplo.com>".

  • license: Tipo de licencia del código. “MIT” es una licencia permisiva común para proyectos open source. Otras opciones populares son “GPL-3.0”, “Apache-2.0”, o “UNLICENSED” para proyectos privados.

Estructura de carpetas recomendada

mi-juego-web/
├── package.json
├── webpack.config.js
├── src/
├── dist/
└── public/
    └── index.html

Instalación de Dependencias

El sistema de gestión de paquetes de npm nos permite instalar bibliotecas y herramientas desarrolladas por la comunidad, evitando tener que escribir todo el código desde cero. En el desarrollo de videojuegos, esto es especialmente valioso porque podemos aprovechar engines de juegos, bibliotecas de física, sistemas de audio y muchas otras funcionalidades ya probadas y optimizadas.

Las dependencias se dividen en dos categorías principales: las de producción (que forman parte del juego final) y las de desarrollo (que solo usamos durante el proceso de creación).

Dependencias de producción

Estas son las bibliotecas que formarán parte de nuestro juego final y que los jugadores descargarán:

# Framework de juegos
npm install phaser

# Utilidades
npm install lodash

# Cliente HTTP
npm install axios

Dependencias de desarrollo

Las herramientas de desarrollo nos ayudan durante el proceso de creación del juego, pero no se incluyen en la versión final que descargan los jugadores. Estas incluyen herramientas para optimizar código, servir archivos durante el desarrollo, y verificar la calidad del código:

# Bundler y servidor de desarrollo
npm install --save-dev webpack webpack-cli webpack-dev-server

# Loaders para recursos
npm install --save-dev html-webpack-plugin css-loader style-loader file-loader

# Herramientas de desarrollo
npm install --save-dev eslint prettier

Configuración de Webpack

Webpack es una herramienta fundamental en el desarrollo moderno de JavaScript que actúa como empaquetador de módulos (bundler). Su función principal es tomar todos los archivos JavaScript, CSS, imágenes y otros recursos de nuestro proyecto y crear uno o varios archivos optimizados para el navegador.

En el contexto del desarrollo de videojuegos, Webpack nos permite organizar nuestro código en múltiples archivos (clases para jugadores, enemigos, sistemas de audio, etc.) y luego combinarlos en un solo paquete eficiente. También puede optimizar imágenes, procesar archivos de audio y gestionar otros recursos del juego. Además, Webpack incluye un servidor de desarrollo con recarga automática que facilita enormemente el proceso de desarrollo.

Para configurar Webpack en nuestro proyecto de videojuego, creamos un archivo webpack.config.js en la raíz del proyecto:

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
    clean: true
  },
  mode: 'development',
  devtool: 'inline-source-map',
  devServer: {
    static: './dist',
    hot: true,
    port: 8080
  },
  externals: {
    phaser: 'Phaser'
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html',
      inject: false
    })
  ],
  resolve: {
    extensions: ['.js']
  }
};

Explicación detallada de cada sección del webpack.config.js:

Importaciones iniciales:

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
  • path: Módulo de Node.js para trabajar con rutas de archivos de forma independiente del sistema operativo. Garantiza que las rutas funcionen tanto en Windows como en Mac/Linux.
  • HtmlWebpackPlugin: Plugin que genera automáticamente el archivo HTML final incluyendo las referencias a los archivos JavaScript generados.

Punto de entrada (entry):

entry: './src/main.js',

Especifica el archivo JavaScript principal desde donde Webpack comienza a construir el grafo de dependencias del proyecto. Webpack analiza este archivo, encuentra todos los imports/requires, y recursivamente incluye todos los módulos necesarios. Para videojuegos, este suele ser el archivo que inicializa el motor de juego y carga la primera escena.

Configuración de salida (output):

output: {
  filename: 'bundle.js',              // Nombre del archivo JavaScript generado
  path: path.resolve(__dirname, 'dist'),  // Directorio donde se guardan los archivos generados
  clean: true                          // Limpia el directorio dist antes de cada build
}
  • filename: Nombre del archivo JavaScript final que contendrá todo nuestro código empaquetado y optimizado.
  • path: Directorio de salida. __dirname es el directorio actual, 'dist' es donde se guardarán los archivos de producción.
  • clean: true: Elimina archivos antiguos antes de generar nuevos, evitando acumulación de builds anteriores.

Modo de desarrollo (mode):

mode: 'development',

Define el entorno de ejecución. Puede ser:

  • 'development': Código sin minificar, con nombres de variables legibles, útil para debugging.
  • 'production': Código minificado y optimizado, menor tamaño, más rápido, pero difícil de debuggear.

Herramientas de debugging (devtool):

devtool: 'inline-source-map',

Genera source maps que permiten debuggear el código original (antes de empaquetarlo) en las DevTools del navegador. Cuando hay un error, el navegador muestra la línea del archivo fuente original en lugar del código empaquetado, facilitando enormemente el debugging.

Servidor de desarrollo (devServer):

devServer: {
  static: './dist',    // Directorio desde donde servir archivos estáticos
  hot: true,           // Hot Module Replacement - actualiza módulos sin recargar la página
  port: 8080          // Puerto donde se ejecuta el servidor de desarrollo
}

Configura el servidor web integrado de Webpack:

  • static: Directorio con archivos estáticos (HTML, imágenes, etc.) que el servidor debe servir.
  • hot: true: Habilita Hot Module Replacement (HMR), que permite actualizar módulos JavaScript en tiempo real sin recargar toda la página. Muy útil durante desarrollo para mantener el estado del juego.
  • port: Puerto donde se ejecutará el servidor (http://localhost:8080).

Librerías externas (externals):

externals: {
  phaser: 'Phaser'
}

Indica a Webpack que ciertas dependencias se cargarán desde fuera del bundle (típicamente desde un CDN). En este caso, Phaser se carga desde un <script> en el HTML en lugar de incluirse en el bundle, reduciendo el tamaño del archivo final. La clave es el nombre del módulo en el código (import Phaser from 'phaser') y el valor es la variable global que expone (window.Phaser).

Plugins:

plugins: [
  new HtmlWebpackPlugin({
    template: './public/index.html',  // Archivo HTML plantilla
    inject: false                      // No inyecta automáticamente los scripts
  })
]
  • HtmlWebpackPlugin: Genera el archivo HTML final a partir de una plantilla.
  • template: Ruta al archivo HTML base que usará como plantilla.
  • inject: false: Desactiva la inyección automática de scripts. Útil cuando queremos control manual sobre dónde y cómo se cargan los scripts en el HTML (por ejemplo, cuando usamos librerías externas como Phaser desde CDN).

Resolución de módulos (resolve):

resolve: {
  extensions: ['.js']
}

Define qué extensiones de archivo Webpack intentará resolver automáticamente cuando importamos módulos sin especificar extensión. Esto permite escribir import Player from './player' en lugar de import Player from './player.js'. Útil para proyectos grandes donde escribir las extensiones constantemente es tedioso.

Scripts de Desarrollo

Una vez configurado Webpack, necesitamos definir comandos que automaticen las tareas más comunes del desarrollo. Los scripts de npm nos permiten crear atajos para ejecutar procesos complejos con comandos simples. Esto es especialmente útil cuando trabajamos en equipo, ya que todos los desarrolladores pueden usar los mismos comandos estándar.

Actualizamos el package.json con scripts útiles para el desarrollo de videojuegos:

{
  "scripts": {
    "dev": "webpack serve --mode development --open",
    "build": "webpack --mode production",
    "lint": "eslint src/",
    "format": "prettier --write src/"
  }
}

7.3 El Lenguaje JavaScript

Características del Lenguaje

La mayoría de las características que se van a repasar están disponibles desde JavaScript ES5, aunque las versiones más recientes ofrecen mejoras significativas. Las versiones más recientes pueden presentar problemas de compatibilidad con navegadores muy antiguos, pero esto no es una preocupación para el desarrollo moderno.

Imperativo y Estructurado

JavaScript mantiene las características imperativas familiares para programadores de Java y C:

  • Se declaran variables
  • Se ejecutan las sentencias en orden
  • Dispone de sentencias de control de flujo (if, while, for…)
  • La sintaxis imperativa/estructurada es muy parecida a Java y C

Lenguaje de Script

  • No existe compilador (aunque se usan transpiladores como Babel para compatibilidad)
  • El navegador carga el código, lo analiza y lo ejecuta
  • El navegador indica tanto errores de sintaxis como errores de ejecución
  • Los motores modernos (V8, SpiderMonkey) incluyen compilación JIT para optimización

Tipado Dinámico

  • Al declarar una variable no se indica su tipo
  • A lo largo de la ejecución del programa una misma variable puede tener valores de diferentes tipos
  • Esto proporciona flexibilidad pero requiere más cuidado en el desarrollo

Orientado a Objetos

  • Todos los valores a excepción de los tipos primitivos son objetos.
  • Los primitivos se encapsulan automáticamente en objetos wrapper (String, Number…) al acceder a sus métodos.
  • Existe recolector de basura para liberar la memoria automáticamente
  • La orientación a objetos está basada en prototipos, aunque ES2015+ añadió sintaxis de clases
  • En tiempo de ejecución se pueden crear objetos, cambiar atributos e invocar métodos
  • En tiempo de ejecución se pueden añadir y borrar atributos y métodos dinámicamente

Funciones como Ciudadanos de Primera Clase

  • Aunque sea orientado a objetos, también permite declarar funciones independientes
  • Las funciones se pueden declarar en cualquier sitio, asignarse a variables y pasarse como parámetro
  • Existen funciones anónimas y arrow functions
  • En JavaScript se puede implementar código siguiendo el paradigma funcional

Modo Estricto

Las primeras versiones de JavaScript permitían escribir código que posteriormente se consideró propenso a errores. ES5 definió un modo estricto que genera errores para código problemático.

Para activar el modo estricto (recomendable) basta poner al principio del código:

"use strict";

En módulos ES2015+, el modo estricto está activado por defecto.

7.4 Integración con HTML

Inclusión de JavaScript

El código JavaScript se puede incluir directamente en el documento HTML en etiquetas <script>, pero es recomendable que el código esté en ficheros independientes:

<html>
<head>
    <!-- Para código que debe ejecutarse antes que el DOM -->
    <script src="js/config.js"></script>
</head>
<body>
    <!-- Contenido HTML -->
    
    <!-- Scripts al final para mejor rendimiento -->
    <script src="js/game.js"></script>
</body>
</html>

Optimización de Carga

Ubicación de scripts: Cuando se carga el JavaScript, el navegador bloquea el procesamiento del HTML. Por ello, se recomienda poner los elementos <script> como último elemento de la página.

Atributos modernos:

<!-- Carga asíncrona, no bloquea el HTML -->
<script src="js/game.js" async></script>

<!-- Carga diferida, ejecuta después del HTML -->
<script src="js/game.js" defer></script>

7.5 Sintaxis Básica

Mostrar Información

// Escribir en el documento HTML (poco usado en desarrollo moderno)
document.write('Texto');

// Escribir en la consola del navegador (debugging)
console.log('Información de debug');
console.error('Error crítico');
console.warn('Advertencia');

Comentarios

// Comentario de una línea

/*
 * Comentario
 * multilínea
 */

Delimitadores

  • Bloques: {}
  • Sentencias: ; (opcionales pero recomendados)

Variables

En JavaScript moderno, tenemos tres formas de declarar variables, cada una con características específicas que las hacen adecuadas para diferentes situaciones. Esta evolución del lenguaje ha mejorado significativamente la robustez y mantenibilidad del código.

Declaraciones modernas (ES2015+):

// const - valor inmutable, block scope
const MAX_LIVES = 3;
const GAME_CONFIG = {
    width: 800,
    height: 600
};

// let - valor mutable, block scope
let currentLives = MAX_LIVES;
let playerPosition = { x: 0, y: 0 };

// var - evitar en código nuevo (function scope)
var oldStyle = "no recomendado";

Ámbito de variables:

El concepto de ámbito (scope) es fundamental para entender cómo JavaScript maneja las variables. El ámbito determina desde qué partes del código podemos acceder a una variable:

  • let y const tienen ámbito de bloque - solo existen dentro del bloque {} donde se declaran
  • var tiene ámbito de función - existe en toda la función donde se declara (puede causar problemas)
  • Variables no declaradas se convierten en globales - esto es un error en modo estricto

Tipos de Datos

JavaScript maneja diferentes tipos de datos que nos permiten representar toda la información necesaria para un videojuego. A diferencia de lenguajes como Java o C++, JavaScript es de tipado dinámico, lo que significa que una variable puede cambiar de tipo durante la ejecución del programa.

Primitivos (se auto-encapsulan en objetos wrapper al invocar métodos sobre ellos):

// Number - enteros y reales
let score = 1000;
let health = 75.5;

// String - cadenas de caracteres
let playerName = "Jugador1";
let message = 'Game Over';

// Boolean
let isGameRunning = true;
let isPaused = false;

// Tipos especiales
let powerUp = null;           // Ausencia intencional de valor
let specialAbility;           // undefined - no inicializada

Template literals (ES2015+):

Los template literals son una característica moderna de JavaScript que nos permite crear strings de manera más expresiva y legible. Son especialmente útiles en videojuegos para construir mensajes dinámicos, interfaces de usuario y contenido HTML generado dinámicamente:

let level = 5;
let experience = 1250;

// Interpolación de strings - insertar valores directamente
let status = `Nivel ${level} - EXP: ${experience}`;

// Strings multilínea - útil para templates HTML
let gameInfo = `
Jugador: ${playerName}
Nivel: ${level}
Puntuación: ${score}
`;

Operadores

JavaScript incluye la mayoría de operadores familiares para programadores de otros lenguajes, pero también añade algunos específicos que son importantes conocer para evitar errores comunes.

Similares a Java:

Los operadores aritméticos y lógicos funcionan de manera similar a otros lenguajes de programación:

// Aritméticos
let damage = baseDamage + bonus;
let remaining = total - used;
let area = width * height;
let average = sum / count;
let remainder = value % modulo;

// Comparación
if (health > 0 && mana >= spellCost) {
    castSpell();
}

// Lógicos
let canAct = isAlive && !isStunned;
let shouldRespawn = isDead || health <= 0;

Específicos de JavaScript:

JavaScript tiene operadores de comparación únicos que es crucial entender para evitar errores sutiles pero problemáticos:

// Igualdad estricta (recomendado)
if (playerID === targetID) {
    // Compara valor y tipo
}

// Desigualdad estricta
if (level !== previousLevel) {
    // Diferentes valor o tipo
}

// Igualdad débil (evitar)
if (score == "100") {
    // true - hace conversión de tipos
}

Operadores modernos (ES2020+):

Las versiones recientes de JavaScript han introducido operadores que simplifican patrones comunes de programación y hacen el código más legible y menos propenso a errores:

// Nullish coalescing - valor por defecto solo para null/undefined
let playerName = savedName ?? "Jugador Anónimo";

// Optional chaining - acceso seguro a propiedades
let weapon = player.inventory?.equipment?.weapon;

// Logical assignment
playerName ||= "Jugador por defecto";  // solo si falsy
playerName ??= "Valor por defecto";   // solo si null/undefined

7.6 Arrays

Los arrays en JavaScript son estructuras de datos fundamentales para el desarrollo de videojuegos, donde frecuentemente necesitamos manejar listas de enemigos, items del inventario, puntuaciones, coordenadas y muchos otros elementos. Aunque comparten similitudes con los arrays de Java, tienen características únicas que los hacen más flexibles pero también requieren mayor cuidado en su uso.

Los arrays de JavaScript son dinámicos por naturaleza, pueden contener elementos de diferentes tipos y crecen automáticamente cuando se necesita. Esta flexibilidad es especialmente útil en videojuegos donde las listas de elementos pueden cambiar constantemente durante el juego.

// Creación de arrays
let empty = [];
let numbers = [1, 2, 3, 4, 5];
let mixed = ["texto", 42, true, null];
let inventory = new Array(10); // Array disperso de longitud 10 con huecos vacíos

// Acceso y modificación
console.log(numbers[0]);        // 1
numbers[2] = 999;              // Modifica el elemento
console.log(numbers.length);    // 5

// Arrays pueden crecer dinámicamente
numbers[10] = 100;             // Crea huecos vacíos (array disperso)
console.log(numbers.length);    // 11

Métodos de Array Modernos

Los métodos modernos de arrays en JavaScript son herramientas poderosas que permiten manipular datos de manera más expresiva y funcional. Estos métodos son especialmente útiles en videojuegos donde constantemente necesitamos filtrar enemigos, transformar datos, buscar elementos específicos o procesar listas de objetos del juego.

Estos métodos siguen un paradigma funcional: no modifican el array original (excepto algunos como push y pop), sino que retornan nuevos arrays o valores. Esto hace el código más predecible y menos propenso a errores:

let enemies = [
    { id: 1, health: 100, type: "orc" },
    { id: 2, health: 50, type: "goblin" },
    { id: 3, health: 0, type: "orc" }
];

// Filtrar elementos
let aliveEnemies = enemies.filter(enemy => enemy.health > 0);
let orcs = enemies.filter(enemy => enemy.type === "orc");

// Transformar elementos
let healthValues = enemies.map(enemy => enemy.health);
let enemyIds = enemies.map(enemy => enemy.id);

// Encontrar elementos
let firstOrc = enemies.find(enemy => enemy.type === "orc");
let orcIndex = enemies.findIndex(enemy => enemy.type === "orc");

// Verificar condiciones
let allDead = enemies.every(enemy => enemy.health === 0);
let someDead = enemies.some(enemy => enemy.health === 0);

// Reducir a un valor
let totalHealth = enemies.reduce((sum, enemy) => sum + enemy.health, 0);

// Modificar array
enemies.push({ id: 4, health: 75, type: "troll" });    // Añadir al final
let firstEnemy = enemies.shift();                       // Quitar del inicio
let lastEnemy = enemies.pop();                          // Quitar del final

// Eliminar/insertar elementos
enemies.splice(1, 2);                 // Eliminar 2 elementos desde índice 1
const newEnemy = { id: 5, health: 60, type: "slime" };
enemies.splice(1, 0, newEnemy);       // Insertar en índice 1

Destructuring de Arrays (ES2015+)

El destructuring es una característica moderna que nos permite extraer valores de arrays de manera más concisa y legible. Es especialmente útil cuando trabajamos con coordenadas, vectores o cualquier conjunto de valores relacionados que se almacenan en arrays:

let coordinates = [100, 200];
let [x, y] = coordinates;  // x = 100, y = 200

let [first, second, ...rest] = inventory;  // Rest operator

7.7 Sentencias de Control de Flujo

Las sentencias de control de flujo en JavaScript son fundamentales para implementar la lógica de nuestros videojuegos. Nos permiten tomar decisiones, repetir acciones y controlar el comportamiento del juego basándose en diferentes condiciones como el estado del jugador, la fase del juego o las acciones del usuario.

Sentencias Básicas

JavaScript utiliza una sintaxis muy similar a Java y C para las estructuras de control, lo que facilita la transición desde otros lenguajes de programación:

// if - else
if (health > 50) {
    statusColor = "green";
} else if (health > 20) {
    statusColor = "yellow";
} else {
    statusColor = "red";
}
// switch
switch (gameState) {
    case "menu":
        showMenu();
        break;
    case "playing":
        updateGame();
        break;
    case "paused":
        showPauseScreen();
        break;
    default:
        handleUnknownState();
}
// Loops
for (let i = 0; i < enemies.length; i++) {
    updateEnemy(enemies[i]);
}

// for...of - iterar valores (ES2015+)
for (let enemy of enemies) {
    updateEnemy(enemy);
}

// for...in - iterar propiedades
for (let key in gameConfig) {
    console.log(key, gameConfig[key]);
}

// while
while (isGameRunning && playerLives > 0) {
    processGameFrame();
}

Valores Falsy

Un concepto importante en JavaScript es el de valores “falsy”. JavaScript es más permisivo que otros lenguajes al evaluar condiciones booleanas, y automáticamente convierte ciertos valores a false en contextos booleanos. Esto puede ser tanto útil como fuente de errores si no se comprende bien.

JavaScript considera falso: false, null, undefined, "" (cadena vacía), 0, NaN

// Verificación de existencia
if (player.weapon) {
    // weapon existe y no es falsy
    player.attack();
}

// Valores por defecto
let playerName = inputName || "Jugador Anónimo";

7.8 Funciones

Las funciones son uno de los conceptos más importantes en JavaScript, especialmente para el desarrollo de videojuegos donde necesitamos organizar el código en bloques reutilizables y manejables. JavaScript trata las funciones como “ciudadanos de primera clase”, lo que significa que pueden almacenarse en variables, pasarse como argumentos a otras funciones y retornarse como valores.

Esta flexibilidad es especialmente valiosa en videojuegos, donde frecuentemente necesitamos sistemas de callbacks para eventos, funciones que generen comportamientos aleatorios, o sistemas de actualización que procesen diferentes tipos de objetos del juego.

Declaración de Funciones

JavaScript ofrece varias formas de declarar funciones, cada una con sus propias características y casos de uso apropiados:

// Declaración tradicional
function calculateDamage(baseDamage, criticalHit) {
    if (criticalHit) {
        return baseDamage * 2;
    }
    return baseDamage;
}

// Expresión de función
let heal = function(amount) {
    player.health += amount;
    if (player.health > player.maxHealth) {
        player.health = player.maxHealth;
    }
};

// Arrow functions (ES2015+)
let movePlayer = (deltaX, deltaY) => {
    player.x += deltaX;
    player.y += deltaY;
};

// Arrow function con una expresión
let isAlive = (entity) => entity.health > 0;

// Arrow function sin parámetros
let generateRandomID = () => Math.random().toString(36);

Parámetros de Función

JavaScript es muy flexible en el manejo de parámetros de función, ofreciendo características modernas que simplifican el código y lo hacen más robusto. Esta flexibilidad es especialmente útil en videojuegos donde las funciones pueden necesitar comportamientos adaptativos según diferentes contextos de juego:

// Parámetros por defecto (ES2015+)
function createEnemy(health = 100, damage = 10) {
    return { health, damage };
}

// Rest parameters (ES2015+)
function logMessage(level, ...messages) {
    console.log(`[${level}]`, ...messages);
}

// Destructuring de parámetros
function updatePosition({ x, y }, { deltaX, deltaY }) {
    return { x: x + deltaX, y: y + deltaY };
}

Closures y Scope

Los closures (clausuras) son un concepto avanzado pero fundamental en JavaScript que permite a las funciones “recordar” el entorno en el que fueron creadas. En el contexto de videojuegos, los closures son especialmente útiles para crear sistemas como contadores de puntuación, generadores de identificadores únicos, o sistemas de estado que mantienen información privada.

Un closure se forma cuando una función interna hace referencia a variables de su función externa, y esa función interna se utiliza fuera de su contexto original. La función interna “cierra” sobre las variables del ámbito externo, manteniéndolas vivas incluso después de que la función externa haya terminado de ejecutarse:

function createCounter(initialValue = 0) {
    let count = initialValue;
    
    return {
        increment: () => ++count,
        decrement: () => --count,
        getValue: () => count
    };
}

let scoreCounter = createCounter(0);
scoreCounter.increment(); // 1
scoreCounter.increment(); // 2
console.log(scoreCounter.getValue()); // 2

7.9 Manejo de Excepciones

El manejo de excepciones en JavaScript funciona de manera muy similar a Java, utilizando los bloques try, catch y finally. En el desarrollo de videojuegos, el manejo adecuado de errores es crucial para crear experiencias robustas que no se rompan cuando ocurren situaciones inesperadas, como fallos al cargar recursos, errores de red, o datos corruptos en las partidas guardadas.

JavaScript permite lanzar cualquier tipo de objeto como excepción, aunque la práctica recomendada es usar objetos Error o crear errores personalizados con propiedades name y message descriptivas:

try {
    // Código que puede fallar
    let gameData = JSON.parse(savedGameString);
    loadGame(gameData);
} catch (error) {
    // Manejo del error
    console.error('Error loading game:', error.message);
    showErrorDialog('No se pudo cargar la partida guardada');
} finally {
    // Código que siempre se ejecuta
    hideLoadingSpinner();
}

// Lanzar excepciones personalizadas
function validatePlayerInput(input) {
    if (!input || input.trim() === '') {
        throw new Error('El nombre del jugador no puede estar vacío');
    }
    
    if (input.length > 20) {
        throw new Error('El nombre del jugador es demasiado largo');
    }
}

7.10 Almacenamiento de Datos en el Navegador

En el desarrollo de videojuegos web, frecuentemente necesitamos almacenar información que persista entre sesiones de juego. Esto incluye datos como puntuaciones máximas, configuraciones del jugador, progreso en niveles, preferencias de audio/video, y estados de partidas guardadas. JavaScript proporciona varios mecanismos para almacenar datos en el navegador del usuario, cada uno con características específicas que los hacen apropiados para diferentes situaciones.

Local Storage

Local Storage es una API moderna del navegador que permite almacenar datos de forma persistente en el dispositivo del usuario. A diferencia de las cookies, los datos del Local Storage no se envían automáticamente al servidor con cada petición HTTP, lo que los hace ideales para almacenar información que solo necesita el cliente.

Las características principales del Local Storage son:

  • Persistencia: Los datos permanecen hasta que el usuario los elimine manualmente o la aplicación los borre
  • Capacidad: Típicamente 5-10MB por dominio (mucho más que las cookies)
  • Sincronía: Las operaciones son síncronas y bloquean el hilo principal
  • Ámbito: Los datos son específicos del dominio y protocolo

Guardar datos

Local Storage solo acepta strings, por lo que para guardar números u objetos debemos convertirlos primero:

// Guardar strings y números
localStorage.setItem('playerName', 'Jugador1');
localStorage.setItem('highScore', '15000');

Para objetos más complejos, utilizamos JSON:

const gameConfig = {
    volume: 0.8,
    difficulty: 'normal'
};
localStorage.setItem('gameConfig', JSON.stringify(gameConfig));

Leer datos

Para recuperar los datos utilizamos getItem():

const playerName = localStorage.getItem('playerName');
const highScore = localStorage.getItem('highScore');

Para objetos, debemos parsear el JSON de vuelta:

const configString = localStorage.getItem('gameConfig');
const config = configString ? JSON.parse(configString) : null;

Verificar existencia

Podemos comprobar si existe un dato antes de intentar leerlo:

if (localStorage.getItem('playerName') !== null) {
    console.log('El jugador ya tiene un nombre guardado');
}

Eliminar datos

Para eliminar datos específicos:

localStorage.removeItem('playerName');

Para eliminar todos los datos del dominio:

localStorage.clear();

Información adicional

Podemos obtener información sobre el storage:

// Número de elementos almacenados
const cantidadItems = localStorage.length;

// Iterar sobre todas las claves
for (let i = 0; i < localStorage.length; i++) {
    const clave = localStorage.key(i);
    const valor = localStorage.getItem(clave);
    console.log(clave, valor);
}

Manejo de errores

// Siempre usar try-catch con localStorage
function guardarDatos(clave, valor) {
    try {
        localStorage.setItem(clave, valor);
        return true;
    } catch (error) {
        console.error('Error al guardar:', error);
        // Puede fallar si el storage está lleno o en modo privado
        return false;
    }
}

function cargarDatos(clave) {
    try {
        return localStorage.getItem(clave);
    } catch (error) {
        console.error('Error al cargar:', error);
        return null;
    }
}

Session Storage

Session Storage funciona de manera idéntica a Local Storage, pero los datos solo persisten durante la sesión actual del navegador. Cuando el usuario cierra la pestaña o ventana, los datos se eliminan automáticamente.

La API es exactamente la misma que localStorage:

// Guardar datos temporales
sessionStorage.setItem('tempData', 'valor temporal');
// Leer datos temporales
const tempData = sessionStorage.getItem('tempData');
// Eliminar datos temporales
sessionStorage.removeItem('tempData');
sessionStorage.clear();

Es especialmente útil para datos que no deben persistir entre sesiones:

// Estado actual del juego que se pierde al cerrar
sessionStorage.setItem('currentGameState', JSON.stringify(gameState));

Cookies

Las cookies son un mecanismo más antiguo pero siguen siendo útiles cuando el servidor necesita acceso a los datos o para configuraciones que deben enviarse automáticamente con las peticiones HTTP.

Características de las cookies:

  • Tamaño máximo de 4KB por cookie
  • Se envían automáticamente con cada petición HTTP al dominio
  • Tienen fecha de expiración configurable
  • Pueden configurarse como seguras (solo HTTPS) o HttpOnly

Escribir cookies

La sintaxis básica para crear una cookie:

document.cookie = "playerName=Jugador1";

Para añadir una fecha de expiración (por ejemplo, 30 días):

const fecha = new Date();
fecha.setTime(fecha.getTime() + (30 * 24 * 60 * 60 * 1000));
document.cookie = `highScore=15000; expires=${fecha.toUTCString()}; path=/`;

Leer cookies

Leer cookies requiere parsear la cadena que devuelve document.cookie:

function getCookie(nombre) {
    const value = `; ${document.cookie}`;
    const parts = value.split(`; ${nombre}=`);
    if (parts.length === 2) {
        return parts.pop().split(';').shift();
    }
    return null;
}
const playerName = getCookie('playerName');

Eliminar cookies

Para eliminar una cookie, establecemos una fecha de expiración en el pasado:

document.cookie = "playerName=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/";

Opciones de seguridad

Las cookies pueden incluir opciones adicionales de seguridad:

document.cookie = "sessionId=abc123; Secure; SameSite=Strict; path=/";

Utilidad para simplificar cookies

Dado que las cookies tienen una sintaxis más compleja, es útil crear funciones auxiliares:

const CookieUtil = {
    set(nombre, valor, dias = 7) {
        const fecha = new Date();
        fecha.setTime(fecha.getTime() + (dias * 24 * 60 * 60 * 1000));
        document.cookie = `${nombre}=${valor}; expires=${fecha.toUTCString()}; path=/`;
    },

    get(nombre) {
        const value = `; ${document.cookie}`;
        const parts = value.split(`; ${nombre}=`);
        return parts.length === 2 ? parts.pop().split(';').shift() : null;
    },

    delete(nombre) {
        document.cookie = `${nombre}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/`;
    }
};

Uso de la utilidad:

CookieUtil.set('playerLevel', '5', 30);  // 30 días
const level = CookieUtil.get('playerLevel');
CookieUtil.delete('playerLevel');

Comparación y Recomendaciones de Uso

Característica localStorage sessionStorage Cookies
Capacidad ~5-10MB ~5-10MB 4KB
Persistencia Hasta eliminar manualmente Solo la sesión Configurable
Envío al servidor No No Automático
API Síncrona Síncrona Manual

Recomendaciones para videojuegos:

  • localStorage: Configuraciones, puntuaciones, progreso del juego
  • sessionStorage: Estado temporal, datos de sesión
  • cookies: Autenticación, preferencias que el servidor necesita

Ejemplo Práctico: Sistema de Guardado

// Sistema simple de guardado para un juego
const GameSave = {
    save(gameData) {
        try {
            localStorage.setItem('gameSave', JSON.stringify(gameData));
            console.log('Juego guardado');
        } catch (error) {
            console.error('Error al guardar:', error);
        }
    },

    load() {
        try {
            const data = localStorage.getItem('gameSave');
            return data ? JSON.parse(data) : null;
        } catch (error) {
            console.error('Error al cargar:', error);
            return null;
        }
    },

    delete() {
        localStorage.removeItem('gameSave');
        console.log('Partida eliminada');
    },

    exists() {
        return localStorage.getItem('gameSave') !== null;
    }
};

// Uso
const gameData = {
    level: 5,
    score: 12500,
    lives: 3,
    powerUps: ['speed', 'jump']
};

GameSave.save(gameData);
const loadedData = GameSave.load();
if (GameSave.exists()) {
    console.log('Hay una partida guardada');
}

7.11 JavaScript Orientado a Objetos

Introducción

JavaScript es el lenguaje fundamental para el desarrollo de videojuegos web modernos. A diferencia de otros lenguajes que hemos estudiado, JavaScript utiliza un sistema de orientación a objetos basado en prototipos en lugar de clases tradicionales, aunque las versiones modernas incluyen sintaxis de clases que simplifican el desarrollo.

En este capítulo profundizaremos en las características únicas de JavaScript para videojuegos: la orientación a objetos basada en prototipos, las clases modernas ES2015+, y cómo trabajar con objetos dinámicos.

Orientación a Objetos en JavaScript

Diferencias con Java: Clases vs Prototipos

Mientras que Java utiliza un sistema basado en clases donde los objetos son instancias de una clase predefinida, JavaScript tradicionalmente utiliza prototipos. En JavaScript, cualquier objeto puede servir como prototipo para otros objetos, creando una cadena de herencia más flexible.

Creación Básica de Objetos
// Objeto literal simple
const enemigo = {
    vida: 100,
    damage: 15,
    atacar() {
        return `Enemigo ataca causando ${this.damage} puntos de daño`;
    }
};

// Crear otro enemigo basado en el prototipo
const goblin = Object.create(enemigo);
goblin.vida = 50;
goblin.damage = 8;
goblin.tipo = "Goblin";

console.log(goblin.atacar()); // "Enemigo ataca causando 8 puntos de daño"
console.log(goblin.vida);     // 50 (propio)
console.log(goblin.toString); // function (heredado de Object)

En este ejemplo, goblin hereda el método atacar() del objeto enemigo. Cuando JavaScript busca una propiedad en goblin y no la encuentra, automáticamente busca en su prototipo (enemigo), y si no la encuentra ahí, continúa hacia arriba en la cadena hasta llegar a Object.prototype.

Herencia con Prototipos

Para crear una jerarquía de herencia más compleja con prototipos, necesitamos establecer la cadena de prototipos manualmente:

// Prototipo base
const personajeBase = {
    mover(x, y) {
        this.x += x;
        this.y += y;
        console.log(`${this.nombre} se mueve a (${this.x}, ${this.y})`);
    },
    
    toString() {
        return `${this.nombre}: vida=${this.vida}, pos=(${this.x},${this.y})`;
    }
};

// Prototipo específico para jugadores
const prototipoJugador = Object.create(personajeBase);
prototipoJugador.atacar = function(objetivo) {
    return `${this.nombre} ataca a ${objetivo.nombre} por ${this.fuerza} puntos`;
};

prototipoJugador.levelUp = function() {
    this.nivel++;
    this.fuerza += 5;
    console.log(`${this.nombre} sube a nivel ${this.nivel}!`);
};

// Crear instancia de jugador
const jugador = Object.create(prototipoJugador);
jugador.nombre = "Aragorn";
jugador.vida = 100;
jugador.x = 0;
jugador.y = 0;
jugador.fuerza = 20;
jugador.nivel = 1;

jugador.mover(5, 3);  // Método heredado de personajeBase
jugador.levelUp();    // Método del prototipoJugador

Este ejemplo muestra una cadena de herencia: jugador → prototipoJugador → personajeBase → Object.prototype. Cada nivel puede añadir o sobrescribir métodos.

Acceso a Propiedades y Métodos de Objetos

JavaScript permite acceder a las propiedades de un objeto tanto con notación punto como con corchetes:

const config = {
    sonido: true,
    volumen: 0.8,
    idioma: "es"
};

// Acceso con notación punto (recomendado)
console.log(config.sonido);     // true
config.volumen = 0.5;

// Acceso con corchetes (útil para propiedades dinámicas)
console.log(config["idioma"]);  // "es"
const propiedad = "volumen";
console.log(config[propiedad]); // 0.5

// Añadir nuevas propiedades dinámicamente
config.dificultad = "normal";
config["maxJugadores"] = 4;

La notación con corchetes es especialmente útil cuando el nombre de la propiedad está en una variable o cuando queremos iterar sobre las propiedades del objeto.

Iteración sobre Propiedades
const inventario = {
    espada: 1,
    pocion: 5,
    oro: 150
};

// Iterar sobre todas las propiedades propias
for (let item in inventario) {
    console.log(`${item}: ${inventario[item]}`);
}

// Verificar si una propiedad existe
if ("oro" in inventario) {
    console.log("El jugador tiene oro");
}

// Obtener todas las claves como array
const items = Object.keys(inventario);
console.log(items); // ["espada", "pocion", "oro"]

// Obtener todos los valores como array
const cantidades = Object.values(inventario);
console.log(cantidades); // [1, 5, 150]

El bucle for...in itera sobre todas las propiedades enumerables del objeto, incluyendo las heredadas del prototipo. Si solo queremos las propiedades propias del objeto, podemos usar Object.keys() o verificar con hasOwnProperty().

Diferencias entre null y undefined

JavaScript tiene dos valores especiales para representar “nada”:

let jugador = null;        // Ausencia intencional de valor
let powerUp;              // undefined - no inicializada

// Verificación segura antes de usar objetos
if (jugador) {
    jugador.mover(5, 0);  // Solo se ejecuta si jugador no es null/undefined
}

// Verificación más específica
if (jugador !== null && jugador !== undefined) {
    jugador.atacar();
}

// Operador de optional chaining (ES2020+)
jugador?.mover?.(5, 0);   // Solo ejecuta si jugador y mover existen

Esta diferencia es importante en videojuegos donde objetos pueden existir o no (enemigos destruidos, power-ups recogidos, etc.).

Función Constructor (Patrón Tradicional)

Antes de ES2015, el patrón más común para crear “clases” era usar funciones constructor:

function Jugador(nombre, x, y) {
    this.nombre = nombre;
    this.x = x;
    this.y = y;
    this.vida = 100;
    this.velocidad = 5;
}

// Métodos compartidos en el prototipo
Jugador.prototype.mover = function(deltaX, deltaY) {
    this.x += deltaX * this.velocidad;
    this.y += deltaY * this.velocidad;
    console.log(`${this.nombre} se mueve a (${this.x}, ${this.y})`);
};

Jugador.prototype.recibirDanio = function(cantidad) {
    this.vida -= cantidad;
    if (this.vida <= 0) {
        console.log(`${this.nombre} ha sido derrotado!`);
        return false;
    }
    return true;
};

// Uso del constructor
const player1 = new Jugador("Aragorn", 10, 20);
player1.mover(2, 3); // "Aragorn se mueve a (20, 35)"

// IMPORTANTE: Siempre usar 'new'
const player2 = Jugador("Legolas", 0, 0); // ¡ERROR! Sin 'new'
// Sin 'new', en modo no estricto 'this' apunta al objeto global (creando
// globales accidentales); en modo estricto lanza un TypeError

La función constructor se ejecuta cuando usamos el operador new. Este operador crea un nuevo objeto, establece this para que apunte a ese objeto, y al final devuelve el objeto automáticamente.

Los métodos se definen en Jugador.prototype para que todos los objetos creados con este constructor compartan las mismas funciones en memoria, siendo más eficiente que definir los métodos dentro del constructor.

Herencia con Funciones Constructor
// Constructor padre
function Personaje(nombre, vida) {
    this.nombre = nombre;
    this.vida = vida;
}

Personaje.prototype.saludar = function() {
    return `Hola, soy ${this.nombre}`;
};

// Constructor hijo
function Guerrero(nombre, vida, fuerza) {
    Personaje.call(this, nombre, vida); // Llamar al constructor padre
    this.fuerza = fuerza;
}

// Establecer herencia de prototipos
Guerrero.prototype = Object.create(Personaje.prototype);
Guerrero.prototype.constructor = Guerrero;

// Añadir métodos específicos del guerrero
Guerrero.prototype.atacar = function() {
    return `${this.nombre} ataca con fuerza ${this.fuerza}`;
};

const conan = new Guerrero("Conan", 150, 25);
console.log(conan.saludar()); // "Hola, soy Conan" (heredado)
console.log(conan.atacar());  // "Conan ataca con fuerza 25" (propio)

Este patrón requiere tres pasos: llamar al constructor padre con call(), establecer la cadena de prototipos con Object.create(), y restaurar la propiedad constructor.

Clases ES2015+ (Sintaxis Moderna)

Las clases modernas ofrecen una sintaxis más familiar para desarrolladores de Java, pero internamente siguen usando prototipos. La palabra clave class es “azúcar sintáctico” sobre el sistema de prototipos existente.

class GameObject {
    constructor(x, y) {
        this.x = x;
        this.y = y;
        this.activo = true;
    }

    actualizar(deltaTime) {
        // Lógica base de actualización que pueden sobrescribir las subclases
        if (!this.activo) return;
    }

    destruir() {
        this.activo = false;
        console.log("Objeto destruido");
    }
}

class Enemigo extends GameObject {
    constructor(x, y, tipo) {
        super(x, y);  // Llamada obligatoria al constructor padre
        this.tipo = tipo;
        this.vida = 50;
        this.velocidad = 2;
        this._direccion = { x: 1, y: 0 };
    }

    // Getter: se accede como una propiedad, pero ejecuta código
    get estaVivo() {
        return this.vida > 0;
    }

    // Setter: permite validación cuando se asigna un valor
    set vida(valor) {
        this._vida = Math.max(0, valor); // No puede ser negativa
        if (this._vida === 0) {
            this.destruir();
        }
    }

    get vida() {
        return this._vida;
    }

    actualizar(deltaTime) {
        super.actualizar(deltaTime); // Llamar al método padre
        if (this.estaVivo) {
            this.x += this._direccion.x * this.velocidad;
            this.y += this._direccion.y * this.velocidad;
        }
    }

    // Método estático: pertenece a la clase, no a las instancias
    static crearOrc() {
        const orc = new Enemigo(0, 0, "Orc");
        orc.vida = 80;
        orc.velocidad = 1.5;
        return orc;
    }
}

Explicación de elementos clave:

  • constructor: Método especial que se ejecuta al crear una instancia. Es equivalente a la función constructor del patrón tradicional.

  • super(): En el constructor, llama al constructor de la clase padre. Debe ser la primera línea antes de usar this.

  • extends: Establece herencia entre clases. Enemigo extends GameObject significa que Enemigo hereda de GameObject.

  • get estaVivo(): Es un getter, una propiedad calculada que se accede como enemigo.estaVivo pero ejecuta código. No necesita paréntesis al accederla.

  • set vida(valor): Es un setter, permite interceptar y validar cuando se asigna un valor a la propiedad. Se usa como enemigo.vida = 100.

  • static crearOrc(): Método estático que pertenece a la clase, no a las instancias. Se llama como Enemigo.crearOrc().

  • super.actualizar(deltaTime): En un método, llama al método del mismo nombre en la clase padre.

Ejemplo de Uso
// Crear enemigos usando el constructor normal
const goblin = new Enemigo(10, 20, "Goblin");
goblin.vida = 30; // Usa el setter, valida que no sea negativo

// Usar el getter
if (goblin.estaVivo) {  // No necesita paréntesis
    console.log("El goblin sigue vivo");
}

// Crear enemigo usando método estático
const orc = Enemigo.crearOrc();

// Polimorfismo: ambos objetos tienen el mismo interfaz
const enemigos = [goblin, orc];
enemigos.forEach(enemigo => {
    enemigo.actualizar(16); // Cada uno usa su propia implementación
});
Ventajas de las Clases ES2015+
  1. Sintaxis más clara: Más familiar para desarrolladores de otros lenguajes orientados a objetos.

  2. Herencia simplificada: extends y super() son más directos que manipular prototipos manualmente.

  3. Getters y setters integrados: Permiten crear propiedades con lógica de validación o cálculo.

  4. Métodos estáticos: Para funciones que pertenecen a la clase conceptualmente pero no necesitan una instancia.

  5. Mejor soporte de herramientas: Los IDEs y linters comprenden mejor las clases modernas.

La sintaxis de clases es la recomendada para proyectos nuevos de videojuegos, especialmente cuando trabajamos en equipo o cuando el proyecto va a crecer en complejidad. Sin embargo, entender el sistema de prototipos subyacente nos ayudará a comprender mejor cómo funciona JavaScript internamente y a debuggear problemas más efectivamente.