Haz click aquí para volver a la página principal

Referencia CKS - Lo básico de CKS

En esta guía explicaré, paso a paso, todo lo que necesitas saber para empezar a programar en CKS y crear tus propios mapas, aventuras, conquistas o mods para Imperivm. Tras una breve explicación de qué es CKS, podrás aprender los conceptos básicos generales que necesitarás para manejarte con el lenguaje. Al final de todo, encontrarás información sobre como se relaciona todo esto con esta guía, y cómo emplearla sacándole el máximo provecho.

¿Qué es CKS y para qué sirve?

Las siglas CKS provienen de Celtic Kings Script (lenguaje de Celtic Kings), y es el lenguaje interno elaborado por los creadores de la saga de Imperivm (originalmente llamada Celtic Kings) para desarrollar el juego, como parte de la elaboración del motor de juego sobre que el se implementaría la saga.

Es un lenguaje de programación con una sintaxis similar a C++, pero simplificado y con características particulares. Puede ser empleado en la elaboración de secuencias en el editor de mapas para realizar todo tipo de acciones: mover unidades, otorgar bonus, crear cinemáticas, lanzar conversaciones, etc. También se usa en la creación de objetos para programar el comportamiento del objeto. Además, es el lenguaje que se utiliza en la propia programación del juego, que encontrarás en los archivos .pak. Con CKS, puedes modificar estos archivos, o crear los tuyos propios para crear mods para el juego.

¿Quieres crear tus propios mapas o aventuras? ¿O quizás modificar el comportamiento de alguna parte del juego? Pues prepárate, con unas nociones básicas y la ayuda de esta guía, podrás hacer todo lo que desees.

Primeros pasos en CKS

note Te recomiendo que abras una nueva aventura en el editor y pruebes los ejemplos y juegues con ellos para irte acostumbrando. Aunque no se explicarán pasos no relacionados directamente a CKS, sí se indicará cuando necesites tener unidades, objetos, etc. con nombres específicos en tu mapa.

Hola mundo

Vamos a empezar con la acción, directamente, así, sin rodeos. Crea una nueva secuencia y activa Permitir autoejecución, dejando Seq. de prerrequisitos en blanco. Esto permitirá que tu secuencia comience a ejecutarse (que lo que escribas empiece a suceder) nada más comience el juego. A continuación haz click en la pestaña Fuente y escribe allí lo siguiente:

pr("Hola mundo!");
visibility Asegúrate de copiarlo exactamente como está, incluido el ;. Pronto notarás que los carácteres que aparecen en el código no suelen ser aleatorios, y cumplen una función muy concreta.

Pulsa Compilar, a la derecha debería aparecer el texto Compilado con éxito. Si guardas y pulsas sobre Probar este mapa, podrás ver Hola mundo! aparecer en el chat, en la parte superior izquierda de la pantalla, nada más comienza la partida.

Vuelve al editor y elimina el ;, por ejemplo. Ahora al pulsar compilar, a la derecha se leerá: Semicolon not found after expression end., y más a la derecha 1, 18. Esto nos indica que se ha encontrado un error al compilar. El texto de la izquierda indica qué tipo de error se ha encontrado, mientras que los números nos indican, respectivamente, la línea (1) y el carácter aproximado en el que se ha dado el error (18). El cursor de escritura, también se mueve al carácter indicado. En este caso, la descripción del error significa Punto y coma no encontrado tras el final de la expresión, que significa que una expresión ha acabado y no se ha encontrado el punto y coma.

El punto y coma, espacios en blanco y comentarios

El código en CKS se ejecuta en orden de lectura y está formado por sentencias, cada una delimitada por un punto y coma (;) final. Observa el siguiente código; como puedes ver, está formado por 4 sentencias:

pr("Soy una sentencia");
pr("Yo soy otra!");
pr("Yo soy la tercera");
pr("Y yo la cuarta :(");

Estas sentencias serán ejecutadas por el juego en el orden en que están escritas, cada una imprimiendo un mensaje nuevo en el chat.

Los espacios en blanco (espacios, tabulaciones, saltos de línea...) son, habitualmente, ignorados por el compilador, y por lo general se pueden omitir (salvo en ciertos casos en que se requieren para diferenciar ciertos elementos). Si cambiamos nuestro ejemplo anterior que imprimía Hola mundo! por un código como el siguiente, veremos que compila perfectamente y el resultado es el mismo:

pr
	(
		"Hola mundo!"
			)
				;

Por tanto, es aconsejable utilizar saltos de línea y tabulaciones para facilitar la lectura del código y hacer así más fácil entender qué es lo que está pasando.

Por último, existe otra parte del código que es completamente ignorada por el compilador: los comentarios. Los comentarios nos permiten escribir texto en el código, a fin de ayudar al lector a comprender lo que sucede más fácilmente. Existen dos tipos de comentario: comentarios de línea y comentarios de bloque.

Los comentarios de línea comienzan con // y continúan hasta el final de la línea, los comentarios de bloque comienzan con /* y continúan hasta el siguiente */. A continuación se muestra un ejemplo de ambos tipos de comentarios:

// Este es un comentario de linea, y por tanto ignorado
pr("Hola mundo!");
/*
    Este, sin embargo, es un comentario de bloque,
    y dura hasta que se encuentre el final del bloque.
*/
pr("Vaya, vaya.."); // Los comentarios pueden aparecer...
pr( /* ...en cualquier parte */ "Hasta luego, mundo!");

Funciones y tipos

Vale, ya sabemos cómo comentar nuestro código, pero ¿qué es lo que hace que sucedan cosas? Las funciones. Aunque CKS no nos permite definir nuestras propias funciones, existe una gran cantidad de ellas provistas por el lenguaje. Un ejemplo de función que ya hemos usado sería pr, que nos permite escribir un mensaje a la consola del juego, que es la misma a la que se escribe cuando escribimos en el chat.

note Nota: pr proviene de print, que significa imprimir en inglés.

Para ejecutar una función, escribiremos su nombre seguido de paréntesis, entre los cuales indicaremos los argumentos (a veces llamados parámetros) a pasar a la función, separados por comas (,). Por ejemplo, en pr("Hola mundo!"); tenemos una única sentencia, que contiene una llamada (ejecutar una función también se denomina llamarla) a la función pr con "Hola mundo!" como argumento.

La función pr no retorna nada, pero algunas funciones pueden retornar valores. Cada uno de los valores que manejamos en CKS tiene asociado un tipo, que indica cuándo puede ser usado como argumento en una función. Por ejemplo, la función MapName() devuelve un str, es decir, una cadena de texto, que contiene el nombre del mapa actual. Dado que pr recibe, como único argumento, un str que escribir a la consola, podemos escribir el siguiente código para imprimir el nombre del mapa a la consola:

pr(MapName);
note Nota: si la función no recibe argumentos, puedes omitir los paréntesis, el nombre de la función es suficiente para llamarla.

Los tipos también determinan qué operadores se pueden usar sobre ellos. Por ejemplo, el operador de suma de enteros (+) opera sobre dos enteros (int) y retorna la suma de ambos. Si queremos imprimir por consola la suma de 56 y 17, aprovechando que también existe una función pr que recibe un argumento de tipo int, podemos escribir lo siguiente:

pr(56 + 17);

Finalmente, algunos tipos disponen de funciones "propias", normalmente llamadas métodos. Puedes pensar en estos métodos como acciones o funciones que sólo ese tipo puede realizar. Para llamar a un método sobre un valor del tipo que lo posee, escribimos un punto (.) y a continuación la llamada al método tal y como lo haríamos con cualquier otra función. Por ejemplo, tenemos la función selo, que devuelve el objeto (Obj) seleccionado, y Obj posee el método health, que nos devuelve la cantidad de vida del objeto (con tipo int), por lo que para escribir la cantidad de vida de la unidad seleccionada en el chat, pondríamos (nota cómo usamos un punto para llamar a health sobre el retorno de selo):

pr(selo.health);
warning Si ejecutas el código anterior, la sentencia se ejecutará nada más el juego comience, y al no haber ningún objeto seleccionado, se imprimirá 0 en el chat. Sin embargo, Imperivm también permite ejecutar comandos desde la ventana de chat: prueba a seleccionar una unidad o edificio, pulsa Intro para abrir la ventana de chat y, a continuación, escribe el comando anterior. Verás que en el chat aparece, justo encima de tu mensaje, la vida del objeto que tienes seleccionado. Esta es una forma fácil de comprobar el resultado de la ejecución de ciertos códigos.
note Nota: en la guía encontrarás que a los métodos que no reciben parámetros y comienzan con minúscula se les llama propiedades. En la práctica, no hay diferencia real entre métodos y propiedades, pero es común distinguirlos, así como es común escribir las propiedades sin paréntesis, mientras que los métodos se suelen llamar con paréntesis incluso si estos no contienen nada.

Algunas funciones tienen el mismo nombre pero reciben diferentes cantidades de argumentos o de distintos tipos. También pueden tener el mismo nombre métodos de diferentes tipos. Por consiguiente, una función se identifica no sólo por su nombre, sino también por los argumentos que recibe y el tipo al que pertenece (en caso de ser un método). A diferentes funciones con el mismo nombre pero diferentes argumentos se les denomina sobrecargas. Un ejemplo serían las diferentes sobrecargas de pr: pr(str), pr(int) y pr(bool).

info Las funciones que "no retornan nada", como pr, en realidad retornan el tipo void.

Flujo de ejecución

Cada secuencia puede ser ejecutada en paralelo a las demás, de forma que varias secuencias se pueden estar ejecutando a la vez. La ejecución de una secuencia o script se lleva a cabo sentencia por sentencia, una tras otra, de forma secuencial. Dentro de una sentencia, las funciones se ejecutan desde dentro hacia afuera, evaluando primero aquellas cuyo retorno sirve como argumento para la llamada a otra función. Así, por ejemplo, en la siguiente secuencia:

pr(GetSettlement("asentamiento").Units[0].raceStr);
View(GetSettlement("asentamiento").GetCentralBuilding().pos, false);

El orden de ejecución sería el siguiente:

  • GetSettlement("asentamiento") obtiene el asentamiento con nombre asentamiento.
  • Units obtiene, del asentamiento retornado por GetSettlement, la lista de todas las unidades contenidas en él.
  • [0] retorna el primer objeto de la lista.
  • raceStr retorna la "raza" de ese objeto y esta es escrita a la consola del juego por pr.
  • A continuación, se procede con la siguiente sentencia.
  • GetSettlement("asentamiento").GetCentralBuilding().pos es ejecutado de forma similar al caso anterior, y finalmente View mueve la cámara a la posición retornada por pos, centrando la vista en el edificio central del asentamiento.

Algunas funciones pueden bloquear o detener la ejecución de la secuencia hasta que se completan. Un ejemplo de este tipo de funciones es Sleep, que detiene la ejecución por el tiempo indicado (en milésimas de segundo), o todas las funciones cuyo nombre comienza por Wait, que detienen la ejecución hasta que se cumpla una cierta condición.

En la siguiente sección veremos algunas directivas que nos permiten controlar el flujo de ejecución, haciendo que ciertos conjuntos de sentencias se ejecuten repetidamente o condicionalmente.

Estructuras de control (if, while, for, …)

De todos los tipos de CKS, hay uno que es, quizás, un poco más especialito que los demás: bool, que se corresponde a un valor de verdad, es decir, sólo puede contener true (verdadero) o false (falso). Usando este tipo, podemos controlar la ejecución condicional o repetida de ciertos grupos de sentencias mediante el uso de algo que llamamos estructuras de control. Estos grupos de sentencias son los bloques, y se indican en el código rodeando las sentencias con llaves ({ y }).

info La secuencia, en sí misma, también es un bloque, aunque no haga falta colocar llaves al principio y final.

Aunque generalmente se utilizan valores bool, los valores de tipo int (números enteros) también pueden ser usados. Si este es el caso, un valor de 0 evalúa como falso (false), mientras que cualquier otro valor evaluará como cierto (true).

note Nota: aunque de ahora en adelante se hablará de cómo las estructuras de control repiten u omiten ciertos bloques, has de saber que las estructuras de control también admiten, en lugar de bloques, sentencias únicas (omitiendo las llaves). En estos casos, puedes imaginar como si hubiese un bloque que contiene únicamente la primera sentencia tras la secuencia de control (hasta la primera ;).

Estructura condicional if / else

La estructura condicional if (del inglés: si) nos permite ejecutar un bloque de código únicamente si una expresión dada evalúa como verdadera (true). Esto puede ser muy útil cuando queremos que cierto bloque se ejecute o no dependiendo de ciertas condiciones. La sintaxis de un if es:

if ( expresión_a_ser_evaluada ) bloque_si_true 

donde expresión_a_ser_evaluada puede ser cualquier cadena de funciones que retorne un valor de tipo bool o int, y bloque_si_true es, o bien una sentencia única, o bien cualquier conjunto de sentencias rodeadas por llaves.

visibility Presta atención a que no se coloca ningún punto y coma (;) entre el if y el bloque o la sentencia a ejecutar condicionalmente. Si colocas un punto y coma tras el if o cualquier otra estructura de control, estás indicando que tu bloque condicional es simplemente una única sentencia vacía.

La estructura if viene acompañada de una cláusula else (del inglés: en otro caso) opcional, que nos permite especificar otro bloque para ejecutar alternativamente, en caso de que la expresión evalúe a falso (false).La sintaxis de un if con cláusula else tiene este aspecto:

if ( expresión_a_ser_evaluada )
    bloque_si_true
else
    bloque_si_false

donde expresión_a_ser_evaluada y bloque_si_true son los mismos que antes, y bloque_si_false es también un bloque de sentencias o una sentencia única.

note Nota: recuerda que los saltos de línea y las tabulaciones son completamente ignoradas por el compilador, simplemente las añadimos para hacer el código más legible. Podrías, si quisieses, escribir toda una secuencia en una única línea.

La ejecución de un if puede ser explicada con el siguiente diagrama (a la izquierda, sin bloque else, a la derecha, con ambos bloques):

diagrama de la ejecución del if, tal como se describe en el texto

Por ejemplo, con el siguiente código comprobamos si la unidad con nombre de scripts miUnidad tiene menos de 100 puntos de vida, y si esto sucede, le damos 50 puntos de vida más y lanzamos la conversación miConversacion. Si no es así y la unidad tiene 100 o más puntos de vida, le reducimos la vida en 75 puntos.

if(GetNamedObj("miUnidad").obj.health < 100) {
    GetNamedObj("miUnidad").obj.Heal(50);
    RunConv("miConversacion");
} else {
    GetNamedObj("miUnidad").obj.Damage(75);
}
info GetNamedObj retorna un NamedObj. Dado que health, Heal y Damage pertenecen a la clase Obj, necesitamos llamar a obj para obtener el Obj sobre el que llamar a estos métodos.

Estructura while

A veces queremos que la ejecución de una serie de sentencias se repita varias veces mientras una condición se cumpla. Por ejemplo, podríamos querer imprimir por consola Estoy vivo! cada segundo mientras una unidad con nombre Eufrasio siga viva. La estructura while (del inglés: mientras) nos permite hacer exactamente eso, su sintaxis es la siguiente:

while ( expresión_a_ser_evaluada )
    bloque_mientras_true

donde expresión_a_ser_evaluada es una expresión que retorna un valor de tipo bool o int, y bloque_mientras_true es un bloque de sentencias (o una sentencia única, si no se utilizan llaves) que ha de ejecutarse si la condición es cierta, para luego volver a comprobar la condición de nuevo.

visibility Presta atención a que no se coloca ningún punto y coma (;) entre el while y el bloque o la sentencia a ejecutar en bucle. Si colocas un punto y coma tras el while o cualquier otra estructura de control, estás indicando que tu bloque es simplemente una única sentencia vacía.

Cuando la ejecución llega a una sentencia while, primero se evalúa la expresión_a_ser_evaluada. Si esta evalúa como cierta (true), entonces se ejecuta el bloque_mientras_true para, a continuación, volver a evaluar la expresión y ejecutar el bloque (hasta el momento en que deje de evaluar como cierta). Si al evaluar la expresión, ésta evalúa a false, la ejecución se salta el bloque_si_true y continúa directamente después del while.

La ejecución de un while puede entenderse mejor con el siguiente diagrama:

diagrama de la ejecución de un while, tal como se describe en el texto

El siguiente ejemplo muestra cómo escribir a consola Estoy vivo! cada segundo mientras una unidad con nombre Eufrasio esté viva, para finalmente escribir He muerto D: una única vez cuando la unidad haya muerto:

while (GetNamedObj("Eufrasio").obj.IsAlive()) {
    pr("Estoy vivo!");
    Sleep(1000); // 1000 milisegundos = 1 segundo
}
pr("He muerto D:");

La ejecución del ejemplo anterior puede ser descrita por el siguiente diagrama:

diagrama de la ejecución del ejemplo, tal como se describe en el texto

Estructura for

Entre los diferentes usos de repetir bloques de sentencias, uno común es el de hacerlo usando algún tipo de contador. Imaginemos que queremos que la vida de una unidad llamada Anacleto varíe desde 100 hasta 1000, incrementándola 10 unidades cada segundo tras imprimir el valor de vida actual. Si utilizásemos una estructura while, el código sería parecido al siguiente:

GetNamedObj("Anacleto").obj.SetHealth(100); // Iniciar la vida a 100
while (GetNamedObj("Anacleto").obj.health < 1000) { // si aún no hemos llegado a 1000
    // Hacemos algo, en este caso escribir cuanta vida tiene
    pr("Vida actual de Anacleto: " + GetNamedObj("Anacleto").obj.health);
    GetNamedObj("Anacleto").obj.Heal(10); // Incrementamos la vida en 10
}

Sin embargo, existe una forma más concisa de escribir este código: la estructura for (del inglés: para). Esta estructura nos permite, de manera compacta, indicar un bucle con una inicialización y un paso entre iteraciones. Su sintaxis en la siguiente:

for ( expr_inicializacion ; expr_a_evaluar ; expr_de_paso )
    bloque_mientras_true

donde expr_inicializacion es una expresión ejecutada justo antes de comenzar el bucle, usada generalmente para inicializar algún tipo de contador; expr_a_evaluar es la expresión que se evaluará antes de cada iteración del bucle para decir si continuar ejecutando el bloque o acabar el bucle y continuar con las sentencias siguientes; expr_de_paso es una expresión que se evaluará tras cada iteración del bucle, antes de comprobar de evaluar de nuevo expr_a_evaluar; y bloque_mientras_true es, al igual que en while, o bien una única sentencia o un grupo de sentencias rodeadas por llaves (un bloque).

La ejecución de un for puede entenderse mejor con el siguiente diagrama:

diagrama de la ejecución de un for, tal como se describe en el texto

De esta forma, el ejemplo que acabamos de exponer sobre nuestro amigo Anacleto, puede ser escrito con un for de la siguiente forma (con exactamente la misma funcionalidad):

for (
    GetNamedObj("Anacleto").obj.SetHealth(100);
    GetNamedObj("Anacleto").obj.health < 1000;
    GetNamedObj("Anacleto").obj.Heal(10)
) {
    pr("Vida actual de Anacleto: " + GetNamedObj("Anacleto").obj.health);
}
note Nota: recuerda que los saltos de línea y las tabulaciones son completamente ignoradas por el compilador, simplemente las añadimos para hacer el código más legible. Podrías, si quisieses, escribir toda una secuencia en una única línea.

Más adelante explicaremos la declaración y uso de variables, que permiten uno de los usos más comunes de la estructura for: iterar una lista indexada numéricamente. Para más información sobre las variables, consulta la sección Variables y scope. Por ahora, simplemente asume que tenemos un int con nombre i y, por ejemplo, una ObjList con nombre miObjList. Con un bucle for podríamos iterar la lista con:

for(i = 0; i < miObjList.count; i += 1) {
    // Aquí podemos hacer lo que queramos con el objeto i-ésimo de miObjList (miObjList[i])
    miObjList[i].SetHealth(miObjList[i].maxhealth);
}
visibility Debemos iterar con un menor estricto porque los índices de una ObjList están indexados en base 0, es decir, el primer elemento es el número 0 y el último elemento es uno menos que el tamaño de la lista.
Nótese también que el uso de llaves aquí es opcional, pues el bloque contiene una única sentencia.
warning Este código y posteriores omite la declaración de las variables i y miObjList, pues ésta se explica más adelante. Por ahora, si quieres probar estos códigos en una secuencia en tu mapa, crea un grupo de unidades con nombre Grupo y añade siempre esto antes del código provisto:
int i;
ObjList miObjList;
miObjList = Group("Grupo").GetObjList();

Directivas break y continue

Cuando tratamos con bucles, ya sean while o for, en ocasiones puede interesarnos omitir ciertas iteraciones (o la parte final de ellas), o salir prematuramente del bucle (antes de que la expresión a evaluar del bucle retorne false). Aunque podríamos lograr cualquier comportamiento deseado modificando la expresión a evaluar y colocando estructuras if que condicionen la ejecución del bloque, a veces puede ser simplemente más cómodo o legible utilizar dos directivas especiales ideadas exactamente para esto: continue y break.

warning Tanto continue como break producen saltos en el flujo de ejecución que, si las directivas pasan desapercibidas al lector, podrían confundirlo y dificultar la lectura del código. En caso de usarlas, asegúrate siempre de que su uso está facilitando la lectura del código y no entorpeciéndola. Es posible realizar la misma funcionalidad que estas directivas logran mediante la modificación de la expresión a evaluar del bucle o la introducción de estructuras if dentro del bloque.

La directiva continue nos permite saltarnos lo que queda del bloque de sentencias y volver directamente al comienzo del bucle (justo antes de evaluar la expresión que lo condiciona). Si estamos en un bucle for, esta directiva hará que la ejecución pase directamente a la expresión de paso. Por ejemplo, imaginemos que tenemos el siguiente bucle que utiliza una variable i de tipo int para iterar sobre una ObjList llamada miObjList curando a todos sus miembros:

for(i = 0; i < miObjList.count; i += 1)
    miObjList[i].SetHealth(miObjList[i].maxhealth);
note Nota: Para más información sobre las variables, consulta la sección Variables y scope.

Imaginemos ahora que deseamos saltarnos todos aquellos objetos cuya energía sea inferior a 5. Si quisiéramos hacerlo utilizando la directiva continue, podríamos hacerlo de la siguiente forma:

for(i = 0; i < miObjList.count; i += 1) {
    if(miObjList[i].stamina < 5)
        continue;
    miObjList[i].SetHealth(miObjList[i].maxhealth);
}

De esta forma, en cada iteración del bucle (para cada objeto en la lista) se comprobará si la energía del objeto es menor que 5, y si es así se pasará directamente a i += 1, saltándonos todo el resto del bucle.

warning Aunque aquí hemos forzado el uso de continue para mostrar un ejemplo de uso, para este código sería probablemente más adecuado utilizar solamente la estructura if con la condición inversa, rodeando la curación del objeto, así:
for(i = 0; i < miObjList.count; i += 1) {
    if(miObjList[i].stamina >= 5)
        miObjList[i].SetHealth(miObjList[i].maxhealth);
}

La directiva break nos permite, en cambio, salir del bucle inmediatamente y continuar con la ejecución posterior. Imaginemos ahora que nuestro bucle cura al objeto y a continuación lo vuelve invisible, el aspecto de un código haciendo tal cosa sería el siguiente:

for(i = 0; i < miObjList.count; i += 1) {
    miObjList[i].SetHealth(miObjList[i].maxhealth);
    miObjList[i].SetVisible(false);
}

Si quisiésemos que, tras curar la primera unidad con energía menor 5, la ejecución saliese del bucle inmediatamente sin continuar con el resto de unidades y antes incluso de hacer invisible a esta última, pondríamos lo siguiente:

for(i = 0; i < miObjList.count; i += 1) {
    miObjList[i].SetHealth(miObjList[i].maxhealth);
    if (miObjList[i].stamina < 5)
        break;
    miObjList[i].SetVisible(false);
}

Cuando tenemos varios bucles anidados, debemos tener cuidado, pues estas directivas sólo afectarán al bucle más interno. De esta forma, podríamos tener un bucle que repite nuestro último código mientras el objeto llamado Casimiro está vivo, de esta forma:

while(GetNamedObj("Casimiro").obj.IsAlive()) {
    for(i = 0; i < miObjList.count; i += 1) {
        miObjList[i].SetHealth(miObjList[i].maxhealth);
        if (miObjList[i].stamina < 5)
            break;
        miObjList[i].SetVisible(false);
    }
    Sleep(1000); // Esperamos 1 segundo antes de volver a curarlos
}

En este caso, cuando el bucle interno encontrase el primer objeto con energía inferior a 5 y llegase a la directiva break, la ejecución saltaría a Sleep(1000), saliendo del bucle interno for pero permaneciendo todavía dentro del bucle más externo while.

Bucles infinitos

En ocasiones, puede ser útil que un código se repita eternamente mientras la partida continúe, en ese caso puedes utilizar un bucle infinito. Para crear un bucle infinito basta con utilizar una expresión a evaluar que evalúe siempre como true, las formas más comunes son utilizar el propio true o su equivalente entero, 1. En el siguiente ejemplo creamos un bucle infinito que nos permitirá comprobar cada segundo que pasa si Agapito tiene al menos 5 de energía, y si los tiene le curaremos 100 de vida:

while(1) {
    if (GetNamedObj("Agapito").obj.stamina >= 5)
        GetNamedObj("Agapito").obj.Heal(100);
    Sleep(1000); // Esperamos 1 segundo antes de volver a curarlos
}

Variables y scope

En ocasiones, puede ser interesante guardar ciertos valores bajo un nombre concreto para utilizarlos más adelante. Por ejemplo, podemos querer guardar la cantidad de objetos que había en el grupo G_MiGrupo cuando comenzó la secuencia, para luego comprobar si ha cambiado diez segundos después. Para realizar esto podemos utilizar las variables.

Para utilizar una variable primero debemos declararla, es decir, debemos indicar al juego el nombre y tipo de nuestra variable. Por ejemplo, si vamos a utilizar una variable llamada miVariable para guardar valores de tipo Obj, necesitamos decirle al compilador: oye, cuando escriba miVariable, me refiero a un valor de tipo Obj. A esto llamamos declarar la variable, y se hace indicando su tipo, seguido de un espacio y el nombre de la variable, por ejemplo: Obj miVariable;.

Si deseamos declarar varias variables del mismo tipo, tenemos la opción de declararlas todas juntas, separando sus valores por comas, así:

Obj miVariable1, miVariable2, miVariable3;

Los nombres de variables deben comenzar siempre por una letra o guión bajo (_), y sólo pueden contener letras, números y guiones bajos.

Si lo deseamos, también podemos darles un valor inicial a nuestra variable a la vez que la declaramos, introduciendo un signo igual (=). Sin embargo, el valor que le asignemos debe ser un valor constante, y no una expresión. El caso más típico de inicialización es usar un valor literal. La forma de escribir valores literales varía según el tipo. Por ejemplo, un literal de tipo int consiste simplemente en una secuencia de dígitos numéricos (0, 1, 2, 3, 4, 5, 6, 7, 8 o 9), opcionalmente precedidos por un signo menos (-). Ejemplos de literales enteros válidos son: 68, -73 o 482. En el caso de los str, se pueden escribir de forma literal rodeándolos con dobles o simples comillas (" o '), por ejemplo: "Yo soy un str!", 'y yo otro', "puedes escapar \" comillas con la barra inclinada". En estos literales puedes usar también \n para introducir saltos de línea o \t para introducir tabulaciones.

info Las variables no inicializadas explícitamente se inicializan a un valor por defecto que depende del tipo de variable. Por ejemplo, los int se inicializan a 0, mientras que los str se inicializan como cadenas vacías (""). Los Obj se inicializan como objetos inválidos, los ObjList como listas vacías, etc…

En el siguiente ejemplo inicializamos una serie de variables a distintos valores:

int a = 6, b = 7;
float f = 1.5;
ObjList ol; // no inicializada
str miHeroe = "Anobolondio";

¿Recuerdas los bloques? Esos grupos de sentencias rodeados por llaves ({ y }). Pues las declaraciones de variables deben ir siempre al principio de un bloque. Cualquier declaración de variables realizada tras la primera sentencia de un bloque resultará en un error de compilación. Por ejemplo:

int a; // Esta declaración es correcta
str b; // Esta también
a = Group("G_MiGrupo").count; // Ojo: primera sentencia no declarativa
int c; // Esta declaración dará un error de compilación
while (Group("G_MiGrupo").count == a) {
    ObjList ol; // aquí podemos declarar de nuevo
    ol = Group("G_MiGrupo").GetObjList();
    // La secuencia continuaría con más cosas...
}
visibility Recuerda que la secuencia, como tal, también constituye un bloque, aunque no se escriban las llaves explícitamente.

Cada variable tiene asociado un scope, es decir, un ámbito en el cual la variable existe, y este ámbito se corresponde con el bloque en el que se declara y todos los bloques contenidos en él. Si un nombre de variable ya está en uso en el scope actual, dará un error de compilación, mientras que si está en uso en un scope más exterior la nueva variable reemplazará a la antigua hasta que el scope finalice.

int a = 3;
a += 5;               // Aquí existe a
if (a > 5) {
    int b;            // b existe sólo en este bloque
    b = a * 8;        // a continúa existiendo en los scopes anidados
    pr("b: " + b);
}
b -= 1;               // ERROR! b ya no existe pues estamos fuera de su scope (eliminar esta línea)
if (a > 7) {
    str a = "Manolo"; // la nueva variable "oculta" la más exterior
    GetNamedObj(a).obj.Heal(100); // a es ahora un str
}
a = 10;               // la a interna desaparece, aquí a vuelve a ser un int

Constantes y nombres disponibles en secuencias

Hay una serie de "variables" que CKS declara por nosotros y cuyo valor no podemos modificar, las constantes. Estas constantes aparecen documentadas a lo largo de la guía, algunos ejemplos serían las diferentes constantes para los enteros de cada raza (Britain, Iberia, …) o las constantes para las habilidades de los héroes (hsHealing, hsCeasefire, …) entre otras.

Cuando trabajamos en secuencias del editor de mapas, además, el compilador declarará como constantes de tipo NamedObj aquellos objetos que tengan nombre de scripts, usando el nombre de scripts como nombre de la variable. De esta forma, si estamos en una secuencia de un mapa en la que existe un edificio con nombre de scripts MiEdificio, escribir en una secuencia MiEdificio.obj.SetPlayer(1); compilará sin problema, pues el compilador declara la constante MiEdificio para nosotros.

warning Esto sólo ocurre en la edición de secuencias del mismo mapa en que se encuentran los objetos, si estamos en un mapa distinto (por ejemplo porque el objeto vino como parte del grupo de viaje desde otro mapa de la misma aventura) la constante no existirá pues el objeto no se encuentra en el mapa a la hora de compilar.

Finalmente, cuando especificamos un método sin indicar nada sobre lo que invocarlo (comenzando la expresión con un punto), el compilador entiende que estamos utilizando la variable especial this. Por eso, podemos encontrarnos secuencias como la siguiente:

Unit this; // declaramos la variable this
this = Ataulfo.obj.AsUnit(); // asignamos la unidad a this
while (1) {
    .SetLevel(.level + 1); // equivalente a this.SetLevel(this.level + 1);
    Sleep(10000);
}

Cómo usar esta guía

Ahora que entiendes lo básico de CKS ya puedes comprender cómo usar esta guía y sacarle el máximo provecho.

Sintaxis de la guía

La sintaxis empleada para describir las funciones en esta guía es más o menos común. Para indicar que un método corresponde a un tipo determinado lo indicamos con ::, por ejemplo: Query::GetObjList() hace referencia al método de nombre GetObjList que puede ser llamado sobre una Query.

Cuando indicamos los parámetros, lo hacemos dándoles un nombre (por el cual nos referimos a ellos en la descripción), seguido de : y el tipo del argumento / parámetro. Por ejemplo, Obj::SetPlayer(jugador: int) tiene un único argumento de nombre jugador y tipo int.

Finalmente, el tipo del valor retornado por la función se especifica después de ella de lo misma forma que lo hacemos para los parámetros, así: Obj::health: int nos indica que la propiedad health del tipo Obj devuelve un valor de tipo int.

La lista completa de funciones se presenta en una sóla página, agrupadas según el criterio seleccionado. Colocando el ratón sobre tipos o funciones veremos un recuadro flotante que nos dará un breve resumen de la descripción del tipo o función mencionado. Haciendo click podemos navegar directamente hasta él.

En la parte superior de la página podemos elegir si agrupar las funciones por el tipo al que pertenecen (las que no sean métodos, es decir, que no pertenezcan a ningún tipo, aparecerán en la sección "Sin clase"), o por tipo del valor de retorno (útil cuando queremos obtener un tipo concreto, como una Query, pero queremos buscar la mejor función para ello entre las disponibles).

También encontramos en la parte superior un cuadro de texto para buscar funciones por nombre. El cuadro no distingue mayúsculas y minúsculas, y hará que se muestren únicamente aquellas funciones que contengan todas las palabras (separadas por espacios) que escribamos en el cuadro. Se permite el uso especial de los carácteres ^ y $. Si comenzamos una palabra con ^ se seleccionarán funciones que empiecen con el texto dado únicamente, mientras que si la terminamos con $ se seleccionarán sólo funciones que acaben con el texto dado. Si usamos ambos carácteres a la vez se buscarán funciones cuyo nombre sea exactamente el texto indicado.

En la parte inferior derecha de cualquier función, encontramos un símbolo de ancla (link) que nos permite copiar un enlace a la función fácilmente, en caso de que queramos referenciarla en alguna otra página.

De la misma forma, muchas funciones contienen una sección de funciones relacionadas, con enlaces a las mismas.

Páginas adicionales

Además de la página principal, que muestra una lista de funciones con sus descripciones, la guía también incluye una página con un listado de clases de objetos, con sus propiedades, métodos, animaciones y otra información relevante, así como una visualización en árbol de las mismas.

Haz click aquí para volver a la página principal