Tabla de contenidos
¿Qué es VR-SCRIPT?
VR-SCRIPT es el lenguaje de scripting nativo de los MSXVR y con el que se pueden desarrollar cualquier tipo de aplicaciones y juegos. Además de la versatilidad del script, permite una programación orientada a objetos entre otras particularidades especiales. Este lenguaje utiliza una sintaxis similar a la del C/C++. Dentro del MSXVR se lanzan programas que ejecutan estos scripts. Estos programas tienen acceso a un conjunto de funciones nativas para poder desarrollar todo tipo de tareas.
Palabras reservadas
_change, _call, _implements, _instanceof, _set, _unset, _get, _state, _sizeof, _eval, _run, break, case, class, constants, continue, default, defines, delete, do, else, exit, externals, for, function, false, float, globals, if, import, implements, int, in, null, new, namespace, native, properties, pointer, protected, private, public, partial, package, return, rep, state, switch, string, sizeof, select, true, this, typeof, until, virtual, while, when
Estructura del archivo
VR-SCRIPT opera con archivos individuales, donde cada archivo hace referencia a una clase (aunque puede incluir subclases). Por defecto el sistema no es sensible a mayúsculas/minúsculas (aunque se puede configurar para que sí lo sea) y permite el uso de directivas de preprocesamiento. La estructura básica de un archivo sería:
namespace IDENTIFIER
import [package] STRING
class IDENTIFIER[<INTERFACE>] [partial STRING [, STRING, ...]] [implements STRING [, STRING, …]]
{
globals:
IDENTIFIER [: TYPE] = VALUE;
...
externals:
IDENTIFIER;
...
defines:
IDENTIFIER [: TYPE] [= VALUE];
...
[private | public | protected] constants:
IDENTIFIER [: TYPE] [= VALUE];
...
[private | public | protected] properties:
IDENTIFIER [: TYPE] = VALUE;
...
[private | public | protected] {function | virtual} NAME ([[&]ARG,[&]ARG,…]) [: TYPE] {}
[private | public | protected] state [virtual] STRING[:] {}
}
Tipos
Existen diferentes tipos de datos en VR-SCRIPT. Son los siguientes:
Nombre | TYPE | Tamaño (Bytes) | Rango |
---|---|---|---|
bool | TYPE_BOOL | 1 | True | False |
byte | TYPE_BYTE | 1 | -128 hasta 127 |
ubyte | TYPE_UBYTE | 1 | 0 hasta 255 |
short | TYPE_SHORT | 2 | -32768 hasta 32767 |
ushort | TYPE_USHORT | 2 | 0 hasta 65535 |
int | TYPE_INT | 4 | -2147483648 hasta 2147483647 |
uint | TYPE_UINT | 4 | 0 hasta 4294967295 |
long | TYPE_LONG | 8 | -263 hasta 263-1 |
ulong | TYPE_ULONG | 8 | 0 hasta 264 |
float | TYPE_FLOAT | 4 | 1.2E-38 hasta 3.4E+38 (6 dec) |
double | TYPE_DOUBLE | 8 | 2.3E-308 hasta 1.7E+308 (15 dec) |
list | TYPE_LIST | – | |
buffer | TYPE_BUFFER | – | |
string | TYPE_STRING | – | |
pointer | TYPE_POINTER | – | |
object | TYPE_OBJECT | – |
Ejemplos de inicialización:
A = 5;
B = 6.5;
C = "Hola";
D = [A, B, "CD", [1, 2]];
E = null;
// Defino un Buffer de 32 bytes
F = {@32@}
// Defino un Buffer de 9 bytes con 9 valores
G = {5,6,4,3,4,5,6,3,2}
// Defino un Buffer de 128 bytes con 4 valores
H = {@128@10, 4, 5, 3}
// Inicializo la variable I como booleano
I = true;
sizeof / typeof
Con «sizeof» podemos saber el tamaño o cantidad de elementos y con «typeof» el tipo de la variable o constante.
Ejemplos:
b = sizeof(5); | b = 4 | Tamaño del entero en bytes |
c = sizeof(6.2); | c = 4 | Tamaño del flotante en bytes |
d = sizeof(«Hola»); | d = 4 | Número de caracteres |
e = sizeof(null); | e = 0 | Valor de NULL |
a = «Hola»; | b = TYPE_STRING | Tipo de la variable \ |
b = typeof(a);
Es posible programar forzando la especificación de tipo. Para ello usaremos la sintaxis:
#pragma typejuggling=off
Esto obligará a determinar el tipo en cada declaración de constantes, propiedades, parámetros de funciones y valores de retorno de función. También es posible definir variables con un tipo concreto usando:
properties:
a = int;
b = byte;
c = bool;
Representación numérica
Veamos diferentes formas de expresar el mismo número:
Inicialización | Tipo | Bytes Tamaño |
---|---|---|
a = 15; | Entero (int) | 4 |
a = 15L; | Entero (long) | 8 |
a = 15.0f; | Flotante (float) | 4 |
a = 15.0; | Flotante (float) | 4 |
a = 15.0L; | Flotante doble (double) | 8 |
a = 0xF; | Hexadecimal (int) | 4 |
a = 0b1111; | Binario (int) | 4 |
a = 0o17; | Octal (int) | 4 |
a = 0xFL; | Hexadecimal (long) | 8 |
a = 0b1111L; | Binario (long) | 8 |
a = 0o17L; | Octal (long) | 8 |
Propiedades
Las propiedades son variables y constantes, que tienen un identificador (cadena de caracteres) y un ámbito de acceso, bien sea global (entre clases) o local a la clase.
Variables y constantes globales
Es posible definir variables globales accesibles desde cualquier instancia en ejecución. Las variables globales existen en el momento en que un script las crea y mientras la instancia de dicho script perdure activa (no se borre). Para crearlas es necesario definirlas en la sección GLOBALS de la clase.Del mismo modo, es posible crear etiquetas (o variables globales de solo lectura) accesibles desde cualquier script. Para ello usaremos la sección DEFINES dentro de la clase del script. Una vez creadas, al igual que las variables globales, son accesibles desde cualquier script en ejecución hasta que
el objeto creador sea eliminado del sistema.Las variables y constantes globales pueden tener el mismo nombre en diferentes namespaces (ámbitos) sin que haya colisión. Véase NameSpaces. Dentro del mismo ámbito podemos tener diferentes scripts que creen las mismas variables o constantes globales. Eso no es un error y la variable/constante
global no se creará dos o más veces, siempre se utilizará la misma.
Ejemplo:
namespace com.retromaking.swords;
class Slurp implements Enemy
{
globals:
DEFAULT_HIT = 3;
defines:
DEFAULT_LIFE = 100;
DEFAULT_ITEM = "Random";
}
Properties y Constants
Las propiedades de clase solo son accesibles desde el script que las contiene. Aunque tengamos varias instancias de la misma clase funcionando a la vez, cada instancia tendrá sus propios valores.Estas propiedades pueden ser de lectura/escritura o constantes (solo lectura). En el caso de ser constantes se crearán bajo la sección CONSTANTS, y en caso de ser de lectura/escritura bajo la sección PROPERTIES. En ambos casos, podemos definir si su acceso es:
- PRIVATE: Solo accesible desde la clase que las crea.
- PROTECTED: Accesible por la clase que las crea e hijas.
- PUBLIC: El caso por omisión, permite el acceso desde cualquier clase.
Para más información, véase Herencia de clases.
Locales
Son aquellas que se definen en funciones o estados y solo son accesibles desde esa función o estado.
class A
{
properties:
X = 5;
function A()
{
_p = 6 + X;
}
function B()
{
Print(_p);
_c = 8 + X;
}
}
Vemos que la propiedad es accesible desde cualquier función. Sin embargo, en el caso de «_p», que se ha definido en la función A, obtendríamos un error de compilación ya que «_p» no existe en la función B.
Namespaces
La función del namespace es definir ámbitos de uso para las variables globales, etiquetas y clases. Si definimos una clase con un namespace, las variables globales y etiquetas creadas se asociarán bajo dicho namespace, por lo que no colisionarán con variables globales o etiquetas con el mismo nombre definidas en otros namespaces.
Acceso por nombre a propiedades
Mediante las sentencias _get y _set podemos leer o escribir valores en propiedades de instancia.
_get ("a");
En el caso de que «a» no exista como propiedad, se dará un aviso de propiedad no encontrada y la función _get devolverá el valor entero cero.
_set ("a", 5);
Si la propiedad ‘a’ no existe, se creará y la función devolverá el valor 2.Si la propiedad ‘a’ ya existe, se asignará el valor y la función devolverá el valor 1.Si la propiedad ‘a’ ya existe y es una constante, no se asignará el valor y la función devolverá el valor 0.Con la sentencia _unset podemos eliminar las propiedades creadas.
_unset ("a");
En caso de que la propiedad exista, devolverá verdadero y se eliminará. Si por el contrario, la propiedad no existe, devolverá falso.
Funciones
Mediante las funciones creamos el comportamiento de las clases. Es el mecanismo con el cual podemos escribir código dentro de las clases.
Polimorfismo
Una clase puede crear el número que quiera de funciones. Estas funciones se identifican con un nombre y solo es posible usar el mismo nombre en dos o más funciones cuando el número de parámetros usado en cada una de ellas es distinto. A este mecanismo se le denomina Polimorfismo.
Virtuales
En caso de tener herencia (véase Herencia de clases), es posible que queramos sobrescribir una función heredada. Esto podemos querer hacerlo o bien sobrescribiendo completamente el comportamiento heredado con uno nuevo, o bien
ampliando o modificando dicho comportamiento. Supongamos que tenemos este ejemplo:
class Enemy
{
properties:
damage = 1;
function SetDamage(_damage)
{
damage = _damage;
}
}
class Troll implements Enemy
{
properties:
myDamage = 0;
function SetDamage(_damage)
{
myDamage = _damage;
}
}
Ahora vamos a ver distintas formas de aplicar la herencia de una función en la clase hija Troll. Para ello vamos a ver diferentes formas de declarar la función SetDamage dentro de la clase:
Sobrescribiendo el comportamiento
function SetDamage(_damage)
{
myDamage = _damage;
}
Un ejemplo:
_a = new Troll();
_a.SetDamage(5);
Lo que hará será invocar a la función existente en la clase Troll. Por tanto solo se ejecutará el código de esa clase.
Heredando el comportamiento
virtual SetDamage(_damage)
{
myDamage += 2;
}
Con el mismo ejemplo de antes, lo que ocurrirá es que primero se invocará la función «SetDamage» de Enemy y posteriormente el «SetDamage» de Troll.
Modificando el comportamiento
function SetDamage(_damage)
{
if (_damage > 10)
_damage = 10;
::SetDamage(_damage);
}
Y, siguiendo con el mismo ejemplo que los anteriores, en este caso controlamos cuándo se invoca «SetDamage» del padre.
Llamada a funciones por nombre
Mediante la sentencia _call (nombre_de_funcion, [arg1, arg2, …]); podemos llamar a funciones de clase usando una cadena de caracteres con el nombre de la función. Por ejemplo:
function Inicializar (base, nombre){}
function Salir (){}
…
_call ('Inicializar', [5, 'Parámetro']);
_call ('Salir');
El primer parámetro de la llamada es obligatorio y recibe una cadena que determina el nombre de la función a llamar. El segundo parámetro es opcional y recibe una lista con los argumentos a pasar en la llamada. Si invocamos nombres
de función que no existen o que tienen número de parámetros distintos, se generará un aviso pero no se detendrá la ejecución.
Funciones ocultas
Es posible crear funciones ocultas, sin nombre, que solo sirven para dar funcionalidad a una llamada concreta en forma de argumento.
_a = SetOnClick(this, function () (_sender, _event, _params)
{
Print("Click received!");
});
El preprocesador
El preprocesador se encarga de preparar el código fuente para su posterior compilación. Lo que hace es modificar el código fuente y crear uno temporal que será el que finalmente se compilará. El preprocesador es útil para poder
compilar diferentes zonas de código en función de valores de estado, añadir código, definir macros, etiquetas y eliminar comentarios o texto que solo es útil al programador. A continuación se muestra la sintaxis disponible:
// Comentario de línea | Comentarios de línea. Terminan una vez alcanzan un retorno de carro o fin de archivo. |
/* Comentario de bloque */ | Comentarios de bloque. Aglutina un comentario en una o varias líneas. |
#include «path» | Incluye un archivo. Llegada esta instrucción del preprocesador, se incluirá el archivo referenciado por «path», que a su vez se volverá a preprocesar y compilar desde ese punto. |
#filetolist «path» | Incluye el contenido de un archivo como una lista de bytes. Esto nos permite hacer cosas como: _a = #filetolist «image.png» |
#define LABEL [VALUE] | Define una constante global con nombre LABEL y un valor VALUE. Este valor podrá ser cualquiera de los soportados por el lenguaje. En caso de omitir el valor, se usará el entero 1. Si la etiqueta ya se definió previamente, se sobrescribirá el valor. |
#undef LABEL | Elimina la referencia de constante global con nombre LABEL. |
#macro NAME CODE #endmacro | Establece una macro con nombre NAME. Cualquier referencia a dicha MACRO en el código, o sea, siempre que se detecte un uso de NAME en el código, se sustituirá por el texto CODE. |
#macro NAME(ARGS) CODE #endmacro | Establece una macro con nombre NAME y argumentos ARGS. Los argumentos son identificadores separados por comas y que luego podremos usar en el texto CODE. Por tanto, cualquier referencia a la macro usando argumentos, si el número de argumentos coincide con el de la macro, se sustituirá por CODE y tendrá en cuenta los argumentos para aplicar las sustituciones correspondientes. Cuando usamos # delante de un nombre de un argumento, se sustituye no por la expresión pasada a la macro, sino por el mismo nombre. #macro myMacro(x,y) x##y #endmacro Sustituye por las expresiones x e y concatenadas. #macro myMacro(x) x=#x #endmacro #x lo sustituirá por la expresión x convertida en texto. Viendo el ejemplo anterior: myMacro(var); se sustituirá por: var = «var»; |
#pragma TYPE | En función de TYPE, permite activar o desactivar diferentes características usadas en tiempo de compilación. Véase sección Pragmas. |
#metadata TEXT | Asocia metadatos a una sección del código compilado. |
#region #endregion | Establece una región de código. Sirve para ofrecer meta-información para el uso en aplicaciones. |
#ifdebug #ifndebug #if CONDITION #elif CONDITION #else #endif | Permite decidir si queremos compilar trozos de código en función de si estamos en DEBUG o no lo estamos, o también dependiendo del valor de ciertas etiquetas (defines). Las condiciones pueden ser: LABEL CONDITION && CONDITION CONDITION || CONDITION LABEL == VALUE LABEL >= VALUE LABEL <= VALUE LABEL > VALUE LABEL < VALUE |
__DEBUG__ | Se sustituye por 1 o 0 en función de si estamos compilando en modo depuración o no. |
__LINE__ | Se sustituye por el número de línea del código que se está compilando. |
__FILE__ | Se sustituye por la ubicación del archivo de código que se está compilando. |
Pragmas
Utilizado por la sintaxis de preprocesador #pragma, permite establecer diferentes condiciones y funcionalidades del compilador.
typejuggling | Obliga o no a la especificación de tipos en todas las declaraciones. Por defecto está desactivado (especificación de tipos no obligatoria). |
strcasecmp | Se indica si se quiere obligar a que se tengan en cuenta mayúsculas y minúsculas a la hora de establecer nombres de propiedades, funciones, etc. Por defecto está desactivado. |
errorifnoreturn | En caso de que una función tenga que devolver algún valor, el compilador dará un error si no se especifica una sentencia \. En el caso de que queramos evitar esto, lo activaremos. Por defecto está activado. |
errorifnoreturntype | En el caso de que tengamos que devolver algún tipo concreto en una función y no lo hagamos, mediante este pragma evitamos que el compilador provoque un error. Por defecto está activado. |
overrridefunctions | Permite sobrescribir funciones que se hayan declarado más de una vez dentro del mismo módulo. Esto quiere decir que si declaramos una función y posteriormente volvemos a declarar la misma función, la primera se anulará y el compilador no dará ningún error. Por defecto está desactivado. |
constantspreserve | En caso de usar constantes en expresiones, el compilador las resuelve por sus valores. Mediante este pragma podemos hacer que el compilador no haga esto y obligue a hacer uso de la propiedad constante referenciada. Por defecto está desactivado. |
Operadores
Op | Entero | Flotante | String | Lista | ||
---|---|---|---|---|---|---|
+ | Sumar | Sumar | Concatenar | Concatenar | ||
+= | Sumar | Sumar | Concatenar | Concatenar | ||
– | Restar | Restar | Quitar | Quitar | ||
-= | Restar | Restar | Quitar | Quitar | ||
++ | Incrementar uno | Incrementar uno | Añadir espacio al final | Añadir cero al final | ||
— | Decrementar uno | Decrementar uno | Quitar último elemento | Quitar último elemento | ||
* | Multiplicar | Multiplicar | ||||
*= | Multiplicar | Multiplicar | Insertar espacio en índice | Insertar cero en índice | ||
/ | Dividir | Dividir | ||||
/= | Dividir | Dividir | Quitar elemento en índice | Quitar elemento en índice | ||
/\ | División Entera | División Entera | ||||
** | Potencia | Potencia | ||||
**= | Potencia | Potencia | ||||
% | Módulo | Módulo | ||||
%= | Módulo | Módulo | ||||
>> | Desplazar bits derecha | |||||
>>= | Desplazar bits derecha | Añade espacios por el principio y quita caracteres por el final | Añade ceros por el principio y quita elementos por el final | |||
<< | Desplazar bits izquierda | |||||
<<= | Desplazar bits izquierda | Añade espacios por el final y quita caracteres por el principio | Añade ceros por el final y quita elementos por el final | |||
== | ¿Es igual? | ¿Es igual? | Comparación no sensible a mayúsculas y minúsculas | Comparación no sensible a mayúsculas y minúsculas | ||
=== | ¿Es igual? | ¿Es igual? | Comparación sensible a mayúsculas y minúsculas | Comparación sensible a mayúsculas y minúsculas | ||
>= | ¿Es mayor que? | ¿Es mayor que? | ¿Es mayor que? | ¿Número de elementos mayor? | ||
<= | ¿Es menor que? | ¿Es menor que? | ¿Es menor que? | ¿Número de elementos menor? | ||
!= | ¿Es distinto que? | ¿Es distinto que? | ¿Es distinto que? | ¿Es distinto que? | ||
! | Negación | |||||
&& | AND Inclusivo | |||||
|| | OR Exclusivo | |||||
| | OR bits | |||||
& | AND bits | Filtrar | Filtrar | |||
^ | XOR bits | |||||
in | ¿Valor existente? | ¿Valor existente? | ||||
() | Prioridad operación | Prioridad operación | Prioridad operación | Prioridad operación | ||
~ | NOT bits | Invertir la cadena | Invertir la lista |
NOTA: La comparación con STRINGS se realiza carácter a carácter. Mientras son iguales, la comparación se resuelve como idéntica; en el momento en que un carácter es distinto a otro, se miran sus códigos ASCII para resolver si es
mayor o menor.
Operadores con bits (Flags)
En este manual existen muchas referencias al término «flags». Este término requiere conocer el concepto de activación, desactivación y consulta de bits (banderas) mediante el uso de números enteros.Los flags son siempre enteros potencia de dos: 1, 2, 4, 8, 16, 32, etc., de manera que en cada bit de un entero podemos guardar información sobre si una bandera (flag) está activa o no (vale 1 o 0). Por tanto, podremos almacenar tantas banderas como bits representen el número entero que vayamos a usar.Los operadores para trabajar con flags son el OR, AND, XOR y NOT.
A continuación mostraremos una tabla con ejemplos de acciones básicas con flags:
_flags = 0; | Inicializamos los flags. Tendremos 32 flags disponibles, ya que se inicializa como entero de 32 bits. | |||
_flags = 1; | Activamos el flag 0. | |||
_flags = 32; | Activamos el flag 5. | |||
_flags &= ~1; | Desactivamos el flag 0. | |||
_check = (_flags & 32); | ¿Está el flag 5 activo? | |||
_flags |=4 | 128 | 1024 | Activamos a la vez los flags 2, 7 y 10. | |||
_flags &= ~(128 | 4) | Desactivamos los flags 2 y 7. | |||
_check = (_flags & (128 | 512)); | ¿Están los flags 7 y 9 activos? |
Strings
Un String hace referencia a un tipo de objeto que conforma una cadena de caracteres de 8 o 16 bits, dependiendo de su codificación: las cadenas de 8 bits se gestionan como ASCII y las de 16 bits como UNICODE.
Ejemplos de operaciones con strings
Operación | Acción | Resultado |
---|---|---|
A = «123» | Inicializamos con un string en formato ASCIIZ | A = «123» |
A += «4» | Agregamos la cadena «4» | A = «1234» |
A -= «23» | Quitamos la cadena «23» | A = «14» |
«6» in A | Busca la cadena «6» en A | FALSE |
«4» in A | Busca la cadena «4» en A | TRUE |
«34» in A | Busca la cadena «34» en A | TRUE |
A ++ | Agrega un espacio al final | A = «34 « |
A — | Quita el último carácter | A = «34» |
A += 5 | Añade 5 espacios al final | A = «34 « |
A -= 5 | Quita 5 espacios al final | A = «34» |
A *= 1 | Inserta un espacio en la posición 1 | A = «3 4» |
A /= 1 | Quita un carácter en la posición 1 | A = «34» |
A >>= 1 | Añade un espacio por la izquierda y elimina el carácter final | A = » 3″ |
A <<= 1 | Añade un espacio por la derecha y elimina el primer carácter | A = «3 « |
A += ascii(‘A’) | Añade un carácter ASCII a la cadena | A = «3 A» |
A = ~A | Invierte la cadena | A = «A 3» |
A = «123» B = «153» C = A & B | Aplica un filtro de coincidencia | C = «13» |
C = A – «2» | Elimina «2» de la cadena A | C = «13» |
C = A + «2» | Añade «2» a la cadena A | C = «132» |
«cad» == «ena» | Compara las cadenas «cad» y «ena» | FALSE |
«cad» == «cad» | Compara las cadenas (no sensible a mayúsculas) | TRUE |
«cad» == «cAd» | Compara las cadenas (no sensible a mayúsculas) | TRUE |
«cad» === «cAd» | Compara las cadenas (sensible a mayúsculas) | FALSE |
A = «cad» A[1] | Accede al carácter de una cadena por índice | ‘a’ (número con valor 97) |
sizeof(«cad») | Tamaño de «cad» | 3 |
strlen(«cad») | Tamaño de «cad» | 3 |
bool(«») | Convierte una cadena a bool | FALSE |
bool(«cad») | Convierte una cadena a bool | FALSE |
bool(«true») | Convierte una cadena a bool | TRUE |
int(«5») | Convierte una cadena a entero | 5 |
int(«cad») | Convierte una cadena a entero | 0 |
string(5) | Convierte un entero a cadena | «5» |
float(«5.56») | Convierte una cadena a float | 5.56 |
string(7.8) | Convierte un float a cadena | «7.8» |
NOTA: Del mismo modo que para bool, int y float, todos los tipos numéricos son susceptibles de poder ser convertidos de cadena al tipo y viceversa.
Listas
Un objeto «lista» está formado por una secuencia ordenada de valores. Los valores pueden ser de cualquiera de los tipos soportados por el VR-SCRIPT, incluyendo las propias listas.
Ejemplos de operaciones con listas
Operación | Acción | Resultado |
A = [] | Inicialización | A = [] |
A = [10, 8] | Inicialización | A = [10, 8] |
A += [5] | Añadir elemento | A = [10, 8, 5] |
A -= [5] | Quitar elemento | A = [10, 8] |
A += [«cad»] | Añadir elemento | A = [10, 8, «cad»] |
A += [10, 12] | Añadir elementos | A = [10, 8, «cad», 10, 12] |
A += [[4, 5]] | Añadir elemento | A = [10, 8, «cad», 10, 12, [4, 5]] |
6 in A | ¿Hay un 6 en A? | FALSE |
[10, 12] in A | ¿Hay un [10, 12] en A? | TRUE |
5 in A | ¿Hay un 5 en A? | FALSE |
A = [5, 4, 3] | Inicialización | A = [5, 4, 3] |
A ++ | Añadir un cero | A = [5, 4, 3, 0] |
A — | Quitar último elemento | A = [5, 4, 3] |
A += 5 | Añadir 5 ceros | A = [5, 4, 3, 0, 0, 0, 0, 0] |
A -= 5 | Quitar 5 últimos elementos | A = [5, 4, 3] |
A *= 1 | Añadir un cero en la posición 1 | A = [5, 0, 4, 3] |
A /= 2 | Quitar elemento en la posición 2 | A = [5, 0, 3] |
A >>= 1 | Desplaza la lista añadiendo un cero por la izquierda | A = [0, 5, 0, 3] |
A <<= 1 | Desplaza la lista añadiendo un cero por la derecha | A = [5, 0, 3, 0] |
A = ~A | Invierte la lista | A = [0, 3, 0, 5] |
A = [3, 5, 6] B = [2, 5, 2, 7] C = A & B | Aplicar filtro de coincidencia | C = [5] |
C = A – [5] | Quitar elemento coincidente | C = [3, 6] |
C = A + [2] | Añade elemento a final de la lista | C = [3, 6, 2] |
sizeof(c) | Tamaño de la lista | 3 |
indexof(c, 6) | Índice del elemento en la lista | 1 |
C[1] | Acceso a la lista por índice | 6 |
Diccionarios
Un Diccionario es un tipo de lista. En este caso, denominaremos Diccionario a la lista que tiene componentes con el binomio CLAVE/VALOR, donde CLAVE es una cadena de caracteres y VALOR puede ser cualquiera de los tipos soportados por VR-SCRIPT. Ejemplo de inicialización:
A = [["barcelona", 1], ["madrid", 2], ["paris", 12], ["luxemburgo", 123]];
Operación | Acción | Resultado |
---|---|---|
A[«madrid»] | Acceso por nombre | 2 |
sizeof(A) | Número de elementos | 4 |
A /= 1 | Borrar elemento 1 | A = [[«barcelona», 1],[«paris», 12], [«luxemburgo», 123]]; |
Las operaciones con listas también son posibles con diccionarios y siguen las mismas reglas.
Estructuras de control
Mediante las estructuras de control vamos a gestionar qué código ejecutar y el número de veces que lo hará. En adelante veremos cómo se componen sintácticamente estas estructuras. Conviene saber qué significan los siguientes términos:
<CONDITION> | Se trata de una expresión que al evaluarse podrá devolver VERDADERO o FALSO, o sea, un tipo booleano. |
<SENTENCE> | Hace referencia exclusivamente a una sentencia. |
<SENTENCES> | Hace referencia a una o varias sentencias de código. Si se trata de varias sentencias, tendremos que ponerlas entre los símbolos { } y además, al final de cada sentencia, el separador punto y coma ‘;’ |
En las estructuras de control con repeticiones podemos acabar creando bucles infinitos o sin salida si en algún momento no hacemos que la condición nos permita abandonar la iteración.
IF – ELSE IF – ELSE
Este tipo de estructuras permite controlar el flujo de ejecución del código usando condicionales. De esta manera, creamos ramas donde ejecutamos código en función de si ciertas condiciones dan verdadero o falso.
Formato:
if (<CONDITION>) <SENTENCES> [else if (<CONDITION>) <SENTENCES>] [else if (<CONDITION>) <SENTENCES>] … [else <SENTENCES>]
Ejemplo:
if (a == 5)
{
print ("a es igual a 5");
}
WHILE
Nos permite repetir la ejecución de código mientras se cumpla cierta condición. Es posible que el código nunca llegue a ejecutarse si inicialmente la condición no se da.
Formato:
while (<CONDITION>) <SENTENCES>
Ejemplo:
a = 1;
while (a < 5)
{
print ("dentro del bucle");
a = a + 1
}
DO-WHILE
Esta versión del WHILE nos permite ejecutar el código al menos una vez, y continuar con la ejecución mientras se siga dando la condición.
Formato:
do <SENTENCES> while (<CONDITION>);
Ejemplo:
a = 5;
do
{
a = a + 1;
print ("dentro del bucle");
}
while (a < 5);
FOR
Este tipo de estructuras está pensado para iterar entre diferentes valores o repetir un código un número determinado de veces. Por un lado, la primera sentencia suele usarse para inicializar una variable índice. La condición sirve
para decidir la duración del bucle y la última sentencia define cómo varía el valor del índice. En cualquier caso, aunque suela ser el uso común del FOR, no significa que no se le puedan dar otros usos.
Formato:
for (<SENTENCE_S>;<CONDITION>;<SENTENCE_E>) <SENTENCES>
Ejemplo:
for (i=0; i<10; i++)
{
print (tabla[i]);
}
Podríamos convertir un FOR en un WHILE del siguiente modo:
<SENTENCE_S>;
while (<CONDITION>)
{
<SENTENCES>
<SENTENCE_E>;
}
Se puede interrumpir la ejecución de estos bucles o ignorar una iteración usando las instrucciones BREAK y CONTINUE.
SWITCH
Este tipo de estructura está pensado para ejecutar diferentes partes de código en función de valores concretos o segmentos de valores. En la sentencia SWITCH se comprueban las condiciones existentes (CASE) una a una y se ejecuta el código de todas ellas a menos que usemos la sentencia BREAK, que cancelaría esta secuencia saliendo del ámbito de la sentencia SWITCH.
Formato:
switch (<EXPRESSION>) { case [==,<,<=,>,>=,in] <EXPRESSION>: <SENTENCES> [break;] case [==,<,<=,>,>=,in] <EXPRESSION>: <SENTENCES> [break;] … [default: <SENTENCES> [break;]] }
Ejemplo:
switch (a)
{
case 5: print ("se trata de un error"); break;
case < 12:
{
print ("es un animal");
v = lista_animales[a];
}
break;
}
REPEAT
Es una variante del WHILE y del FOR. Nos permite repetir código en base a una condición, o bien ejecutar código un número concreto de veces.
Formato:
repeat [while <CONDITION>] <SENTENCES>
repeat (<EXPRESSION>) <SENTENCES>
Ejemplo:
repeat (5) { print ("esto se ejecutará 5 veces"); }
i = 0;
repeat (i<5) { print ("esto también"); i++; }
BREAK, CONTINUE
Para el break:
DO/WHILE | Cancela la ejecución del bucle y sale del mismo. Iría a la siguiente instrucción después del WHILE. |
WHILE | Cancela la ejecución del bucle y sale del mismo. Iría a la siguiente instrucción después del WHILE. |
FOR | Cancela la ejecución del bucle y sale del mismo. Iría a la siguiente instrucción fuera del ámbito del FOR. |
SWITCH | Cancelar la ejecución de más código y sale del ámbito del SWITCH. |
Para el continue:
DO/WHILE | Permite cancelar la iteración volviendo a la primera línea de código tras el DO. |
WHILE | Permite cancelar la iteración volviendo a la primera línea de código tras el WHILE. |
FOR | Ejecuta el código que define la siguiente iteración y vuelve a la primera línea de código tras el FOR. |
Tanto BREAK como CONTINUE solo tienen uso en las sentencias que se indican en las tablas anteriores. Usar estas instrucciones en otras sentencias dará un error de compilación.
Creación y borrado de instancias
Para crear instancias de objetos con VR-SCRIPT se utilizan las palabras reservadas new (para la creación) y delete (para liberar recursos).Podemos usar new de las siguientes formas:
_instancia = new nombre_de_clase ();
_instancia = new nombre_de_clase (parámetros);
«nombre_de_clase» hace referencia al nombre de una clase accesible por el script. Hay que tener en cuenta si la clase a la que hacemos referencia es hija de la clase desde la cual se hace la llamada a new. También si la clase pertenece
al mismo namespace o si es accesible a través de los directorios importados.La clase puede tener diferentes constructores _operator_new implementados. En función de cómo usemos el new, se invocará el que cumpla con el número de parámetros usado. Por ejemplo:
_enemy = new Enemy ("troll", 10);
Para eliminar una instancia de clase creada, utilizaremos:
delete _instancia;
Esto provocará que se ejecute el «_operator_delete» si está implementado en la clase de la instancia a eliminar. Este operador no usa parámetros nunca, y por tanto solo podemos definir uno por clase.
Herencia de clases
La herencia de clases permite crear jerarquía y encapsular comportamientos. Mediante este mecanismo, podemos asociar, extender y/o redefinir código además de propiedades.Ejemplo:Tenemos dos archivos, Enemy.pi y Slurp.pi. Ambos son scripts.
Archivo: Enemy.pi
class ACTOR
{
properties:
life = 100;
damage = 5;
function Init ()
{
}
function Move ()
{
}
}
Archivo: Slurp.pi
class ACTOR implements Enemy
{
properties:
life = 80; // Redefine el valor del padre
damage = 8; // Redefine el valor del padre
salto_h = 12.0f; // Crea una nueva propiedad
function Init ()
{
::Init (); // LLama al Init del padre
}
virtual Move ()
{
}
}
Al declarar como virtual, siempre se llamará primero a la función Move del padre (Enemy::Move). Básicamente el virtual equivale a:
function Move () { ::Move(); }
Partials
Mediante este mecanismo podemos incrustar código de archivos script dentro de otro script. No es lo mismo que «implements», donde los métodos y propiedades forman parte de una clase. En un «partial», se incluye el código de otro script como si fuese código propio. Una similitud a esto sería la directiva de preprocesador #include.
Ejemplo:
class Base
{
properties:
id = 0;
name = "";
function GetID()
{
return id;
}
}
class Book partial Base
{
properties:
ref = "";
content = "";
function GetContent()
{
return content;
}
function GetID()
{
return ref;
}
}
_a = new Book();
Ahora la instancia creada dispondrá tanto de funciones como de propiedades de Base como si fuesen propias, en el sentido de que si creásemos alguna propiedad con el mismo nombre en Book que en Base, tendríamos un error de compilación. En cuanto a las funciones, si en la clase Book creamos funciones que ya existen en
Base, estas se ignoran y siempre se usan las que se incluyan de Base. Nótese que no hay forma de invocar la función de Book y la de Base de ningún modo: para disponer de herencia hay que usar implements.
Máquina de estados
VR-SCRIPT permite trabajar también con una máquina de estados. Estos se declaran en el cuerpo de la clase como:
state "NAME"
{
}
Mediante la sentencia: _change(«»); cambiamos de un estado a otro. Los estados se ejecutan atómicamente; quiere decirse que todas las sentencias de un estado se ejecutan una detrás de otra dentro de la actualización de la lógica
del sistema.
Ejemplo:
class Enemy
{
state "Quiet":
{
if (EnemyDistance (this, player) < 100.0f)
{
_change ("FindPlayer");
}
}
state "FindPlayer":
{
}
}
Evaluación de expresiones al vuelo
Mediante _eval podemos evaluar expresiones en formato texto al vuelo.
a = 10;
b = _eval ("a + 10"); // esto hace que b valga 20.
Ejecución de código al vuelo
Mediante _run podemos ejecutar bloques en formato texto de código al vuelo.
_block = @@ if (a==5) print ("Es correcto!") else a=10; @@
_run (_block);
Sobrecarga de operadores
Son funciones que se pueden implementar y su uso va estrictamente relacionado con el tipo Pointer. El parámetro \ sería un tipo de dato (int, float, pointer, etc.). La función en teoría debería devolver el tipo requerido en la sobrecarga. Mediante el uso de new y delete es posible crear nuevos objetos y eliminarlos.
function _operator_new ()
function _operator_delete ()
function _operator_sizeof ()
function _operator_get (indice)
function _operator_set (indice, value)
function _operator_mod (v1, v2)
function _operator_mul (v1, v2)
function _operator_pow (v1, v2)
function _operator_div (v1, v2)
function _operator_add (v1, v2)
function _operator_sub (v1, v2)
function _operator_equal (v1, v2)
function _operator_notequal (v1, v2)
function _operator_great (v1, v2)
function _operator_less (v1, v2)
function _operator_greatequal (v1, v2)
function _operator_lessequal (v1, v2)
function _operator_logand (v1, v2)
function _operator_logor (v1, v2)
function _operator_lognot (v)
function _operator_not (v)
function _operator_and (v1, v2)
function _operator_xor (v1, v2)
function _operator_or (v1, v2)
function _operator_lshift (v)
function _operator_rshift (v)
Optimizaciones
Un acceso a una propiedad o variable global es un 25% más rápido que una llamada a función. El acceso a los elementos de una cadena es más lento que el acceso a los elementos de una lista.
cadena = "ABCDEF";
lista = [65, 66, 67, 68, 69, 70];
//... dentro de un bucle
a = cadena[1]; // devuelve 66
b = lista[1]; // devuelve 66, y es más rápido
//... fin del bucle
La condición de un bucle se evalúa en cada paso del bucle, y cada cálculo que hacemos dentro, como sizeof(value), se vuelve a calcular cada vez, así que es mejor moverlo fuera del bucle.El preprocesador es lento. Cuando se define una macro el procesador debe analizar todo el archivo, hacer conversiones de texto, etc., lo cual puede ralentizar la ejecución antes del Init. Es recomendable poner las macros/defines en un archivo pequeño.
Definición de funciones con número de parámetros indefinido
Podemos declarar una función que puede contener un numero de parámetros indefinido:
function Test (...)
{
}
A esta función la podemos llamar con el número de parámetros que queramos:
Test();
Test(5);
Test("hola", 6, "adios");
Para poder acceder a los parámetros desde la función usaremos _get indicando el índice del parámetro:
function Test (...)
{
Print(_get(0));
}
Con _get(«\@») obtendremos el número de parámetros variables que se le pasan a la función. En el siguiente ejemplo, tenemos un parámetro fijo obligatorio «_a» y el resto serían opcionales:
function Test2 (_a, ...)
{
}
Si la función se invoca como:
Test2(5, 6, "hello");
_get("@")
La función _get(«\@») nos devolverá 2.
Instanceof
Con _instanceof podemos saber si un objeto es una instancia de una clase, y en qué grado o nivel. Por ejemplo, con la siguiente estructura de clases:
Archivo: Animal.pi
class Animal
{
properties:
A = 5;
}
Archivo: Mamifero.pi
class Mamifero implements Animal
{
properties:
B = "delfin";
}
Archivo: Perro.pi
class Perro implements Mamifero
{
properties:
C = "pastor alemán";
function Ladra()
{
// ...
}
}
Podemos hacer lo siguiente:
A = new Animal();
M = new Mamifero();
P = new Perro();
_res = A._instanceof("Animal"); // _res valdrá 1
_res = M._instanceof("Animal"); // _res valdrá 2
_res = P._instanceof("Animal"); // _res valdrá 3
_res = A._instanceof("Perro"); // _res valdrá 0
Implements
Con la función _implements, podemos saber si un objeto implementa una función o si tiene alguna propiedad/constante que queramos averiguar. Usando el ejemplo anterior:
_res = P._implements("Ladra"); // _res = 1
_res = A._implements("Ladra"); // _res = 0
_res = P._implements("C"); // _res = 1
_res = P._implements("A");; // _res = 1
_res = M._implements("C");; // _res = 0