12 de Julio, 2020

Mi calculadora es... Python

Resulta que sigo a Thilo en Twitter, y es un apasionado de las calculadoras. Es un tema interesante, aparte del coleccionismo, y me pilló una temporada que estaba haciendo muchos cálculos por temas de un proyecto para ZX Spectrum.

En un momento me planteé, ¿podría sacarle partido a una calculadora? Es posible, pero tendría que soportar hexadecimal, claro. ¿Hay calculadoras para programadores?

Tampoco es lo que quiero comentar en esta anotación, pero por distintos motivos los números en hexadecimal son cómodos para trabajar en máquinas de 8-bits. Por una parte porque las memoria suele estar organizadas en páginas de 16384 bytes, en un byte tienes dos grupos de 4-bit que puedes acceder muy convenientemente (es más fácil ver a qué bit se refiere 0x80 que, por ejemplo, en decimal 128), o simplemente por costumbre (¿te suena 49152? pues eso).

Al final estuve ojeando en eBay y conseguí una TI-36X solar casi por nada; y resulta que no me vale :D.

Mi TI
Iba a escribir 0xc000, pero me he comido un dígito :P

Sí, soporta hexadecimal (y octal y binario, algo no muy usual en las calculadoras científicas más comunes), pero resulta que hay algunos problemas en los que no caí:

  • Es una calculadora solar, y suelo programar de noche. Parece una tontería, pero resulta que en mi escritorio no hay suficiente luz para que la calculadora funcione bien :(. Bueno, tengo una lámpara flexo (no es ideal).
  • Estamos cada vez más mayores, y para ver la calculadora me tengo que quitar las gafas para ver de cerca :').
  • La calculadora tiene que estar muy a mano, sino no la uso. Y bueno, con los críos por aquí, es complicado :D.
  • Cambiar de costumbre es difícil.

Porque resulta que, sin darme cuenta, al final lo que acaba pasando es que abro un pane en tmux y ejecuto python.

Para cálculos rápidos, que además siempre los necesito cuando estoy programando, es una calculadora programable que no necesita luz, que veo bien (la pantalla está a la distancia perfecta), y que además puedo copiar sin problemas el resultado y usarlo donde corresponda. Y estoy acostumbrado a trabajar así (ni me acuerdo que vim puede hacer cálculos sencillos sin salir del editor).

Es una pena, porque me encanta la calculadora (aunque no sé casi usarla), pero resulta que trabajando con un ordenador... ya tengo algo para calcular: ¡Python!

Hay 0 comentarios, anotación clasificada en: programming, python.

3 de Julio, 2020

Punteros a campos de estructuras

Esta es otra técnica orientada a mejorar el código generado por el compilador (especialmente SDCC), cuando trabajamos con micros de 8-bits como por el ejemplo el Z80.

Usar punteros a campos de una estructura entra quizás en la categoría de usar variables globales, porque se trata de utilizar un método de acceso a variables en C que mejor se traduce a ensamblador de Z80.

El caso de uso es el descrito cuando expliqué mis listas en 8-bits:

mientras el juego no acabe:
   leer entrada teclado/joystick
   actualizar entidades
   dibujar entidades
   esperar si es necesario, para velocidad constante

Cuando recorremos la lista y llamamos por ejemplo a la función de actualizar estado para cada entidad, esa entidad tendrá acceso a sus datos usando un puntero a una estructura, que tendrá un número de campos.

Veamos cómo se acceden a esos campos con el siguiente ejemplo.

#include<stdint.h>

struct st_entity
{
    uint8_t type;
    uint8_t x;
    uint8_t y;
    void (*update)();
    void (*draw)();
    struct st_entity *n;
};

struct st_entity *it;

void update()
{
    it->x += 2;
    it->y++;
}

Como ejemplo no es muy significativo porque SDCC no va a generar muy mal código en una función tan pequeña, pero al menos podremos ver de una forma empírica cómo se accede a los campos de la estructura.

El código generado es:

_update::
;test2.c:17: it->x += 2;
	ld	hl, (_it)
	inc	hl
	ld	a, (hl)
	add	a, #0x02
	ld	(hl), a
;test2.c:18: it->y++;
	ld	hl, (_it)
	inc	hl
	inc	hl
	inc	(hl)
;;test2.c:19: }
	ret

El compilador carga en hl la posición de memoria guardada en el puntero a la estructura, y luego incrementa ese registro hasta llegar a la posición de memoria en la estructura donde están los datos que necesitamos.

Ya vemos como ha cargado el puntero cada vez, y se ha desplazado al campo. En estructuras más grandes y con más código, el compilador generará código que es mucho peor (probablemente usando más registros, incluso ix).

Una vez expuesto el problema, veamos una posible solución: usar punteros a los campos de la estructura.

En el código que llama a la función update prepararemos esos punteros antes de hacer la llamada y, aunque seguiremos teniendo el mismo problema en ese punto, el código generado en las funciones de actualización de todas nuestras entidades será más corto y más rápido.

El código que prepara la llamada sería algo como:

// variables globales
uint8_t *it_x, *it_y;

// antes de llamar a update
it_x = &it->x;
it_y = &it->y;

De esta forma la función que actualiza el estado de esta entidad podrá acceder a los campos x e y directamente usando *it_x e *it_y (que en C significa "opera con el valor apuntado por el puntero").

void update()
{
    *it_x += 2;
    (*it_y)++;
}

Aún en nuestro ejemplo tan simple se puede ver la mejora:

_update::
;test2.c:25: *it_x += 2;
	ld	hl, (_it_x)
	ld	a, (hl)
	add	a, #0x02
	ld	(hl), a
;test2.c:26: (*it_y)++;
	ld	hl, (_it_y)
	inc	(hl)
;test2.c:27: }
	ret

Mucho mejor porque evitamos tener incrementar hl para llegar al campo de la estructura porque estamos accediendo a él directamente con un puntero. Además vemos que algunas operaciones simples se pueden hacer directamente sobre hl en menos pasos (como incrementar una variable de 8-bits).

Esta técnica es probablemente la que más rendimiento me ha permitido conseguir en muchos de mis juegos para Z80, aparte de ayudar al compilador a conseguir código más compacto y ahorrar memoria (que muchas veces es muy escasa en estas máquinas).

La única pega quizás es la notación en C que es un poco incómoda, pero es muy fácil acostumbrarse y los beneficios bien merecen la pena ;).

Hay 3 comentarios, anotación clasificada en: c, programming, gamedev.

1 de Julio, 2020

»Trabajando en Brick Rick · Me quedé atascado con mi proyecto para MSX2, y parece que estoy haciendo un nuevo juego para Amstrad CPC que casi encaja con la idea que tenía en mente para MSX2, salvo que en el CPC tengo más color y me está resultando mucho más fácil dibujar. Las rutinas de scroll para el MSX2 están listas, y las usaré para otro proyecto, pero ahora mismo estoy con un arcade de pantalla única similar a Magica (el protagonista es un obrero llamado Rick que lanza ladrillos), con un nuevo motor que programé en un par de días :D, y que parece que promete.

Hay 0 comentarios, anotación clasificada en: cpc.

26 de Junio, 2020

Variables globales

Ya comenté en mi última anotación que, trabajando con micros de 8-bits, a veces es importante usar variables globales por temas de rendimiento, pero todo depende en gran medida de qué compilador estemos usando. En mi caso trabajo con SDCC para máquinas con Z80 como CPU.

SDCC no está al nivel de compiladores modernos, como GCC o Clang, y es una de las pocas opciones disponibles porque los compiladores modernos no tienen soporte para este tipo de CPU; y uno de los principales problemas es el número de registros y cómo el compilador decide qué registro usar cuándo.

En C las variables locales usan memoria que se obtiene de la pila cuando llamamos a una función. Vamos a ver un ejemplo sencilo.

#include <stdio.h>
#include <stdint.h>

uint8_t fn()
{
    uint8_t i, a;

    for (i = 0, a = 0; i < 10; ++i)
        if (i & 1)
            a += i;

    return a;
}

Si compilamos este código con SDCC y echamos un vistazo al código intermedio que SDCC genera, tenemos lo siguiente:

_fn::
;test.c:8: for (i = 0, a = 0; i < 10; ++i)
	ld	l, #0x00
	ld	c, #0x00
00105$:
	ld	a, c
	sub	a, #0x0a
	ret	NC
;test.c:9: if (i & 1)
	bit	0, c
	jr	Z,00106$
;test.c:10: a += i;
	add	hl, bc
00106$:
;test.c:8: for (i = 0, a = 0; i < 10; ++i)
	inc	c
;test.c:12: return a;
;test.c:13: }
	jr	00105$

Por defecto SDCC genera ficheros intermedios ASM con el código C en los comentarios, lo cuál es muy útil (aunque hay que saber algo de Z80).

En este caso el resultado es bastante bueno. El compilador ha hecho un buen trabajo usando registros para las variables locales, en lugar de usar la pila: c para el contador del bucle y l para acumular el resultado (además, el valor de retorno va en l). Seguramente no lo hubiéramos escrito así a mano (hubiera sido más eficiente que el contador fuera decreciente para evitar usar el acumulador), pero no está nada mal.

Las cosas se complican mucho cuando la función es más grande o, como en el caso de la semana pasada, se usan punteros. Veamos otro ejemplo.

#include <stdio.h>
#include <stdint.h>

uint8_t fn(uint8_t *p)
{
    uint8_t *b = p;

    while(b[1])
        ++b;

    return *b == *p;
}

Esta función devuelve 1 si en una cadena acabada en 0, el primer elemento es igual al penúltimo. Los detalles tampoco son importantes, pero digamos que "arma" es cierto y "escudo" es falso.

El código que genera SDCC se complica.

_fn::
	push	ix
	ld	ix,#0
	add	ix,sp
	push	af
;test.c:6: uint8_t *b = p;
	ld	c, 4 (ix)
	ld	b, 5 (ix)
	inc	sp
	inc	sp
	push	bc
;test.c:8: while(b[1])
00101$:
	pop	de
	push	de
	inc	de
	ld	a, (de)
	or	a, a
	jr	Z,00103$
;test.c:9: ++b;
	inc	sp
	inc	sp
	push	de
	jr	00101$
00103$:
;test.c:11: return *b == *p;
	pop	hl
	push	hl
	ld	e, (hl)
	ld	a, (bc)
	ld	c, a
	ld	a, e
	sub	a, c
	ld	a, #0x01
	jr	Z,00117$
	xor	a, a
00117$:
	ld	l, a
;test.c:12: }
	ld	sp, ix
	pop	ix
	ret

Hay que controlar un poco más de Z80 para entender completamente la salida del compilador, pero la idea básica son las operaciones que realiza al comienzo de la función para reservar memoria para nuestra variable local. Si usamos Z80count o una herramienta similar, veremos que salen unos 287 ciclos en total.

Supongamos que b es una variable global, y tenemos una función fn_g, el resultado sería:

_fn_g::
;test.c:18: b = p;
	pop	de
	pop	bc
	push	bc
	push	de
	ld	(_b), bc
;test.c:20: while(b[1])
00101$:
	ld	hl, (_b)
	inc	hl
	ld	a, (hl)
	or	a, a
	jr	Z,00103$
;test.c:21: ++b;
	ld	(_b), hl
	jr	00101$
00103$:
;test.c:23: return *b == *p;
	ld	hl, (_b)
	ld	e, (hl)
	ld	a, (bc)
	ld	c, a
	ld	a, e
	sub	a, c
	ld	a, #0x01
	jr	Z,00117$
	xor	a, a
00117$:
	ld	l, a
;test.c:24: }
	ret

Mucho menos código, que suele ser más rápido :D. En este caso son 192 ciclos.

No solo evitamos hacer todas las operaciones con la pila sino que además el compilador no ha usado ix, que es un registro bastante lento en el Z80.

No todo son ventajas. La principal limitación es que nuestro código no será reentrante (no podemos llamar a la función desde dentro de la función), y tampoco podemos suponer que el compilador siempre hará un mal trabajo con variables locales. Además hay casos que usar la pila será buena idea :D.

Aún así, en general creo que se puede asumir que usar la pila va a ser más lento, a no ser que el compilador haga un buen trabajo con el código generado y no use la pila, y en caso de punteros o variables más grandes que un byte, usar variables globales es bastante probable que sea más rápido.

Hay 0 comentarios, anotación clasificada en: c, programming, gamedev.

19 de Junio, 2020

Usando listas en 8-bits

Escribo muy poco por aquí, y llevo bastante tiempo que lo que escribo es más bien una bitácora de novedades en lo que voy haciendo en temas gamedev (hacer juegos).

Tengo un rato libre, así que a ver si consigo explicar cómo uso listas en mis juegos de 8-bits.

Las listas son la base del sistema de entidades que uso en mis juegos, que es además el corazón del bucle principal:

mientras el juego no acabe:
   leer entrada teclado/joystick
   actualizar entidades <- ¡recorrer la lista!
   dibujar entidades <- ¡recorrer la lista otra vez!
   esperar si es necesario, para velocidad constante

Que en realidad es probablemente como lo haría en C en cualquier otro entorno, solo que con algunas características especiales:

  • Usando variables globales. Esto es por temas de rendimiento, porque variables locales o parámetros requerirá trabajar con la pila y malgastaremos valiosos ciclos de CPU.
  • Nada de memoria dinámica. En realidad son dos listas, una con la memoria libre y otra con la memoria en uso. Como curiosidad, la idea original la tomé de cómo MINIX gestionaba la memoria en una de sus primeras versiones.
  • Opcionalmente, escrito en ensamblador, por temas velocidad.

Lo primero es definir la estructura que define el tipo de dato que usamos en la lista, con una estructura tal que:

struct st_entity
{
    uint8_t type;
    uint8_t x;
    uint8_t y;
    void (*update)();
    void (*draw)();
    struct st_entity *n;
};

Muy simplificado solo tenemos lo necesario, más dos propiedades de ejemplo (x e y).

Con esta estructura ya podemos declarar algunas variables globales:

// sp_used es el principio de nuestra lista de "en uso"
// sp_new siempre apunta a la última entidad creada
struct sp_entity *sp_used, *sp_new;
// sp_free es la lista de memoria libre
struct sp_entity *sp_free;
// entities es donde realmente reservamos memoria estática, con límite
struct sp_entity entities[MAX_ENTITIES];
// sp_collect indica cuando hay memoria para liberar
// de sp_used a sp_free
uint8_t sp_collect;

He puesto comentarios en el código, así que espero que esté claro.

Lo siguiente es inicializar la memoria. En los 8-bits el compilador de C es bastante posible que soporte una sección para inicializar variables, pero sueles ser menos eficiente en temas espacio.

void init_entities()
{
    uint8_t i;

    // todo a 0
    memset(entities, 0, sizeof(struct st_entity) * MAX_ENTITIES);

    // podemos el puntero "siguiente" apuntando al siguiente en la lista
    for (i = 0; i < MAX_ENTITIES - 1; ++i)
        entities[i].n = entities + i + 1;

    // nada en uso, todo libre, nada que liberar
    sp_used = NULL;
    sp_free = entities;
    sp_collect = 0;
}

Hasta ahora es bastante simple, ¿verdad? Lo único es el tema de las variables globales (que no he usado en la variable del bucle porque esto se ejecuta una vez, no hay problemas de rendimiento; y es posible que en un trozo de código tan sencillo el compilador haga un buen trabajo usando un registro en lugar de la pila).

¿Cómo obtendríamos una nueva entidad?

uint8_t new_entity()
{
    if (!sp_free)
    	return 0;

    sp_new = sp_free;
    sp_free = sp_free->n;
    sp_new->n = sp_used;
    sp_used = sp_new;

    return 1;
}

Si reservamos memoria con éxito, la función devuelve 1; y tendremos la entidad en sp_new. Sino es 0 y, bueno... hemos intentado manejar más entidades de lo que habíamos planeado y habrá que reaccionar en consecuencia.

Una vez que tenemos una nueva entidad, hay que inicializar los campos de la estructura:

if (!new_entity())
    panic();

// aquí usaría un enum; asumamos 0 es libre y 1 en uso
sp_new->type = 1;
sp_new->draw = my_draw;
sp_new->update = my_update;

// x, y son cero, para el ejemplo nos vale

De esta forma draw y update que son punteros a funciones, apuntarán al código que tiene que dibujar y actualizar esta entidad (en realidad este tipo de entidad).

Solo nos quedan dos cosas: recorrer la lista y destruir (liberar) entidades.

Recorrer la lista es muy simple. La función que llamamos para dibujar o actualizar el estado tendrá en sp_it un puntero a los datos de la entidad (en este ejemplo tiene x e y).

// este es nuestro "iterador", y mejor global y con cuidado
// de no usarlo en dos sitios a la vez
struct sp_entity *sp_it;

// recorremos la lista de usados llamando a nuestra función de dibujado
// de cada entidad
for (sp_it = sp_used; sp_it; sp_it = sp_it->n)
    sp_it->draw();

En este caso asumimos que todas las entidades en nuestra lista tienen que ser dibujadas, porque en la rutina de dibujado tiene una regla: no se pueden destruir entidades.

Para actualizad entidades hacemos algo similar, solo teniendo en cuenta que al final vamos a liberar memoria (estoy haciendo un esfuerzo para no llamar a esto garbage collector porque se parece, pero no es :D).

// este es nuestro "iterador", y mejor global y con cuidado
// de no usarlo en dos sitios a la vez
struct sp_entity *sp_it;

// recorremos la lista de usados llamando a nuestra función de dibujado
// de cada entidad
for (sp_it = sp_used; sp_it; sp_it = sp_it->n)
    sp_it->update();

// si hay memoria a liberar, lo hacemos
if (sp_collect)
    free_entities();

La diferencia es que en update podemos marcar una entidad para ser destruida; y lo hacemos con:

// 0 para no en uso
sp_it->type = 0;
// avisamos que hay una entidad a liberar
sp_collect++;

Como sp_it apunta a la entidad que estamos procesando, podemos hacer el cambio desde nuestra my_update.

Liberar memoria, que simplemente es manipular la lista de forma que la entidad que estaba en uso pasa a libre, es la parte más complicada y costosa (por eso liberamos todas las entidades de una vez):

void free_entities()
{
    // si no hay nada que liberar...
    if (!sp_used)
        return;

    // dos iteradores: actual, anterior
    for (sp_it = sp_it2 = sp_used; sp_it && sp_collect;)
    {
        // 0 era libre
        if (sp_it->type == 0)
        {
            sp_collect--;

            if (sp_it == sp_used)
            {
                sp_it2 = sp_free;
                sp_free = sp_used;
                sp_used = sp_used->n;
                sp_free->n = sp_it2;
                sp_it = sp_it2 = sp_used;
                continue;
            }
            else
            {
                sp_it2->n = sp_it->n;
                sp_it->n = sp_free;
                sp_free = sp_it;
                sp_it = sp_it2->n;
                continue;
            }
        }
        sp_it2 = sp_it;
        sp_it = sp_it->n;
    }
}

Básicamente es recorrer la lista moviendo entidades marcadas como libre de la lista de entidades en uso a entidades libres, además contando cuántas entidades nos quedan por liberar, para poder abandonar la lista cuanto antes y ahorrar tiempo.

Y esto es todo. Se puede ver un ejemplo con Z88DK, que incluye uno de mis juegos para ZX Spectrum.

Espero no haber metido la pata con ninguno de los ejemplos (aunque puede ser útil, así es más importante entender que copiar código). Igual si tu juego no tiene que crear/destruir entidades, o no te preocupa que crear una entidad pueda ser más lento (en este caso es O(1)), con un array ya lo tienes (al final tampoco manejamos tantas entidades a la vez).

He tardado un poco más de la cuenta en escribir todo esto, pero no ha estado mal. Intentaré repetir, ¿una vez a la semana? Veremos :D.

Hay 2 comentarios, anotación clasificada en: programming, gamedev, c.

1 de Junio, 2020

Scala.js y Canvas 2D

Cuando hice mi reto de un juego al mes (en el 2014), probé varios lenguajes y plataformas, incluyendo un poco de Javascript y Canvas 2D (incluso acabando algunos juegos, como por ejemplo Alien Gamma).

Desde entonces, parece que el soporte para Canvas 2D acelerado por hardware en Linux es de primera, pero Javascript sigue sin gustarme (que da para una anotación larga, incluso fijándonos en las partes buenas, ya escribí entonces JavaScript es un campo de minas).

Hace tiempo probé Dart, y era muy solo Google y casi hacía falta usar su propio IDE para ser productivo (algo que no me atrae demasiado); e imagino que es más o menos por lo que no me ha interesado TypeScript (viene de Microsoft, aunque creo que sí se podría trabajar bien con VIM, no sé porqué no me llama; parece la mejor opción porque es una capa muy fina sobre JavaScript y el rendimiento es muy bueno).

Al final, como estoy trabajando a diario con Scala, ¿por qué no probar Scala.js? Y la verdad es que está muy bien: es como programar en Scala, con las herramientas a las que estoy acostumbrado y casi toda las cosas que tiene el lenguaje, pero para la web (compila a Javascript).

He estado haciendo experimentos que se pueden seguir en este repositorio: scalajs-canvas2d. Por ahora no he hecho ningún juego, porque necesito dibujar gráficos y tener la idea de qué programar, pero me gustaría hacer algo con Canvas y, posiblemente, que sea multi-jugador (con un componente servidor, ¡escrito en Scala!). Suena interesante, a ver si saco tiempo y sigo avanzando la idea.

Hay 0 comentarios, anotación clasificada en: scala, programming, javascript.

24 de Mayo, 2020

»Volviendo a leer blogs · Hace mucho tiempo (que parece una eternidad), Google Reader cerró, y además el buscador decidió ignorar a las bitácoras; y por una u otra cosa (osea: redes sociales), dejé de tener un lector de feeds instalado. Porque Hacker News ya me daba parte del contenido, y porque la mayoría de las bitácoras que seguía dejaron de escribir. Voy a retomar el hábito y, con suerte, estar más informado. He probado varios servicios y por ahora Inoreader me parece muy buena opción (lo estoy usando en Android, para empezar; ¡y esta bitácora tiene 19 lectores!). Seguiremos informando.

Hay 3 comentarios, anotación clasificada en: blog.

9 de Mayo, 2020

»Nuevo proyecto para MSX2 · Después de que publicara dos juegos para el MSX (Night Knight y Uchūsen Gamma), imagino que era inevitable que algunos fans del sistema me pidieran que hiciera un juego para MSX2. Se me ocurrió decir en mi Twitter que había mucha gente pidiendo pero que nadie me enviaba un MSX2. Bueno, resulta que alguien me envió un MSX2, así que ahora estoy un poco comprometido a hacer un juego para la segunda iteración del estándar japonés ;). Afortunadamente las cosas no han cambiado mucho, por ahora he decidido usar un modo gráfico similar al que ya conozco pero que me deja elegir una paleta de colores, más sprites por linea (¡con más colores!), y scroll vertical por hardware. Aún falta por ver de qué va a ir el juego, pero la parte técnica creo que ya está lista.

Hay 0 comentarios, anotación clasificada en: msx.

17 de Abril, 2020

»Kitsune's Curse · Ya está publicado, en su página web: Kitsune's Curse. Ya comenté que ha sido un camino largo y, aunque es un personaje que me gusta mucho, me alegro de haber cerrado la historia de Kitsune. Ahora a disfrutar un poco de lo que me vaya comentando la gente (espero que positivo :D).

Actualización: ya van saliendo reacciones.

Hay 0 comentarios, anotación clasificada en: cpc.

13 de Abril, 2020

El camino a Kitsune's Curse

Bueno, parece que Kitsune's Curse está acabado. La primera versión candidata para ser publicada está con los probadores para ver que todo está bien.

Este juego ha tardado mucho más de lo habitual, por varios motivos, que voy a comentar por aquí.

Cargando...
Pantalla de carga del juego

Primero porque he implementado un nuevo motor de tiles y sprites, y la verdad es que me llevó tiempo porque es bastante complejo (igual demasiado).

La idea se me ocurrió una vez acabado Magica, buscando usar menos memoria para un buffer doble. En lugar de tener un bloque de memoria lineal para hacer operaciones y luego volcar los cambios a vídeo lo más rápido posible (evitando así parpadeos), se trata de tener muchos bloques pequeños que se usan (y reutilizan) según hace falta.

Esto permite reducir el uso de memoria de los 12800 bytes que usaba Golden Tail a usar tan solo 2560 bytes, lo cual permite más gráficos, pantallas, música; bueno, más de todo a cambio de más complejidad y de sacrificar un poco de velocidad.

El motor me llevó un tiempo, y cuando estaba acabado, me encontré con el segundo problema: hacer una continuación es mucho más difícil de lo que pensaba :(.

La mecánica de Golden Tail es bastante especial, así que la continuación tenía que usar la misma idea, sin ser otra vez el mismo juego. En un principio me puse con un guión muy interesante, que luego resultó ser impracticable porque no sabía como dibujar las cosas que tenía que dibujar :D.

Así que me puse a mirar otro tema y acabé programando The Dawn of Kernel, usando el motor que tenía.

Tampoco es tan malo, porque Dawn of Kernel es un buen juego que me alegro de haber hecho, y además siendo más exigente que la idea original para la que quería usar el motor, me obligó a mejorar el motor en muchos aspectos; y todo esto se ha notado para bien en Kitsune's Curse.

Así que al final desde Agosto del 2017, hasta que (con suerte) el juego se publique la semana que viene, ha sido bastante tiempo. Evidentemente no he estado trabajando en el proyecto de forma constante, y he publicado 3 juegos entre medio, pero alguna duda he llegado a tener de si acabaría el juego o no.

Me parece que al final ha quedado bien: es más grande que el original, comparte la mecánica principal (corrigiendo lo que estaba mal), hay nuevos retos (puzzles con palancas y tiempo, por ejemplo), y más enemigos.

¡Espero que guste! Sobretodo a los fans del primero, porque este juego está principalmente pensado para ellos (aunque no es necesario haber jugado a Golden Tail). Ya queda poco.

Hay 1 comentario, anotación clasificada en: cpc.

Entradas antiguasEntradas nuevas