Devlog #7: A new grant from 2dcl - Mazes in Weekends in the Woods - cuentitos in version 0.2 - Laidaxai walks slower but still standing.
It seems like yesterday when I wrote the previous post. And yes, two weeks pass by very quickly!
When I uploaded last week's post, I realized that the articles were becoming very long, and Guido suggested adding an index to each post. I thought it was a great idea, so we implemented it right away (zola ♥️) and it turned out great.
Now, readers can jump to the section they want within each project without having to scroll through things that don't interest them. Thank you, Guido, for the idea! After doing that, I started thinking about what features I would like our blog to have while keeping it simple, and I think the one I would like the most is allowing people to leave comments. So, I believe I will prioritize that in the upcoming weeks.
2dcl
The day we uploaded the previous blog post was also the day we submitted a proposal for a new grant in the DAO. Excited about what I wrote in the blog, I decided to sit down, complete the forms, and submit it on the same day.
Today, the voting ended, and we are happy to announce that we have a new grant to continue working on 2dcl 🎉🥳🎊.
I want to take this opportunity to thank everyone who voted for us: 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. ❤️
Also, to those who voted against or abstained, because it allows us to maintain a high level of accountability and a goal of improvement in this grant: Kyllian, dax, InJesterr, Jenn, CheddarQueso, mazafaka, Guest#60fd, yancy#bacc, Maryana, gamer.
Lastly, to all those who chose to remain anonymous but also voted, we respect your decision for anonymity, and thank you!
This means that starting tomorrow, we will begin working on the new features for the client.
Login with Metamask
The first feature we will work on is the ability to log in to the client using MetaMask and have the avatar generated automatically once logged in.
In the current state of the client, one has to edit a configuration file and run 2dcl
to update the avatar. By bypassing this step and including the login, which will also serve other purposes in the long run, we will make things much easier for users.
Since MetaMask does not have a client in Rust, we will have to figure out how to implement this.
The first option we will try is to have our client start a local web server that serves a web page with all the login logic, and that page connects to the client to pass all the login data. This is exactly what the dcl
command from the foundation does, but they use Node.js for the CLI.
We could also explore other native options that don't require a local server and a JavaScript context for login, but it's worth solving problems more related to the client itself than the Ethereum infrastructure initially.
I even believe that by using a web server, we could easily include other wallets (Fortmatic, WalletConnect, Coinbase, etc.).
Deployment
In addition to working on the login, this month we will also focus on the scene deployment flow. We want the process to be just a few clicks, no more than that.
Currently, to upload a scene, one has to create the scene, compile it, copy it to a 2dcl
folder within a 3D scene, and upload it to Catalyst with the Foundation's CLI.
The goal is to use the same login trick to sign the deployment transactions, as well as create the necessary functions in the CLI and the preview mode to upload scenes.
Weekends in the Woods
Wow! What a couple of weeks we had with Woods!!! We made a lot of progress, and I'm really happy with the outcome.
Juli won the Illustrator & Animator of the Year award. She has been creating visual concepts like crazy, and she almost has the spritesheet for April ready. Meanwhile, I've been working on the representation and navigation of the mazes.
About the Genre
Weekends in the Woods is evolving in our minds. What started as a Visual Novel with some random elements is gradually becoming more of an RPG with a strong narrative base. You could say we are creating a hybrid between an RPG and a Visual Novel.
Something interesting (and a little daunting) is that RPGs have very strong expectations regarding duration.
Just take a look at the average durations of Visual Novels vs. RPGs:
The average duration of Visual Novels is 17 hours, while RPGs have an average duration of 41 hours.
Rant about Averages
I would like statistical tools to start looking beyond the average. It works well when the variation in the population is minimal, but we can't gather much useful information when the variation is significant.
In the case of RPGs, the shortest game lasts 10 minutes and 32 seconds
, while the longest one lasts 1231 hours
. This range is too broad for the average to make much sense, at least for my use case.
Furthermore, different types of RPGs (MMOs, Single-Player) are clearly mixed in these statistics.
Returning from the Rant
If players' expectations for Visual Novels are an average of 17 hours, while for RPGs it's 40 hours, this worries me a bit. The difference is significant, and I feel overwhelmed thinking about creating 40 hours of content.
Many RPGs rely on heavy grinding to reach the extended gameplay time, but as a designer, I disagree with that approach.
What I can do at this moment is to analyze how I can reinterpret RPG systems to make them more casual and set clear expectations.
Our game's target length will be in the range of 15-20 hours, and we will make it clear on our Steam page when it's released, so RPG players know what to expect.
April
Juli has been conducting some tests with April, our favorite investigator.
First of all, she completed an initial pass of the running spritesheet.
A very wise decision Juli made was to use fewer frames for the animations, partly to save work but mainly as an aesthetic choice. We want to achieve highly expressive and slightly "choppy" animations, a style we really like from shows like Pocoyo and Oni.
I'm really pleased with the results, and we will see how we can apply this to other parts of the game.
Mazes
In the last update, I shared some concept art about the mazes, and now I'll talk a bit more about how they fit into the game and how we're implementing them.
The topic of movement in space has been a subject of extensive discussion between Juli and me.
Initially, I imagined the game as a vast map where you would wander through the forest and encounter creatures along the way. However, Juli envisioned the game as a dungeon crawler from the beginning. Initially, I wasn't fully convinced, but I asked her to create concepts regarding it, which you saw in the previous post.
When I saw the concepts, I started brainstorming with the idea of interconnected diorama mazes, and I absolutely loved it. We decided that was the direction the game would take. This brought the game closer to RPGs, as I mentioned before, and it opened up space for me to rethink the various RPG systems and how they could work in our game. We'll gradually explore this as things become more solidified.
Implementation
Once we decided that our game would have tile-based mazes, I started implementing it.
The first thing I did was to create the data structures to represent the tiles. The essential information that defines each tile is its enabled exits, so I created an enum with all possible combinations:
pub enum Tile {
None,
NS,
EW,
NW,
SW,
SE,
NE,
NSEW,
NEW,
SEW,
NSE,
NSW,
S,
N,
E,
W,
}
And each of our mazes (which I temporarily named Map
) has a size and a matrix of tiles:
pub struct Map {
pub size: V2,
pub tiles: Vec<Vec<Tile>>,
}
To simplify the creation of maps, I needed an operator that would allow me to connect two tiles:
pub fn link(&mut self, from: V2, to: V2)
What this method does is, given two adjacent positions on the map, it adds the necessary exits to each tile to connect them.
To simplify this operation, I added the addition operator to the tiles:
assert_eq!(Tile::N + Tile::S, Tile::NS);
In this example, if a tile has a north exit, and I add a south exit to it, it becomes a tile with exits to the north and south. Quite simple.
With this operation, the link
method became straightforward. I just need to know what exits I need and add them to the original tile.
Maze Generation
Maze generation is a problem that has already been solved. There are many algorithms available, and there's no need to reinvent the wheel. So, I turned to the divine grace of open-source code for this.
I found maze-rs, a small project written by Cian Ruane that implements various algorithms.
The repository is a bit abandoned, but it had enough for me to copy a couple of classes and create something that uses their generator and transforms it into the representation we have in our game. Yay!
In about 3-4 hours, I had a functioning maze generator that I could display and edit in a small application I wrote for this purpose:
For the curious: I built the editor using Tauri, Tailwind CSS, and JavaScript. The interesting thing about Tauri is that I can reuse the game's structs and enums for the editor, sharing all the logic between them. But I can implement the interface in HTML+JS+CSS, which takes much less time than doing it natively in Rust.
With a maze generator and a small editor to adjust the generated mazes, I was ready for the next step.
Maze Navigation
Navigation involves a small runtime that keeps track of which tile the player is on and a renderer that is responsible for rendering the specific tile.
This is a personal preference, but I like to separate the runtimes from the renderers. My experience tells me that it is much simpler to implement and automatically test the runtimes separately.
I call runtime the entire logic needed to execute the game actions, and rendering the visual representation of the game state.
Putting this barrier between rendering and runtime allows me to be able to run the game and simulate actions or tests without the need to have graphics running.
For example: testing that the player can move north if the tiles are connected does not require a graphical interface, it is something that should be able to be tested automatically and even simulated to find errors without the need for a video card. This is important for automating game testing on continuous integration servers (we use GitHub Actions for this).
This is why I separate the logic from the visual representation of the state.
The code becomes easier to read as well, and the different systems communicate with events and that's it.
For example, we have a system that listens for the event I want to change tile, man! (ChangeTileEvent
) and decides if it can or not, and if it can it sends the event Stop yelling that I already changed it!!! (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,
});
}
}
}
As you can see, this system is super easy to read and has no rendering logic making noise in the middle.
The best part is that this system can be tested very easily:
#[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);
}
In this test, I see that the player can move north once on a 5x5 map that only allows him to move once in each direction from the center.
Essentially, I do a couple of things here:
- I initialize the game
- I load the 5x5 map
- I put in a system that sends the event for him to move north:
test_sender_n
- I put in a system that counts the times he moved north:
test_receiver
- I put in the system that I want to test
tile_change_event_handler
- I run a frame
- I use
assert_eq!
to validate that he moved north - I run another frame
- I validate that he didn't move further north because the map doesn't allow it
I was able to do all of this without having to lift a renderer and consume energy that the world so much needs to show things we don't need to show: this is what is called headless, logic without rendering. If my rendering code was in the same system, this would be much more complex to test.
Mini-map
Stepping away from programming and returning to design, we have been analyzing different ways to represent the mini-map and the expanded map.
We haven't made a final decision on which one we're going to use, but we have different options and we're going to test several.
We have two main options, using an isometric mini-map:
Or topdown:
The advantage of the isometric is that it closely aligns with the representation of the scene, but we find it a bit harder to read than the top-down.
We're going to implement both and test them.
Scene Editor
One of the questions Juli asked me was if we could create a scene editor to implement the tiles. I told her it was a lot of work but I proposed to do something that I've been doing since The Insulines in 2012: use an SVG editor that supports linking images as a level editor.
I wrote an article about this in 2013, but I don't know where it ended up. So I might write a new one, with code showing how I implement it in Rust, but that's for another time!
We've already been doing some tests on this and it's been working well. I'll let you know when I finish implementing it.
This is the last thing I had to report on Weekend in the Woods, I hope you liked it!
cuentitos
Good news about cuentitos
!! Pablo has been working hard these past two weeks and managed to implement everything we set out to do!
We had a design problem that I had to solve: I couldn't decide whether I wanted the bifurcations to work as in ink or not, that is, whether I wanted that once you go to a section and this section ends, to go back or not to the previous point.
I find uses for both options so I decided to create two types of bifurcation, the common one that doesn't go back represented with ->
, and the 'boomerang' that goes and comes back represented with <->
. It's a bit more complex to have two bifurcations, but I think it's worth having that distinction at the logical level, in fact I've already started using them and I'm happy with the result.
Given that now we have everything from 2dcl
on top, I don't know if we'll have time to make an official launch with bells and whistles of version 0.2, but at least it will be available for anyone who wishes to test it.
We have to correct some bugs that we identified towards the end of last week and improve some functionalities of the CLI (a small application that we use to test the cuentitos scripts).
Once this is done we can publish it (of course, this is open source, so if you code you can see everything we're doing on GitHub).
Laidaxai: Just Walking a Bit Slower (by Dani)
🛠️ In the last two weeks, the development of Laidaxai has experienced some delays. While creativity and the project are important to me, I've also had to consider my mental health, which has gone through ups and downs. Sometimes, overwhelming emotions can impact my creative performance, causing blocks and headaches. However, it's crucial to take a breather and tackle tasks appropriately.
🎲 We are currently reviewing the game's gameplay. We've moved from the paper prototype and are now using TableTop Simulator to prototype the roguelike part of Laidaxai. On the other hand, Ink serves as a provisional narrative support.
🧠 Staying focused on our goals and taking care of ourselves throughout the development process is critical, especially in creative areas. Plus, having the human support team of Hidden People Club🌼 is very helpful.
Goals
In the next 6 weeks, we want to work on:
2dcl
- Login with Metamask (2dcl Grant): As we've already explained in the update, we're going to implement the capability to log in using MetaMask.
- Deployment (2dcl Grant): We're also going to implement simplified scene deployment.
- Deployment Videos: As soon as we finish implementing simplified deployment, I'll record videos to document the entire scene creation process.
Weekends in the Woods
- Scene Loading (Weekends in the Woods): We're going to finish implementing scene loading from SVGs.
- Character Movement: We're going to implement everything related to Abril's movement in the mazes.
- Stream about Scene Loading (?): I'm thinking that everything about scene loading could be implemented in a stream, we'll see.
- Visual Novel Features: We're going to integrate
cuentitos
with woods and start implementing all the dialogue features.
Cuentitos
- CLI Watch Mode: Among other things, I want to have a
watch
mode where the CLI is listening if the script changes and it asks you if you want to recompile and reload. - Release: We're going to do the 0.2 release on GitHub (finally!).
- Record Explanatory Video: I want to record a video or do a stream about how to write scripts in cuentitos.
Laidaxai
- Continue with Laidaxai's narrative: Applying changes to make dialogues work based on the game's design. We've limited some character appearances and set others as conditions.
- Laidaxai Cinematics: New art for cinematics, we're experimenting with a paper-cut style or simulating it, storyboard + concept.
- GDD Creation and Prototyping: We're putting together a GDD with all the information and prototyping the mechanics in the TableTop Simulator video game.
Community
- Start Streaming: Everything is ready! In the end, I bought an Elgato camera instead of the CamLink, because both the DSLR and the GoPro didn't work as I wanted. I also installed 2 lights, for which I had to modify my desk a little, but it's ready and running. We're going to announce the streams on our Mastodon and our Discord, so if this interests you, join us.
- Add Comments to the Blog: a blog without comments is not a blog. So, I'm going to evaluate the different options we have to implement comments here. I want to use Mastodon as a comment system, or webmentions, we'll see.
- Version en Español 🇦🇷🇧🇴🇨🇱🇨🇴🇨🇷🇨🇺🇩🇴🇪🇨🇸🇻🇬🇶🇬🇹🇭🇳🇲🇽🇳🇮🇵🇦🇵🇾🇵🇪🇪🇸🇺🇾🇻🇪
- English Version 🇦🇬🇧🇸🇧🇧🇧🇿🇧🇼🇨🇦🇩🇲🇫🇯🇬🇲🇬🇭🇬🇩🇬🇾🇮🇪🇯🇲🇰🇪🇱🇷🇲🇼🇲🇺🇫🇲🇳🇬🇵🇬🇰🇳🇱🇨🇻🇨🇸🇱🇸🇬🇸🇸🇹🇹🇿🇲
Comments