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
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!");
;
. 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.
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);
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);
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.
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)
.
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 nombreasentamiento
.Units
obtiene, del asentamiento retornado porGetSettlement
, 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 porpr
.- A continuación, se procede con la siguiente sentencia.
GetSettlement("asentamiento").GetCentralBuilding().pos
es ejecutado de forma similar al caso anterior, y finalmenteView
mueve la cámara a la posición retornada porpos
, 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 }
).
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
).
;
).
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.
;
) 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.
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):

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);
}
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.
;
) 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:

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:

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:

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);
}
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);
}
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.
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
.
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);
Nota: Para más información sobre las variables, consulta la sección 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.
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.
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...
}
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.
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.
Navegación
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 (
) 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.