El Aetheria Game Engine, programa que se encarga de poner a funcionar el juego de Aetheria, está escrito en Java, y lo que hace es cargar todos los objetos que compondrán el juego de ficheros que tienen un formato determinado. Estos objetos interactúan entre sí y con el jugador. La mayoría de ellos se pueden especificar dando una serie de datos en su correspondiente fichero: por ejemplo, de una piedra podemos conocer su aspecto, volumen, peso y algunos datos más, y toda esa información aparecerá en el fichero. ¿Pero qué pasa si en vez de una piedra queremos crear la Legendaria Espada de Arlis, por poner un ejemplo, una espada que es capaz de absorber la energía vital de los enemigos y convertirla en energía mágica que transmite a su portador? Todo eso podría estar programado en el engine, y luego se podría especificar en el fichero de ese objeto que "es la Espada de Arlis", y que el programa se encargara de que hiciese lo que tiene que hacer. Pero, ¿acaso no sería mucho más interesante que cada objeto pudiera, por sí solo, mantener propiedades como ésas o incluso mucho más complejas, sin necesidad de que el programa las soportara antes? Si se da a los objetos la suficiente autonomía como para definir sus propias reglas de juego, se puede conseguir una flexibilidad mucho mayor que contando sólo con parámetros y funciones preprogramadas. Se pueden crear objetos realmente únicos, y se puede utilizar el engine de Aetheria para soportar juegos totalmente distintos.
¿Cómo se consigue, entonces, que un objeto pueda poner sus propias reglas? Dejándole ejecutar código. La idea es tener un lenguaje de programación que se pueda introducir en los objetos, y que pueda llamar a las funciones que existen en el engine de Aetheria y hacer operaciones por sí sólo, sobre todo combinando éstas. Por ejemplo, la espada de Arlis podría tener un mini-programa que se ejecutase al dañar a un enemigo, multiplicando el daño hecho por algún factor y añadiéndolo a la energía mágica del jugador mediante alguna función para manejar dicha energía que tenga el engine de Aetheria.
Ese lenguaje es, de momento, el Ensamblador Virtual de Aetheria (EVA). Lo he implementado como un lenguaje de tipo ensamblador sencillamente porque no creo tener los conocimientos suficientes para crear un intérprete de un lenguaje de alto nivel. La idea es soportar un mini-lenguaje de alto nivel más tarde (no hay ningún problema en soportar varios lenguajes a la vez, ya ocuparán bastante memoria los objetos del juego como para preocuparse de la que ocupe ese soporte). Si alguien puede y quiere programar un mini-intérprete de alto nivel en Java, puede dirigirse a la sección de colaboraciones.
El código en EVA se puede introducir en el fichero de especificación de cualquier objeto. Basta por incluirlo tras un número que indica que empieza un código EVA. A continuación van todas las funciones EVA que soporte ese objeto. ¿Cuándo se ejecutan dichas funciones? Depende de su nombre: cuando se den ciertas situaciones en el juego, se buscará un código EVA con ese nombre en el objeto y se ejecutará. Así, el código EVA se puede utilizar para:
- Comandos extra: comandos del juego que sólo sean reconocidos en una determinada habitación o con un determinado objeto.
- Eventos: reacciones de objetos ante estímulos externos, como por ejemplo el efecto de la espada descrita antes.
Cabe destacar que los hechizos son también objetos con estado que harán su efecto gracias al código.
He aquí un ejemplo de código EVA:
Fichero completo de una piedra que cuenta hasta diez al cogerla, incluyendo su código EVA. Sí, ya sé que una piedra que cuente hasta diez al cogerla no es muy útil (aunque a ver cuánto dinero se podría hacer con ella en un circo) pero es una prueba...
099 Piedra rojiza, el primer objeto con código. 001 000006 003 0 004 Piedra rojiza 010 0&0&Es una piedra rojiza de prueba. 011 0&0&piedra rojiza 012 0&0&piedras rojizas 013 0 014 Piedra rojiza$Piedra 015 Piedras rojizas$Piedras 016 4 017 4 080 .INIT $r0:0 .DATA HOLA:0 .CODE event_get: loadi $a0, ¡Atención! La piedra va a contar hasta 10. function escribir function newline loadi $r0, 0 loadi $r1, 10 bucle: addi $r0, $r0, 1 mov $a0, $r0 function escribir bne $r0, $r1, bucle function newline end 081 fin codigo
Registros:
Como este ensamblador no corresponde a ninguna máquina física, los registros no tienen tamaño fijo, no están limitados a almacenar un valor entero o flotante: almacenan cadenas de caracteres. Esas cadenas de caracteres pueden representar un valor entero o flotante, o una cadena "real", según nuestras necesidades.
Los registros son:
$r0..$r99 Propósito general. Usados para cualquier tipo de operación. En el registro $r0 se solerá poner el valor 0.
$a0..$a9 Parámetros para método. Para pasar argumentos a los métodos que ejecutemos del engine de Aetheria.
$v0..$v9 Valores de retorno. Aquí escribirá el engine de Aetheria los valores de retorno de los métodos.
$pc Contador del programa. Número de instrucción que se está ejecutando. No debería ser necesario conocer su existencia para programar en EVA.
$ra Dirección de retorno de subrutina. Dirección a la que se vuelve al terminar una subrutina con "return". Tampoco debería ser necesario.
$Exc Registro de excepciones. Aquí escribirá el engine de Aetheria si se produce una excepción al ejecutar un método que ha pedido el programador en EVA, y escribirá el programador en EVA si quiere que sea su código el que produzca una excepción y la mande al juego.
$Obj Registro de objeto (para métodos). Para especificar qué objeto ejecuta un método del engine de Aetheria. Los objetos se identifican por su número o ID, esto es lo que hay que poner tanto aquí como en los registros $a0..$a9 si queremos pasar objetos como parámetros.
Segmentos:
El código EVA tiene segmentos de inicialización, de datos y de código.
El formato es el siguiente:
.INIT
(líneas de inicialización)
.DATA
(líneas de datos)
.CODE
(líneas de código)
Puede haber varios segmentos de un mismo tipo, aunque el efecto será exactamente el mismo que si se juntara todo en un solo segmento.
Segmento de inicialización (INIT):
El segmento INIT se utiliza para especificar los valores iniciales de uno o varios registros. Aquellos registros que no aparezcan en INIT (salvo el PC) contendrán un valor indefinido al ejecutar el código.
Ejemplo:
.INIT $r0:0 $r1:27 $r5:Hola, Mundo.
Los registros $r0, $r1 y $r5 tienen los valores iniciales que se especifican tras los dos puntos cada vez que Aetheria llama a cualquier rutina del código. El mismo efecto lo conseguiremos si al principio de todas las subrutinas que puedan ser llamadas directamente por el juego colocamos instrucciones de carga para inicializar esos registros.
Segmento de datos (DATA):
El segmento DATA se utiliza para inicializar las líneas de memoria. Como no estamos en una máquina real, no tenemos realmente una unidad de direccionamiento de memoria, así que cualquier cadena de caracteres se puede utilizar como dirección. Esto nos permite nombrar las direcciones según la variable asociada.
Ejemplo:
.DATA id_hechizo:0047 habitacion:24 jugador:Hegel el Enano.
Aquí, por ejemplo, la dirección de memoria "jugador" contiene el dato "Hegel el Enano".
Importante: Además de las líneas de memoria que inicialicemos
nosotros en el segmento DATA, y las que podamos inicializar después mediante
instrucciones de almacenamiento en memoria, pueden estar presentes otras líneas
de memoria al empezar a ejecutarse el código. Son líneas que añade
Aetheria para que desde el código EVA podamos acceder a la información
que necesitaremos para interactuar con los objetos del juego. (se puede observar
que con lo que hemos visto sólo tenemos datos constantes).
Estas líneas de información que añade Aetheria dependen
de la rutina que se esté ejecutando, y para saber a cuáles se
tiene acceso hay que consultar la especificación de la subrutina.
Ejemplo: Al ejecutar la subrutina "event_get"
de un item, que se ejecuta cuando alguien lo coge, contamos con la línea
"this", que contiene la ID del propio objeto. Podemos usar esa información
para llamar a métodos del objeto. (si el objeto tuviera como ID 23, sería
como si una línea de .DATA pusiera this:23,
a efectos de event_get)
Segmento de código (CODE):
El segmento de código es el que contiene las instrucciones ejecutables del Ensamblador Virtual de Aetheria.
El formato de un trozo de código podría ser:
etiqueta1: instruccion1 instruccion2 instruccion3 etiqueta2: instruccion4 instruccion5 end
La ejecución de las instrucciones es secuencial: si Aetheria llamara a la subrutina llamada "etiqueta2" se ejecutarían las instrucciones 4 y 5, y si llamara a la llamada "etiqueta1" se ejecutarían 1, 2 y 3 (salvo en el caso de que las instrucciones contengan saltos). Cada instrucción debe estar en una línea diferente.
La instrucción end es necesaria para devolver el control al juego, y si no se pone provocará errores.
Nota: Como el "parser" es bastante limitado, al menos de momento no es posible dejar ninguna línea del segmento de código sin instrucción. Es decir, sería ilegal una sintaxis del tipo:
etiqueta1: etiqueta2: etiqueta3: instruccion1 instruccion2 etiqueta4: instruccion3 end
Sin embargo, si se quiere disponer de un código que haga lo mismo que éste (en ocasiones puede ser útil tener varias etiquetas en la misma instrucción, para que un mismo código se ejecute frente a varios eventos, por ejemplo) se puede conseguir así:
etiqueta1: nop etiqueta2: nop etiqueta3: instruccion1 instruccion2 etiqueta4: instruccion3 end
(la instrucción nop no hace nada)
Instrucciones:
El juego de instrucciones se parece mucho a los de procesadores RISC con arquitectura de carga/almacenamiento tipo MIPS o DLX. A los que conozcan el repertorio de instrucciones de algún procesador de este tipo, especialmente el MIPS, les resultará familiar, aunque el EVA es mucho más simple al no tener nada que ver con una máquina física.
Instrucción nop o noop:
No hace nada.
Formato: nop
Instrucción load o ld:
Carga datos de las líneas de memoria a un registro.
Formato: ld registro, etiqueta / load registro, etiqueta
Ejemplo: load $r4, this
(carga en el registro $r4 lo que hay en la línea de memoria etiquetada
"this").
Instrucción loadi o li:
Carga un dato dado en la instrucción a un registro.
Formato: li registro, imm / loadi registro, imm
Ejemplo: loadi $r3, 47
(pone en el registro $r3 el valor 47)
Instrucción store o sd:
Guarda el dato de un registro a una línea de memoria.
Formato: sd registro, etiqueta / store registro, etiqueta
Ejemplo: store $r3, dato
(en la línea de memoria "dato" copia el dato del registro $r3)
Nota: la línea de memoria no tiene que existir (estar inicializada en el segmento de datos o por el juego): si no existe, se crea automáticamente.
Instrucción mov o move:
Mueve datos de un registro a otro.
Formato: mov rt, rs / move rt, rs (copia el dato de rs a rt)
Ejemplo: move $a0, $r2
(pone en $a0 el mismo dato que hay en $a2)
Instrucciones add, sub, mult y div:
Sumar, restar, multiplicar y dividir (respectivamente) los datos ENTEROS que hay en los dos registros fuente y poner el resultado en el registro resultado.
Formato: add (sub/mult/div) rd, rs, rt (suma los enteros que hay en rs y en rt y coloca el resultado en rd)
Ejemplo: div $a0, $r5, $r6
(hace dato[$a0] = dato[$r5] / dato[$r6] )
Nota: si los datos de alguno de los registros fuente no son enteros, no se
escribirá el resultado en el registro destino rd, y el juego nos transmite
una excepción, colocando en el registro de excepción $Exc el valor
"NotANumber".
Si intentamos dividir por cero, sucede lo mismo, siendo la excepción
"DivisionByZero".
Instrucciones addi, subi, multi y divi:
Sumar, restar, multiplicar y dividir (respectivamente) el dato ENTERO que hay en el registro fuente y el entero que se especifica en la instrucción.
Formato: addi (subi/multi/divi) rd, rs, imm (suma el entero que hay en rs con el entero imm y coloca el resultado en rd)
Ejemplo: addi $r1, $r1, 1
(hace dato[$r1] = dato[$r1] + 1)
Del mismo modo que en el caso anterior, si el registro no contiene un entero o el dato inmediato no lo es se producirá la excepción "NotANumber", y si intentamos dividir por cero la excepción "DivisionByZero".
Instrucciones fadd, fsub, fmult, fdiv, faddi, fsubi, fmulti y fdivi:
Hacen exactamente lo mismo que add, sub, mult, div, addi, subi, multi y divi, respectivamente; pero con números en punto flotante en lugar de enteros. Cualquier representación como strings de números en punto flotante que sea soportada por Double.valueOf(elString).doubleValue() en Java es válida.
Instrucciones bne, beq, bge, bgt, ble y blt:
Son las instrucciones de salto condicional: saltan a una dirección dada si los datos de los dos registros también dados cumplen una determinada condición.
Formato: beq (bne/bge/bgt/ble/blt) rs, rt, label
Ejemplo: beq $r1, $r2, subrutina1
(si el dato de $r1 es igual al de $r2, la siguiente instrucción que se
ejecuta es la etiquetada "subrutina1", si no, se sigue la ejecución
secuencialmente)
beq: salto si los datos de los dos registros son iguales.
bne: si son distintos.
bge: si dato[rs] >= dato[rt].
bgt: si dato[rs] > dato[rt].
ble: si dato[rs] <= dato[rt].
blt: si dato[rs] < dato[rt].
Nota: las comparaciones de igualdad, beq y bne, son comparaciones de cadenas, es decir, funcionarán tanto para números enteros como para cadenas genéricas. El resto de comparaciones son para números enteros, y provocarán la excepción NotANumber ante cualquier valor que no sea un entero.
Instrucción j:
Es el salto incondicional (goto). Salta a la dirección dada.
Formato: j label
Ejemplo: j bucle
(salta a la instrucción etiquetada "bucle")
Instrucción jal:
Es la llamada a subrutina (jump and link). Salta a la dirección dada, y guarda la dirección de la instrucción siguiente a jal en $ra para poder volver más tarde de la subrutina con la instrucción return. (el programador no se tiene por qué preocupar de $ra si todas las subrutinas son subrutinas hoja y no hay recursividad, pues jal y return lo manejan implícitamente)
Formato: jal label
Ejemplo: j subrutina1
(llama a subrutina1, y podremos hacer "return" para volver a donde
estamos)
Instrucción return:
Es la instrucción de retorno de subrutina. Vuelve al punto donde invocamos la subrutina, es decir, a la dirección guardada en $ra por la última instrucción jal.
Formato: return
Instrucción push:
Pone el contenido del registro dado en la pila.
Formato: push registro
Ejemplo: push $ra
(pone el dato del registro $ra en la pila)
Instrucción pop:
Pone en el registro dado el dato más recientemente insertado en la pila de los que en ella hay, borrándolo de la pila.
Formato: pop registro
Ejemplo: pop $ra
(pone en $ra el dato de la cima de la pila)
Instrucción fun o function:
Llama a una función de Aetheria. Es la instrucción principal para comunicarse con el juego.
- El nombre del método llamada es el que se especifica en la instrucción.
- El objeto que invoca el método es aquél cuya ID está
guardada en $Obj.
- Los parámetros para el método, si los tiene, se han de guardar
en $a0, $a1, ..., por orden.
- Los valores de retorno, si los hay, aparecerán en $v0, $v1... y las
posibles excepciones en $Exc.
Formato: fun método / function método
Ejemplo: fun getState
(pondrá en $v0 el valor del estado del objeto cuya ID hemos almacenado
previamente en $Obj)
Instrucción exc o exception:
Lanza al juego una excepción de tipo EVASemanticException con el mensaje guardado en $Exc.
Formato: exc / exception
Métodos llamables por function:
La instrucción "function" nos permite utilizar métodos de Aetheria. He aquí una lista de los que de momento están soportados de momento.
La sintaxis que uso para denotar los métodos es:
clase :: retorno nombre_metodo ( arg1, arg2, ... ) throws excepcion1, excepcion2 ...
El nombre de las excepciones es la cadena que aparecerá en $Exc si se dan. Si en lugar de una clase aparece static, quiere decir que no nos tenemos que preocupar de a qué objeto corresponde el método, no necesitamos poner nada en $Obj (no quiere decir que sea un método estático en el programa Java)
[static] :: void escribir ( String s )
Saca por pantalla, o cualquiera que sea la salida de Aetheria, la cadena guardada en $a0.
[static] :: void newline ( void )
Saca por pantalla, o cualquiera que sea la salida de Aetheria, un salto de línea.
Entity :: int getstate ( void ) throws NotANumber, NotAnIDNumber
Devuelve en $v0 el estado del objeto cuya ID está en $Obj.
Entity :: void setstate ( int newstate , long tuleft ) throws NotANumber , NotAnIDNumber
Cambia el estado del objeto al nuevo estado dado durante las unidades de tiempo dadas.