Hidden People Blog

Devlog #7: Un nuevo grant de 2dcl - Laberintos en Weekends in the Woods - cuentitos en version 0.2 - Laidaxai camina más lento pero sigue en pié

Parece ayer que escribí el post anterior. Y sí, dos semanas pasan muy rápido!

Cuando subí el post de la semana pasada me dí cuenta de que estaban quedando artículos muy largos, y Guido sugirió agregar un índice en cada post. Me pareció una idea genial así que lo implementamos en el momento (zola ♥️) y quedó genial.

Ahora pueden saltar a la sección que quieran dentro de cada proyecto sin tener que scrollear por cosas que no les interesen, gracias Guido por la idea! Después de hacer eso me puse a pensar en qué funciones querría que tenga nuestro blog, pero que se mantenga simple como es, y creo que lo que más querría es que la gente pueda dejar comentarios, así que creo que voy a priorizar eso en estas semanas.

Te gusta lo que hacemos?
Sumate a nuestra lista de correo para recibir notificaciones de nuestras actualizaciones y lanzamientos.

2dcl

El día que subimos el blog anterior también fue el día que publicamos una propuesta para un nuevo grant en la DAO. Entusiasmado por lo que escribí en el blog, decidí sentarme a completar los formularios y subirlo ese mismo día.

Hoy terminó la votación y podemos anunciar felizmente que tenemos un nuevo grant para seguir trabajando en 2dcl 🎉🥳🎊.

Quiero aprovechar este espacio para agradecer a todos los que nos votaron: pablo, Canessa, Eibriel, Szjanko, LordLike, ckbubbles, AwedJob, NikkiFuego, Potradamus, Tony, 1010, Rednitrous18, RizkGh, mattimus, 1Existence, akasya, ManaDaiquiri, FriskyBumbleBee, NFTKING, Atro, KENJI, REDACTED, fractilians, JimFdcl, Lbster#11a2, Cerf, pepco, Fern, zCayel, ct1aic, Hioranth#1390, bill#ace8, Walrus#ec98, Nakabaka#19c1, Guest#9a2a, Focleshew#66fb, Unnealasan#1bf0, ValarDohaeris#3613, naser33#507f, PEDRAM#acf8, amircrypto82, saeidkomij#6e17, Domble, pornj#f806, yzj#01be, jt603. ❤️

También a aquellos que votaron que no o se abstuvieron, porque nos permiten mantener la responsabilidad alta y un objetivo de mejorar en este grant: Kyllian, dax, InJesterr, Jenn, CheddarQueso, mazafaka, Guest#60fd, yancy#bacc, Maryana, gamer.

Y por último a todos aquellos que decidieron mantenerse anónimos pero que también votaron, respetamos su decisión de anonimato, y gracias!

Esto significa que a partir de mañana nos ponemos a trabajar en las nuevas funcionalidades del cliente.

Login con Metamask

La primera funcionalidad en la que vamos a trabajar es la capacidad de loguearse al cliente usando MetaMask y que una vez logueado, se genere el avatar automáticamente.

En el estado actual del cliente hay que editar un archivo de configuración y correr 2dcl para actualizar el avatar. Evitando este paso e incluyendo el login que a largo plazo va a servir para otras cosas más vamos a hacer las cosas mucho más fáciles para los usuarios.

Como MetaMask no tiene un cliente en Rust, vamos a tener que ver cómo implementamos esto.

La primera opción que vamos a probar es que nuestro cliente inicie un web server local que sirva una pagina web con toda la lógica de login y que esa página se conecte con el cliente para pasarle todos los datos de login. Esto es exactamente lo mismo que hace el comando dcl de la fundación, pero ellos usan node.js para el CLI.

También podríamos investigar un poco otras opciones más nativas, que no necesiten un server local y un contexto de JavaScript para el login, pero vale la pena resolver los problemas más relacionados al cliente en sí que a la infraestructura de Ethereum en un principio.

Incluso creo que al usar un webserver después podamos incluir otras wallets más fácilmente (fortmatic, walletconnect, coinbase, etc).

Deployment

Además de trabajar en el Login, este mes vamos a concentrarnos también en el flujo de deployment de escenas. Queremos que el proceso sea de un par de clicks, no más que eso.

En este momento para subir una escena uno tiene que crear la escena, compilarla, copiarla a una carpeta 2dcl dentro de una escena 3d y subirla a catalyst con el CLI de la fundación.

El objetivo es utilizar el mismo truco del login para firmar las transacciones de deployment, además de crear las funciones necesarias en el CLI y el preview mode para subir las escenas.

Weekends in the Woods

Wow! Qué par de semanas tuvimos con Woods!!! Hicimos muchos avances y la verdad estoy muy contento con el progreso.

Juli se ganó el premio de Maquina de Ilustrar & Animar del Año, estuvo haciendo concepts visuales a lo loco y casi ya tiene listo el spritesheet de Abril. Yo estuve implementando la representación y navegación de los laberintos.

Acerca del género

Weekends in the Woods está evolucionando un poco en nuestras cabezas. Lo que comenzó como una Visual Novel con algunos elementos aleatorios nos está llevando cada vez más a nivel sistemas a algo más cercano a un RPG con mucha base narrativa. Se podría decir que estamos haciendo un híbrido entre RPG y Visual Novel.

Algo interesante (y que me da un poco de miedito) es que los RPGs tienen expectativas muy fuertes sobre la duración.

Sólo alcanza con ver los promedios de duración de juegos de Visual Novel vs RPG:

Stats Visual Novel: 17hs

Stats RPG: 41hs

El promedio de las visual novel es 17hs y de los RPGs 41hs.

Rant acerca del promedio

Me gustaría que las personas que hacen herramientas estadísticas comiencen a ver un poco más allá del promedio. Sirve muy bien cuando la variación en la población es mínima, pero no podemos obtener tanto información útil si la variación es muy grande.

En el caso de los RPGs, el juego más corto dura 10m 32s mientras que el más largo 1231h. Este rango es demasiado grande como para que el promedio tenga mucho sentido, al menos para el caso de uso que tengo yo.

Además acá claramente están mezclados distintos tipos de RPG (MMOs, Single-Player)

Volviendo del rant

Si las expectativas de los jugadores de visual novel es un promedio de 17hs y el de los RPGs es de 40, esto me preocupa un poco. Es mucha diferencia y la verdad me abruma un poco pensar en generar 40 horas de contenido.

Muchos RPGs recaen en progresiones con mucho grinding para llegar a muchas horas de "juego" pero como diseñador no estoy de acuerdo con eso.

Lo que sí puedo hacer en este momento es analizar cómo puedo reinterpretar los sistemas de los RPGs para hacerlos más casuales y ser muy claro con las expectativas.

El largo objetivo de nuestro juego va a ser en el rango de 15-20hs, y lo vamos a aclarar en nuestra página de Steam cuando salga así los jugadores de RPG saben qué es lo que van a encontrar.

Abril

Juli estuvo haciendo algunas pruebas sobre Abril, nuestra investigadora favorita.

Primero que nada terminó una primera pasada del spritesheet de correr.

Abril Corriendo

Una decisión más que acertada que tomó Juli es la de usar pocos frames para las animaciones, un poco para ahorrar trabajo, pero sobre todo como una búsqueda estética. Queremos lograr tener animaciones muy expresivas y un poco "choppy", un estilo que nos gusta mucho como Pocoyo u Oni.

Animacion Pocoyo

Animacion Oni

La verdad yo estoy muy contento con los resultados, vamos a ir viendo cómo aplica esto a otras partes del juego.

Laberintos

En el último update los dejé con algo de concept art sobre los laberintos, ahora voy a hablar un poco más sobre cómo forman parte del juego y cómo los estamos implementando.

El tema del movimiento en el espacio es algo que estuvimos hablando mucho con Juli.

Yo inicialmente me imaginaba el juego como un mapa muy grande donde ibas por el bosque y te encontrabas criaturas por ahí. Juli, en cambio, desde un principio se imaginó el juego como un dungeon crawler. Al principio yo no estaba muy convencido, pero le pedí que hiciera concepts al respecto, y eso es lo que vieron en el post anterior.

Al ver los concepts empecé a maquinar con esta idea de un laberinto de dioramas interconectados y la verdad me encantó. Decidimos que esa era la dirección que iba a tomar el juego. Así que esto acercó el juego un poco más a los RPGs como venía contando, y me dejó el espacio abierto para repensar un poco los distintos sistemas que tienen los juegos RPG y ver cómo podrían funcionar en nuestro juego, pero esto lo vamos a ir viendo poco a poco a medida que vayan cristalizándose.

🔧 Implementación

🚨 Toda esta sección es bien dura, para programadores. La podés saltear si no te interesa la programación.

Cuando ya decidimos que nuestro juego iba a tener laberintos basados en losetas comencé a implementar esto.

Lo primero que hice fue crear las estructuras de datos para poder representar las losetas. La información esencial que define cada loseta es qué salidas tiene habilitadas así que creé un enum con todas las combinaciones posibles:

pub enum Tile {
  None,
  NS,
  EW,
  NW,
  SW,
  SE,
  NE,
  NSEW,
  NEW,
  SEW,
  NSE,
  NSW,
  S,
  N,
  E,
  W,
}

Y cada uno de nuestros laberintos (que por ahora llamé Map) tiene un tamaño y una matriz de locetas:

pub struct Map {
  pub size: V2,
  pub tiles: Vec<Vec<Tile>>,
}

Para poder simplificar la creación de mapas, necesitaba un operador que me permitiese conectar dos losetas:

pub fn link(&mut self, from: V2, to: V2)

Lo que hace este método es dadas dos posiciones contiguas en el mapa, le agrega a cada una la salida necesaria para que estén conectadas.

Para simplificar esta operación, lo que hice fue agregarle el operador de suma a las losetas:

assert_eq!(Tile::N + Tile::S, Tile::NS);

En este ejemplo si una loseta tiene una salida al norte, y le sumo una al sur, termina siendo una loseta con salidas al norte y al sur. Bastante simple.

Teniendo esta operación el método link se volvió bastante simple, sólo tengo que saber qué salidas necesito y sumárselas a la loseta original.

Generación de Laberintos

La generación de laberintos es un problema ya resuelto. Hay muchos algoritmos dando vueltas y no tiene sentido implementarlos de cero, así que recurrí a la gracia divina del código abierto para esto.

Encontré maze-rs, un pequeño proyecto escrito por Cian Ruane que implementa varios algoritmos.

Está un poco abandonado el repositorio, pero tenía suficiente como para que copie un par de clases y pueda hacer algo que utilice su generador y lo transforme en la representación que tenemos en nuestro juego. Yay!

En 3/4 horas tuve un generador de laberintos andado que podía mostrar y editar en una aplicacioncita que escribí para esto:

Editor de laberintos

Para los curiosos: el editor lo hice usando Tauri, TailwindCSS y JavaScript. Lo interesante de Tauri es que puedo reutilizar los structs y enums del juego para el editor y compartir toda la lógica entre ambos, pero implementar la interfaz en HTML+JS+CSS que me lleva mucho menos tiempo que hacerlo nativo en Rust.

Con un generador de laberintos y un pequeño editor para corregir lo generado, ya estaba listo para el siguiente paso.

La navegación implicaba un pequeño runtime que mantuviese el estado de en qué loseta está el jugador y un renderer que se encargase de renderar la loseta específica.

Esto es una preferencia personal, pero me gusta separar los runtimes de los renderers. Mi experiencia me dice que es mucho más simple de implementar y testear automáticamente los runtimes por separado.

Yo llamo runtime a toda la logica que se necesita para ejecutar las acciones del juego, y rendering a la representación visual del estado del juego.

Poner esta barrera entre rendering y runtime me permite poder ejecutar el juego y simular acciones o tests sin necesidad de tener graficos andando.

Por ejemplo: probar que el jugador puede moverse en dirección norte si los tiles están conectados no necesita de interfaz gráfica, es algo que debería poder probarse automáticamente e incluso simularse para encontrar errores sin necesidad de tener una placa de video. Esto es importante para automatizar el testing del juego en servidores de integración continua (nosotros usamos GitHub Actions para esto).

Es por esto que yo separo la lógica de la representación visual del estado.

El código queda más fácil de leer también, y los distintos sistemas se comunican con eventos y ya.

Por ejemplo, tenemos un sistema que se encarga de escuchar el evento Quiero cambiar de loseta, viejo! (ChangeTileEvent) y decide si se puede o no, y si se puede manda el evento Para de gritar que ya la cambie!!! (TileChangedEvent).

fn tile_change_event_handler(
  mut state: ResMut<TileState>,
  mut events: EventReader<ChangeTileEvent>,
  mut writer: EventWriter<TileChangedEvent>,
) {
  let from = state.position;

  for event in events.iter() {
    let direction = event.0;
    let to = match direction {
      Direction::N => from - V2(0, 1),
      Direction::E => from + V2(1, 0),
      Direction::W => from - V2(1, 0),
      Direction::S => from + V2(0, 1),
    };

    if state.map.linked(from, to) {
      state.previous_position = from;
      state.position = to;
      writer.send(TileChangedEvent {
        direction,
        from,
        to,
      });
    }
  }
}

Como verán este sistema queda súper fácil de leer y no tiene nada de logica de rendering haciendo ruido en el medio.

Lo mejor de todo es que este sistema se puede testear muy fácilmente:

#[test]
  fn tile_movement_moving_north() {
    let mut app = App::new();
    let mut state = TileState::default();

    let json = include_str!("../../fixtures/square_closed_map.json");
    state.map = Map::from_json(json).unwrap();
    state.position = V2(2, 2);

    fn test_sender_n(mut writer: EventWriter<ChangeTileEvent>) {
      writer.send(ChangeTileEvent(Direction::N));
    }

    fn test_receiver(
      mut received: ResMut<ReceivedCount>,
      mut reader: EventReader<TileChangedEvent>,
    ) {
      for event in reader.iter() {
        if event.direction == Direction::N {
          received.0 = received.0 + 1;
        }
      }
    }

    app
      .insert_resource(state)
      .insert_resource(ReceivedCount::default())
      .add_event::<ChangeTileEvent>()
      .add_event::<TileChangedEvent>()
      .add_system(test_sender_n)
      .add_system(tile_change_event_handler.after(test_sender_n))
      .add_system(test_receiver.after(tile_change_event_handler));

    // If I can move north
    app.update();

    let state = app.world.resource::<TileState>();
    assert_eq!(state.previous_position, V2(2, 2));
    assert_eq!(state.position, V2(2, 1));
    assert_eq!(app.world.resource::<ReceivedCount>().0, 1);

    // If I can't keep moving north
    app.update();

    let state = app.world.resource::<TileState>();
    assert_eq!(state.previous_position, V2(2, 2));
    assert_eq!(state.position, V2(2, 1));
    assert_eq!(app.world.resource::<ReceivedCount>().0, 1);
  }

En este test veo que el jugador se puede mover hacia el norte una vez en un mapa de 5x5 que solo le permite moverse una vez en cada dirección desde el centro.

Esencialmente hago un par de cosas acá:

  • Inicializo el juego
  • Le cargo el mapa de 5x5
  • Le pongo un sistema que manda el evento para que se mueva al norte: test_sender_n
  • Le pongo un sistema que cuenta las veces que se movió al norte: test_receiver
  • Le pongo el sistema que quiero testear tile_change_event_handler
  • Corro un frame
  • Uso assert_eq! para validar que se movió al norte
  • Corro otro frame
  • Valido que no se movió más al norte porque el mapa no lo permite

Todo esto lo pude hacer sin tener que levantar un renderer y consumir energía que el mundo tanto necesita en mostrar cosas que no necesitamos mostrar: esto es lo que se llama headless, logica sin rendering. Si mi código de rendering estuviese en el mismo sistema, esto sería mucho más complejo de testear.

Mini mapa

Saliendo un poco de la programación y volviendo al diseño, estuvimos analizando distintas formas de representar el mini-mapa y el mapa ampliado.

Todavía no tomamos una decisión final de cuál vamos a usar, pero tenemos distintas opciones y vamos a probar varias.

Tenemos dos opciones principalmente, usar un minimapa isometrico:

Mapa Isometrico

O topdown:

Mapa Topdown

La ventaja del isométrico es que se condice mucho con la representación de la escena, pero se nos hace que es un poco más difícil de leer que el top-down.

Vamos a implementar ambos y probarlos.

Editor de escenas

Una de las preguntas que me hizo Juli es si podíamos crear un editor de escenas para implementar las losetas. Yo le dije que era mucho trabajo pero le propuse hacer algo que vengo haciendo desde The Insulines en 2012: usar un editor SVG que soporte linkear imágenes como editor de niveles.

Yo había escrito un artículo sobre esto en 2013, pero no se dónde fue a parar. Así que puede que escriba uno nuevo, con código que muestre cómo lo implemento en Rust, pero esto es para otro momento!

Ya estuvimos haciendo algunas pruebas sobre esto y la verdad que viene funcionando bien. Cuando lo termine de implementar les cuento.

Esto es lo último que tenía para reportar sobre Weekend in the Woods, espero que les haya gustado!

cuentitos

Buenas noticias sobre cuentitos!! Pablo estuvo a full estas dos semanas y logró implementar todo lo que nos propusimos!

Tuvimos un problema de diseño que tuve que resolver: no me podía decidir si quería que las bifurcaciones funcionen como en ink o no, es decir, si quería que una vez que te vas a una sección y esta sección termina, si volver o no al punto anterior.

Encuentro usos para ambas opciones así que decidí crear dos tipos de bifurcación, la común que no vuelve representada con ->, y la 'boomerang' que va y vuelve representada con <->. Es un poco más complejo tener dos bifurcaciones, pero creo que a nivel lógico vale la pena tener esa distinción, de hecho ya empecé a usarlas y estoy contento con el resultado.

Dado que ahora tenemos todo lo de 2dcl encima no se si vamos a tener tiempo de hacer un lanzamiento oficial con bombos y platillos de la versión 0.2, pero al menos va a estar disponible para que la pruebe quien así lo desee.

Nos queda corregir algunos bugs que identificamos hacia el fín de la semana pasada y mejorar unas funcionalidades del CLI (una pequeña aplicación que usamos para probar los scripts de cuentitos).

Una vez que esté esto ya podemos publicarlo (igual obvio, esto es open source, así que si programás podés ver todo lo que estamos haciendo en GitHub).

Laidaxai: Solo camino un poco mas lento (por Dani)

🛠️ En las últimas dos semanas, el desarrollo de Laidaxai ha tenido algunas demoras. Aunque la creatividad y el proyecto son importantes para mí, también he tenido en cuenta mi salud mental, que ha pasado por altibajos. A veces, las emociones abrumadoras pueden afectar mi rendimiento creativo, generando bloqueos y dolores de cabeza. Sin embargo, es crucial tomarse un respiro y abordar las tareas adecuadamente.

🎲 Actualmente, estamos revisando gameplay del juego pasamos del prototipo en papel y ahora estamos utilizando TableTop Simulator para prototipar la parte roguelike de Laidaxai. Mientras que por otro lado Ink sirve de soporte narrativo provisorio.

🧠 Mantenernos enfocados en nuestros objetivos y cuidarnos a lo largo del proceso de desarrollo es crucial, especialmente en áreas creativas. Además de contar con el apoyo del equipo humano de Hidden People Club🌼.

Muy pronto escalaremos el árbol 🌳

Objetivos

En las próximas 6 semanas queremos trabajar en:

2dcl

  • Login con Metamask (2dcl Grant): Como ya explicamos en el update vamos a implementar la capacidad de hacer login usando MetaMask.
  • Deployment (2dcl Grant): También vamos a implementar el deployment de escenas simplificado.
  • Videos de deployment: Apenas terminemos de implementar el deployment simplificado voy a grabar unos videos para documentar todo el proceso de creación de escenas.

Weekends in the Woods

  • Carga de escenas (Weekends in the Woods): Vamos a terminar de implementar la carga de escenas desde SVGs.
  • Movimiento de personaje: Vamos a implementar todo lo que tiene que ver con el movimiento de Abril en los laberintos.
  • Stream sobre carga de escenas (?): Estoy pensando en que todo lo que sea de carga de escenas podría implementarlo en stream, voy a ver.
  • Features de Visual Novel: Vamos a integrar cuentitos con woods y comenzar a implementar todas las features de diálogo.

Cuentitos

  • CLI watch mode: Entre otras cosas quiero tener un modo watch donde el CLI esté escuchando si se cambia el script y te pregunte si lo querés recompilar y recargar.
  • Release: Vamos a hacer el release 0.2 en github (por fin!).
  • Grabar video explicativo: Quiero grabar un video o hacer un stream sobre cómo escribir scripts en cuentitos.

Laidaxai

  • Continuar con la narrativa de Laidaxai: Aplicando cambios para que funcionen los diálogos en base al diseño del juego. Limitamos algunas apariciones de personajes. Y otras las ponemos como condición.
  • Cinemáticas Laidaxai: Arte nuevo para cinemáticas, estamos experimentando con un estilo paper cut o simulándolo, story board + concept.
  • Creación del GDD y prototipado: Se esta armando un GDD con toda la información y se está prototipando las mecánicas en el videojuego Table Top Simulator.

Comunidad

  • Comenzar a stremear: Ya tengo todo listo! Al final compré una cámara de Elgato en lugar del CamLink, porque tanto la DSLR como la GoPro no funcionaban como querría. También instalé 2 luces, para lo que tuve que modificar mi escritorio un poco, pero ya está listo y andando. Vamos a anunciar los streams en nuestro Mastodon y nuestro Discord, así que si es algo que te interesa, sumate.
  • Agregar comentarios al blog: un blog sin comentarios no es un blog. Así que voy a evaluar las distintas opciones que tenemos para implementar comentarios acá. Tengo ganas de usar Mastodon como sistema de comments, o webmentions, no se, ya veremos.
Te gusta lo que hacemos?
Sumate a nuestra lista de correo para recibir notificaciones de nuestras actualizaciones y lanzamientos.