@ agnasg

¿Cómo se hizo tilemovers?

15-01-2021 3:04 PM

¿Por qué hacerlo fácil si lo podemos hacer difícil?
— típico programador sabelotodo

Sinópsis

tilemovers es un juego de sliding tiles (¡baldosas deslizantes!) donde el objetivo es mover un tile de la posición inicial a la final. Este tipo de juegos es como los “3 Chiflados”: el 50% de la gente los ama, y el 50% de la gente los odia. Para colmo, le he agregado 3 niveles: “Normal”, “Nightmare” y “Apocalypse”. Es decir, fácil, difícil y superdifícil. Pero para el que conoce este tipo de juegos debería ser divertido.

La diferencia con los otros 1000’s juegos similares es que los tiles vienen en 4 tamaños: 1×1, 1×2, 1×3 y el endiablado 2×2. La página oficial para descargar el juego es https://agnasg.itch.io/tilemovers, también hay un video (no apto para cardiacos) https://www.youtube.com/watch?v=wbcf3ICA2es.

El devlog en español está aquí https://www.micronosis.com/category/tilemovers/ y en inglés en tigsource https://forums.tigsource.com/index.php?topic=71419.0. Lo pueden jugar en windows (https://agnasg.itch.io/tilemovers) o en Android (https://play.google.com/store/apps/details…).

Módulos

tilemoves se puede descomponer en 4 módulos:

  • El sistema gráfico implementado en SDL2
  • La lógica del juego que implementa los movimientos de los tiles, sus tipos y los comandos que los gobiernan.
  • El módulo de bases de datos que usa una bd SQLite para almacenar y cargar la información del juego.
  • Un editor que permite crear los puzzles y los almacena en la base de datos.

En este post vamos a hablar de este último módulo, el editor de puzzles. Cuando entendí la necesidad de tener una herramienta para agregar, quitar, cambiar el tipo de tile, y guardar los cambios, me propuse los siguientes objetivos:

  • Facil de usar, es decir agregar y borrar los tiles debe ser rápido y fácil.
  • Fácil de cambiar el tipo de tile (estático, rápido, angel y humano)
  • Integrado al juego para repetir el ciclo de probar, cambiar, probar rápidamente.
  • Debe ser fácil de implementar

Para cumplir con todos estos requerimientos es invevitable que el panel de control quede congestionado sobretodo porque mi objetivo es que funcionara integrado al juego. Estuve viendo algunas ideas de cómo hacer el gui, incluyendo paneles como este:

DeadImgui Example

Para lo cual tendría que utilizar una tercera librería (ya el juego está usando SDL2 y SQLite) y eso no es bueno, al menos para este proyecto. Estoy pensando en planificar el estudio para analizar el posible uso de DearImgui para los gui de los siguientes juegos incluyendo el próximo juego (memory + arcade + platform, todavía sin nombre) y para khpx, pero no para tilemovers porque eso iba a agregarle más tiempo al ya abultado proyecto.

Por eso simplifiqué todo y me quedé con un panel incrustado con todos los botones:

La primera línea son operaciones con los tiles: intercambiar posición, cambiar su estatus, agregar nuevo tile, eliminar tile. Luego botones para guardar cambios, guardar como un nuevo puzzle, opciones para generar un puzzle en forma aleatoria (cuando estaba sin mucha inspiración). También opción para cambiar de nivel cuando su nivel de dificultad no era apropiado para su nivel actual. Finalmente botones para asignar el tipo de tile: faster (se mueve en todas direcciones), static (no se mueve), angel (se mueve verticalmente, human (se mueve horizontalmente).

Las operaciones de agregar y borrar tile se ven mejor en video:

Conclusión

No llevé contabilidad de cuánto tiempo le dediqué al editor específicamente, pero a grosso modo fue cercano a un mes (lo cual podríamos traducir en 24-30 horas, que es lo que yo le dedicaba mensualmente a este proyecto). En comparación al total de tiempo empleado podría decir que el editor ocupó un 20% del tiempo del proyecto. Creo que es el tiempo mejor empleado en todo el proyecto.

Cómo migrar una db implementada en Windows y SQLite a Android sin decir grocerías

06-01-2021 8:23 AM

Object-oriented no es el único patrón de diseño válido. A muchos programadores se les ha enseñado a pensar puramente en términos de objetos. Y, para ser justos, los objetos son a menudo una buena manera de descomponer un problema. Pero los objetos no son la única manera, y no siempre son la mejor manera de descomponer un problema. A veces el viejo y buen código de procedimientos es más fácil de escribir, más fácil de mantener y entender, y más rápido que el código orientado a objetos.

Por qué SQLite está programado en C

En el post anterior, describo mis peripecias migrando un juego desarrollado en C++ y SDL2 a Android.

El juego es tilemovers y ya está publicado en itch.io. Puedes echarle una mirada.

Versión Android en el Google Store. Por favor descarga la versión Android y enviame tus comentarios.

En este post describo cómo fue el proceso de migrar el código relacionado a SQLite y la base de datos. Por qué mi juego tilemovers requiere una base de datos, es tema de otro post, baste decir, por ahora, que la idea detrás de programar videojuegos es la diversión intrinseca en hacerlo, es decir, para mí tiene/debe ser divertido hacerlo, y no hay nada más divertido que programar juegos que usen complicadas bases de datos. Si alquien se divierte jugando el juego, es un extra maravilloso, pero no requerido.

Como explico en el post anterior, si es la primera vez que programas una aplicación para Android (y para mobile en general) hay muchas preguntas obvias cuya respuesta desconoces (aunque es algo que un programador de mobile sabe desde el prescolar y Plaza Sésamo).

¿Dónde se guardan los archivos, y en particular, la base de datos de SQLite?

Es un asset como cualquier otro y se declara en la carpeta assets dentro de la estructura de archivos de Android manejada por Android Studio (en adelante AS). Es decir,

\app\src\main\assets

pero no puedes escribir ahí, es decir, esos archivos son read-only. Tienes que moverla al área de datos de la aplicación (se dice fácil pero es una epopeya: leer más adelante)

¿Y hay que pedir permiso para escribir en la base de datos?

Como dice la documentación oficial, la aplicación no requiere autorización para leer o escribir en los directorios asignados para almacenamiento interno (de nuevo, no estamos hablando de la carpeta assets, donde solo se puede leer)

¿Por qué no se puede abrir un asset usando ifstream o apuntadores a archivos?

De acuerdo a esta respuesta en stackoverflow, porque no. Pero en el ámbito de esta respuesta, si se puede utilizar sqlite.c (es decir no hay que migrar SQLite a la versión java/kotlin) pero la base de datos tiene que estar en el área de datos de la aplicación. (¿por qué estoy repitiendo esto tantas veces? Porque un programador avezado y con experiencia en otras plataformas va a encontrar bien complicado de entender que una aplicación tiene al mismo tiempo 2 áreas de datos asignadas, una read only, y la otra read-write-has-lo-que-quieras).

Luego de mi investigación (que puede no estar completa, y puede contener errores). Las alternativas a la mano para trabajar con bases de datos o archivos de datos en una aplicación Android son las siguientes:

  1. Necesitas trabajar con AAssetManager, AssetManager_open, AAsset_read para acceder a la base de datos tal como se explica aquí. Esta es una solución implementada en C usando NDK. Se llama SQLite-NDK. No hice esto, aunque dejo abierta la posibilidad para otros juegos, ver abajo.
  2. La bases de datos almacenada en assets no se puede usar directamente, hay que instalarla en:
    “data/data/your-package-name/databases” (ver punto 5 abajo)
  3. Una idea es tener los datos en un archivo json o xml, crear la base de datos y luego cargar los datos (como se explica aquí).
  4. Otra sugerencia es usar el Content Provider
  5. Otra idea es utilizar una clase DataBaseHelper derivada de SQLiteOpenHelper para copiar la base de datos de assets al área de datos de la aplicación. Aquí se explica cómo es el proceso. Esta explicación al parecer es más clara. Este fue el procedimiento implementado.
  6. Para obtener la ubicación donde se va a ubicar la base de datos se debe utilizar:
    File dbFile = context.getDatabasePath(name_of_database_file);
    (ver aquí, y ver documentación en developer.android.com, hay que usar esta función o la aplicación puede fallar en algunos teléfonos inteligentes nuevos, por ejemplo, Huawei)

Hay varios retos para hacer que todo esto funcione. Primero hay que averiguar cómo llamar código escrito en java desde la aplicación desarrollada en C/SDL2 (sí, en cierto momento surge la pregunta de por qué no migrar completamente a kotlin (el lenguaje nativo de aplicaciones android) y la respuesta es que la idea desde el comienzo es migrar una aplicación desarrollada en C++, en Windows usando SDL2 a Android).

La solución es simple tal como se explica aquí. El siguiente paso es cómo integrar una clase java a una aplicación desarrollada usando NDK, dentro de Android Studio (lo inverso es fácil y aparece aquí, es decir, como usar NDK desde una aplicación desarrollada en kotlin).

Lo interesante es que viendo ese artículo recordé que Android Studio tiene menus, muchos menus, y entre ellos hay un “Add Java class”, el cual funciona pero no necesariamente sobre el nombre del proyecto (como mi intuición me indica) sino sobre la carpeta del paquete que en el caso de mi aplicación basada en sdl se llama org.libsdl.app

Al agregar la clase, Android Studio pregunta a cuál paquete lo vamos a agregar, indicamos org.libsdl.app. Copiamos el contenido de nuestra clase al archivo creado. Este archivo es almacenado en app\src\main\java\org\libsdl\app

Nuestro archivo java es basicamente una clase ( DataBaseHelper) que verifica que la base de datos existe. Si no existe la copia desde assets al área de datos. La soluión completa está aquí.

La declaración de la clase luce así:

public class DataBaseHelper extends SQLiteOpenHelper

Ahora, como dije antes todo parace simple siempre y cuando puedas hacer llamadas a java desde tu aplicación desarrollada en C.

Para ello, básicamente tienes que usar esta llamada

jclass dbhelper = env->FindClass(“org/libsdl/app/DataBaseHelper”);

donde estás declarando un apuntador (creo que en el universo java tiene otro nombre) a la clase (que debes identificar colocando el path completo)

El primer problema es cómo consigues inicializar la variable “env”. Si comienzas a buscar en stackoverflow quedas en un lazo infinito porque para inicializarla, necesitas hacer;

JNIEnv *env;
g_JavaVM->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6)

Y ahora tu problema es como inicializar la variable g_JavaVM, el apuntador a la máquina virtual. El cuento es largo y lo voy a omitir, al final la forma de hacerlo es inicializar la llamada en el momento de carga de JNI, lo que se hace a través de esta función:

JavaVM* g_JavaVM = NULL;
extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved) {

   g_JavaVM = vm;
   return JNI_VERSION_1_6;
}

Yo estuve varias horas tratando de utilizar Android_JNI_GetEnv(void); sin suerte, función que se encuentra declarada en SDL_android.h (en el código de la versión Android de SDL2) pero no funciona. La única forma que descubrí fue con la llamada JNI_OnLoad ().

Luego que tienes el apuntador a la clase debes crear una instancia de la clase (un objeto) lo que haces a través de lo siguiente:

jclass javaGlobalClass = reinterpret_cast<jclass>(env->NewGlobalRef(dbhelper));

jmethodID helperConstructor = env->GetMethodID(javaGlobalClass, “<init>”, “(Landroid/content/Context;)V”);

Con esto puedes hacer la llamada para la ejecución del constructor de la clase:

jmethodID helperConstructor = env->GetMethodID(javaGlobalClass, “<init>”, “(Landroid/content/Context;)V”);

Y con el contructor haces la llamada a createDataBase:

jmethodID createDataBaseMethod = env->GetMethodID(dbhelper, "createDataBase", "()V");
env->CallVoidMethod(obj, createDataBaseMethod);

Y con la base de datos, ya puedes acceder a ella utilizando las llamadas de SQLite de forma normal, tal como lo haces en cualquier programa C/C++ implementado con SDL2, que en nuestro caso es tilemovers.

Cómo migrar un juego desarrollado con SDL2 en Windows a Android

04-01-2021 1:22 PM

“El hombre, en su miserable condición, levanta con la mente complicadas arquitecturas y cree que aplicándolas con rigor conseguirá poner orden al tumultuoso y caótico latido de su sangre” Álvaro Mutis

Nunca digas no

Durante los últimos 18 años, que he estado trabajando como freelancer, me han preguntado al menos una vez al mes si trabajo mobile. Es decir, si hago aplicaciones para el play store. Mi respuesta indubitable siempre ha sido “No“. A veces “Yo no trabajo eso” con énfasis en “eso“. Y cuando estoy realmente de mal humor “Que no chico, que no“.

Hoy, estoy descargando Android Studio (AS), el jdk actualizado, y otras librerías para hacer mi primera aplicación “comercial” en el play store. He trabajado algunos proyectos formando parte de un equipo y mi trabajo ha consistido en resolver “detalles“. En estos casos hemos usado Qt. Esta es la primera vez que hago un proyecto 100% utilizando Android Studio, yo, solo.

Lo que pasa es que yo aprendo lentamente: las primeras 3 reglas que debemos aprender en la vida de los negocios son:

  1. No permitas que nadie dirija tu vida (“Claim your life“)
  2. Aprende a ser despiadado (“Learn to be an asshole“)
  3. Nunca digas no. (“two are enough!!!”)

Tardé 41 años para aprender la primera regla, 37 segundos para aprender la segunda y 18 años para aprender la tercera. Nunca es tarde para aprender, ¿cierto?

Si eres programador freelance, tienes sólidos conocimientos de negocios y quieres expandir tus horizontes (¿quién no?), mobile es una apuesta inevitable. Además, no se trata de hacer un mmo con temática espacial y que requiere 3 años de desarrollo (como khpx). Los juegos mobile son, casi por definición, fáciles de jugar y “fáciles” de programar (¿cierto?)

Los juegos en el play store tienen que ser de bajo nivel, nivel idiota por decirlo de alguna forma. Estilo flappy bird. Y por lo que se puede ver con khpx, a mi me gustan los juegos complicados, y tardar años en ellos. Así que voy a tratar de migrar mis juegos cortos (que tengo unos cuantos) y luego paso a juegos complicados.

¿Tardar años en ellos?” ¿Qué clase de loco soy? ¿Cuál es el problema? Si 3 años te parece demasiado, piensalo de nuevo. Por ejemplo, este señor dedicó 11 años de su vida a hacer un Line Rider Track!!!

A continuación mis anotaciones sobre el proceso de migrar tilemovers, un juego de tiles, que se mueven, y son tiles, (y se mueven) y la idea es mover uno de ellos desde la posición inicial, hasta la posición final. Listo. Esa es toda la documentación. The end.

Esta decisión de colocar khpx en suspenso y trabajar juegos pequeños no es al azar. Hubo mucha intelectualización (una palabra que acabo de inventar). Baste decir que a veces, un resultado temprano es necesario. Hay otras consideraciones pero este post está demasiado largo, así que no va a pasar la edición final. Aquí algunos comentarios sobre juegos pequeños:

Hacer juegos cortos e intensos:
piensa en haiku, no en épico.
Piensa en poesía, no en prosa.”

Más adelante estaré migrando otros: un juego de memoria, un juego tipo asteroides y un juego tipo counter strike pero muy simple y por supuesto monojugador.

Voy a trabajar en:

  • C++ (no hay sorpresas aquí)
  • SDL2 (podría ser Qt pero la parafernalia es demasiado aparatosa (y parafernálica), lo cual no digo que la de SDL2 no lo sea, pero es más por el tema de AS)

Retos

SDL2. El problema con SDL2 es SDL. Hay dos versiones y en todas partes (StackOverflow e inclusive algunos sitios de tutoriales sobre SDL*) se confunden las filosofias de los dos. El enfoque es ligeramente diferente y las llamadas son diferentes. SDL2 se parece a SDL pero es diferente. De hecho, el mencionado juego de memoria está implementado en SDL, y lo he estado viendo de reojo, o sea, abro el archivo, lo veo, salgo corriendo, me tomo nerviosamente un whisky, lo vuelvo a ver, me tomo otro whisky. Etcétera. Creo que lo voy a reescribir en SDL2 sin siquiera verlo otra vez. De hecho voy a borrar todo el directorio que lo contiene:

format c: \ memorybreaker

Listo lo borreddddddddddddd

Es infinitamente repetitivo. Después de 50 juegos realizados comenzar a migrar/adaptar un juego a una nueva plataforma es extremadamente aburrido y repetitivo. Los tiempos en que algo nuevo me entusiasmaba ya pasaron así que tengo que armarme de entusiasmo y optimismo.

Android. Ya he hecho algunas cosas, nada del otro mundo así que esto es nuevo, o casi nuevo. No conozco Android Studio así que veremos.

Ahora, el problema desde el punto de vista de negocios es que este proyecto (publicar una aplicación en el play store y ganar dinero con eso) se supone que debe ser algo que no debe durar más de 2 semanas de comienzo a fin: voy ya por la segunda y apenas la aplicación está lista para comenzar la migración. Además de la migración, falta el marketing, otras muchas cosas. (NOTA: desde que escribí eso hasta hoy ya han transcurrido 1 2 3 4 5 meses).

Es como el proceso de ingresar en Tiragarde Sound (World of Warcraft) un alt, debería ser rápido (10, 15 minutos?) Pero en realidad son 2-3 horas. Finalmente yo soy un programador como Arthur Bishop (el personaje interpretado por Charles Bronson en Asesino a precio fijo (ves, otra película que en español tiene un mejor nombre (en inglés es The mechanic (los que han leído otros post en este sitio sabrán de qué estoy hablando ))))

Arthur Bishop recibe contratos para asesinar a funcionarios rivales, millonarios etc. Pero él es un asesino metódico e infalible: él estudia a su objetivo durante semanas para descubrir los puntos débiles de seguridad en su rutina diaria. Una vez identificado el momento perfecto cuándo su victima es más vulnerable, procede a ejecutar a su objetivo.

Yo procedo de una forma similar. Tengo 3 días pensando un algoritmo para generar los niveles del juego de una forma automática. Pronto estará liquidado. Pero va a tomar su tiempo, necesito verificar todos sus ángulos.

Nota del editor: lo anterior fue escrito alrededor del 19 de julio, y los planes segun se puede entender, es que tilemovers mobile esté listo en 1-2 semanas. Hoy es 16 de octubre.

El proyecto se demoró un poco porque se me ocurrió hacer un algoritmo para resolver los puzzles. Resulta ser que este es un problema complicado. Pasé un mes haciendo un programa no recursivo que algunas veces resuelve el puzzle, otra veces comienza a generar soluciones con muchos pasos innecesarios. Entonces pasé 2 semanas haciendo un programa para eliminar los pasos inncesarios. Finalmente decidi probar los puzzles a mano. El programa quedará como un caso de estudio. Si alguna vez lo termino colocaré el resultado en github como una contribución a la humanidad.

Tratar de hacer un programa para resolver uno de estos puzzles es algo bien difícil de justificar. Es como Kurz (Apocalipsis ahora) entrenando para ser paracaidista a los 38 años. “¿Por qué hizo algo así?

Yo tengo una buena razón. Lo prometo. Es una muy buena razón. Estaba tratando de invertir la búsqueda de buenos puzzles. Al diseñar los puzzles se llega a un momento en que las ideas se agotan. Así que quería hacer un algoritmo capaz de resolverlos, y de esa forma encontrar puzzles con soluciones difíciles. Es decir, diseñar el puzzle basado en soluciones difíciles de encontrar. Este camino no funcionó.

El juego tiene 5 4 3 niveles de dificultal. El primer nivel Fácil Normal tiene 512 256 128 puzzles (todos probados a mano. En algún sitio tengo notas sobre algunos de ellos, los publicaré en un próximo post). El segundo nivel Difficult Imposible tiene 64 32 28 y el tercer nivel Kill me now Apocalipse tiene 12 puzzles. En futuras versiones a estar disponibles pronto agregaré más puzzles.

Versión Windows

Como está programado en SDL2 potencialmente se puede publicar además de Windows en Linux, Ios y Android. La versión windows se puede descargar aquí:

https://agnasg.itch.io/tilemovers

Es gratis. Si usted lo desea puede contribuir con $2 o más, lo cual me permitirá hacer más juegos similares (más rápidamente).

También puede contribuir en mi página de patreon que me ayudará a terminar mi mmo, khpx5 y otros juegos del baúl de los zombies.

Versión Android

Nota: esta sección discute mis vicisitudes migrando tilemovers a Android. Es una aventura con mucha acción, intriga, emboscadas, romance, suspenso y un desenlace feliz.

Versión Android en el Google Store.

Protagonistas:

Android Studio: el ambiente de desarrollo (algo así como Visual Studio)

Gradle: el build system, permite organizar y ejecutar la compilación y linkeo con todos los componentes involucrados.

ndk: Native Development Kit, un conjunto de herramientas que permiten usar C++ con Android.

Para la instalación de android, cosa que que hago por primera vez (como dije al comienzo yo nunca he trabajado con app mobile) seguí esta guía. https://lazyfoo.net/tutorials/SDL/52_hello_mobile/android_windows/index.php. Hay otras guías en google, pero esta me pareció la mejor.

Nota (04-01-2021): otra guía es esta que está ligeramente actualizada y tiene explicaciones pormenorizadas. Lamentablemente la encontré muy tarde. Como dice el autor, “estás entrando en el reino de desarrollo Android… respira profundo…“.

Como suele suceder en estos casos la guía está desactualizada y en el paso 5 las cosas comenzaron a aparecer diferentes. En el momento de la instalación, no aparece la pregunta sobre la ubicación del Android SDK (que debe ser inicializado a c:\androidsdk). Esa pregunta aparece luego de instalar y ejecutar por primera vez la aplicación.

La guía está tan desactualizada que indica instrucciones para Android SDK 16 (la versión que estoy instalando en octubre de 2020 dice Android 30. Hay errores que no me sucedieron a mi, y otros que sí: por ejemplo, el paso 15 Error:(688) Android NDK: Module main depends on undefined modules: SDL2.

Problemas encontrados: muchos, incluyendo algunos inexplicables. Uno de los tantos escoyos (algo que particularmente sucede con aplicaciones Java) es que hay múltiples versiones de multiples paquetes y tablas de compatibilidades entre paquetes y multiples errores debido (quizás) a esas incompatibilidades. Por ejemplo, aqui aparece las compatibilidades entre versiones entre Android Gradle Plugin y Android Gradle: trabajando este proyecto tuve que revisar innumerables veces stackoverflow, y encontrar tablas como esta con mucha frecuencia.

En resumen, el error que no se encuentra SDL2 se resuelve tal como se explica en la guía. El error “fatal error: ‘string’ file not found” se presta para confusión porque el archivo makefile “Android.mk” no tiene la línea:

#APP_STL := stlport_static

Simplemente se agrega y el error desaparece. (esta línea no hace falta, ver más abajo)

Otro error que apareció no está incluído en la guía:

Unsupported method: TaskExecutionResult.getExecutionReasons()

La consola indica que hay una incompatibilidad con Gradle sin dar mayores detalles. Un paso que me salté en la guía es el paso 12 porque a mi no me apareció el error:

Minimum supported Gradle version is 4.1. Current version is 2.14.1.

Pues bien, en la nueva versión, o debido a que yo tenía la versión 3.1.x el mensaje de error es diferente (después de varios tropiezos, resbalones, caidas, risas, gritos desconsolados a la media noche, alaridos y otras manifestaciones de frustración apareció sin explicación el mensaje ” Minimum supported Gradle version“. Si siempre estuvo ahí, no estoy seguro ahora) . Para resolverlo se abre el archivo build.gradle del proyecto y se modifica la línea

classpath ‘com.android.tools.build:gradle:3.1.4’

a

classpath ‘com.android.tools.build:gradle:4.0.2’

y en el archivo gradle-wrapper.properties la línea distributionUrl debe decir:

distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip

(Aquí 6.1.1 se debe sustituir por la última versión).

La ventana va a desplegar un enlace para hacer Sync, lo que va a permitir descargar la actualización.

Salir del proyecto y volver a entrar, a veces funcionó para restaurar valores y hacer desaparecer errores. Como estoy trabajando como trabajamos todos los programadores, es decir, sin leer el manual, la mayor parte del tiempo estoy en un modo de descubrimiento. En stackoverflow y en otros sitios se habla de cosas como “Edit Configurations“, en Android Studio eso podría sea cualquier cosa, si no sabes exactamente dónde está (está dentro del menu de “Run”, ¿cómo es que no se me ocurrió?)

Android Studio es un caballo difícil de domar. ¿Cómo se definen las arquitecturas que se deben compilar? Por defecto, las compila todas, "armeabi-v7a", "armeabi", “x86”, “x86-64”, “arm64-v8a” (respuesta). Si voy a cambiar un asset, qué debo hacer? Si presiono “rebuild”, comienza a compilar todo de nuevo, eso no es lo que quiero, sino que el apk final incluya los nuevos assets. ¿Cómo se crea un nuevo proyecto basado en uno viejo? Me sorprende que “Import” de un proyecto viejo es lo mismo que “Abrir el proyecto“, aparte de abrirlo no hace nada más (esto puede generar problemas, ¿cierto?) (al final simplemente se copia la carpeta y se abre el proyecto desde la nueva carpeta).

Eventualmente conseguí las respuestas a estas y otras preguntas, pero hay que estar preparado para dedicarle tiempo (es decir, la curva de aprendizaje tiene subidas bien pronunciadas).

Mi impresión es que el proceso de instalación y ajustes de la aplicación tiene muchos detalles y cambian a medida que cambian las versiones de los plugins y paquetes. Lo mejor es descargar la aplicación y comenzar a trabajar paso por paso. Eventualmente todo funciona.

SDL es de temer

Hay que tener cuidado porque SDL no necesariamente es inofensivo. Puede causar dolor, y mucho. Por ejemplo perdí 1 hora tratando de descubrir por qué mi programa al ejecutar decía que no encontraba el SDL_main en la librería. Resulta que este define
#define SDL_MAIN_HANDLED
funciona en windows pero no en Android (es decir, en Android debe estar deshabilitado) Usualmente esto debería resolverse rápidamente con una visita a la documentación, pero tenía chrome cerrado porque mi máquina tiene solo 8gb y Visual Studio, Android Studio y el emulador mobile se lo devoran todo. Luego para resolver este problema temporalmente dupliqué mi memoria agregando a mi artillería mi laptop, ahora chrome tenía 4gb para él solito.

¿Otras emboscadas?

La variable abiFilters que es la que indica cuales arquitecturas van a ser soportadas por el ejecutable de Android (las posibilidades son ‘armeabi-v7a’, ‘arm64-v8a’, ‘x86’, ‘x86_64’) solamente debe modificarse en el archivo build.gradle de la aplicación ( el que está en la carpeta app) también se puede ajustar en Application.mk a través de APP_ABI := armeabi-v7a arm64-v8a x86 x86_64 pero o es ignorado o el efecto es desconocido (al menos para mi).

Como un truco de productividad hay que tener a la mano el archivo build.gradle(:app) y cambiar la variable ‘abiFilters’ a la arquitectura del dispositivo que estás usando. Por ejemplo, pescando el problema con la base de datos sqlite corrupta, tuve que provar altenativamente entre el emulador Pixel 2 API 30 (abiFilters ‘x86’) y mi dispositivo celular (un antiguo BLU R1 HD) (abiFilters ‘armeabi-v7a’)

Otra nota curiosa: el emulador de Pixel 2 API 30 no es tan lento como dice se puede trabajar en él. Tuve que pasar varios minutos (más de los que desearía admitir) porque no encontraba cómo llegar a Settings->Applications para borrar ek juego e instalarlo de nuevo (también se puede hacer en AS -> Tools -> AVD Manager). Resulta que en google pixel el menú con todos los iconos se abre deslizando desde abajo en el borde inferior hacia arriba (en todos los dispositivos androides que he conocido es de arriba hacia abajo).

Todavía otra nota mucho más curiosa: implementar el movimiento deslizando los tiles require una combinación SDL_FINGERMOTION y SDL_FINGERUP. Ya yo había implementado todas las combinaciones y validaciones para el movimiento de los tiles, pero todavía tuve que crear nuevas funciones especialmente para SDL_FINGERMOTION. La forma correcta es olvidar la cantidad de movimiento reportado, al menos las 2-3 horas que estuve tradando de convertir esos deltas en la resolución correcta, en windows y en todas las combinaciones de dispositivos en android, determiné que es inútil. Igualmente inútil es tratar de convertir el valor float 0…1.0 que arroja SDL_FINGERUP a la resolución correcta. Mi implementación final es determinar la dirección del movimiento con SDL_FINGERMOTION y hacer los cálculos cuando se dispara SDL_FINGERUP .

En los últimos meses de 2020 estuve trabajando simultáneamente Python, Java, PHP, C++ y C#. Este último me tiene sorprendido, le huía como al Covid 19, pero ahora me gusta, me entusiasma, y siento una felicidad tan grande cuando cruzo el puente y entrego una flamante aplicación funcionando, que me sorprende que he hecho tan pocos desarrollos en C#.

Java. Las energías en el universo se compensan, y como Java sabe que yo lo odio, porque tenemos historia, él me paga con odio. Por ejemplo, el código para abrir una página web en android. Al parecer hay una forma de hacerlo desde ndk, pero como había tantas preguntas huí por la derecha como el león Melquíades. Además la versión java era tan simple, mjummmmm…

public static void openWebPage(String url) {
    Uri webpage = Uri.parse(url);
    Intent intent = new Intent(Intent.ACTION_VIEW, webpage);
    if (intent.resolveActivity(getPackageManager()) != null) {
        startActivity(intent);
    }
}

Ok, al tratar de compilar “eso” comenzó a decir “cannot find symbol”. Uri.parse fue fácil simplemente hay que agregar

import android.net.Uri;

y lo mismo con Intent intent = new Intent(Intent.ACTION_VIEW, webpage); hay que agregar:

import android.content.Intent;

El resto fue otra historia. ¿por qué sistemáticamente toda la documentación ignora/soslaya/oculta cuál es el archivo que hay que importar? Inclusive las respuestas en stackoverflow no incluyen los “import” respectivos, por razones que desconozco. Yo creo que todos los 200 programadores java en el mundo saben algo que yo no sé. O en alguna documentación hay una clave de como deducir el “import” basado en el nombre de la función.

Lo ignoro.

Que la documentación es imprecisa, incompleta o sospechosamente ambigua es algo que se ve por todas partes. Tengo múltiples ejemplos, pero veamos el de agregar el icono a la aplicación. Hayq eu olvidarse de stackoverflow porque esto ha cambiado mucho durante los años, y hay respuestas que corresponden a 2013, 2016 y 2018, ninguna sirve para la versión actual (Android Studio 4.0).

El manual dice que hay que usar un tool llamado Image Asset Studio. No dice que este tool se invoca directamente dentro de Android Studio, debemos suponer eso (aunque la forma que es descrito parece un tool aparte/externo). Pero más abajo nos dan las instrucciones (incompletas) de cómo invocarlo desde AS. Dice que en la ventana del proyecto se debe seleccionar la vista Android, tal como se muestra en la siguiente imagen:

Android Studio debería tener mapas que dicen “Ud. está aqui”

Luego dice como paso 2 ” Right-click the res folder  ” es decir hacer click derecho sobre la “carpeta res” pero ahi solamente está Gradle Scripts y “External Build Files”. ¿Donde está la carpeta res? Oh, sí, aparece otra vez app pero eso contiene los fuentes del sistema eso no debe ser, ¿verdad?. Pues sí es, dentro de app está la carpeta res. Pero eso lo omite el manual porque sí.

INSTALL_PARSE_FAILED_NO_CERTIFICATES

Este error aparece al tratar de instalar la version release, y, al parecer, no se ha completado correctamente el proceso de signature del apk. Vamos a ver que dice stackoverflow. Esto me dejó con un verdadero overflow.

  • Rebuild la aplicación a mi no me funcionó. Y eso que esperé los 10 minutos que tarda en mi equipo para generar el paquete para las 4 arquitecturas, release, etc.
  • Puesto que dice “APK signature verification failed.” debe ser algo con la firma. Cuando hago build dice Generate Signed Bundle… App bundle(s) generated successfully… así que el proceso Falló exitosamente. No entiendo.
  • Es algo con la firma pero “v2SigningEnabled true” en build.gradle no funciona.
  • Tampoco parece que tenga que ver con las versión de la firma v1 o v2 (full APK Signature), porque AS ya no tiene opción para indicar cuál versión se va a usar (no la tiene cuando generas un Bundle, pero cuando generas un APK, si aparece)
  • Si en vez de usar “Generate Signed Bundle” seleccionamos “Generate Signed APK” el error es el mismo.
  • Finalmente el problema se resolvió validando que en  “Project Structure” > “Modules” > “Signing Configs” esté seleccionado una configuración de firma correcta. Por defecto “Signing Configs” no está seleccionado. Esto debe ser un bug.
  • Jugué un extra tiempo aquí tratando de encontrar “Project Structure” . Es una opción en el primer menú, File, está entre “Settings” y “Other Settings“. Ya había estado ahí hace un mes y todavía se me pierde.
  • La primera vez que intenté esto me generó un error “Keystore file not set for signing config dev“, así que probé con la otra configuración “$signingConfigs.debug” y funcionó sin errores. El problema es que estaba trabajando con build release así que no quería nada “debug” involucrado.
  • Edité el resto porque ya este post ha crecido desmesuradamente. Aquí tenía al menos otras 3 notas adicionales con otras escaramuzas. Sigamos.

SQLite no tiene la culpa

La compilación de tilemovers avanzó sin problemas hasta que apareció este error al momento de ejecutar el linker:

../toolchains/llvm/prebuilt/windows-x86_64/sysroot/usr/include/c++/v1/ios:547: error: undefined reference to ‘std::__ndk1::ios_base::clear(unsigned int)’

Android Studio no indica por ninguna parte cuál código en mi aplicación está generando el problema, simplemente lanza ese error sin anestesia. Supuse que podía ser SQLite ya que yo estaba usando el código fuente directamente y no la librería que hay en la página de descargas:

sqlite-android-3330000.aar

Así que seguí este procedimiento para agregar directamente la librería. Pero eso no quitó el error.

Luego el error se hizo viral, y cada cambio generaba errores similares pero con diferentes funciones:

undefined reference to ‘std::__ndk1::__next_prime

undefined reference to ‘std::__ndk1::locale::locale

Aquí encontré una sugerencia, que tengo una mezcla entre compilación con el parámetro c++_static y el parámetro gnustl_static, es decir unas partes las estoy compilando con c++_static y otras (por ejemplo una librería) con gnustl_static, pero yo no estoy usando librerías externas más allá de SQLite.

Es muy gracioso cuando se presentan situaciones como estas (porque entre otras cosas no debería suceder). En mis pruebas, traté de utilizar SQLite librería (el archivo .aaa que se encuentra en la página de descargas) y removí el código fuente sqlite.c. Importé el archivo .aaa, quedó agregado como un nuevo módulo en el proyecto. Hice mis pruebas y no hubo cambios, el error continuó. Entonces traté de remover el módulo: ups, eso no es tan fácil como hacer RIGHT-CLICK Remove. No señor, eso en sí es una epopeya.

Aparentemente es suficiente removerlo del archivo y en la sección dependencies quitarlo de ahí (de build.gradle ). Pero eso no lo remueve del árbol del proyecto. Alrededor de stackoverflow se indica que basta con:

Removerlo en Build -> Edit Libraries And Dependencies (sigue en el árbol)

Removerlo del archivo settings.gradle (no está ahí)

Right click en el módulo y seleccionar “Select Open Module Settings ” (eso no existe, tampoco “Module Settings”, debe ser en una versión vieja)

No, “Module Settings” está en Right click sobre Project (no, no y no)

En settings.gradle buscar la línea':modulenameyouchooseduringimport' y borrarlo de ahi (no).

Al final como no parecía haber forma de borrarlo, comencé hacer cosas sin sentido. Por ejemplo, salir y entrar de Android Studio: el módulo aparece, y luego de unos 20-30 segundos desaparece (Android Studio tarda en mi máquina 2 minutos para cargar completamente). Parece que al borrarlo de build.gradle no se refleja el cambio en el árbol del proyecto a menos que salgas de la aplicación y entres de nuevo (eso es un bug, ¿correcto?)

En diversos sitios dice (por ejemplo) que ese error (undefined reference to ‘std::__ndk1::locale::locale ) se corrige agregando en el archivo build.gradle lo siguiente:

externalNativeBuild {
    cmake {
        cppFlags "-DANDROID_STL=c++_shared"
    }
}

Lamentablemente nada de eso funcionó probando todas las combinaciones.

Así que comencé a eliminar archivos en el juego con la esperanza de conseguir información adicional, y efectivamente así fue. Al ir eliminando módulos los errores cambiaban, pero siempre eran del tipo undefined reference to ‘std::__ndk1:: xx (algún método). Hasta que apareció

undefined reference to ‘std::__ndk1:: to_string ()

Eso encendió el bombillo sobre mi cabeza. ¿ndk/gradle/android Studio no está aceptando C++11, solamente C99? Porque std::to_string () es una típica llamada agregada en C++11. Pero ya había revisado eso casi desde el comienzo, en el Android.mk estaba el APP_STL := c++_shared que supuestamente (aquí lo confirman) habilita el soporte hasta C++17.

Cualquier combinación de esto falló:

APP_STL := c++_shared
APP_STL := c++_static
# Enable c++11 extentions in source code
APP_CPPFLAGS += -std=c++11
LOCAL_CFLAGS := -std=gnu++11
LOCAL_FLAGS := -std=c++11

Lo cual me llevó a un dead-end, un cul-de-sac, un callejón sin salida. Investigando cómo viajar en el tiempo usando gradle, descubrí que en la carpeta

\tilemovers\app.cxx\ndkBuild\debug\armeabi-v7a

Está el archivo donde se muestran todos los flags que clang está usando para cada uno de los archivos del proyecto. La línea que corresponde a mi archivo de prueba no tenía el flag:

–std:c++11

Curiosamente otros archivos sí tienen este flag, por ejemplo el archivo de SDL2

SDL2\src\hidapi\android\hid.cpp

Así que simplemente fui a esa carpeta y revisé el archivo Android.mk

La instrucción que se debe colocar es esta:

LOCAL_CPPFLAGS := -std=c++11

(eso no aparece en ninguna parte hasta donde me alcanzó la paciencia para revisar la documentación. Más adelante veremos que esto tampoco es necesario)

Al agregarlo la línea de comandos de mi archivo con std::to_string () apareció con el flag correcto (-std=c++11) pero el error siguió apareciendo.

Mi siguiente prueba fue determinar si estaba usando ese flag, así que elimine y agregué algo típico de c++11. Lambda expressions. Algo como esto (tomado de aquí):

auto unhex = [](char c) -> int {
return c >= ‘0’ && c <= ‘9’ ? c – ‘0’ : c >= ‘A’ && c <= ‘Z’ ? c – ‘A’ + 10 : c >= ‘a’ && c <= ‘f’ ? c – ‘a’ + 10 : -1;
};

Funcionó. Así que si estaba compilando tomando en cuenta las extensiones de c++11. ¿Y entonces? ¿Otro cul-de-sac?

Recordé que en algún sitio alguien decía que en alguna parte se estaba deshabilitando o asignando en forma incorrecta el uso de ndk. Comencé a buscar y revisar todos los archivos dentro de app y ahí estaba:

Application.mk

dentro de app\jni.

# Uncomment this if you're using STL in your project
# APP_STL := c++_shared

Funcionó (el error undefined std::to_string() desapareció)

Es decir, Android Studio ignora cualquier cosa que coloquemos en build.gradle, Android.mk y utiliza la opción en Application.mk

Lamentablemente las secciones dedicadas a C++ en la documentación no mencionan esto (por ejemplo aquí), pero sí en la sección dedicada a ndk. que por supuesto tiene sentido, dado que ndk es específico para C++. Como siempre, una vez resuelto el problema, resulta raro cómo es que no encontré la solución al comienzo.

Este error undefined std::to_string() representó unas 2 horas de escaramuzas e intrigas, pero fue divertido. Me recordó algo que siempre he criticado, la proliferación de archivos de configuración, algo muy común en sistemas java. En el caso de Android Studio hay que estar pendiente de 5 archivos de configuración. Afortunadamente, esto es algo que se hace una sola vez la primera vez que se instala/crea un proyecto, y más nunca en la vida. De aquí en adelante es un copy-paste de los proyectos.

¿Leer el manual? ¿Quién? ¿yo?

¿Por qué no leo la documentación? Porque es inútil. Por ejemplo, en el trabajo de programación diario, (edición-compilación-decepción-frustración-aceptación-repetición) en Android Studio vamos a ver con mucha frecuencia esto abajo en el UI:

Si usted va a trabajar con Android Studio prepárese a ver esa imágen dando vueltas durante minutos y más minutos. Mi record personal es 15 minutos. Algunas veces se queda atascado ahi y no te deja hacer nada más. No es que está procesando, está actualizando alguna librería de alguna parte de internet (eso no está en la documentación, eso lo leí en stackoverflow) Claro que eso se puede deshabilitar, simplemente hay que ir a View-Tool Windows-Gradle. No aparece una ventana popup, sino una ventana en el UI. En el borde superior hay un icono con dos rayas cruzadas por una línea: ese ícono significa trabajar offline. Si está presionado (en negrillas) significa que Gradle está trabajando offline, de lo contrario está online. ¿Conseguir todo eso en la documentación? Ni lo sueñes.

Historias similares ocurrieron con otras actividades Cómo haces para actualizar la aplicación en el dispositivo? si la ejecutas del Android Studio, no se actualiza sino que se ejecuta la versión instalada, no importa si AS sabe que acabas de hacer dramáticas modificaciones al código fuente, por defecto, ejecuta la versión en el dispositivo. what the fuck!. (Simply uninstall the application from your mobile device and then run your app, no eso no funciona= Al final, resulta ser que es un bug. Hay que hace clean y rebuild. Y AS es muy gracioso: Luego de tardar varios minutos haciendo varias cosas termina diciendo:
CONFIGURE SUCCESSFUL in 16s

AS tiene serios problemas de conducta

AS tiene sus peculiaridades. En internet dicen con frecuencia que es una inmensa montaña de !#@%. (AS is complete joke and bs. I am just tired of this nonsense ) Y a veces pienso que es posible. Por ejemplo, yo no he leido el manual, pero qué tan difícil puede ser hacer build de la aplicación. Inspeccionando el UI no hay ningún icono que diga “Build”, pero si hay un menu “Build”. Dentro del menú hay un “Make Project” que no me suena a “Build“, eso debe hacer otras cosas aparte de “Build” luego hay un “Make module app” eso si se acerca bastante a lo que quiero hacer, porque la otra opción, “Run generate sources Gradle Task” nuevamente se aleja demasiado de “Build“. Pero hay un problema: “Make module app” está deshabilitado. Luego de buscar todo el UI observamos que en la ventana del proyecto “app” no está seleccionado. Si hacemos click se torna azul (seleccionado) y ahora “Make module app” está habilitado. Nuestro siguiente problema es que no vemos qué está pasando y eso se debe a que la ventana de “Build” está oculta: tenemos que hacer click sobre el tab “Build” en el sector del UI de ventanas de outputs. Si tenemos suerte y el “Build” resulta exitoso, ahora podemos hacer “Run” (aleluya: si hay un ícono “Run”). Nuevamente tenemos que hacer click sobre el tab “Run” en el sector del UI de ventanas de outputs. ¿Eso no debería ser automático? Bien, ejecutamos el juego, el cambio que tratamos de hacer no funcionó así que tenemos que ir al editor, corregir y repetir el ciclo. ¿Saben qué? Nuevamente hay que hacer click en “app” en la ventana del proyecto porque nuevamente se deshabilitó, hacer click en la ventana de output de “Build”, y así horas y horas haciendo cosas inútiles como si no tuviéramos otras cosas que hacer. Hmm sí, AS tiene serios problemas de conducta.

Nota final: al publicar la aplicación en el Play Store (también conocido como Google Store), el Play Console me arrojó este error “Debes utilizar otro nombre de paquete porque “org.libsdl.app” ya existe en Google Play.”. En toda la explicación precedente hay que incluir que se debe colocar un nombre nuevo en Android Studio. Yo sabía eso hace 6 meses e intenté hacerlo, pero al parecer, a pesar de que el nombre “tilemovers” aparece en todas partes, desde el punto de vista muy personal de Android, la aplicación se llama “app”.

Ok respira profundo:

  1. Hay que modificar en res/values/string the string name=”app_name” y colocarle el nombre de la aplicación. Lo hice desde el comienzo, No es suficiente.
  2. Bueno etc. La solución (como explico aquí) es cambiar en build.gradle applicationId “org.libsdl.app” a applicationId “org.libsdl.tilemovers”

Nota sopotocientos: Si al publicar en Play Store o Google Play o como se llame te aparece este error: ” Tienes que usar un código de versión diferente para el APK o Android App Bundle porque ya cuentas con un archivo con el código de versión 1. “, tienes que corregir el valor en build.gradle(:app) versionCode 1 => versionCode 2 o el siguiente valor. También aparece en el manifiesto (AndroidManifest.xml) pero no tiene efecto (es decir, lo que cuenta es lo que aparece en build.gradle(:app) versionCode .)

Adios, me despido de ti

Mi cuaderno de notas está lleno de anotaciones sobre las peripecias con el manejador de bases de datos sqlite, pero ya eso es tema de otro post. Se cansa uno.

Me abro al cierre

30-12-2020 7:55 AM

La gente con poder lo tiene porque tú se lo diste.

¿Me conoces?

Hoy aprendí que el despiadado y siniestro ruso corrupto que aparece en Jack Reacher es Werner Herzog (!)

How lonely is the life of the solo dev (Qué solitaria es la vida del desarrollador de juegos que trabaja solo)

Bien lo dijo García Márquez: el inglés es compacto, le da fuerza al mensaje. Esa frase (“How lonely is the life of the solo dev “) pierde su pureza al traducirla al español. Como quiera que sea (“anyway“) , eso. Para ser solo dev hay que tener una psique especial, ser alguna especie de lobo estepario de la tecnología, olfateando el ambiente huyendo de los rebaños. Porque el rebaño lo aleja de su objetivo. Terminar “su proyecto que no forma parte de su trabajo 8am-5pm sino que es un proyecto personal” (“finish the side project“). Y en mi caso, doble, porque en este momento estoy trabajando el side project de mi side project. Uff. Me espera mucha soledad. Hola soledad!!

“Escribe código. No mucho. Principalmente funciones”

Este artículo hace analogía entre escribir código y comer. Interesante. Ambos satisfacen una necesidad y un deseo. ¿Por qué escribo código? Necesito hacerlo. Es como Picasso pintando. Necesita hacerlo.

Pero no es eso lo que me llama la atención del artículo, sino la explicación de esa recomendación. Es una paráfrasis de lo que Michael Pollan dijo sobre la comida: “Come comida. No mucha. Principalmente vegetales”, y la explicación de esto es: “… el valor de la comida es más una devoción religiosa y de moda a la mitología de las soluciones simples que una conclusión convincente y fiable de la investigación científica incontrovertible” maravillosamente equivocado pero acertadamente falso. Parece verdad pero es en realidad una colección de palabras colocadas una detrás de otra. Me pregunto si eso es el fenómeno que sucede en twitter. Para los que necesitamos comer y necesitamos programar, todo eso es un madejo de gamelote: necesito programar, ergo necesito comer. Punto. Para programar necesito hacer miles de millones de conexiones sinápticas en mis neuronas. Ese proceso consume electricidad. La electricidad la produce mi cuerpo. Mi cuerpo necesita comida, principalmente proteina.

Pero estoy de acuerdo en comer vegetales. Nuestro colon nos lo agradece todos los días. Y en relación a las funciones, una función más no hace daño. Toda función puede ser reescrita como una colección de llamadas a otras funciones (originalmente había escrito aquí “hay que escribir todas las funciones, ni una más ni una menos”. Dejo las dos opciones abiertas)

Enlace.

Decisiones… Todo cuenta

Puesto que realmente yo no trabajo en C++ sino en C + STL, hace sentido buscar alternativas a STL para trabajar en C más esa alternativa. Existen algunas pero pienso que quizás esta es la mejor, C TEMPLATE LIBRARY. El problema es que revisando el código de khpx yo utilizo algunas cosas adicionales de C++ que son cómodas. Estoy anotando aquí en un papelito hacer un post sobre qué cosas de C++ además de STL utilizo en khpx . Espero no perderlo.

Sin embargo, me parece una locura claudicar STL por una oscura y desconocida librería-header de github. Los comentarios en hacker news son favorables, pero. Eso, hay un “pero” ahí escondido en alguna parte. Decisiones… todo cuenta… cada día. Mejor no.

2020

Este año ha sido tan malo pero tan malo que cuando termine mañana todo el mundo va a decir “Por fin”. Este año ha sido tan malo pero tan malo que he estado pensando en cambiar el nombre de los niveves en tilemovers de “Normal, Difícil, Imposible” a “Normal, Difícil, 2020”. (tilemovers es un juego de tiles, el side project de mi side project. Corre en windows y android!).

Este año ha sido tan malo pero tan malo que cuando alguien cuente algo malo que le sucedió en su vida, la gente le va a preguntar: “ya va, eso fue en el 2020 o un año regular?”. De igual forma, este año ha sido tan malo pero tan malo que las películas de terror van a tener una aclaratoria al comienzo que diga “Los siguientes acontecimientos no ocurrieron en el 2020”.

Este año ha sido tan malo pero tan malo que todo el mundo va a sonreir y ser feliz el resto de sus vidas, no por cualquier razón sino porque sobrevivieron al 2020.